@gemboss/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/gemboss-ui.css ADDED
@@ -0,0 +1,2889 @@
1
+ /*
2
+ * GemBoss admin design system — GENERATED — do not edit; regenerate from gemboss:
3
+ * node apps/admin/scripts/export-ui-css.mjs --out <path> (npm run export:ui-css)
4
+ * gemboss commit: cf59218eb5d076a1585551bfb3eca691a812c31b
5
+ * sources sha256: 89ec556bd16fd74dc4733abb73c0550383b1efb0cb7b6c5bb33d86f98d35bcab
6
+ * contents: 1 allow-listed rule from apps/admin/src/app/globals.css + packages/ui/src/gemboss-tokens.css
7
+ * (verbatim) + packages/ui/src/ui.css (verbatim) + packages/ui/src/gemboss-field.css (verbatim)
8
+ * + 180 rules from packages/ui/src/shell.css (the shared shell)
9
+ * Polaris --p-* references are resolved to their fallbacks; declarations with no fallback are dropped
10
+ * (0 dropped).
11
+ */
12
+ /* ═══ globals.css — allow-listed reset ═══ */
13
+ * {
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ /* ═══ gemboss-tokens.css ═══ */
18
+ /* The design tokens, and ONLY the tokens.
19
+ *
20
+ * Split out of gemboss-shell.css because that file is 203 KB of dashboard component CSS and it was
21
+ * loaded by the ROOT layout — so /try and /login, which render no .gemboss-* component at all,
22
+ * blocked their first paint on it. Tokens are 6 KB of the 203; /login uses var(--gemboss-primary)
23
+ * and the rewritten /try uses the palette, so everyone needs these and almost nobody needs the rest.
24
+ *
25
+ * Order matters and is preserved: the second :root block below redefines --gemboss-primary and
26
+ * friends from the first, exactly as it did inside the shell file. The Polaris --p-* overrides stay
27
+ * in gemboss-shell.css, next to the Polaris stylesheet they override.
28
+ */
29
+ :root {
30
+ --gemboss-primary: #1b1c1e; /* near-black — primary action (buttons / CTA) */
31
+ --gemboss-primary-dark: #000000; /* black hover */
32
+ --gemboss-primary-soft: #f2f3f5; /* neutral tint */
33
+ --gemboss-accent: #5661f5; /* iris — brand accent, điểm nhấn only (links / focus / active) */
34
+ --gemboss-accent-strong: #3a44de; /* iris strong (hover / text) */
35
+ --gemboss-accent-soft: #eef0fe; /* iris tint */
36
+ --gemboss-text-strong: #1b1c1e; /* neutral near-black */
37
+ --gemboss-text: #1b1c1e;
38
+ --gemboss-text-muted: #686b71; /* neutral gray */
39
+ --gemboss-surface: #ffffff;
40
+ --gemboss-canvas: #ffffff; /* white page background (was #f6f7f9 gray) — body + Polaris --p-color-bg */
41
+ --gemboss-preview-canvas: #eceef1; /* neutral sunken backdrop behind a live preview/widget stage */
42
+ --gemboss-rail-bg: #ffffff; /* white sidebar (was #f4f5f7) — separated by border-right divider */
43
+ --gemboss-panel-bg: #ffffff; /* white secondary panels (was #f2f4f6) */
44
+ --gemboss-divider: #e5e7ea; /* neutral hairline border */
45
+
46
+ /* Chiều cao THẬT của lời mời desktop, do DesktopInvite.tsx ghi đè lúc chạy bằng ResizeObserver
47
+ (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ẽ:
48
+ 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
49
+ `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
50
+ tài liệu — xem `.gemboss-platform-main { padding-bottom: … }` trong gemboss-shell.css. */
51
+ --gemboss-invite-h: 0px;
52
+ --gemboss-divider-soft: #edeff2;
53
+ --gemboss-hover: #eceef2;
54
+ --gemboss-active-bg: #f0f1f4; /* neutral selected (Polaris-style) */
55
+ --gemboss-active-fg: #1b1c1e; /* near-black selected text */
56
+ --gemboss-danger: #d72c0d;
57
+ --gemboss-danger-strong: #b42318;
58
+ --gemboss-danger-soft: #fff0e9;
59
+ --gemboss-danger-border: #f5c9bc;
60
+ --gemboss-success: #007f5f;
61
+ --gemboss-success-strong: #0a6a4c;
62
+ --gemboss-success-soft: #e3f1df;
63
+ --gemboss-success-border: #b7dfcd;
64
+ --gemboss-warning: #9a6700;
65
+ --gemboss-warning-soft: #fff8e0;
66
+ --gemboss-warning-border: #f5d569;
67
+ /* Corner-radius scale — Polaris border-radius steps (2/4/6/8/12/16/20 + pill). The old
68
+ long tail of one-off radii (5/7/9/10/11/14/18px) was consolidated onto these steps
69
+ (round-half-up on ties: 5→6, 7→8, 9→8, 10→12, 11→12, 14→16, 18→20) — the
70
+ check-typography guard now keeps radii on-scale. Prefer a token below; raw px must
71
+ still land on a step. */
72
+ --gemboss-radius-xs: 4px;
73
+ --gemboss-radius-sm: 6px;
74
+ --gemboss-radius-md: 8px;
75
+ --gemboss-radius-lg: 12px;
76
+ --gemboss-radius-xl: 16px;
77
+ --gemboss-radius-2xl: 20px;
78
+ --gemboss-radius-full: 999px;
79
+ /* ── Elevation scale (design-language §G) ────────────────────────────────────
80
+ THREE layers, one shadow each — was ~25 bespoke box-shadows (0 6/8/12/16/28px
81
+ at a dozen opacities + mixed shadow colours) = no elevation system. Warm-neutral
82
+ shadow (rgba 31,30,29) to match the ivory canvas. Use a token, never a hand-rolled
83
+ drop shadow. (Directional edge shadows, coloured glows, focus rings, and insets are
84
+ NOT elevation — they stay literal.) */
85
+ --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 */
86
+ --gemboss-shadow-raised: 0 4px 16px rgba(31, 30, 29, 0.10); /* dropdowns · popovers · hovercards */
87
+ --gemboss-shadow-overlay: 0 16px 48px rgba(31, 30, 29, 0.18); /* modals · sheets · large floating panels */
88
+ /* Interaction transition speeds — fast = hover/focus on buttons & toggles, base = the
89
+ general default. One-off animation timings (toast/enter, 240–420ms) stay literal. */
90
+ --gemboss-transition-fast: 0.12s;
91
+ --gemboss-transition-base: 0.15s;
92
+ /* ── Type scale ─────────────────────────────────────────────────────────────
93
+ ONE font-size scale for all custom (non-Polaris-<Text>) chrome. Before this,
94
+ gemboss-shell.css carried ~20 ad-hoc sizes (9px → 40px, incl. fractional 10.5 /
95
+ 11.5 / 12.5 / 13.5) — every component hand-nudged its own, drifting smaller
96
+ and reading as "the font keeps shrinking". New rule: pick a step here, never a
97
+ raw px. Floor is 12px (--fs-caption); 11px (--fs-micro) is reserved for bold
98
+ micro-badges/count-pills ONLY — nothing below that. Body default is 14px, one
99
+ step up from Polaris bodySm(13) which the app had been leaning on for prose. */
100
+ --gemboss-fs-micro: 11px; /* bold count-pills / status chips only */
101
+ --gemboss-fs-caption: 12px; /* metadata, timestamps, secondary labels */
102
+ --gemboss-fs-body: 14px; /* default body / reading text */
103
+ --gemboss-fs-body-lg: 16px; /* primary descriptions, empty-state prose */
104
+ --gemboss-fs-h3: 16px; /* card titles */
105
+ --gemboss-fs-h2: 20px; /* zone / section headers */
106
+ --gemboss-fs-h1: 24px; /* page titles */
107
+ --gemboss-fs-display: 30px; /* hero / banner display */
108
+ /* Headings use the same clean sans as Polaris body (Shopify-admin feel) rather
109
+ * than a serif display face — a dashboard reads calmer/more neutral that way.
110
+ * Name kept as `--gemboss-serif` to avoid churning ~10 call sites. */
111
+ /* -- Khung nen TOI, dung chung cho hai be mat toi --------------------------
112
+ `/admin` khai `--adm-*`, `/intel` khai `--in-*`, va bay gia tri trung tinh cua chung trung
113
+ nhau TUNG BYTE duoi hai bo ten khac nhau -- do 10/09/2026:
114
+
115
+ #0a0a0c adm-bg in-bg #2a2a31 adm-divider in-border
116
+ #0e0e11 adm-rail in-bg2 #ededf0 adm-text in-text
117
+ #16161a adm-surface in-panel #9a9aa4 adm-muted in-muted
118
+ #1e1e24 adm-surface-2 in-panel2
119
+
120
+ Do la mot ban sao, khong phai mot bien the: hai console cung mot nen, va lam nen sang them
121
+ mot bac thi phai nho sua o hai cho mang ten khac nhau. Bay gia tri nay song o day; `--adm-*`
122
+ va `--in-*` tro thanh BI DANH tro vao, nen 595 cho dang goi chung khong phai doi mot chu.
123
+
124
+ KHONG gop phan con lai: `--in-panel3/-border2/-faint` la bac rieng intel that su can, va bon
125
+ mau ngu nghia cua no (xanh - ho phach - san ho - tim) la ngon ngu rieng cua be mat do. Gop
126
+ nen, giu diem nhan. */
127
+ --gemboss-dark-bg: #0a0a0c;
128
+ --gemboss-dark-rail: #0e0e11;
129
+ --gemboss-dark-surface: #16161a;
130
+ --gemboss-dark-surface-2: #1e1e24;
131
+ --gemboss-dark-divider: #2a2a31;
132
+ --gemboss-dark-text: #ededf0;
133
+ --gemboss-dark-muted: #9a9aa4;
134
+ --gemboss-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
135
+ /* The BODY font, for surfaces without Polaris. The admin never reads it: its body text comes from
136
+ Polaris's `html, body, button` rule (--p-font-family-sans, @shopify/polaris 13.9.5). The fallback
137
+ below is that stack character for character, and export-ui-css.mjs ships the fallback, so a
138
+ surface on gemboss-ui.css picks the admin's face on the same machine (Inter is not loaded as a web
139
+ font anywhere; it renders where installed). Painted by packages/ui/src/app-shell.css; re-theme by
140
+ redefining the token. Added 28/09/2026: before it, those surfaces rendered in default serif. */
141
+ --gemboss-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'San Francisco', 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
142
+ /* COMPONENT colours that used to be literals, so a surface with a dark theme re-themes them by
143
+ redefining the token instead of overriding the rule (29/09/2026). Each value is the literal it
144
+ replaced, so the admin paints exactly what it painted before. Measured reason: gempress's dark
145
+ theme had to override `.gemboss-account-chip` (initials #303633 on #16161a all but vanished)
146
+ and keep its own copy of the field and the nav count rather than use the package's.
147
+ - field: the fallbacks of gemboss-field.css. The admin still reads Polaris's --p-* first;
148
+ export-ui-css.mjs resolves --p-* away, so on every other surface these tokens are the look.
149
+ - chip: `.gemboss-account-chip` in apps/admin/src/app/gemboss-shell.css.
150
+ - on-primary: text on a --gemboss-primary fill (the active `.gnav-count` pill). */
151
+ --gemboss-field-bg: #fdfdfd;
152
+ --gemboss-field-bg-hover: #fafafa;
153
+ --gemboss-field-ink: #303030;
154
+ --gemboss-field-placeholder: #616161;
155
+ --gemboss-field-line: #8a8a8a;
156
+ --gemboss-field-line-hover: #616161;
157
+ --gemboss-field-line-active: #1a1a1a;
158
+ --gemboss-chip-ink: #303633;
159
+ --gemboss-chip-line: #cfd6ce;
160
+ --gemboss-chip-line-hover: #aab4ad;
161
+ --gemboss-chip-bg-hover: #f6f7f5;
162
+ --gemboss-on-primary: #ffffff;
163
+ /* The shell's own surfaces (29/09/2026), for the same reason as the slots above: a dark surface
164
+ (gempress) had to override `.gemboss-platform-topbar`, `.icon-nav` and `.gnav-item:hover` by rule.
165
+ Defaults are exactly what the shell painted. */
166
+ --gemboss-topbar-bg: rgba(255, 255, 255, 0.98);
167
+ --gemboss-nav-bg: var(--gemboss-surface);
168
+ --gemboss-nav-hover: rgba(20, 19, 18, 0.045);
169
+ }
170
+
171
+ :root {
172
+ --gemboss-primary: #1b1c1e;
173
+ --gemboss-primary-dark: #000000;
174
+ --gemboss-primary-soft: #f2f3f5;
175
+ --gemboss-accent: #5661f5;
176
+ --gemboss-accent-strong: #3a44de;
177
+ --gemboss-accent-soft: #eef0fe;
178
+ --gemboss-active-bg: #f0f1f4; /* neutral selected (Polaris-style) */
179
+ --gemboss-active-fg: #1b1c1e; /* near-black selected text */
180
+ --gemboss-app-bg: #ffffff; /* clean white main content (was #f6f7f9 gray) — cards keep separation via border/resting-shadow */
181
+ }
182
+
183
+ /* ═══ ui.css ═══ */
184
+ /* ════════════════════════════════════════════════════════════════════════════
185
+ Tool/App UI kit — the shared primitive layer for the internal operator surfaces
186
+ (admin console, seller app chrome, theme-editor). ONE semantic token spine
187
+ (`--ui-*`) that every primitive reads, plus the primitive CSS itself.
188
+
189
+ Why this exists: buttons/cards/badges were forked into 4–8 hand-rolled class
190
+ families per surface, each re-deriving color/spacing/centering — so the same
191
+ primitive drifted (e.g. a link-button whose text wasn't vertically centered).
192
+ See docs/frontend-shared-system-plan.md.
193
+
194
+ The `--ui-*` layer is SEMANTIC and resolved per surface: components only read
195
+ `--ui-*`; each surface maps those to its own tokens (admin → `--adm-*` below),
196
+ so one <Button> looks native everywhere without hard-coding a palette.
197
+ ════════════════════════════════════════════════════════════════════════════ */
198
+
199
+ :root {
200
+ --ui-surface: #ffffff;
201
+ /* --ui-* is a SEMANTIC layer resolved PER SURFACE — it references each surface's own base tokens
202
+ rather than guessing from the OS. The default (`:root`) maps onto the seller app's warm-ivory
203
+ `--gemboss-*` set; `.gemboss-admin-shell` below remaps onto the dark `--adm-*` set. Literal fallbacks
204
+ keep the kit usable if a surface's base stylesheet isn't loaded. This is the "unify under --ui-*"
205
+ backbone — one token spine, no duplicated palette. */
206
+ --ui-surface: var(--gemboss-surface, #ffffff);
207
+ --ui-surface-2: var(--gemboss-hover, #eceef2);
208
+ --ui-text: var(--gemboss-text, #1b1c1e);
209
+ --ui-text-muted: var(--gemboss-text-muted, #686b71);
210
+ --ui-border: var(--gemboss-divider, #e5e7ea);
211
+ --ui-accent: var(--gemboss-primary, #1b1c1e);
212
+ --ui-accent-hover: var(--gemboss-primary-dark, #000000);
213
+ --ui-accent-fg: #ffffff;
214
+ --ui-accent-soft: var(--gemboss-active-bg, #f0f1f4); /* tinted bg for a selected/active state */
215
+ --ui-accent-strong: var(--gemboss-active-fg, #1b1c1e); /* readable accent text ON --ui-accent-soft */
216
+ --ui-text-strong: var(--gemboss-text-strong, #1b1c1e);
217
+ --ui-focus: var(--gemboss-primary, #1b1c1e);
218
+ --ui-radius-sm: 6px;
219
+ --ui-radius-md: 8px;
220
+ --ui-radius-lg: 12px;
221
+ --ui-space-1: 4px;
222
+ --ui-space-2: 8px;
223
+ --ui-space-3: 12px;
224
+ --ui-space-4: 16px;
225
+ --ui-control-h: 34px;
226
+ --ui-fs-body: 13px;
227
+ --ui-fs-caption: 12px;
228
+ /* Content column width for the seller app shell — ONE source of truth for every wide surface
229
+ right of the 240px sidebar (bare Page, .gemboss-w-dashboard, .gemboss-w-gallery, and the
230
+ navigation skeleton), so they cannot drift apart again. The /admin console is a SEPARATE
231
+ surface and still sets its own 1200px in admin/admin-shell.css; it does not read this token.
232
+ 900px. Polaris' own cap is 998px — 41.375rem primary column + 20rem secondary + 1rem gutter,
233
+ read out of the installed @shopify/polaris stylesheet — and this sits deliberately BELOW it.
234
+
235
+ The reason is a measurement, not a preference about Shopify. The previous value was 1080/1040,
236
+ and moving to Polaris' 998 was the obvious "correct" answer, so that shipped first (#2250). Then
237
+ it turned out to be invisible: 1040 → 998 is 42px total, which is 21px of extra gutter per side.
238
+ Screenshotted side by side at 1440 on the same page seconds apart, the two rows were
239
+ indistinguishable, and Chris — who asked for this — could not see any change at all.
240
+
241
+ A cap only does its job if the breathing room it buys is legible. Measured gutters on
242
+ .Polaris-Page at 1440 beside the 240px sidebar:
243
+
244
+ 1040px → 75 / 86 (before)
245
+ 998px → 96 / 107 Polaris' number; +21px a side, not perceivable
246
+ 960px → 115 / 126
247
+ 900px → 145 / 156 +70px a side, roughly double the old gutter
248
+
249
+ 900 is the first value where the column visibly stops leaning on the rail. The one cost, and it
250
+ was measured rather than guessed: card descriptions gain a line (Cart drawer 2 → 3). The 3-up
251
+ grid holds, and no width from 320 to 1920 overflows.
252
+
253
+ Still fixed px, no percentage, for the reason Polaris uses px: a % cap grows the reading measure
254
+ with the monitor (80% of 2560 is 2048px of line length). Responsiveness belongs to the padding
255
+ and to the secondary column dropping, not to the cap.
256
+
257
+ .gemboss-w-form stays 662px — that IS `.Polaris-Page--narrowWidth`, the primary column alone,
258
+ and a form is the one surface where Polaris' measure is about the input, not the page. */
259
+ --ui-content-max: 900px;
260
+ }
261
+
262
+ /* Admin console bridge: the black chrome defines `--adm-*`; remap `--ui-*` onto them so every
263
+ <Button>/<Select>/<Tag>/<Modal> inside .gemboss-admin-shell adopts the dark palette automatically. */
264
+ .gemboss-admin-shell {
265
+ --ui-surface: var(--adm-surface);
266
+ --ui-surface-2: var(--adm-surface-2);
267
+ --ui-text: var(--adm-text);
268
+ --ui-text-muted: var(--adm-muted);
269
+ --ui-border: var(--adm-divider);
270
+ --ui-accent: var(--adm-accent);
271
+ --ui-accent-hover: var(--gemboss-primary-dark, #000000);
272
+ --ui-accent-fg: #ffffff;
273
+ --ui-accent-soft: var(--adm-accent-soft);
274
+ --ui-accent-strong: var(--adm-accent-fg);
275
+ --ui-text-strong: var(--adm-text-strong);
276
+ --ui-focus: var(--adm-accent);
277
+ }
278
+
279
+ /* ── Button ──────────────────────────────────────────────────────────────────
280
+ inline-flex + center so a <button> and an <a> render identically (anchors ignore
281
+ `height` as inline elements — the root cause of the misaligned link-buttons). */
282
+ .ui-btn {
283
+ display: inline-flex; align-items: center; justify-content: center; gap: var(--ui-space-2);
284
+ height: var(--ui-control-h); padding: 0 var(--ui-space-4); box-sizing: border-box;
285
+ line-height: 1; white-space: nowrap; vertical-align: middle;
286
+ border-radius: var(--ui-radius-md); font-family: inherit; font-size: var(--ui-fs-body); font-weight: 550;
287
+ cursor: pointer; text-decoration: none; user-select: none;
288
+ border: 1px solid var(--ui-border); background: transparent; color: var(--ui-text);
289
+ transition: background .12s ease, border-color .12s ease;
290
+ }
291
+ .ui-btn:hover { background: var(--ui-surface-2); text-decoration: none; }
292
+ .ui-btn:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
293
+ .ui-btn[disabled], .ui-btn[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }
294
+ .ui-btn--primary { background: var(--ui-accent); border-color: var(--ui-accent); color: var(--ui-accent-fg); }
295
+ .ui-btn--primary:hover { background: var(--ui-accent-hover); border-color: var(--ui-accent-hover); }
296
+ .ui-btn--ghost { border-color: transparent; background: transparent; color: var(--ui-text-muted); }
297
+ .ui-btn--ghost:hover { background: var(--ui-surface-2); color: var(--ui-text); }
298
+ .ui-btn--sm { height: 28px; padding: 0 var(--ui-space-3); font-size: var(--ui-fs-caption); }
299
+
300
+ /* ── Card ──────────────────────────────────────────────────────────────────── */
301
+ .ui-card {
302
+ background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
303
+ padding: var(--ui-space-4); color: var(--ui-text);
304
+ }
305
+ .ui-card__title { font-size: 14px; font-weight: 600; margin: 0 0 var(--ui-space-2); }
306
+
307
+ /* ── Select ──────────────────────────────────────────────────────────────────
308
+ Native <select> reskinned; custom caret as an inline SVG data-URI (caret color
309
+ uses the muted token via %-encoding fallback).
310
+ Its BOX is the text field's (29/09/2026, Chris): same --gemboss-field-* tokens for ground, line and
311
+ ink, same hover, same focus ring as `.gemboss-field`. Before, a grey --ui-surface-2 select stood next
312
+ to a bordered white field in the same form (gempress editor, admin forms), and `:focus` removed the
313
+ outline and only recoloured the border, so a keyboard user saw almost nothing. Height stays 36px
314
+ (--ui-control-h), because a select sits in toolbars beside 36px `.ui-btn`s. */
315
+ .ui-select {
316
+ height: 36px; padding: 0 32px 0 12px; max-width: 100%;
317
+ background: var(--gemboss-field-bg) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 20 20'><path fill='%239a9aa4' d='M5 7l5 5 5-5z'/></svg>") no-repeat right 10px center;
318
+ border: 1px solid var(--gemboss-field-line); border-radius: var(--ui-radius-md);
319
+ color: var(--gemboss-field-ink); font-family: inherit; font-size: var(--ui-fs-body); font-weight: 500;
320
+ appearance: none; -webkit-appearance: none; cursor: pointer;
321
+ }
322
+ .ui-select:hover:not(:disabled) {
323
+ border-color: var(--gemboss-field-line-hover);
324
+ background-color: var(--gemboss-field-bg-hover);
325
+ }
326
+ .ui-select:focus { outline: none; }
327
+ .ui-select:focus-visible {
328
+ outline: 2px solid #005bd3; outline-offset: 1px;
329
+ border-color: var(--gemboss-field-line-active);
330
+ }
331
+ .ui-select:disabled { opacity: .5; cursor: not-allowed; }
332
+ .ui-select--slim { height: 30px; font-size: var(--ui-fs-caption); }
333
+
334
+ /* ── Tag / Chip ──────────────────────────────────────────────────────────────
335
+ Small inline labels. `solid` = filled muted pill (was gemboss-vault-tag); `chip` =
336
+ bordered mono token (was gemboss-vault-chip). `accent` tone for the highlighted state. */
337
+ .ui-tag {
338
+ display: inline-flex; align-items: center; gap: 6px;
339
+ font-size: 10px; font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase;
340
+ padding: 1px 6px; border-radius: 6px; background: var(--ui-surface-2); color: var(--ui-text-muted);
341
+ }
342
+ .ui-tag--accent { background: var(--adm-accent-soft, rgba(20,22,26,.05)); color: var(--adm-accent-fg, var(--ui-accent)); }
343
+ .ui-tag--chip {
344
+ text-transform: none; letter-spacing: 0; font-size: 11px; font-weight: 500;
345
+ padding: 2px 8px; background: transparent; border: 1px solid var(--ui-border);
346
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
347
+ }
348
+ .ui-tag--chip.ui-tag--accent { color: var(--adm-accent-fg, var(--ui-accent)); border-color: var(--ui-accent); background: transparent; }
349
+
350
+ /* ── Badge ────────────────────────────────────────────────────────────────────
351
+ Uppercase mono pill for a status/label (was gemboss-admin-storage-badge). */
352
+ .ui-badge {
353
+ display: inline-flex; align-items: center;
354
+ font-size: 11px; font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase;
355
+ padding: 1px 8px; border-radius: 6px; background: var(--ui-surface-2); color: var(--ui-text-muted);
356
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
357
+ }
358
+
359
+ /* ── Modal ────────────────────────────────────────────────────────────────────
360
+ Rendered INLINE (not portaled): a portal escapes the .gemboss-admin-shell token
361
+ scope and would render light. Kept in-tree so --ui-* resolves to the surface. */
362
+ .ui-overlay {
363
+ position: fixed; inset: 0; z-index: 500;
364
+ background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(2px);
365
+ display: grid; place-items: center; padding: 20px;
366
+ }
367
+ .ui-modal {
368
+ width: min(460px, 100%);
369
+ background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: 16px;
370
+ box-shadow: var(--gemboss-shadow-overlay, 0 20px 60px rgba(0, 0, 0, 0.4)); overflow: hidden;
371
+ }
372
+ /* Head = title + the one close button every modal used to draw for itself. */
373
+ .ui-modal__head {
374
+ display: flex; align-items: flex-start; gap: 16px;
375
+ padding: 16px 20px; border-bottom: 1px solid var(--ui-border);
376
+ font-size: 15px; font-weight: 620; color: var(--ui-text);
377
+ }
378
+ .ui-modal__title { min-width: 0; flex: 1 1 auto; }
379
+ .ui-modal__close {
380
+ flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
381
+ width: 44px; height: 44px; margin: -8px -8px -8px 0; /* a 44px target that does not grow the head */
382
+ border: 0; border-radius: var(--ui-radius-md, 8px); background: transparent;
383
+ color: var(--ui-text-muted); cursor: pointer;
384
+ }
385
+ .ui-modal__close svg { width: 20px; height: 20px; }
386
+ .ui-modal__close:hover { background: var(--ui-surface-2); color: var(--ui-text); }
387
+ .ui-modal__close:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: -2px; }
388
+
389
+ /* `size="lg"` — the screen-shaped dialog. Only the HOOK lives here: this sheet is imported by the
390
+ root layout, so every rule in it is paid on every screen, and the geometry of a dialog with one
391
+ consumer belongs in that consumer's chunk (see SettingsDialog.module.css). */
392
+ .ui-modal--lg { position: relative; }
393
+
394
+ .ui-textarea {
395
+ width: 100%; min-height: 96px; resize: vertical; box-sizing: border-box;
396
+ background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md);
397
+ padding: 12px; color: var(--ui-text); font-size: 14px; font-family: inherit; outline: none;
398
+ }
399
+ .ui-textarea:focus { border-color: var(--ui-accent); }
400
+
401
+ /* ── Segmented control ────────────────────────────────────────────────────────
402
+ Grouped toggle: icon squares (.ui-seg button) or text tabs (.is-text). The
403
+ selected segment uses the accent-soft state. */
404
+ .ui-seg { display: inline-flex; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); overflow: hidden; }
405
+ .ui-seg button {
406
+ display: inline-grid; place-items: center; width: 36px; height: 36px;
407
+ border: 0; background: transparent; color: var(--ui-text-muted); cursor: pointer; font-family: inherit;
408
+ }
409
+ .ui-seg button.is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
410
+ .ui-seg button + button { border-left: 1px solid var(--ui-border); }
411
+ .ui-seg button:hover { background: var(--ui-surface-2); color: var(--ui-text); }
412
+ .ui-seg button.active { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
413
+ .ui-seg svg { width: 16px; height: 16px; fill: currentColor; }
414
+
415
+ /* ── Stat chip ────────────────────────────────────────────────────────────────
416
+ Count-led pill; clickable ones double as filters (active = accent-soft). */
417
+ .ui-stats { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); margin-bottom: 24px; }
418
+ .ui-stat {
419
+ display: inline-flex; align-items: center; gap: var(--ui-space-2);
420
+ padding: 6px 12px; border-radius: 999px; font-family: inherit;
421
+ background: var(--ui-surface); border: 1px solid var(--ui-border);
422
+ font-size: var(--ui-fs-caption); color: var(--ui-text-muted);
423
+ }
424
+ .ui-stat b { color: var(--ui-text-strong); font-weight: 650; }
425
+ .ui-stat.is-clickable { cursor: pointer; transition: border-color 0.14s ease, background-color 0.14s ease; }
426
+ .ui-stat.is-clickable:hover { border-color: var(--ui-text-muted); background: var(--ui-surface-2); }
427
+ .ui-stat.is-clickable.active { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
428
+ .ui-stat.is-clickable.active b { color: var(--ui-accent-strong); }
429
+
430
+ /* ── Toast / notification bar ─────────────────────────────────────────────────
431
+ App-wide "done / failed" pill for async actions (delete/duplicate/publish…).
432
+ Polaris-style: dark pill, bottom-center, error variant in critical red. Lives
433
+ in <ToastProvider> (components/ui/Toast.tsx), portaled to <body>. */
434
+ .gemboss-toast-viewport {
435
+ position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
436
+ z-index: 2147483000; /* above every overlay/modal/popup */
437
+ display: flex; flex-direction: column; align-items: center; gap: 12px;
438
+ width: max-content; max-width: min(560px, calc(100vw - 32px)); pointer-events: none;
439
+ }
440
+ .gemboss-toast {
441
+ pointer-events: auto;
442
+ display: flex; align-items: center; gap: 12px;
443
+ min-width: 220px; max-width: 100%; padding: 12px 12px 12px 16px;
444
+ background: var(--ui-text-strong, #1a1917); color: #fff; border-radius: 12px;
445
+ font-size: var(--ui-fs-body, 13px); line-height: 1.4; font-weight: 500;
446
+ box-shadow: var(--gemboss-shadow-overlay, 0 8px 28px rgba(0, 0, 0, 0.24));
447
+ animation: gemboss-toast-in 240ms cubic-bezier(0.16, 1, 0.3, 1);
448
+ }
449
+ .gemboss-toast.is-error { background: var(--gemboss-danger, #d72c0d); }
450
+ .gemboss-toast-text { flex: 1 1 auto; min-width: 0; }
451
+ .gemboss-toast-action {
452
+ flex: 0 0 auto; border: 0; background: transparent; color: #fff; cursor: pointer;
453
+ padding: 4px 8px; border-radius: 8px; font: inherit; font-weight: 650;
454
+ text-decoration: underline; text-underline-offset: 2px;
455
+ transition: background 0.14s ease;
456
+ }
457
+ .gemboss-toast-action:hover { background: rgba(255, 255, 255, 0.16); }
458
+ .gemboss-toast-dismiss {
459
+ flex: 0 0 auto; display: inline-grid; place-items: center;
460
+ width: 24px; height: 24px; border: 0; border-radius: 8px; cursor: pointer;
461
+ background: rgba(255, 255, 255, 0.12); color: #fff;
462
+ transition: background 0.14s ease;
463
+ }
464
+ .gemboss-toast-dismiss:hover { background: rgba(255, 255, 255, 0.26); }
465
+ .gemboss-toast-action:focus-visible,
466
+ .gemboss-toast-dismiss:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
467
+ @keyframes gemboss-toast-in {
468
+ from { opacity: 0; transform: translateY(12px); }
469
+ to { opacity: 1; transform: translateY(0); }
470
+ }
471
+ @media (prefers-reduced-motion: reduce) { .gemboss-toast { animation: none; } }
472
+
473
+ /* ── The kit's controls under a thumb ─────────────────────────────────────────────────────────
474
+ * Measured by hit test on /admin/{theme-kernel,media,style-packs} at 390 and 1366 with a coarse
475
+ * pointer: segmented buttons 36x36, selects 30-36 tall, clickable stat tiles 29, the asset row's
476
+ * note button 30x30. §9.1 puts every one of them in the CONTROL tier (44px).
477
+ *
478
+ * HERE rather than in each surface's stylesheet, because this is the shared primitive layer — the
479
+ * same reason the kit exists at all. The admin console and the seller app both read it.
480
+ *
481
+ * `pointer: coarse` only: a mouse keeps the density these surfaces were designed at. Growing a box
482
+ * for every pointer is what #2147 did to a working width.
483
+ */
484
+ @media (pointer: coarse) {
485
+ /* NOT `.ui-textarea`: it already declares `min-height: 96px` earlier in this file, so adding it to
486
+ a 44px group LOWERS it — same specificity, later line. The note dialog's box would have gone from
487
+ 96 to ~58 on every touch device, inside a block whose whole point is bigger targets.
488
+ NOT `.ui-btn--sm` either: `min-height` always beats `height`, so a 44px floor voids the compact
489
+ variant — including four of them on the seller's own /app/settings/store. A density variant is a
490
+ decision, not an oversight; it gets its own measurement pass or none. */
491
+ .ui-btn:not(.ui-btn--sm),
492
+ .ui-stat.is-clickable,
493
+ .ui-seg button { min-height: 44px; }
494
+ /* A clickable stat whose label is one short token ("7d" on /admin/gem-intelligence) measured 40x44
495
+ in the 13/09/2026 sweep: the height floor held, the width did not. */
496
+ .ui-stat.is-clickable { min-width: 44px; justify-content: center; }
497
+ /* `--slim` is a second single class and lands later in this file, so it would win on order over a
498
+ bare `.ui-select`. Doubled here to decide it on specificity instead. */
499
+ .ui-select.ui-select,
500
+ .ui-select--slim.ui-select--slim { min-height: 44px; }
501
+ /* The icon-only segment is 36 WIDE as well — a target is an area. */
502
+ .ui-seg button { min-width: 44px; }
503
+ }
504
+
505
+ /* ── Skeleton ─────────────────────────────────────────────────────────────────
506
+ 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
507
+ trí — nên nó tắt hẳn với `prefers-reduced-motion`, giữ nguyên vạch tĩnh. */
508
+ .ui-skeleton { display: flex; flex-direction: column; gap: var(--ui-space-2); }
509
+ .ui-skeleton__line {
510
+ display: block; height: 12px; border-radius: 6px;
511
+ background: linear-gradient(90deg, var(--ui-surface-2) 25%, var(--ui-border) 37%, var(--ui-surface-2) 63%);
512
+ background-size: 400% 100%;
513
+ animation: ui-skeleton-sheen 1.4s ease-in-out infinite;
514
+ }
515
+ @keyframes ui-skeleton-sheen { 0% { background-position: 100% 50%; } 100% { background-position: 0 50%; } }
516
+ @media (prefers-reduced-motion: reduce) {
517
+ .ui-skeleton__line { animation: none; background: var(--ui-surface-2); }
518
+ }
519
+ /* 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. */
520
+ .ui-sr-only {
521
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
522
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
523
+
524
+ }
525
+
526
+ /* ── Tabs · Table · Empty state ───────────────────────────────────────────────
527
+ Plain-CSS primitives for surfaces that are NOT React (gemcare's vanilla dashboard reads this
528
+ file through `npm run export:ui-css`). The React app has Polaris <Tabs>/<DataTable>/<EmptyState>
529
+ for these; nothing in apps/admin uses the classes below, and they restyle no existing component.
530
+ Only `--ui-*` / `--gemboss-*` tokens, so the dark `.gemboss-admin-shell` bridge applies too.
531
+
532
+ Tabs: `<div class="ui-tabs" role="tablist"><button role="tab" aria-selected="true">…` — the
533
+ selected state IS `aria-selected`, so what a screen reader announces and what is painted cannot
534
+ disagree. 44px under EVERY pointer (§9.1 control tier), not only coarse: tabs sit 4px apart, and
535
+ that gap is only acceptable between targets that are already 44px. */
536
+ .ui-tabs {
537
+ display: flex; gap: var(--ui-space-1); border-bottom: 1px solid var(--ui-border);
538
+ overflow-x: auto; scrollbar-width: none;
539
+ }
540
+ .ui-tabs > button {
541
+ position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
542
+ min-height: 44px; padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
543
+ border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;
544
+ font-family: inherit; font-size: var(--gemboss-fs-body); font-weight: 500; white-space: nowrap;
545
+ color: var(--ui-text-muted);
546
+ transition: color var(--gemboss-transition-fast) ease, background-color var(--gemboss-transition-fast) ease;
547
+ }
548
+ .ui-tabs > button:hover { color: var(--ui-text); background: var(--ui-surface-2); }
549
+ .ui-tabs > button:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
550
+ .ui-tabs > button[aria-selected="true"] { color: var(--ui-text-strong); font-weight: 600; }
551
+ .ui-tabs > button[aria-selected="true"]::after {
552
+ content: ""; position: absolute; left: var(--ui-space-3); right: var(--ui-space-3); bottom: 0;
553
+ height: 2px; border-radius: var(--gemboss-radius-full); background: var(--ui-accent);
554
+ }
555
+ .ui-tabs > button:disabled { opacity: .5; cursor: not-allowed; }
556
+
557
+ /* Table: wrap it — `<div class="ui-table-wrap"><table class="ui-table">` — so a wide table scrolls
558
+ inside its card instead of pushing the page sideways at 320px. Numeric cells take `.is-num`:
559
+ right-aligned tabular figures, so a column of numbers lines up digit for digit. */
560
+ .ui-table-wrap {
561
+ overflow-x: auto; max-width: 100%;
562
+ background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
563
+ }
564
+ .ui-table { width: 100%; border-collapse: collapse; font-size: var(--gemboss-fs-body); color: var(--ui-text); }
565
+ .ui-table th,
566
+ .ui-table td {
567
+ padding: var(--ui-space-2) var(--ui-space-3); text-align: left; vertical-align: middle;
568
+ border-bottom: 1px solid var(--ui-border);
569
+ }
570
+ .ui-table th {
571
+ font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-text-muted); white-space: nowrap;
572
+ }
573
+ .ui-table tbody tr:last-child > td { border-bottom: 0; }
574
+ .ui-table tbody tr:hover { background: var(--ui-surface-2); }
575
+ .ui-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
576
+
577
+ /* Empty state: `<div class="ui-empty"><p class="ui-empty__title">…</p><p class="ui-empty__body">…</p>
578
+ <button class="ui-btn">…</button></div>`. Says what is missing and what to do next. */
579
+ .ui-empty {
580
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--ui-space-2);
581
+ padding: var(--ui-space-4); padding-block: 40px; text-align: center;
582
+ color: var(--ui-text-muted); font-size: var(--gemboss-fs-body);
583
+ }
584
+ .ui-empty__title { margin: 0; font-size: var(--gemboss-fs-h3); font-weight: 600; color: var(--ui-text-strong); }
585
+ .ui-empty__body { margin: 0; max-width: 48ch; line-height: 1.5; }
586
+ .ui-empty > .ui-btn { margin-top: var(--ui-space-2); }
587
+
588
+ /* ═══ gemboss-field.css ═══ */
589
+ /* THE ONE TEXT FIELD, in a file of its own.
590
+ *
591
+ * It lives apart from gemboss-shell.css because the shell sheet is loaded ONLY by `app/app/layout.tsx`
592
+ * — the /app subtree — while `/login` and `/try` are deliberately lean pages outside it. Adding the
593
+ * class on either changed NOTHING, because neither loads the shell. Measured on prod 23/09/2026 (the
594
+ * build before the change): /login drew its own field family in styled-jsx — 44px, radius 8, 14px,
595
+ * border rgb(201,204,207) — and /try's textarea was the native box, radius 0. An earlier version of
596
+ * this comment said both stayed "21px, unrounded, native border"; that was one field on /try written
597
+ * up as if it described both pages. A few hundred bytes of field rules can go where the shell cannot.
598
+ */
599
+ /* ── ONE text field for every hand-written <input>/<select> in the admin ─────────────────────────
600
+ Measured 22/09/2026 across every /app route: the 21 Polaris fields were one shape (32px, radius 8,
601
+ the input-border token, 13px → 16px on a phone), and the 13 hand-written ones were FIVE shapes —
602
+ heights 33/36/38/42/44, radii 8/9/10, one warm off-token border, and every one of them still 13-14px
603
+ on a phone, which is the size iOS Safari ZOOMS the page for the moment you tap the field. This is the
604
+ Polaris field, written once: a custom field wears `gemboss-field` and nothing else decides its look.
605
+ Its touch height lives in the pointer:coarse block with the Polaris rule it mirrors. */
606
+ /* …and the same box on a PRESSABLE control. Three buttons wore `gemboss-field` itself — the campaign
607
+ compare's "↻ Refresh", which sits in a row between two selects that wear it, and the inspector's ✕
608
+ and + — because what they wanted was this box, lined up with the fields beside them. But a button
609
+ is not a text field, and the class that MEANS "text field" should not be what says so: it makes the
610
+ ratchet count a button as a converted field, and it invites the next person to put it on anything
611
+ box-shaped. One box, two names, declared together so they can never drift apart. */
612
+ .gemboss-field,
613
+ .gemboss-control {
614
+ box-sizing: border-box; height: 32px; min-width: 0; padding: 0 12px;
615
+ font: inherit; font-size: 13px; line-height: 20px;
616
+ color: var(--gemboss-field-ink);
617
+ background-color: var(--gemboss-field-bg);
618
+ border: 1px solid var(--gemboss-field-line);
619
+ border-radius: 8px;
620
+ }
621
+ .gemboss-field::placeholder { color: var(--gemboss-field-placeholder); }
622
+ /* A NARROW field — the inspector's T/R/B/L boxes and its number spinners are 40-68px wide, and the
623
+ shared 12px side padding left 38px of content box for a three-digit value (measured in Chromium:
624
+ clientWidth 38, scrollWidth 58 for "120"). Same box, tighter lane. */
625
+ /* A field that is a 44px target at EVERY width, not only on a phone. Some surfaces asked for that in
626
+ their own inline style (a popup's `minHeight: 44`), and that intent is theirs to keep — it moves here
627
+ so the look still comes from one place. */
628
+ .gemboss-field--tall { min-height: 44px; }
629
+ .gemboss-field--narrow { padding: 0 6px; appearance: textfield; -moz-appearance: textfield; }
630
+ /* …and no spinner: the arrows eat ~11px of a 40px box, so "120" was still cut with the tighter
631
+ padding (measured: clientWidth 38, scrollWidth 49). The value is typed here, not stepped. */
632
+ .gemboss-field--narrow::-webkit-outer-spin-button,
633
+ .gemboss-field--narrow::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
634
+ .gemboss-field:hover:not(:disabled),
635
+ .gemboss-control:hover:not(:disabled) { border-color: var(--gemboss-field-line-hover); background-color: var(--gemboss-field-bg-hover); }
636
+ .gemboss-field:focus-visible,
637
+ .gemboss-control:focus-visible { outline: 2px solid #005bd3; outline-offset: 1px; border-color: var(--gemboss-field-line-active); }
638
+ .gemboss-field:disabled,
639
+ .gemboss-control:disabled { opacity: 0.5; cursor: not-allowed; }
640
+ /* Polaris' own breakpoint for the same switch: under it a field reads 16px, so iOS does not zoom. */
641
+ /* On a PHONE-WIDTH screen a field is 16px (iOS does not zoom) and 44px tall (§9.1) — by WIDTH, not by
642
+ pointer. A desktop browser resized to 320px still reports `pointer: fine`, and the page-revision
643
+ gate measures the publication select's height at 320px: dropping that panel's own `min-height: 44px`
644
+ in favour of the shared class left it 32px there, and the gate went red (CI of #3201). */
645
+ /* 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
646
+ 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
647
+ control đọc ra MỘT họ: nút Refresh của compare view ngồi giữa hai select, và ở 13px giữa hai
648
+ ô 16px thì nó là một cỡ chữ thứ hai trong cùng một hàng (/code-review, 23/09/2026). */
649
+ @media (max-width: 47.9975em) { .gemboss-field, .gemboss-control { font-size: 16px; min-height: 44px; } }
650
+ /* A multi-line field: Polaris' multiline TextField — the same box, its height from `rows`. */
651
+ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
652
+ /* A field made of parts — a fixed prefix ("/pages/") or a colour swatch beside the text. The WRAPPER
653
+ wears `gemboss-field gemboss-field--group` and is the box; the text inside wears `gemboss-field__bare`
654
+ and draws nothing of its own, taking the box's font size (13px, 16px on a phone) by inheritance. */
655
+ /* The GROUP grows to whatever it holds: `gemboss-field` fixes a 32px height, and a panel that asks
656
+ for 44px targets (the section content panel does, and `page-saved-continuation-fixture` measures
657
+ EVERY control there) puts a 44px input inside this box. Fixing the group at 32 and shrinking the
658
+ input to fit clipped it and broke that gate; the box follows its content instead. */
659
+ .gemboss-field--group { display: flex; align-items: center; gap: 6px; height: auto; min-height: 32px; padding-right: 0; overflow: hidden; }
660
+ /* The phone floor again, for the GROUP. `min-height: 32px` above is the same specificity as the
661
+ `.gemboss-field` rule in the media query and comes LATER in the file, so it won the cascade at
662
+ phone width and quietly took the group back down to 32px — the URL-handle and background-colour
663
+ rows, the two places a field is made of parts. A media query does not outrank anything; source
664
+ order decided it, and the fix is to say it again after. (/code-review, 23/09/2026.) */
665
+ @media (max-width: 47.9975em) { .gemboss-field--group { min-height: 44px; } }
666
+ .gemboss-field--group:focus-within { outline: 2px solid #005bd3; outline-offset: 1px; border-color: var(--gemboss-field-line-active); }
667
+ /* The colour swatch in a group: FLUSH to the group's left edge and full height — it covers the group's
668
+ 1px border top and bottom and its 12px left padding, so it is the whole 44px target the panel's rule
669
+ asks for, rounded by the group's own corner (overflow: hidden), and never clipped. A 44px swatch
670
+ INSIDE the 42px the border leaves was cut by a pixel top and bottom (/code-review #3201). */
671
+ .gemboss-field--group input[type="color"] { flex: none; align-self: stretch; width: 44px; min-width: 44px; height: auto; min-height: 0; margin: -1px 0 -1px -12px; padding: 0; border: 0; border-right: 1px solid var(--gemboss-field-line); border-radius: 0; background: none; cursor: pointer; }
672
+ /* ↑ the right edge is what keeps a WHITE swatch visible: without it the block melted into the field (measured). */
673
+ .gemboss-field--group input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
674
+ .gemboss-field--group input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 0; }
675
+ .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; }
676
+ /* …and the placeholder, which "draws nothing" does not cover: the surfaces that used to declare
677
+ their own (the /admin search rows, the settings and palette rows) stopped when they stopped
678
+ owning the box, and without this every bare field would fall back to the browser's grey. */
679
+ .gemboss-field__bare::placeholder { color: var(--gemboss-field-placeholder); }
680
+
681
+ /* Under a thumb the field is 44px, the same height this app gives a Polaris field there. It sits in
682
+ THIS file, not in the shell's pointer:coarse block, so /login and /try — which load only this
683
+ sheet — get it too. */
684
+ @media (pointer: coarse) { .gemboss-field, .gemboss-control { height: auto; min-height: 44px; } }
685
+ /* A control is pressable; a field is not. That is the whole difference. */
686
+ .gemboss-control { cursor: pointer; }
687
+ /* An ICON control — the repeater's ✕ — is square and narrow beside its row. It was pinned to
688
+ `width: 32` in an inline style, which no media query can reach, so under a thumb it was a
689
+ 32x44 target: tall enough, too narrow (§9.1 asks 44 in BOTH dimensions for a control).
690
+ The width lives here now, and grows with the pointer. (/code-review, 23/09/2026.) */
691
+ /* Hai class trên cùng phần tử, có chủ ý: `.gemboss-ei-inp` đặt `width: 100%` ở cùng trọng số và
692
+ nằm SAU trong thứ tự nạp (shell @import file này ở đầu), nên một selector đơn thua nó — đo
693
+ đượ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
694
+ vào CSS thì phải mang theo trọng số của chính nó. */
695
+ .gemboss-control.gemboss-control--icon { width: 32px; flex: 0 0 auto; padding: 0; }
696
+ @media (pointer: coarse) { .gemboss-control.gemboss-control--icon { width: 44px; min-width: 44px; } }
697
+
698
+ /* ═══ shell.css — the shared shell (rail + topbar + main + the assistant's thread rail) ═══ */
699
+ .gemboss-popover-surface {
700
+ background: var(--gemboss-surface);
701
+ background: color-mix(in srgb, var(--gemboss-surface) 93%, transparent);
702
+ -webkit-backdrop-filter: blur(14px) saturate(1.4);
703
+ backdrop-filter: blur(14px) saturate(1.4);
704
+ border: 1px solid var(--gemboss-divider);
705
+ border-radius: var(--gemboss-radius-lg);
706
+ box-shadow: var(--gemboss-shadow-raised);
707
+ }
708
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
709
+ .gemboss-popover-surface {
710
+ background: var(--gemboss-surface);
711
+ }
712
+ }
713
+ .icon-nav {
714
+ position: fixed;
715
+ top: 56px;
716
+ left: 0;
717
+ z-index: 200;
718
+ width: 72px;
719
+ height: calc(100dvh - 56px);
720
+ background: var(--gemboss-rail-bg);
721
+ border-right: 1px solid var(--gemboss-divider);
722
+ display: flex;
723
+ flex-direction: column;
724
+ padding-top: 8px;
725
+ overflow: hidden;
726
+ }
727
+ .gemboss-account-menu {
728
+ position: absolute;
729
+ top: calc(100% + 6px);
730
+ right: 0;
731
+ min-width: 220px;
732
+ z-index: 320;
733
+ overflow: hidden;
734
+ }
735
+ .gemboss-account-menu-item {
736
+ display: block;
737
+ width: 100%;
738
+ min-height: 44px;
739
+ padding: 12px 16px;
740
+ border: 0;
741
+ background: transparent;
742
+ font: inherit;
743
+ font-size: 13px;
744
+ color: var(--gemboss-text);
745
+ text-align: left;
746
+ text-decoration: none;
747
+ cursor: pointer;
748
+ transition: background var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
749
+ }
750
+ .gemboss-account-menu-item:hover {
751
+ background: var(--gemboss-hover);
752
+ }
753
+ .gemboss-account-menu-item:focus-visible {
754
+ outline: 2px solid var(--gemboss-primary);
755
+ outline-offset: -2px;
756
+ background: var(--gemboss-hover);
757
+ }
758
+ .gemboss-account-menu-item.danger {
759
+ color: var(--gemboss-danger-strong);
760
+ }
761
+ .gemboss-account-chip {
762
+ width: 30px;
763
+ height: 30px;
764
+ border: 1px solid #cfd6ce;
765
+ border-radius: var(--gemboss-radius-full);
766
+ background: #f5f7f4;
767
+ color: #303633;
768
+ display: inline-flex;
769
+ align-items: center;
770
+ justify-content: center;
771
+ flex: 0 0 auto;
772
+ font-size: 11px;
773
+ font-weight: 700;
774
+ cursor: pointer;
775
+ }
776
+ .gemboss-platform-main {
777
+ box-sizing: border-box;
778
+ min-height: 100dvh;
779
+ margin-left: 72px;
780
+ padding-top: 56px;
781
+ background: var(--gemboss-app-bg);
782
+ }
783
+ .icon-nav {
784
+ top: 56px;
785
+ height: calc(100dvh - 56px);
786
+ background: var(--gemboss-nav-bg);
787
+ border-right-color: var(--gemboss-divider);
788
+ }
789
+ @media (max-width: 1040px) {
790
+ .gemboss-platform-topbar {
791
+ grid-template-columns: minmax(120px, 1fr) minmax(220px, 520px) minmax(120px, 1fr);
792
+ }
793
+ }
794
+ @media (max-width: 760px) {
795
+ .gemboss-platform-main {
796
+ margin-left: 0;
797
+ }
798
+ }
799
+ .gemboss-account-chip {
800
+ width: 32px;
801
+ height: 32px;
802
+ border-radius: 50%;
803
+ background: var(--gemboss-primary);
804
+ color: #fff;
805
+ font-weight: 600;
806
+ font-size: 12px;
807
+ display: inline-flex;
808
+ align-items: center;
809
+ justify-content: center;
810
+ border: none;
811
+ cursor: pointer;
812
+ transition: transform var(--gemboss-transition-base) ease;
813
+ }
814
+ .gemboss-account-chip:hover {
815
+ transform: scale(1.05);
816
+ }
817
+ .gemboss-account-chip:focus-visible {
818
+ outline: 2px solid var(--gemboss-primary);
819
+ outline-offset: 2px;
820
+ }
821
+ .gemboss-platform-main {
822
+ margin-top: 56px;
823
+ margin-left: 72px;
824
+ min-height: calc(100vh - 56px);
825
+ padding: 24px;
826
+ background: #fafafb;
827
+ }
828
+ @media (max-width: 768px) {
829
+ .gemboss-platform-main {
830
+ margin-left: 0;
831
+ padding: 16px;
832
+ }
833
+ }
834
+ .gemboss-platform-shell {
835
+ display: block;
836
+ min-height: 100vh;
837
+ background: var(--gemboss-app-bg);
838
+ }
839
+ .gemboss-platform-topbar {
840
+ position: fixed;
841
+ inset: 0 0 auto 0;
842
+ z-index: 300;
843
+ height: 56px;
844
+ border-bottom: 1px solid var(--gemboss-divider);
845
+ background: var(--gemboss-topbar-bg);
846
+ display: grid;
847
+ grid-template-columns: minmax(200px, 1fr) minmax(280px, 560px) minmax(200px, 1fr);
848
+ align-items: center;
849
+ gap: 16px;
850
+ padding: 0 16px;
851
+ box-shadow: var(--gemboss-shadow-resting);
852
+ }
853
+ .gemboss-account-chip {
854
+ width: 32px;
855
+ height: 32px;
856
+ border: 1px solid var(--gemboss-chip-line);
857
+ border-radius: var(--gemboss-radius-full);
858
+ background: var(--gemboss-surface);
859
+ color: var(--gemboss-chip-ink);
860
+ display: inline-flex;
861
+ align-items: center;
862
+ justify-content: center;
863
+ flex: 0 0 auto;
864
+ font-size: 11px;
865
+ font-weight: 700;
866
+ cursor: pointer;
867
+ transition: border-color var(--gemboss-transition-base) ease, background-color var(--gemboss-transition-base) ease;
868
+ }
869
+ .gemboss-account-chip:hover {
870
+ border-color: var(--gemboss-chip-line-hover);
871
+ background: var(--gemboss-chip-bg-hover);
872
+ transform: none;
873
+ }
874
+ .icon-nav {
875
+ position: fixed;
876
+ top: 56px;
877
+ left: 0;
878
+ z-index: 200;
879
+ width: 72px;
880
+ height: calc(100dvh - 56px);
881
+ border-right: 1px solid var(--gemboss-divider);
882
+ background: var(--gemboss-nav-bg);
883
+ padding-top: 12px;
884
+ }
885
+ .gemboss-platform-main {
886
+ min-height: calc(100vh - 56px);
887
+ margin-top: 56px;
888
+ margin-left: 72px;
889
+ background: var(--gemboss-app-bg);
890
+ padding: 24px;
891
+ }
892
+ @media (max-width: 1120px) {
893
+ .gemboss-platform-topbar {
894
+ grid-template-columns: 220px minmax(220px, 1fr) auto;
895
+ gap: 12px;
896
+ }
897
+ }
898
+ @media (max-width: 760px) {
899
+ .gemboss-platform-topbar {
900
+ height: auto;
901
+ min-height: 56px;
902
+ grid-template-columns: minmax(0, 1fr) auto;
903
+ padding: 12px 12px;
904
+ }
905
+ .gemboss-account-chip {
906
+ display: none;
907
+ }
908
+ .icon-nav {
909
+ display: none;
910
+ }
911
+ .gemboss-platform-main {
912
+ margin-left: 0;
913
+ padding: 16px;
914
+ padding-top: 104px;
915
+ }
916
+ }
917
+ .gnav-scroll {
918
+ scrollbar-width: thin;
919
+ scrollbar-color: rgba(31,30,29,0.22) transparent;
920
+ }
921
+ .gemboss-assistant-railfoot.is-button {
922
+ width: 100%;
923
+ border: none;
924
+ background: none;
925
+ cursor: pointer;
926
+ text-align: left;
927
+ font: inherit;
928
+ border-radius: 8px;
929
+ transition: background 0.12s ease, color 0.12s ease;
930
+ }
931
+ .gemboss-assistant-railfoot.is-button:hover {
932
+ background: var(--gemboss-primary-soft);
933
+ color: var(--gemboss-primary-dark);
934
+ }
935
+ .gemboss-assistant-empty {
936
+ padding: 24px 20px;
937
+ color: var(--gemboss-text-muted);
938
+ font-size: 13px;
939
+ }
940
+ @media (max-width: 760px) {
941
+ .gemboss-platform-topbar {
942
+ display: flex;
943
+ align-items: center;
944
+ gap: 8px;
945
+ height: 56px;
946
+ min-height: 56px;
947
+ padding: 0 12px;
948
+ }
949
+ .gemboss-account-chip {
950
+ display: inline-flex;
951
+ }
952
+ .gemboss-platform-main {
953
+ padding-top: 56px;
954
+ padding-bottom: calc(56px + env(safe-area-inset-bottom));
955
+ }
956
+ }
957
+ @media (pointer: coarse) {
958
+ .gemboss-account-chip {
959
+ width: 44px;
960
+ height: 44px;
961
+ }
962
+ .gnav-item {
963
+ min-height: 44px;
964
+ }
965
+ .gemboss-assistant-collapse,
966
+ .gemboss-assistant-newchat {
967
+ min-height: 44px;
968
+ min-width: 44px;
969
+ }
970
+ }
971
+ .icon-nav {
972
+ width: 240px;
973
+ padding: 12px 12px 0;
974
+ overflow: hidden;
975
+ background: var(--gemboss-nav-bg);
976
+ display: flex;
977
+ flex-direction: column;
978
+ }
979
+ .gnav-scroll {
980
+ flex: 1;
981
+ overflow-y: auto;
982
+ overflow-x: hidden;
983
+ }
984
+ .gnav-bottom {
985
+ padding-bottom: 12px;
986
+ }
987
+ .gnav-item {
988
+ display: flex;
989
+ align-items: center;
990
+ gap: 12px;
991
+ width: 100%;
992
+ padding: 8px 12px;
993
+ margin: 1px 0;
994
+ border: 0;
995
+ background: transparent;
996
+ cursor: pointer;
997
+ text-align: left;
998
+ border-radius: var(--gemboss-radius-md);
999
+ text-decoration: none;
1000
+ font-family: inherit;
1001
+ color: var(--gemboss-text);
1002
+ font-size: var(--gemboss-fs-body);
1003
+ font-weight: 500;
1004
+ line-height: 1.2;
1005
+ transition: background-color var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
1006
+ }
1007
+ .gnav-item:hover {
1008
+ background: var(--gemboss-nav-hover);
1009
+ }
1010
+ .gnav-item.active {
1011
+ color: var(--gemboss-active-fg);
1012
+ font-weight: 600;
1013
+ }
1014
+ .gnav-group:not(.has-active-child) > .gnav-item.active {
1015
+ background: var(--gemboss-active-bg);
1016
+ }
1017
+ .gnav-group:not(.has-active-child) > .gnav-item.active:hover {
1018
+ background: var(--gemboss-active-bg);
1019
+ }
1020
+ .gnav-group.has-active-child > .gnav-item.active:hover {
1021
+ background: var(--gemboss-nav-hover);
1022
+ }
1023
+ .gnav-ico {
1024
+ flex: 0 0 20px;
1025
+ width: 20px;
1026
+ height: 20px;
1027
+ display: grid;
1028
+ place-items: center;
1029
+ }
1030
+ .gnav-ico svg {
1031
+ width: 20px;
1032
+ height: 20px;
1033
+ fill: currentColor;
1034
+ }
1035
+ .gnav-label {
1036
+ flex: 1;
1037
+ min-width: 0;
1038
+ white-space: nowrap;
1039
+ overflow: hidden;
1040
+ text-overflow: ellipsis;
1041
+ }
1042
+ .gnav-sub {
1043
+ list-style: none;
1044
+ margin: 1px 0 6px;
1045
+ padding: 0;
1046
+ }
1047
+ .gnav-subitem {
1048
+ position: relative;
1049
+ display: flex;
1050
+ align-items: center;
1051
+ justify-content: space-between;
1052
+ gap: 8px;
1053
+ padding: 6px 12px 6px 44px;
1054
+ margin: 1px 0;
1055
+ border-radius: var(--gemboss-radius-sm);
1056
+ color: var(--gemboss-text-muted);
1057
+ font-size: 13px;
1058
+ line-height: 1.2;
1059
+ text-decoration: none;
1060
+ white-space: nowrap;
1061
+ transition: background-color var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
1062
+ }
1063
+ @media (pointer: coarse) {
1064
+ .gnav-subitem {
1065
+ min-height: 44px;
1066
+ }
1067
+ }
1068
+ .gnav-subitem:hover {
1069
+ background: var(--gemboss-nav-hover);
1070
+ color: var(--gemboss-text);
1071
+ }
1072
+ .gnav-subitem.active {
1073
+ color: var(--gemboss-active-fg);
1074
+ font-weight: 600;
1075
+ background: var(--gemboss-active-bg);
1076
+ }
1077
+ .gnav-subitem.active:hover {
1078
+ background: var(--gemboss-active-bg);
1079
+ color: var(--gemboss-active-fg);
1080
+ }
1081
+ .gnav-subitem.active::before {
1082
+ content: "";
1083
+ position: absolute;
1084
+ left: 20px;
1085
+ top: 50%;
1086
+ transform: translateY(-50%);
1087
+ width: 3px;
1088
+ height: 15px;
1089
+ border-radius: 2px;
1090
+ background: var(--gemboss-primary);
1091
+ }
1092
+ .gnav-subitem.is-soon {
1093
+ opacity: 0.5;
1094
+ cursor: default;
1095
+ }
1096
+ .gnav-subitem em {
1097
+ font-style: normal;
1098
+ font-size: var(--gemboss-fs-micro);
1099
+ font-weight: 600;
1100
+ letter-spacing: 0.04em;
1101
+ text-transform: uppercase;
1102
+ border: 1px solid var(--gemboss-divider);
1103
+ border-radius: var(--gemboss-radius-sm);
1104
+ padding: 1px 6px;
1105
+ }
1106
+ .gnav-sep {
1107
+ height: 1px;
1108
+ background: var(--gemboss-divider);
1109
+ margin: 8px 6px;
1110
+ }
1111
+ .gnav-item.gnav-assistant {
1112
+ color: var(--gemboss-primary-dark);
1113
+ font-weight: 600;
1114
+ background: color-mix(in srgb, var(--gemboss-primary) 8%, #fff);
1115
+ }
1116
+ .gnav-item.gnav-assistant .gnav-ico svg {
1117
+ fill: var(--gemboss-primary);
1118
+ }
1119
+ .gnav-item.gnav-assistant:hover {
1120
+ background: color-mix(in srgb, var(--gemboss-primary) 15%, #fff);
1121
+ }
1122
+ .gnav-item.gnav-assistant.active {
1123
+ background: var(--gemboss-active-bg);
1124
+ color: var(--gemboss-active-fg);
1125
+ }
1126
+ .gnav-item.gnav-assistant.active .gnav-ico svg {
1127
+ fill: var(--gemboss-active-fg);
1128
+ }
1129
+ .gemboss-platform-main {
1130
+ margin-left: 240px;
1131
+ }
1132
+ .gemboss-mobile-menu-btn {
1133
+ display: none;
1134
+ }
1135
+ .gemboss-mobile-nav-backdrop {
1136
+ display: none;
1137
+ }
1138
+ @media (max-width: 760px) {
1139
+ .gemboss-platform-main {
1140
+ margin-left: 0;
1141
+ padding-bottom: env(safe-area-inset-bottom);
1142
+ }
1143
+ .gemboss-mobile-menu-btn {
1144
+ display: inline-flex;
1145
+ align-items: center;
1146
+ justify-content: center;
1147
+ flex: 0 0 auto;
1148
+ width: 38px;
1149
+ height: 38px;
1150
+ margin-left: -2px;
1151
+ border: 0;
1152
+ border-radius: 8px;
1153
+ background: transparent;
1154
+ cursor: pointer;
1155
+ color: var(--gemboss-text);
1156
+ }
1157
+ .gemboss-mobile-menu-btn:active {
1158
+ background: var(--gemboss-hover);
1159
+ }
1160
+ .icon-nav {
1161
+ display: flex;
1162
+ width: min(82vw, 300px);
1163
+ transform: translateX(-100%);
1164
+ transition: transform 0.26s cubic-bezier(0.4, 0, 0.2, 1);
1165
+ z-index: 1000;
1166
+ box-shadow: 2px 0 26px rgba(31, 30, 29, 0.20);
1167
+ }
1168
+ .gemboss-platform-shell.is-mobile-nav-open .icon-nav {
1169
+ transform: translateX(0);
1170
+ }
1171
+ .gemboss-mobile-nav-backdrop {
1172
+ display: block;
1173
+ position: fixed;
1174
+ inset: 56px 0 0 0;
1175
+ width: 100%;
1176
+ border: 0;
1177
+ padding: 0;
1178
+ background: rgba(20, 19, 18, 0.38);
1179
+ opacity: 0;
1180
+ pointer-events: none;
1181
+ transition: opacity 0.22s ease;
1182
+ z-index: 999;
1183
+ }
1184
+ .gemboss-platform-shell.is-mobile-nav-open .gemboss-mobile-nav-backdrop {
1185
+ opacity: 1;
1186
+ pointer-events: auto;
1187
+ }
1188
+ }
1189
+ @media (max-width: 305.98px) {
1190
+ .gemboss-platform-topbar {
1191
+ padding: 0 8px;
1192
+ }
1193
+ }
1194
+ @media (min-width: 306px) and (max-width: 460px) {
1195
+ .gemboss-platform-topbar {
1196
+ padding: 0 8px;
1197
+ }
1198
+ .gemboss-mobile-menu-btn {
1199
+ width: 44px;
1200
+ height: 44px;
1201
+ }
1202
+ .gemboss-account-chip {
1203
+ width: 44px;
1204
+ height: 44px;
1205
+ }
1206
+ }
1207
+ .gemboss-assistant-chatslot {
1208
+ display: flex;
1209
+ flex-direction: column;
1210
+ flex: 1 1 auto;
1211
+ min-height: 0;
1212
+ min-width: 0;
1213
+ }
1214
+ .gemboss-assistant-chatslot[hidden] {
1215
+ display: none;
1216
+ }
1217
+ .gemboss-assistant-page > .gemboss-assistant-tabs {
1218
+ flex: 0 0 auto;
1219
+ display: flex;
1220
+ align-items: center;
1221
+ gap: 4px;
1222
+ padding: 4px 8px 0;
1223
+ overflow-x: auto;
1224
+ scrollbar-width: none;
1225
+ }
1226
+ .gemboss-assistant-tabs::-webkit-scrollbar {
1227
+ display: none;
1228
+ }
1229
+ .gemboss-assistant-tab {
1230
+ display: flex;
1231
+ align-items: center;
1232
+ flex: 0 1 auto;
1233
+ min-width: 0;
1234
+ max-width: 190px;
1235
+ border: 1px solid var(--gemboss-divider);
1236
+ border-radius: var(--gemboss-radius-md);
1237
+ background: transparent;
1238
+ }
1239
+ .gemboss-assistant-tab.active {
1240
+ background: var(--gemboss-active-bg);
1241
+ border-color: var(--gemboss-active-bg);
1242
+ }
1243
+ .gemboss-assistant-tab-main {
1244
+ display: flex;
1245
+ align-items: center;
1246
+ gap: 6px;
1247
+ min-width: 0;
1248
+ min-height: 44px;
1249
+ padding: 0 4px 0 8px;
1250
+ border: 0;
1251
+ background: transparent;
1252
+ font: inherit;
1253
+ font-size: var(--gemboss-fs-caption);
1254
+ color: var(--gemboss-text-muted);
1255
+ cursor: pointer;
1256
+ }
1257
+ .gemboss-assistant-tab.active .gemboss-assistant-tab-main {
1258
+ color: var(--gemboss-text-strong);
1259
+ }
1260
+ .gemboss-assistant-tab-title {
1261
+ overflow: hidden;
1262
+ text-overflow: ellipsis;
1263
+ white-space: nowrap;
1264
+ }
1265
+ .gemboss-assistant-tab-dot {
1266
+ flex: 0 0 auto;
1267
+ width: 6px;
1268
+ height: 6px;
1269
+ border-radius: var(--gemboss-radius-full);
1270
+ background: var(--gemboss-divider);
1271
+ }
1272
+ .gemboss-assistant-tab-dot.is-running {
1273
+ background: var(--gemboss-primary);
1274
+ animation: gemboss-assistant-pulse 1.4s ease-in-out infinite;
1275
+ }
1276
+ .gemboss-assistant-tab-close {
1277
+ flex: 0 0 auto;
1278
+ width: 44px;
1279
+ height: 44px;
1280
+ display: grid;
1281
+ place-items: center;
1282
+ border: 0;
1283
+ background: transparent;
1284
+ color: var(--gemboss-text-muted);
1285
+ cursor: pointer;
1286
+ padding: 0;
1287
+ }
1288
+ .gemboss-assistant-tab-close:hover {
1289
+ color: var(--gemboss-text-strong);
1290
+ }
1291
+ .gemboss-assistant-tab-close svg {
1292
+ width: 14px;
1293
+ height: 14px;
1294
+ }
1295
+ .gemboss-assistant-sronly {
1296
+ position: absolute;
1297
+ width: 1px;
1298
+ height: 1px;
1299
+ padding: 0;
1300
+ margin: -1px;
1301
+ overflow: hidden;
1302
+ clip: rect(0 0 0 0);
1303
+ white-space: nowrap;
1304
+ border: 0;
1305
+ }
1306
+ .gemboss-assistant-thread-running {
1307
+ flex: 0 0 auto;
1308
+ width: 6px;
1309
+ height: 6px;
1310
+ border-radius: var(--gemboss-radius-full);
1311
+ background: var(--gemboss-primary);
1312
+ animation: gemboss-assistant-pulse 1.4s ease-in-out infinite;
1313
+ }
1314
+ @media (prefers-reduced-motion: reduce) {
1315
+ .gemboss-assistant-tab-dot.is-running,
1316
+ .gemboss-assistant-thread-running {
1317
+ animation: none;
1318
+ }
1319
+ }
1320
+ .gemboss-assistant-route-claim {
1321
+ display: block;
1322
+ height: 100%;
1323
+ }
1324
+ .gemboss-assistant-layout.is-dock {
1325
+ height: auto;
1326
+ min-height: 0;
1327
+ }
1328
+ .gemboss-assistant-layout.is-dock .gemboss-assistant-page {
1329
+ height: auto;
1330
+ max-width: none;
1331
+ padding: 4px 0 8px;
1332
+ }
1333
+ .gnav-item.gnav-assistant {
1334
+ position: relative;
1335
+ }
1336
+ body.gemboss-assistant-busy .gnav-item.gnav-assistant::after,
1337
+ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
1338
+ content: "";
1339
+ position: absolute;
1340
+ top: 6px;
1341
+ left: 26px;
1342
+ width: 8px;
1343
+ height: 8px;
1344
+ border-radius: 50%;
1345
+ border: 2px solid var(--gemboss-surface);
1346
+ }
1347
+ body.gemboss-assistant-busy .gnav-item.gnav-assistant::after {
1348
+ background: var(--gemboss-primary);
1349
+ animation: gemboss-assistant-pulse 1.4s ease-in-out infinite;
1350
+ }
1351
+ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
1352
+ background: var(--gemboss-success-strong);
1353
+ }
1354
+ @media (prefers-reduced-motion: reduce) {
1355
+ body.gemboss-assistant-busy .gnav-item.gnav-assistant::after {
1356
+ animation: none;
1357
+ }
1358
+ }
1359
+ .gemboss-assistant-layout {
1360
+ display: flex;
1361
+ height: calc(100dvh - 56px);
1362
+ width: 100%;
1363
+ min-height: 0;
1364
+ }
1365
+ .gemboss-assistant-rail {
1366
+ flex: 0 0 264px;
1367
+ width: 264px;
1368
+ display: flex;
1369
+ flex-direction: column;
1370
+ min-height: 0;
1371
+ border-right: 1px solid var(--gemboss-divider);
1372
+ background: transparent;
1373
+ padding: 16px 12px 12px;
1374
+ gap: 12px;
1375
+ transition: flex-basis 180ms ease, width 180ms ease;
1376
+ }
1377
+ .gemboss-assistant-rail-top {
1378
+ display: flex;
1379
+ align-items: center;
1380
+ gap: 6px;
1381
+ }
1382
+ .gemboss-assistant-newchat {
1383
+ display: flex;
1384
+ align-items: center;
1385
+ gap: 8px;
1386
+ flex: 1 1 auto;
1387
+ min-width: 0;
1388
+ padding: 12px 12px;
1389
+ border: 1px solid var(--gemboss-divider);
1390
+ border-radius: 12px;
1391
+ background: transparent;
1392
+ color: var(--gemboss-text);
1393
+ font-size: var(--gemboss-fs-body);
1394
+ font-weight: 600;
1395
+ cursor: pointer;
1396
+ transition: background 120ms ease, border-color 120ms ease;
1397
+ }
1398
+ .gemboss-assistant-newchat:hover {
1399
+ background: rgba(0,0,0,0.035);
1400
+ border-color: var(--gemboss-text-muted);
1401
+ }
1402
+ .gemboss-assistant-newchat svg {
1403
+ width: 17px;
1404
+ height: 17px;
1405
+ fill: currentColor;
1406
+ flex: 0 0 auto;
1407
+ }
1408
+ .gemboss-assistant-newchat span {
1409
+ white-space: nowrap;
1410
+ overflow: hidden;
1411
+ text-overflow: ellipsis;
1412
+ flex: 1 1 auto;
1413
+ }
1414
+ .gemboss-assistant-kbd {
1415
+ flex: 0 0 auto;
1416
+ font-size: var(--gemboss-fs-micro);
1417
+ font-weight: 600;
1418
+ line-height: 1;
1419
+ padding: 4px 6px;
1420
+ border: 1px solid var(--gemboss-divider);
1421
+ border-radius: 6px;
1422
+ background: transparent;
1423
+ color: var(--gemboss-text-muted);
1424
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
1425
+ }
1426
+ .gemboss-assistant-search:focus-within .gemboss-assistant-kbd {
1427
+ display: none;
1428
+ }
1429
+ .gemboss-assistant-collapse {
1430
+ flex: 0 0 auto;
1431
+ display: grid;
1432
+ place-items: center;
1433
+ width: 34px;
1434
+ height: 34px;
1435
+ border: 0;
1436
+ border-radius: 8px;
1437
+ background: transparent;
1438
+ color: var(--gemboss-text-muted);
1439
+ cursor: pointer;
1440
+ transition: background 120ms ease, color 120ms ease;
1441
+ }
1442
+ .gemboss-assistant-collapse:hover {
1443
+ background: rgba(0,0,0,0.05);
1444
+ color: var(--gemboss-text);
1445
+ }
1446
+ .gemboss-assistant-collapse svg {
1447
+ width: 16px;
1448
+ height: 16px;
1449
+ fill: currentColor;
1450
+ }
1451
+ .gemboss-assistant-search {
1452
+ gap: 6px;
1453
+ }
1454
+ .gemboss-assistant-search > svg {
1455
+ width: 15px;
1456
+ height: 15px;
1457
+ fill: var(--gemboss-text-muted);
1458
+ flex: 0 0 auto;
1459
+ }
1460
+ .gemboss-assistant-search-clear {
1461
+ display: grid;
1462
+ place-items: center;
1463
+ width: 20px;
1464
+ height: 20px;
1465
+ border: 0;
1466
+ border-radius: 6px;
1467
+ background: transparent;
1468
+ color: var(--gemboss-text-muted);
1469
+ cursor: pointer;
1470
+ }
1471
+ .gemboss-assistant-search-clear:hover {
1472
+ background: rgba(0,0,0,0.07);
1473
+ color: var(--gemboss-text);
1474
+ }
1475
+ .gemboss-assistant-search-clear svg {
1476
+ width: 13px;
1477
+ height: 13px;
1478
+ fill: currentColor;
1479
+ }
1480
+ .gemboss-assistant-railfoot {
1481
+ flex: 0 0 auto;
1482
+ display: flex;
1483
+ align-items: center;
1484
+ gap: 6px;
1485
+ padding: 8px 12px 2px;
1486
+ border-top: 1px solid var(--gemboss-divider);
1487
+ margin-top: 4px;
1488
+ color: var(--gemboss-text-muted);
1489
+ font-size: var(--gemboss-fs-caption);
1490
+ line-height: 1.35;
1491
+ }
1492
+ .gemboss-assistant-railfoot svg {
1493
+ width: 13px;
1494
+ height: 13px;
1495
+ flex: 0 0 auto;
1496
+ fill: currentColor;
1497
+ opacity: 0.8;
1498
+ }
1499
+ .gemboss-assistant-threads {
1500
+ flex: 1 1 auto;
1501
+ min-height: 0;
1502
+ overflow-y: auto;
1503
+ display: flex;
1504
+ flex-direction: column;
1505
+ gap: 2px;
1506
+ }
1507
+ .gemboss-assistant-threads-empty {
1508
+ padding: 16px 8px;
1509
+ color: var(--gemboss-text-muted);
1510
+ font-size: var(--gemboss-fs-caption);
1511
+ }
1512
+ .gemboss-assistant-group {
1513
+ display: flex;
1514
+ flex-direction: column;
1515
+ gap: 2px;
1516
+ }
1517
+ .gemboss-assistant-group + .gemboss-assistant-group {
1518
+ margin-top: 12px;
1519
+ }
1520
+ .gemboss-assistant-group-label {
1521
+ padding: 4px 12px 4px;
1522
+ font-size: 11px;
1523
+ font-weight: 600;
1524
+ letter-spacing: 0.02em;
1525
+ color: var(--gemboss-text-muted);
1526
+ text-transform: uppercase;
1527
+ }
1528
+ .gemboss-assistant-thread {
1529
+ position: relative;
1530
+ display: flex;
1531
+ align-items: center;
1532
+ border-radius: 8px;
1533
+ transition: background 120ms ease;
1534
+ }
1535
+ .gemboss-assistant-thread:hover {
1536
+ background: var(--gemboss-hover, rgba(0,0,0,0.04));
1537
+ }
1538
+ .gemboss-assistant-thread.active {
1539
+ background: rgba(0,0,0,0.07);
1540
+ }
1541
+ .gemboss-assistant-thread-main {
1542
+ flex: 1 1 auto;
1543
+ min-width: 0;
1544
+ display: flex;
1545
+ flex-direction: column;
1546
+ align-items: flex-start;
1547
+ gap: 2px;
1548
+ padding: 8px 12px;
1549
+ border: 0;
1550
+ background: transparent;
1551
+ text-align: left;
1552
+ cursor: pointer;
1553
+ }
1554
+ .gemboss-assistant-thread-titlerow {
1555
+ display: flex;
1556
+ align-items: center;
1557
+ gap: 6px;
1558
+ width: 100%;
1559
+ min-width: 0;
1560
+ }
1561
+ .gemboss-assistant-pin-dot {
1562
+ width: 12px;
1563
+ height: 12px;
1564
+ flex: 0 0 auto;
1565
+ fill: var(--gemboss-text-muted);
1566
+ }
1567
+ .gemboss-assistant-thread-title {
1568
+ flex: 1 1 auto;
1569
+ min-width: 0;
1570
+ font-size: 13px;
1571
+ font-weight: 500;
1572
+ color: var(--gemboss-text);
1573
+ white-space: nowrap;
1574
+ overflow: hidden;
1575
+ text-overflow: ellipsis;
1576
+ }
1577
+ .gemboss-assistant-thread-meta {
1578
+ font-size: var(--gemboss-fs-caption);
1579
+ color: var(--gemboss-text-muted);
1580
+ }
1581
+ .gemboss-assistant-thread.menu-open {
1582
+ background: rgba(0,0,0,0.07);
1583
+ }
1584
+ .gemboss-assistant-thread.menu-open .gemboss-assistant-thread-actions {
1585
+ opacity: 1;
1586
+ }
1587
+ .gemboss-assistant-menu {
1588
+ z-index: 60;
1589
+ width: 184px;
1590
+ padding: 6px;
1591
+ display: flex;
1592
+ flex-direction: column;
1593
+ }
1594
+ .gemboss-assistant-menu-confirm {
1595
+ padding: 8px 12px 6px;
1596
+ font-size: var(--gemboss-fs-caption);
1597
+ font-weight: 600;
1598
+ color: var(--gemboss-text);
1599
+ }
1600
+ .gemboss-assistant-menu button {
1601
+ display: flex;
1602
+ align-items: center;
1603
+ gap: 8px;
1604
+ width: 100%;
1605
+ padding: 8px 12px;
1606
+ border: 0;
1607
+ border-radius: 8px;
1608
+ background: transparent;
1609
+ text-align: left;
1610
+ font-size: 13px;
1611
+ color: var(--gemboss-text);
1612
+ cursor: pointer;
1613
+ }
1614
+ .gemboss-assistant-menu button:hover {
1615
+ background: var(--gemboss-hover, rgba(0,0,0,0.05));
1616
+ }
1617
+ .gemboss-assistant-menu button.danger {
1618
+ color: var(--gemboss-danger-strong);
1619
+ }
1620
+ .gemboss-assistant-menu button svg {
1621
+ width: 15px;
1622
+ height: 15px;
1623
+ fill: currentColor;
1624
+ flex: 0 0 auto;
1625
+ }
1626
+ .gemboss-assistant-thread-actions {
1627
+ position: absolute;
1628
+ right: 6px;
1629
+ display: flex;
1630
+ gap: 2px;
1631
+ opacity: 0;
1632
+ transition: opacity 120ms ease;
1633
+ }
1634
+ .gemboss-assistant-thread:hover .gemboss-assistant-thread-actions,
1635
+ .gemboss-assistant-thread:focus-within .gemboss-assistant-thread-actions {
1636
+ opacity: 1;
1637
+ }
1638
+ .gemboss-assistant-thread-act {
1639
+ display: grid;
1640
+ place-items: center;
1641
+ width: 26px;
1642
+ height: 26px;
1643
+ border: 0;
1644
+ border-radius: 8px;
1645
+ background: transparent;
1646
+ color: var(--gemboss-text-muted);
1647
+ cursor: pointer;
1648
+ transition: background 120ms ease, color 120ms ease;
1649
+ }
1650
+ .gemboss-assistant-thread-act:hover {
1651
+ background: rgba(0,0,0,0.07);
1652
+ color: var(--gemboss-text);
1653
+ }
1654
+ .gemboss-assistant-thread-act.danger:hover {
1655
+ color: var(--gemboss-danger-strong);
1656
+ }
1657
+ .gemboss-assistant-thread-act svg {
1658
+ width: 15px;
1659
+ height: 15px;
1660
+ fill: currentColor;
1661
+ }
1662
+ .gemboss-assistant-thread.editing {
1663
+ background: rgba(0,0,0,0.07);
1664
+ }
1665
+ .gemboss-assistant-thread-edit {
1666
+ display: flex;
1667
+ align-items: center;
1668
+ gap: 2px;
1669
+ width: 100%;
1670
+ padding: 6px 6px;
1671
+ }
1672
+ .gemboss-assistant-thread-edit input {
1673
+ flex: 1 1 auto;
1674
+ min-width: 0;
1675
+ }
1676
+ .gemboss-assistant-layout .gemboss-assistant-page {
1677
+ height: 100%;
1678
+ max-width: 820px;
1679
+ flex: 1 1 auto;
1680
+ min-width: 0;
1681
+ padding-top: 16px;
1682
+ }
1683
+ .gemboss-assistant-layout .gemboss-assistant-page.is-switching {
1684
+ opacity: 0.55;
1685
+ transition: opacity 120ms ease;
1686
+ pointer-events: none;
1687
+ }
1688
+ .gemboss-assistant-rail.is-collapsed {
1689
+ flex-basis: 60px;
1690
+ width: 60px;
1691
+ padding: 16px 8px 12px;
1692
+ }
1693
+ .gemboss-assistant-rail.is-collapsed .gemboss-assistant-rail-top {
1694
+ flex-direction: column;
1695
+ gap: 8px;
1696
+ }
1697
+ .gemboss-assistant-rail.is-collapsed .gemboss-assistant-newchat {
1698
+ justify-content: center;
1699
+ padding: 8px;
1700
+ flex: 0 0 auto;
1701
+ }
1702
+ .gemboss-assistant-rail.is-collapsed .gemboss-assistant-newchat span,
1703
+ .gemboss-assistant-rail.is-collapsed .gemboss-assistant-search,
1704
+ .gemboss-assistant-rail.is-collapsed .gemboss-assistant-railfoot,
1705
+ .gemboss-assistant-rail.is-collapsed .gemboss-assistant-threads {
1706
+ display: none;
1707
+ }
1708
+ @media (max-width: 760px) {
1709
+ .gemboss-assistant-rail {
1710
+ flex-basis: 60px;
1711
+ width: 60px;
1712
+ padding: 16px 8px 12px;
1713
+ }
1714
+ .gemboss-assistant-rail .gemboss-assistant-rail-top {
1715
+ flex-direction: column;
1716
+ gap: 8px;
1717
+ }
1718
+ .gemboss-assistant-newchat {
1719
+ justify-content: center;
1720
+ padding: 8px;
1721
+ flex: 0 0 auto;
1722
+ }
1723
+ .gemboss-assistant-newchat span,
1724
+ .gemboss-assistant-search,
1725
+ .gemboss-assistant-railfoot,
1726
+ .gemboss-assistant-threads,
1727
+ .gemboss-assistant-collapse {
1728
+ display: none;
1729
+ }
1730
+ }
1731
+ .gemboss-assistant-page {
1732
+ height: calc(100dvh - 56px);
1733
+ width: 100%;
1734
+ max-width: 760px;
1735
+ margin: 0 auto;
1736
+ padding: 24px 0 16px;
1737
+ display: flex;
1738
+ flex-direction: column;
1739
+ min-height: 0;
1740
+ }
1741
+ .gemboss-assistant-page > * {
1742
+ flex: 1 1 auto;
1743
+ min-height: 0;
1744
+ }
1745
+ .gemboss-assistant-empty {
1746
+ flex: 1;
1747
+ display: grid;
1748
+ place-items: center;
1749
+ color: var(--gemboss-text-muted);
1750
+ font-size: 14px;
1751
+ }
1752
+ .gemboss-platform-shell {
1753
+ display: flow-root;
1754
+ }
1755
+ .gemboss-platform-main {
1756
+ padding-bottom: calc(24px + var(--gemboss-invite-h, 0px));
1757
+ }
1758
+ .gb-account {
1759
+ position: relative;
1760
+ }
1761
+ .gb-account > summary {
1762
+ list-style: none;
1763
+ }
1764
+ .gb-account > summary::-webkit-details-marker {
1765
+ display: none;
1766
+ }
1767
+ .gb-account-head {
1768
+ padding: 12px 16px;
1769
+ border-bottom: 1px solid var(--gemboss-divider);
1770
+ }
1771
+ .gb-account-name {
1772
+ font-size: 13px;
1773
+ font-weight: 600;
1774
+ color: var(--gemboss-text-strong);
1775
+ }
1776
+ .gb-account-email {
1777
+ font-size: 12px;
1778
+ color: var(--gemboss-text-muted);
1779
+ }
1780
+ .gb-account-role {
1781
+ display: inline-block;
1782
+ margin-top: 4px;
1783
+ padding: 1px 6px;
1784
+ border-radius: 4px;
1785
+ font-size: 11px;
1786
+ font-weight: 600;
1787
+ color: var(--gemboss-active-fg);
1788
+ background: var(--gemboss-active-bg);
1789
+ }
1790
+ .gb-account-form {
1791
+ margin: 0;
1792
+ }
1793
+ .gnav-scroll::-webkit-scrollbar {
1794
+ width: 9px;
1795
+ }
1796
+ .gnav-scroll::-webkit-scrollbar-track {
1797
+ background: transparent;
1798
+ }
1799
+ .gnav-scroll::-webkit-scrollbar-thumb {
1800
+ background: rgba(31,30,29,0.2);
1801
+ border-radius: 6px;
1802
+ border: 2px solid transparent;
1803
+ background-clip: content-box;
1804
+ }
1805
+ @keyframes gemboss-assistant-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
1806
+
1807
+ /* ═══ @gemboss/ui src/app-shell.css ═══ */
1808
+ /* Styles for the parts of the shell the PACKAGE owns — `AppShell`'s three topbar slots,
1809
+ * `RailHeading`, `RailCount`. Everything else (rail, topbar, drawer, tokens, kit) is the admin's
1810
+ * own CSS, exported by apps/admin/scripts/export-ui-css.mjs and concatenated ahead of this file into
1811
+ * `gemboss-ui.css`. The admin does not load this file: it composes the shell itself and has no
1812
+ * slots, headings or pills of this kind.
1813
+ *
1814
+ * Built only from --gemboss-* tokens, so a palette change in gemboss-tokens.css reaches it. */
1815
+
1816
+ /* The base font. The admin's comes from Polaris, which no other surface loads, so without this rule
1817
+ * every surface on gemboss-ui.css rendered in the browser's default serif. Same elements Polaris
1818
+ * sets (html, body, button), same stack (--gemboss-font-sans resolves to Polaris's
1819
+ * --p-font-family-sans fallback). `:where()` keeps it at (0,0,0): any rule a surface writes for
1820
+ * these elements wins, wherever it sits in the cascade. Re-theme with the token, not a body rule.
1821
+ * The admin does not load this file, so its own rendering is untouched. */
1822
+ :where(html, body, button) { font-family: var(--gemboss-font-sans); }
1823
+
1824
+ .gemboss-topbar-start { display: flex; align-items: center; gap: 8px; min-width: 0; }
1825
+ .gemboss-topbar-center { display: flex; justify-content: center; min-width: 0; }
1826
+ .gemboss-topbar-end { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; }
1827
+ @media (max-width: 760px) {
1828
+ /* The topbar is one flex row here: the middle slot takes what is left. */
1829
+ .gemboss-topbar-center { flex: 1 1 auto; justify-content: flex-start; }
1830
+ }
1831
+
1832
+ .gnav-heading {
1833
+ padding: 10px 12px 4px;
1834
+ font-size: var(--gemboss-fs-micro); font-weight: 600; line-height: 1.2;
1835
+ letter-spacing: 0.08em; text-transform: uppercase;
1836
+ color: var(--gemboss-text-muted);
1837
+ }
1838
+ .gnav-scroll > .gnav-heading:first-child { padding-top: 2px; }
1839
+
1840
+ .gnav-count {
1841
+ margin-left: auto; flex: 0 0 auto; min-width: 20px; padding: 0 6px;
1842
+ border-radius: var(--gemboss-radius-full);
1843
+ background: var(--gemboss-hover); color: var(--gemboss-text-muted);
1844
+ font-size: var(--gemboss-fs-micro); font-weight: 700; line-height: 18px;
1845
+ text-align: center; font-variant-numeric: tabular-nums;
1846
+ }
1847
+ .gnav-item.active .gnav-count { background: var(--gemboss-primary); color: var(--gemboss-on-primary); }
1848
+
1849
+ /* ── BrandLockup (./brand.tsx) ─────────────────────────────────────────────────────────────────
1850
+ * The admin's `.gemboss-brand-lockup` box (40px tall, 8px padding, hover wash) around the wordmark's
1851
+ * own proportions. Not that class itself: its ≤460px rule swaps in a background image at an
1852
+ * admin-only path, which every other host would 404. Ink is the logo file's #14151a. */
1853
+ .gb-brand {
1854
+ min-width: 0; height: 40px; margin: 0; padding: 0 8px;
1855
+ border-radius: var(--gemboss-radius-md);
1856
+ display: inline-flex; align-items: center; gap: 4.5px;
1857
+ color: var(--gb-brand-ink, #14151a); text-decoration: none;
1858
+ transition: background-color var(--gemboss-transition-base) ease;
1859
+ }
1860
+ .gb-brand:hover { background: var(--gemboss-hover); }
1861
+ .gb-brand:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: 2px; }
1862
+ .gb-brand-gem { display: block; flex: 0 0 auto; }
1863
+ .gb-brand-name {
1864
+ font-family: Inter, var(--gemboss-font-sans); font-size: 13.3px; font-weight: 700;
1865
+ letter-spacing: -0.13px; line-height: 1; white-space: nowrap;
1866
+ overflow: hidden; text-overflow: ellipsis; min-width: 0;
1867
+ }
1868
+ /* The admin shows only its mark below 460px; so does this. */
1869
+ @media (max-width: 460px) { .gb-brand-name { display: none; } }
1870
+
1871
+ /* ── AccountMenu (./account.tsx): its rules live in shell.css since 29/09/2026, because the admin
1872
+ * renders AccountMenu too now and loads shell.css, not this file. */
1873
+
1874
+ /* ═══ @gemboss/ui src/auth.css ═══ */
1875
+ /* The GemBoss sign-in screen — @gemboss/ui (28/09/2026).
1876
+ *
1877
+ * Moved here VERBATIM from the admin's styled-jsx block (apps/admin/src/app/login/LoginClient.tsx),
1878
+ * which was the original every other sign-in copied by hand: gemcommunity's .auth-wrap,
1879
+ * content-radar's and gemwatcher's login.css, gemcare's inbox, partner-radar. Class names took a
1880
+ * `gb-auth` prefix (gemcare and gempress already have a `.login-card` of their own); the panel's
1881
+ * colours became tokens; nothing else changed. The admin's /login was screenshot at nine sizes in
1882
+ * both themes before and after the move and compared pixel for pixel.
1883
+ *
1884
+ * Rendered by AuthLayout / AuthForm / PasswordField (auth.tsx). What only the admin has — the Google
1885
+ * button, the "or" divider — stays in the admin.
1886
+ */
1887
+
1888
+ /* dvh, not vh: on a phone 100vh is the height with the URL bar HIDDEN, so a centred card sat
1889
+ under the toolbar. The card centres with margin:auto rather than align-items:center — auto
1890
+ margins collapse to 0 when the content is taller than the screen, so the top of the card
1891
+ can never be pushed above the fold (short landscape phone, keyboard open). */
1892
+ /* FLUID, not stepped. Every vertical gap is clamp(compact, N cqh, roomy): N is picked so the roomy
1893
+ value lands at ~800px of height, and the compact floor at ~400px. Between the two the page
1894
+ breathes in proportion to the screen instead of jumping at a breakpoint — the stepped version
1895
+ snapped its card from 379px to 469px between 640 and 680 tall, and still overflowed below
1896
+ 400. Controls never scale: inputs and buttons keep min-height 44px at every size, only the
1897
+ AIR between them gives. cqh is the SHELL's height, which is the visible height — URL bar and
1898
+ on-screen keyboard included (see --vvh above the return). */
1899
+ /* The shell is exactly the visible height and is the size container every rule below measures
1900
+ (cqh, @container gb-auth). It scrolls itself if even the shed layout does not fit (< ~260px). */
1901
+ /* ONE PALETTE, TWO REPOS. Every colour on this screen is a --gb-* token, and gemcommunity's
1902
+ sign-in (app/globals.css, the block scoped to .auth-wrap) declares the SAME names with the
1903
+ SAME values. That is what keeps the two sign-in screens identical: a colour changed in one
1904
+ place is a colour that can be changed in the other, by name, instead of two piles of hex
1905
+ drifting apart — which is what they had been doing (card 380 vs 400, radius 14 vs 12,
1906
+ wordmark 22 vs 24, and this screen with no dark mode at all while that one had it).
1907
+ Dark follows the OS. There is no toggle here, unlike gemcommunity: that app persists a
1908
+ data-theme the admin has no reader for, and a switch that only half-works is worse than
1909
+ none. The [data-theme] hook is still honoured so a person who set it over there, in the
1910
+ same browser, is not thrown back to white when they land here. */
1911
+ .gb-auth {
1912
+ --gb-surface: #ffffff;
1913
+ /* The admin's own near-black, by name — the colour guard asks for the token and it is the
1914
+ right answer here anyway: light mode IS the admin's palette, and only the dark block
1915
+ below has values the admin has no token for. */
1916
+ --gb-ink: var(--gemboss-text-strong, #1b1c1e);
1917
+ --gb-muted: #6d7175;
1918
+ --gb-line: #e1e3e5;
1919
+ --gb-field-line: #c9cccf;
1920
+ --gb-field-bg: #ffffff;
1921
+ --gb-field-hover: #fafafa;
1922
+ --gb-placeholder: #868b94;
1923
+ --gb-accent: #5661f5;
1924
+ --gb-btn: linear-gradient(120deg, #1b1c1e 0%, #000000 100%);
1925
+ --gb-btn-ink: #ffffff;
1926
+ --gb-wordmark: linear-gradient(120deg, #1b1c1e 0%, #000000 100%);
1927
+ --gb-hover: #f7f7f5;
1928
+ --gb-hover-line: #b9bbc0;
1929
+ --gb-err-bg: #fee2e2;
1930
+ --gb-err-ink: #b91c1c;
1931
+ --gb-err-line: transparent;
1932
+ --gb-card-shadow: 0 24px 64px rgba(15, 23, 42, 0.08);
1933
+ --gb-mark-bg: #0b0b0c;
1934
+ /* The brand's own facet geometry as a texture for the panel, rather than a stock diagonal
1935
+ hatch: it is the same path the mark draws, blown up and held at 7% opacity. Inline so it
1936
+ costs no request on the screen that must render before anything else exists. */
1937
+ --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");
1938
+ /* The composition is the gemboss one and stays the gemboss one in dark: ink wash top-left,
1939
+ iris wash bottom-right, flat paper underneath. Only the three colours flip. */
1940
+ --gb-page:
1941
+ radial-gradient(1200px 600px at 20% 10%, rgba(27, 28, 30, 0.05), transparent 60%),
1942
+ radial-gradient(1000px 500px at 80% 90%, rgba(86, 97, 245, 0.06), transparent 60%),
1943
+ #fafafb;
1944
+ --gb-page-flat: #ffffff;
1945
+ /* The brand panel, as tokens (28/09/2026, @gemboss/ui): the admin's is INK with iris used
1946
+ sparingly, because its mark is an outline on ink. A surface whose mark is filled with iris
1947
+ (gemcommunity, content-radar) redefines these five on its own root; nothing else changes. */
1948
+ --gb-panel-bg:
1949
+ radial-gradient(900px 520px at 78% 12%, rgba(86, 97, 245, 0.26), transparent 62%),
1950
+ radial-gradient(700px 480px at 12% 96%, rgba(86, 97, 245, 0.12), transparent 60%),
1951
+ #0b0b0c;
1952
+ --gb-panel-ink: #f4f4f2;
1953
+ --gb-panel-muted: rgba(244, 244, 242, 0.72);
1954
+ --gb-panel-eyebrow: #8a93ff;
1955
+ --gb-panel-facet-o: 0.05;
1956
+ color-scheme: light dark;
1957
+ position: fixed;
1958
+ top: var(--vvt, 0px);
1959
+ left: 0;
1960
+ right: 0;
1961
+ height: 100vh;
1962
+ height: var(--vvh, 100dvh);
1963
+ container: gb-auth / size;
1964
+ overflow-y: auto;
1965
+ /* The recolor swapped the lower wash to iris but missed this one, which was
1966
+ rgba(194,65,12,.08) — retired coral — so the sign-in screen kept a peach cast in the
1967
+ top-left corner. Ink for depth, iris for the single accent: B&W ground, one colour. */
1968
+ background: var(--gb-page);
1969
+ color: var(--gb-ink);
1970
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
1971
+ }
1972
+ /* ONE COLUMN BY DEFAULT. The split is opt-IN, at a size where a second column is a gift
1973
+ rather than a squeeze — and the form column keeps every clamp and shed step it had, so
1974
+ nothing about the phone or the short-landscape case changes. */
1975
+ .gb-auth-split {
1976
+ display: grid;
1977
+ grid-template-columns: 1fr;
1978
+ min-height: 100%;
1979
+ }
1980
+ .gb-auth-stage {
1981
+ box-sizing: border-box;
1982
+ display: flex;
1983
+ min-height: 100%;
1984
+ padding: clamp(12px, 3cqh, 24px) clamp(16px, 4vw, 24px);
1985
+ }
1986
+ .gb-auth-panel-in {
1987
+ position: relative;
1988
+ box-sizing: border-box;
1989
+ width: 100%;
1990
+ max-width: 520px;
1991
+ padding: 48px clamp(32px, 5vw, 64px);
1992
+ }
1993
+ /* The eyebrow is the one place a monospace face earns its keep here: it labels the panel
1994
+ without competing with the headline, and the letterspacing does the work the size would
1995
+ otherwise have to. */
1996
+ .gb-auth-eyebrow {
1997
+ margin: 0 0 20px;
1998
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
1999
+ font-size: 12px;
2000
+ font-weight: 500;
2001
+ letter-spacing: 0.12em;
2002
+ text-transform: uppercase;
2003
+ color: var(--gb-panel-eyebrow);
2004
+ }
2005
+ .gb-auth-panel-h {
2006
+ margin: 0 0 16px;
2007
+ font-size: clamp(28px, 3.2vw, 40px);
2008
+ line-height: 1.15;
2009
+ letter-spacing: -0.02em;
2010
+ font-weight: 700;
2011
+ color: var(--gb-panel-ink);
2012
+ }
2013
+ .gb-auth-panel-p {
2014
+ /* This used to be 0 0 32px, the gap down to the CTA. The button is gone, so that 32px was
2015
+ dangling off the panel's last line with nothing to balance it. */
2016
+ margin: 0;
2017
+ font-size: 15px;
2018
+ line-height: 1.6;
2019
+ /* 0.72 of #f4f4f2 on #0b0b0c measures 8.9:1 — well past AA, and quiet enough that the
2020
+ headline still leads. */
2021
+ color: var(--gb-panel-muted);
2022
+ }
2023
+ /* THE PANEL IS OFF UNTIL EARNED. Hidden by default rather than shown-then-hidden: a panel
2024
+ that paints first and disappears at a breakpoint is a panel that flashes on every phone. */
2025
+ .gb-auth-panel {
2026
+ display: none;
2027
+ }
2028
+ /* Wide AND tall. Width alone is not enough — a 1440x420 window is a wide letterbox, and a
2029
+ column of headline plus button in 420px of height is the crush this whole screen spent its
2030
+ shed ladder avoiding. 900 wide is where 380px of card plus a readable second column stop
2031
+ fighting.
2032
+ 520 tall, not 620 (26/09/2026): a 1366x768 laptop with a bookmarks bar leaves under 620px
2033
+ of viewport, so the most common laptop never saw this column. Its content measures 286px
2034
+ padding included, so 520 still leaves over 200px spare. The form sheds on height whether
2035
+ or not the panel is there, so the panel costs it nothing. Same number as the gemcommunity
2036
+ sign-in (app/globals.css); change both or neither. */
2037
+ @container gb-auth (min-width: 900px) and (min-height: 520px) {
2038
+ .gb-auth-split {
2039
+ grid-template-columns: minmax(420px, 1fr) minmax(0, 1.05fr);
2040
+ }
2041
+ /* The column IS the structure now, so the card stops drawing a second one. A border and
2042
+ a 64px shadow around a form that already sits in its own half of the screen is chrome
2043
+ about chrome. The card keeps every bit of this below 900px, where it is the only thing
2044
+ giving the form an edge. */
2045
+ /* SCOPED UNDER .gb-auth-split ON PURPOSE — (0,2,0) against the base rules' (0,1,0), so this
2046
+ wins wherever it is written. The first version of these two relied on coming LATER in
2047
+ the sheet than the card's own rule, and it did not: a container query carries the specificity
2048
+ of the selectors inside it, nothing more, and the tie went to the rule written first.
2049
+ Measured then: card still 380 wide with its border and 64px shadow intact. */
2050
+ .gb-auth-split .gb-auth-stage {
2051
+ background: var(--gb-surface);
2052
+ }
2053
+ .gb-auth-split .gb-auth-card {
2054
+ border: none;
2055
+ box-shadow: none;
2056
+ max-width: 400px;
2057
+ }
2058
+ .gb-auth-panel {
2059
+ display: flex;
2060
+ align-items: center;
2061
+ position: relative;
2062
+ overflow: hidden;
2063
+ /* INK-DOMINANT, because the gemboss mark is an OUTLINE on ink. Iris appears twice on
2064
+ this panel and both times it is small: the eyebrow and the button. */
2065
+ background: var(--gb-panel-bg);
2066
+ color: var(--gb-panel-ink);
2067
+ }
2068
+ /* The facet lines of the mark, blown up and laid over the ground — the brand's own geometry
2069
+ instead of a stock diagonal hatch. Tiny opacity on purpose: at 1920 it should read as
2070
+ texture you notice second, not a pattern you notice first. */
2071
+ .gb-auth-panel::before {
2072
+ content: "";
2073
+ position: absolute;
2074
+ /* Deliberately OVERSIZED and pushed off two edges, so what lands on the panel is a few
2075
+ facet lines crossing it — texture. Sized to fit, it read as a big grey triangle in the
2076
+ corner: a picture of a logo rather than a ground for one. */
2077
+ inset: -45% -38% auto auto;
2078
+ width: 132%;
2079
+ aspect-ratio: 1;
2080
+ background-image: var(--gb-facet);
2081
+ background-size: contain;
2082
+ background-repeat: no-repeat;
2083
+ opacity: var(--gb-panel-facet-o);
2084
+ pointer-events: none;
2085
+ }
2086
+ }
2087
+ .gb-auth-card {
2088
+ width: 100%;
2089
+ max-width: 380px;
2090
+ margin: auto;
2091
+ background: var(--gb-surface);
2092
+ border: 1px solid var(--gb-line);
2093
+ border-radius: 14px;
2094
+ padding: clamp(18px, 3.5cqh, 28px) 28px;
2095
+ box-shadow: var(--gb-card-shadow);
2096
+ }
2097
+ .gb-auth-header {
2098
+ text-align: center;
2099
+ margin-bottom: clamp(12px, 3cqh, 24px);
2100
+ }
2101
+ /* The header gives up height LAST, with no layout switch: the tagline goes in ONE step under
2102
+ 600px of height, then the mark shrinks from 56px at 560 to 32px at 480.
2103
+ The tagline used to fold CONTINUOUSLY (max-height ramping from 20px at 600 to 0 at 560), which
2104
+ meant every height in that band showed the line cut through the middle: 4px of a 16px line at
2105
+ 320x568, the iPhone SE size, flagged text_clipped by scripts/visual-gate on 28/09/2026. A line of
2106
+ text has no useful partial state, so it is shown whole or not at all (content-radar's copy of this
2107
+ screen had already made the same fix, 27/09/2026). The mark CAN shrink, so it still does.
2108
+ A mark has a floor: shrunk continuously to zero it passed through an 11px smudge that read as
2109
+ a rendering bug (screenshot at 390x480). So under 480 it goes in one step (~38px) and a
2110
+ landscape phone gets the wordmark and the form. The old breakpoint moved the mark beside the
2111
+ name and made the card jump 60px between two heights 40px apart. */
2112
+ .gb-auth-mark {
2113
+ display: inline-flex;
2114
+ margin-bottom: clamp(6px, calc(6px + (100cqh - 480px) * 0.075), 12px);
2115
+ }
2116
+ /* `img` too (29/09/2026): a surface whose CSP forbids inline `style` (gemcare: `style-src 'self'`)
2117
+ cannot use GemMark, which paints its tile with style="fill:var(--gb-mark-bg)". It passes its
2118
+ vendored gemboss-mark.svg as an <img> through `mark`, and that must shrink on a short screen too. */
2119
+ .gb-auth-mark svg,
2120
+ .gb-auth-mark img {
2121
+ width: clamp(32px, calc(32px + (100cqh - 480px) * 0.3), 56px);
2122
+ height: clamp(32px, calc(32px + (100cqh - 480px) * 0.3), 56px);
2123
+ }
2124
+ @container gb-auth (max-height: 479.98px) {
2125
+ .gb-auth-mark {
2126
+ display: none;
2127
+ }
2128
+ }
2129
+ .gb-auth-header h1 {
2130
+ font-size: clamp(19px, 2.75cqh, 22px);
2131
+ font-weight: 700;
2132
+ margin: 0 0 clamp(0px, calc((100cqh - 560px) * 0.1), 4px);
2133
+ background: var(--gb-wordmark);
2134
+ -webkit-background-clip: text;
2135
+ -webkit-text-fill-color: transparent;
2136
+ background-clip: text;
2137
+ }
2138
+ .gb-auth-header p {
2139
+ margin: 0;
2140
+ font-size: 13px;
2141
+ color: var(--gb-muted);
2142
+ }
2143
+ /* Out of sight, not out of the page: the same visually-hidden pattern the header uses under 400,
2144
+ so a screen reader still hears the tagline, as it did when the fold took it to 0px. */
2145
+ @container gb-auth (max-height: 599.98px) {
2146
+ .gb-auth-header p {
2147
+ position: absolute;
2148
+ width: 1px;
2149
+ height: 1px;
2150
+ margin: -1px;
2151
+ overflow: hidden;
2152
+ clip: rect(0 0 0 0);
2153
+ white-space: nowrap;
2154
+ }
2155
+ }
2156
+ .gb-auth-form {
2157
+ display: flex;
2158
+ flex-direction: column;
2159
+ gap: clamp(8px, 1.5cqh, 12px);
2160
+ }
2161
+ .gb-auth-form label {
2162
+ display: flex;
2163
+ flex-direction: column;
2164
+ gap: clamp(4px, 0.75cqh, 6px);
2165
+ font-size: 12px;
2166
+ font-weight: 500;
2167
+ color: var(--gb-ink);
2168
+ }
2169
+ /* 38px → 44px. §9.1 wants a CONTROL at 44 (Apple HIG / WCAG 2.5.5 AAA); measured on the
2170
+ live page 2026-09-09 these were 38, the sign-in button 40, the Google button 42, the
2171
+ password toggle 13 and the Google connect link 15. All five were pre-existing, on the
2172
+ first screen a merchant sees — and now the screen the MCP connect flow sends strangers to,
2173
+ which is why they are fixed here rather than filed. min-height, not height, so a
2174
+ browser that needs more room for the text still gets it. */
2175
+ /* THE FIELD'S SHAPE. In the admin it is the shared `gemboss-field gemboss-field--tall` class
2176
+ (packages/ui/src/gemboss-field.css), which the admin's inputs keep wearing. Every other
2177
+ surface has no such sheet, so `PasswordField`/`AuthField` default to `gb-auth-input`: the same
2178
+ numbers, resolved from that file's Polaris fallbacks — 32px box raised to a 44px floor, 12px
2179
+ side padding, 13px on 20px, radius 8, 1px border; 16px text under 768px (iOS zooms any field
2180
+ under 16px on focus and does not zoom back); height:auto under a coarse pointer. */
2181
+ .gb-auth-input {
2182
+ box-sizing: border-box; height: 32px; min-height: 44px; min-width: 0; padding: 0 12px;
2183
+ font: inherit; font-size: 13px; line-height: 20px;
2184
+ border: 1px solid; border-radius: 8px;
2185
+ }
2186
+ @media (max-width: 47.9975em) { .gb-auth-input { font-size: 16px; } }
2187
+ @media (pointer: coarse) { .gb-auth-input { height: auto; } }
2188
+ /* Colour only, over whichever class drew the shape. */
2189
+ .gb-auth-form input {
2190
+ width: 100%;
2191
+ box-sizing: border-box;
2192
+ background-color: var(--gb-field-bg);
2193
+ border-color: var(--gb-field-line);
2194
+ color: var(--gb-ink);
2195
+ }
2196
+ .gb-auth-form input::placeholder {
2197
+ color: var(--gb-placeholder);
2198
+ }
2199
+ /* HOVER AND FOCUS HAVE TO BE RESTATED HERE, and finding that out is the reason this block
2200
+ exists. gemboss-field.css owns them, at (0,3,0) for the hover; the rule above out-specifies
2201
+ the class — (0,3,1) when it lived in the admin's styled-jsx, (0,1,1) here, still above
2202
+ (0,1,0) — so it took the hover background with it and the field stopped reacting to the
2203
+ pointer (measured then: rest and hover identical in both themes). A token that wins the
2204
+ cascade silently takes the states with it.
2205
+ The focus ring is restated for a different reason: gemboss-field.css paints it Polaris
2206
+ blue (#005bd3), which is not a colour this screen uses anywhere. This screen is B&W plus
2207
+ iris, deliberately outside Polaris, and its own password toggle already rings in near-
2208
+ black. Ink, in both themes, and the same ink gemcommunity's sign-in rings in. */
2209
+ .gb-auth-form input:hover:not(:disabled) {
2210
+ background-color: var(--gb-field-hover);
2211
+ border-color: var(--gb-hover-line);
2212
+ }
2213
+ .gb-auth-form input:focus-visible {
2214
+ outline: 2px solid var(--gb-ink);
2215
+ outline-offset: 1px;
2216
+ border-color: var(--gb-ink);
2217
+ }
2218
+ /* Same shape as a .gb-auth-form label, for the one field whose input had to leave the label.
2219
+ Declared rather than reused so a change to labels cannot silently un-match this one. */
2220
+ .gb-auth-field {
2221
+ display: flex;
2222
+ flex-direction: column;
2223
+ gap: clamp(4px, 0.75cqh, 6px);
2224
+ font-size: 12px;
2225
+ font-weight: 500;
2226
+ color: var(--gb-ink);
2227
+ }
2228
+ .gb-auth-pw {
2229
+ position: relative;
2230
+ display: flex;
2231
+ }
2232
+ .gb-auth-pw input {
2233
+ width: 100%;
2234
+ /* Room for the button. Without it the last characters of a revealed password sit UNDER the
2235
+ eye — which is only visible in the one state the button exists to produce. */
2236
+ padding-right: 48px;
2237
+ }
2238
+ .gb-auth-pw-toggle {
2239
+ /* min-WIDTH as well as height. #2144 gave this 44px of height and stopped, because the
2240
+ script I checked it with only read heights — the repo's own gate reads both and caught
2241
+ it at 41.22px wide, one check away from shipping "fixed" twice. */
2242
+ min-height: 44px;
2243
+ min-width: 44px;
2244
+ position: absolute;
2245
+ right: 0;
2246
+ top: 0;
2247
+ bottom: 0;
2248
+ display: inline-flex;
2249
+ align-items: center;
2250
+ justify-content: center;
2251
+ padding: 0;
2252
+ margin: 0;
2253
+ background: none;
2254
+ border: none;
2255
+ border-radius: 8px;
2256
+ cursor: pointer;
2257
+ color: var(--gb-muted);
2258
+ transition: none;
2259
+ }
2260
+ .gb-auth-pw-toggle:hover {
2261
+ color: var(--gb-ink);
2262
+ }
2263
+ /* focus-visible, not focus: a mouse click on the eye should not leave a ring sitting inside
2264
+ the field. §9.2 wants the state to exist, not to fire for pointer users. */
2265
+ .gb-auth-pw-toggle:focus-visible {
2266
+ outline: 2px solid var(--gemboss-primary, #1b1c1e);
2267
+ outline-offset: -2px;
2268
+ }
2269
+ .gb-auth-error {
2270
+ background: var(--gb-err-bg);
2271
+ border: 1px solid var(--gb-err-line);
2272
+ color: var(--gb-err-ink);
2273
+ padding: 8px 10px;
2274
+ font-size: 13px;
2275
+ border-radius: 6px;
2276
+ }
2277
+ /* Scoped to the submit button BY ITS OWN CLASS, NOT a bare "button". This rule paints a filled
2278
+ black pill, and as ".login-form button" it was specificity 0,1,1 against ".pw-toggle" at 0,1,0 —
2279
+ so the eye INSIDE the password field was painted as a second Sign-in button: black
2280
+ gradient, white icon, and a 6px margin-top that pushed it out of the input. Measured in a
2281
+ production build at all six widths (SS9.1): backgroundImage=GRADIENT,
2282
+ color=rgb(255,255,255), marginTop=6px.
2283
+ Second time in this repo — composer-owns-only-its-own-buttons.test.ts bans the same shape
2284
+ in the chat composer. SS9.3: one visual owner, and the owner names its own element. */
2285
+ .gb-auth-form > .gb-auth-submit {
2286
+ margin-top: clamp(2px, 0.75cqh, 6px);
2287
+ min-height: 44px;
2288
+ font-size: 14px;
2289
+ font-weight: 600;
2290
+ border: none;
2291
+ border-radius: 8px;
2292
+ background: var(--gb-btn);
2293
+ color: var(--gb-btn-ink);
2294
+ cursor: pointer;
2295
+ font-family: inherit;
2296
+ transition: opacity 0.15s ease, transform 0.15s ease;
2297
+ }
2298
+ .gb-auth-form > .gb-auth-submit:hover:not(:disabled) {
2299
+ transform: translateY(-1px);
2300
+ }
2301
+ .gb-auth-form > .gb-auth-submit:disabled {
2302
+ opacity: 0.6;
2303
+ cursor: not-allowed;
2304
+ }
2305
+ /* The signup line. 24px of target, not 44: WCAG 2.5.8 is the threshold that has a standard
2306
+ behind it for a LINK in a line of text, and inline-block is what makes the padding a target
2307
+ at all — on an inline element it paints and measures nothing. */
2308
+ .gb-auth-foot {
2309
+ margin: clamp(10px, 2cqh, 16px) 0 0;
2310
+ font-size: 13px;
2311
+ color: var(--gb-muted);
2312
+ text-align: center;
2313
+ line-height: 1.5;
2314
+ }
2315
+ .gb-auth-foot a {
2316
+ display: inline-block;
2317
+ min-height: 24px;
2318
+ line-height: 24px;
2319
+ padding: 0 2px;
2320
+ color: var(--gb-accent);
2321
+ font-weight: 600;
2322
+ text-decoration: none;
2323
+ }
2324
+ .gb-auth-foot a:hover,
2325
+ .gb-auth-foot a:focus-visible {
2326
+ text-decoration: underline;
2327
+ }
2328
+ .gb-auth-card .gb-auth-help {
2329
+ margin: clamp(8px, 1.75cqh, 14px) 0 0;
2330
+ font-size: 12px;
2331
+ color: var(--gb-muted);
2332
+ text-align: center;
2333
+ line-height: 1.5;
2334
+ }
2335
+ /* The "trouble getting in?" line. min-height 24px: WCAG 2.5.8 for a link. */
2336
+ .gb-auth-card .gb-auth-help a {
2337
+ display: inline-block;
2338
+ min-height: 24px;
2339
+ line-height: 24px;
2340
+ /* The page's link colour, not the grey of the text around it (#3129): the reader of this line
2341
+ is stuck and scanning for a way out. #5661f5 on white is 4.75:1, above the 4.5:1 12px needs. */
2342
+ color: var(--gb-accent);
2343
+ font-weight: 600;
2344
+ text-decoration: none;
2345
+ }
2346
+ .gb-auth-card .gb-auth-help a:hover,
2347
+ .gb-auth-card .gb-auth-help a:focus-visible {
2348
+ text-decoration: underline;
2349
+ }
2350
+ /* PHONE: the card IS the page. A floating card inside a 16px gutter spends ~90px of a 320px
2351
+ screen on border, shadow and double padding for nothing — the pattern every major sign-in
2352
+ page (Google, Apple, Shopify, Stripe) uses below tablet width is a full-bleed white sheet.
2353
+ Inputs go to 16px because iOS Safari zooms the page on focus into any field under 16px,
2354
+ and the zoom does not undo itself after sign-in. Safe-area padding keeps the form out of
2355
+ the notch/home indicator in the desktop-app and standalone webviews. LAST in the sheet on
2356
+ purpose: a short phone matches the max-height block too, and this one must win the padding. */
2357
+ @media (max-width: 480px) {
2358
+ .gb-auth {
2359
+ background: var(--gb-page-flat);
2360
+ }
2361
+ .gb-auth-stage {
2362
+ padding: max(clamp(10px, 3cqh, 20px), env(safe-area-inset-top)) max(16px, env(safe-area-inset-right))
2363
+ max(clamp(10px, 3cqh, 20px), env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
2364
+ }
2365
+ .gb-auth-card {
2366
+ border: none;
2367
+ border-radius: 0;
2368
+ box-shadow: none;
2369
+ padding: 0;
2370
+ }
2371
+ /* (no font-size here: the field's shape class — gemboss-field in the admin, gb-auth-input
2372
+ elsewhere — reads 16px under this same breakpoint, which is what stops iOS zooming.) */
2373
+ }
2374
+ /* ALWAYS INSIDE THE VIEWPORT. Once the clamps above are at their floors, the only height left
2375
+ is in what can go. Shed in order of what a person trying to sign in needs least, each step
2376
+ at the height where the one before stopped fitting (measured, 390 and 1280 wide):
2377
+ < 440 the "or" divider — the Google button is self-explanatory without it
2378
+ < 400 the GemBoss wordmark — kept for screen readers, it is still the page heading
2379
+ < 360 the signup and help lines, and the desktop card trims its padding
2380
+ Never shed: email, password, Sign in, Google. Those are the page. The signup line survives
2381
+ to 360 rather than going with the wordmark at 400: a stranger with no account is exactly
2382
+ who this screen strands, and below 360 there is a scrollbar to reach it with. LAST in the sheet so
2383
+ it wins over the phone block above. */
2384
+ @container gb-auth (max-height: 399.98px) {
2385
+ .gb-auth-header {
2386
+ position: absolute;
2387
+ width: 1px;
2388
+ height: 1px;
2389
+ margin: -1px;
2390
+ overflow: hidden;
2391
+ clip: rect(0 0 0 0);
2392
+ white-space: nowrap;
2393
+ }
2394
+ }
2395
+ @container gb-auth (max-height: 359.98px) {
2396
+ .gb-auth-card .gb-auth-help,
2397
+ .gb-auth-foot {
2398
+ display: none;
2399
+ }
2400
+ .gb-auth-stage {
2401
+ padding-top: max(8px, env(safe-area-inset-top));
2402
+ padding-bottom: max(8px, env(safe-area-inset-bottom));
2403
+ }
2404
+ }
2405
+ /* The card only has padding to trim where it IS a card; on a phone it is already 0. */
2406
+ @container gb-auth (max-height: 359.98px) and (min-width: 481px) {
2407
+ .gb-auth-card {
2408
+ padding-top: 14px;
2409
+ padding-bottom: 14px;
2410
+ }
2411
+ }
2412
+ /* DARK — a pure token flip. Not one rule above changes; every colour on this screen reads a
2413
+ --gb-* token, so the whole screen inverts by redefining eighteen values here. The same
2414
+ eighteen, with the same values, are what gemcommunity's .auth-wrap block declares.
2415
+ Two selectors on purpose: the OS ask, ignored when a data-theme of light is set, and an
2416
+ explicit data-theme of dark. LAST in the sheet so it beats the phone block, which sets a
2417
+ flat page colour of its own.
2418
+ When this sheet lived in the admin's styled-jsx, the root part needed :global() — styled-jsx
2419
+ glued its scope class onto ":root", no html element carries one, and the whole dark block
2420
+ matched nothing (measured 23/09/2026: --gb-surface still #ffffff on a dark-OS browser, with
2421
+ typecheck and the text-reading tests all green). As a plain stylesheet it needs nothing; the
2422
+ dark screenshots taken for the move are what show it still applies.
2423
+ WHAT IT IS NOT: a dark mode for the admin. Nothing behind /login has one yet, so signing in
2424
+ moves a dark-OS person to a light app. That is the same handoff gemcommunity has shipped
2425
+ since its auth skin landed, and the alternative was leaving this screen the one white
2426
+ rectangle on a dark machine. */
2427
+ @media (prefers-color-scheme: dark) {
2428
+ :root:not([data-theme="light"]) .gb-auth {
2429
+ --gb-surface: #141517;
2430
+ --gb-ink: #f4f4f3;
2431
+ --gb-muted: #969c97;
2432
+ --gb-line: rgba(255, 255, 255, 0.11);
2433
+ --gb-field-line: rgba(255, 255, 255, 0.17);
2434
+ --gb-field-bg: #1b1c1f;
2435
+ --gb-field-hover: #212226;
2436
+ --gb-placeholder: #7f838c;
2437
+ --gb-accent: #8a93ff;
2438
+ --gb-btn: linear-gradient(120deg, #f4f4f3 0%, #dfe0e4 100%);
2439
+ --gb-btn-ink: #0b0b0c;
2440
+ --gb-wordmark: linear-gradient(120deg, #f4f4f3 0%, #c9cbd2 100%);
2441
+ --gb-hover: rgba(255, 255, 255, 0.06);
2442
+ --gb-hover-line: rgba(255, 255, 255, 0.28);
2443
+ --gb-err-bg: rgba(215, 44, 13, 0.16);
2444
+ --gb-err-ink: #ff9d88;
2445
+ --gb-err-line: rgba(245, 120, 95, 0.4);
2446
+ --gb-card-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
2447
+ --gb-mark-bg: #202127;
2448
+ --gb-page:
2449
+ radial-gradient(1200px 600px at 20% 10%, rgba(255, 255, 255, 0.05), transparent 60%),
2450
+ radial-gradient(1000px 500px at 80% 90%, rgba(138, 147, 255, 0.1), transparent 60%),
2451
+ #0b0b0c;
2452
+ --gb-page-flat: #0b0b0c;
2453
+ }
2454
+ }
2455
+ :root[data-theme="dark"] .gb-auth {
2456
+ --gb-surface: #141517;
2457
+ --gb-ink: #f4f4f3;
2458
+ --gb-muted: #969c97;
2459
+ --gb-line: rgba(255, 255, 255, 0.11);
2460
+ --gb-field-line: rgba(255, 255, 255, 0.17);
2461
+ --gb-field-bg: #1b1c1f;
2462
+ --gb-field-hover: #212226;
2463
+ --gb-placeholder: #7f838c;
2464
+ --gb-accent: #8a93ff;
2465
+ --gb-btn: linear-gradient(120deg, #f4f4f3 0%, #dfe0e4 100%);
2466
+ --gb-btn-ink: #0b0b0c;
2467
+ --gb-wordmark: linear-gradient(120deg, #f4f4f3 0%, #c9cbd2 100%);
2468
+ --gb-hover: rgba(255, 255, 255, 0.06);
2469
+ --gb-hover-line: rgba(255, 255, 255, 0.28);
2470
+ --gb-err-bg: rgba(215, 44, 13, 0.16);
2471
+ --gb-err-ink: #ff9d88;
2472
+ --gb-err-line: rgba(245, 120, 95, 0.4);
2473
+ --gb-card-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
2474
+ --gb-mark-bg: #202127;
2475
+ --gb-page:
2476
+ radial-gradient(1200px 600px at 20% 10%, rgba(255, 255, 255, 0.05), transparent 60%),
2477
+ radial-gradient(1000px 500px at 80% 90%, rgba(138, 147, 255, 0.1), transparent 60%),
2478
+ #0b0b0c;
2479
+ --gb-page-flat: #0b0b0c;
2480
+ }
2481
+
2482
+ /* ═══ @gemboss/ui src/assistant.css ═══ */
2483
+ /* The assistant's popup / status bar / full-screen host — @gemboss/ui (28/09/2026).
2484
+ *
2485
+ * Moved from gemboss apps/admin/src/components/assistant/AssistantDockHost.module.css, where it was
2486
+ * written for the admin's assistant; gemwatcher's and content-radar's assistants now wear the same
2487
+ * host. Class names took a `gb-dock` prefix instead of CSS-module hashes (a consumer without CSS
2488
+ * modules must be able to load it); every value is unchanged. The geometry assumes the GemBoss shell
2489
+ * (@gemboss/ui AppShell): 56px topbar, 240px rail, drawer below 760px.
2490
+ *
2491
+ * Rendered by AssistantHost (assistant/host.tsx). State and the rules for when it shows are
2492
+ * assistant/dock.ts + assistant/provider.tsx.
2493
+ */
2494
+
2495
+
2496
+ .gb-dock {
2497
+ position: fixed;
2498
+ z-index: 320; /* above the mobile tab bar (300), below the page editor (400) */
2499
+ right: 24px;
2500
+ bottom: 0;
2501
+ /* MEASURED 2026-09-12, not assumed: the rail is 240px at every width above 760 (the shell's own
2502
+ `margin-left: 240px`), and there is NO bottom tab bar — `.gemboss-mobile-nav` is not in the DOM
2503
+ at 320 or 390. An earlier pass here wrote 72px and 56px from a stale comment in AppShell and
2504
+ the popup covered a third of the rail, swallowing the menu clicks this feature exists to free.
2505
+ 240 (rail) + 24 (right gutter) + 24 (left gap) = 288. */
2506
+ /* 460 is the app's own modal width (`.ui-modal`), so the assistant reads as part of the same
2507
+ family instead of a wider one-off. 600 was a guess; this one already exists. */
2508
+ width: min(460px, calc(100vw - 288px));
2509
+ display: flex;
2510
+ flex-direction: column;
2511
+ min-height: 0;
2512
+ /* Frosted, the way the settings dialog reads — translucent surface over a small blur, so the page
2513
+ underneath shows through as texture rather than detail.
2514
+ NOT a full-screen scrim, though that is what settings uses. A scrim would block the page, and a
2515
+ popup you cannot work behind is just the assistant page in a smaller box: the whole point of
2516
+ this surface is that the screen under it stays usable. */
2517
+ /* 93%, AND THE 5 POINTS THIS GAINED ON 25/09/2026 ARE A CONTRAST MEASUREMENT.
2518
+ A translucent surface is `a·white + (1−a)·backdrop`, so the darkest it can ever go is `a·white`.
2519
+ At 88% that floor was #E0E0E0, and `--gemboss-text-muted` (#686b71) measures 4.05:1 there —
2520
+ UNDER AA. That is not a hypothetical corner of this component: `.header` and `.miniBar` both
2521
+ declare `background: transparent` on purpose (see the note beside them), so `.headerStatus`
2522
+ 12px, `.miniStatus` 12px and `.booting` 14px all sit DIRECTLY on this surface in that colour.
2523
+ 93% puts the floor at #EDEDED and muted at 4.56:1. Chosen with, and kept equal to,
2524
+ `.gemboss-popover-surface` in app/gemboss-shell.css — `one-popover-surface-four-wearers.test.ts`
2525
+ fails if the two numbers ever stop agreeing. Change them THERE too.
2526
+
2527
+ TWO BACKGROUNDS, and the first is not decoration. `color-mix` and `backdrop-filter` ship on
2528
+ DIFFERENT schedules — Firefox 103–112 and Safari 15.4–16.1 have the blur and not the mix — and
2529
+ a declaration the parser rejects is DROPPED, not defaulted. Without the plain line, the dock had
2530
+ NO background at all in those browsers: the page running straight through the chat. It shipped
2531
+ that way on 12/09/2026 and nobody could see it from here. NOT a custom property: `background:
2532
+ var(--some-mix-token)` would fail at computed-value time instead, which falls back to `unset`
2533
+ rather than to the line above — worse than the bug it would be trying to share. */
2534
+ background: var(--gemboss-surface);
2535
+ background: color-mix(in srgb, var(--gemboss-surface) 93%, transparent);
2536
+ -webkit-backdrop-filter: blur(14px) saturate(1.4);
2537
+ backdrop-filter: blur(14px) saturate(1.4);
2538
+ border: 1px solid var(--gemboss-divider);
2539
+ border-bottom: 0;
2540
+ border-radius: 12px 12px 0 0;
2541
+ box-shadow: 0 -2px 6px rgba(15, 23, 42, 0.05), 0 24px 64px rgba(15, 23, 42, 0.18);
2542
+ overflow: hidden;
2543
+
2544
+ /* ── Arriving ────────────────────────────────────────────────────────────────────────────────
2545
+ Short and plain: 140ms, ease-out, a small rise and a fade. Long enough to read as "it came
2546
+ from somewhere", short enough that nobody waits for it — a popup a seller opens twenty times
2547
+ a day must not perform.
2548
+
2549
+ TRANSFORM AND OPACITY ONLY. Height and horizontal offset are set from a pointer, and a
2550
+ transition on those means the box trails the finger instead of following it. Those two stay
2551
+ instant by construction, not by luck. */
2552
+ transition: transform 140ms cubic-bezier(0.2, 0.8, 0.3, 1), opacity 140ms ease-out;
2553
+ }
2554
+ /* The entry itself. `@starting-style` is what lets an element animate the frame it appears in
2555
+ (Chrome 117+/Safari 17.5+; the desktop app is Chromium 132). Anywhere older simply appears —
2556
+ degrading to instant is the right failure for chrome nobody asked to watch. */
2557
+ @starting-style {
2558
+ .gb-dock-dock, .gb-dock-mini { transform: translateY(10px); opacity: 0; }
2559
+ }
2560
+ /* Nothing animates while a pointer is setting the numbers — see `dragging` in the host. */
2561
+ .gb-dock-dragging, .gb-dock-dragging * { transition: none !important; }
2562
+ /* Where `backdrop-filter` is missing there is no blur to smear the page, and 93% opaque over sharp
2563
+ body text is a chat you read through. Back to a solid surface — what this dock wore before it was
2564
+ frosted. Same reasoning, same shape, as `.gemboss-popover-surface`. */
2565
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
2566
+ .gb-dock { background: var(--gemboss-surface); }
2567
+ }
2568
+ @media (prefers-reduced-motion: reduce) {
2569
+ .gb-dock { transition: none; }
2570
+ }
2571
+
2572
+ /* The floating popup IS the base geometry above; this class exists so the DOM names the state —
2573
+ `styles[mode]` was `undefined` for it, which is invisible in a screenshot and unnameable in a test. */
2574
+ .gb-dock-dock { }
2575
+
2576
+ /* Still mounted — that is what keeps a run alive — just not on screen. */
2577
+ .gb-dock-hidden { display: none; }
2578
+
2579
+ /* ── Full: exactly the area `main` occupies, so the dedicated page looks untouched ── */
2580
+ .gb-dock-full {
2581
+ /* BELOW `.gemboss-route-shell` (z-index 5), which covers this exact rectangle while the next route
2582
+ loads. At the popup's 320 the assistant stayed painted over the holding screen, so leaving this
2583
+ page looked like nothing happened — and the screen's click-catcher was defeated, so clicks
2584
+ during the wait landed on chat controls. Fixed position still puts it over `main`'s content. */
2585
+ z-index: 4;
2586
+ top: 56px;
2587
+ left: 240px; /* the rail, measured — see the note on .host */
2588
+ right: 0;
2589
+ bottom: 0;
2590
+ width: auto;
2591
+ border: 0;
2592
+ border-radius: 0;
2593
+ box-shadow: none;
2594
+ background: var(--gemboss-app-bg);
2595
+ }
2596
+
2597
+ /* ── Full: the one control full screen needs — the way back to the popup ──────────────────────
2598
+ Floated over the chat's own header, whose right half is empty, rather than given a bar of its
2599
+ own: a whole strip of chrome for one button would push the conversation down on every screen. */
2600
+ .gb-dock-fullchrome {
2601
+ position: absolute;
2602
+ top: 10px;
2603
+ right: 16px;
2604
+ z-index: 1;
2605
+ display: flex;
2606
+ gap: 8px;
2607
+ }
2608
+ .gb-dock-shrink {
2609
+ display: inline-flex;
2610
+ align-items: center;
2611
+ gap: 6px;
2612
+ min-height: 44px; /* §9.1 control tier */
2613
+ padding: 0 12px;
2614
+ border: 1px solid var(--gemboss-divider);
2615
+ border-radius: var(--gemboss-radius-full);
2616
+ background: var(--gemboss-surface);
2617
+ color: var(--gemboss-text-muted);
2618
+ font: inherit;
2619
+ font-size: var(--gemboss-fs-caption);
2620
+ cursor: pointer;
2621
+ white-space: nowrap;
2622
+ }
2623
+ .gb-dock-shrink:hover { background: var(--gemboss-hover); color: var(--gemboss-text); }
2624
+ .gb-dock-shrink:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: 2px; }
2625
+ .gb-dock-shrink svg { width: 16px; height: 16px; flex: 0 0 auto; }
2626
+ /* On a phone the label would crowd the chat header; the icon carries it there. */
2627
+ @media (max-width: 560px) {
2628
+ .gb-dock-shrink span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
2629
+ .gb-dock-shrink { padding: 0; width: 44px; justify-content: center; }
2630
+ /* Except the way to a person: a head-and-shoulders icon alone does not say "a human answers". */
2631
+ .gb-dock-shrink-keep span { position: static; width: auto; height: auto; overflow: visible; clip: auto; }
2632
+ .gb-dock-shrink-keep { padding: 0 12px; width: auto; }
2633
+ /* Two controls do not fit in the empty half of the chat header at phone width — floated, they sat
2634
+ on the title ("Ask th…", measured at 320 and 390). In the flow they cost one short strip here,
2635
+ and only here. */
2636
+ .gb-dock-fullchrome { position: static; justify-content: flex-end; padding: 8px 12px 0; }
2637
+ }
2638
+
2639
+ /* ── Mini: a status bar, not a window ── */
2640
+ .gb-dock-mini {
2641
+ height: auto;
2642
+ width: min(320px, calc(100vw - 288px));
2643
+ -webkit-backdrop-filter: blur(14px) saturate(1.4); /* Safari; the host above carries the rest */
2644
+ backdrop-filter: blur(14px) saturate(1.4);
2645
+ border-radius: 12px 12px 0 0;
2646
+ }
2647
+ .gb-dock-mini .gb-dock-body { display: none; }
2648
+ .gb-dock-minirow { display: flex; align-items: center; padding-right: 4px; }
2649
+ .gb-dock-minirow .gb-dock-minibar { flex: 1 1 auto; min-width: 0; }
2650
+ /* `width: 44px` is a BASIS, not a floor: the default `flex-shrink: 1` let this button give ground to
2651
+ the row beside it, so the × measured under 44 at all six widths while the rule above read as done.
2652
+ MEASURED 2026-09-12 — the same trap as every other "the declaration is there" bug today. */
2653
+ .gb-dock-minirow .gb-dock-icon { flex: 0 0 auto; }
2654
+
2655
+ .gb-dock-minibar {
2656
+ display: flex;
2657
+ align-items: center;
2658
+ gap: 8px;
2659
+ width: 100%;
2660
+ min-height: 48px;
2661
+ padding: 0 12px;
2662
+ border: 0;
2663
+ background: transparent;
2664
+ font: inherit;
2665
+ color: var(--gemboss-text-strong);
2666
+ cursor: pointer;
2667
+ text-align: left;
2668
+ }
2669
+ .gb-dock-minibar:hover { background: var(--gemboss-hover); }
2670
+ .gb-dock-minibar strong { font-size: 13px; font-weight: 600; flex: 0 0 auto; }
2671
+ .gb-dock-ministatus {
2672
+ flex: 1 1 auto;
2673
+ min-width: 0;
2674
+ font-size: 12px;
2675
+ color: var(--gemboss-text-muted);
2676
+ overflow: hidden;
2677
+ text-overflow: ellipsis;
2678
+ white-space: nowrap;
2679
+ }
2680
+ .gb-dock-minichevron { font-size: 10px; color: var(--gemboss-text-muted); }
2681
+
2682
+ /* ── Resize grip (dock only) ── */
2683
+ .gb-dock-grip {
2684
+ /* OVERLAID on the header's top edge, not stacked above it. As its own 14px row it pushed the
2685
+ title down and the header read as top-heavy — Chris: "header hơi lệch do padding top hơi nhiều".
2686
+ The drag area is unchanged; it simply no longer costs vertical space. */
2687
+ position: absolute;
2688
+ top: 0;
2689
+ left: 0;
2690
+ right: 0;
2691
+ height: 14px;
2692
+ z-index: 2;
2693
+ display: grid;
2694
+ place-items: center;
2695
+ cursor: ns-resize;
2696
+ touch-action: none;
2697
+ background: transparent;
2698
+ }
2699
+ .gb-dock-grip > span {
2700
+ display: block;
2701
+ width: 44px; /* a mouse target wide enough to find without aiming */
2702
+ height: 4px;
2703
+ border-radius: 2px;
2704
+ background: var(--gemboss-divider);
2705
+ }
2706
+ .gb-dock-grip:hover > span, .gb-dock-grip:focus-visible > span { background: var(--gemboss-text-muted); }
2707
+ .gb-dock-grip:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; }
2708
+
2709
+ /* ── Dock header ── */
2710
+ .gb-dock-header {
2711
+ flex: 0 0 auto;
2712
+ cursor: grab; /* the strip itself drags the popup sideways */
2713
+ touch-action: none;
2714
+ user-select: none;
2715
+ display: flex;
2716
+ align-items: center;
2717
+ gap: 8px;
2718
+ padding: 0 4px 0 16px;
2719
+ min-height: 48px;
2720
+ border-bottom: 1px solid var(--gemboss-divider);
2721
+ background: transparent; /* the frosted host shows through; a second fill would double it */
2722
+ }
2723
+ .gb-dock-title {
2724
+ font-size: 13px;
2725
+ font-weight: 600;
2726
+ color: var(--gemboss-text-strong);
2727
+ white-space: nowrap;
2728
+ /* Let the NAME absorb a squeeze. Adding the + button pushed the header to 290px inside a 277px
2729
+ popup at 320 — a nowrap title with no min-width cannot give ground, so the row overflowed
2730
+ instead. Measured 2026-09-12. */
2731
+ flex: 0 1 auto;
2732
+ min-width: 0;
2733
+ overflow: hidden;
2734
+ text-overflow: ellipsis;
2735
+ }
2736
+ .gb-dock-status {
2737
+ flex: 1 1 auto;
2738
+ min-width: 0;
2739
+ font-size: 12px;
2740
+ color: var(--gemboss-text-muted);
2741
+ overflow: hidden;
2742
+ text-overflow: ellipsis;
2743
+ white-space: nowrap;
2744
+ }
2745
+ .gb-dock-header:active { cursor: grabbing; }
2746
+ .gb-dock-actions { display: flex; align-items: center; gap: 2px; cursor: default; }
2747
+ /* One ACTION, then the three WINDOW controls — ordered by how much each changes the window: bigger,
2748
+ smaller, gone. The gap is what says they are two different kinds of button; without it four icons
2749
+ in a row read as one undifferentiated strip, which is what "các icon đang không đồng đều" was
2750
+ describing. The 📌 that used to sit in the middle of them is gone entirely — see lib/assistant-dock. */
2751
+ .gb-dock-action { margin-right: 8px; }
2752
+
2753
+ .gb-dock-icon {
2754
+ /* 44px control target — Apple HIG / WCAG 2.5.5. The icon inside stays 18px. */
2755
+ width: 44px;
2756
+ height: 44px;
2757
+ display: grid;
2758
+ place-items: center;
2759
+ border: 0;
2760
+ border-radius: 8px;
2761
+ background: transparent;
2762
+ color: var(--gemboss-text-muted);
2763
+ cursor: pointer;
2764
+ padding: 0;
2765
+ }
2766
+ .gb-dock-icon:hover { background: var(--gemboss-hover); color: var(--gemboss-text-strong); }
2767
+ .gb-dock-icon:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; }
2768
+ .gb-dock-icon svg { width: 18px; height: 18px; }
2769
+ .gb-dock-icon-on { color: var(--gemboss-active-fg); background: var(--gemboss-active-bg); }
2770
+
2771
+ .gb-dock-dot {
2772
+ flex: 0 0 auto;
2773
+ width: 8px;
2774
+ height: 8px;
2775
+ border-radius: 50%;
2776
+ background: var(--gemboss-divider);
2777
+ }
2778
+ .gb-dock-dot-busy { background: var(--gemboss-primary); animation: gb-dock-pulse 1.4s ease-in-out infinite; }
2779
+ .gb-dock-dot-unread { background: var(--gemboss-success-strong); }
2780
+ @keyframes gb-dock-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
2781
+ @media (prefers-reduced-motion: reduce) { .gb-dock-dot-busy { animation: none; } }
2782
+
2783
+ /* ── Tabs: the assistant, or a person on the Gem team (dock only) ──────────────────────────────
2784
+ Their own row under the header rather than inside it: at 320px the header already spends 176px
2785
+ on four 44px controls, and two text tabs do not fit in what is left. */
2786
+ .gb-dock-tabs {
2787
+ flex: 0 0 auto;
2788
+ display: flex;
2789
+ gap: 4px;
2790
+ padding: 0 8px;
2791
+ border-bottom: 1px solid var(--gemboss-divider);
2792
+ }
2793
+ .gb-dock-tab {
2794
+ position: relative;
2795
+ display: inline-flex;
2796
+ align-items: center;
2797
+ gap: 6px;
2798
+ min-height: 44px; /* §9.1 control tier */
2799
+ padding: 0 12px;
2800
+ border: 0;
2801
+ border-bottom: 2px solid transparent;
2802
+ background: transparent;
2803
+ color: var(--gemboss-text-muted);
2804
+ font: inherit;
2805
+ font-size: 13px;
2806
+ cursor: pointer;
2807
+ white-space: nowrap;
2808
+ }
2809
+ .gb-dock-tab:hover { color: var(--gemboss-text-strong); }
2810
+ .gb-dock-tab[aria-selected="true"] { color: var(--gemboss-text-strong); font-weight: 600; border-bottom-color: var(--gemboss-primary); }
2811
+ .gb-dock-tab:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: -2px; }
2812
+ .gb-dock-badge {
2813
+ min-width: 18px;
2814
+ padding: 0 6px;
2815
+ border-radius: var(--gemboss-radius-full);
2816
+ background: var(--gemboss-primary);
2817
+ color: #fff;
2818
+ font-size: 11px;
2819
+ font-weight: 700;
2820
+ line-height: 18px;
2821
+ text-align: center;
2822
+ }
2823
+
2824
+ /* ── Body: the assistant itself ── */
2825
+ .gb-dock-body {
2826
+ flex: 1 1 auto;
2827
+ min-height: 0;
2828
+ display: flex;
2829
+ min-width: 0;
2830
+ }
2831
+ .gb-dock-body > * { flex: 1 1 auto; min-width: 0; min-height: 0; }
2832
+ /* Each tab's pane. A hidden pane stays MOUNTED — `display:none`, never unmount (see the host). */
2833
+ .gb-dock-pane { display: flex; min-width: 0; min-height: 0; }
2834
+ .gb-dock-pane > * { flex: 1 1 auto; min-width: 0; min-height: 0; }
2835
+ /* The package's own workspace (AssistantWorkspaceLayout) sizes itself to the viewport on its own page;
2836
+ inside the host it fills the host instead. Moved here from the admin's assistant-dock-admin.css
2837
+ (28/09/2026): every surface that puts the layout in the host needs it, or the layout keeps
2838
+ `height: calc(100dvh - 56px)` inside a popup a few hundred pixels tall. */
2839
+ .gb-dock-body .gemboss-assistant-layout,
2840
+ .gb-dock-body .gemboss-assistant-page { height: auto; min-height: 0; }
2841
+ .gb-dock-pane[hidden] { display: none; }
2842
+
2843
+ .gb-dock-booting {
2844
+ flex: 1 1 auto;
2845
+ display: grid;
2846
+ place-items: center;
2847
+ padding: 24px;
2848
+ color: var(--gemboss-text-muted);
2849
+ font-size: 14px;
2850
+ }
2851
+
2852
+ .gb-dock-sr {
2853
+ position: absolute;
2854
+ width: 1px; height: 1px;
2855
+ padding: 0; margin: -1px;
2856
+ overflow: hidden;
2857
+ clip: rect(0 0 0 0);
2858
+ white-space: nowrap;
2859
+ border: 0;
2860
+ }
2861
+
2862
+ /* ── Phone: the popup spans the screen above the bottom tab bar; full mode loses the rail gutter ── */
2863
+ /* ── Narrow: five 44px controls + a title do not fit ────────────────────────────────────────
2864
+ MEASURED 2026-09-12 at 320px: the header row wanted 342px inside a 291px popup. 5 × 44 = 220
2865
+ leaves 71px for a title and a status line. So the two controls that are about BROWSING WHILE IT
2866
+ RUNS — the pin, and the word for what the dot already says — stand down; new chat, full screen,
2867
+ minimize and close stay. Dropping a control beats letting the row overflow off the edge. */
2868
+ @media (max-width: 420px) {
2869
+ .gb-dock-status { display: none; }
2870
+ }
2871
+
2872
+ /* ── Phone: the rail is an off-canvas drawer, so both geometries reclaim the full width ── */
2873
+ @media (max-width: 760px) {
2874
+ .gb-dock {
2875
+ right: 8px;
2876
+ left: 8px;
2877
+ width: auto;
2878
+ padding-bottom: env(safe-area-inset-bottom);
2879
+ border-radius: 12px 12px 0 0;
2880
+ }
2881
+ .gb-dock-mini { width: auto; }
2882
+ .gb-dock-full {
2883
+ left: 0;
2884
+ right: 0;
2885
+ top: 56px;
2886
+ bottom: 0;
2887
+ border-radius: 0;
2888
+ }
2889
+ }