/*
 * GemBoss admin design system — GENERATED — do not edit; regenerate from gemboss:
 *   node apps/admin/scripts/export-ui-css.mjs --out <path>   (npm run export:ui-css)
 * gemboss commit: e3f3f92eca42c88926a75ca4405b314e92757ab5
 * sources sha256: 9b0060f874885bf4ba4ab9dc6448bc06ae386328bb9bfebcf23bfc6c2671561d
 * contents: 1 allow-listed rule from apps/admin/src/app/globals.css + packages/ui/src/gemboss-tokens.css
 *   (verbatim) + packages/ui/src/ui.css (verbatim) + packages/ui/src/gemboss-field.css (verbatim)
 *   + 181 rules from packages/ui/src/shell.css (the shared shell)
 * Polaris --p-* references are resolved to their fallbacks; declarations with no fallback are dropped
 *   (0 dropped).
 */
/* ═══ globals.css — allow-listed reset ═══ */
* {
  box-sizing: border-box;
}

/* ═══ gemboss-tokens.css ═══ */
/* The design tokens, and ONLY the tokens.
 *
 * Split out of gemboss-shell.css because that file is 203 KB of dashboard component CSS and it was
 * loaded by the ROOT layout — so /try and /login, which render no .gemboss-* component at all,
 * blocked their first paint on it. Tokens are 6 KB of the 203; /login uses var(--gemboss-primary)
 * and the rewritten /try uses the palette, so everyone needs these and almost nobody needs the rest.
 *
 * Order matters and is preserved: the second :root block below redefines --gemboss-primary and
 * friends from the first, exactly as it did inside the shell file. The Polaris --p-* overrides stay
 * in gemboss-shell.css, next to the Polaris stylesheet they override.
 */
:root {
  --gemboss-primary: #1b1c1e;        /* near-black — primary action (buttons / CTA) */
  --gemboss-primary-dark: #000000;   /* black hover */
  --gemboss-primary-soft: #f2f3f5;   /* neutral tint */
  --gemboss-accent: #5661f5;         /* iris — brand accent, điểm nhấn only (links / focus / active) */
  --gemboss-accent-strong: #3a44de;  /* iris strong (hover / text) */
  --gemboss-accent-soft: #eef0fe;    /* iris tint */
  --gemboss-text-strong: #1b1c1e;    /* neutral near-black */
  --gemboss-text: #1b1c1e;
  --gemboss-text-muted: #686b71;     /* neutral gray */
  --gemboss-surface: #ffffff;
  --gemboss-canvas: #ffffff;         /* white page background (was #f6f7f9 gray) — body + Polaris --p-color-bg */
  --gemboss-preview-canvas: #eceef1; /* neutral sunken backdrop behind a live preview/widget stage */
  --gemboss-rail-bg: #ffffff;        /* white sidebar (was #f4f5f7) — separated by border-right divider */
  --gemboss-panel-bg: #ffffff;       /* white secondary panels (was #f2f4f6) */
  --gemboss-divider: #e5e7ea;        /* neutral hairline border */

  /* Chiều cao THẬT của lời mời desktop, do DesktopInvite.tsx ghi đè lúc chạy bằng ResizeObserver
     (chip ~44px, thẻ mở ~328px) và GỠ HẲN khi lời mời không hiện. Khai ở đây bằng 0px vì hai lẽ:
     cổng `export-ui-css` đòi mọi token phải được khai (nó bắt được đúng thiếu sót này), và một
     `var()` không có chủ sở hữu là thứ không ai tra ngược được. Trang dùng nó để chừa chỗ ở cuối
     tài liệu — xem `.gemboss-platform-main { padding-bottom: … }` trong gemboss-shell.css. */
  --gemboss-invite-h: 0px;
  --gemboss-divider-soft: #edeff2;
  --gemboss-hover: #eceef2;
  --gemboss-active-bg: #f0f1f4;      /* neutral selected (Polaris-style) */
  --gemboss-active-fg: #1b1c1e;      /* near-black selected text */
  --gemboss-danger: #d72c0d;
  --gemboss-danger-strong: #b42318;
  --gemboss-danger-soft: #fff0e9;
  --gemboss-danger-border: #f5c9bc;
  --gemboss-success: #007f5f;
  --gemboss-success-strong: #0a6a4c;
  --gemboss-success-soft: #e3f1df;
  --gemboss-success-border: #b7dfcd;
  --gemboss-warning: #9a6700;
  --gemboss-warning-soft: #fff8e0;
  --gemboss-warning-border: #f5d569;
  /* Corner-radius scale — Polaris border-radius steps (2/4/6/8/12/16/20 + pill). The old
     long tail of one-off radii (5/7/9/10/11/14/18px) was consolidated onto these steps
     (round-half-up on ties: 5→6, 7→8, 9→8, 10→12, 11→12, 14→16, 18→20) — the
     check-typography guard now keeps radii on-scale. Prefer a token below; raw px must
     still land on a step. */
  --gemboss-radius-xs: 4px;
  --gemboss-radius-sm: 6px;
  --gemboss-radius-md: 8px;
  --gemboss-radius-lg: 12px;
  --gemboss-radius-xl: 16px;
  --gemboss-radius-2xl: 20px;
  --gemboss-radius-full: 999px;
  /* ── Elevation scale (design-language §G) ────────────────────────────────────
     THREE layers, one shadow each — was ~25 bespoke box-shadows (0 6/8/12/16/28px
     at a dozen opacities + mixed shadow colours) = no elevation system. Warm-neutral
     shadow (rgba 31,30,29) to match the ivory canvas. Use a token, never a hand-rolled
     drop shadow. (Directional edge shadows, coloured glows, focus rings, and insets are
     NOT elevation — they stay literal.) */
  --gemboss-shadow-resting: 0 1px 2px rgba(31, 30, 29, 0.04), 0 2px 6px rgba(31, 30, 29, 0.06); /* cards at rest on canvas */
  --gemboss-shadow-raised: 0 4px 16px rgba(31, 30, 29, 0.10);   /* dropdowns · popovers · hovercards */
  --gemboss-shadow-overlay: 0 16px 48px rgba(31, 30, 29, 0.18); /* modals · sheets · large floating panels */
  /* Interaction transition speeds — fast = hover/focus on buttons & toggles, base = the
     general default. One-off animation timings (toast/enter, 240–420ms) stay literal. */
  --gemboss-transition-fast: 0.12s;
  --gemboss-transition-base: 0.15s;
  /* ── Type scale ─────────────────────────────────────────────────────────────
     ONE font-size scale for all custom (non-Polaris-<Text>) chrome. Before this,
     gemboss-shell.css carried ~20 ad-hoc sizes (9px → 40px, incl. fractional 10.5 /
     11.5 / 12.5 / 13.5) — every component hand-nudged its own, drifting smaller
     and reading as "the font keeps shrinking". New rule: pick a step here, never a
     raw px. Floor is 12px (--fs-caption); 11px (--fs-micro) is reserved for bold
     micro-badges/count-pills ONLY — nothing below that. Body default is 14px, one
     step up from Polaris bodyMd (13px; bodySm is 12) which the app had been leaning on for prose. The kit (ui.css, kit-plain.css) reads --ui-fs-body, 13px, because it sits beside Polaris components; since 0.12.0 it reads none of body/h1-h3 here (README "Admin density"). */
  --gemboss-fs-micro: 11px;   /* bold count-pills / status chips only */
  --gemboss-fs-caption: 12px; /* metadata, timestamps, secondary labels */
  --gemboss-fs-body: 14px;    /* default body / reading text */
  --gemboss-fs-body-lg: 16px; /* primary descriptions, empty-state prose */
  --gemboss-fs-h3: 16px;      /* card titles */
  --gemboss-fs-h2: 20px;      /* zone / section headers */
  --gemboss-fs-h1: 24px;      /* page titles */
  --gemboss-fs-display: 30px; /* hero / banner display */
  /* Headings use the same clean sans as Polaris body (Shopify-admin feel) rather
   * than a serif display face — a dashboard reads calmer/more neutral that way.
   * Name kept as `--gemboss-serif` to avoid churning ~10 call sites. */
  /* -- Khung nen TOI, dung chung cho hai be mat toi --------------------------
     `/admin` khai `--adm-*`, `/intel` khai `--in-*`, va bay gia tri trung tinh cua chung trung
     nhau TUNG BYTE duoi hai bo ten khac nhau -- do 10/09/2026:

       #0a0a0c  adm-bg        in-bg        #2a2a31  adm-divider    in-border
       #0e0e11  adm-rail      in-bg2       #ededf0  adm-text       in-text
       #16161a  adm-surface   in-panel     #9a9aa4  adm-muted      in-muted
       #1e1e24  adm-surface-2 in-panel2

     Do la mot ban sao, khong phai mot bien the: hai console cung mot nen, va lam nen sang them
     mot bac thi phai nho sua o hai cho mang ten khac nhau. Bay gia tri nay song o day; `--adm-*`
     va `--in-*` tro thanh BI DANH tro vao, nen 595 cho dang goi chung khong phai doi mot chu.

     KHONG gop phan con lai: `--in-panel3/-border2/-faint` la bac rieng intel that su can, va bon
     mau ngu nghia cua no (xanh - ho phach - san ho - tim) la ngon ngu rieng cua be mat do. Gop
     nen, giu diem nhan. */
  --gemboss-dark-bg: #0a0a0c;
  --gemboss-dark-rail: #0e0e11;
  --gemboss-dark-surface: #16161a;
  --gemboss-dark-surface-2: #1e1e24;
  --gemboss-dark-divider: #2a2a31;
  --gemboss-dark-text: #ededf0;
  --gemboss-dark-muted: #9a9aa4;
  --gemboss-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  /* The BODY font, for surfaces without Polaris. The admin never reads it: its body text comes from
     Polaris's `html, body, button` rule (--p-font-family-sans, @shopify/polaris 13.9.5). The fallback
     below is that stack character for character, and export-ui-css.mjs ships the fallback, so a
     surface on gemboss-ui.css picks the admin's face on the same machine (Inter is not loaded as a web
     font anywhere; it renders where installed). Painted by packages/ui/src/app-shell.css; re-theme by
     redefining the token. Added 28/09/2026: before it, those surfaces rendered in default serif. */
  --gemboss-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'San Francisco', 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
  /* COMPONENT colours that used to be literals, so a surface with a dark theme re-themes them by
     redefining the token instead of overriding the rule (29/09/2026). Each value is the literal it
     replaced, so the admin paints exactly what it painted before. Measured reason: gempress's dark
     theme had to override `.gemboss-account-chip` (initials #303633 on #16161a all but vanished)
     and keep its own copy of the field and the nav count rather than use the package's.
     - field: the fallbacks of gemboss-field.css. The admin still reads Polaris's --p-* first;
       export-ui-css.mjs resolves --p-* away, so on every other surface these tokens are the look.
     - chip: `.gemboss-account-chip` in apps/admin/src/app/gemboss-shell.css.
     - on-primary: text on a --gemboss-primary fill (the active `.gnav-count` pill). */
  --gemboss-field-bg: #fdfdfd;
  --gemboss-field-bg-hover: #fafafa;
  --gemboss-field-ink: #303030;
  --gemboss-field-placeholder: #616161;
  --gemboss-field-line: #8a8a8a;
  --gemboss-field-line-hover: #616161;
  --gemboss-field-line-active: #1a1a1a;
  --gemboss-chip-ink: #303633;
  --gemboss-chip-line: #cfd6ce;
  --gemboss-chip-line-hover: #aab4ad;
  --gemboss-chip-bg-hover: #f6f7f5;
  --gemboss-on-primary: #ffffff;
  /* The shell's own surfaces (29/09/2026), for the same reason as the slots above: a dark surface
     (gempress) had to override `.gemboss-platform-topbar`, `.icon-nav` and `.gnav-item:hover` by rule.
     Defaults are exactly what the shell painted. */
  --gemboss-topbar-bg: rgba(255, 255, 255, 0.98);
  --gemboss-nav-bg: var(--gemboss-surface);
  --gemboss-nav-hover: rgba(20, 19, 18, 0.045);
}

:root {
  --gemboss-primary: #1b1c1e;
  --gemboss-primary-dark: #000000;
  --gemboss-primary-soft: #f2f3f5;
  --gemboss-accent: #5661f5;
  --gemboss-accent-strong: #3a44de;
  --gemboss-accent-soft: #eef0fe;
  --gemboss-active-bg: #f0f1f4;      /* neutral selected (Polaris-style) */
  --gemboss-active-fg: #1b1c1e;      /* near-black selected text */
  --gemboss-app-bg: #ffffff; /* clean white main content (was #f6f7f9 gray) — cards keep separation via border/resting-shadow */
}

/* ═══ ui.css ═══ */
/* ══
   Tool/App UI kit — the shared primitive layer for the internal operator surfaces
   (admin console, seller app chrome, theme-editor). ONE semantic token spine
   (`--ui-*`) that every primitive reads, plus the primitive CSS itself.

   Why this exists: buttons/cards/badges were forked into 4–8 hand-rolled class
   families per surface, each re-deriving color/spacing/centering — so the same
   primitive drifted (e.g. a link-button whose text wasn't vertically centered).
   See docs/frontend-shared-system-plan.md.

   The `--ui-*` layer is SEMANTIC and resolved per surface: components only read
   `--ui-*`; each surface maps those to its own tokens (admin → `--adm-*` below),
   so one <Button> looks native everywhere without hard-coding a palette.
   ══ */

:root {
  --ui-surface: #ffffff;
  /* --ui-* is a SEMANTIC layer resolved PER SURFACE — it references each surface's own base tokens
     rather than guessing from the OS. The default (`:root`) maps onto the seller app's warm-ivory
     `--gemboss-*` set; `.gemboss-admin-shell` below remaps onto the dark `--adm-*` set. Literal fallbacks
     keep the kit usable if a surface's base stylesheet isn't loaded. This is the "unify under --ui-*"
     backbone — one token spine, no duplicated palette. */
  --ui-surface: var(--gemboss-surface, #ffffff);
  --ui-surface-2: var(--gemboss-hover, #eceef2);
  --ui-text: var(--gemboss-text, #1b1c1e);
  --ui-text-muted: var(--gemboss-text-muted, #686b71);
  --ui-border: var(--gemboss-divider, #e5e7ea);
  --ui-accent: var(--gemboss-primary, #1b1c1e);
  --ui-accent-hover: var(--gemboss-primary-dark, #000000);
  --ui-accent-fg: #ffffff;
  --ui-accent-soft: var(--gemboss-active-bg, #f0f1f4);   /* tinted bg for a selected/active state */
  --ui-accent-strong: var(--gemboss-active-fg, #1b1c1e); /* readable accent text ON --ui-accent-soft */
  --ui-text-strong: var(--gemboss-text-strong, #1b1c1e);
  /* Chữ theo tông (ui-metric, …). Nền tối của admin ánh xạ lại bên dưới để còn đọc được. */
  --ui-success-fg: var(--gemboss-success);
  --ui-warning-fg: var(--gemboss-primary-dark);
  --ui-danger-fg: var(--gemboss-danger-strong);
  /* ONE focus ring for every control (02/10/2026): the admin's, iris — gemboss-shell.css points Polaris'
     --p-color-border-focus at --gemboss-accent. Before, kit buttons, tabs and checks drew black, fields
     and selects #005bd3 (Polaris' default, which the export shipped), and the admin drew iris. */
  --ui-focus: var(--gemboss-accent, #5661f5);
  --ui-radius-sm: 6px;
  --ui-radius-md: 8px;
  --ui-radius-lg: 12px;
  --ui-space-1: 4px;
  --ui-space-2: 8px;
  --ui-space-3: 12px;
  --ui-space-4: 16px;
  /* Polaris' two heights under a mouse (0.12.0): a FIELD (gemboss-field, ui-select) is TextField/Select, 32px; a BUTTON
     (ui-btn, ui-seg, ui-tabs) is Button, 28px. A toolbar row is 32 + 28, as in the admin. Under a thumb every control is
     44px (the coarse block below; kit-plain.css adds phone WIDTH for non-admin surfaces). */
  --ui-control-h: 32px;
  --ui-btn-h: 28px;
  /* Polaris Card (13.9.5): no grey frame of its own, --p-shadow-100 under it. The edge is ONE 1px line in the surface's
     --ui-border (Polaris paints a bevel inset on a ::before; a border cannot be covered by a full-bleed child, and it
     follows a surface's dark --ui-border, which a light bevel would not). */
  --ui-card-shadow: 0 1px 0 rgba(26,26,26,.07);
  --ui-fs-body: 13px;
  --ui-fs-caption: 12px;
  /* Content column width for the seller app shell — ONE source of truth for every wide surface
     right of the 240px sidebar (bare Page, .gemboss-w-dashboard, .gemboss-w-gallery, and the
     navigation skeleton), so they cannot drift apart again. The /admin console is a SEPARATE
     surface and still sets its own 1200px in admin/admin-shell.css; it does not read this token.
     900px. Polaris' own cap is 998px — 41.375rem primary column + 20rem secondary + 1rem gutter,
     read out of the installed @shopify/polaris stylesheet — and this sits deliberately BELOW it.

     The reason is a measurement, not a preference about Shopify. The previous value was 1080/1040,
     and moving to Polaris' 998 was the obvious "correct" answer, so that shipped first (#2250). Then
     it turned out to be invisible: 1040 → 998 is 42px total, which is 21px of extra gutter per side.
     Screenshotted side by side at 1440 on the same page seconds apart, the two rows were
     indistinguishable, and Chris — who asked for this — could not see any change at all.

     A cap only does its job if the breathing room it buys is legible. Measured gutters on
     .Polaris-Page at 1440 beside the 240px sidebar:

       1040px → 75 / 86      (before)
        998px → 96 / 107     Polaris' number; +21px a side, not perceivable
        960px → 115 / 126
        900px → 145 / 156    +70px a side, roughly double the old gutter

     900 is the first value where the column visibly stops leaning on the rail. The one cost, and it
     was measured rather than guessed: card descriptions gain a line (Cart drawer 2 → 3). The 3-up
     grid holds, and no width from 320 to 1920 overflows.

     Still fixed px, no percentage, for the reason Polaris uses px: a % cap grows the reading measure
     with the monitor (80% of 2560 is 2048px of line length). Responsiveness belongs to the padding
     and to the secondary column dropping, not to the cap.

     .gemboss-w-form stays 662px — that IS `.Polaris-Page--narrowWidth`, the primary column alone,
     and a form is the one surface where Polaris' measure is about the input, not the page. */
  --ui-content-max: 900px;
}

/* Admin console bridge: the black chrome defines `--adm-*`; remap `--ui-*` onto them so every
   <Button>/<Select>/<Tag>/<Modal> inside .gemboss-admin-shell adopts the dark palette automatically. */
.gemboss-admin-shell {
  --ui-surface: var(--adm-surface);
  --ui-surface-2: var(--adm-surface-2);
  --ui-text: var(--adm-text);
  --ui-text-muted: var(--adm-muted);
  --ui-border: var(--adm-divider);
  --ui-accent: var(--adm-accent);
  --ui-accent-hover: var(--gemboss-primary-dark, #000000);
  --ui-accent-fg: #ffffff;
  --ui-accent-soft: var(--adm-accent-soft);
  --ui-accent-strong: var(--adm-accent-fg);
  --ui-text-strong: var(--adm-text-strong);
  /* Tông trên nền tối #16161a: pha 70% với trắng ⇒ tương phản 6,2 / 9,5 / 4,8 (đo 01/10/2026, review #3609). */
  --ui-success-fg: color-mix(in srgb, var(--gemboss-success) 70%, #ffffff);
  --ui-warning-fg: color-mix(in srgb, #c99a2e 70%, #ffffff);
  --ui-danger-fg: color-mix(in srgb, var(--gemboss-danger-strong) 70%, #ffffff);
  --ui-focus: var(--adm-accent);
  --ui-card-shadow: none;
}

/* ── Button ──
   inline-flex + center so a <button> and an <a> render identically (anchors ignore
   `height` as inline elements — the root cause of the misaligned link-buttons). */
.ui-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--ui-space-2);
  height: var(--ui-btn-h); padding: 0 var(--ui-space-3); box-sizing: border-box;
  line-height: 1; white-space: nowrap; vertical-align: middle;
  border-radius: var(--ui-radius-md); font-family: inherit; font-size: var(--ui-fs-caption); font-weight: 550;
  cursor: pointer; text-decoration: none; user-select: none;
  border: 1px solid var(--ui-border); background: transparent; color: var(--ui-text);
  transition: background .12s ease, border-color .12s ease;
}
.ui-btn:hover { background: var(--ui-surface-2); text-decoration: none; }
.ui-btn:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
.ui-btn[disabled], .ui-btn[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }
.ui-btn--primary { background: var(--ui-accent); border-color: var(--ui-accent); color: var(--ui-accent-fg); }
.ui-btn--primary:hover { background: var(--ui-accent-hover); border-color: var(--ui-accent-hover); }
.ui-btn--ghost { border-color: transparent; background: transparent; color: var(--ui-text-muted); }
.ui-btn--ghost:hover { background: var(--ui-surface-2); color: var(--ui-text); }
/* `ui-btn--sm`: the default size under a mouse since 0.12.0; it only marks a button the admin keeps compact under a finger. */

/* ── Card ── */
.ui-card {
  background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-card-shadow); padding: var(--ui-space-4); color: var(--ui-text);
}
.ui-card__title { font-size: 14px; line-height: 20px; font-weight: 650; margin: 0 0 var(--ui-space-2); }
/* Đầu thẻ có nút: `<div class="ui-card__head"><h2 class="ui-card__title">…</h2><button class="ui-btn">…</button></div>`.
   Tiêu đề trái, hành động phải, xuống dòng khi hẹp (01/10/2026: partner-radar rel-head / agr). */
.ui-card__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--ui-space-2); margin-bottom: var(--ui-space-2); }
.ui-card__head > .ui-card__title { margin: 0; }
.ui-card__head > :last-child:not(.ui-card__title) { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); }

/* ── Select ──
   Native <select> reskinned; custom caret as an inline SVG data-URI (caret color
   uses the muted token via %-encoding fallback).
   Its BOX is the text field's (29/09/2026, Chris): same --gemboss-field-* tokens for ground, line and
   ink, same hover, same focus ring as `.gemboss-field`. Before, a grey --ui-surface-2 select stood next
   to a bordered white field in the same form (gempress editor, admin forms), and `:focus` removed the
   outline and only recoloured the border, so a keyboard user saw almost nothing. Height is
   --ui-control-h, the field's (Polaris Select, 32): a field, not a button (28, --ui-btn-h). */
.ui-select {
  height: var(--ui-control-h); padding: 0 32px 0 12px; max-width: 100%;
  background: var(--gemboss-field-bg) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 20 20'><path fill='%239a9aa4' d='M5 7l5 5 5-5z'/></svg>") no-repeat right 10px center;
  border: 1px solid var(--gemboss-field-line); border-radius: var(--ui-radius-md);
  color: var(--gemboss-field-ink); font-family: inherit; font-size: var(--ui-fs-body); font-weight: 450;
  appearance: none; -webkit-appearance: none; cursor: pointer;
}
.ui-select:hover:not(:disabled) {
  border-color: var(--gemboss-field-line-hover);
  background-color: var(--gemboss-field-bg-hover);
}
.ui-select:focus { outline: none; }
.ui-select:focus-visible:not(:disabled) {
  outline: 2px solid var(--ui-focus); outline-offset: 1px;
  border-color: var(--gemboss-field-line-active);
}
.ui-select:disabled { opacity: .5; cursor: not-allowed; }
.ui-select--slim { height: 30px; font-size: var(--ui-fs-caption); }

/* ── Tag / Chip ──
   Small inline labels. `solid` = filled muted pill (was gemboss-vault-tag); `chip` =
   bordered mono token (was gemboss-vault-chip). `accent` tone for the highlighted state. */
.ui-tag {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: var(--ui-fs-caption); line-height: 20px; font-weight: 450;
  padding: 0 6px; border-radius: var(--ui-radius-md); background: var(--ui-surface-2); color: var(--ui-text-muted);
}
.ui-tag--accent { background: var(--adm-accent-soft, rgba(20,22,26,.05)); color: var(--adm-accent-fg, var(--ui-accent)); }
.ui-tag--chip {
  text-transform: none; letter-spacing: 0; font-size: 11px; font-weight: 500;
  padding: 2px 8px; background: transparent; border: 1px solid var(--ui-border);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.ui-tag--chip.ui-tag--accent { color: var(--adm-accent-fg, var(--ui-accent)); border-color: var(--ui-accent); background: transparent; }

/* Danh sách link dạng chip: `<div class="ui-chips"><a class="ui-tag ui-tag--chip" href>…</a>…</div>` (01/10/2026,
   partner-radar /competitors: "đã tích hợp với GemPages"). `.ui-tag--chip` vốn là nhãn TĨNH cao ~19px, dưới mức 24px
   của link trong danh sách (WCAG 2.5.8, §9.1) — visual-gate đo đúng chỗ đó đỏ ở 320 / 390 / 768. Chip là link thì cao
   ≥ 28px, chữ thường (không mono), có hover + focus-visible; dưới ngón tay là điều khiển, lên 44px (khối coarse dưới).
   Khe 8px: hai target nhỏ hơn 44px phải cách nhau ≥ 8px. */
.ui-chips { display: flex; flex-wrap: wrap; gap: var(--ui-space-2, 8px); margin: 0; padding: 0; list-style: none; }
.ui-chips > a.ui-tag--chip {
  min-height: 28px; padding: 4px 12px; border-radius: var(--ui-radius-md, 8px);
  font-family: inherit; font-size: var(--gemboss-fs-caption, 12px); color: var(--ui-text); text-decoration: none;
}
.ui-chips > a.ui-tag--chip:hover { background: var(--ui-surface-2); border-color: var(--ui-text-muted); }
.ui-chips > a.ui-tag--chip:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
/* Danh sách LỌC dài (chuyên môn, hạng…) dùng ui-chips thay ui-seg: ui-seg không xuống dòng nên tràn ngang ở 320px.
   Ô đang chọn = `aria-current="page"` (lọc là một URL), cùng tông với ui-seg đang chọn. */
.ui-chips > a.ui-tag--chip[aria-current="page"] { background: var(--ui-accent-soft); border-color: var(--ui-accent); color: var(--ui-accent-strong); font-weight: 600; }

/* ── Tông màu cho tag: `ui-tag--success | --warning | --danger` (token ngữ nghĩa có sẵn). Trạng thái, nguồn,
   kết quả kiểm — thay cho màu viết cứng ở từng surface. */
.ui-tag--success { background: var(--gemboss-success-soft); color: var(--gemboss-success-strong, var(--gemboss-success)); }
.ui-tag--warning { background: var(--gemboss-warning-soft); color: var(--gemboss-warning); }
.ui-tag--danger { background: var(--gemboss-danger-soft); color: var(--gemboss-danger-strong, var(--gemboss-danger)); }

/* ── Chữ phụ: `<p class="ui-muted">`, `<span class="ui-muted">` — mô tả, chú thích, dòng meta. Cỡ caption, màu muted.
   Trước đây mỗi surface tự viết `.muted` (partner-radar: 180 chỗ). */
.ui-muted { color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }

/* ── Tiêu đề trang: `<div class="ui-page-head"><h1>…</h1><p>…</p></div>`. Đưa lên NGUYÊN VĂN từ admin
   `.gemboss-admin-page-head` (01/10/2026, Chris: một chủ sở hữu; admin dùng bản này). Không đặt margin cho h1
   và không dùng flex: margin 0 đến từ reset (Polaris ở admin, app-shell.css trong shell của bề mặt từ 0.12.0). */
.ui-page-head { margin-bottom: 20px; }
.ui-page-head h1 { color: var(--ui-text-strong); font-size: 20px; line-height: 24px; font-weight: 700; letter-spacing: -0.01em; } /* own line-height: the 20px body line overlapped a wrapped title (0.5.1) */
.ui-page-head p { color: var(--ui-text-muted); font-size: var(--ui-fs-body); margin-top: 4px; }

/* ── Thanh công cụ: `<div class="ui-toolbar">` — ô tìm giãn, điều khiển bám phải, một hàng gọn, xuống dòng khi hẹp.
   Đưa lên từ admin `.gemboss-admin-toolbar`. Chỉ BỐ CỤC: ô tìm là `gemboss-field gemboss-field--group`. */
.ui-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-bottom: 20px; }

/* ── Ô số liệu: `<div class="ui-metrics"><div class="ui-metric"><span class="ui-metric__val">12</span>
   <span class="ui-metric__lbl">Nhãn</span></div></div>`; tông `ui-metric--success | --warning | --danger`.
   Đưa lên từ admin `.gemboss-ec-metric` (GemChat). Khác `ui-stat` (viên đếm nhỏ, có thể bấm làm bộ lọc). */
.ui-metrics { display: flex; flex-wrap: wrap; gap: 8px; margin: 2px 0; }
.ui-metric { display: flex; flex-direction: column; gap: 1px; min-width: 78px; padding: 8px 12px; background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: 12px; }
/* Cả ô là link (`<a class="ui-metric" href>`): vùng chạm là cả ô (~52px), không phải con số 20px bên trong
   (visual-gate đỏ ở partner-radar /ops 01/10/2026 khi link nằm trong __val). */
a.ui-metric { text-decoration: none; color: inherit; }
a.ui-metric:hover { border-color: var(--ui-text-muted); }
a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
.ui-metric__val { font-size: 17px; font-weight: 700; color: var(--ui-text); font-variant-numeric: tabular-nums; line-height: 1.15; }
.ui-metric__lbl { font-size: var(--ui-fs-caption); font-weight: 500; color: var(--ui-text-muted); }
.ui-metric--success { border-color: color-mix(in srgb, var(--gemboss-success) 40%, transparent); }
.ui-metric--success .ui-metric__val { color: var(--ui-success-fg); }
.ui-metric--warning { border-color: color-mix(in srgb, #c99a2e 40%, transparent); }
.ui-metric--warning .ui-metric__val { color: var(--ui-warning-fg); }
.ui-metric--danger { border-color: color-mix(in srgb, var(--gemboss-danger-strong) 40%, transparent); }
.ui-metric--danger .ui-metric__val { color: var(--ui-danger-fg); }

/* ── Cặp nhãn – giá trị: `<dl class="ui-dl"><div><dt>Quốc gia</dt><dd>IL</dd></div>…</dl>` — dữ kiện của một hồ sơ.
   Lưới tự giãn: mỗi ô ≥ 160px, xuống dòng khi hẹp. */
.ui-dl { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px 16px; margin: 0; }
.ui-dl > div { min-width: 0; }
.ui-dl dt { font-size: var(--ui-fs-caption); color: var(--ui-text-muted); }
.ui-dl dd { margin: 4px 0 0; font-weight: 600; color: var(--ui-text); overflow-wrap: anywhere; }

/* ── Thông báo: `<div class="ui-notice ui-notice--warning">…</div>`. Lỗi tải, cảnh báo cấu hình, việc cần người
   làm. Mỗi tông mang màu chữ riêng (nền tông luôn sáng, kể cả trong admin tối). Không có tông = ghi chú trung tính.
   role="status"/"alert" CHỈ khi thông báo xuất hiện sau khi trang đã tải (vùng live); có sẵn lúc tải thì không role. */
.ui-notice { padding: 12px 16px; border-radius: var(--ui-radius-md); border: 1px solid var(--ui-border); background: var(--ui-surface-2); color: var(--ui-text); font-size: var(--ui-fs-body); overflow-wrap: anywhere; } /* a URL or key in a notice wraps instead of being cut by its card (gemwatcher Pulse, 0.6.2) */
.ui-notice--warning { background: var(--gemboss-warning-soft); border-color: var(--gemboss-warning-border); color: var(--gemboss-warning); }
.ui-notice--danger { background: var(--gemboss-danger-soft); border-color: var(--gemboss-danger-border); color: var(--gemboss-danger-strong); }
.ui-notice--success { background: var(--gemboss-success-soft); border-color: var(--gemboss-success-border); color: var(--gemboss-success-strong); }

/* ── Phân trang: `<nav class="ui-pager" aria-label="Trang"><a href rel="prev"><span aria-hidden="true">‹</span> Trước</a><span aria-current="page">2 / 9</span>
   <a href rel="next">Sau ›</a></nav>`. Link ≥ 32px (≥ 24px WCAG 2.5.8), 44px dưới ngón tay (khối coarse). */
.ui-pager { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 16px 0; }
.ui-pager > a { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); color: var(--ui-text); text-decoration: none; font-size: var(--ui-fs-body); }
.ui-pager > a:hover { background: var(--ui-surface-2); }
.ui-pager > a:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
.ui-pager > [aria-current="page"] { color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }

/* ── Badge ──
   Polaris' Badge (02/10/2026; was an uppercase mono pill): 12px/450 (its text span; 550 was the box), padding 2/8, radius 8, the secondary
   fill. The admin's /admin console draws it too (components/ui/Badge.tsx). Tones (`--success|--warning|--critical|
   --info`, `--dot`) are in kit-plain.css, for the non-React surfaces; the seller app uses Polaris <Badge tone>. */
.ui-badge {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: var(--ui-fs-caption); line-height: 16px; font-weight: 450; white-space: nowrap;
  padding: 2px 8px; border-radius: var(--ui-radius-md); background: color-mix(in srgb, var(--ui-text) 8%, transparent); color: var(--ui-text-muted);
} /* a tint of the ink, not --ui-surface-2: that is the table row hover, and a badge vanished on hover (0.5.1) */

/* ── Modal ──
   Rendered INLINE (not portaled): a portal escapes the .gemboss-admin-shell token
   scope and would render light. Kept in-tree so --ui-* resolves to the surface.
   0.7.3: Polaris Modal's density (#1099, README "Popup density"). Buttons are 28 everywhere since 0.12.0 (--ui-btn-h). */
.ui-overlay {
  position: fixed; inset: 0; z-index: 500;
  background: rgba(0, 0, 0, 0.5);
  display: grid; place-items: center; padding: 20px;
}
.ui-modal {
  width: min(460px, 100%);
  background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: 16px;
  box-shadow: var(--gemboss-shadow-overlay, 0 20px 60px rgba(0, 0, 0, 0.4)); overflow: hidden;
}
/* Head = title + the one close button every modal used to draw for itself. */
.ui-modal__head {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 16px; border-bottom: 1px solid var(--ui-border);
  font-size: 14px; line-height: 20px; font-weight: 650; color: var(--ui-text);
}
.ui-modal__title { min-width: 0; flex: 1 1 auto; }
.ui-modal__close {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; padding: 0;
  border: 0; border-radius: var(--ui-radius-md, 8px); background: transparent;
  color: var(--ui-text-muted); cursor: pointer;
}
.ui-modal__close svg { width: 20px; height: 20px; }
.ui-modal__close:hover { background: var(--ui-surface-2); color: var(--ui-text); }
.ui-modal__close:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
/* Here, not in kit-plain.css: the admin loads only this file (0.7.3). */
.ui-modal__body { padding: 16px; }
.ui-modal__foot {
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
  padding: 12px 16px; border-top: 1px solid var(--ui-border);
}
/* 44px under a thumb or at phone width; -8px keeps the head 53. */
@media (pointer: coarse), (max-width: 47.9975em) {
  .ui-modal__close { width: 44px; height: 44px; margin: -8px -8px -8px 0; }
  :is(.ui-modal__head, .ui-modal__foot) .ui-btn { min-height: 44px; }
}

/* `size="lg"`: only the HOOK here (this sheet is paid on every screen); its geometry is in
   SettingsDialog.module.css, the one consumer's chunk. */
.ui-modal--lg { position: relative; }

/* The kit's multi-line field is the ONE field family (01/10/2026, Chris: "form control đồng bộ"): same
   border token, hover and focus ring as `gemboss-field` / `.ui-select` (it drew a third look on GemFactory). */
.ui-textarea {
  width: 100%; min-height: 96px; resize: vertical; box-sizing: border-box;
  background: var(--gemboss-field-bg);
  border: 1px solid var(--gemboss-field-line); border-radius: var(--ui-radius-md);
  padding: 12px; color: var(--gemboss-field-ink); font-size: var(--ui-fs-body); line-height: 1.5; font-family: inherit;
}
.ui-textarea::placeholder { color: var(--gemboss-field-placeholder); }
.ui-textarea:hover:not(:disabled) {
  border-color: var(--gemboss-field-line-hover);
  background-color: var(--gemboss-field-bg-hover);
}
.ui-textarea:focus { outline: none; }
.ui-textarea:focus-visible:not(:disabled) {
  outline: 2px solid var(--ui-focus); outline-offset: 1px;
  border-color: var(--gemboss-field-line-active);
}
.ui-textarea:disabled { opacity: .5; cursor: not-allowed; }
/* On a PHONE-WIDTH screen every field of the family reads 16px (iOS zooms the page on tap below that)
   and a select is a 44px target — by WIDTH, as `gemboss-field` does, because a desktop browser at 320px
   still reports pointer: fine. Measured on GemFactory at 390px before this: select 13px, textarea 14px. */
@media (max-width: 47.9975em) {
  .ui-select.ui-select, .ui-textarea { font-size: 16px; }
  /* Height only for the full select: `--slim` is a density decision (table rows), as the coarse block keeps it. */
  .ui-select:not(.ui-select--slim) { min-height: 44px; }
}

/* ── Segmented control ──
   Grouped toggle: icon squares (.ui-seg button) or text tabs (.is-text). The
   selected segment uses the accent-soft state. */
.ui-seg { display: inline-flex; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); overflow: hidden; }
.ui-seg :is(button, a) {
  display: inline-grid; place-items: center; width: calc(var(--ui-btn-h) - 2px); height: calc(var(--ui-btn-h) - 2px);
  border: 0; background: transparent; color: var(--ui-text-muted); cursor: pointer; font-family: inherit;
}
.ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-caption); font-weight: 550; }
.ui-seg :is(button, a) + :is(button, a) { border-left: 1px solid var(--ui-border); }
.ui-seg :is(button, a):hover { background: var(--ui-surface-2); color: var(--ui-text); }
.ui-seg :is(button, a):focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
/* Lọc / chế độ xem bằng LINK (trang server-render, chọn = đổi URL): `<a aria-current="page">` là ô đang chọn. */
.ui-seg a { display: inline-flex; align-items: center; justify-content: center; text-decoration: none; color: var(--ui-text-muted); }
.ui-seg a[aria-current="page"],
.ui-seg button.active { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
.ui-seg svg { width: 16px; height: 16px; fill: currentColor; }

/* ── Stat chip ──
   Count-led pill; clickable ones double as filters (active = accent-soft). */
.ui-stats { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); margin-bottom: 24px; }
.ui-stat {
  display: inline-flex; align-items: center; gap: var(--ui-space-2);
  padding: 6px 12px; border-radius: 999px; font-family: inherit;
  background: var(--ui-surface); border: 1px solid var(--ui-border);
  font-size: var(--ui-fs-caption); color: var(--ui-text-muted);
}
.ui-stat b { color: var(--ui-text-strong); font-weight: 650; }
.ui-stat.is-clickable { cursor: pointer; transition: border-color 0.14s ease, background-color 0.14s ease; }
.ui-stat.is-clickable:hover { border-color: var(--ui-text-muted); background: var(--ui-surface-2); }
.ui-stat.is-clickable.active { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
.ui-stat.is-clickable.active b { color: var(--ui-accent-strong); }

/* ── Toast / notification bar ──
   App-wide "done / failed" pill for async actions (delete/duplicate/publish…).
   Polaris-style: dark pill, bottom-center, error variant in critical red. Lives
   in <ToastProvider> (components/ui/Toast.tsx), portaled to <body>. */
.gemboss-toast-viewport {
  position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
  z-index: 2147483000; /* above every overlay/modal/popup */
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  width: max-content; max-width: min(560px, calc(100vw - 32px)); pointer-events: none;
}
.gemboss-toast {
  pointer-events: auto;
  display: flex; align-items: center; gap: 12px;
  min-width: 220px; max-width: 100%; padding: 12px 12px 12px 16px;
  background: var(--ui-text-strong, #1a1917); color: var(--gemboss-on-primary, #fff); border-radius: 12px;
  font-size: var(--ui-fs-body, 13px); line-height: 1.4; font-weight: 500;
  box-shadow: var(--gemboss-shadow-overlay, 0 8px 28px rgba(0, 0, 0, 0.24));
  animation: gemboss-toast-in 240ms cubic-bezier(0.16, 1, 0.3, 1);
}
.gemboss-toast.is-error { background: var(--gemboss-danger, #d72c0d); }
.gemboss-toast-text { flex: 1 1 auto; min-width: 0; }
.gemboss-toast-action {
  flex: 0 0 auto; border: 0; background: transparent; color: var(--gemboss-on-primary, #fff); cursor: pointer;
  padding: 4px 8px; border-radius: 8px; font: inherit; font-weight: 650;
  text-decoration: underline; text-underline-offset: 2px;
  transition: background 0.14s ease;
}
.gemboss-toast-action:hover { background: rgba(255, 255, 255, 0.16); }
.gemboss-toast-dismiss {
  flex: 0 0 auto; display: inline-grid; place-items: center;
  width: 24px; height: 24px; border: 0; border-radius: 8px; cursor: pointer;
  background: rgba(255, 255, 255, 0.12); color: var(--gemboss-on-primary, #fff);
  transition: background 0.14s ease;
}
.gemboss-toast-dismiss:hover { background: rgba(255, 255, 255, 0.26); }
.gemboss-toast-action:focus-visible,
.gemboss-toast-dismiss:focus-visible { outline: 2px solid var(--gemboss-on-primary, #fff); outline-offset: 2px; }
@keyframes gemboss-toast-in {
  from { opacity: 0; transform: translateY(12px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) { .gemboss-toast { animation: none; } }

/* ── The kit's controls under a thumb ──
 * Measured by hit test on /admin/{theme-kernel,media,style-packs} at 390 and 1366 with a coarse
 * pointer: segmented buttons 36x36, selects 30-36 tall, clickable stat tiles 29, the asset row's
 * note button 30x30. §9.1 puts every one of them in the CONTROL tier (44px).
 *
 * HERE rather than in each surface's stylesheet, because this is the shared primitive layer — the
 * same reason the kit exists at all. The admin console and the seller app both read it.
 *
 * The block below is `pointer: coarse` only: a mouse at desktop width keeps the density these surfaces were
 * designed at. Growing a box for every pointer is what #2147 did to a working width.
 */
/* Phone WIDTH (< 768) with a mouse too (a narrow window): buttons and segments are 44 (0.12.0; at desktop width 28). */
@media (max-width: 47.9975em) { .ui-btn:not(.ui-btn--sm), .ui-seg :is(button, a) { min-height: 44px; } .ui-seg :is(button, a):not(.is-text) { min-width: 44px; } }
@media (pointer: coarse) {
  /* NOT `.ui-textarea`: it already declares `min-height: 96px` earlier in this file, so adding it to
     a 44px group LOWERS it — same specificity, later line. The note dialog's box would have gone from
     96 to ~58 on every touch device, inside a block whose whole point is bigger targets.
     NOT `.ui-btn--sm` either: `min-height` always beats `height`, so a 44px floor voids the compact
     variant — including four of them on the seller's own /app/settings/store. A density variant is a
     decision, not an oversight; it gets its own measurement pass or none. */
  .ui-btn:not(.ui-btn--sm),
  .ui-stat.is-clickable,
  .ui-chips > a.ui-tag--chip,
  .ui-pager > a,
  .ui-seg :is(button, a) { min-height: 44px; }
  /* A clickable stat whose label is one short token ("7d" on /admin/gem-intelligence) measured 40x44
     in the 13/09/2026 sweep: the height floor held, the width did not. */
  .ui-stat.is-clickable { min-width: 44px; justify-content: center; }
  /* Same for a one-word link chip: partner-radar /posts "All" measured 41x44 at 320 / 390 / 768 (visual-gate 03/10/2026,
     after its labels went English and got shorter). */
  .ui-chips > a.ui-tag--chip { min-width: 44px; justify-content: center; }
  /* `--slim` is a second single class and lands later in this file, so it would win on order over a
     bare `.ui-select`. Doubled here to decide it on specificity instead. */
  .ui-select.ui-select,
  .ui-select--slim.ui-select--slim { min-height: 44px; }
  /* The icon-only segment is 36 WIDE as well — a target is an area. */
  .ui-seg :is(button, a) { min-width: 44px; }
}

/* ── Skeleton ──
   Vạch xám giữ chỗ cho nội dung chưa về. Ánh sáng chạy qua là gợi ý "đang chờ", không phải trang
   trí — nên nó tắt hẳn với `prefers-reduced-motion`, giữ nguyên vạch tĩnh. */
.ui-skeleton { display: flex; flex-direction: column; gap: var(--ui-space-2); }
.ui-skeleton__line {
  display: block; height: 12px; border-radius: 6px;
  background: linear-gradient(90deg, var(--ui-surface-2) 25%, var(--ui-border) 37%, var(--ui-surface-2) 63%);
  background-size: 400% 100%;
  animation: ui-skeleton-sheen 1.4s ease-in-out infinite;
}
@keyframes ui-skeleton-sheen { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
@media (prefers-reduced-motion: reduce) {
  .ui-skeleton__line { animation: none; background: var(--ui-surface-2); }
}
/* Nhãn cho trình đọc màn hình — thứ `<p>Loading…</p>` cũ vốn có và một khối hình thuần sẽ mất. */
.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;

}

/* ── Resizer: the seam you drag between two panels ──
   One look for every split in the fleet: the admin's editors (chat | preview) today; made for gemcare's
   inbox columns too (they adopt it at their next ui:sync). The element is the drag target; its GEOMETRY (a flex item, a grid edge, an absolutely
   placed strip) belongs to the surface. While a drag runs the surface puts `ui-resizing` on <body>
   (and may put `dragging` on the seam itself to keep its grip lit).
   Markup: <div class="ui-resizer" role="separator" aria-orientation="vertical" tabindex="0"
   aria-label="…" aria-valuenow aria-valuemin aria-valuemax>. Pointer events, not mouse events (touch
   is a pointer); arrow keys move it — a focusable separator that ignores the keyboard is INERT. */
/* `touch-action: none` is what makes a FINGER drag reach this element at all. `preventDefault()` on
   `pointerdown` does not stop panning: the browser hands the gesture to the nearest scroller and sends
   `pointercancel`, so the seam jumps once and stops. */
.ui-resizer { cursor: col-resize; position: relative; touch-action: none; outline: none; }
/* The grip pill is invisible at rest (a clean seam) and fades in over the zone, on keyboard focus,
   and while a drag runs. Focus shows the pill rather than a ring: on a 5px seam a ring is a smear. */
.ui-resizer::before {
  content: ""; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 4px; height: 44px; border-radius: var(--gemboss-radius-full);
  background: var(--gemboss-primary);
  opacity: 0;
  transition: opacity 0.14s ease, height 0.14s ease;
}
.ui-resizer:hover::before,
.ui-resizer:focus-visible::before,
.ui-resizer.dragging::before,
body.ui-resizing .ui-resizer::before { opacity: 1; height: 52px; }
/* A FINGER NEEDS MORE THAN 5px, and the line must still look like 5px. WCAG 2.5.8 puts the floor for
   this kind of target at 24px, so the HIT area grows and the visible seam does not — widening the
   element itself would also move the columns. Only where the pointer is coarse. */
@media (pointer: coarse) {
  .ui-resizer::after {
    content: ""; position: absolute; top: 0; bottom: 0; left: -14px; right: -14px;
    touch-action: none;   /* the band must claim the gesture too, or the scroller still wins */
    z-index: 2;           /* above the panels either side, transparent */
  }
}
body.ui-resizing, body.ui-resizing * { cursor: col-resize !important; user-select: none !important; }
/* While dragging, a full-viewport transparent shield sits above everything — iframes included — so the
   pointer stream is never captured by a cross-document frame (the classic "stuck cursor"). */
body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999; cursor: col-resize; }

/* ── Tabs · Table · Empty state ──
   Plain-CSS primitives for surfaces that are NOT React (gemcare's vanilla dashboard reads this
   file through `npm run export:ui-css`). The React app has Polaris <Tabs>/<DataTable>/<EmptyState>
   for these; nothing in apps/admin uses the classes below, and they restyle no existing component.
   Only `--ui-*` / `--gemboss-*` tokens, so the dark `.gemboss-admin-shell` bridge applies too.

   Tabs: `<div class="ui-tabs" role="tablist"><button role="tab" aria-selected="true">…` — the
   selected state IS `aria-selected`, so what a screen reader announces and what is painted cannot
   disagree. Polaris Tabs under a mouse (0.12.0): 28px, 12px/550; 8px apart, the §9.1 gap for targets under 44.
   44px under a finger or at phone width (the rule after these). */
.ui-tabs {
  display: flex; gap: var(--ui-space-2); border-bottom: 1px solid var(--ui-border);
  overflow-x: auto; scrollbar-width: none;
}
.ui-tabs > button,
.ui-tabs > a {
  position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
  min-height: var(--ui-btn-h); padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
  border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;
  font-family: inherit; font-size: var(--ui-fs-caption); font-weight: 550; white-space: nowrap;
  color: var(--ui-text-muted);
  transition: color var(--gemboss-transition-fast) ease, background-color var(--gemboss-transition-fast) ease;
}
.ui-tabs > button:hover,
.ui-tabs > a:hover { color: var(--ui-text); background: var(--ui-surface-2); }
.ui-tabs > button:focus-visible,
.ui-tabs > a:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
.ui-tabs > a { text-decoration: none; }
/* Tab bằng link (mỗi tab là một URL): `<nav class="ui-tabs"><a aria-current="page">` — cùng hình với tab nút. */
.ui-tabs > a[aria-current="page"],
.ui-tabs > button[aria-selected="true"] { color: var(--ui-text-strong); }
.ui-tabs > a[aria-current="page"]::after,
.ui-tabs > button[aria-selected="true"]::after {
  content: ""; position: absolute; left: var(--ui-space-3); right: var(--ui-space-3); bottom: 0;
  height: 2px; border-radius: var(--gemboss-radius-full); background: var(--ui-accent);
}
.ui-tabs > button:disabled { opacity: .5; cursor: not-allowed; }
@media (pointer: coarse), (max-width: 47.9975em) { .ui-tabs > :is(button, a) { min-height: 44px; } }

/* Table: wrap it — `<div class="ui-table-wrap"><table class="ui-table">` — so a wide table scrolls
   inside its card instead of pushing the page sideways at 320px. Numeric cells take `.is-num`:
   right-aligned tabular figures, so a column of numbers lines up digit for digit. */
.ui-table-wrap {
  overflow-x: auto; max-width: 100%;
  background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
  box-shadow: var(--ui-card-shadow);
}
.ui-table { width: 100%; border-collapse: collapse; font-size: var(--ui-fs-body); color: var(--ui-text); }
.ui-table th,
.ui-table td {
  padding: var(--ui-space-2) 6px; text-align: left; vertical-align: middle;
  border-bottom: 1px solid var(--ui-border);
}
/* Polaris DataTable (13.9.5, measured on /app/analytics 05/10/2026): cells 8px 6px (a row 36), header 12px 6px (45),
   the first column 12px in from the edge. */
/* :where() keeps the edge columns at the base rule's weight, so a surface's own `.x-table td { padding }` still wins. */
.ui-table :is(th, td):where(:first-child) { padding-left: var(--ui-space-3); }
.ui-table :is(th, td):where(:last-child) { padding-right: var(--ui-space-3); }
.ui-table th { font-weight: 450; color: var(--ui-text-muted); white-space: nowrap; }
.ui-table thead th { padding-block: var(--ui-space-3); } /* the column header; a row header (tbody th) is a cell */
.ui-table tbody tr:last-child > td { border-bottom: 0; }
.ui-table tbody tr:hover { background: var(--ui-surface-2); }
.ui-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
/* Dòng phụ dưới nội dung chính của một ô (lớp, nguồn, ngày…): `<td><a>Loox</a><span class="ui-sub">Cộng sinh</span></td>`.
   Trước đây mỗi surface tự viết `.muted` riêng trong ô bảng (partner-radar, gemwatcher). */
/* Link nằm thẳng trong ô (tên bản ghi dẫn sang trang chi tiết) là link trong DANH SÁCH: ≥ 24px (WCAG 2.5.8, §9.1).
   Chữ 13px cho hộp 17px; visual-gate đo đỏ ở partner-radar /competitors 01/10/2026. inline-flex giữ nó trên dòng. */
.ui-table td > a { display: inline-flex; align-items: center; min-height: 24px; }
.ui-table .ui-sub { display: block; margin-top: 2px; font-size: var(--ui-fs-caption); color: var(--ui-text-muted); font-weight: 400; }

/* Empty state: `<div class="ui-empty"><p class="ui-empty__title">…</p><p class="ui-empty__body">…</p>
   <button class="ui-btn">…</button></div>`. Says what is missing and what to do next. */
.ui-empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--ui-space-2);
  padding: var(--ui-space-4); padding-block: 40px; text-align: center;
  color: var(--ui-text-muted); font-size: var(--ui-fs-body);
}
.ui-empty__title { margin: 0; font-size: 14px; line-height: 20px; font-weight: 650; color: var(--ui-text-strong); }
.ui-empty__body { margin: 0; max-width: 48ch; line-height: 1.5; }
.ui-empty > .ui-btn { margin-top: var(--ui-space-2); }

/* ═══ gemboss-field.css ═══ */
/* THE ONE TEXT FIELD, in a file of its own.
 *
 * It lives apart from gemboss-shell.css because the shell sheet is loaded ONLY by `app/app/layout.tsx`
 * — the /app subtree — while `/login` and `/try` are deliberately lean pages outside it. Adding the
 * class on either changed NOTHING, because neither loads the shell. Measured on prod 23/09/2026 (the
 * build before the change): /login drew its own field family in styled-jsx — 44px, radius 8, 14px,
 * border rgb(201,204,207) — and /try's textarea was the native box, radius 0. An earlier version of
 * this comment said both stayed "21px, unrounded, native border"; that was one field on /try written
 * up as if it described both pages. A few hundred bytes of field rules can go where the shell cannot.
 */
/* ── ONE text field for every hand-written <input>/<select> in the admin ─────────────────────────
   Measured 22/09/2026 across every /app route: the 21 Polaris fields were one shape (32px, radius 8,
   the input-border token, 13px → 16px on a phone), and the 13 hand-written ones were FIVE shapes —
   heights 33/36/38/42/44, radii 8/9/10, one warm off-token border, and every one of them still 13-14px
   on a phone, which is the size iOS Safari ZOOMS the page for the moment you tap the field. This is the
   Polaris field, written once: a custom field wears `gemboss-field` and nothing else decides its look.
   Its touch height lives in the pointer:coarse block with the Polaris rule it mirrors. */
/* …and the same box on a PRESSABLE control. Three buttons wore `gemboss-field` itself — the campaign
   compare's "↻ Refresh", which sits in a row between two selects that wear it, and the inspector's ✕
   and ＋ — because what they wanted was this box, lined up with the fields beside them. But a button
   is not a text field, and the class that MEANS "text field" should not be what says so: it makes the
   ratchet count a button as a converted field, and it invites the next person to put it on anything
   box-shaped. One box, two names, declared together so they can never drift apart. */
.gemboss-field,
.gemboss-control {
  box-sizing: border-box; height: 32px; min-width: 0; padding: 0 12px;
  font: inherit; font-size: 13px; line-height: 20px;
  color: var(--gemboss-field-ink);
  background-color: var(--gemboss-field-bg);
  border: 1px solid var(--gemboss-field-line);
  border-radius: 8px;
}
/* WebKit draws `input[type=search]` as `-webkit-appearance: searchfield`: its own padding and rounding, so in Safari
   (macOS and iOS, the platform the 16px rule exists for) a search box did not match the fields beside it. Same box for
   every text type; the clear button stays. (GemFactory #98 review, 01/10/2026.) */
input[type="search"]:is(.gemboss-field, .gemboss-field__bare) { -webkit-appearance: none; appearance: none; }
.gemboss-field::placeholder { color: var(--gemboss-field-placeholder); }
/* A NARROW field — the inspector's T/R/B/L boxes and its number spinners are 40-68px wide, and the
   shared 12px side padding left 38px of content box for a three-digit value (measured in Chromium:
   clientWidth 38, scrollWidth 58 for "120"). Same box, tighter lane. */
/* A field that is a 44px target at EVERY width, not only on a phone. Some surfaces asked for that in
   their own inline style (a popup's `minHeight: 44`), and that intent is theirs to keep — it moves here
   so the look still comes from one place. */
.gemboss-field--tall { min-height: 44px; }
.gemboss-field--narrow { padding: 0 6px; appearance: textfield; -moz-appearance: textfield; }
/* …and no spinner: the arrows eat ~11px of a 40px box, so "120" was still cut with the tighter
   padding (measured: clientWidth 38, scrollWidth 49). The value is typed here, not stepped. */
.gemboss-field--narrow::-webkit-outer-spin-button,
.gemboss-field--narrow::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.gemboss-field:hover:not(:disabled),
.gemboss-control:hover:not(:disabled) { border-color: var(--gemboss-field-line-hover); background-color: var(--gemboss-field-bg-hover); }
.gemboss-field:focus-visible,
.gemboss-control:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 1px; border-color: var(--gemboss-field-line-active); }
.gemboss-field:disabled,
.gemboss-control:disabled { opacity: 0.5; cursor: not-allowed; }
/* Polaris' own breakpoint for the same switch: under it a field reads 16px, so iOS does not zoom. */
/* On a PHONE-WIDTH screen a field is 16px (iOS does not zoom) and 44px tall (§9.1) — by WIDTH, not by
   pointer. A desktop browser resized to 320px still reports `pointer: fine`, and the page-revision
   gate measures the publication select's height at 320px: dropping that panel's own `min-height: 44px`
   in favour of the shared class left it 32px there, and the gate went red (CI of #3201). */
/* Cả hai tên nhận CÙNG hai thứ ở khổ điện thoại. Bản đầu chỉ cho `--control` chiều cao, không cho
   cỡ chữ — và 16px ở đây KHÔNG phải để chặn iOS zoom (một cái nút không zoom) mà để một hàng
   control đọc ra MỘT họ: nút Refresh của compare view ngồi giữa hai select, và ở 13px giữa hai
   ô 16px thì nó là một cỡ chữ thứ hai trong cùng một hàng (/code-review, 23/09/2026). */
@media (max-width: 47.9975em) { .gemboss-field, .gemboss-control { font-size: 16px; min-height: 44px; } }
/* A multi-line field: Polaris' multiline TextField — the same box, its height from `rows`. */
textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
/* A field made of parts — a fixed prefix ("/pages/") or a colour swatch beside the text. The WRAPPER
   wears `gemboss-field gemboss-field--group` and is the box; the text inside wears `gemboss-field__bare`
   and draws nothing of its own, taking the box's font size (13px, 16px on a phone) by inheritance. */
/* The GROUP grows to whatever it holds: `gemboss-field` fixes a 32px height, and a panel that asks
   for 44px targets (the section content panel does, and `page-saved-continuation-fixture` measures
   EVERY control there) puts a 44px input inside this box. Fixing the group at 32 and shrinking the
   input to fit clipped it and broke that gate; the box follows its content instead. */
.gemboss-field--group { display: flex; align-items: center; gap: 6px; height: auto; min-height: 32px; padding-right: 0; overflow: hidden; }
/* The phone floor again, for the GROUP. `min-height: 32px` above is the same specificity as the
   `.gemboss-field` rule in the media query and comes LATER in the file, so it won the cascade at
   phone width and quietly took the group back down to 32px — the URL-handle and background-colour
   rows, the two places a field is made of parts. A media query does not outrank anything; source
   order decided it, and the fix is to say it again after. (/code-review, 23/09/2026.) */
@media (max-width: 47.9975em) { .gemboss-field--group { min-height: 44px; } }
.gemboss-field--group:focus-within { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 1px; border-color: var(--gemboss-field-line-active); }
/* The colour swatch in a group: FLUSH to the group's left edge and full height — it covers the group's
   1px border top and bottom and its 12px left padding, so it is the whole 44px target the panel's rule
   asks for, rounded by the group's own corner (overflow: hidden), and never clipped. A 44px swatch
   INSIDE the 42px the border leaves was cut by a pixel top and bottom (/code-review #3201). */
.gemboss-field--group input[type="color"] { flex: none; align-self: stretch; width: 44px; min-width: 44px; height: auto; min-height: 0; margin: -1px 0 -1px -12px; padding: 0; border: 0; border-right: 1px solid var(--gemboss-field-line); border-radius: 0; background: none; cursor: pointer; }
/* ↑ the right edge is what keeps a WHITE swatch visible: without it the block melted into the field (measured). */
.gemboss-field--group input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.gemboss-field--group input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 0; }
.gemboss-field__bare { flex: 1; min-width: 0; align-self: stretch; margin-block: -1px; /* over the group's 1px border: the input is the 44px target, not 42 (0.5.1) */ padding: 0 12px 0 0; border: 0; border-radius: 0; outline: none; background: transparent; color: inherit; font: inherit; }
/* …and the placeholder, which "draws nothing" does not cover: the surfaces that used to declare
   their own (the /admin search rows, the settings and palette rows) stopped when they stopped
   owning the box, and without this every bare field would fall back to the browser's grey. */
.gemboss-field__bare::placeholder { color: var(--gemboss-field-placeholder); }

/* Under a thumb the field is 44px, the same height this app gives a Polaris field there. It sits in
   THIS file, not in the shell's pointer:coarse block, so /login and /try — which load only this
   sheet — get it too. */
@media (pointer: coarse) { .gemboss-field, .gemboss-control { height: auto; min-height: 44px; } }
/* A control is pressable; a field is not. That is the whole difference. */
.gemboss-control { cursor: pointer; }
/* An ICON control — the repeater's ✕ — is square and narrow beside its row. It was pinned to
   `width: 32` in an inline style, which no media query can reach, so under a thumb it was a
   32x44 target: tall enough, too narrow (§9.1 asks 44 in BOTH dimensions for a control).
   The width lives here now, and grows with the pointer. (/code-review, 23/09/2026.) */
/* Hai class trên cùng phần tử, có chủ ý: `.gemboss-ei-inp` đặt `width: 100%` ở cùng trọng số và
   nằm SAU trong thứ tự nạp (shell @import file này ở đầu), nên một selector đơn thua nó — đo
   được: nút ✕ giãn ra 472px. Bề rộng vốn là inline style nên trước đây nó thắng mọi thứ; đưa
   vào CSS thì phải mang theo trọng số của chính nó. */
.gemboss-control.gemboss-control--icon { width: 32px; flex: 0 0 auto; padding: 0; }
@media (pointer: coarse) { .gemboss-control.gemboss-control--icon { width: 44px; min-width: 44px; } }

/* ═══ shell.css — the shared shell (rail + topbar + main + the assistant's thread rail) ═══ */
.gemboss-popover-surface {
  background: var(--gemboss-surface);
  background: color-mix(in srgb, var(--gemboss-surface) 93%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  backdrop-filter: blur(14px) saturate(1.4);
  border: 1px solid var(--gemboss-divider);
  border-radius: var(--gemboss-radius-lg);
  box-shadow: var(--gemboss-shadow-raised);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .gemboss-popover-surface {
    background: var(--gemboss-surface);
  }
}
.icon-nav {
  position: fixed;
  top: 56px;
  left: 0;
  z-index: 200;
  width: 72px;
  height: calc(100dvh - 56px);
  background: var(--gemboss-rail-bg);
  border-right: 1px solid var(--gemboss-divider);
  display: flex;
  flex-direction: column;
  padding-top: 8px;
  overflow: hidden;
}
.gemboss-account-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 220px;
  z-index: 320;
  overflow: hidden;
}
.gemboss-account-menu-item,
.ui-menu__item {
  display: block;
  width: 100%;
  min-height: 44px;
  padding: 12px 16px;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 13px;
  color: var(--gemboss-text);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
}
.gemboss-account-menu-item:hover,
.ui-menu__item:hover {
  background: var(--gemboss-hover);
}
.gemboss-account-menu-item:focus-visible,
.ui-menu__item:focus-visible {
  outline: 2px solid var(--gemboss-primary);
  outline-offset: -2px;
  background: var(--gemboss-hover);
}
.gemboss-account-menu-item.danger,
.ui-menu__item--danger {
  color: var(--gemboss-danger-strong);
}
.gemboss-account-chip {
  width: 30px;
  height: 30px;
  border: 1px solid #cfd6ce;
  border-radius: var(--gemboss-radius-full);
  background: #f5f7f4;
  color: #303633;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 700;
  cursor: pointer;
}
.gemboss-platform-main {
  box-sizing: border-box;
  min-height: 100dvh;
  margin-left: 72px;
  padding-top: 56px;
  background: var(--gemboss-app-bg);
}
.icon-nav {
  top: 56px;
  height: calc(100dvh - 56px);
  background: var(--gemboss-nav-bg);
  border-right-color: var(--gemboss-divider);
}
@media (max-width: 1040px) {
  .gemboss-platform-topbar {
    grid-template-columns: minmax(120px, 1fr) minmax(220px, 520px) minmax(120px, 1fr);
  }
}
@media (max-width: 760px) {
  .gemboss-platform-main {
    margin-left: 0;
  }
}
.gemboss-account-chip {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--gemboss-primary);
  color: #fff;
  font-weight: 600;
  font-size: 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  cursor: pointer;
  transition: transform var(--gemboss-transition-base) ease;
}
.gemboss-account-chip:hover {
  transform: scale(1.05);
}
.gemboss-account-chip:focus-visible {
  outline: 2px solid var(--gemboss-primary);
  outline-offset: 2px;
}
.gemboss-platform-main {
  margin-top: 56px;
  margin-left: 72px;
  min-height: calc(100vh - 56px);
  padding: 24px;
  background: #fafafb;
}
@media (max-width: 768px) {
  .gemboss-platform-main {
    margin-left: 0;
    padding: 16px;
  }
}
.gemboss-platform-shell {
  display: block;
  min-height: 100vh;
  background: var(--gemboss-app-bg);
}
.gemboss-platform-topbar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 300;
  height: 56px;
  border-bottom: 1px solid var(--gemboss-divider);
  background: var(--gemboss-topbar-bg);
  display: grid;
  grid-template-columns: minmax(200px, 1fr) minmax(280px, 560px) minmax(200px, 1fr);
  align-items: center;
  gap: 16px;
  padding: 0 16px;
  box-shadow: var(--gemboss-shadow-resting);
}
.gemboss-account-chip {
  width: 32px;
  height: 32px;
  border: 1px solid var(--gemboss-chip-line);
  border-radius: var(--gemboss-radius-full);
  background: var(--gemboss-surface);
  color: var(--gemboss-chip-ink);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  font-size: 11px;
  font-weight: 700;
  cursor: pointer;
  transition: border-color var(--gemboss-transition-base) ease, background-color var(--gemboss-transition-base) ease;
}
.gemboss-account-chip:hover {
  border-color: var(--gemboss-chip-line-hover);
  background: var(--gemboss-chip-bg-hover);
  transform: none;
}
.icon-nav {
  position: fixed;
  top: 56px;
  left: 0;
  z-index: 200;
  width: 72px;
  height: calc(100dvh - 56px);
  border-right: 1px solid var(--gemboss-divider);
  background: var(--gemboss-nav-bg);
  padding-top: 12px;
}
.gemboss-platform-main {
  min-height: calc(100vh - 56px);
  margin-top: 56px;
  margin-left: 72px;
  background: var(--gemboss-app-bg);
  padding: 24px;
}
@media (max-width: 1120px) {
  .gemboss-platform-topbar {
    grid-template-columns: 220px minmax(220px, 1fr) auto;
    gap: 12px;
  }
}
@media (max-width: 760px) {
  .gemboss-platform-topbar {
    height: auto;
    min-height: 56px;
    grid-template-columns: minmax(0, 1fr) auto;
    padding: 12px 12px;
  }
  .gemboss-account-chip {
    display: none;
  }
  .icon-nav {
    display: none;
  }
  .gemboss-platform-main {
    margin-left: 0;
    padding: 16px;
    padding-top: 104px;
  }
}
.gnav-scroll {
  scrollbar-width: thin;
  scrollbar-color: rgba(31,30,29,0.22) transparent;
}
.gemboss-assistant-railfoot.is-button {
  width: 100%;
  border: none;
  background: none;
  cursor: pointer;
  text-align: left;
  font: inherit;
  border-radius: 8px;
  transition: background 0.12s ease, color 0.12s ease;
}
.gemboss-assistant-railfoot.is-button:hover {
  background: var(--gemboss-primary-soft);
  color: var(--gemboss-primary-dark);
}
.gemboss-assistant-empty {
  padding: 24px 20px;
  color: var(--gemboss-text-muted);
  font-size: 13px;
}
@media (max-width: 760px) {
  .gemboss-platform-topbar {
    display: flex;
    align-items: center;
    gap: 8px;
    height: 56px;
    min-height: 56px;
    padding: 0 12px;
  }
  .gemboss-account-chip {
    display: inline-flex;
  }
  .gemboss-platform-main {
    padding-top: 56px;
    padding-bottom: calc(56px + env(safe-area-inset-bottom));
  }
}
@media (pointer: coarse) {
  .gemboss-account-chip {
    width: 44px;
    height: 44px;
  }
  .gnav-item {
    min-height: 44px;
  }
  .gemboss-assistant-collapse,
  .gemboss-assistant-newchat {
    min-height: 44px;
    min-width: 44px;
  }
}
.icon-nav {
  width: 240px;
  padding: 12px 12px 0;
  overflow: hidden;
  background: var(--gemboss-nav-bg);
  display: flex;
  flex-direction: column;
}
.gnav-scroll {
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
}
.gnav-bottom {
  padding-bottom: 12px;
}
.gnav-item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 8px 12px;
  margin: 1px 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  text-align: left;
  border-radius: var(--gemboss-radius-md);
  text-decoration: none;
  font-family: inherit;
  color: var(--gemboss-text);
  font-size: var(--gemboss-fs-body);
  font-weight: 500;
  line-height: 1.2;
  transition: background-color var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
}
.gnav-item:hover {
  background: var(--gemboss-nav-hover);
}
.gnav-item:focus-visible {
  outline: 2px solid var(--gemboss-accent);
  outline-offset: -2px;
}
.gnav-item.active {
  color: var(--gemboss-active-fg);
  font-weight: 600;
}
.gnav-group:not(.has-active-child) > .gnav-item.active {
  background: var(--gemboss-active-bg);
}
.gnav-group:not(.has-active-child) > .gnav-item.active:hover {
  background: var(--gemboss-active-bg);
}
.gnav-group.has-active-child > .gnav-item.active:hover {
  background: var(--gemboss-nav-hover);
}
.gnav-ico {
  flex: 0 0 20px;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
}
.gnav-ico svg {
  width: 20px;
  height: 20px;
  fill: currentColor;
}
.gnav-label {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.gnav-sub {
  list-style: none;
  margin: 1px 0 6px;
  padding: 0;
}
.gnav-subitem {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 6px 12px 6px 44px;
  margin: 1px 0;
  border-radius: var(--gemboss-radius-sm);
  color: var(--gemboss-text-muted);
  font-size: 13px;
  line-height: 1.2;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
}
@media (pointer: coarse) {
  .gnav-subitem {
    min-height: 44px;
  }
}
.gnav-subitem:hover {
  background: var(--gemboss-nav-hover);
  color: var(--gemboss-text);
}
.gnav-subitem.active {
  color: var(--gemboss-active-fg);
  font-weight: 600;
  background: var(--gemboss-active-bg);
}
.gnav-subitem.active:hover {
  background: var(--gemboss-active-bg);
  color: var(--gemboss-active-fg);
}
.gnav-subitem.active::before {
  content: "";
  position: absolute;
  left: 20px;
  top: 50%;
  transform: translateY(-50%);
  width: 3px;
  height: 15px;
  border-radius: 2px;
  background: var(--gemboss-primary);
}
.gnav-subitem.is-soon {
  opacity: 0.5;
  cursor: default;
}
.gnav-subitem em {
  font-style: normal;
  font-size: var(--gemboss-fs-micro);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border: 1px solid var(--gemboss-divider);
  border-radius: var(--gemboss-radius-sm);
  padding: 1px 6px;
}
.gnav-sep {
  height: 1px;
  background: var(--gemboss-divider);
  margin: 8px 6px;
}
.gnav-item.gnav-assistant {
  color: var(--gemboss-accent-strong);
  font-weight: 600;
  background: var(--gemboss-accent-soft);
}
.gnav-item.gnav-assistant .gnav-ico svg {
  fill: var(--gemboss-accent);
}
.gnav-item.gnav-assistant:hover {
  background: color-mix(in srgb, var(--gemboss-accent) 16%, var(--gemboss-surface));
}
.gnav-item.gnav-assistant.active {
  background: var(--gemboss-active-bg);
  color: var(--gemboss-active-fg);
}
.gnav-item.gnav-assistant.active .gnav-ico svg {
  fill: var(--gemboss-active-fg);
}
.gemboss-platform-main {
  margin-left: 240px;
}
.gemboss-mobile-menu-btn {
  display: none;
}
.gemboss-mobile-nav-backdrop {
  display: none;
}
@media (max-width: 760px) {
  .gemboss-platform-main {
    margin-left: 0;
    padding-bottom: env(safe-area-inset-bottom);
  }
  .gemboss-mobile-menu-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 38px;
    height: 38px;
    margin-left: -2px;
    border: 0;
    border-radius: 8px;
    background: transparent;
    cursor: pointer;
    color: var(--gemboss-text);
  }
  .gemboss-mobile-menu-btn:active {
    background: var(--gemboss-hover);
  }
  .icon-nav {
    display: flex;
    width: min(82vw, 300px);
    transform: translateX(-100%);
    transition: transform 0.26s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.26s cubic-bezier(0.4, 0, 0.2, 1);
    z-index: 1000;
    box-shadow: none;
  }
  .gemboss-platform-shell.is-mobile-nav-open .icon-nav {
    transform: translateX(0);
    box-shadow: 2px 0 26px rgba(31, 30, 29, 0.20);
  }
  .gemboss-mobile-nav-backdrop {
    display: block;
    position: fixed;
    inset: 56px 0 0 0;
    width: 100%;
    border: 0;
    padding: 0;
    background: rgba(20, 19, 18, 0.38);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.22s ease;
    z-index: 999;
  }
  .gemboss-platform-shell.is-mobile-nav-open .gemboss-mobile-nav-backdrop {
    opacity: 1;
    pointer-events: auto;
  }
}
@media (max-width: 305.98px) {
  .gemboss-platform-topbar {
    padding: 0 8px;
  }
}
@media (min-width: 306px) and (max-width: 460px) {
  .gemboss-platform-topbar {
    padding: 0 8px;
  }
  .gemboss-mobile-menu-btn {
    width: 44px;
    height: 44px;
  }
  .gemboss-account-chip {
    width: 44px;
    height: 44px;
  }
}
.gemboss-assistant-chatslot {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
  min-width: 0;
}
.gemboss-assistant-chatslot[hidden] {
  display: none;
}
.gemboss-assistant-page > .gemboss-assistant-tabs {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.gemboss-assistant-tabs::-webkit-scrollbar {
  display: none;
}
.gemboss-assistant-tab {
  display: flex;
  align-items: center;
  flex: 0 1 auto;
  min-width: 0;
  max-width: 190px;
  border: 1px solid var(--gemboss-divider);
  border-radius: var(--gemboss-radius-md);
  background: transparent;
}
.gemboss-assistant-tab.active {
  background: var(--gemboss-active-bg);
  border-color: var(--gemboss-active-bg);
}
.gemboss-assistant-tab-main {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  min-height: 44px;
  padding: 0 4px 0 8px;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: var(--gemboss-fs-caption);
  color: var(--gemboss-text-muted);
  cursor: pointer;
}
.gemboss-assistant-tab.active .gemboss-assistant-tab-main {
  color: var(--gemboss-text-strong);
}
.gemboss-assistant-tab-title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gemboss-assistant-tab-dot {
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: var(--gemboss-radius-full);
  background: var(--gemboss-divider);
}
.gemboss-assistant-tab-dot.is-running {
  background: var(--gemboss-primary);
  animation: gemboss-assistant-pulse 1.4s ease-in-out infinite;
}
.gemboss-assistant-tab-close {
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 0;
  background: transparent;
  color: var(--gemboss-text-muted);
  cursor: pointer;
  padding: 0;
}
.gemboss-assistant-tab-close:hover {
  color: var(--gemboss-text-strong);
}
.gemboss-assistant-tab-close svg {
  width: 14px;
  height: 14px;
}
.gemboss-assistant-sronly {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.gemboss-assistant-thread-running {
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: var(--gemboss-radius-full);
  background: var(--gemboss-primary);
  animation: gemboss-assistant-pulse 1.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .gemboss-assistant-tab-dot.is-running,
  .gemboss-assistant-thread-running {
    animation: none;
  }
}
.gemboss-assistant-route-claim {
  display: block;
  height: 100%;
}
.gemboss-assistant-layout.is-dock {
  height: auto;
  min-height: 0;
}
.gemboss-assistant-layout.is-dock .gemboss-assistant-page {
  height: auto;
  max-width: none;
  padding: 4px 0 8px;
}
.gnav-item.gnav-assistant {
  position: relative;
}
body.gemboss-assistant-busy .gnav-item.gnav-assistant::after,
body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
  content: "";
  position: absolute;
  top: 6px;
  left: 26px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: 2px solid var(--gemboss-surface);
}
body.gemboss-assistant-busy .gnav-item.gnav-assistant::after {
  background: var(--gemboss-primary);
  animation: gemboss-assistant-pulse 1.4s ease-in-out infinite;
}
body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
  background: var(--gemboss-success-strong);
}
@media (prefers-reduced-motion: reduce) {
  body.gemboss-assistant-busy .gnav-item.gnav-assistant::after {
    animation: none;
  }
}
.gemboss-assistant-layout {
  display: flex;
  height: calc(100dvh - 56px);
  width: 100%;
  min-height: 0;
}
.gemboss-assistant-rail {
  flex: 0 0 264px;
  width: 264px;
  display: flex;
  flex-direction: column;
  min-height: 0;
  border-right: 1px solid var(--gemboss-divider);
  background: transparent;
  padding: 16px 12px 12px;
  gap: 12px;
  transition: flex-basis 180ms ease, width 180ms ease;
}
.gemboss-assistant-rail-top {
  display: flex;
  align-items: center;
  gap: 6px;
}
.gemboss-assistant-newchat {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
  padding: 12px 12px;
  border: 1px solid var(--gemboss-divider);
  border-radius: 12px;
  background: transparent;
  color: var(--gemboss-text);
  font-size: var(--gemboss-fs-body);
  font-weight: 600;
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
.gemboss-assistant-newchat:hover {
  background: rgba(0,0,0,0.035);
  border-color: var(--gemboss-text-muted);
}
.gemboss-assistant-newchat svg {
  width: 17px;
  height: 17px;
  fill: currentColor;
  flex: 0 0 auto;
}
.gemboss-assistant-newchat span {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 1 1 auto;
}
.gemboss-assistant-kbd {
  flex: 0 0 auto;
  font-size: var(--gemboss-fs-micro);
  font-weight: 600;
  line-height: 1;
  padding: 4px 6px;
  border: 1px solid var(--gemboss-divider);
  border-radius: 6px;
  background: transparent;
  color: var(--gemboss-text-muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.gemboss-assistant-search:focus-within .gemboss-assistant-kbd {
  display: none;
}
.gemboss-assistant-collapse {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--gemboss-text-muted);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.gemboss-assistant-collapse:hover {
  background: rgba(0,0,0,0.05);
  color: var(--gemboss-text);
}
.gemboss-assistant-collapse svg {
  width: 16px;
  height: 16px;
  fill: currentColor;
}
.gemboss-assistant-search {
  gap: 6px;
}
.gemboss-assistant-search > svg {
  width: 15px;
  height: 15px;
  fill: var(--gemboss-text-muted);
  flex: 0 0 auto;
}
.gemboss-assistant-search-clear {
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--gemboss-text-muted);
  cursor: pointer;
}
.gemboss-assistant-search-clear:hover {
  background: rgba(0,0,0,0.07);
  color: var(--gemboss-text);
}
.gemboss-assistant-search-clear svg {
  width: 13px;
  height: 13px;
  fill: currentColor;
}
.gemboss-assistant-railfoot {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 12px 2px;
  border-top: 1px solid var(--gemboss-divider);
  margin-top: 4px;
  color: var(--gemboss-text-muted);
  font-size: var(--gemboss-fs-caption);
  line-height: 1.35;
}
.gemboss-assistant-railfoot svg {
  width: 13px;
  height: 13px;
  flex: 0 0 auto;
  fill: currentColor;
  opacity: 0.8;
}
.gemboss-assistant-threads {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.gemboss-assistant-threads-empty {
  padding: 16px 8px;
  color: var(--gemboss-text-muted);
  font-size: var(--gemboss-fs-caption);
}
.gemboss-assistant-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.gemboss-assistant-group + .gemboss-assistant-group {
  margin-top: 12px;
}
.gemboss-assistant-group-label {
  padding: 4px 12px 4px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--gemboss-text-muted);
  text-transform: uppercase;
}
.gemboss-assistant-thread {
  position: relative;
  display: flex;
  align-items: center;
  border-radius: 8px;
  transition: background 120ms ease;
}
.gemboss-assistant-thread:hover {
  background: var(--gemboss-hover, rgba(0,0,0,0.04));
}
.gemboss-assistant-thread.active {
  background: rgba(0,0,0,0.07);
}
.gemboss-assistant-thread-main {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 8px 12px;
  border: 0;
  background: transparent;
  text-align: left;
  cursor: pointer;
}
.gemboss-assistant-thread-titlerow {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-width: 0;
}
.gemboss-assistant-pin-dot {
  width: 12px;
  height: 12px;
  flex: 0 0 auto;
  fill: var(--gemboss-text-muted);
}
.gemboss-assistant-thread-title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 13px;
  font-weight: 500;
  color: var(--gemboss-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.gemboss-assistant-thread-meta {
  font-size: var(--gemboss-fs-caption);
  color: var(--gemboss-text-muted);
}
.gemboss-assistant-thread.menu-open {
  background: rgba(0,0,0,0.07);
}
.gemboss-assistant-thread.menu-open .gemboss-assistant-thread-actions {
  opacity: 1;
}
.gemboss-assistant-menu {
  z-index: 60;
  width: 184px;
  padding: 6px;
  display: flex;
  flex-direction: column;
}
.gemboss-assistant-menu-confirm {
  padding: 8px 12px 6px;
  font-size: var(--gemboss-fs-caption);
  font-weight: 600;
  color: var(--gemboss-text);
}
.gemboss-assistant-menu button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 12px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  text-align: left;
  font-size: 13px;
  color: var(--gemboss-text);
  cursor: pointer;
}
.gemboss-assistant-menu button:hover {
  background: var(--gemboss-hover, rgba(0,0,0,0.05));
}
.gemboss-assistant-menu button.danger {
  color: var(--gemboss-danger-strong);
}
.gemboss-assistant-menu button svg {
  width: 15px;
  height: 15px;
  fill: currentColor;
  flex: 0 0 auto;
}
.gemboss-assistant-thread-actions {
  position: absolute;
  right: 6px;
  display: flex;
  gap: 2px;
  opacity: 0;
  transition: opacity 120ms ease;
}
.gemboss-assistant-thread:hover .gemboss-assistant-thread-actions,
.gemboss-assistant-thread:focus-within .gemboss-assistant-thread-actions {
  opacity: 1;
}
.gemboss-assistant-thread-act {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--gemboss-text-muted);
  cursor: pointer;
  transition: background 120ms ease, color 120ms ease;
}
.gemboss-assistant-thread-act:hover {
  background: rgba(0,0,0,0.07);
  color: var(--gemboss-text);
}
.gemboss-assistant-thread-act.danger:hover {
  color: var(--gemboss-danger-strong);
}
.gemboss-assistant-thread-act svg {
  width: 15px;
  height: 15px;
  fill: currentColor;
}
.gemboss-assistant-thread.editing {
  background: rgba(0,0,0,0.07);
}
.gemboss-assistant-thread-edit {
  display: flex;
  align-items: center;
  gap: 2px;
  width: 100%;
  padding: 6px 6px;
}
.gemboss-assistant-thread-edit input {
  flex: 1 1 auto;
  min-width: 0;
}
.gemboss-assistant-layout .gemboss-assistant-page {
  height: 100%;
  max-width: 820px;
  flex: 1 1 auto;
  min-width: 0;
  padding-top: 16px;
}
.gemboss-assistant-layout .gemboss-assistant-page.is-switching {
  opacity: 0.55;
  transition: opacity 120ms ease;
  pointer-events: none;
}
.gemboss-assistant-rail.is-collapsed {
  flex-basis: 60px;
  width: 60px;
  padding: 16px 8px 12px;
}
.gemboss-assistant-rail.is-collapsed .gemboss-assistant-rail-top {
  flex-direction: column;
  gap: 8px;
}
.gemboss-assistant-rail.is-collapsed .gemboss-assistant-newchat {
  justify-content: center;
  padding: 8px;
  flex: 0 0 auto;
}
.gemboss-assistant-rail.is-collapsed .gemboss-assistant-newchat span,
.gemboss-assistant-rail.is-collapsed .gemboss-assistant-search,
.gemboss-assistant-rail.is-collapsed .gemboss-assistant-railfoot,
.gemboss-assistant-rail.is-collapsed .gemboss-assistant-threads {
  display: none;
}
@media (max-width: 760px) {
  .gemboss-assistant-rail {
    flex-basis: 60px;
    width: 60px;
    padding: 16px 8px 12px;
  }
  .gemboss-assistant-rail .gemboss-assistant-rail-top {
    flex-direction: column;
    gap: 8px;
  }
  .gemboss-assistant-newchat {
    justify-content: center;
    padding: 8px;
    flex: 0 0 auto;
  }
  .gemboss-assistant-newchat span,
  .gemboss-assistant-search,
  .gemboss-assistant-railfoot,
  .gemboss-assistant-threads,
  .gemboss-assistant-collapse {
    display: none;
  }
}
.gemboss-assistant-page {
  height: calc(100dvh - 56px);
  width: 100%;
  max-width: 760px;
  margin: 0 auto;
  padding: 24px 0 16px;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.gemboss-assistant-page > * {
  flex: 1 1 auto;
  min-height: 0;
}
.gemboss-assistant-empty {
  flex: 1;
  display: grid;
  place-items: center;
  color: var(--gemboss-text-muted);
  font-size: 14px;
}
.gemboss-platform-shell {
  display: flow-root;
}
.gemboss-platform-main {
  padding-bottom: calc(24px + var(--gemboss-invite-h, 0px));
}
.gb-account {
  position: relative;
}
.gb-account > summary {
  list-style: none;
}
.gb-account > summary::-webkit-details-marker {
  display: none;
}
.gb-account-head {
  padding: 12px 16px;
  border-bottom: 1px solid var(--gemboss-divider);
}
.gb-account-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--gemboss-text-strong);
}
.gb-account-email {
  font-size: 12px;
  color: var(--gemboss-text-muted);
}
.gb-account-role {
  display: inline-block;
  margin-top: 4px;
  padding: 1px 6px;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 600;
  color: var(--gemboss-active-fg);
  background: var(--gemboss-active-bg);
}
.gb-account-form {
  margin: 0;
}
.gnav-scroll::-webkit-scrollbar {
  width: 9px;
}
.gnav-scroll::-webkit-scrollbar-track {
  background: transparent;
}
.gnav-scroll::-webkit-scrollbar-thumb {
  background: rgba(31,30,29,0.2);
  border-radius: 6px;
  border: 2px solid transparent;
  background-clip: content-box;
}
@keyframes gemboss-assistant-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* ═══ @gemboss/ui src/app-shell.css ═══ */
/* Styles for the parts of the shell the PACKAGE owns — `AppShell`'s three topbar slots,
 * `RailHeading`, `RailCount`. Everything else (rail, topbar, drawer, tokens, kit) is the admin's
 * own CSS, exported by apps/admin/scripts/export-ui-css.mjs and concatenated ahead of this file into
 * `gemboss-ui.css`. The admin does not load this file: it composes the shell itself and has no
 * slots, headings or pills of this kind.
 *
 * Built only from --gemboss-* tokens, so a palette change in gemboss-tokens.css reaches it. */

/* The base font. The admin's comes from Polaris, which no other surface loads, so without this rule
 * every surface on gemboss-ui.css rendered in the browser's default serif. Same elements Polaris
 * sets (html, body, button), same stack (--gemboss-font-sans resolves to Polaris's
 * --p-font-family-sans fallback). `:where()` keeps it at (0,0,0): any rule a surface writes for
 * these elements wins, wherever it sits in the cascade. Re-theme with the token, not a body rule.
 * The admin does not load this file, so its own rendering is untouched. */
:where(html, body, button) { font-family: var(--gemboss-font-sans); }
/* …and the rest of Polaris' body text (02/10/2026): bodyMd, 13px on a 20px line, weight 450, the text colour,
 * antialiased. With the face alone, every surface picked its own size — measured on the sign-in pages that day:
 * 14, 15 and 16px bodies, one in Lora serif — so every line outside a kit class read differently from the admin.
 * Same (0,0,0) as above: a surface that sets its own body text still wins, and should delete that rule.
 * BODY only, never `html`: the root size is what `rem` means, and gemwatcher alone has ~200 rem lengths that a 13px
 * root would shrink by a fifth (review #3665). Polaris does the same: html stays at the browser's 100%. */
:where(body) {
  font-size: 13px; line-height: 20px; font-weight: 450; color: var(--gemboss-text);
  -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}
/* Headings get their own line (Polaris heading line-heights are ~1.25-1.4): inheriting the body's 20px made a wrapped 22px
 * title overlap itself (gempress, 0.5.1). */
:where(h1, h2, h3) { letter-spacing: -0.01em; line-height: 1.3; }
/* Polaris' heading reset (0.12.0, 05/10/2026): in the admin an <h2> or a <p> with no class is body text with no margin,
 * because Polaris' global sheet sets `h1…h6, p { margin: 0; font-size: 1em; font-weight: regular }` and every heading
 * that looks like one is a Text variant. Surfaces had the browser's instead: a bare <h2> at 19.5px bold with 16px margins
 * sat in GemFactory and taskdesk pages beside kit cards titled 14px. Here, only INSIDE the shell (`.gemboss-platform-
 * shell`, the AppShell root every surface on the kit renders): a login page or a public page keeps its own type. Never
 * inside an article the surface shows: `.ui-prose` or `[data-content]` (the contract), and the wrappers the fleet already
 * uses for rendered HTML, read on 05/10/2026, by exact name (gemcommunity `md-body` and `recap-md`,
 * content-radar `article-body`, the assistants' `asst-md`; any class containing `prose` or `markdown`, any case, so ProseMirror too). A NEW wrapper: mark it `ui-prose` or
 * `data-content`. scripts/density.mjs skips exactly this list (ARTICLE); density.test.ts fails if the two differ. A post body keeps the browser's scale. Only an element WITHOUT a class: a surface's `h2.gf-title` that sets only its size keeps the
 * browser's bold and margins (a zero-specificity author rule still beats the browser's own sheet, review #3787). */
:where(.gemboss-platform-shell) :where(h1, h2, h3, h4, h5, h6, p):where(:not([class])):not(:where(.ui-prose, [data-content], [class*="prose" i], [class*="markdown" i], .md-body, .recap-md, .asst-md, .article-body) *) {
  margin: 0; font-size: 1em; font-weight: inherit; line-height: inherit;
}

.gemboss-topbar-start { display: flex; align-items: center; gap: 8px; min-width: 0; }
.gemboss-topbar-center { display: flex; justify-content: center; min-width: 0; }
.gemboss-topbar-end { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; }
@media (max-width: 760px) {
  /* The topbar is one flex row here: the middle slot takes what is left. Basis 0, not auto: with `auto` the search's own
   * width (up to 520px) was its claim, so the other slots had to give way to it (0.6.0 pinned them instead, see below). With
   * 0 it only takes what the others leave, down to a 96px floor: an input with its icon still reads as search. */
  .gemboss-topbar-center { flex: 1 1 0; min-width: 96px; justify-content: flex-start; }
  .gemboss-topbar-center:empty { min-width: 0; }
  /* No actions: the empty slot would still take one flex `gap` (8px) off the search at 320px. */
  .gemboss-topbar-end:empty { display: none; }
  /* The actions never give: with `min-width: 0` the slot shrank and its buttons (justify flex-end) spilled LEFT over the
   * search (assistant + account, 15px over partner-radar's search at 320 and 360, 0.6.0).
   * The start slot gives LAST, after the search reaches its floor: the brand name ellipsizes (.gb-brand-name) instead of
   * the account chip leaving the screen. 0.6.0 pinned it (`flex: 0 0 auto`) and a 30-character brand at 461px pushed the
   * chip 44px past the right edge (review #3690). The menu button and the mark keep their 44px. */
  .gemboss-topbar-end { flex: 0 0 auto; }
  .gemboss-topbar-start { flex: 0 1 auto; }
  .gemboss-topbar-start > .gemboss-mobile-menu-btn { flex: 0 0 auto; }
  .gemboss-topbar-start > .gb-brand { min-width: 44px; }
}

.gnav-heading {
  padding: 10px 12px 4px;
  font-size: var(--gemboss-fs-micro); font-weight: 600; line-height: 1.2;
  letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--gemboss-text-muted);
}
.gnav-scroll > .gnav-heading:first-child,
.gnav-scroll > .gnav-section:first-child > .gnav-heading:first-child { padding-top: 2px; }

.gnav-count {
  margin-left: auto; flex: 0 0 auto; min-width: 20px; padding: 0 6px;
  border-radius: var(--gemboss-radius-full);
  background: var(--gemboss-hover); color: var(--gemboss-text-muted);
  font-size: var(--gemboss-fs-micro); font-weight: 700; line-height: 18px;
  text-align: center; font-variant-numeric: tabular-nums;
}
.gnav-item.active .gnav-count { background: var(--gemboss-primary); color: var(--gemboss-on-primary); }

/* ── Navigation pending (0.10.0): a click answers on the first frame ─────────────────────────────────────────────
 * Chris 05/10/2026: content-radar's rail (the icon turns into a spinner the moment you click, sub items too) is the
 * standard for every surface. Measured 04/10 on content-radar with a server ~280 ms away: nothing on screen changed
 * for 321-503 ms after a click; with this, 6-8 ms.
 *
 * ONE state, three ways to set it, so a React rail, a Next rail and a server-rendered rail look the same:
 *   - `aria-busy="true"` on the item: RailItem / RailSubItem `pending` (React), kit-plain.js on a followed link;
 *   - a `.gnav-pending` child: NavLink from `@gemboss/ui/next` (Next's useLinkStatus is only readable INSIDE the
 *     link, so the link cannot set its own attribute; it renders this status text and `:has()` reads it).
 * `:is()` is forgiving: a browser without `:has()` still honours the aria-busy half of each rule.
 * The icon keeps its box (visibility, not display), so the label never moves. Here and not in shell.css: the
 * admin's shell sheet has <1 KB of first-load headroom, and the admin answers a click with its own holding screen.
 */
.gnav-item:is([aria-busy="true"], :has(> .gnav-pending)),
.gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending)) { cursor: progress; }
.gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico > * { visibility: hidden; }
.gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico::after,
.gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after,
.gb-pending-mark {
  content: ""; display: inline-block; vertical-align: middle; grid-area: 1 / 1; flex: none; box-sizing: border-box;
  width: 20px; height: 20px; border-radius: 50%;
  /* the item's own ink, so the spinner follows the active and hover colours */
  border: 2px solid color-mix(in srgb, currentColor 18%, transparent); border-top-color: currentColor;
  animation: gnav-spin 0.7s linear infinite;
}
/* A sub item has no icon box: a 12px spinner in its 44px indent, just before the label, out of the flow. At the row's end
 * it would be a third item in a space-between row and push a trailing count to the middle (review #3777). */
.gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after {
  position: absolute; left: 22px; top: 50%; margin-top: -6px; width: 12px; height: 12px; border-width: 1.5px;
}
@keyframes gnav-spin { to { transform: rotate(360deg); } }
/* Slower, never off: it is the only sign that the click was heard. */
@media (prefers-reduced-motion: reduce) {
  .gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico::after,
  .gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after,
  .gb-pending-mark { animation-duration: 2s; }
}
/* A link in the PAGE (a list row, a card, a tab) opts in with `data-gb-pending`: PendingLink (`@gemboss/ui/next`) or
 * kit-plain.js put `span.gb-pending-mark` (role=status) inside it while the page loads. "inline" (default): 12px after
 * the text. "corner": 16px in the top right corner, out of the flow, for a link that wraps a whole card (a grid card
 * gains no column). Marked by attribute, not class: a surface that colours bare links with `a:not([class])` keeps it. */
a[data-gb-pending]:has(> .gb-pending-mark), a[data-gb-pending][aria-busy="true"] { cursor: progress; }
a[data-gb-pending] > .gb-pending-mark { display: inline-block; width: 12px; height: 12px; border-width: 1.5px; margin-left: 6px; vertical-align: -1px; }
a[data-gb-pending="corner"] { position: relative; }
a[data-gb-pending="corner"] > .gb-pending-mark { position: absolute; top: 12px; right: 12px; width: 16px; height: 16px; margin: 0; }
/* RouteHolding (./route-holding.tsx): a route group's loading boundary. The kit's skeleton lines in the content column,
 * one shape for every route: a title bar, then four lines of falling length (the admin's holding screen). */
.gb-route-holding { padding-top: 4px; }
.gb-route-holding .ui-skeleton { gap: 16px; }
.gb-route-holding__heading { margin: 0 0 20px; font-size: 20px; font-weight: 650; line-height: 1.3; color: var(--gemboss-text); }
.gb-route-holding .ui-skeleton__line { height: 10px; }
.gb-route-holding .ui-skeleton__line.gb-route-holding__title { height: 20px; width: 34%; margin-bottom: 8px; }
.gb-route-holding .ui-skeleton__line:nth-last-child(4) { width: 46%; }
.gb-route-holding .ui-skeleton__line:nth-last-child(3) { width: 92%; }
.gb-route-holding .ui-skeleton__line:nth-last-child(2) { width: 78%; }
.gb-route-holding .ui-skeleton__line:nth-last-child(1) { width: 61%; }
/* RouteCover: the same frame over the content area from the click until the navigation commits. Shown only while
 * `html[data-gb-navigating]` (set by the @gemboss/ui/next links while pending). No grace delay: the target is the new
 * screen within 100 ms of the click, and a 40 ms grace measured 120-132 ms on content-radar's rail (05/10/2026). The
 * 80 ms fade is what a router cache hit (20-90 ms) sees of it: a faint, partial frame, not a full flash.
 * z 5: above the outgoing page, below the rail, the topbar (300) and the assistant (290). */
.gb-route-cover { display: none; }
html[data-gb-navigating] .gb-route-cover {
  display: block; position: fixed; inset: 56px 0 0 240px; z-index: 5; overflow: hidden; box-sizing: border-box;
  padding: 24px; background: var(--gemboss-app-bg); pointer-events: none;
  animation: gb-route-cover-in 0.08s ease-out both;
}
@keyframes gb-route-cover-in { from { opacity: 0; } to { opacity: 1; } }
@media (max-width: 760px) { html[data-gb-navigating] .gb-route-cover { left: 0; padding: 16px; } }
@media (prefers-reduced-motion: reduce) { html[data-gb-navigating] .gb-route-cover { animation-duration: 0s; } }
/* The status text a NavLink renders inside a rail item: read, not seen. */
.gnav-pending {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

/* ── BrandLockup (./brand.tsx) ─────────────────────────────────────────────────────────────────
 * The admin's `.gemboss-brand-lockup` box (40px tall, 8px padding, hover wash) around the wordmark's
 * own proportions. Not that class itself: its ≤460px rule swaps in a background image at an
 * admin-only path, which every other host would 404. Ink is the logo file's #14151a. */
.gb-brand {
  min-width: 0; height: 40px; margin: 0; padding: 0 8px;
  border-radius: var(--gemboss-radius-md);
  display: inline-flex; align-items: center; gap: 4.5px;
  color: var(--gb-brand-ink, #14151a); text-decoration: none;
  transition: background-color var(--gemboss-transition-base) ease;
}
.gb-brand:hover { background: var(--gemboss-hover); }
.gb-brand:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: 2px; }
.gb-brand-gem { display: block; flex: 0 0 auto; }
.gb-brand-name {
  font-family: Inter, var(--gemboss-font-sans); font-size: 13.3px; font-weight: 700;
  letter-spacing: -0.13px; line-height: 1; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
/* The admin shows only its mark below 460px; so does this. */
@media (max-width: 460px) {
  .gb-brand-name { display: none; }
  /* The mark alone is ~21px wide: a 44px target, centred (visual-gate tap_target_link on content-radar at 320, 0.5.1). */
  .gb-brand { min-width: 44px; height: 44px; justify-content: center; }
}

/* ── AccountMenu (./account.tsx): its rules live in shell.css since 29/09/2026, because the admin
 * renders AccountMenu too now and loads shell.css, not this file. */

/* ── Topbar search (TopbarSearch, search.tsx; 0.6.3) ────────────────────────────────────────────────────────
   The admin's "Search ⌘K" box as a real form for AppShell's `center` slot. Same metrics as the admin's
   .gemboss-command-search (gemboss-shell.css): 36px under a mouse, 44px under a finger (pinned in the admin's
   kit-controls-reach-44.test.ts), 18px icon, 13px text, the shortcut hint at the right. Field tokens, so it
   wears the same box as every other text field in the kit. */
.gemboss-topbar-search {
  width: 100%;
  max-width: 560px;
  height: 36px;
  min-width: 0;
  margin: 0 auto;
  border: 1px solid var(--gemboss-field-line);
  border-radius: var(--gemboss-radius-md);
  background: var(--gemboss-field-bg);
  padding: 0 8px 0 12px;
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr) auto;
  gap: 8px;
  align-items: center;
  color: var(--gemboss-text-muted);
}
.gemboss-topbar-search:hover { border-color: var(--gemboss-field-line-hover); background: var(--gemboss-field-bg-hover); }
.gemboss-topbar-search:focus-within { border-color: var(--gemboss-field-line-active); background: var(--gemboss-surface); }
.gemboss-topbar-search-icon { width: 18px; height: 18px; color: currentColor; }
/* The INPUT is the tap target, not the box: the box is a <form>, and a tap on its icon or border does not focus
   anything. Inside a 1px border, `height: 100%` left the input 42 of the box's 44 (visual-gate tap_target_size at
   320/390/768 on GemFactory, 03/10/2026). It reaches over both border lines; transparent, so the border still shows. */
.gemboss-topbar-search-input {
  min-width: 0; height: calc(100% + 2px); border: 0; background: transparent; padding: 0; margin: -1px 0;
  font: inherit; font-size: var(--gemboss-fs-body); line-height: 18px; color: var(--gemboss-field-ink); outline: none;
}
.gemboss-topbar-search-input::placeholder { color: var(--gemboss-field-placeholder); opacity: 1; }
.gemboss-topbar-search-kbd {
  border: 1px solid var(--gemboss-field-line); border-radius: var(--gemboss-radius-sm); background: var(--gemboss-surface);
  padding: 1px 6px; color: var(--gemboss-text-muted); font-family: inherit; font-size: var(--gemboss-fs-micro); line-height: 16px;
}
.gemboss-topbar-search:focus-within .gemboss-topbar-search-kbd { display: none; }
/* 44 under a finger AND at phone width (0.6.4, 03/10/2026): the same pair of conditions the kit's own controls use
   (kit-plain.css, `max-width: 47.9975em`). Under `pointer: coarse` alone, a narrow desktop window and every visual-gate
   run measured the box at 36 and failed `tap_target_size` at 320/390/768 on GemFactory /framework. */
@media (pointer: coarse), (max-width: 47.9975em) { .gemboss-topbar-search { height: 44px; } }
/* A phone has no ⌘ key, and the box needs every pixel the bar has left. */
@media (max-width: 460px) { .gemboss-topbar-search-kbd { display: none; } }

/* ── Page width (0.8.1) ──────────────────────────────────────────────────────────────────────────────
 * Chris 04/10/2026: the rail on the left, and on the right a content column with ONE max width across every
 * service, not edge to edge. The admin's tiers (gemboss-shell.css, `.gemboss-w-*` around a Polaris Page), same
 * numbers; `page-width.test.tsx` fails if the two disagree:
 *   .ui-page          → --ui-content-max (900px, ui.css): lists, tables, card grids, dashboards
 *   .ui-page--feed    → 720px: read top to bottom (activity, logs, timelines)
 *   .ui-page--form    → 662px: one column of fields (Polaris narrowWidth)
 *   .ui-page--canvas  → 1440px: content measured to be wider than a reading column (admin: the funnel path)
 *   .ui-page--full    → no cap: a board or an editor that is the screen. Say why where you use it.
 * A cap only exists where it is SMALLER than the room it sits in. Right of the 240px rail with main's 24px padding,
 * 900 bites from 1188px up (1280 → 992 of room); 1440 bites only from ~1730px. Below that the column is simply the
 * room, so the tier never causes a horizontal scroll.
 * Here, not in ui.css: ui.css is exported verbatim into the admin under a 90 KB budget (89.8 KB at 0.7.3), and the
 * admin draws its tiers on Polaris' Page instead. Fixed px, not %: a % cap grows the line length with the monitor. */
.ui-page {
  box-sizing: border-box;
  width: 100%;
  min-width: 0;
  max-width: var(--ui-content-max, 900px);
  margin-inline: auto;
}
.ui-page--feed { max-width: 720px; }
.ui-page--form { max-width: 662px; }
.ui-page--canvas { max-width: 1440px; }
.ui-page--full { max-width: none; }

/* ═══ @gemboss/ui src/auth.css ═══ */
/* The GemBoss sign-in screen — @gemboss/ui (28/09/2026).
 *
 * Moved here VERBATIM from the admin's styled-jsx block (apps/admin/src/app/login/LoginClient.tsx),
 * which was the original every other sign-in copied by hand: gemcommunity's .auth-wrap,
 * content-radar's and gemwatcher's login.css, gemcare's inbox, partner-radar. Class names took a
 * `gb-auth` prefix (gemcare and gempress already have a `.login-card` of their own); the panel's
 * colours became tokens; nothing else changed. The admin's /login was screenshot at nine sizes in
 * both themes before and after the move and compared pixel for pixel.
 *
 * Rendered by AuthLayout / AuthForm / PasswordField (auth.tsx). What only the admin has — the Google
 * button, the "or" divider — stays in the admin.
 */

/* dvh, not vh: on a phone 100vh is the height with the URL bar HIDDEN, so a centred card sat
   under the toolbar. The card centres with margin:auto rather than align-items:center — auto
   margins collapse to 0 when the content is taller than the screen, so the top of the card
   can never be pushed above the fold (short landscape phone, keyboard open). */
/* FLUID, not stepped. Every vertical gap is clamp(compact, N cqh, roomy): N is picked so the roomy
   value lands at ~800px of height, and the compact floor at ~400px. Between the two the page
   breathes in proportion to the screen instead of jumping at a breakpoint — the stepped version
   snapped its card from 379px to 469px between 640 and 680 tall, and still overflowed below
   400. Controls never scale: inputs and buttons keep min-height 44px at every size, only the
   AIR between them gives. cqh is the SHELL's height, which is the visible height — URL bar and
   on-screen keyboard included (see --vvh above the return). */
/* The shell is exactly the visible height and is the size container every rule below measures
   (cqh, @container gb-auth). It scrolls itself if even the shed layout does not fit (< ~260px). */
/* ONE PALETTE, TWO REPOS. Every colour on this screen is a --gb-* token, and gemcommunity's
   sign-in (app/globals.css, the block scoped to .auth-wrap) declares the SAME names with the
   SAME values. That is what keeps the two sign-in screens identical: a colour changed in one
   place is a colour that can be changed in the other, by name, instead of two piles of hex
   drifting apart — which is what they had been doing (card 380 vs 400, radius 14 vs 12,
   wordmark 22 vs 24, and this screen with no dark mode at all while that one had it).
   Dark follows the OS. There is no toggle here, unlike gemcommunity: that app persists a
   data-theme the admin has no reader for, and a switch that only half-works is worse than
   none. The [data-theme] hook is still honoured so a person who set it over there, in the
   same browser, is not thrown back to white when they land here. */
.gb-auth {
  --gb-surface: #ffffff;
  /* The admin's own near-black, by name — the colour guard asks for the token and it is the
     right answer here anyway: light mode IS the admin's palette, and only the dark block
     below has values the admin has no token for. */
  --gb-ink: var(--gemboss-text-strong, #1b1c1e);
  --gb-muted: #6d7175;
  --gb-line: #e1e3e5;
  --gb-field-line: #c9cccf;
  --gb-field-bg: #ffffff;
  --gb-field-hover: #fafafa;
  --gb-placeholder: #868b94;
  --gb-accent: #5661f5;
  --gb-btn: linear-gradient(120deg, #1b1c1e 0%, #000000 100%);
  --gb-btn-ink: #ffffff;
  --gb-wordmark: linear-gradient(120deg, #1b1c1e 0%, #000000 100%);
  --gb-hover: #f7f7f5;
  --gb-hover-line: #b9bbc0;
  --gb-err-bg: #fee2e2;
  --gb-err-ink: #b91c1c;
  --gb-err-line: transparent;
  --gb-card-shadow: 0 24px 64px rgba(15, 23, 42, 0.08);
  --gb-mark-bg: #0b0b0c;
  /* The brand's own facet geometry as a texture for the panel, rather than a stock diagonal
     hatch: it is the same path the mark draws, blown up and held at 7% opacity. Inline so it
     costs no request on the screen that must render before anything else exists. */
  --gb-facet: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 120'%3E%3Cg fill='none' stroke='%23ffffff' stroke-width='0.45' stroke-linejoin='round'%3E%3Cpath d='M26,14 74,14 96,50 50,110 4,50Z'/%3E%3Cpath d='M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110'/%3E%3C/g%3E%3C/svg%3E");
  /* The composition is the gemboss one and stays the gemboss one in dark: ink wash top-left,
     iris wash bottom-right, flat paper underneath. Only the three colours flip. */
  --gb-page:
    radial-gradient(1200px 600px at 20% 10%, rgba(27, 28, 30, 0.05), transparent 60%),
    radial-gradient(1000px 500px at 80% 90%, rgba(86, 97, 245, 0.06), transparent 60%),
    #fafafb;
  --gb-page-flat: #ffffff;
  /* The brand panel, as tokens (28/09/2026, @gemboss/ui): the admin's is INK with iris used
     sparingly, because its mark is an outline on ink. A surface whose mark is filled with iris
     (gemcommunity, content-radar) redefines these five on its own root; nothing else changes. */
  --gb-panel-bg:
    radial-gradient(900px 520px at 78% 12%, rgba(86, 97, 245, 0.26), transparent 62%),
    radial-gradient(700px 480px at 12% 96%, rgba(86, 97, 245, 0.12), transparent 60%),
    #0b0b0c;
  --gb-panel-ink: #f4f4f2;
  --gb-panel-muted: rgba(244, 244, 242, 0.72);
  --gb-panel-eyebrow: #8a93ff;
  --gb-panel-facet-o: 0.05;
  color-scheme: light dark;
  position: fixed;
  top: var(--vvt, 0px);
  left: 0;
  right: 0;
  height: 100vh;
  height: var(--vvh, 100dvh);
  container: gb-auth / size;
  overflow-y: auto;
  /* The recolor swapped the lower wash to iris but missed this one, which was
     rgba(194,65,12,.08) — retired coral — so the sign-in screen kept a peach cast in the
     top-left corner. Ink for depth, iris for the single accent: B&W ground, one colour. */
  background: var(--gb-page);
  color: var(--gb-ink);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
/* ONE COLUMN BY DEFAULT. The split is opt-IN, at a size where a second column is a gift
   rather than a squeeze — and the form column keeps every clamp and shed step it had, so
   nothing about the phone or the short-landscape case changes. */
.gb-auth-split {
  display: grid;
  grid-template-columns: 1fr;
  min-height: 100%;
}
.gb-auth-stage {
  box-sizing: border-box;
  display: flex;
  min-height: 100%;
  padding: clamp(12px, 3cqh, 24px) clamp(16px, 4vw, 24px);
}
.gb-auth-panel-in {
  position: relative;
  box-sizing: border-box;
  width: 100%;
  max-width: 520px;
  padding: 48px clamp(32px, 5vw, 64px);
}
/* The eyebrow is the one place a monospace face earns its keep here: it labels the panel
   without competing with the headline, and the letterspacing does the work the size would
   otherwise have to. */
.gb-auth-eyebrow {
  margin: 0 0 20px;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--gb-panel-eyebrow);
}
.gb-auth-panel-h {
  margin: 0 0 16px;
  font-size: clamp(28px, 3.2vw, 40px);
  line-height: 1.15;
  letter-spacing: -0.02em;
  font-weight: 700;
  color: var(--gb-panel-ink);
}
.gb-auth-panel-p {
  /* This used to be 0 0 32px, the gap down to the CTA. The button is gone, so that 32px was
     dangling off the panel's last line with nothing to balance it. */
  margin: 0;
  font-size: 15px;
  line-height: 1.6;
  /* 0.72 of #f4f4f2 on #0b0b0c measures 8.9:1 — well past AA, and quiet enough that the
     headline still leads. */
  color: var(--gb-panel-muted);
}
/* THE PANEL IS OFF UNTIL EARNED. Hidden by default rather than shown-then-hidden: a panel
   that paints first and disappears at a breakpoint is a panel that flashes on every phone. */
.gb-auth-panel {
  display: none;
}
/* Wide AND tall. Width alone is not enough — a 1440x420 window is a wide letterbox, and a
   column of headline plus button in 420px of height is the crush this whole screen spent its
   shed ladder avoiding. 900 wide is where 380px of card plus a readable second column stop
   fighting.
   520 tall, not 620 (26/09/2026): a 1366x768 laptop with a bookmarks bar leaves under 620px
   of viewport, so the most common laptop never saw this column. Its content measures 286px
   padding included, so 520 still leaves over 200px spare. The form sheds on height whether
   or not the panel is there, so the panel costs it nothing. Same number as the gemcommunity
   sign-in (app/globals.css); change both or neither. */
@container gb-auth (min-width: 900px) and (min-height: 520px) {
  .gb-auth-split {
    grid-template-columns: minmax(420px, 1fr) minmax(0, 1.05fr);
  }
  /* The column IS the structure now, so the card stops drawing a second one. A border and
     a 64px shadow around a form that already sits in its own half of the screen is chrome
     about chrome. The card keeps every bit of this below 900px, where it is the only thing
     giving the form an edge. */
  /* SCOPED UNDER .gb-auth-split ON PURPOSE — (0,2,0) against the base rules' (0,1,0), so this
     wins wherever it is written. The first version of these two relied on coming LATER in
     the sheet than the card's own rule, and it did not: a container query carries the specificity
     of the selectors inside it, nothing more, and the tie went to the rule written first.
     Measured then: card still 380 wide with its border and 64px shadow intact. */
  .gb-auth-split .gb-auth-stage {
    background: var(--gb-surface);
  }
  .gb-auth-split .gb-auth-card {
    border: none;
    box-shadow: none;
    max-width: 400px;
  }
  .gb-auth-panel {
    display: flex;
    align-items: center;
    position: relative;
    overflow: hidden;
    /* INK-DOMINANT, because the gemboss mark is an OUTLINE on ink. Iris appears twice on
       this panel and both times it is small: the eyebrow and the button. */
    background: var(--gb-panel-bg);
    color: var(--gb-panel-ink);
  }
  /* The facet lines of the mark, blown up and laid over the ground — the brand's own geometry
     instead of a stock diagonal hatch. Tiny opacity on purpose: at 1920 it should read as
     texture you notice second, not a pattern you notice first. */
  .gb-auth-panel::before {
    content: "";
    position: absolute;
    /* Deliberately OVERSIZED and pushed off two edges, so what lands on the panel is a few
       facet lines crossing it — texture. Sized to fit, it read as a big grey triangle in the
       corner: a picture of a logo rather than a ground for one. */
    inset: -45% -38% auto auto;
    width: 132%;
    aspect-ratio: 1;
    background-image: var(--gb-facet);
    background-size: contain;
    background-repeat: no-repeat;
    opacity: var(--gb-panel-facet-o);
    pointer-events: none;
  }
}
.gb-auth-card {
  width: 100%;
  max-width: 380px;
  margin: auto;
  background: var(--gb-surface);
  border: 1px solid var(--gb-line);
  border-radius: 14px;
  padding: clamp(18px, 3.5cqh, 28px) 28px;
  box-shadow: var(--gb-card-shadow);
}
.gb-auth-header {
  text-align: center;
  margin-bottom: clamp(12px, 3cqh, 24px);
}
/* The header gives up height LAST, with no layout switch: the tagline goes in ONE step under
   600px of height, then the mark shrinks from 56px at 560 to 32px at 480.
   The tagline used to fold CONTINUOUSLY (max-height ramping from 20px at 600 to 0 at 560), which
   meant every height in that band showed the line cut through the middle: 4px of a 16px line at
   320x568, the iPhone SE size, flagged text_clipped by scripts/visual-gate on 28/09/2026. A line of
   text has no useful partial state, so it is shown whole or not at all (content-radar's copy of this
   screen had already made the same fix, 27/09/2026). The mark CAN shrink, so it still does.
   A mark has a floor: shrunk continuously to zero it passed through an 11px smudge that read as
   a rendering bug (screenshot at 390x480). So under 480 it goes in one step (~38px) and a
   landscape phone gets the wordmark and the form. The old breakpoint moved the mark beside the
   name and made the card jump 60px between two heights 40px apart. */
.gb-auth-mark {
  display: inline-flex;
  margin-bottom: clamp(6px, calc(6px + (100cqh - 480px) * 0.075), 12px);
}
/* `img` too (29/09/2026): a surface whose CSP forbids inline `style` (gemcare: `style-src 'self'`)
   cannot use GemMark, which paints its tile with style="fill:var(--gb-mark-bg)". It passes its
   vendored gemboss-mark.svg as an <img> through `mark`, and that must shrink on a short screen too. */
.gb-auth-mark svg,
.gb-auth-mark img {
  width: clamp(32px, calc(32px + (100cqh - 480px) * 0.3), 56px);
  height: clamp(32px, calc(32px + (100cqh - 480px) * 0.3), 56px);
}
@container gb-auth (max-height: 479.98px) {
  .gb-auth-mark {
    display: none;
  }
}
.gb-auth-header h1 {
  font-size: clamp(19px, 2.75cqh, 22px);
  font-weight: 700;
  margin: 0 0 clamp(0px, calc((100cqh - 560px) * 0.1), 4px);
  background: var(--gb-wordmark);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.gb-auth-header p {
  margin: 0;
  font-size: 13px;
  color: var(--gb-muted);
}
/* Out of sight, not out of the page: the same visually-hidden pattern the header uses under 400,
   so a screen reader still hears the tagline, as it did when the fold took it to 0px. */
@container gb-auth (max-height: 599.98px) {
  .gb-auth-header p {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
}
.gb-auth-form {
  display: flex;
  flex-direction: column;
  gap: clamp(8px, 1.5cqh, 12px);
}
.gb-auth-form label {
  display: flex;
  flex-direction: column;
  gap: clamp(4px, 0.75cqh, 6px);
  font-size: 12px;
  font-weight: 500;
  color: var(--gb-ink);
}
/* 38px → 44px. §9.1 wants a CONTROL at 44 (Apple HIG / WCAG 2.5.5 AAA); measured on the
   live page 2026-09-09 these were 38, the sign-in button 40, the Google button 42, the
   password toggle 13 and the Google connect link 15. All five were pre-existing, on the
   first screen a merchant sees — and now the screen the MCP connect flow sends strangers to,
   which is why they are fixed here rather than filed. min-height, not height, so a
   browser that needs more room for the text still gets it. */
/* THE FIELD'S SHAPE. In the admin it is the shared `gemboss-field gemboss-field--tall` class
   (packages/ui/src/gemboss-field.css), which the admin's inputs keep wearing. Every other
   surface has no such sheet, so `PasswordField`/`AuthField` default to `gb-auth-input`: the same
   numbers, resolved from that file's Polaris fallbacks — 32px box raised to a 44px floor, 12px
   side padding, 13px on 20px, radius 8, 1px border; 16px text under 768px (iOS zooms any field
   under 16px on focus and does not zoom back); height:auto under a coarse pointer. */
.gb-auth-input {
  box-sizing: border-box; height: 32px; min-height: 44px; min-width: 0; padding: 0 12px;
  font: inherit; font-size: 13px; line-height: 20px;
  border: 1px solid; border-radius: 8px;
}
@media (max-width: 47.9975em) { .gb-auth-input { font-size: 16px; } }
@media (pointer: coarse) { .gb-auth-input { height: auto; } }
/* Colour only, over whichever class drew the shape. */
.gb-auth-form input {
  width: 100%;
  box-sizing: border-box;
  background-color: var(--gb-field-bg);
  border-color: var(--gb-field-line);
  color: var(--gb-ink);
}
.gb-auth-form input::placeholder {
  color: var(--gb-placeholder);
}
/* HOVER AND FOCUS HAVE TO BE RESTATED HERE, and finding that out is the reason this block
   exists. gemboss-field.css owns them, at (0,3,0) for the hover; the rule above out-specifies
   the class — (0,3,1) when it lived in the admin's styled-jsx, (0,1,1) here, still above
   (0,1,0) — so it took the hover background with it and the field stopped reacting to the
   pointer (measured then: rest and hover identical in both themes). A token that wins the
   cascade silently takes the states with it.
   The focus ring is restated for a different reason: gemboss-field.css paints it Polaris
   blue (#005bd3), which is not a colour this screen uses anywhere. This screen is B&W plus
   iris, deliberately outside Polaris, and its own password toggle already rings in near-
   black. Ink, in both themes, and the same ink gemcommunity's sign-in rings in. */
.gb-auth-form input:hover:not(:disabled) {
  background-color: var(--gb-field-hover);
  border-color: var(--gb-hover-line);
}
.gb-auth-form input:focus-visible {
  outline: 2px solid var(--gb-ink);
  outline-offset: 1px;
  border-color: var(--gb-ink);
}
/* Same shape as a .gb-auth-form label, for the one field whose input had to leave the label.
   Declared rather than reused so a change to labels cannot silently un-match this one. */
.gb-auth-field {
  display: flex;
  flex-direction: column;
  gap: clamp(4px, 0.75cqh, 6px);
  font-size: 12px;
  font-weight: 500;
  color: var(--gb-ink);
}
.gb-auth-pw {
  position: relative;
  display: flex;
}
.gb-auth-pw input {
  width: 100%;
  /* Room for the button. Without it the last characters of a revealed password sit UNDER the
     eye — which is only visible in the one state the button exists to produce. */
  padding-right: 48px;
}
.gb-auth-pw-toggle {
  /* min-WIDTH as well as height. #2144 gave this 44px of height and stopped, because the
     script I checked it with only read heights — the repo's own gate reads both and caught
     it at 41.22px wide, one check away from shipping "fixed" twice. */
  min-height: 44px;
  min-width: 44px;
  position: absolute;
  right: 0;
  top: 0;
  bottom: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  margin: 0;
  background: none;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  color: var(--gb-muted);
  transition: none;
}
.gb-auth-pw-toggle:hover {
  color: var(--gb-ink);
}
/* focus-visible, not focus: a mouse click on the eye should not leave a ring sitting inside
   the field. §9.2 wants the state to exist, not to fire for pointer users. */
.gb-auth-pw-toggle:focus-visible {
  outline: 2px solid var(--gemboss-primary, #1b1c1e);
  outline-offset: -2px;
}
.gb-auth-error {
  background: var(--gb-err-bg);
  border: 1px solid var(--gb-err-line);
  color: var(--gb-err-ink);
  padding: 8px 10px;
  font-size: 13px;
  border-radius: 6px;
}
/* Scoped to the submit button BY ITS OWN CLASS, NOT a bare "button". This rule paints a filled
   black pill, and as ".login-form button" it was specificity 0,1,1 against ".pw-toggle" at 0,1,0 —
   so the eye INSIDE the password field was painted as a second Sign-in button: black
   gradient, white icon, and a 6px margin-top that pushed it out of the input. Measured in a
   production build at all six widths (SS9.1): backgroundImage=GRADIENT,
   color=rgb(255,255,255), marginTop=6px.
   Second time in this repo — composer-owns-only-its-own-buttons.test.ts bans the same shape
   in the chat composer. SS9.3: one visual owner, and the owner names its own element. */
.gb-auth-form > .gb-auth-submit {
  margin-top: clamp(2px, 0.75cqh, 6px);
  min-height: 44px;
  font-size: 14px;
  font-weight: 600;
  border: none;
  border-radius: 8px;
  background: var(--gb-btn);
  color: var(--gb-btn-ink);
  cursor: pointer;
  font-family: inherit;
  transition: opacity 0.15s ease, transform 0.15s ease;
}
.gb-auth-form > .gb-auth-submit:hover:not(:disabled) {
  transform: translateY(-1px);
}
.gb-auth-form > .gb-auth-submit:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
/* The signup line. 24px of target, not 44: WCAG 2.5.8 is the threshold that has a standard
   behind it for a LINK in a line of text, and inline-block is what makes the padding a target
   at all — on an inline element it paints and measures nothing. */
.gb-auth-foot {
  margin: clamp(10px, 2cqh, 16px) 0 0;
  font-size: 13px;
  color: var(--gb-muted);
  text-align: center;
  line-height: 1.5;
}
.gb-auth-foot a {
  display: inline-block;
  min-height: 24px;
  line-height: 24px;
  padding: 0 2px;
  color: var(--gb-accent);
  font-weight: 600;
  text-decoration: none;
}
.gb-auth-foot a:hover,
.gb-auth-foot a:focus-visible {
  text-decoration: underline;
}
.gb-auth-card .gb-auth-help {
  margin: clamp(8px, 1.75cqh, 14px) 0 0;
  font-size: 12px;
  color: var(--gb-muted);
  text-align: center;
  line-height: 1.5;
}
/* The "trouble getting in?" line. min-height 24px: WCAG 2.5.8 for a link. */
.gb-auth-card .gb-auth-help a {
  display: inline-block;
  min-height: 24px;
  line-height: 24px;
  /* The page's link colour, not the grey of the text around it (#3129): the reader of this line
     is stuck and scanning for a way out. #5661f5 on white is 4.75:1, above the 4.5:1 12px needs. */
  color: var(--gb-accent);
  font-weight: 600;
  text-decoration: none;
}
.gb-auth-card .gb-auth-help a:hover,
.gb-auth-card .gb-auth-help a:focus-visible {
  text-decoration: underline;
}
/* PHONE: the card IS the page. A floating card inside a 16px gutter spends ~90px of a 320px
   screen on border, shadow and double padding for nothing — the pattern every major sign-in
   page (Google, Apple, Shopify, Stripe) uses below tablet width is a full-bleed white sheet.
   Inputs go to 16px because iOS Safari zooms the page on focus into any field under 16px,
   and the zoom does not undo itself after sign-in. Safe-area padding keeps the form out of
   the notch/home indicator in the desktop-app and standalone webviews. LAST in the sheet on
   purpose: a short phone matches the max-height block too, and this one must win the padding. */
@media (max-width: 480px) {
  .gb-auth {
    background: var(--gb-page-flat);
  }
  .gb-auth-stage {
    padding: max(clamp(10px, 3cqh, 20px), env(safe-area-inset-top)) max(16px, env(safe-area-inset-right))
      max(clamp(10px, 3cqh, 20px), env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
  }
  .gb-auth-card {
    border: none;
    border-radius: 0;
    box-shadow: none;
    padding: 0;
  }
  /* (no font-size here: the field's shape class — gemboss-field in the admin, gb-auth-input
     elsewhere — reads 16px under this same breakpoint, which is what stops iOS zooming.) */
}
/* ALWAYS INSIDE THE VIEWPORT. Once the clamps above are at their floors, the only height left
   is in what can go. Shed in order of what a person trying to sign in needs least, each step
   at the height where the one before stopped fitting (measured, 390 and 1280 wide):
     < 440  the "or" divider — the Google button is self-explanatory without it
     < 400  the GemBoss wordmark — kept for screen readers, it is still the page heading
     < 360  the signup and help lines, and the desktop card trims its padding
   Never shed: email, password, Sign in, Google. Those are the page. The signup line survives
   to 360 rather than going with the wordmark at 400: a stranger with no account is exactly
   who this screen strands, and below 360 there is a scrollbar to reach it with. LAST in the sheet so
   it wins over the phone block above. */
@container gb-auth (max-height: 399.98px) {
  .gb-auth-header {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
}
@container gb-auth (max-height: 359.98px) {
  .gb-auth-card .gb-auth-help,
  .gb-auth-foot {
    display: none;
  }
  .gb-auth-stage {
    padding-top: max(8px, env(safe-area-inset-top));
    padding-bottom: max(8px, env(safe-area-inset-bottom));
  }
}
/* The card only has padding to trim where it IS a card; on a phone it is already 0. */
@container gb-auth (max-height: 359.98px) and (min-width: 481px) {
  .gb-auth-card {
    padding-top: 14px;
    padding-bottom: 14px;
  }
}
/* DARK — a pure token flip. Not one rule above changes; every colour on this screen reads a
   --gb-* token, so the whole screen inverts by redefining eighteen values here. The same
   eighteen, with the same values, are what gemcommunity's .auth-wrap block declares.
   Two selectors on purpose: the OS ask, ignored when a data-theme of light is set, and an
   explicit data-theme of dark. LAST in the sheet so it beats the phone block, which sets a
   flat page colour of its own.
   When this sheet lived in the admin's styled-jsx, the root part needed :global() — styled-jsx
   glued its scope class onto ":root", no html element carries one, and the whole dark block
   matched nothing (measured 23/09/2026: --gb-surface still #ffffff on a dark-OS browser, with
   typecheck and the text-reading tests all green). As a plain stylesheet it needs nothing; the
   dark screenshots taken for the move are what show it still applies.
   WHAT IT IS NOT: a dark mode for the admin. Nothing behind /login has one yet, so signing in
   moves a dark-OS person to a light app. That is the same handoff gemcommunity has shipped
   since its auth skin landed, and the alternative was leaving this screen the one white
   rectangle on a dark machine. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .gb-auth {
    --gb-surface: #141517;
    --gb-ink: #f4f4f3;
    --gb-muted: #969c97;
    --gb-line: rgba(255, 255, 255, 0.11);
    --gb-field-line: rgba(255, 255, 255, 0.17);
    --gb-field-bg: #1b1c1f;
    --gb-field-hover: #212226;
    --gb-placeholder: #7f838c;
    --gb-accent: #8a93ff;
    --gb-btn: linear-gradient(120deg, #f4f4f3 0%, #dfe0e4 100%);
    --gb-btn-ink: #0b0b0c;
    --gb-wordmark: linear-gradient(120deg, #f4f4f3 0%, #c9cbd2 100%);
    --gb-hover: rgba(255, 255, 255, 0.06);
    --gb-hover-line: rgba(255, 255, 255, 0.28);
    --gb-err-bg: rgba(215, 44, 13, 0.16);
    --gb-err-ink: #ff9d88;
    --gb-err-line: rgba(245, 120, 95, 0.4);
    --gb-card-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
    --gb-mark-bg: #202127;
    --gb-page:
      radial-gradient(1200px 600px at 20% 10%, rgba(255, 255, 255, 0.05), transparent 60%),
      radial-gradient(1000px 500px at 80% 90%, rgba(138, 147, 255, 0.1), transparent 60%),
      #0b0b0c;
    --gb-page-flat: #0b0b0c;
  }
}
:root[data-theme="dark"] .gb-auth {
  --gb-surface: #141517;
  --gb-ink: #f4f4f3;
  --gb-muted: #969c97;
  --gb-line: rgba(255, 255, 255, 0.11);
  --gb-field-line: rgba(255, 255, 255, 0.17);
  --gb-field-bg: #1b1c1f;
  --gb-field-hover: #212226;
  --gb-placeholder: #7f838c;
  --gb-accent: #8a93ff;
  --gb-btn: linear-gradient(120deg, #f4f4f3 0%, #dfe0e4 100%);
  --gb-btn-ink: #0b0b0c;
  --gb-wordmark: linear-gradient(120deg, #f4f4f3 0%, #c9cbd2 100%);
  --gb-hover: rgba(255, 255, 255, 0.06);
  --gb-hover-line: rgba(255, 255, 255, 0.28);
  --gb-err-bg: rgba(215, 44, 13, 0.16);
  --gb-err-ink: #ff9d88;
  --gb-err-line: rgba(245, 120, 95, 0.4);
  --gb-card-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
  --gb-mark-bg: #202127;
  --gb-page:
    radial-gradient(1200px 600px at 20% 10%, rgba(255, 255, 255, 0.05), transparent 60%),
    radial-gradient(1000px 500px at 80% 90%, rgba(138, 147, 255, 0.1), transparent 60%),
    #0b0b0c;
  --gb-page-flat: #0b0b0c;
}

/* ═══ @gemboss/ui src/assistant.css ═══ */
/* The assistant's popup / status bar / full-screen host — @gemboss/ui (28/09/2026).
 *
 * Moved from gemboss apps/admin/src/components/assistant/AssistantDockHost.module.css, where it was
 * written for the admin's assistant; gemwatcher's and content-radar's assistants now wear the same
 * host. Class names took a `gb-dock` prefix instead of CSS-module hashes (a consumer without CSS
 * modules must be able to load it); every value is unchanged. The geometry assumes the GemBoss shell
 * (@gemboss/ui AppShell): 56px topbar, 240px rail, drawer below 760px.
 *
 * Rendered by AssistantHost (assistant/host.tsx). State and the rules for when it shows are
 * assistant/dock.ts + assistant/provider.tsx.
 */


.gb-dock {
  position: fixed;
  z-index: 320;              /* above the mobile tab bar (300), below the page editor (400) */
  right: 24px;
  bottom: 0;
  /* MEASURED 2026-09-12, not assumed: the rail is 240px at every width above 760 (the shell's own
     `margin-left: 240px`), and there is NO bottom tab bar — `.gemboss-mobile-nav` is not in the DOM
     at 320 or 390. An earlier pass here wrote 72px and 56px from a stale comment in AppShell and
     the popup covered a third of the rail, swallowing the menu clicks this feature exists to free.
     240 (rail) + 24 (right gutter) + 24 (left gap) = 288. */
  /* 460 is the app's own modal width (`.ui-modal`), so the assistant reads as part of the same
     family instead of a wider one-off. 600 was a guess; this one already exists. */
  width: min(460px, calc(100vw - 288px));
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* Frosted, the way the settings dialog reads — translucent surface over a small blur, so the page
     underneath shows through as texture rather than detail.
     NOT a full-screen scrim, though that is what settings uses. A scrim would block the page, and a
     popup you cannot work behind is just the assistant page in a smaller box: the whole point of
     this surface is that the screen under it stays usable. */
  /* 93%, AND THE 5 POINTS THIS GAINED ON 25/09/2026 ARE A CONTRAST MEASUREMENT.
     A translucent surface is `a·white + (1−a)·backdrop`, so the darkest it can ever go is `a·white`.
     At 88% that floor was #E0E0E0, and `--gemboss-text-muted` (#686b71) measures 4.05:1 there —
     UNDER AA. That is not a hypothetical corner of this component: `.header` and `.miniBar` both
     declare `background: transparent` on purpose (see the note beside them), so `.headerStatus`
     12px, `.miniStatus` 12px and `.booting` 14px all sit DIRECTLY on this surface in that colour.
     93% puts the floor at #EDEDED and muted at 4.56:1. Chosen with, and kept equal to,
     `.gemboss-popover-surface` in app/gemboss-shell.css — `one-popover-surface-four-wearers.test.ts`
     fails if the two numbers ever stop agreeing. Change them THERE too.

     TWO BACKGROUNDS, and the first is not decoration. `color-mix` and `backdrop-filter` ship on
     DIFFERENT schedules — Firefox 103–112 and Safari 15.4–16.1 have the blur and not the mix — and
     a declaration the parser rejects is DROPPED, not defaulted. Without the plain line, the dock had
     NO background at all in those browsers: the page running straight through the chat. It shipped
     that way on 12/09/2026 and nobody could see it from here. NOT a custom property: `background:
     var(--some-mix-token)` would fail at computed-value time instead, which falls back to `unset`
     rather than to the line above — worse than the bug it would be trying to share. */
  background: var(--gemboss-surface);
  background: color-mix(in srgb, var(--gemboss-surface) 93%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.4);
  backdrop-filter: blur(14px) saturate(1.4);
  border: 1px solid var(--gemboss-divider);
  border-bottom: 0;
  border-radius: 12px 12px 0 0;
  box-shadow: 0 -2px 6px rgba(15, 23, 42, 0.05), 0 24px 64px rgba(15, 23, 42, 0.18);
  overflow: hidden;

  /* ── Arriving ────────────────────────────────────────────────────────────────────────────────
     Short and plain: 140ms, ease-out, a small rise and a fade. Long enough to read as "it came
     from somewhere", short enough that nobody waits for it — a popup a seller opens twenty times
     a day must not perform.

     TRANSFORM AND OPACITY ONLY. Height and horizontal offset are set from a pointer, and a
     transition on those means the box trails the finger instead of following it. Those two stay
     instant by construction, not by luck. */
  transition: transform 140ms cubic-bezier(0.2, 0.8, 0.3, 1), opacity 140ms ease-out;
}
/* The entry itself. `@starting-style` is what lets an element animate the frame it appears in
   (Chrome 117+/Safari 17.5+; the desktop app is Chromium 132). Anywhere older simply appears —
   degrading to instant is the right failure for chrome nobody asked to watch. */
@starting-style {
  .gb-dock-dock, .gb-dock-mini { transform: translateY(10px); opacity: 0; }
  .gb-dock-side { transform: translateX(12px); opacity: 0; }
}
/* Nothing animates while a pointer is setting the numbers — see `dragging` in the host. */
.gb-dock-dragging, .gb-dock-dragging * { transition: none !important; }
/* Where `backdrop-filter` is missing there is no blur to smear the page, and 93% opaque over sharp
   body text is a chat you read through. Back to a solid surface — what this dock wore before it was
   frosted. Same reasoning, same shape, as `.gemboss-popover-surface`. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .gb-dock { background: var(--gemboss-surface); }
}
@media (prefers-reduced-motion: reduce) {
  .gb-dock { transition: none; }
}

/* The floating popup IS the base geometry above; this class exists so the DOM names the state —
   `styles[mode]` was `undefined` for it, which is invisible in a screenshot and unnameable in a test. */
.gb-dock-dock { }

/* Still mounted — that is what keeps a run alive — just not on screen. */
.gb-dock-hidden { display: none; }

/* ── Full: exactly the area `main` occupies, so the dedicated page looks untouched ── */
.gb-dock-full {
  /* BELOW `.gemboss-route-shell` (z-index 5), which covers this exact rectangle while the next route
     loads. At the popup's 320 the assistant stayed painted over the holding screen, so leaving this
     page looked like nothing happened — and the screen's click-catcher was defeated, so clicks
     during the wait landed on chat controls. Fixed position still puts it over `main`'s content. */
  z-index: 4;
  top: 56px;
  left: 240px;             /* the rail, measured — see the note on .host */
  right: 0;
  bottom: 0;
  width: auto;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  background: var(--gemboss-app-bg);
}

/* ── Side: a full-height panel pinned to the right edge (01/10/2026) ─────────────────────────────
   The third working shape, beside the popup and full screen: a long conversation next to the screen
   it is about. Opt-in per surface (`<AssistantHost allowSide>`), because unlike the popup it TAKES
   ROOM — the page beside it moves over (rule below), so nothing under it is hidden. That is why the
   surface is solid rather than frosted: nothing sits behind it to show through.
   Above `.gemboss-route-shell` (5), so the panel stays put while routes change — a sidebar that
   vanishes on every click is a popup with extra steps. BELOW the topbar (300): the account menu
   opens down out of the topbar's right end, exactly where this panel starts, and at the popup's 320
   the menu fell behind the panel (review). 290 still clears the desktop invite (260/280). The host
   is rendered inside `main` (AppShell's slot), which works because `main` makes no stacking context
   or containing block; a surface that gives its `main` a transform or z-index must mount it outside. */
.gb-dock-side {
  z-index: 290;
  top: 56px;
  right: 0;
  bottom: 0;
  width: var(--gb-dock-side-w, 400px);
  background: var(--gemboss-surface);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border: 0;
  border-left: 1px solid var(--gemboss-divider);
  border-radius: 0;
  box-shadow: none;
  overflow: visible;         /* the seam's touch band reaches 14px past the left edge */
}
.gb-dock-side .gb-dock-body, .gb-dock-side .gb-dock-pane { overflow: hidden; }
/* The header does not drag a panel that is pinned. */
.gb-dock-side .gb-dock-header { cursor: default; touch-action: auto; }
/* The word for what the dot already says stands down here, as it does in a popup under 420px: the panel has
   five controls in its header, and the title needs the room (measured: "Gem assi…" / "Re…" at 400). Visually
   only — the dot is aria-hidden, so the word is the one place a screen reader hears "Working" (review #3622). */
.gb-dock-side .gb-dock-status {
  flex: 0 0 auto; position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.gb-dock-side .gb-dock-header:active { cursor: default; }
/* The seam: the panel's left edge, the fleet's one `.ui-resizer` (ui.css draws the grip). A 5px strip
   centred on the border, ABOVE the page beside it. */
/* `.gb-dock-side >` is not decoration: `.ui-resizer { position: relative }` (ui.css) has the weight of one
   class too, and which file a bundler loads last would decide whether the seam is a 5px strip or a 0px
   line nobody can grab. */
.gb-dock-side > .gb-dock-sidegrip {
  position: absolute;
  top: 0;
  bottom: 0;
  left: -3px;
  width: 5px;
  z-index: 3;
}
/* The page makes room. `margin-right`, not padding: main's padding is set by four rules in shell.css
   and a fifth here would fight them. The provider puts the class and the width on <body>; the default
   `bodyClassPrefix` is `gemboss-assistant` — a surface with another prefix writes its own copy of this
   one rule. No transition: the seam is set from a pointer, and a page that trails the finger is the
   bug `.gb-dock` already avoids. */
@media (min-width: 1100px) {
  body.gemboss-assistant-side .gemboss-platform-main { margin-right: var(--gb-assistant-side-w, 400px); }
}


/* ── Full: the one control full screen needs — the way back to the popup ──────────────────────
   Floated over the chat's own header, whose right half is empty, rather than given a bar of its
   own: a whole strip of chrome for one button would push the conversation down on every screen. */
.gb-dock-fullchrome {
  position: absolute;
  top: 10px;
  right: 16px;
  z-index: 1;
  display: flex;
  gap: 8px;
}
.gb-dock-shrink {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;          /* §9.1 control tier */
  padding: 0 12px;
  border: 1px solid var(--gemboss-divider);
  border-radius: var(--gemboss-radius-full);
  background: var(--gemboss-surface);
  color: var(--gemboss-text-muted);
  font: inherit;
  font-size: var(--gemboss-fs-caption);
  cursor: pointer;
  white-space: nowrap;
}
.gb-dock-shrink:hover { background: var(--gemboss-hover); color: var(--gemboss-text); }
.gb-dock-shrink:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: 2px; }
.gb-dock-shrink svg { width: 16px; height: 16px; flex: 0 0 auto; }
/* On a phone the label would crowd the chat header; the icon carries it there. */
@media (max-width: 560px) {
  .gb-dock-shrink span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .gb-dock-shrink { padding: 0; width: 44px; justify-content: center; }
  /* Except the way to a person: a head-and-shoulders icon alone does not say "a human answers". */
  .gb-dock-shrink-keep span { position: static; width: auto; height: auto; overflow: visible; clip: auto; }
  .gb-dock-shrink-keep { padding: 0 12px; width: auto; }
  /* Two controls do not fit in the empty half of the chat header at phone width — floated, they sat
     on the title ("Ask th…", measured at 320 and 390). In the flow they cost one short strip here,
     and only here. */
  .gb-dock-fullchrome { position: static; justify-content: flex-end; padding: 8px 12px 0; }
}

/* ── Mini: a status bar, not a window ── */
.gb-dock-mini {
  height: auto;
  width: min(320px, calc(100vw - 288px));
  -webkit-backdrop-filter: blur(14px) saturate(1.4);   /* Safari; the host above carries the rest */
  backdrop-filter: blur(14px) saturate(1.4);
  border-radius: 12px 12px 0 0;
}
.gb-dock-mini .gb-dock-body { display: none; }
.gb-dock-minirow { display: flex; align-items: center; padding-right: 4px; }
.gb-dock-minirow .gb-dock-minibar { flex: 1 1 auto; min-width: 0; }
/* `width: 44px` is a BASIS, not a floor: the default `flex-shrink: 1` let this button give ground to
   the row beside it, so the × measured under 44 at all six widths while the rule above read as done.
   MEASURED 2026-09-12 — the same trap as every other "the declaration is there" bug today. */
.gb-dock-minirow .gb-dock-icon { flex: 0 0 auto; }

.gb-dock-minibar {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-height: 48px;
  padding: 0 12px;
  border: 0;
  background: transparent;
  font: inherit;
  color: var(--gemboss-text-strong);
  cursor: pointer;
  text-align: left;
}
.gb-dock-minibar:hover { background: var(--gemboss-hover); }
.gb-dock-minibar strong { font-size: 13px; font-weight: 600; flex: 0 0 auto; }
.gb-dock-ministatus {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 12px;
  color: var(--gemboss-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gb-dock-minichevron { font-size: 10px; color: var(--gemboss-text-muted); }

/* ── Resize grip (dock only) ── */
.gb-dock-grip {
  /* OVERLAID on the header's top edge, not stacked above it. As its own 14px row it pushed the
     title down and the header read as top-heavy — Chris: "header hơi lệch do padding top hơi nhiều".
     The drag area is unchanged; it simply no longer costs vertical space. */
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 14px;
  z-index: 2;
  display: grid;
  place-items: center;
  cursor: ns-resize;
  touch-action: none;
  background: transparent;
}
.gb-dock-grip > span {
  display: block;
  width: 44px;          /* a mouse target wide enough to find without aiming */
  height: 4px;
  border-radius: 2px;
  background: var(--gemboss-divider);
}
.gb-dock-grip:hover > span, .gb-dock-grip:focus-visible > span { background: var(--gemboss-text-muted); }
.gb-dock-grip:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; }

/* ── Dock header ── */
.gb-dock-header {
  flex: 0 0 auto;
  cursor: grab;              /* the strip itself drags the popup sideways */
  touch-action: none;
  user-select: none;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 4px 0 16px;
  min-height: 48px;
  border-bottom: 1px solid var(--gemboss-divider);
  background: transparent;   /* the frosted host shows through; a second fill would double it */
}
.gb-dock-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--gemboss-text-strong);
  white-space: nowrap;
  /* Let the NAME absorb a squeeze. Adding the ＋ button pushed the header to 290px inside a 277px
     popup at 320 — a nowrap title with no min-width cannot give ground, so the row overflowed
     instead. Measured 2026-09-12. */
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.gb-dock-status {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 12px;
  color: var(--gemboss-text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gb-dock-header:active { cursor: grabbing; }
/* `margin-left: auto`: the status word was the only thing that stretched, so wherever it stands down (a popup under
   420px, the side panel) the controls followed the title instead of the right edge — 350px adrift in a 720px panel
   (review #3622). With the word showing, it still takes the free space first and nothing moves. */
.gb-dock-actions { display: flex; align-items: center; gap: 2px; cursor: default; margin-left: auto; }
/* One ACTION, then the three WINDOW controls — ordered by how much each changes the window: bigger,
   smaller, gone. The gap is what says they are two different kinds of button; without it four icons
   in a row read as one undifferentiated strip, which is what "các icon đang không đồng đều" was
   describing. The 📌 that used to sit in the middle of them is gone entirely — see lib/assistant-dock. */
.gb-dock-action { margin-right: 8px; }

.gb-dock-icon {
  /* 44px control target — Apple HIG / WCAG 2.5.5. The icon inside stays 18px. */
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--gemboss-text-muted);
  cursor: pointer;
  padding: 0;
}
.gb-dock-icon:hover { background: var(--gemboss-hover); color: var(--gemboss-text-strong); }
.gb-dock-icon:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; }
.gb-dock-icon svg { width: 18px; height: 18px; }
.gb-dock-icon-on { color: var(--gemboss-active-fg); background: var(--gemboss-active-bg); }

.gb-dock-dot {
  flex: 0 0 auto;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--gemboss-divider);
}
.gb-dock-dot-busy { background: var(--gemboss-primary); animation: gb-dock-pulse 1.4s ease-in-out infinite; }
.gb-dock-dot-unread { background: var(--gemboss-success-strong); }
@keyframes gb-dock-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) { .gb-dock-dot-busy { animation: none; } }

/* ── Tabs: the assistant, or a person on the Gem team (dock only) ──────────────────────────────
   Their own row under the header rather than inside it: at 320px the header already spends 176px
   on four 44px controls, and two text tabs do not fit in what is left. */
.gb-dock-tabs {
  flex: 0 0 auto;
  display: flex;
  gap: 4px;
  padding: 0 8px;
  border-bottom: 1px solid var(--gemboss-divider);
}
.gb-dock-tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;          /* §9.1 control tier */
  padding: 0 12px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: transparent;
  color: var(--gemboss-text-muted);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  white-space: nowrap;
}
.gb-dock-tab:hover { color: var(--gemboss-text-strong); }
.gb-dock-tab[aria-selected="true"] { color: var(--gemboss-text-strong); font-weight: 600; border-bottom-color: var(--gemboss-primary); }
.gb-dock-tab:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; }
.gb-dock-badge {
  min-width: 18px;
  padding: 0 6px;
  border-radius: var(--gemboss-radius-full);
  background: var(--gemboss-primary);
  color: var(--gemboss-on-primary, #fff);
  font-size: 11px;
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}

/* ── Body: the assistant itself ── */
.gb-dock-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  min-width: 0;
}
.gb-dock-body > * { flex: 1 1 auto; min-width: 0; min-height: 0; }
/* Each tab's pane. A hidden pane stays MOUNTED — `display:none`, never unmount (see the host). */
.gb-dock-pane { display: flex; min-width: 0; min-height: 0; }
.gb-dock-pane > * { flex: 1 1 auto; min-width: 0; min-height: 0; }
/* The package's own workspace (AssistantWorkspaceLayout) sizes itself to the viewport on its own page;
   inside the host it fills the host instead. Moved here from the admin's assistant-dock-admin.css
   (28/09/2026): every surface that puts the layout in the host needs it, or the layout keeps
   `height: calc(100dvh - 56px)` inside a popup a few hundred pixels tall. */
.gb-dock-body .gemboss-assistant-layout,
.gb-dock-body .gemboss-assistant-page { height: auto; min-height: 0; }
.gb-dock-pane[hidden] { display: none; }

.gb-dock-booting {
  flex: 1 1 auto;
  display: grid;
  place-items: center;
  padding: 24px;
  color: var(--gemboss-text-muted);
  font-size: 14px;
}

.gb-dock-sr {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ── Phone: the popup spans the screen above the bottom tab bar; full mode loses the rail gutter ── */
/* ── Narrow: five 44px controls + a title do not fit ────────────────────────────────────────
   MEASURED 2026-09-12 at 320px: the header row wanted 342px inside a 291px popup. 5 × 44 = 220
   leaves 71px for a title and a status line. So the two controls that are about BROWSING WHILE IT
   RUNS — the pin, and the word for what the dot already says — stand down; new chat, full screen,
   minimize and close stay. Dropping a control beats letting the row overflow off the edge. */
@media (max-width: 420px) {
  .gb-dock-status { display: none; }
}

/* ── Phone: the rail is an off-canvas drawer, so both geometries reclaim the full width ── */
@media (max-width: 760px) {
  .gb-dock {
    right: 8px;
    left: 8px;
    width: auto;
    padding-bottom: env(safe-area-inset-bottom);
    border-radius: 12px 12px 0 0;
  }
  .gb-dock-mini { width: auto; }
  .gb-dock-full {
    left: 0;
    right: 0;
    top: 56px;
    bottom: 0;
    border-radius: 0;
  }
  /* No room beside a phone screen: the side panel takes the screen, the way full does, and there is
     nothing to drag. The page is not pushed (the rule above stops at 761). */
  .gb-dock-side {
    left: 0;
    right: 0;
    width: auto;
    border-left: 0;
    border-radius: 0;        /* the phone .gb-dock rule above rounds the top; this panel is the screen */
  }
  .gb-dock-sidegrip { display: none; }
  /* …so the button that pins it there would only say "full screen" a second time. */
  .gb-dock-pinside { display: none; }
}

/* 1100 = rail 240 + the panel's narrowest 380 + the page's narrowest 480 (SIDE_MIN_WIDTH, SIDE_PAGE_MIN in
   assistant/dock.ts). Below it the two do not fit side by side — MEASURED at 768: pushed, the page kept
   120px and its table ran under the panel. So between the phone and 1100 the panel covers the content
   area the way full screen does, nothing is pushed, there is no seam to drag, and the popup does not
   offer to pin.
   AT THE END OF THE FILE on purpose: `.gb-dock-icon { display: grid }` comes later than the side
   section and has the same weight, so placed there this block lost and the pin still showed at 768
   (measured). */
@media (min-width: 761px) and (max-width: 1099.98px) {
  .gb-dock-side { left: 240px; width: auto; }
  .gb-dock-side .gb-dock-sidegrip, .gb-dock-pinside { display: none; }
}

/* ── AssistantLauncher: the button that opens the assistant. NOT floating (03/10/2026) ────────────────────────────────
   0.4.2 floated it bottom-right (z 280). visual-gate on partner-radar prod, 03/10: `content_covered` red at 5 of 6 widths
   on every page measured (/, /apps, /sources): a fixed button always sits on whatever text scrolls under it, and at
   scroll 0 that is a table column. GemCommunity had dropped its floating buttons for the same reason (#239). Chris chose
   "bỏ nút nổi". It now lives where nothing scrolls under it:
   · `--rail`   wide screens: the last row of the rail (`railBottom`), a full-width primary button. Hidden ≤760px,
                where the rail is an off-canvas drawer nobody sees until they open it.
   · `--topbar` phones (≤760px): a 44px icon button in the topbar's `actions`, left of the account chip. Hidden above.
   The top-right corner stays the ACCOUNT. A surface renders both; exactly one is displayed at any width. */
.gb-assistant-launcher {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: 0;
  cursor: pointer;
  font-family: var(--gemboss-font-sans, system-ui, sans-serif);
}
.gb-assistant-launcher svg { flex: 0 0 auto; }
.gb-assistant-launcher:focus-visible { outline: 2px solid var(--gemboss-accent, #5661f5); outline-offset: 2px; }
.gb-assistant-launcher--rail {
  width: 100%;
  min-height: 44px; /* a control, not a list link: §9.1 asks 44 (visual-gate red at 768 with 36) */
  margin: 8px 0 4px;
  padding: 0 12px;
  border-radius: var(--gemboss-radius-md, 8px);
  /* The SAME iris pill as the admin's rail Assistant (.gnav-item.gnav-assistant, 0.6.8), not a solid near-black button
     (0.7.1, 04/10/2026): content-radar and partner-radar open their assistant through this launcher, and after 0.6.8 the
     two drew the one AI front door in two different colours beside the admin. */
  background: var(--gemboss-accent-soft);
  color: var(--gemboss-accent-strong);
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  text-align: left;
}
.gb-assistant-launcher--rail:hover { background: color-mix(in srgb, var(--gemboss-accent) 16%, var(--gemboss-surface)); }
.gb-assistant-launcher--rail .gb-assistant-launcher-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gb-assistant-launcher-kbd {
  font: inherit; font-size: 11px; opacity: 0.7;
  border: 1px solid currentColor; border-radius: 5px; padding: 1px 5px;
}
.gb-assistant-launcher--topbar {
  display: none;
  position: relative; /* holds the clipped label: an absolute child of a static button escapes to the nearest positioned box */
  justify-content: center;
  flex: 0 0 auto;
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: var(--gemboss-radius-md, 8px);
  background: transparent;
  color: var(--gemboss-text, #1b1c1e);
}
.gb-assistant-launcher--topbar:hover, .gb-assistant-launcher--topbar:active { background: var(--gemboss-hover, #eceef2); }
.gb-assistant-launcher--topbar .gb-assistant-launcher-label, .gb-assistant-launcher--topbar .gb-assistant-launcher-kbd {
  position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}
@media (max-width: 760px) {
  .gb-assistant-launcher--rail { display: none; }
  .gb-assistant-launcher--topbar { display: inline-flex; }
}

/* ═══ @gemboss/ui src/kit-plain.css ═══ */
/* kit-plain.css — kit primitives that only the NON-React surfaces use (GemFactory, gemcare, gempress write the
   markup by hand). They ship inside gemboss-ui.css (scripts/build-css.mjs `own`), but the admin does NOT import
   this file: `ui.css` is loaded by the admin's root layout, so every rule there is paid on every admin screen,
   and adding these to it pushed the admin shell 0.2 KB over its first-load budget (CI of #3591, 01/10/2026).
   The admin has Polaris for menus, checkboxes and description lists. */

/* ── Icon button · Menu · Key-value list ──────────────────────────────────────
   Added 01/10/2026 for GemFactory's item dialog, which had seven actions in one header row; the
   secondary five now sit behind one "⋯" button. Plain CSS, same contract as the tabs and tables in ui.css.

   Icon button: `<button class="ui-btn ui-btn--icon" aria-label="…"><svg…></button>` — square, the
   label is REQUIRED (there is no text for a screen reader to read otherwise).

   Menu: a `<details>` so it opens with no script. `<details class="ui-menu ui-menu--end">
   <summary class="ui-btn ui-btn--icon" aria-label="More actions">…</summary><div class="ui-menu__list">
   <button class="ui-menu__item">Copy #12<span class="ui-menu__hint">for Claude Code</span></button>
   <hr class="ui-menu__sep"><button class="ui-menu__item ui-menu__item--danger">Delete…</button></div>
   </details>`. `--end` aligns the list to the right edge of its button. Closing on an outside click,
   on an item, and on Escape is the page's job (a few lines; GemFactory's ui-kit.mjs has them).
   Items are 44px under EVERY pointer, not only coarse: they touch each other (0px apart), and §9.1
   only allows a gap under 8px between targets that are already 44px.

   Key-value list: `<dl class="ui-kv"><dt>Owner</dt><dd>Lily</dd>…</dl>` — label column sized to
   its longest label, value column takes the rest and wraps long keys and URLs. */
.ui-btn--icon { flex: none; width: var(--ui-btn-h); padding: 0; }
.ui-btn--icon svg { width: 18px; height: 18px; }
/* Menu: the SURFACE is the shell's `gemboss-popover-surface` and the rows are the account menu's row (shell.css
   declares `.gemboss-account-menu-item, .ui-menu__item` together), so a kit menu and the account menu on one page
   look the same and a fix to one reaches the other (§9.3). Markup: `<div class="ui-menu__list gemboss-popover-surface">`.
   Here: only placement, the hint, the separator and the disabled state. */
.ui-menu { position: relative; display: inline-block; }
.ui-menu > summary { list-style: none; cursor: pointer; }
.ui-menu > summary::-webkit-details-marker { display: none; }
.ui-menu__list { position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; display: grid; min-width: 220px; padding: var(--ui-space-1); box-sizing: border-box; }
.ui-menu--end .ui-menu__list { left: auto; right: 0; }
.ui-menu__item { display: flex; align-items: center; gap: var(--ui-space-2); white-space: nowrap; border-radius: var(--ui-radius-sm); }
.ui-menu__item:is([disabled], [aria-disabled="true"]) { opacity: .5; cursor: not-allowed; pointer-events: none; }
.ui-menu__hint { margin-left: auto; padding-left: var(--ui-space-4); color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }
.ui-menu__sep { height: 1px; margin: var(--ui-space-1) 0; border: 0; background: var(--ui-border); }
/* Checkbox: the LABEL is the target; disabled turns both the label and the box to not-allowed. Focus ring in the
   kit's own token (no --p-* here: this file is appended after the export's Polaris resolver runs). */
.ui-check { display: inline-flex; align-items: center; gap: var(--ui-space-2); min-height: 44px; max-width: 100%; overflow-wrap: break-word; cursor: pointer; font-size: var(--ui-fs-body); color: var(--ui-text); }
.ui-check > input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); cursor: inherit; }
.ui-check > input:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
.ui-check:has(> input:disabled) { opacity: .5; cursor: not-allowed; }
/* 0.9.0: a long run with no spaces (a URL, "clinical/premium/…/utility") breaks inside the label instead of widening
   the page: gemwatcher Operations overflowed 69px at 320 on one switch description (#375). Three parts: the label is
   capped at its parent (max-width above), its TEXT child may shrink (min-width 0, the last child only so a badge in
   front keeps its width), and break-word breaks the run once it cannot fit a line.
   break-word, NOT anywhere: anywhere also drops the label's min-content, and a flex row then squeezes a short label
   ("Lock" beside a long band name in GemFactory) into "Lo/ck". Two cases stay the surface's job: the text must be an
   element (`input + span`; bare text is an anonymous flex item min-width cannot reach), and in a flex ROW the label
   keeps its min-content, so that row must wrap. */
.ui-check > :last-child:not(input) { min-width: 0; }
.ui-label { display: block; margin: 0 0 var(--ui-space-1); font-size: var(--ui-fs-caption); font-weight: 600; color: var(--ui-text-muted); }
.ui-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--ui-space-2) var(--ui-space-4); margin: 0; font-size: var(--ui-fs-body); }
.ui-kv > dt { color: var(--ui-text-muted); }
.ui-kv > dd { margin: 0; min-width: 0; overflow-wrap: anywhere; color: var(--ui-text); }

/* Under a thumb the icon-only button is 44 WIDE as well (the kit's coarse block already raises every .ui-btn to 44 tall). */
@media (pointer: coarse) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; } }
/* …and at phone WIDTH, as gemboss-field does: a desktop browser at 320px still reports pointer: fine. */
@media (max-width: 47.9975em) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; min-height: 44px; } }

/* ── Link · Media card (02/10/2026) ──────────────────────────────────────────
   GemFactory, gemcare and gempress left text links in the browser's default blue-and-purple, and each drew its own
   thumbnail card. The admin uses Polaris' Link and has no media card, so both live here, not in ui.css.

   Link: `<a class="ui-link" href>` for a link inside text. Iris (--gemboss-accent-strong, 6.2:1 on white), underlined
   so it is not told by colour alone (WCAG 1.4.1); visited looks the same (these are tools, not articles). Markdown or
   other generated HTML you cannot add classes to: wrap it in `.ui-prose` and its bare `<a>` get the same look.
   `.ui-prose` links sit at :where() specificity (0.7.3), so any component's own link rule wins inside it.

   Media card: `<article class="ui-card ui-card--media"><a|button class="ui-card__media"><img alt=""></…>
   <div class="ui-card__body">…</div><div class="ui-card__foot">…</div></article>`. The picture is 16:10 and
   cropped to fill; the card has no padding of its own, the body and foot do. */
.ui-link,
.ui-prose :where(a:not([class])) {
  color: var(--gemboss-accent-strong, #3a44de);
  text-decoration: underline;
  text-decoration-thickness: from-font;
  text-underline-offset: 2px;
}
.ui-link:visited,
.ui-prose :where(a:not([class])):visited { color: var(--gemboss-accent-strong, #3a44de); }
.ui-link:hover,
.ui-prose :where(a:not([class])):hover { color: var(--gemboss-accent, #5661f5); text-decoration-thickness: 2px; }
.ui-link:focus-visible,
.ui-prose :where(a:not([class])):focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 2px; border-radius: 2px; }

.ui-card--media { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
.ui-card__media {
  display: block; width: 100%; aspect-ratio: 16 / 10; margin: 0; padding: 0; border: 0;
  border-bottom: 1px solid var(--ui-border); background: var(--ui-surface-2); overflow: hidden; color: inherit;
}
.ui-card__media > img { display: block; width: 100%; height: 100%; object-fit: cover; }
button.ui-card__media, a.ui-card__media { cursor: pointer; }
.ui-card__media:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: -2px; }
/* justify-items: start: a tag or a button in the body keeps its own width instead of stretching across the card. */
.ui-card__body { display: grid; justify-items: start; gap: var(--ui-space-2, 8px); padding: var(--ui-space-3, 12px) var(--ui-space-4, 16px); min-width: 0; }
.ui-card__foot {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--ui-space-2, 8px);
  margin-top: auto; padding: 0 var(--ui-space-4, 16px) var(--ui-space-3, 12px);
  color: var(--ui-text-muted); font-size: var(--ui-fs-caption, 13px);
}

/* Popup body and foot: `<div class="ui-modal__body">…</div><div class="ui-modal__foot"><button class="ui-btn">…</button></div>`.
   Their padding moved to ui.css in 0.7.3 (the admin loads that file, not this one). kit-plain.js's openModal writes
   exactly this shape. What stays here is what only the plain-HTML popups use:

   A head that carries more than a title (a large popup's subtitle and its own actions, GemFactory's detail popup):
   `<div class="ui-modal__head"><div class="ui-modal__title">Mega menu<span class="ui-modal__subtitle">chrome/header</span>
   </div><div class="ui-modal__actions"><button class="ui-btn">Copy</button></div><button class="ui-modal__close">…` */
.ui-modal__subtitle { display: block; margin-top: 2px; font-size: var(--ui-fs-caption); line-height: 16px; font-weight: 450; color: var(--ui-text-muted); overflow-wrap: anywhere; }
.ui-modal__actions { flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2); }
/* A popup taller than the screen: the body scrolls, the head and foot stay, so the actions are always reachable
   (320×568 with a long form put Huỷ/Xoá below the fold, unreachable under the locked page; review #3633). */
.ui-overlay > .ui-modal { display: flex; flex-direction: column; max-height: calc(100dvh - 40px); }
.ui-modal > .ui-modal__head, .ui-modal > .ui-modal__foot { flex: none; }
.ui-modal > .ui-modal__body { flex: 1 1 auto; min-height: 0; overflow: auto; }

/* ── Báo cáo: delta · KPI lớn · thanh ngang · biểu đồ cột · chú thích · phễu · lưới · tiêu đề mục ─────────────────
   Thêm 03/10/2026 cho trang số của gemnews (gemnews.ops.gemboss.ai/admin): nó tự vẽ thanh ngang, biểu đồ theo ngày và
   lưới hai cột trong admin.css riêng, và bề mặt thứ hai cần báo cáo sẽ chép lại y như vậy. README "promote what a second
   surface needs". Chỉ CSS: số liệu và tỉ lệ do bề mặt tính, kit chỉ vẽ.

   Màu chuỗi: `--ui-series-1..5`, theo thứ tự dùng. 1 là iris thương hiệu; các màu sau đủ khác nhau cả với người mù màu
   đỏ-lục (không đặt đỏ cạnh lục). Đặt màu cho một chuỗi bằng `style="--c:var(--ui-series-2)"` trên đúng phần tử. */
:root {
  --ui-series-1: var(--gemboss-accent, #5661f5);
  --ui-series-2: #0f9f8f;
  --ui-series-3: #d9822b;
  --ui-series-4: #c2417a;
  --ui-series-5: #6b7280;
}

/* Delta: `<span class="ui-delta ui-delta--good"><span aria-hidden="true">↑</span> 12%</span>`. Tốt / xấu là của BỀ MẶT
   quyết (lượt đọc tăng là tốt, tỉ lệ thoát tăng là xấu), mũi tên là hướng. Không tông = trung tính (không đổi, hoặc
   "chưa có kỳ trước"). Luôn kèm chữ cho trình đọc màn hình nếu mũi tên là thứ duy nhất nói hướng. */
/* Wraps (0.6.2): a delta that carries its comparison ("▼ 50% vs previous 30 days (20m)") ran 73px out of a 145px metric at
   390 on gemcare. Short ones ("↑ 18%") still sit on one line; long ones break between words, never inside a number. */
.ui-delta { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 2px; font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-text-muted); font-variant-numeric: tabular-nums; }
.ui-delta--good { color: var(--ui-success-fg); }
.ui-delta--bad { color: var(--ui-danger-fg); }

/* KPI lớn trong lưới đều: `<div class="ui-metrics ui-metrics--grid"><div class="ui-metric ui-metric--lg">…`. Lưới
   auto-fit chia đều hàng, nên 4 ô không bao giờ thành 3 + 1 mồ côi ở khổ rộng; ≤ 520px luôn hai cột. `__foot` là dòng
   dưới cùng (delta + so với gì). */
.ui-metrics--grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); gap: var(--ui-space-3); margin: 0; }
.ui-metric--lg { gap: 4px; padding: var(--ui-space-3) var(--ui-space-4); }
.ui-metric--lg .ui-metric__val { font-size: 24px; line-height: 32px; } /* Polaris headingXl, as the admin's analytics cards */
.ui-metric--lg .ui-metric__lbl { order: -1; font-size: var(--ui-fs-body); } /* bodyMd 13 (0.12: was 14) */
.ui-metric__foot { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
@media (max-width: 520px) { .ui-metrics--grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

/* Thanh ngang: `<ol class="ui-barlist"><li><span class="ui-barlist__label">Google</span><span class="ui-barlist__val">60
   <span class="ui-barlist__share">93,8%</span></span><span class="ui-barlist__bar" style="--v:.94"></span></li></ol>`.
   `--v` = độ dài 0..1 (thường n / n lớn nhất), `--c` đổi màu. Nhãn và số ở một dòng, thanh ở dòng dưới chạy hết bề
   ngang: không có cột nhãn cố định nào để bị bóp ở 320px. `__note` = dòng phụ (ví dụ "Đọc hết 30%"). */
.ui-barlist { display: grid; gap: var(--ui-space-3); margin: 0; padding: 0; list-style: none; font-size: var(--ui-fs-body, 13px); }
.ui-barlist > li { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: baseline; gap: 4px var(--ui-space-3); }
.ui-barlist__label { min-width: 0; overflow-wrap: anywhere; color: var(--ui-text); }
.ui-barlist__label a { color: inherit; }
.ui-barlist__val { font-weight: 600; color: var(--ui-text); font-variant-numeric: tabular-nums; white-space: nowrap; text-align: right; }
.ui-barlist__share { margin-left: 6px; font-weight: 400; color: var(--ui-text-muted); }
.ui-barlist__note { grid-column: 1 / -1; margin-top: -2px; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
.ui-barlist__bar { grid-column: 1 / -1; position: relative; height: 6px; border-radius: 999px; background: var(--ui-surface-2); overflow: hidden; }
.ui-barlist__bar::after { content: ""; position: absolute; inset: 0 auto 0 0; width: calc(var(--v, 0) * 100%); min-width: 3px; border-radius: inherit; background: var(--c, var(--ui-series-1)); }

/* Biểu đồ cột chồng theo thời gian, thêm (tuỳ chọn) một chuỗi CHẤM:
   `<figure class="ui-chart"><div class="ui-chart__plot" data-max="80"><div class="ui-chart__col" title="02/10 · …">
   <span class="ui-chart__seg" style="--v:.4;--c:var(--ui-series-1)"></span><span class="ui-chart__seg" …></span>
   <span class="ui-chart__dot" style="--v:.5"></span></div>…</div><div class="ui-chart__axis"><span>01/10</span>…</div>
   <figcaption class="ui-legend">…</figcaption></figure>`. `--v` = giá trị / trục (0..1), chồng từ dưới lên theo thứ tự
   markup. Cột rộng tối đa 48px và dàn đều hết bề ngang: 4 ngày số liệu không thành 4 khối to. `data-max` in ở góc
   trên trái làm mốc trục. Đường kẻ ngang ở 25/50/75%. Số thật cho trình đọc màn hình là
   việc của bề mặt (một bảng `ui-sr-only` cạnh biểu đồ): phần vẽ nên `aria-hidden`. */
.ui-chart { margin: 0; }
.ui-chart__plot { position: relative; display: flex; align-items: flex-end; justify-content: space-between; gap: 3px; height: 180px; padding-top: 18px; border-bottom: 1px solid var(--ui-border);
  background: linear-gradient(to top, var(--ui-border) 1px, transparent 1px) 0 0 / 100% 25% repeat-y; background-origin: content-box; background-clip: content-box; }
.ui-chart__plot[data-max]::before { content: attr(data-max); position: absolute; top: 0; left: 0; font-size: var(--gemboss-fs-micro, 11px); line-height: 1; color: var(--ui-text-muted); font-variant-numeric: tabular-nums; }
.ui-chart__col { position: relative; flex: 1 1 0; min-width: 0; max-width: 48px; height: 100%; display: flex; flex-direction: column-reverse; }
.ui-chart__col:hover { background: color-mix(in srgb, var(--ui-text) 6%, transparent); }
.ui-chart__seg { display: block; flex: none; height: calc(var(--v, 0) * 100%); background: var(--c, var(--ui-series-1)); }
.ui-chart__dot { position: absolute; left: 50%; bottom: calc(var(--v, 0) * 100%); width: 8px; height: 8px; margin: 0 0 -4px -4px; box-sizing: border-box; border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--c, var(--ui-text-strong, var(--ui-text))); }
.ui-chart__axis { display: flex; justify-content: space-between; gap: var(--ui-space-2); margin-top: 6px; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); font-variant-numeric: tabular-nums; }

/* Chú thích: `<p class="ui-legend"><span class="ui-legend__item"><i style="--c:var(--ui-series-1)"></i>Tiện ích</span>
   <span class="ui-legend__item ui-legend__item--dot"><i></i>Lượt mở</span></p>`. `--dot` = chuỗi vẽ bằng chấm rỗng. */
.ui-legend { display: flex; flex-wrap: wrap; gap: 6px 16px; margin: var(--ui-space-2) 0 0; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
.ui-legend__item { display: inline-flex; align-items: center; gap: 6px; }
.ui-legend__item > i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; background: var(--c, var(--ui-series-1)); }
.ui-legend__item--dot > i { border-radius: 50%; background: var(--ui-surface); border: 2px solid var(--c, var(--ui-text-strong, var(--ui-text))); box-sizing: border-box; }

/* Phễu: `<ol class="ui-funnel"><li><span class="ui-funnel__val">105</span><span class="ui-funnel__lbl">Lời mời hiện
   </span></li><li><span class="ui-funnel__rate">4,8%</span><span class="ui-funnel__val">5</span>…</li></ol>`. Mỗi bước
   sau bước đầu mang `__rate` = tỉ lệ đi tiếp từ bước NGAY TRƯỚC (bề mặt tính). Ngang ở khổ rộng, dọc ở ≤ 520px. */
.ui-funnel { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: var(--ui-space-2); margin: 0; padding: 0; list-style: none; }
.ui-funnel > li { position: relative; display: flex; flex-direction: column; gap: 2px; padding: var(--ui-space-3); border-radius: var(--ui-radius-md); background: var(--ui-surface-2); min-width: 0; }
.ui-funnel__val { font-size: 20px; font-weight: 700; color: var(--ui-text); font-variant-numeric: tabular-nums; line-height: 24px; } /* Polaris headingLg */
.ui-funnel__lbl { font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); overflow-wrap: anywhere; }
.ui-funnel__rate { order: -1; font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-series-1); font-variant-numeric: tabular-nums; }
@media (max-width: 520px) { .ui-funnel { grid-auto-flow: row; } }

/* Lưới bảng điều khiển: `<div class="ui-grid ui-grid--2">` card cạnh card, xuống một cột ở ≤ 960px (--3: ≤ 1100px hai
   cột, ≤ 720px một). Card trong lưới không kéo cao theo card bên cạnh. Tiêu đề MỤC trong trang (nhỏ hơn ui-page-head,
   lớn hơn tiêu đề card): `<div class="ui-section-head"><h2>Nội dung</h2><p>…</p></div>`. */
.ui-grid { display: grid; gap: var(--ui-space-4); align-items: start; }
.ui-grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.ui-grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 1100px) { .ui-grid--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 960px) { .ui-grid--2 { grid-template-columns: minmax(0, 1fr); } }
@media (max-width: 720px) { .ui-grid--3 { grid-template-columns: minmax(0, 1fr); } }
.ui-section-head { margin: var(--ui-space-4) 0 0; }
.ui-section-head > h2 { margin: 0; font-size: 14px; line-height: 20px; font-weight: 650; color: var(--ui-text); } /* Polaris headingMd (0.12: was 16) */
.ui-section-head > p { margin: 2px 0 0; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }

/* Nhóm nút CHỮ không bao giờ bẻ chữ (03/10/2026, thanh lọc của gemnews ở 390px: "Mọi ngôn ngữ" thành hai dòng, "中文"
   thành 中 / 文, vì ô cao cố định 36px mà nút được co). Nút giữ đủ bề ngang chữ; cả nhóm rộng hơn chỗ trống thì cuộn
   ngang trong chính nó, trang không tràn. Ở đây chứ không ở ui.css: ngân sách CSS first-load của admin còn ~0,1 KB. */
.ui-seg { max-width: 100%; overflow-x: auto; }   /* thanh cuộn KHÔNG ẩn: lựa chọn bị khuất phải có dấu hiệu (review #3630) */
.ui-seg :is(button, a).is-text { flex: none; white-space: nowrap; }
/* Ở 320px, bốn nút chữ (Tất cả · Tiếng Việt · English · 中文) thiếu ~10px: bớt padding thay vì bắt người dùng cuộn mới
   thấy lựa chọn cuối. Nút vẫn cao 36px, chữ vẫn một dòng. */
@media (max-width: 360px) { .ui-seg :is(button, a).is-text { padding: 0 9px; } }
/* Tab / nhóm nút là ĐIỀU KHIỂN: ≥ 44px dưới ngón tay (§9.1). Khối coarse của ui.css chỉ bắt `pointer: coarse`; ở khổ điện
   thoại trình duyệt máy tính vẫn báo `fine`, nên nâng theo khổ như ui-btn--icon ở trên. */
@media (pointer: coarse), (max-width: 47.9975em) { .ui-seg :is(button, a) { height: 44px; min-height: 44px; } }

/* ── Polaris parity: the primitives every surface was drawing for itself (02/10/2026) ──────────────────────
   An audit of the six surfaces on the package (gemwatcher, GemFactory, gempress, gemcare, partner-radar,
   content-radar) found the same gaps re-filled six ways: no destructive button (four surfaces fell back to the
   browser's `confirm()`), one-tone badges, banners as bare colour blocks, hand-made radio / switch / tooltip /
   spinner, and help and error text under a field in five sizes. The admin has all of them from Polaris; this is
   the same shape in plain CSS, with Polaris' measurements and GemBoss' tokens (the danger / success / warning
   set the kit's tags and notices already use). The admin does not load this file. */

/* Destructive button: `ui-btn ui-btn--critical` (red text on the default button) and
   `ui-btn ui-btn--primary ui-btn--critical` (red fill), Polaris' `tone="critical"` in both variants. Red text is
   --gemboss-danger-strong: 7.3:1 on white, 5.7:1 on the hover ground (shell.css says why not --gemboss-danger). */
.ui-btn--critical { color: var(--gemboss-danger-strong); }
.ui-btn--critical:hover { background: var(--gemboss-danger-soft); color: var(--gemboss-danger-strong); }
.ui-btn--primary.ui-btn--critical { background: var(--gemboss-danger); border-color: var(--gemboss-danger); color: var(--gemboss-on-primary); }
.ui-btn--primary.ui-btn--critical:hover { background: var(--gemboss-danger-strong); border-color: var(--gemboss-danger-strong); color: var(--gemboss-on-primary); }
.ui-btn--critical:focus-visible { outline-color: var(--gemboss-danger-strong); }

/* Under a thumb, and at phone WIDTH (a desktop browser at 320px reports pointer: fine), EVERY kit button is a 44px
   control, the compact one included: §9.1 has no density exception, and GemFactory, gempress and gemcare each
   restated this floor for `--sm` themselves. ui.css's coarse block leaves `--sm` alone for the admin's own
   settings page; this file is not loaded there. */
@media (pointer: coarse), (max-width: 47.9975em) {
  .ui-btn, .ui-btn.ui-btn--sm { min-height: 44px; }
  .ui-select.ui-select { min-height: 44px; }
  /* A target is an AREA: the compact icon button (28 wide) and an icon segment (30) grow sideways as well. */
  .ui-btn--icon.ui-btn--sm, .ui-seg :is(button, a):not(.is-text) { min-width: 44px; }
}

/* Badge tones: `<span class="ui-badge ui-badge--success">Active</span>`. Polaris' tones on GemBoss' soft grounds (Polaris' `attention` is `--warning` here: the fleet has one amber).
   `ui-badge--dot` puts a status dot before the label (filled = done, the tone's colour). */
.ui-badge--success { background: var(--gemboss-success-soft); color: var(--gemboss-success-strong); }
.ui-badge--warning { background: var(--gemboss-warning-soft); color: var(--gemboss-warning); }
.ui-badge--critical { background: var(--gemboss-danger-soft); color: var(--gemboss-danger-strong); }
.ui-badge--info { background: var(--gemboss-accent-soft); color: var(--gemboss-accent-strong); }
.ui-badge--dot::before { content: ""; flex: none; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }

/* Banner: Polaris' in-page Banner. A tone strip with an icon and a title, a body, optional actions, optional
   dismiss. `ui-notice` stays for a one-line note; a banner is for something the person must read or act on.
   <div class="ui-banner ui-banner--warning" role="status">
     <div class="ui-banner__head"><svg class="ui-banner__icon" …/><h3 class="ui-banner__title">Slack token expired</h3>
       <button type="button" class="ui-banner__close" aria-label="Dismiss">…</button></div>
     <div class="ui-banner__body"><p>Monday's digest will not send.</p>
       <div class="ui-banner__actions"><a class="ui-btn" href>Reconnect</a></div></div></div>
   No tone = neutral. role="status" / "alert" only when it appears after the page loaded. */
.ui-banner {
  --ui-banner-bg: var(--ui-surface-2); --ui-banner-fg: var(--ui-text);
  border-radius: var(--ui-radius-lg); overflow: hidden; background: var(--ui-surface); color: var(--ui-text);
  box-shadow: 0 0 0 1px var(--ui-border), 0 3px 1px -1px rgba(26, 26, 26, 0.07);
  font-size: var(--ui-fs-body); line-height: 20px;
}
.ui-banner--success { --ui-banner-bg: var(--gemboss-success-soft); --ui-banner-fg: var(--gemboss-success-strong); }
.ui-banner--warning { --ui-banner-bg: var(--gemboss-warning-soft); --ui-banner-fg: var(--gemboss-warning); }
.ui-banner--critical { --ui-banner-bg: var(--gemboss-danger-soft); --ui-banner-fg: var(--gemboss-danger-strong); }
.ui-banner--info { --ui-banner-bg: var(--gemboss-accent-soft); --ui-banner-fg: var(--gemboss-accent-strong); }
.ui-banner__head { display: flex; align-items: center; gap: var(--ui-space-2); min-height: 32px; padding: 6px var(--ui-space-3); background: var(--ui-banner-bg); color: var(--ui-banner-fg); }
.ui-banner__icon { flex: none; width: 20px; height: 20px; fill: currentColor; }
.ui-banner__title { flex: 1 1 auto; min-width: 0; margin: 0; font-size: var(--ui-fs-body); font-weight: 650; line-height: 20px; }
.ui-banner__close {
  flex: none; display: inline-grid; place-items: center; width: 32px; height: 32px; margin: -6px -8px -6px 0;
  border: 0; border-radius: var(--ui-radius-md); background: transparent; color: inherit; cursor: pointer;
}
.ui-banner__close:hover { background: color-mix(in srgb, currentColor 10%, transparent); }
.ui-banner__close:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
.ui-banner__close svg { width: 16px; height: 16px; }
.ui-banner__body { padding: var(--ui-space-3) var(--ui-space-4); }
.ui-banner__body > :first-child { margin-top: 0; }
.ui-banner__body > :last-child { margin-bottom: 0; }
.ui-banner__actions { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); margin-top: var(--ui-space-3); }
@media (pointer: coarse), (max-width: 47.9975em) { .ui-banner__close { width: 44px; height: 44px; margin: -12px -12px -12px 0; } }

/* Help and error text under a field, Polaris' Labelled help text and InlineError:
   <label class="ui-label" for="x">Domain</label><input id="x" class="gemboss-field gemboss-field--tall"
     aria-describedby="x-help x-err" aria-invalid="true"><p class="ui-help" id="x-help">Without https://</p>
   <p class="ui-error" id="x-err">This store is already tracked.</p>
   aria-invalid paints the field in the critical tone; the message says what to do, not only what is wrong. */
.ui-help { margin: var(--ui-space-1) 0 0; font-size: var(--ui-fs-body); line-height: 20px; color: var(--ui-text-muted); }
.ui-error:empty { display: none; }   /* a placeholder kept for aria-describedby draws no icon */
.ui-error { display: flex; gap: 6px; margin: var(--ui-space-1) 0 0; font-size: var(--ui-fs-body); line-height: 20px; color: var(--gemboss-danger-strong); }
.ui-error::before {
  content: ""; flex: none; width: 16px; height: 16px; margin-top: 2px; background: currentColor;
  -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/></svg>") center / contain no-repeat;
          mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path d='M10 2a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 4h1.5v5.5h-1.5V6Zm.75 9a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z'/></svg>") center / contain no-repeat;
}
.gemboss-field[aria-invalid="true"], .gemboss-field--group:has([aria-invalid="true"]),
.ui-select[aria-invalid="true"], .ui-textarea[aria-invalid="true"] {
  border-color: var(--gemboss-danger-strong); background-color: var(--gemboss-danger-soft);
}

/* Radio: the checkbox's row with `type="radio"` — `<fieldset class="ui-choices"><legend class="ui-label">Plan</legend>
   <label class="ui-check"><input type="radio" name="plan" checked><span>Basic</span></label>…</fieldset>`.
   One minmax(0, 1fr) column: an auto column grows to the longest unbroken word in any label (0.9.0). */
.ui-choices { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; margin: 0; padding: 0; border: 0; min-width: 0; }
.ui-choices > legend { padding: 0; }

/* Switch: a checkbox that reads as on / off, for a setting that applies at once (no Save button).
   `<label class="ui-check ui-switch"><input type="checkbox" role="switch" checked><span>Send Monday digest</span></label>`.
   Track 32×20, thumb 16; the row stays the checkbox's 44px target. */
.ui-switch > input[type="checkbox"] {
  appearance: none; -webkit-appearance: none; position: relative; width: 32px; height: 20px; margin: 0;
  border-radius: var(--gemboss-radius-full); background: var(--gemboss-field-line); /* 3.5:1 on white, WCAG 1.4.11 */ transition: background-color var(--gemboss-transition-fast) ease;
}
.ui-switch > input[type="checkbox"]::before {
  content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
  background: var(--gemboss-surface); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); transition: transform var(--gemboss-transition-fast) ease;
}
.ui-switch > input[type="checkbox"]:checked { background: var(--ui-accent); }
.ui-switch > input[type="checkbox"]:checked::before { transform: translateX(12px); }
@media (prefers-reduced-motion: reduce) { .ui-switch > input[type="checkbox"], .ui-switch > input[type="checkbox"]::before { transition: none; } }

/* Tooltip: a SHORT label for an icon-only control, shown on hover and keyboard focus. Not for anything the person
   needs to act on (a touch screen never hovers).
   <span class="ui-tooltip"><button class="ui-btn ui-btn--icon" aria-describedby="t1" aria-label="Archive">…</button>
     <span class="ui-tooltip__bubble" role="tooltip" id="t1">Archive</span></span>   `--end` aligns it right. */
.ui-tooltip { position: relative; display: inline-flex; }
.ui-tooltip__bubble {
  position: absolute; z-index: 30; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
  width: max-content; max-width: 240px; padding: 4px 8px; border-radius: var(--ui-radius-md);
  background: var(--ui-surface); color: var(--ui-text); font-size: var(--ui-fs-body); line-height: 20px; font-weight: 450;
  box-shadow: 0 8px 16px -4px rgba(26, 26, 26, 0.22), 0 0 0 1px var(--ui-border);
  opacity: 0; visibility: hidden; pointer-events: none; transition: opacity var(--gemboss-transition-fast) ease;
}
.ui-tooltip--end .ui-tooltip__bubble { left: auto; right: 0; transform: none; }
.ui-tooltip:hover > .ui-tooltip__bubble, .ui-tooltip:focus-within > .ui-tooltip__bubble { opacity: 1; visibility: visible; }

/* Spinner: `<span class="ui-spinner" role="status"><span class="ui-sr-only">Loading…</span></span>`, 20px
   (Polaris small); `--lg` is 44px. Reduced motion turns it slower, never off: it is the only sign of waiting. */
.ui-spinner {
  display: inline-block; flex: none; width: 20px; height: 20px; box-sizing: border-box; border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--ui-text) 18%, transparent); border-top-color: var(--ui-text);
  animation: ui-spin 0.7s linear infinite; vertical-align: middle;
}
.ui-spinner--lg { width: 44px; height: 44px; border-width: 4px; }
@keyframes ui-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .ui-spinner { animation-duration: 2s; } }

/* Progress: `<progress class="ui-progress" value="37" max="100" aria-label="Import">37%</progress>`, 8px, the
   primary fill (Polaris' small ProgressBar). Tones: `--success`, `--critical`. */
.ui-progress { appearance: none; -webkit-appearance: none; display: block; width: 100%; height: 8px; border: 0; border-radius: var(--gemboss-radius-full); overflow: hidden; background: var(--gemboss-divider); color: var(--ui-accent); }
.ui-progress::-webkit-progress-bar { background: var(--gemboss-divider); border-radius: inherit; }
.ui-progress::-webkit-progress-value { background: currentColor; border-radius: inherit; }
.ui-progress::-moz-progress-bar { background: currentColor; border-radius: inherit; }
.ui-progress--success { color: var(--gemboss-success); }
.ui-progress--critical { color: var(--gemboss-danger); }

/* Wider popup: `openModal({ className: "ui-modal--wide" })`, Polaris' default Modal (620px). The kit's own 460 is
   Polaris' small step plus one; a form with two columns or a table needs this one. */
.ui-modal--wide { width: min(620px, 100%); }

/* Native popup: `<dialog class="ui-modal">` opened with `showModal()`, for React (and other) surfaces that do not load
   kit-plain.js. The browser does what openModal does by hand: Escape closes it, the page behind is inert, it sits in
   the top layer above the shell's topbar. Same head / body / foot markup; `::backdrop` is the kit's overlay. gemcare
   and content-radar already used `<dialog>`, each with its own width, padding and backdrop (02/10/2026: four shapes,
   420 to 760 wide). Close on a backdrop click yourself: a click whose target IS the dialog landed outside the panel.
   `<dialog class="ui-modal" aria-labelledby="t"><div class="ui-modal__head"><div class="ui-modal__title" id="t">…</div>
   <button type="button" class="ui-modal__close" aria-label="Close">…</button></div><div class="ui-modal__body">…</div>
   <div class="ui-modal__foot">…</div></dialog>` */
dialog.ui-modal { padding: 0; max-width: calc(100% - 40px); max-height: calc(100dvh - 40px); color: var(--ui-text); }
dialog.ui-modal[open] { display: flex; flex-direction: column; }
dialog.ui-modal::backdrop { background: rgba(0, 0, 0, 0.5); } /* the overlay's own scrim (0.7.3: Polaris' .5, no blur) */

/* ── 0.5.1: what the first surfaces on 0.5.0 still drew themselves (GemFactory, content-radar, 03/10/2026) ─────────── */

/* At phone width a short compact button ("Bỏ") was 44 tall but 41 wide: a target is an area. */
@media (pointer: coarse), (max-width: 47.9975em) { .ui-btn.ui-btn--sm { min-width: 44px; } }

/* A long label in a badge ("Đã dùng 1 lần + 4 bài cùng nguồn · 2 Hot News") wraps instead of running out of its card. */
.ui-badge--wrap { white-space: normal; overflow-wrap: anywhere; line-height: 16px; }

/* Page head with actions on the right, wrapping under the title when narrow:
   `<div class="ui-page-head ui-page-head--actions"><div><h1>…</h1><p>…</p></div><div class="ui-page-head__actions">
   <button class="ui-btn ui-btn--primary">…</button></div></div>`. */
.ui-page-head--actions { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 12px 16px; }
.ui-page-head--actions > :first-child { min-width: 0; }
.ui-page-head__actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2); }

/* Chips that TOGGLE (a filter applied in place, no URL): `<div class="ui-chips"><button type="button" class="ui-tag
   ui-tag--chip" aria-pressed="true">CRO</button>…`. Same box as the link chips (ui.css); pressed = the link's current. */
.ui-chips > button.ui-tag--chip {
  min-height: 28px; padding: 4px 12px; border-radius: var(--ui-radius-md, 8px); cursor: pointer;
  font-family: inherit; font-size: var(--gemboss-fs-caption, 12px); color: var(--ui-text);
}
.ui-chips > button.ui-tag--chip:hover { background: var(--ui-surface-2); border-color: var(--ui-text-muted); }
.ui-chips > button.ui-tag--chip:focus-visible { outline: 2px solid var(--ui-focus, #5661f5); outline-offset: 1px; }
.ui-chips > button.ui-tag--chip[aria-pressed="true"] { background: var(--ui-accent-soft); border-color: var(--ui-accent); color: var(--ui-accent-strong); font-weight: 600; }
@media (pointer: coarse), (max-width: 47.9975em) { .ui-chips > button.ui-tag--chip { min-height: 44px; min-width: 44px; justify-content: center; } }

/* Chat at phone WIDTH (gemwatcher's assistant, measured 03/10/2026): the composer read 14px (iOS zooms the page on tap
   below 16) and "new chat" was 35px wide under a fine pointer. shell.css raises the latter under `pointer: coarse`; a
   desktop window at 390px reports fine. Here and not in chat.css / shell.css: the admin's first-load budget. */
@media (max-width: 47.9975em) {
  /* Class doubled: chat.css is appended AFTER this file in gemboss-ui.css, so a same-weight rule there would win. */
  .gemboss-ec-input.gemboss-ec-input textarea { font-size: 16px; }
  .gemboss-assistant-collapse, .gemboss-assistant-newchat { min-height: 44px; min-width: 44px; }
}

/* Segmented buttons that say their state the accessible way: `aria-pressed="true"` looks like `.active` (ui-tabs reads
   aria-selected already; gemcare had to set both on every group). */
.ui-seg button[aria-pressed="true"] { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }

/* Read-only field: visibly not editable (the admin's field has no such state; gempress kept its own `.field[readonly]`).
   The attribute doubled = (0,3,0), so it also holds over the field's own hover rule, which comes earlier. */
:is(.gemboss-field, .ui-textarea)[readonly][readonly] { background: var(--ui-surface-2); border-color: var(--ui-border); cursor: default; }

/* A popup whose body and foot sit inside one <form>: the form takes no box, so `.ui-modal > .ui-modal__body` (the
   scrolling body) and the pinned foot still lay out as the popup's own children (gemcare's escalate form, 0.5.1). */
.ui-modal > form { display: contents; }
.ui-modal > form > .ui-modal__head, .ui-modal > form > .ui-modal__foot { flex: none; }
.ui-modal > form > .ui-modal__body { flex: 1 1 auto; min-height: 0; overflow: auto; }

/* A metric that FILTERS the view below it, as a toggle: `<button type="button" class="ui-metric" aria-pressed="true">`
   (gemwatcher's Radar cards: as links to the same hash, Cmd-click opened an unfiltered tab, Space did nothing and the
   chosen card was told by colour alone). A metric link that is the current view: `a.ui-metric[aria-current="page"]`.
   Selected = iris edge, the shell's "this one" colour. */
button.ui-metric { font: inherit; text-align: left; color: inherit; cursor: pointer; }
button.ui-metric:hover { border-color: var(--ui-text-muted); }
button.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
/* Class doubled: the chosen card is usually the one under the pointer, and `button.ui-metric:hover` (0,2,1) would
   repaint its edge grey the moment it is clicked (measured on gemwatcher Radar). */
.ui-metric.ui-metric:is([aria-pressed="true"], [aria-current="page"]) { border-color: var(--gemboss-accent); box-shadow: inset 0 0 0 1px var(--gemboss-accent); }

/* 0.7.3: numbered pager + ui-more, for the plain surfaces (the admin uses Polaris Pagination). Markup and rationale:
   README "Long lists". Here, not in ui.css: ui.css is exported verbatim into the admin, under a 90 KB budget. */
.ui-pager > a, .ui-pager > [aria-disabled="true"] { min-width: 32px; justify-content: center; box-sizing: border-box; gap: 4px; }
.ui-pager > a[aria-current="page"] { background: var(--ui-text); border-color: var(--ui-text); color: var(--ui-surface); font-size: var(--ui-fs-body); font-weight: 600; }
.ui-pager > a[aria-current="page"]:hover { background: var(--ui-text); }
.ui-pager > [aria-disabled="true"] { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); color: var(--ui-text-muted); font-size: var(--ui-fs-body); opacity: .55; cursor: default; }
.ui-pager > [aria-disabled="true"]:hover { background: none; }
.ui-pager__gap { min-width: 16px; text-align: center; color: var(--ui-text-muted); }
.ui-pager__range { margin-left: auto; color: var(--ui-text-muted); font-size: var(--ui-fs-body); font-variant-numeric: tabular-nums; }
.ui-more { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 12px; padding: 16px 0; color: var(--ui-text-muted); font-size: var(--ui-fs-body); text-align: center; }
.ui-more[hidden] { display: none; }
@media (pointer: coarse), (max-width: 47.9975em) { .ui-pager > a, .ui-pager > [aria-disabled="true"] { min-width: 44px; min-height: 44px; } }
/* Phone: 11 targets of 44px wrapped into 3-4 rows at 320 (measured). Keep Previous · current · Next; the range wraps under. */
@media (max-width: 35.9975em) { .ui-pager > a:not([rel]):not([aria-current]):not([aria-disabled]), .ui-pager__gap { display: none; } }

/* 0.12.0: filter chip and card stack, both drawn by hand on every surface the 05/10/2026 density audit read.
   Filter chip: `<div class="ui-chips"><button type="button" class="ui-chip" aria-pressed="true">Shopify Plus</button>
   <a class="ui-chip" href="?country=us" aria-current="page">US</a></div>`. Polaris FilterPill's shape (radius 8, a 1px line,
   dashed until applied) at the Button's density: 28px, 12px/550, so a filter row and a button row are one height.
   Applied = `aria-pressed="true"` (in place) or `aria-current="page"` (a URL), solid line and the selected tint. */
.ui-chip {
  display: inline-flex; align-items: center; justify-content: center; gap: 4px; box-sizing: border-box;
  height: var(--ui-btn-h, 28px); padding: 0 12px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius-md);
  background: var(--ui-surface); color: var(--ui-text); font: inherit; font-size: var(--ui-fs-caption); font-weight: 550;
  line-height: 16px; white-space: nowrap; text-decoration: none; cursor: pointer;
}
.ui-chip:hover { border-style: solid; background: var(--ui-surface-2); }
.ui-chip:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
.ui-chip:is([aria-pressed="true"], [aria-current="page"]) { border-style: solid; border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
@media (pointer: coarse), (max-width: 47.9975em) { .ui-chip { height: auto; min-height: 44px; min-width: 44px; } }
/* Card stack: `<div class="ui-stack"><div class="ui-card">…</div><div class="ui-card">…</div></div>`. 16px between blocks,
   Polaris BlockStack gap 400, the admin's spacing between cards on a page. `--tight` is 8 (inside a card). */
.ui-stack { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--ui-space-4); }
.ui-stack--tight { gap: var(--ui-space-2); }

/* ═══ @gemboss/ui src/chat.css ═══ */
/*
 * The chat BODY — what an answer looks like, wherever a GemBoss surface puts a chat (01/10/2026).
 *
 * Chris: every surface's AI assistant (gemcare, content-radar, partner-radar, GemCommunity) must be built from the
 * GemBoss UI framework. Until today only the FRAME was shared (assistant.css: popup / bar / full screen; shell.css:
 * the thread rail); the body lived in the admin's gemboss-shell.css and the exporter left it out on purpose, so every
 * surface drew its own bubbles, markdown and composer. These are those rules, moved here VERBATIM (same selectors,
 * same values, same order) — the admin @imports this file at the top of its shell, so it is still the one owner.
 *
 * What is here: the header (`.gemboss-ec-head/title/sub`), the message list (`.gemboss-ec-scroll`), messages and
 * bubbles (`.gemboss-ec-msg.user|.assistant`, `.gemboss-ec-bubble`), the working indicator and the streaming cursor
 * (`.gemboss-ec-pending*`, `.gemboss-ec-cursor`), markdown (`.gemboss-md-*`, `.gemboss-ec-md-link`), step narration
 * (`.gemboss-narr*`), starters and option chips (`.gemboss-ec-starter(s)`, `.gemboss-ec-chip(s)`), and the composer
 * (`.gemboss-ec-input` with its textarea and submit button).
 * What is NOT: the panel's geometry (`.gemboss-ec` width, editor columns, tabs), attachments, CTA / action cards,
 * the engine picker and work mode — those are still the admin's, and a surface brings its own. ONE exception, on
 * purpose: the chat panel's touch / mouse TARGET blocks (`@media (pointer: coarse|fine)` below) name every control
 * of the panel, the admin-only ones (tabs, upload, engine picker, work mode, attach, stop) included. They are one
 * rule per pointer kind; split in two they wrote their declarations twice and the admin's shell budget had no room.
 * Those selectors match nothing on a surface without those controls. (`.gemboss-ec-workmode`: the click-area
 * `::before` is here, its label-sizing `::after` stays the admin's.)
 *
 * Markup: see the catalog's "Thân chat" section. Behaviour (streaming, stop, keyboard) belongs to the surface.
 */

.gemboss-ec-head { padding: 16px 16px; border-bottom: 1px solid var(--gemboss-divider); display: flex; flex-direction: column; gap: 2px; }
.gemboss-ec-title { font-family: var(--gemboss-serif); font-size: 15px; font-weight: 600; }
.gemboss-ec-sub { font-size: 12px; color: var(--gemboss-text-muted); }
.gemboss-ec-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 16px; scrollbar-gutter: stable; }
/* Visible thin scrollbar on the chat message list — the default macOS overlay bar is
   hidden until you actively scroll, so the list reads as "can't scroll" (esp. on the
   dedicated Assistant page, a fixed-height chat where the page itself doesn't scroll).
   A persistent thin bar makes the scroll affordance obvious; matches the composer input. */
.gemboss-ec-scroll { scrollbar-width: thin; scrollbar-color: rgba(31,30,29,0.22) transparent; }
.gemboss-ec-scroll::-webkit-scrollbar { width: 9px; }
.gemboss-ec-scroll::-webkit-scrollbar-track { background: transparent; }
.gemboss-ec-scroll::-webkit-scrollbar-thumb { background: rgba(31,30,29,0.2); border-radius: 6px; border: 2px solid transparent; background-clip: content-box; }
.gemboss-ec-msg { display: flex; flex-direction: column; gap: 8px; max-width: 100%; }
.gemboss-ec-msg.user { align-items: flex-end; }
.gemboss-ec-msg.assistant { align-items: flex-start; }
.gemboss-ec-bubble {
  /* Cap reading measure: a chat reply stays at a readable line length, not the bubble's full 90%. */
  font-size: var(--gemboss-fs-body); line-height: 1.5; padding: 12px 12px; border-radius: 16px; max-width: min(90%, 68ch);
  white-space: pre-wrap; word-break: break-word;
}
.gemboss-ec-msg.user .gemboss-ec-bubble { background: var(--gemboss-primary); color: var(--gemboss-on-primary, #fff); border-bottom-right-radius: 4px; }
.gemboss-ec-msg.assistant .gemboss-ec-bubble { background: var(--gemboss-surface); color: var(--gemboss-text); border: 1px solid var(--gemboss-divider); border-bottom-left-radius: 4px; }
.gemboss-ec-input button[type="submit"] svg { width: 18px; height: 18px; fill: currentColor; }
/* Live "working" heartbeat — the Claude-Code-style running asterisk shown before the first token.
   Slim, muted, single line so the chat stays clean; the motion (pulsing ✳, crossfading verb, live
   counter) is what makes the wait feel fast instead of frozen. */
.gemboss-ec-pending { display: inline-flex; align-items: center; gap: 8px; padding: 6px 2px; font-size: var(--gemboss-fs-caption); color: var(--gemboss-text-muted); }
.gemboss-ec-pending-star { color: var(--gemboss-primary); font-size: 12px; line-height: 1; animation: gemboss-star-pulse 1.1s ease-in-out infinite; }
.gemboss-ec-pending-verb { color: var(--gemboss-text); animation: gemboss-verb-in 0.4s ease-out; }
.gemboss-ec-pending-meta { font-variant-numeric: tabular-nums; opacity: 0.7; font-size: var(--gemboss-fs-caption); }
@keyframes gemboss-star-pulse { 0%,100% { transform: scale(0.85); opacity: 0.55; } 50% { transform: scale(1.15); opacity: 1; } }
@keyframes gemboss-verb-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }
/* Typewriter cursor — a thin blinking bar trailing the answer while tokens stream in. */
.gemboss-ec-cursor { display: inline-block; width: 2px; height: 1.05em; margin-left: 2px; vertical-align: -2px; background: var(--gemboss-primary); border-radius: 2px; animation: gemboss-cursor-blink 1s steps(1) infinite; }
@keyframes gemboss-cursor-blink { 0%,45% { opacity: 1; } 50%,95% { opacity: 0.15; } 100% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .gemboss-ec-pending-star, .gemboss-ec-cursor { animation: none; }
}
/* Markdown link inside the answer bubble — a friendly inline link, never a raw path. */
.gemboss-ec-md-link { color: var(--gemboss-primary-dark); font-weight: 600; text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--gemboss-primary) 35%, transparent); cursor: pointer; }
.gemboss-ec-md-link:hover { border-bottom-color: var(--gemboss-primary); }
/* Suggestion starters = compact wrapping CHIPS (pills), not stacked full-width boxes —
   reads as a chat's quick-reply chips and stops one prompt eating a whole row. */
.gemboss-ec-starters { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
.gemboss-ec-starter {
  text-align: left; font-size: 13px; padding: 6px 12px; border-radius: var(--gemboss-radius-full); cursor: pointer;
  background: var(--gemboss-surface); border: 1px solid var(--gemboss-divider); color: var(--gemboss-text);
  white-space: nowrap; max-width: 100%;
}
.gemboss-ec-starter:hover { border-color: var(--gemboss-primary); background: rgba(86, 97, 245, 0.06); }
/* (removed) dead legacy rail styles — `.icon-nav-assistant` + the hover-flyout submenu
   (`.icon-nav-flyout*`). The current nav (IconNav.tsx) is a persistent 240px labeled
   sidebar using `.gnav-*` classes with route-driven sub-nav; there is no 72px icon rail
   and no hover flyout, so these selectors matched nothing. */

/* Composer = ONE bordered rounded shell (ChatGPT/Claude-style): the textarea sits borderless
   inside it and the attach (📎) + send (↑) buttons float within the same box — cleaner than the
   old three-separate-boxes row. The shell is inset from the panel edges (margin) and lifts its
   border to primary on focus-within, so the whole composer reads as one focused field. */
/* TWO ROWS when there are chips (ChatComposer): the textarea on its own full-width row, the toolbar
   under it. The toolbar is where chips of any length live, and when they shared a row with the text
   box the send button was the thing that gave way — measured 22/09/2026 at 320px, it wrapped to a
   THIRD line. With no chips at all, ONE row — see `:has` below.
   8px inside, not 6: at 6 the text sat 13px from the border and the box read as cramped. What the
   toolbar SAYS at each width is ChatComposer's job (data-density), measured rather than guessed. */
.gemboss-ec-input {
  display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
  margin: 12px; padding: 8px;
  border: 1px solid var(--gemboss-divider); border-radius: 16px;
  background: var(--gemboss-surface);
  transition: border-color var(--gemboss-transition-fast) ease, box-shadow var(--gemboss-transition-fast) ease;
}
.gemboss-ec-input:focus-within {
  border-color: var(--gemboss-primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--gemboss-primary) 12%, transparent);
}
.gemboss-ec-input textarea {
  flex: 1 0 100%; min-width: 0; width: 100%; box-sizing: border-box; resize: none; border-radius: 0; background: transparent;
  /* The CHIP's box model — a transparent 1px border and 8px of padding — so the first letter typed
     lines up with the first letter of "Claude Code" below it. It was 6px with no border: the text
     started 3px left of the chips, measured on every host at every width, and the box read as two
     columns. (Box model rather than nine pixels of padding: nine is off the spacing scale the CI guard holds.) */
  border: 1px solid transparent;
  padding: 6px 8px; font-size: var(--gemboss-fs-body); font-family: inherit;
  /* Starts at one line; GemChat's auto-grow JS expands it up to ~3 lines (max-height),
     then scrolls inside (ChatGPT/Gemini-style), and writes overflow-y itself (hidden until
     the cap, auto past it). The DEFAULT here is auto, for every surface that has no such
     JS: it was hidden, and there a message taller than the box could not be scrolled at
     all — wheel and touch do nothing under overflow:hidden. Measured 02/10/2026 in Chromium:
     wheel scrolled 0/9 overflowing boxes with hidden, 9/9 with auto; a box whose text
     fits showed no overflow at 13.5 / 14 / 15px (no rounding scrollbar). Found on
     partner-radar's assistant: 264px of text in a 75px box, unreadable. */
  min-height: 0; max-height: 84px; line-height: 1.5; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: rgba(31,30,29,0.22) transparent; /* thin, not the OS bar */
}
.gemboss-ec-input textarea::-webkit-scrollbar { width: 7px; }
.gemboss-ec-input textarea::-webkit-scrollbar-track { background: transparent; }
.gemboss-ec-input textarea::-webkit-scrollbar-thumb { background: rgba(31,30,29,0.2); border-radius: 4px; border: 2px solid transparent; background-clip: content-box; }
.gemboss-ec-input textarea:focus { outline: none; }
/* Send button — a filled primary circle floating at the shell's bottom-right. inline-flex centers
   the ↑ SVG (the old rule had no flex-centering, so the icon sat high).

   SCOPED TO THE SUBMIT BUTTON, and that word is load-bearing. This rule used to read
   `.gemboss-ec-input button`, which reached EVERY button in the composer — including whatever a host
   passes in through `composerLeading`, a slot that exists precisely so hosts can put their own
   controls here. The 📎 attach button already had to fight it back with a class of its own (see the
   comment above `.gemboss-ec-attach-btn`), which was the first sign that this selector owns more than
   it should. The second was the engine picker: a Polaris Button, rendered into the slot, came out as a
   34px black circle with its label ("Claude Code · Opus") clipped to three characters. Nothing was
   wrong with the picker — this rule was painting it.

   §9.3: one visual owner per component. The composer owns its own send button and nothing that is
   handed to it. Guarded by composer-owns-only-its-own-buttons.test.ts.

   44px, not 34: it is a CONTROL (§9.1, Apple HIG / WCAG 2.5.5 AAA). This branch had already raised
   it — but on the OLD unscoped selector, the very rule main had just narrowed. The two changes were
   fixing different faults in one declaration, so both are kept: main's scope, this branch's size. */
.gemboss-ec-input button[type="submit"] {
  flex: 0 0 auto; width: 44px; height: 44px; min-width: 44px; min-height: 44px; border-radius: 50%; border: 0; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--gemboss-primary); color: var(--gemboss-on-primary, #fff); font-size: 16px; font-weight: 700;
  transition: background var(--gemboss-transition-fast) ease, filter var(--gemboss-transition-fast) ease;
}
.gemboss-ec-input button[type="submit"]:hover:not(:disabled) { background: var(--gemboss-primary-dark); }
.gemboss-ec-input button[type="submit"]:active:not(:disabled) { background: var(--gemboss-primary-dark); filter: brightness(0.94); }
.gemboss-ec-input button[type="submit"]:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: 2px; }
/* Disabled (empty input): a clear neutral grey circle with a muted arrow — reads as
   "inactive" rather than a washed-out faint-primary blob. */
.gemboss-ec-input button[type="submit"]:disabled { background: var(--gemboss-divider); color: var(--gemboss-text-muted); opacity: 1; cursor: default; }
/* ── Narration timeline ── */
.gemboss-narr { list-style: none; margin: 0; padding: 8px 0 0; display: flex; flex-direction: column; gap: 8px; }
.gemboss-narr-row { display: flex; align-items: center; gap: 8px; font-size: var(--gemboss-fs-caption); color: var(--gemboss-text-muted); }
.gemboss-narr-dot { width: 16px; height: 16px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; flex: 0 0 auto; }
.gemboss-narr-row.ok .gemboss-narr-dot { background: var(--gemboss-active-bg); color: var(--gemboss-active-fg); }
.gemboss-narr-row.failed .gemboss-narr-dot { background: #fde8e6; color: var(--gemboss-danger); }
.gemboss-narr-row.running .gemboss-narr-dot { background: transparent; }
/* THE ENGINE'S MARK, in the dot's slot. No pill behind it: the ✓/✕ dots earn their background by
   being a glyph in a circle, and a logo in a coloured circle reads as a badge the engine awarded
   itself. It takes the ROW's colour, so a failed turn's mark goes red with everything else — a mark
   that stayed brand-coloured over a failure would be the one cheerful thing on a broken row. */
.gemboss-narr-dot-engine { background: transparent !important; color: inherit; }
.gemboss-narr-row.ok .gemboss-narr-dot-engine { color: var(--gemboss-text); }
.gemboss-narr-row.failed .gemboss-narr-dot-engine { color: var(--gemboss-danger); }
.gemboss-narr-row.ok .gemboss-narr-text, .gemboss-narr-row.failed .gemboss-narr-text { color: var(--gemboss-text); }
/* A message the seller typed INTO the running turn. It reads as THEIR line, not as a step the agent
   took: full-strength text (the timeline is muted by default) against the page's own text colour, and
   a `❯` prompt caret instead of a status dot. It must be legible at a glance while the agent is still
   writing above it — a seller scanning a long timeline needs to find the moment they interrupted. */
.gemboss-narr-row.steer .gemboss-narr-dot { background: transparent; color: var(--gemboss-text-muted); }
/* `anywhere`, not `normal`: a seller correcting a turn pastes URLs and product handles, and one
   unbroken 60-character token in a 320px chat column is a row that pushes the panel sideways. Measured
   2026-09-23 — the default `overflow-wrap: normal` on this row does not break such a token. */
.gemboss-narr-steer { color: var(--gemboss-text); font-weight: 500; overflow-wrap: anywhere; }
.gemboss-narr-spin { width: 11px; height: 11px; border: 2px solid var(--gemboss-divider-soft); border-top-color: var(--gemboss-primary); border-radius: 50%; animation: gemboss-spin 0.7s linear infinite; }
@keyframes gemboss-spin { to { transform: rotate(360deg); } }
/* Live-reasoning ("thinking") row: muted + italic while streaming, collapsible once settled. */
.gemboss-narr-row.thinking { align-items: flex-start; }
.gemboss-narr-row.thinking .gemboss-narr-dot { color: var(--gemboss-text-muted); background: transparent; }
.gemboss-narr-thinking-live { font-style: italic; color: var(--gemboss-text-muted); white-space: pre-wrap; opacity: 0.9; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.gemboss-narr-thinking { font-size: var(--gemboss-fs-caption); }
.gemboss-narr-thinking > summary { cursor: pointer; color: var(--gemboss-text-muted); list-style: none; user-select: none; }
.gemboss-narr-thinking > summary::-webkit-details-marker { display: none; }
.gemboss-narr-thinking > summary::before { content: "▸ "; }
.gemboss-narr-thinking[open] > summary::before { content: "▾ "; }
.gemboss-narr-thinking-body { margin-top: 6px; padding: 8px 8px; border-left: 2px solid var(--gemboss-divider-soft); color: var(--gemboss-text-muted); font-style: italic; white-space: pre-wrap; line-height: 1.5; }
/* TOOL TRANSCRIPT rows — the Claude-Code shape: a monospace call line, its return value folded
   behind a disclosure. Muted on purpose: the transcript is evidence you can audit, not the answer. */
.gemboss-narr-row.trace code { font-family: var(--gemboss-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: var(--gemboss-fs-caption); word-break: break-word; }
.gemboss-narr-trace-call { color: var(--gemboss-text-muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.gemboss-narr-trace { font-size: var(--gemboss-fs-caption); min-width: 0; }
.gemboss-narr-trace > summary { cursor: pointer; color: var(--gemboss-text-muted); list-style: none; user-select: none;
  /* A tool call with real arguments is long and the chat panel is narrow: a full payload wrapped to five
     lines buries the rows around it. Clamp while closed; opening the row shows the call in full. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.gemboss-narr-trace[open] > summary { display: block; overflow: visible; }
.gemboss-narr-trace > summary::-webkit-details-marker { display: none; }
/* The fold is now the ONLY way to see a payload or a finished thought (both start closed), so its
   summary is a target: ≥24px tall, the list-link tier of Gems/CLAUDE.md §9.1 (WCAG 2.5.8). Measured
   21px before, 13/09. */
.gemboss-narr-trace > summary, .gemboss-narr-thinking > summary { min-height: 24px; box-sizing: border-box; padding-block: 2px; }
.gemboss-narr-trace > summary::before { content: "▸ "; }
.gemboss-narr-trace[open] > summary::before { content: "▾ "; }
.gemboss-narr-trace-body { margin-top: 6px; padding: 8px; border-left: 2px solid var(--gemboss-divider-soft); color: var(--gemboss-text-muted); white-space: pre-wrap; line-height: 1.5; overflow-x: auto; }
@media (max-width: 640px) {
  /* Composer auto-grows from one line (see GemChat) — no fixed floor needed here. */
  .gemboss-ec-input textarea { min-height: 0; }
  .gemboss-ec-scroll { padding: 12px; }
}
/* ── Chat option chips (persona/goal polling) ── */
.gemboss-ec-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
/* 44px because a chip is a CONTROL, not a link in a list — Apple HIG / WCAG 2.5.5 AAA, Gems CLAUDE.md
   §9.1. Measured 2026-09-09 at 390px and 1366px: padding alone rendered these at 34px, ten short, and
   they are the only way to answer a poll or resume an unfinished run on a phone. `min-height` rather
   than `height` so a wrapped two-line label still grows instead of clipping. */
.gemboss-ec-chip {
  background: var(--gemboss-surface); border: 1px solid var(--gemboss-divider); border-radius: var(--gemboss-radius-full);
  padding: 8px 16px; font-size: 13px; cursor: pointer; color: var(--gemboss-text);
  min-height: 44px;
}
.gemboss-ec-chip:hover { border-color: var(--gemboss-primary); color: var(--gemboss-active-fg); }
.gemboss-ec-chip:disabled { opacity: 0.5; cursor: default; }
/* ════ Mouse targets — the same 44px, bought with an overlay instead of a bigger box ═══════
   §9.1 asks 44px of a CONTROL, and it does not say "on touch only". The chat panel's controls are
   28-37px under a mouse, which clears WCAG 2.5.8 AA (24px) and not 2.5.5 AAA (44px). The dense look
   is deliberate and worth keeping, so the TARGET grows and the ink does not — the same trick the
   editor topbar already uses on its device buttons.

   WHICH ONES, AND WHY NOT THE REST. A target that reaches into a neighbour steals clicks meant for
   it, which is worse than a small one. So this was decided by measuring the gap between every pair
   of vertically adjacent controls at 390 / 768 / 1366 and asking whether BOTH could grow into it —
   a control's own clearance is not enough, because its neighbour is growing too. Measured
   2026-09-09, three pairs could not:

     starter chips, row to row   gap 8px, both need 7  -> the ROW GAP goes to 16px below, which buys
                                                          it without changing how the chips look
     textarea / engine picker    gap 8px, needs 10     -> the textarea does not grow (see below)
     textarea / send             gap 6px, needs 9      -> same

   THE MESSAGE BOX IS EXCLUDED ON PURPOSE. It is a full-width field 37px tall: its AREA is already
   enormous, the only dimension short of 44 is its height, and it is the one control here you reach
   by clicking INTO text rather than at a point. Growing it also collides with both of its
   neighbours. With it left alone, everything else fits: send needs 5 of the 6px above it, the
   picker needs 8 of 8.

   Height only. Horizontally these controls are already 34-196px wide, and an overlay that also grew
   sideways would collide with the chip beside it, which is 8px away. */
@media (pointer: fine) {
  .gemboss-ec-starters { row-gap: 16px; }
  .gemboss-ec-tab,
  .gemboss-ec-starter,
  .gemboss-ec-upload,
  .gemboss-ec-enginepick button,
  .gemboss-ec-workmode,
  .gemboss-ec-input .gemboss-ec-attach-btn,
  .gemboss-ec-stop,
  /* Shares the toolbar row with Stop, which is already 44px tall — so a 44px overlay here grows into
     a row that is that tall already, and reaches into nothing above or below it. */
  .gemboss-ec-stop-send,
  .gemboss-ec-input button[type="submit"] { position: relative; }
  .gemboss-ec-tab::after,
  .gemboss-ec-starter::after,
  .gemboss-ec-upload::after,
  .gemboss-ec-enginepick button::after,
  /* `::before` on the work-mode wrapper: its `::after` is the invisible label that SIZES it, and a
     second use of the same pseudo-element would silently replace that. */
  .gemboss-ec-workmode::before,
  .gemboss-ec-input .gemboss-ec-attach-btn::after,
  .gemboss-ec-stop::after,
  .gemboss-ec-stop-send::after,
  .gemboss-ec-input button[type="submit"]::after {
    content: "";
    position: absolute;
    left: 0; right: 0;
    top: 50%;
    transform: translateY(-50%);
    height: 44px;
    /* No background at all: a pseudo-element is transparent already, and the `background` shorthand
       is banned in this family because it resets `background-image` — the work-mode caret. My own
       guard from #2158 caught this line, which is the guard doing exactly what it was written for. */
    pointer-events: auto;
    /* Above the panel chrome it overhangs — the chrome is not interactive, so a click landing on the
       control the overlay belongs to is the point.
       This does NOT rescue a control sitting flush against the top of `.gemboss-ec-scroll`: an
       overflow container clips its absolutely-positioned descendants, and z-index cannot cross that.
       Measured at 768px, where the first row of starter chips begins at the exact pixel the header
       ends: their target is 37px until the list scrolls, and 44px after. A scroll position, not a
       layout constant — the same chips measure 44px at 390, 1366 and 1920. Left alone rather than
       bought with padding on the scroll area, which every seller would see all the time to fix a
       target in one transient position. */
    z-index: 1;
  }
}
@media (pointer: coarse) {
  .gemboss-ec-input button[type="submit"] { width: 44px; height: 44px; font-size: 17px; }
  .gemboss-ec-starter, .gemboss-ec-opt { min-height: 44px; }
  .gemboss-ec-input textarea { min-height: 44px; }
}
/* ── The answer bubble's markdown ────────────────────────────────────────────────────────────────
 *
 * The bubble understood bold, links and line breaks. The models writing into it do not restrict
 * themselves to three things: a question about pages came back as a markdown TABLE and rendered as
 * literal pipes down the page. These are the styles for what it actually writes. */

.gemboss-md-p { margin: 0 0 8px; }
.gemboss-md-p:last-child { margin-bottom: 0; }
/* Demoted one level in the markup (h1 → h3) because the bubble sits under the page's own headings.
 * The SIZE hierarchy is kept here, because that is what a reader uses to skim. */
.gemboss-md-h { margin: 16px 0 6px; font-weight: 650; line-height: 1.3; }
.gemboss-md-h:first-child { margin-top: 0; }
/* RESTORED after #3370 deleted these three. MarkdownLite.tsx builds the class by interpolation —
 * `gemboss-md-h gemboss-md-h${b.level}` — so the literal `gemboss-md-h1` exists in no source file
 * and in no compiled chunk, and all three of that PR's detection passes read them as dead. The gate
 * now carries the prefix rule that catches this shape. Do not "clean" these up. */
.gemboss-md-h1 { font-size: 17px; }
.gemboss-md-h2 { font-size: 15px; }
.gemboss-md-h3 { font-size: var(--gemboss-fs-body); color: var(--gemboss-text-subdued, #616161); }
.gemboss-md-list { margin: 0 0 8px; padding-left: 20px; }
.gemboss-md-list li { margin: 2px 0; }
.gemboss-md-quote {
  margin: 0 0 8px;
  padding: 2px 0 2px 12px;
  border-left: 3px solid var(--gemboss-border, #e3e3e3);
  color: var(--gemboss-text-subdued, #616161);
}
.gemboss-md-rule { margin: 12px 0; border: 0; border-top: 1px solid var(--gemboss-border, #e3e3e3); }
/* Inline code: a chip, so an identifier stops reading as prose. */
.gemboss-md-code {
  padding: 1px 6px;
  border-radius: 6px;
  background: var(--gemboss-surface-subdued, #f1f1f1);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.9em;
  word-break: break-word;
}
/* Tables scroll INSIDE their own box. The chat lives in a 380px sidebar in the theme editor, and a
 * five-column table there would otherwise push the whole panel sideways — §9.1's rule that wide
 * content scrolls in its own container, not the page. */
.gemboss-md-tablewrap { margin: 0 0 12px; overflow-x: auto; }
.gemboss-md-table { border-collapse: collapse; font-size: var(--gemboss-fs-caption); min-width: 100%; }
.gemboss-md-table th,
.gemboss-md-table td {
  border: 1px solid var(--gemboss-border, #e3e3e3);
  padding: 6px 8px;
  text-align: left;
  vertical-align: top;
  white-space: nowrap;
}
.gemboss-md-table th { background: var(--gemboss-surface-subdued, #f7f7f7); font-weight: 600; }
/* A picture in an answer (only from hosts markdown-blocks.ts trusts). Never wider than the bubble — the
 * chat can sit in a 380px sidebar — and capped in height so five thumbnails read as a shortlist, not
 * five screens. The link around it is the tap target: keyboard focus has to be visible on it. */
.gemboss-md-imglink { display: inline-block; max-width: 100%; margin: 4px 0 8px; border-radius: 8px; line-height: 0; }
.gemboss-md-imglink:focus-visible { outline: 2px solid var(--gemboss-focus, var(--ui-focus, #5661f5)); outline-offset: 2px; }
.gemboss-md-img {
  display: block;
  max-width: 100%;
  max-height: 320px;
  height: auto;
  border: 1px solid var(--gemboss-border, #e3e3e3);
  border-radius: 8px;
  background: var(--gemboss-surface-subdued, #f7f7f7);
  object-fit: contain;
}
/* A code block, with the fold. A 200-line answer should not push everything after it off screen. */
.gemboss-md-codeblock {
  position: relative;
  margin: 0 0 12px;
  border: 1px solid var(--gemboss-border, #e3e3e3);
  border-radius: 8px;
  background: var(--gemboss-surface-subdued, #f7f7f7);
  overflow: hidden;
}
.gemboss-md-codelang {
  display: block;
  padding: 4px 12px 0;
  color: var(--gemboss-text-subdued, #8a8a8a);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px;
}
.gemboss-md-codeblock pre { margin: 0; padding: 8px 12px; overflow-x: auto; }
.gemboss-md-codeblock code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 12px;
  line-height: 1.5;
  white-space: pre;
}
.gemboss-md-codemore {
  display: block;
  width: 100%;
  padding: 6px 12px;
  border: 0;
  border-top: 1px solid var(--gemboss-border, #e3e3e3);
  background: transparent;
  color: var(--gemboss-text-subdued, #616161);
  font-size: 12px;
  text-align: left;
  cursor: pointer;
}
.gemboss-md-codemore:hover { background: rgba(0, 0, 0, 0.03); }
/* ── The payload, live, in the chat ──────────────────────────────────────────────────────────────
   A trace row said `apply_section_edit(section_id:"hero")` — that something happened, and nothing about
   what. This is what. Open by default: folding it would put the answer back behind a click. */
.gemboss-narr-row.source { align-items: flex-start; }
.gemboss-narr-source { min-width: 0; flex: 1; }
.gemboss-narr-source-head { font-size: 11px; color: var(--gemboss-text-muted, #686b71); margin-bottom: 4px; }
.gemboss-narr-source-head code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.gemboss-narr-source-code {
  margin: 0; padding: 8px 12px; border-radius: 8px; max-height: 220px; overflow: auto;
  background: var(--gemboss-primary-soft, #f2f3f5); border: 1px solid var(--gemboss-divider, #e5e7ea);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--gemboss-fs-micro); line-height: 17px;
  white-space: pre; tab-size: 2;
}
/* Light-ground syntax colours for the payload rows (the code-highlight.ts `c-*` spans). Contrast checked
   against #fff: every one clears 4.5:1. (The page editor's Code tab shared them until it was removed, #2651.) */
.gemboss-narr-source-code .c-com { color: #6a7a86; font-style: italic; }
.gemboss-narr-source-code .c-str { color: #9a3412; }
.gemboss-narr-source-code .c-num { color: #1d6fa5; }
.gemboss-narr-source-code .c-kw  { color: #7c3aed; font-weight: 600; }
.gemboss-narr-source-code .c-key { color: #0b6b3a; }
.gemboss-narr-source-code .c-tag { color: #b0197a; }
.gemboss-narr-source-code .c-attr{ color: #7c3aed; }
.gemboss-narr-source-code .c-pun { color: #6a6d73; }
@media (max-width: 520px) {
  .gemboss-narr-source-code { max-height: 160px; font-size: 11px; }
}
/* A checklist row's box IS its marker; the dot column stays for alignment and shows nothing. */
.gemboss-narr-dot-quiet { visibility: hidden; }

/* ═══ @gemboss/ui src/chat-composer.css ═══ */
/*
 * The chat COMPOSER — @gemboss/ui (moved from apps/admin/src/components/chat/chat-composer.css, 02/10/2026).
 *
 * The toolbar row under the text box (chips lead, tools, status, the one action pinned right), the one-row
 * layout when there are no chips, the engine picker's layout and the Stop button. A second surface needs the
 * toolbar — gemcare's assistant composer is a text box and a send button, i.e. the one-row case — and a copy
 * would be a second owner. The box itself (`form.gemboss-ec-input`, its textarea and send button) is in chat.css.
 *
 * A SEPARATE file from chat.css on purpose, as it was a separate file in the admin: chat.css rides the shell's
 * first load on every screen, while this sheet follows the chat chunk — the admin imports it from ChatComposer
 * and EnginePicker, not from the layout (the shell sat at 239.1 / 240 KB brotli when this moved). Other surfaces
 * get it inside gemboss-ui.css. Rules and their order are unchanged.
 *
 * Every selector here names a composer-only class (chiplabel · chipmark · chipsub · enginepick · enginepick-btn ·
 * stop · toolbar · toolbar-lead · toolbar-tools), so a rule only matches while a composer is on screen.
 */

/* The toolbar row: chips on the left, then status, then the one action button pinned right.
   `align-items: center` — the old row was `flex-end`, so 28px chips sat on the BOTTOM edge of a 44px
   button and their centres ran 8px below its centre. Measured at 1366: chips cy 819, send cy 811.
   The lead group is the only thing that shrinks. Send is `flex: none` and the chips inside the lead
   carry `min-width: 0`, so a narrow composer ellipsises a label instead of pushing send off the row. */
.gemboss-ec-toolbar { flex: 1 0 100%; display: flex; align-items: center; gap: 4px; min-width: 0; }
.gemboss-ec-toolbar-lead { flex: 1 1 auto; display: flex; align-items: center; gap: 4px; min-width: 0; }
/* The lead GROWS, and that is what pins status + action to the right edge. With nothing in it (a
   browser tab has no engine picker, a driver without work mode has no select) it is removed, and
   whatever follows takes the auto margin instead. */
.gemboss-ec-toolbar-lead:empty { display: none; }
.gemboss-ec-toolbar-lead:empty + * { margin-left: auto; }
/* TOOLS ARE NOT CHIPS. The 📎 lives here, never in the lead: the composer gate reads `leading` as the
   chip strip twice over — `edge` wants the first chip's text flush with the typed text, and `oneRow`
   only runs when the lead is EMPTY. A button parked in the lead pushed the chips 47px right AND left
   `oneRow` unable to fail at all; the gate's own negative control said so out loud
   ("MÙ với oneRow — đối chứng ĐÃ QUAN SÁT ĐƯỢC và vẫn xanh") once the breakage stopped registering.
   `flex: none` so it never steals the shrink budget the lead needs to ellipsise a long chip label. */
.gemboss-ec-toolbar-tools { display: flex; align-items: center; gap: 4px; flex: none; min-width: 0; }
.gemboss-ec-toolbar-tools:empty { display: none; }
/* NO CHIPS ⇒ ONE ROW. Product Studio has nothing in its lead, and it still spent a whole 44px row on
   a lone send button beside empty space — 99-120px tall for a one-line box. The text box shares the
   row now and send sits at its bottom edge, where a growing message keeps it. */
.gemboss-ec-input:has(> .gemboss-ec-toolbar > .gemboss-ec-toolbar-lead:empty) { flex-wrap: nowrap; align-items: flex-end; }
.gemboss-ec-input:has(> .gemboss-ec-toolbar > .gemboss-ec-toolbar-lead:empty) > textarea { flex: 1 1 0; }
.gemboss-ec-input:has(> .gemboss-ec-toolbar > .gemboss-ec-toolbar-lead:empty) > .gemboss-ec-toolbar { flex: 0 0 auto; }
/* The engine picker is a `gemboss-ec-chipctl` like the select beside it — its colour, weight, padding
   and hover all come from that one rule. What is left here is layout: Popover wraps the activator in an
   element of its own, and the chip can only shrink if every box between it and the toolbar can.
   DESCENDANT, not child, for that reason — `>` matched nothing when the button sat inside Polaris'
   wrapper, measured, after the first attempt left it unstyled. */
.gemboss-ec-enginepick { flex: 0 3 auto; min-width: 0; display: inline-flex; }
.gemboss-ec-enginepick > * { min-width: 0; max-width: 100%; display: flex; }
.gemboss-ec-enginepick button {
  min-width: 0; max-width: 100%;
  /* The same caret the work-mode select draws, at the same inset, so the two chips read as one type. */
  padding-right: 24px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 7px center;
}
.gemboss-ec-chiplabel { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gemboss-ec-chipmark { display: none; }
.gemboss-ec-chipmark .gemboss-engine-mark { display: block; width: 16px; height: 16px; margin: 2px 0; }
/* The steps ChatComposer measured its way down to (data-density on the form). The ellipsis above stays
   as the floor for an engine with no mark, never as the plan. */
.gemboss-ec-input:not([data-density="full"]) .gemboss-ec-chipsub { display: none; }
.gemboss-ec-input:is([data-density="icon"], [data-density="tight"]) .gemboss-ec-enginepick-btn.has-mark .gemboss-ec-chiplabel { display: none; }
.gemboss-ec-input:is([data-density="icon"], [data-density="tight"]) .gemboss-ec-enginepick-btn.has-mark .gemboss-ec-chipmark { display: block; }
/* A mark does not ellipsise, so once the chip IS the mark it holds its width — shrunk further it slid
   under the mode chip beside it. The overflow that leaves is what tells ChatComposer to step on. */
.gemboss-ec-input:is([data-density="icon"], [data-density="tight"]) .gemboss-ec-enginepick:has(.has-mark) { flex-shrink: 0; }
/* Open: it is showing a popover, so it stops being quiet. */
.gemboss-ec-enginepick button[aria-expanded="true"] {
  color: var(--gemboss-text); background-color: var(--gemboss-hover, rgba(0,0,0,0.05));
}
@media (pointer: coarse) {
  /* SCOPED, and that word is load-bearing — this line was DEAD as `.gemboss-ec-stop`.
     A media query adds no specificity, so the unscoped rule for the same class, declared ~1800 lines
     LATER at its mouse size, beat this one on source order alone. Measured
     2026-09-09 in a real coarse-pointer context: `matchMedia('(pointer: coarse)').matches === true`
     and the computed width unchanged at its mouse size. Every phone got a Stop button ten pixels
     under the target, while the rule meant to
     fix that sat here reading as done — and the source test beside this block could not see it,
     because a name being PRESENT is not the same fact as a rule WINNING.
     `.gemboss-ec-input` is the same idiom the attach button above already uses for the same reason. */
  .gemboss-ec-input .gemboss-ec-stop { width: 44px; height: 44px; min-width: 44px; }
  /* SCOPED for the reason above: this control's own look is declared later, in the shell sheet. */
  .gemboss-ec-input .gemboss-ec-stop-send { min-height: 44px; }
  .gemboss-ec-enginepick button { min-height: 44px; }
}
/* Sized like the send button it replaces, so the composer does not reflow when a turn starts —
 * a control that moves under the cursor is a control people misclick. So it is THE SAME CIRCLE AS SEND
 * — same 44px, same 50% radius, only the colour and the glyph change. It used to be a 32px red
 * rounded square beside a 44px round send: two shapes, two sizes, one slot. */
.gemboss-ec-stop {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  min-width: 44px;
  border: none;
  border-radius: 50%;
  background: var(--gemboss-critical, #d72c0d);
  color: #fff;
  cursor: pointer;
}
.gemboss-ec-stop svg { width: 12px; height: 12px; fill: currentColor; }
.gemboss-ec-stop:disabled { opacity: 0.6; cursor: default; }
.gemboss-ec-stop:hover { filter: brightness(1.08); }
.gemboss-ec-stop:focus-visible { outline: 2px solid var(--gemboss-focus, var(--ui-focus, #5661f5)); outline-offset: 2px; }

/* ═══ @gemboss/ui src/portal.css ═══ */
/* ═══ Portal pieces — @gemboss/ui 0.7.0 (04/10/2026) ════════════════════════════════════════════
   TopbarNav, Avatar, Leaderboard, Podium (portal.tsx), SearchDialog (dialog.tsx), ThemeToggle (theme.tsx).
   Component rules only, no rule on :root, html or body (a test holds that): a surface with its own skin
   can take this file with gemboss-tokens.css alone. The dark palette is theme-dark.css. */

/* ── TopbarNav: section tabs in the topbar, after the brand (Featurebase's "Feedback · Roadmap"). The
   active tab is underlined on the topbar's bottom edge. 44px targets under a finger, 36 under a mouse
   (the kit's two-tier rule, as TopbarSearch). A phone scrolls the row sideways rather than wrapping it. */
.gb-topnav { display: flex; align-items: stretch; gap: 4px; min-width: 0; height: 100%; margin-left: 12px; padding-left: 12px;
  border-left: 1px solid var(--gemboss-divider); overflow-x: auto; scrollbar-width: none; }
.gb-topnav::-webkit-scrollbar { display: none; }
/* Each tab is as tall as the topbar, so its underline lands on the topbar's bottom edge. */
.gb-topnav-link { position: relative; display: inline-flex; align-items: center; align-self: stretch; flex: 0 0 auto; min-height: 36px; padding: 0 10px;
  border-radius: 8px; color: var(--gemboss-text-muted); font-size: 14px; font-weight: 520; text-decoration: none; white-space: nowrap; }
.gb-topnav-link:hover { color: var(--gemboss-text-strong); background: var(--gemboss-hover); }
.gb-topnav-link:focus-visible { outline: 2px solid var(--gemboss-accent); outline-offset: -2px; }
.gb-topnav-link[aria-current="page"] { color: var(--gemboss-text-strong); font-weight: 640; }
.gb-topnav-link[aria-current="page"]::after { content: ""; position: absolute; left: 10px; right: 10px; bottom: -1px; height: 2px;
  border-radius: 2px; background: var(--gemboss-text-strong); }
@media (pointer: coarse) { .gb-topnav-link { min-height: 44px; } }
/* A topbar that carries tabs has no centre search: brand + tabs take the room, actions sit right. The
   default columns keep 280–560px for a search box, which cut "Roadmap" to "Ro" and the brand to "G…". */
.gemboss-platform-topbar:has(.gb-topnav):has(> .gemboss-topbar-center:empty) { grid-template-columns: minmax(0, 1fr) auto; }
.gemboss-platform-topbar:has(.gb-topnav) > .gemboss-topbar-center:empty { display: none; }
.gemboss-topbar-start:has(.gb-topnav) { min-width: 0; height: 100%; }
.gemboss-topbar-start:has(.gb-topnav) > .gb-brand { flex: 0 0 auto; }
@media (max-width: 760px) { .gb-topnav { margin-left: 8px; padding-left: 8px; } .gb-topnav-link { min-height: 44px; padding: 0 8px; }
}
.gb-topnav-row { display: flex; align-items: stretch; gap: 4px; min-width: 0; height: 100%; }
/* The folded form (under 600px): one button naming the open tab, the tabs in a popover under it. */
.gb-topnav-fold { display: none; position: relative; align-self: stretch; }
.gb-topnav-fold > summary { list-style: none; cursor: pointer; gap: 4px; height: 100%; }
.gb-topnav-fold > summary::-webkit-details-marker { display: none; }
.gb-topnav-fold-btn[aria-current="page"]::after { display: none; }
.gb-topnav-chevron { flex: 0 0 auto; transition: transform .15s ease; }
.gb-topnav-fold[open] .gb-topnav-chevron { transform: rotate(180deg); }
.gb-topnav-pop { position: fixed; top: 56px; left: 8px; min-width: 200px; padding: 6px; display: grid; gap: 2px; z-index: 60; }
.gb-topnav-item { display: flex; align-items: center; min-height: 44px; padding: 0 12px; border-radius: 8px;
  color: var(--gemboss-text); font-size: 14px; font-weight: 520; text-decoration: none; }
.gb-topnav-item:hover { background: var(--gemboss-hover); }
.gb-topnav-item:focus-visible { outline: 2px solid var(--gemboss-accent); outline-offset: -2px; }
.gb-topnav-item[aria-current="page"] { color: var(--gemboss-active-fg); background: var(--gemboss-active-bg); font-weight: 640; }
@media (max-width: 600px) {
  .gb-topnav { overflow: visible; }
  .gb-topnav-row { display: none; }
  .gb-topnav-fold { display: flex; }
}
@media (prefers-reduced-motion: reduce) { .gb-topnav-chevron { transition: none; } }

/* Text for screen readers only ("Rank 1:", ", you"). Its own rule, not ui.css's .ui-sr-only: a surface that
   takes portal.css without the rest of the kit (GemCommunity) would otherwise print it on the page. */
.gb-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; }

/* ── Avatar: picture or initials on one of eight tones picked from the name. */
.gb-avatar { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; border-radius: 50%; overflow: hidden;
  font-weight: 700; line-height: 1; letter-spacing: .01em; user-select: none; }
.gb-avatar--24 { width: 24px; height: 24px; font-size: 9.5px; }
.gb-avatar--32 { width: 32px; height: 32px; font-size: 11px; }
.gb-avatar--40 { width: 40px; height: 40px; font-size: 13px; }
.gb-avatar--56 { width: 56px; height: 56px; font-size: 18px; }
.gb-avatar-img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* One hue per tone; the disc and the initials are both mixed from it (light: dark initials on a pale
   disc; dark: light initials on a deep disc). portal.test.tsx checks every tone clears 4.5:1 in both. */
.gb-avatar[data-tone] { background: color-mix(in srgb, var(--gb-tone) 16%, #ffffff); color: color-mix(in srgb, var(--gb-tone) 70%, #000000); }
.gb-avatar[data-tone="0"] { --gb-tone: #5661f5; }
.gb-avatar[data-tone="1"] { --gb-tone: #0a8f6a; }
.gb-avatar[data-tone="2"] { --gb-tone: #c26a04; }
.gb-avatar[data-tone="3"] { --gb-tone: #db2777; }
.gb-avatar[data-tone="4"] { --gb-tone: #0891b2; }
.gb-avatar[data-tone="5"] { --gb-tone: #7c3aed; }
.gb-avatar[data-tone="6"] { --gb-tone: #dc2626; }
.gb-avatar[data-tone="7"] { --gb-tone: #6b7280; }

/* ── Leaderboard: medals 1–3, then the rank; avatar, name, count; the leader's count in gold. Rows are
   44px, so neighbours need no gap (§9.1). */
.gb-leaderboard { display: grid; gap: 6px; min-width: 0; }
.gb-leaderboard-title { margin: 0; display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 650; letter-spacing: .04em;
  text-transform: uppercase; color: var(--gemboss-text-muted); }
.gb-leaderboard-caption { margin: -2px 0 2px; font-size: 12px; color: var(--gemboss-text-muted); }
.gb-leaderboard-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.gb-leaderboard-link { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 6px; border-radius: 8px;
  color: var(--gemboss-text); text-decoration: none; }
a.gb-leaderboard-link:hover { background: var(--gemboss-hover); }
a.gb-leaderboard-link:focus-visible { outline: 2px solid var(--gemboss-accent); outline-offset: -2px; }
.gb-leaderboard-rank { width: 20px; flex: 0 0 auto; text-align: center; font-size: 15px; line-height: 1; }
.gb-leaderboard-rank.is-number { font-size: 12px; font-weight: 700; color: var(--gemboss-text-muted); font-variant-numeric: tabular-nums; }
.gb-leaderboard-name { flex: 1 1 auto; min-width: 0; font-size: 13px; font-weight: 560; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gb-leaderboard-value { flex: 0 0 auto; font-size: 13px; font-weight: 650; color: var(--gemboss-text-muted); font-variant-numeric: tabular-nums; }
.gb-leaderboard-row.is-first .gb-leaderboard-value { color: #8a5a00; font-weight: 750; }
/* The viewer's own row and podium slot: the selected tint, never a colour of its own. */
.gb-leaderboard-row.is-current > .gb-leaderboard-link { background: var(--gemboss-active-bg); }
.gb-leaderboard-badge { flex: 0 1 auto; min-width: 0; overflow: hidden; }
.gb-leaderboard-all { justify-self: center; display: inline-flex; align-items: center; min-height: 24px; font-size: 12px; font-weight: 600;
  color: var(--gemboss-text-muted); text-decoration: none; }
.gb-leaderboard-all:hover { color: var(--gemboss-text-strong); text-decoration: underline; }

/* ── Podium: second · first · third, the first raised. */
.gb-podium { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; align-items: end; }
.gb-podium-link { display: grid; justify-items: center; gap: 6px; padding: 14px 8px; border: 1px solid var(--gemboss-divider); border-radius: 12px;
  background: var(--gemboss-surface); color: var(--gemboss-text); text-decoration: none; text-align: center; }
a.gb-podium-link:hover { background: var(--gemboss-hover); }
.gb-podium-slot { min-width: 0; }
/* A podium with fewer than three people keeps its place: second and third stay empty columns. */
.gb-podium-slot.is-empty { visibility: hidden; }
.gb-podium-slot--1 .gb-podium-link { padding-top: 22px; padding-bottom: 20px; border-color: color-mix(in srgb, #d4a017 45%, var(--gemboss-divider)); }
.gb-podium-slot--2 .gb-podium-link { padding-bottom: 12px; }
.gb-podium-slot--3 .gb-podium-link { padding-bottom: 8px; }
.gb-podium-medal { font-size: 22px; line-height: 1; }
.gb-podium-name { max-width: 100%; font-size: 13px; font-weight: 620; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gb-podium-value { font-size: 15px; font-weight: 750; font-variant-numeric: tabular-nums; }
.gb-podium-slot--1 .gb-podium-value { color: #8a5a00; }
.gb-podium-unit { font-size: 11px; font-weight: 500; color: var(--gemboss-text-muted); }
.gb-podium-slot.is-current .gb-podium-link { border-color: var(--gemboss-text-strong); background: var(--gemboss-active-bg); }

/* ── SearchDialog: the topbar's search button, and the popup (640 wide, near the top, like Featurebase's). */
.gb-search-trigger, .gb-theme-toggle { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; flex: 0 0 auto;
  border: 0; border-radius: 8px; background: transparent; color: var(--gemboss-text-muted); cursor: pointer; }
.gb-search-trigger:hover, .gb-theme-toggle:hover { background: var(--gemboss-hover); color: var(--gemboss-text-strong); }
.gb-search-trigger:focus-visible, .gb-theme-toggle:focus-visible { outline: 2px solid var(--gemboss-accent); outline-offset: -2px; }
@media (pointer: coarse), (max-width: 760px) { .gb-search-trigger, .gb-theme-toggle { width: 44px; height: 44px; } }
dialog.ui-modal.gb-search { width: min(640px, 100%); margin-top: 8vh; max-height: min(560px, calc(100dvh - 16vh)); }
.gb-search-box { position: relative; display: flex; align-items: center; border-bottom: 1px solid var(--ui-border); }
.gb-search-icon { position: absolute; left: 18px; color: var(--ui-text-muted); pointer-events: none; }
.gb-search-input { flex: 1 1 auto; min-width: 0; height: 52px; padding: 0 18px 0 46px; border: 0; background: transparent; color: var(--ui-text);
  font: inherit; font-size: 15px; outline: none; }
.gb-search-input::-webkit-search-cancel-button { display: none; }
.gb-search-results { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 8px; }
.gb-search-section + .gb-search-section { margin-top: 8px; }
.gb-search-section-title { padding: 6px 10px; font-size: 12px; font-weight: 600; color: var(--ui-text-muted); }
.gb-search-item { display: grid; gap: 2px; min-height: 44px; align-content: center; padding: 6px 10px; border-radius: 8px; color: var(--ui-text); text-decoration: none; }
.gb-search-item.is-active { background: var(--ui-surface-2); }
.gb-search-item-title { font-size: 14px; font-weight: 520; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gb-search-item-meta { display: flex; gap: 8px; align-items: center; font-size: 12px; color: var(--ui-text-muted); }
.gb-search-empty { margin: 0; padding: 18px 18px 0; font-size: 13px; color: var(--ui-text-muted); }
.gb-search-empty:empty { padding: 0; }
.gb-search-foot { display: flex; gap: 16px; justify-content: flex-end; padding: 8px 14px; border-top: 1px solid var(--ui-border);
  font-size: 12px; color: var(--ui-text-muted); }
@media (max-width: 760px) { dialog.ui-modal.gb-search { margin-top: 12px; } .gb-search-foot { display: none; } }

/* ═══ @gemboss/ui src/theme-dark.css ═══ */
/* ═══ Dark palette — @gemboss/ui 0.7.2 ═══════════════════════════════════════════════════════════════
   Split out of portal.css (04/10/2026). `ThemeToggle` / `themeScript` set data-theme on <html>, and this
   file turns every --gemboss-* colour dark under it. It lives apart so that a surface which takes only
   the leaderboard (gemboss-tokens.css + portal.css) does not inherit it: GemCommunity sets
   data-theme="dark" for its own sign-in pages and remembers it, and the rule
   `:root[data-theme="dark"] body` here outranks its body rule, so a member who once picked dark there
   would get a dark page behind light cards. gemboss-ui.css still carries this file. */
/* Dark palette. `ThemeToggle` / `themeScript` set data-theme on <html>; a surface without them stays
   light. Only colours change: the components above and every kit class read these tokens. */
:root[data-theme="dark"] {
  color-scheme: dark;
  --gemboss-primary: #f2f3f5;
  --gemboss-primary-dark: #ffffff;
  --gemboss-primary-soft: #23242a;
  --gemboss-on-primary: #0d0d12;
  --gemboss-accent: #7c84ff;
  --gemboss-accent-strong: #a3a9ff;
  --gemboss-accent-soft: #1f2140;
  --gemboss-text-strong: #f2f3f5;
  --gemboss-text: #e3e4e8;
  --gemboss-text-muted: #9a9ca6;
  --gemboss-surface: #16171d;
  --gemboss-canvas: #0d0d12;
  --gemboss-app-bg: #0d0d12;
  --gemboss-rail-bg: #0d0d12;
  --gemboss-panel-bg: #16171d;
  --gemboss-topbar-bg: rgba(13, 13, 18, 0.96);
  --gemboss-nav-bg: #0d0d12;
  --gemboss-nav-hover: rgba(255, 255, 255, 0.06);
  --gemboss-divider: #2a2b33;
  --gemboss-divider-soft: #22232a;
  --gemboss-hover: #22232b;
  --gemboss-active-bg: #262833;
  --gemboss-active-fg: #f2f3f5;
  --gemboss-field-bg: #121318;
  --gemboss-field-bg-hover: #17181e;
  --gemboss-field-ink: #e3e4e8;
  --gemboss-field-placeholder: #8a8c96;
  --gemboss-field-line: #3a3c46;
  --gemboss-field-line-hover: #50525e;
  --gemboss-field-line-active: #c9cbd3;
  --gemboss-chip-ink: #d6d8de;
  --gemboss-chip-line: #3a3c46;
  --gemboss-chip-line-hover: #50525e;
  --gemboss-chip-bg-hover: #22232b;
  --gemboss-success: #34c38f;
  --gemboss-success-strong: #5fd6a8;
  --gemboss-success-soft: #13291f;
  --gemboss-success-border: #22533e;
  --gemboss-warning: #e0b341;
  --gemboss-warning-soft: #2b2412;
  --gemboss-warning-border: #5a4a1d;
  --gemboss-danger: #ff6b5a;
  --gemboss-danger-strong: #ff8a7c;
  --gemboss-danger-soft: #2e1714;
  --gemboss-danger-border: #5c2a24;
  --ui-accent-fg: #0d0d12;
  --ui-warning-fg: #e8c063;
  --ui-card-shadow: none; /* a dark drop under a dark card is noise */
  --gb-brand-ink: #f2f3f5;
}
/* Avatar initials in dark: the tone's light end, so the text clears 4.5:1 on its tinted disc. */
:root[data-theme="dark"] .gb-avatar[data-tone] { background: color-mix(in srgb, var(--gb-tone) 30%, #16171d); color: color-mix(in srgb, var(--gb-tone) 35%, #ffffff); }
:root[data-theme="dark"] body { background: var(--gemboss-app-bg); color: var(--gemboss-text); }
:root[data-theme="dark"] .gb-leaderboard-row.is-first .gb-leaderboard-value,
:root[data-theme="dark"] .gb-podium-slot--1 .gb-podium-value { color: #e0b341; }
