/*
 * 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: d870879b0ebaec62e91c32054943e5204b73698c
 * sources sha256: b252e4383a1d50a1c78827b0af61d5bb4fc34d15ef373697f24665db9f570099
 * contents: 1 allow-listed rule from apps/admin/src/app/globals.css + apps/admin/src/app/gemboss-tokens.css
 *   (verbatim) + apps/admin/src/components/ui/ui.css (verbatim) + apps/admin/src/app/gemboss-field.css (verbatim)
 *   + 154 allow-listed rules from apps/admin/src/app/gemboss-shell.css
 * 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 bodySm(13) which the app had been leaning on for prose. */
  --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;
}

: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);
  --ui-focus: var(--gemboss-primary, #1b1c1e);
  --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;
  --ui-control-h: 34px;
  --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);
  --ui-focus: var(--adm-accent);
}

/* ── 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-control-h); padding: 0 var(--ui-space-4); 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-body); 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 { height: 28px; padding: 0 var(--ui-space-3); font-size: var(--ui-fs-caption); }

/* ── Card ──────────────────────────────────────────────────────────────────── */
.ui-card {
  background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
  padding: var(--ui-space-4); color: var(--ui-text);
}
.ui-card__title { font-size: 14px; font-weight: 600; margin: 0 0 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). */
.ui-select {
  height: 36px; padding: 0 32px 0 12px; max-width: 100%;
  background: var(--ui-surface-2) 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(--ui-border); border-radius: var(--ui-radius-md);
  color: var(--ui-text); font-family: inherit; font-size: var(--ui-fs-body); font-weight: 500;
  appearance: none; -webkit-appearance: none; cursor: pointer;
}
.ui-select:hover { border-color: var(--ui-text-muted); }
.ui-select:focus { outline: none; border-color: var(--ui-accent); }
.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: 10px; font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase;
  padding: 1px 6px; border-radius: 6px; 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; }

/* ── Badge ────────────────────────────────────────────────────────────────────
   Uppercase mono pill for a status/label (was gemboss-admin-storage-badge). */
.ui-badge {
  display: inline-flex; align-items: center;
  font-size: 11px; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase;
  padding: 1px 8px; border-radius: 6px; background: var(--ui-surface-2); color: var(--ui-text-muted);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* ── 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. */
.ui-overlay {
  position: fixed; inset: 0; z-index: 500;
  background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px);
  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: flex-start; gap: 16px;
  padding: 16px 20px; border-bottom: 1px solid var(--ui-border);
  font-size: 15px; font-weight: 620; 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: 44px; height: 44px; margin: -8px -8px -8px 0;   /* a 44px target that does not grow the head */
  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-accent); outline-offset: -2px; }

/* `size="lg"` — the screen-shaped dialog. Only the HOOK lives here: this sheet is imported by the
   root layout, so every rule in it is paid on every screen, and the geometry of a dialog with one
   consumer belongs in that consumer's chunk (see SettingsDialog.module.css). */
.ui-modal--lg { position: relative; }

.ui-textarea {
  width: 100%; min-height: 96px; resize: vertical; box-sizing: border-box;
  background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md);
  padding: 12px; color: var(--ui-text); font-size: 14px; font-family: inherit; outline: none;
}
.ui-textarea:focus { border-color: var(--ui-accent); }

/* ── 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 button {
  display: inline-grid; place-items: center; width: 36px; height: 36px;
  border: 0; background: transparent; color: var(--ui-text-muted); cursor: pointer; font-family: inherit;
}
.ui-seg button.is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
.ui-seg button + button { border-left: 1px solid var(--ui-border); }
.ui-seg button:hover { background: var(--ui-surface-2); color: var(--ui-text); }
.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: #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: #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: #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 #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.
 *
 * `pointer: coarse` only: a mouse keeps the density these surfaces were designed at. Growing a box
 * for every pointer is what #2147 did to a working width.
 */
@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-seg button { 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; }
  /* `--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 button { 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;

}

/* ── 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. 44px under EVERY pointer (§9.1 control tier), not only coarse: tabs sit 4px apart, and
   that gap is only acceptable between targets that are already 44px. */
.ui-tabs {
  display: flex; gap: var(--ui-space-1); border-bottom: 1px solid var(--ui-border);
  overflow-x: auto; scrollbar-width: none;
}
.ui-tabs > button {
  position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
  min-height: 44px; 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(--gemboss-fs-body); font-weight: 500; 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 { color: var(--ui-text); background: var(--ui-surface-2); }
.ui-tabs > button:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
.ui-tabs > button[aria-selected="true"] { color: var(--ui-text-strong); font-weight: 600; }
.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; }

/* 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);
}
.ui-table { width: 100%; border-collapse: collapse; font-size: var(--gemboss-fs-body); color: var(--ui-text); }
.ui-table th,
.ui-table td {
  padding: var(--ui-space-2) var(--ui-space-3); text-align: left; vertical-align: middle;
  border-bottom: 1px solid var(--ui-border);
}
.ui-table th {
  font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-text-muted); white-space: nowrap;
}
.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; }

/* 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(--gemboss-fs-body);
}
.ui-empty__title { margin: 0; font-size: var(--gemboss-fs-h3); font-weight: 600; 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: #303030;
  background-color: #fdfdfd;
  border: 1px solid #8a8a8a;
  border-radius: 8px;
}
.gemboss-field::placeholder { color: #616161; }
/* 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: #616161; background-color: #fafafa; }
.gemboss-field:focus-visible,
.gemboss-control:focus-visible { outline: 2px solid #005bd3; outline-offset: 1px; border-color: #1a1a1a; }
.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 #005bd3; outline-offset: 1px; border-color: #1a1a1a; }
/* 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 #8a8a8a; 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; 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: #616161; }

/* 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; } }

/* ═══ gemboss-shell.css — allow-listed extract (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-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-surface);
  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-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: rgba(255, 255, 255, 0.98);
  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);
}
.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-surface);
  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;
  }
  .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-platform-main {
    padding-top: 56px;
    padding-bottom: calc(56px + env(safe-area-inset-bottom));
  }
}
@media (pointer: coarse) {
  .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-surface);
  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: rgba(20, 19, 18, 0.045);
}
.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: rgba(20, 19, 18, 0.045);
}
.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: rgba(20, 19, 18, 0.045);
  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-primary-dark);
  font-weight: 600;
  background: color-mix(in srgb, var(--gemboss-primary) 8%, #fff);
}
.gnav-item.gnav-assistant .gnav-ico svg {
  fill: var(--gemboss-primary);
}
.gnav-item.gnav-assistant:hover {
  background: color-mix(in srgb, var(--gemboss-primary) 15%, #fff);
}
.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);
    z-index: 1000;
    box-shadow: 2px 0 26px rgba(31, 30, 29, 0.20);
  }
  .gemboss-platform-shell.is-mobile-nav-open .icon-nav {
    transform: translateX(0);
  }
  .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-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));
}
@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); }

.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. */
  .gemboss-topbar-center { flex: 1 1 auto; justify-content: flex-start; }
}

.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 { 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: #fff; }

/* ═══ @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);
}
.gb-auth-mark svg {
  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
   (apps/admin/src/app/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; }
}
/* 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);
}

/* ── 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; }
.gb-dock-actions { display: flex; align-items: center; gap: 2px; cursor: default; }
/* 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: #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;
  }
}
