@gemboss/ui 0.8.1 → 0.12.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 CHANGED
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  * GemBoss admin design system — GENERATED — do not edit; regenerate from gemboss:
3
3
  * node apps/admin/scripts/export-ui-css.mjs --out <path> (npm run export:ui-css)
4
- * gemboss commit: 22b5d0daf43ab644a6a9bffef08352a93cc5ca1c
5
- * sources sha256: 7d836f2fc37705cee220b28af995f1e1006b2cb1a41e0ac162c29fa9276f70d3
4
+ * gemboss commit: e4c3bf3203ec143af9abdade7f6c686be85f7de0
5
+ * sources sha256: 9b0060f874885bf4ba4ab9dc6448bc06ae386328bb9bfebcf23bfc6c2671561d
6
6
  * contents: 1 allow-listed rule from apps/admin/src/app/globals.css + packages/ui/src/gemboss-tokens.css
7
7
  * (verbatim) + packages/ui/src/ui.css (verbatim) + packages/ui/src/gemboss-field.css (verbatim)
8
8
  * + 181 rules from packages/ui/src/shell.css (the shared shell)
@@ -96,7 +96,7 @@
96
96
  and reading as "the font keeps shrinking". New rule: pick a step here, never a
97
97
  raw px. Floor is 12px (--fs-caption); 11px (--fs-micro) is reserved for bold
98
98
  micro-badges/count-pills ONLY — nothing below that. Body default is 14px, one
99
- step up from Polaris bodyMd (13px; bodySm is 12) which the app had been leaning on for prose. The kit (ui.css) reads --ui-fs-body, 13px, because it sits beside Polaris components. */
99
+ step up from Polaris bodyMd (13px; bodySm is 12) which the app had been leaning on for prose. The kit (ui.css, kit-plain.css) reads --ui-fs-body, 13px, because it sits beside Polaris components; since 0.12.0 it reads none of body/h1-h3 here (README "Admin density"). */
100
100
  --gemboss-fs-micro: 11px; /* bold count-pills / status chips only */
101
101
  --gemboss-fs-caption: 12px; /* metadata, timestamps, secondary labels */
102
102
  --gemboss-fs-body: 14px; /* default body / reading text */
@@ -181,7 +181,7 @@
181
181
  }
182
182
 
183
183
  /* ═══ ui.css ═══ */
184
- /* ════════════════════════════════════════════════════════════════════════════
184
+ /* ══
185
185
  Tool/App UI kit — the shared primitive layer for the internal operator surfaces
186
186
  (admin console, seller app chrome, theme-editor). ONE semantic token spine
187
187
  (`--ui-*`) that every primitive reads, plus the primitive CSS itself.
@@ -194,7 +194,7 @@
194
194
  The `--ui-*` layer is SEMANTIC and resolved per surface: components only read
195
195
  `--ui-*`; each surface maps those to its own tokens (admin → `--adm-*` below),
196
196
  so one <Button> looks native everywhere without hard-coding a palette.
197
- ════════════════════════════════════════════════════════════════════════════ */
197
+ ══ */
198
198
 
199
199
  :root {
200
200
  --ui-surface: #ffffff;
@@ -229,10 +229,15 @@
229
229
  --ui-space-2: 8px;
230
230
  --ui-space-3: 12px;
231
231
  --ui-space-4: 16px;
232
- /* ONE control height under a mouse: Polaris' TextField/Select (32px). Field, select, button and
233
- segment share it, so a toolbar row is one height (was 32 / 36 / 34 / 36). Under a thumb every
234
- control is 44px (the coarse block below; kit-plain.css adds phone WIDTH for non-admin surfaces). */
232
+ /* Polaris' two heights under a mouse (0.12.0): a FIELD (gemboss-field, ui-select) is TextField/Select, 32px; a BUTTON
233
+ (ui-btn, ui-seg, ui-tabs) is Button, 28px. A toolbar row is 32 + 28, as in the admin. Under a thumb every control is
234
+ 44px (the coarse block below; kit-plain.css adds phone WIDTH for non-admin surfaces). */
235
235
  --ui-control-h: 32px;
236
+ --ui-btn-h: 28px;
237
+ /* Polaris Card (13.9.5): no grey frame of its own, --p-shadow-100 under it. The edge is ONE 1px line in the surface's
238
+ --ui-border (Polaris paints a bevel inset on a ::before; a border cannot be covered by a full-bleed child, and it
239
+ follows a surface's dark --ui-border, which a light bevel would not). */
240
+ --ui-card-shadow: 0 1px 0 rgba(26,26,26,.07);
236
241
  --ui-fs-body: 13px;
237
242
  --ui-fs-caption: 12px;
238
243
  /* Content column width for the seller app shell — ONE source of truth for every wide surface
@@ -288,16 +293,17 @@
288
293
  --ui-warning-fg: color-mix(in srgb, #c99a2e 70%, #ffffff);
289
294
  --ui-danger-fg: color-mix(in srgb, var(--gemboss-danger-strong) 70%, #ffffff);
290
295
  --ui-focus: var(--adm-accent);
296
+ --ui-card-shadow: none;
291
297
  }
292
298
 
293
- /* ── Button ──────────────────────────────────────────────────────────────────
299
+ /* ── Button ──
294
300
  inline-flex + center so a <button> and an <a> render identically (anchors ignore
295
301
  `height` as inline elements — the root cause of the misaligned link-buttons). */
296
302
  .ui-btn {
297
303
  display: inline-flex; align-items: center; justify-content: center; gap: var(--ui-space-2);
298
- height: var(--ui-control-h); padding: 0 var(--ui-space-4); box-sizing: border-box;
304
+ height: var(--ui-btn-h); padding: 0 var(--ui-space-3); box-sizing: border-box;
299
305
  line-height: 1; white-space: nowrap; vertical-align: middle;
300
- border-radius: var(--ui-radius-md); font-family: inherit; font-size: var(--ui-fs-body); font-weight: 550;
306
+ border-radius: var(--ui-radius-md); font-family: inherit; font-size: var(--ui-fs-caption); font-weight: 550;
301
307
  cursor: pointer; text-decoration: none; user-select: none;
302
308
  border: 1px solid var(--ui-border); background: transparent; color: var(--ui-text);
303
309
  transition: background .12s ease, border-color .12s ease;
@@ -309,33 +315,33 @@
309
315
  .ui-btn--primary:hover { background: var(--ui-accent-hover); border-color: var(--ui-accent-hover); }
310
316
  .ui-btn--ghost { border-color: transparent; background: transparent; color: var(--ui-text-muted); }
311
317
  .ui-btn--ghost:hover { background: var(--ui-surface-2); color: var(--ui-text); }
312
- .ui-btn--sm { height: 28px; padding: 0 var(--ui-space-3); font-size: var(--ui-fs-caption); }
318
+ /* `ui-btn--sm`: the default size under a mouse since 0.12.0; it only marks a button the admin keeps compact under a finger. */
313
319
 
314
- /* ── Card ──────────────────────────────────────────────────────────────────── */
320
+ /* ── Card ── */
315
321
  .ui-card {
316
322
  background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
317
- padding: var(--ui-space-4); color: var(--ui-text);
323
+ box-shadow: var(--ui-card-shadow); padding: var(--ui-space-4); color: var(--ui-text);
318
324
  }
319
- .ui-card__title { font-size: 14px; font-weight: 600; margin: 0 0 var(--ui-space-2); }
325
+ .ui-card__title { font-size: 14px; line-height: 20px; font-weight: 650; margin: 0 0 var(--ui-space-2); }
320
326
  /* Đầu thẻ có nút: `<div class="ui-card__head"><h2 class="ui-card__title">…</h2><button class="ui-btn">…</button></div>`.
321
327
  Tiêu đề trái, hành động phải, xuống dòng khi hẹp (01/10/2026: partner-radar rel-head / agr). */
322
328
  .ui-card__head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--ui-space-2); margin-bottom: var(--ui-space-2); }
323
329
  .ui-card__head > .ui-card__title { margin: 0; }
324
330
  .ui-card__head > :last-child:not(.ui-card__title) { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); }
325
331
 
326
- /* ── Select ──────────────────────────────────────────────────────────────────
332
+ /* ── Select ──
327
333
  Native <select> reskinned; custom caret as an inline SVG data-URI (caret color
328
334
  uses the muted token via %-encoding fallback).
329
335
  Its BOX is the text field's (29/09/2026, Chris): same --gemboss-field-* tokens for ground, line and
330
336
  ink, same hover, same focus ring as `.gemboss-field`. Before, a grey --ui-surface-2 select stood next
331
337
  to a bordered white field in the same form (gempress editor, admin forms), and `:focus` removed the
332
338
  outline and only recoloured the border, so a keyboard user saw almost nothing. Height is
333
- --ui-control-h, the button's and the field's, because a select sits in toolbars beside both. */
339
+ --ui-control-h, the field's (Polaris Select, 32): a field, not a button (28, --ui-btn-h). */
334
340
  .ui-select {
335
341
  height: var(--ui-control-h); padding: 0 32px 0 12px; max-width: 100%;
336
342
  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;
337
343
  border: 1px solid var(--gemboss-field-line); border-radius: var(--ui-radius-md);
338
- color: var(--gemboss-field-ink); font-family: inherit; font-size: var(--ui-fs-body); font-weight: 500;
344
+ color: var(--gemboss-field-ink); font-family: inherit; font-size: var(--ui-fs-body); font-weight: 450;
339
345
  appearance: none; -webkit-appearance: none; cursor: pointer;
340
346
  }
341
347
  .ui-select:hover:not(:disabled) {
@@ -350,13 +356,13 @@
350
356
  .ui-select:disabled { opacity: .5; cursor: not-allowed; }
351
357
  .ui-select--slim { height: 30px; font-size: var(--ui-fs-caption); }
352
358
 
353
- /* ── Tag / Chip ──────────────────────────────────────────────────────────────
359
+ /* ── Tag / Chip ──
354
360
  Small inline labels. `solid` = filled muted pill (was gemboss-vault-tag); `chip` =
355
361
  bordered mono token (was gemboss-vault-chip). `accent` tone for the highlighted state. */
356
362
  .ui-tag {
357
363
  display: inline-flex; align-items: center; gap: 6px;
358
- font-size: var(--gemboss-fs-micro, 11px); font-weight: 600; letter-spacing: 0.03em; text-transform: uppercase;
359
- padding: 1px 6px; border-radius: 6px; background: var(--ui-surface-2); color: var(--ui-text-muted);
364
+ font-size: var(--ui-fs-caption); line-height: 20px; font-weight: 450;
365
+ padding: 0 6px; border-radius: var(--ui-radius-md); background: var(--ui-surface-2); color: var(--ui-text-muted);
360
366
  }
361
367
  .ui-tag--accent { background: var(--adm-accent-soft, rgba(20,22,26,.05)); color: var(--adm-accent-fg, var(--ui-accent)); }
362
368
  .ui-tag--chip {
@@ -390,13 +396,13 @@
390
396
 
391
397
  /* ── Chữ phụ: `<p class="ui-muted">`, `<span class="ui-muted">` — mô tả, chú thích, dòng meta. Cỡ caption, màu muted.
392
398
  Trước đây mỗi surface tự viết `.muted` (partner-radar: 180 chỗ). */
393
- .ui-muted { color: var(--ui-text-muted); font-size: var(--gemboss-fs-caption); }
399
+ .ui-muted { color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }
394
400
 
395
401
  /* ── Tiêu đề trang: `<div class="ui-page-head"><h1>…</h1><p>…</p></div>`. Đưa lên NGUYÊN VĂN từ admin
396
- `.gemboss-admin-page-head` (01/10/2026, Chris: một chủ sở hữu; admin dùng bản này). Cố ý không đặt margin cho h1
397
- và không dùng flex: admin không reset margin tiêu đề, nên đổi ở đây là đổi khoảng cách của 14 trang admin. */
402
+ `.gemboss-admin-page-head` (01/10/2026, Chris: một chủ sở hữu; admin dùng bản này). Không đặt margin cho h1
403
+ và không dùng flex: margin 0 đến từ reset (Polaris ở admin, app-shell.css trong shell của bề mặt từ 0.12.0). */
398
404
  .ui-page-head { margin-bottom: 20px; }
399
- .ui-page-head h1 { color: var(--ui-text-strong); font-size: 22px; line-height: 28px; font-weight: 650; letter-spacing: -0.01em; } /* own line-height: the 20px body line overlapped a wrapped title (0.5.1) */
405
+ .ui-page-head h1 { color: var(--ui-text-strong); font-size: 20px; line-height: 24px; font-weight: 700; letter-spacing: -0.01em; } /* own line-height: the 20px body line overlapped a wrapped title (0.5.1) */
400
406
  .ui-page-head p { color: var(--ui-text-muted); font-size: var(--ui-fs-body); margin-top: 4px; }
401
407
 
402
408
  /* ── Thanh công cụ: `<div class="ui-toolbar">` — ô tìm giãn, điều khiển bám phải, một hàng gọn, xuống dòng khi hẹp.
@@ -414,7 +420,7 @@ a.ui-metric { text-decoration: none; color: inherit; }
414
420
  a.ui-metric:hover { border-color: var(--ui-text-muted); }
415
421
  a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
416
422
  .ui-metric__val { font-size: 17px; font-weight: 700; color: var(--ui-text); font-variant-numeric: tabular-nums; line-height: 1.15; }
417
- .ui-metric__lbl { font-size: var(--gemboss-fs-caption); font-weight: 500; color: var(--ui-text-muted); }
423
+ .ui-metric__lbl { font-size: var(--ui-fs-caption); font-weight: 500; color: var(--ui-text-muted); }
418
424
  .ui-metric--success { border-color: color-mix(in srgb, var(--gemboss-success) 40%, transparent); }
419
425
  .ui-metric--success .ui-metric__val { color: var(--ui-success-fg); }
420
426
  .ui-metric--warning { border-color: color-mix(in srgb, #c99a2e 40%, transparent); }
@@ -426,7 +432,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
426
432
  Lưới tự giãn: mỗi ô ≥ 160px, xuống dòng khi hẹp. */
427
433
  .ui-dl { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px 16px; margin: 0; }
428
434
  .ui-dl > div { min-width: 0; }
429
- .ui-dl dt { font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
435
+ .ui-dl dt { font-size: var(--ui-fs-caption); color: var(--ui-text-muted); }
430
436
  .ui-dl dd { margin: 4px 0 0; font-weight: 600; color: var(--ui-text); overflow-wrap: anywhere; }
431
437
 
432
438
  /* ── Thông báo: `<div class="ui-notice ui-notice--warning">…</div>`. Lỗi tải, cảnh báo cấu hình, việc cần người
@@ -443,22 +449,22 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
443
449
  .ui-pager > a { display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); color: var(--ui-text); text-decoration: none; font-size: var(--ui-fs-body); }
444
450
  .ui-pager > a:hover { background: var(--ui-surface-2); }
445
451
  .ui-pager > a:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
446
- .ui-pager > [aria-current="page"] { color: var(--ui-text-muted); font-size: var(--gemboss-fs-caption); }
452
+ .ui-pager > [aria-current="page"] { color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }
447
453
 
448
- /* ── Badge ────────────────────────────────────────────────────────────────────
449
- Polaris' Badge (02/10/2026; was an uppercase mono pill): 12px/550, padding 2/8, radius 8, the secondary
454
+ /* ── Badge ──
455
+ Polaris' Badge (02/10/2026; was an uppercase mono pill): 12px/450 (its text span; 550 was the box), padding 2/8, radius 8, the secondary
450
456
  fill. The admin's /admin console draws it too (components/ui/Badge.tsx). Tones (`--success|--warning|--critical|
451
457
  --info`, `--dot`) are in kit-plain.css, for the non-React surfaces; the seller app uses Polaris <Badge tone>. */
452
458
  .ui-badge {
453
459
  display: inline-flex; align-items: center; gap: 4px;
454
- font-size: var(--ui-fs-caption); line-height: 16px; font-weight: 550; white-space: nowrap;
460
+ font-size: var(--ui-fs-caption); line-height: 16px; font-weight: 450; white-space: nowrap;
455
461
  padding: 2px 8px; border-radius: var(--ui-radius-md); background: color-mix(in srgb, var(--ui-text) 8%, transparent); color: var(--ui-text-muted);
456
462
  } /* a tint of the ink, not --ui-surface-2: that is the table row hover, and a badge vanished on hover (0.5.1) */
457
463
 
458
- /* ── Modal ────────────────────────────────────────────────────────────────────
464
+ /* ── Modal ──
459
465
  Rendered INLINE (not portaled): a portal escapes the .gemboss-admin-shell token
460
466
  scope and would render light. Kept in-tree so --ui-* resolves to the surface.
461
- 0.7.3: Polaris Modal's density (#1099, README "Popup density"). Popup only: --ui-control-h stays 32. */
467
+ 0.7.3: Polaris Modal's density (#1099, README "Popup density"). Buttons are 28 everywhere since 0.12.0 (--ui-btn-h). */
462
468
  .ui-overlay {
463
469
  position: fixed; inset: 0; z-index: 500;
464
470
  background: rgba(0, 0, 0, 0.5);
@@ -491,7 +497,6 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
491
497
  display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
492
498
  padding: 12px 16px; border-top: 1px solid var(--ui-border);
493
499
  }
494
- :is(.ui-modal__head, .ui-modal__foot) .ui-btn { height: 28px; padding: 0 12px; font-size: var(--ui-fs-caption); }
495
500
  /* 44px under a thumb or at phone width; -8px keeps the head 53. */
496
501
  @media (pointer: coarse), (max-width: 47.9975em) {
497
502
  .ui-modal__close { width: 44px; height: 44px; margin: -8px -8px -8px 0; }
@@ -530,15 +535,15 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
530
535
  .ui-select:not(.ui-select--slim) { min-height: 44px; }
531
536
  }
532
537
 
533
- /* ── Segmented control ────────────────────────────────────────────────────────
538
+ /* ── Segmented control ──
534
539
  Grouped toggle: icon squares (.ui-seg button) or text tabs (.is-text). The
535
540
  selected segment uses the accent-soft state. */
536
541
  .ui-seg { display: inline-flex; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md); overflow: hidden; }
537
542
  .ui-seg :is(button, a) {
538
- display: inline-grid; place-items: center; width: calc(var(--ui-control-h) - 2px); height: calc(var(--ui-control-h) - 2px);
543
+ display: inline-grid; place-items: center; width: calc(var(--ui-btn-h) - 2px); height: calc(var(--ui-btn-h) - 2px);
539
544
  border: 0; background: transparent; color: var(--ui-text-muted); cursor: pointer; font-family: inherit;
540
545
  }
541
- .ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-body); font-weight: 500; }
546
+ .ui-seg :is(button, a).is-text { width: auto; padding: 0 12px; font-size: var(--ui-fs-caption); font-weight: 550; }
542
547
  .ui-seg :is(button, a) + :is(button, a) { border-left: 1px solid var(--ui-border); }
543
548
  .ui-seg :is(button, a):hover { background: var(--ui-surface-2); color: var(--ui-text); }
544
549
  .ui-seg :is(button, a):focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }
@@ -548,7 +553,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
548
553
  .ui-seg button.active { background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
549
554
  .ui-seg svg { width: 16px; height: 16px; fill: currentColor; }
550
555
 
551
- /* ── Stat chip ────────────────────────────────────────────────────────────────
556
+ /* ── Stat chip ──
552
557
  Count-led pill; clickable ones double as filters (active = accent-soft). */
553
558
  .ui-stats { display: flex; flex-wrap: wrap; gap: var(--ui-space-2); margin-bottom: 24px; }
554
559
  .ui-stat {
@@ -563,7 +568,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
563
568
  .ui-stat.is-clickable.active { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
564
569
  .ui-stat.is-clickable.active b { color: var(--ui-accent-strong); }
565
570
 
566
- /* ── Toast / notification bar ─────────────────────────────────────────────────
571
+ /* ── Toast / notification bar ──
567
572
  App-wide "done / failed" pill for async actions (delete/duplicate/publish…).
568
573
  Polaris-style: dark pill, bottom-center, error variant in critical red. Lives
569
574
  in <ToastProvider> (components/ui/Toast.tsx), portaled to <body>. */
@@ -606,7 +611,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
606
611
  }
607
612
  @media (prefers-reduced-motion: reduce) { .gemboss-toast { animation: none; } }
608
613
 
609
- /* ── The kit's controls under a thumb ─────────────────────────────────────────────────────────
614
+ /* ── The kit's controls under a thumb ──
610
615
  * Measured by hit test on /admin/{theme-kernel,media,style-packs} at 390 and 1366 with a coarse
611
616
  * pointer: segmented buttons 36x36, selects 30-36 tall, clickable stat tiles 29, the asset row's
612
617
  * note button 30x30. §9.1 puts every one of them in the CONTROL tier (44px).
@@ -614,9 +619,11 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
614
619
  * HERE rather than in each surface's stylesheet, because this is the shared primitive layer — the
615
620
  * same reason the kit exists at all. The admin console and the seller app both read it.
616
621
  *
617
- * `pointer: coarse` only: a mouse keeps the density these surfaces were designed at. Growing a box
618
- * for every pointer is what #2147 did to a working width.
622
+ * The block below is `pointer: coarse` only: a mouse at desktop width keeps the density these surfaces were
623
+ * designed at. Growing a box for every pointer is what #2147 did to a working width.
619
624
  */
625
+ /* Phone WIDTH (< 768) with a mouse too (a narrow window): buttons and segments are 44 (0.12.0; at desktop width 28). */
626
+ @media (max-width: 47.9975em) { .ui-btn:not(.ui-btn--sm), .ui-seg :is(button, a) { min-height: 44px; } .ui-seg :is(button, a):not(.is-text) { min-width: 44px; } }
620
627
  @media (pointer: coarse) {
621
628
  /* NOT `.ui-textarea`: it already declares `min-height: 96px` earlier in this file, so adding it to
622
629
  a 44px group LOWERS it — same specificity, later line. The note dialog's box would have gone from
@@ -643,7 +650,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
643
650
  .ui-seg :is(button, a) { min-width: 44px; }
644
651
  }
645
652
 
646
- /* ── Skeleton ─────────────────────────────────────────────────────────────────
653
+ /* ── Skeleton ──
647
654
  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
648
655
  trí — nên nó tắt hẳn với `prefers-reduced-motion`, giữ nguyên vạch tĩnh. */
649
656
  .ui-skeleton { display: flex; flex-direction: column; gap: var(--ui-space-2); }
@@ -664,7 +671,7 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
664
671
 
665
672
  }
666
673
 
667
- /* ── Resizer: the seam you drag between two panels ────────────────────────────
674
+ /* ── Resizer: the seam you drag between two panels ──
668
675
  One look for every split in the fleet: the admin's editors (chat | preview) today; made for gemcare's
669
676
  inbox columns too (they adopt it at their next ui:sync). The element is the drag target; its GEOMETRY (a flex item, a grid edge, an absolutely
670
677
  placed strip) belongs to the surface. While a drag runs the surface puts `ui-resizing` on <body>
@@ -704,7 +711,7 @@ body.ui-resizing, body.ui-resizing * { cursor: col-resize !important; user-selec
704
711
  pointer stream is never captured by a cross-document frame (the classic "stuck cursor"). */
705
712
  body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999; cursor: col-resize; }
706
713
 
707
- /* ── Tabs · Table · Empty state ───────────────────────────────────────────────
714
+ /* ── Tabs · Table · Empty state ──
708
715
  Plain-CSS primitives for surfaces that are NOT React (gemcare's vanilla dashboard reads this
709
716
  file through `npm run export:ui-css`). The React app has Polaris <Tabs>/<DataTable>/<EmptyState>
710
717
  for these; nothing in apps/admin uses the classes below, and they restyle no existing component.
@@ -712,18 +719,18 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
712
719
 
713
720
  Tabs: `<div class="ui-tabs" role="tablist"><button role="tab" aria-selected="true">…` — the
714
721
  selected state IS `aria-selected`, so what a screen reader announces and what is painted cannot
715
- disagree. 44px under EVERY pointer (§9.1 control tier), not only coarse: tabs sit 4px apart, and
716
- that gap is only acceptable between targets that are already 44px. */
722
+ disagree. Polaris Tabs under a mouse (0.12.0): 28px, 12px/550; 8px apart, the §9.1 gap for targets under 44.
723
+ 44px under a finger or at phone width (the rule after these). */
717
724
  .ui-tabs {
718
- display: flex; gap: var(--ui-space-1); border-bottom: 1px solid var(--ui-border);
725
+ display: flex; gap: var(--ui-space-2); border-bottom: 1px solid var(--ui-border);
719
726
  overflow-x: auto; scrollbar-width: none;
720
727
  }
721
728
  .ui-tabs > button,
722
729
  .ui-tabs > a {
723
730
  position: relative; flex: 0 0 auto; display: inline-flex; align-items: center; gap: var(--ui-space-2);
724
- min-height: 44px; padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
731
+ min-height: var(--ui-btn-h); padding: 0 var(--ui-space-3); border: 0; background: transparent; cursor: pointer;
725
732
  border-radius: var(--ui-radius-sm) var(--ui-radius-sm) 0 0;
726
- font-family: inherit; font-size: var(--ui-fs-body); font-weight: 500; white-space: nowrap;
733
+ font-family: inherit; font-size: var(--ui-fs-caption); font-weight: 550; white-space: nowrap;
727
734
  color: var(--ui-text-muted);
728
735
  transition: color var(--gemboss-transition-fast) ease, background-color var(--gemboss-transition-fast) ease;
729
736
  }
@@ -734,13 +741,14 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
734
741
  .ui-tabs > a { text-decoration: none; }
735
742
  /* Tab bằng link (mỗi tab là một URL): `<nav class="ui-tabs"><a aria-current="page">` — cùng hình với tab nút. */
736
743
  .ui-tabs > a[aria-current="page"],
737
- .ui-tabs > button[aria-selected="true"] { color: var(--ui-text-strong); font-weight: 600; }
744
+ .ui-tabs > button[aria-selected="true"] { color: var(--ui-text-strong); }
738
745
  .ui-tabs > a[aria-current="page"]::after,
739
746
  .ui-tabs > button[aria-selected="true"]::after {
740
747
  content: ""; position: absolute; left: var(--ui-space-3); right: var(--ui-space-3); bottom: 0;
741
748
  height: 2px; border-radius: var(--gemboss-radius-full); background: var(--ui-accent);
742
749
  }
743
750
  .ui-tabs > button:disabled { opacity: .5; cursor: not-allowed; }
751
+ @media (pointer: coarse), (max-width: 47.9975em) { .ui-tabs > :is(button, a) { min-height: 44px; } }
744
752
 
745
753
  /* Table: wrap it — `<div class="ui-table-wrap"><table class="ui-table">` — so a wide table scrolls
746
754
  inside its card instead of pushing the page sideways at 320px. Numeric cells take `.is-num`:
@@ -748,16 +756,21 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
748
756
  .ui-table-wrap {
749
757
  overflow-x: auto; max-width: 100%;
750
758
  background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-lg);
759
+ box-shadow: var(--ui-card-shadow);
751
760
  }
752
761
  .ui-table { width: 100%; border-collapse: collapse; font-size: var(--ui-fs-body); color: var(--ui-text); }
753
762
  .ui-table th,
754
763
  .ui-table td {
755
- padding: var(--ui-space-2) var(--ui-space-3); text-align: left; vertical-align: middle;
764
+ padding: var(--ui-space-2) 6px; text-align: left; vertical-align: middle;
756
765
  border-bottom: 1px solid var(--ui-border);
757
766
  }
758
- .ui-table th {
759
- font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-text-muted); white-space: nowrap;
760
- }
767
+ /* Polaris DataTable (13.9.5, measured on /app/analytics 05/10/2026): cells 8px 6px (a row 36), header 12px 6px (45),
768
+ the first column 12px in from the edge. */
769
+ /* :where() keeps the edge columns at the base rule's weight, so a surface's own `.x-table td { padding }` still wins. */
770
+ .ui-table :is(th, td):where(:first-child) { padding-left: var(--ui-space-3); }
771
+ .ui-table :is(th, td):where(:last-child) { padding-right: var(--ui-space-3); }
772
+ .ui-table th { font-weight: 450; color: var(--ui-text-muted); white-space: nowrap; }
773
+ .ui-table thead th { padding-block: var(--ui-space-3); } /* the column header; a row header (tbody th) is a cell */
761
774
  .ui-table tbody tr:last-child > td { border-bottom: 0; }
762
775
  .ui-table tbody tr:hover { background: var(--ui-surface-2); }
763
776
  .ui-table .is-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
@@ -766,7 +779,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
766
779
  /* Link nằm thẳng trong ô (tên bản ghi dẫn sang trang chi tiết) là link trong DANH SÁCH: ≥ 24px (WCAG 2.5.8, §9.1).
767
780
  Chữ 13px cho hộp 17px; visual-gate đo đỏ ở partner-radar /competitors 01/10/2026. inline-flex giữ nó trên dòng. */
768
781
  .ui-table td > a { display: inline-flex; align-items: center; min-height: 24px; }
769
- .ui-table .ui-sub { display: block; margin-top: 2px; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); font-weight: 400; }
782
+ .ui-table .ui-sub { display: block; margin-top: 2px; font-size: var(--ui-fs-caption); color: var(--ui-text-muted); font-weight: 400; }
770
783
 
771
784
  /* Empty state: `<div class="ui-empty"><p class="ui-empty__title">…</p><p class="ui-empty__body">…</p>
772
785
  <button class="ui-btn">…</button></div>`. Says what is missing and what to do next. */
@@ -775,7 +788,7 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
775
788
  padding: var(--ui-space-4); padding-block: 40px; text-align: center;
776
789
  color: var(--ui-text-muted); font-size: var(--ui-fs-body);
777
790
  }
778
- .ui-empty__title { margin: 0; font-size: var(--gemboss-fs-h3); font-weight: 600; color: var(--ui-text-strong); }
791
+ .ui-empty__title { margin: 0; font-size: 14px; line-height: 20px; font-weight: 650; color: var(--ui-text-strong); }
779
792
  .ui-empty__body { margin: 0; max-width: 48ch; line-height: 1.5; }
780
793
  .ui-empty > .ui-btn { margin-top: var(--ui-space-2); }
781
794
 
@@ -2040,6 +2053,19 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
2040
2053
  /* Headings get their own line (Polaris heading line-heights are ~1.25-1.4): inheriting the body's 20px made a wrapped 22px
2041
2054
  * title overlap itself (gempress, 0.5.1). */
2042
2055
  :where(h1, h2, h3) { letter-spacing: -0.01em; line-height: 1.3; }
2056
+ /* Polaris' heading reset (0.12.0, 05/10/2026): in the admin an <h2> or a <p> with no class is body text with no margin,
2057
+ * because Polaris' global sheet sets `h1…h6, p { margin: 0; font-size: 1em; font-weight: regular }` and every heading
2058
+ * that looks like one is a Text variant. Surfaces had the browser's instead: a bare <h2> at 19.5px bold with 16px margins
2059
+ * sat in GemFactory and taskdesk pages beside kit cards titled 14px. Here, only INSIDE the shell (`.gemboss-platform-
2060
+ * shell`, the AppShell root every surface on the kit renders): a login page or a public page keeps its own type. Never
2061
+ * inside an article the surface shows: `.ui-prose` or `[data-content]` (the contract), and the wrappers the fleet already
2062
+ * uses for rendered HTML, read on 05/10/2026, by exact name (gemcommunity `md-body` and `recap-md`,
2063
+ * content-radar `article-body`, the assistants' `asst-md`; any class containing `prose` or `markdown`, any case, so ProseMirror too). A NEW wrapper: mark it `ui-prose` or
2064
+ * `data-content`. scripts/density.mjs skips exactly this list (ARTICLE); density.test.ts fails if the two differ. A post body keeps the browser's scale. Only an element WITHOUT a class: a surface's `h2.gf-title` that sets only its size keeps the
2065
+ * browser's bold and margins (a zero-specificity author rule still beats the browser's own sheet, review #3787). */
2066
+ :where(.gemboss-platform-shell) :where(h1, h2, h3, h4, h5, h6, p):where(:not([class])):not(:where(.ui-prose, [data-content], [class*="prose" i], [class*="markdown" i], .md-body, .recap-md, .asst-md, .article-body) *) {
2067
+ margin: 0; font-size: 1em; font-weight: inherit; line-height: inherit;
2068
+ }
2043
2069
 
2044
2070
  .gemboss-topbar-start { display: flex; align-items: center; gap: 8px; min-width: 0; }
2045
2071
  .gemboss-topbar-center { display: flex; justify-content: center; min-width: 0; }
@@ -2081,6 +2107,82 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
2081
2107
  }
2082
2108
  .gnav-item.active .gnav-count { background: var(--gemboss-primary); color: var(--gemboss-on-primary); }
2083
2109
 
2110
+ /* ── Navigation pending (0.10.0): a click answers on the first frame ─────────────────────────────────────────────
2111
+ * Chris 05/10/2026: content-radar's rail (the icon turns into a spinner the moment you click, sub items too) is the
2112
+ * standard for every surface. Measured 04/10 on content-radar with a server ~280 ms away: nothing on screen changed
2113
+ * for 321-503 ms after a click; with this, 6-8 ms.
2114
+ *
2115
+ * ONE state, three ways to set it, so a React rail, a Next rail and a server-rendered rail look the same:
2116
+ * - `aria-busy="true"` on the item: RailItem / RailSubItem `pending` (React), kit-plain.js on a followed link;
2117
+ * - a `.gnav-pending` child: NavLink from `@gemboss/ui/next` (Next's useLinkStatus is only readable INSIDE the
2118
+ * link, so the link cannot set its own attribute; it renders this status text and `:has()` reads it).
2119
+ * `:is()` is forgiving: a browser without `:has()` still honours the aria-busy half of each rule.
2120
+ * The icon keeps its box (visibility, not display), so the label never moves. Here and not in shell.css: the
2121
+ * admin's shell sheet has <1 KB of first-load headroom, and the admin answers a click with its own holding screen.
2122
+ */
2123
+ .gnav-item:is([aria-busy="true"], :has(> .gnav-pending)),
2124
+ .gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending)) { cursor: progress; }
2125
+ .gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico > * { visibility: hidden; }
2126
+ .gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico::after,
2127
+ .gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after,
2128
+ .gb-pending-mark {
2129
+ content: ""; display: inline-block; vertical-align: middle; grid-area: 1 / 1; flex: none; box-sizing: border-box;
2130
+ width: 20px; height: 20px; border-radius: 50%;
2131
+ /* the item's own ink, so the spinner follows the active and hover colours */
2132
+ border: 2px solid color-mix(in srgb, currentColor 18%, transparent); border-top-color: currentColor;
2133
+ animation: gnav-spin 0.7s linear infinite;
2134
+ }
2135
+ /* A sub item has no icon box: a 12px spinner in its 44px indent, just before the label, out of the flow. At the row's end
2136
+ * it would be a third item in a space-between row and push a trailing count to the middle (review #3777). */
2137
+ .gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after {
2138
+ position: absolute; left: 22px; top: 50%; margin-top: -6px; width: 12px; height: 12px; border-width: 1.5px;
2139
+ }
2140
+ @keyframes gnav-spin { to { transform: rotate(360deg); } }
2141
+ /* Slower, never off: it is the only sign that the click was heard. */
2142
+ @media (prefers-reduced-motion: reduce) {
2143
+ .gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico::after,
2144
+ .gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after,
2145
+ .gb-pending-mark { animation-duration: 2s; }
2146
+ }
2147
+ /* A link in the PAGE (a list row, a card, a tab) opts in with `data-gb-pending`: PendingLink (`@gemboss/ui/next`) or
2148
+ * kit-plain.js put `span.gb-pending-mark` (role=status) inside it while the page loads. "inline" (default): 12px after
2149
+ * the text. "corner": 16px in the top right corner, out of the flow, for a link that wraps a whole card (a grid card
2150
+ * gains no column). Marked by attribute, not class: a surface that colours bare links with `a:not([class])` keeps it. */
2151
+ a[data-gb-pending]:has(> .gb-pending-mark), a[data-gb-pending][aria-busy="true"] { cursor: progress; }
2152
+ a[data-gb-pending] > .gb-pending-mark { display: inline-block; width: 12px; height: 12px; border-width: 1.5px; margin-left: 6px; vertical-align: -1px; }
2153
+ a[data-gb-pending="corner"] { position: relative; }
2154
+ a[data-gb-pending="corner"] > .gb-pending-mark { position: absolute; top: 12px; right: 12px; width: 16px; height: 16px; margin: 0; }
2155
+ /* RouteHolding (./route-holding.tsx): a route group's loading boundary. The kit's skeleton lines in the content column,
2156
+ * one shape for every route: a title bar, then four lines of falling length (the admin's holding screen). */
2157
+ .gb-route-holding { padding-top: 4px; }
2158
+ .gb-route-holding .ui-skeleton { gap: 16px; }
2159
+ .gb-route-holding__heading { margin: 0 0 20px; font-size: 20px; font-weight: 650; line-height: 1.3; color: var(--gemboss-text); }
2160
+ .gb-route-holding .ui-skeleton__line { height: 10px; }
2161
+ .gb-route-holding .ui-skeleton__line.gb-route-holding__title { height: 20px; width: 34%; margin-bottom: 8px; }
2162
+ .gb-route-holding .ui-skeleton__line:nth-last-child(4) { width: 46%; }
2163
+ .gb-route-holding .ui-skeleton__line:nth-last-child(3) { width: 92%; }
2164
+ .gb-route-holding .ui-skeleton__line:nth-last-child(2) { width: 78%; }
2165
+ .gb-route-holding .ui-skeleton__line:nth-last-child(1) { width: 61%; }
2166
+ /* RouteCover: the same frame over the content area from the click until the navigation commits. Shown only while
2167
+ * `html[data-gb-navigating]` (set by the @gemboss/ui/next links while pending). No grace delay: the target is the new
2168
+ * screen within 100 ms of the click, and a 40 ms grace measured 120-132 ms on content-radar's rail (05/10/2026). The
2169
+ * 80 ms fade is what a router cache hit (20-90 ms) sees of it: a faint, partial frame, not a full flash.
2170
+ * z 5: above the outgoing page, below the rail, the topbar (300) and the assistant (290). */
2171
+ .gb-route-cover { display: none; }
2172
+ html[data-gb-navigating] .gb-route-cover {
2173
+ display: block; position: fixed; inset: 56px 0 0 240px; z-index: 5; overflow: hidden; box-sizing: border-box;
2174
+ padding: 24px; background: var(--gemboss-app-bg); pointer-events: none;
2175
+ animation: gb-route-cover-in 0.08s ease-out both;
2176
+ }
2177
+ @keyframes gb-route-cover-in { from { opacity: 0; } to { opacity: 1; } }
2178
+ @media (max-width: 760px) { html[data-gb-navigating] .gb-route-cover { left: 0; padding: 16px; } }
2179
+ @media (prefers-reduced-motion: reduce) { html[data-gb-navigating] .gb-route-cover { animation-duration: 0s; } }
2180
+ /* The status text a NavLink renders inside a rail item: read, not seen. */
2181
+ .gnav-pending {
2182
+ position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
2183
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
2184
+ }
2185
+
2084
2186
  /* ── BrandLockup (./brand.tsx) ─────────────────────────────────────────────────────────────────
2085
2187
  * The admin's `.gemboss-brand-lockup` box (40px tall, 8px padding, hover wash) around the wordmark's
2086
2188
  * own proportions. Not that class itself: its ≤460px rule swaps in a background image at an
@@ -3373,8 +3475,7 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
3373
3475
 
3374
3476
  Key-value list: `<dl class="ui-kv"><dt>Owner</dt><dd>Lily</dd>…</dl>` — label column sized to
3375
3477
  its longest label, value column takes the rest and wraps long keys and URLs. */
3376
- .ui-btn--icon { flex: none; width: var(--ui-control-h); padding: 0; }
3377
- .ui-btn--icon.ui-btn--sm { width: 28px; }
3478
+ .ui-btn--icon { flex: none; width: var(--ui-btn-h); padding: 0; }
3378
3479
  .ui-btn--icon svg { width: 18px; height: 18px; }
3379
3480
  /* Menu: the SURFACE is the shell's `gemboss-popover-surface` and the rows are the account menu's row (shell.css
3380
3481
  declares `.gemboss-account-menu-item, .ui-menu__item` together), so a kit menu and the account menu on one page
@@ -3391,10 +3492,19 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
3391
3492
  .ui-menu__sep { height: 1px; margin: var(--ui-space-1) 0; border: 0; background: var(--ui-border); }
3392
3493
  /* Checkbox: the LABEL is the target; disabled turns both the label and the box to not-allowed. Focus ring in the
3393
3494
  kit's own token (no --p-* here: this file is appended after the export's Polaris resolver runs). */
3394
- .ui-check { display: inline-flex; align-items: center; gap: var(--ui-space-2); min-height: 44px; cursor: pointer; font-size: var(--ui-fs-body); color: var(--ui-text); }
3495
+ .ui-check { display: inline-flex; align-items: center; gap: var(--ui-space-2); min-height: 44px; max-width: 100%; overflow-wrap: break-word; cursor: pointer; font-size: var(--ui-fs-body); color: var(--ui-text); }
3395
3496
  .ui-check > input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); cursor: inherit; }
3396
3497
  .ui-check > input:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
3397
3498
  .ui-check:has(> input:disabled) { opacity: .5; cursor: not-allowed; }
3499
+ /* 0.9.0: a long run with no spaces (a URL, "clinical/premium/…/utility") breaks inside the label instead of widening
3500
+ the page: gemwatcher Operations overflowed 69px at 320 on one switch description (#375). Three parts: the label is
3501
+ capped at its parent (max-width above), its TEXT child may shrink (min-width 0, the last child only so a badge in
3502
+ front keeps its width), and break-word breaks the run once it cannot fit a line.
3503
+ break-word, NOT anywhere: anywhere also drops the label's min-content, and a flex row then squeezes a short label
3504
+ ("Lock" beside a long band name in GemFactory) into "Lo/ck". Two cases stay the surface's job: the text must be an
3505
+ element (`input + span`; bare text is an anonymous flex item min-width cannot reach), and in a flex ROW the label
3506
+ keeps its min-content, so that row must wrap. */
3507
+ .ui-check > :last-child:not(input) { min-width: 0; }
3398
3508
  .ui-label { display: block; margin: 0 0 var(--ui-space-1); font-size: var(--ui-fs-caption); font-weight: 600; color: var(--ui-text-muted); }
3399
3509
  .ui-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--ui-space-2) var(--ui-space-4); margin: 0; font-size: var(--ui-fs-body); }
3400
3510
  .ui-kv > dt { color: var(--ui-text-muted); }
@@ -3456,8 +3566,6 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3456
3566
  </div><div class="ui-modal__actions"><button class="ui-btn">Copy</button></div><button class="ui-modal__close">…` */
3457
3567
  .ui-modal__subtitle { display: block; margin-top: 2px; font-size: var(--ui-fs-caption); line-height: 16px; font-weight: 450; color: var(--ui-text-muted); overflow-wrap: anywhere; }
3458
3568
  .ui-modal__actions { flex: none; display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-space-2); }
3459
- /* An icon button in the popup's head or foot is square at the popup's 28 (ui.css gives those buttons 0/12 padding). */
3460
- :is(.ui-modal__head, .ui-modal__foot) .ui-btn--icon { width: 28px; padding: 0; }
3461
3569
  /* A popup taller than the screen: the body scrolls, the head and foot stay, so the actions are always reachable
3462
3570
  (320×568 with a long form put Huỷ/Xoá below the fold, unreachable under the locked page; review #3633). */
3463
3571
  .ui-overlay > .ui-modal { display: flex; flex-direction: column; max-height: calc(100dvh - 40px); }
@@ -3493,8 +3601,8 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3493
3601
  dưới cùng (delta + so với gì). */
3494
3602
  .ui-metrics--grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); gap: var(--ui-space-3); margin: 0; }
3495
3603
  .ui-metric--lg { gap: 4px; padding: var(--ui-space-3) var(--ui-space-4); }
3496
- .ui-metric--lg .ui-metric__val { font-size: var(--gemboss-fs-h1, 24px); line-height: 1.1; }
3497
- .ui-metric--lg .ui-metric__lbl { order: -1; font-size: var(--gemboss-fs-body, 14px); }
3604
+ .ui-metric--lg .ui-metric__val { font-size: 24px; line-height: 32px; } /* Polaris headingXl, as the admin's analytics cards */
3605
+ .ui-metric--lg .ui-metric__lbl { order: -1; font-size: var(--ui-fs-body); } /* bodyMd 13 (0.12: was 14) */
3498
3606
  .ui-metric__foot { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
3499
3607
  @media (max-width: 520px) { .ui-metrics--grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
3500
3608
 
@@ -3542,7 +3650,7 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3542
3650
  sau bước đầu mang `__rate` = tỉ lệ đi tiếp từ bước NGAY TRƯỚC (bề mặt tính). Ngang ở khổ rộng, dọc ở ≤ 520px. */
3543
3651
  .ui-funnel { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: var(--ui-space-2); margin: 0; padding: 0; list-style: none; }
3544
3652
  .ui-funnel > li { position: relative; display: flex; flex-direction: column; gap: 2px; padding: var(--ui-space-3); border-radius: var(--ui-radius-md); background: var(--ui-surface-2); min-width: 0; }
3545
- .ui-funnel__val { font-size: var(--gemboss-fs-h2, 20px); font-weight: 700; color: var(--ui-text); font-variant-numeric: tabular-nums; line-height: 1.15; }
3653
+ .ui-funnel__val { font-size: 20px; font-weight: 700; color: var(--ui-text); font-variant-numeric: tabular-nums; line-height: 24px; } /* Polaris headingLg */
3546
3654
  .ui-funnel__lbl { font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); overflow-wrap: anywhere; }
3547
3655
  .ui-funnel__rate { order: -1; font-size: var(--gemboss-fs-caption); font-weight: 600; color: var(--ui-series-1); font-variant-numeric: tabular-nums; }
3548
3656
  @media (max-width: 520px) { .ui-funnel { grid-auto-flow: row; } }
@@ -3557,7 +3665,7 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3557
3665
  @media (max-width: 960px) { .ui-grid--2 { grid-template-columns: minmax(0, 1fr); } }
3558
3666
  @media (max-width: 720px) { .ui-grid--3 { grid-template-columns: minmax(0, 1fr); } }
3559
3667
  .ui-section-head { margin: var(--ui-space-4) 0 0; }
3560
- .ui-section-head > h2 { margin: 0; font-size: var(--gemboss-fs-h3, 16px); font-weight: 650; color: var(--ui-text); }
3668
+ .ui-section-head > h2 { margin: 0; font-size: 14px; line-height: 20px; font-weight: 650; color: var(--ui-text); } /* Polaris headingMd (0.12: was 16) */
3561
3669
  .ui-section-head > p { margin: 2px 0 0; font-size: var(--gemboss-fs-caption); color: var(--ui-text-muted); }
3562
3670
 
3563
3671
  /* Nhóm nút CHỮ không bao giờ bẻ chữ (03/10/2026, thanh lọc của gemnews ở 390px: "Mọi ngôn ngữ" thành hai dòng, "中文"
@@ -3661,12 +3769,13 @@ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3661
3769
  }
3662
3770
 
3663
3771
  /* Radio: the checkbox's row with `type="radio"` — `<fieldset class="ui-choices"><legend class="ui-label">Plan</legend>
3664
- <label class="ui-check"><input type="radio" name="plan" checked> Basic</label>…</fieldset>`. */
3665
- .ui-choices { display: grid; gap: 0; margin: 0; padding: 0; border: 0; min-width: 0; }
3772
+ <label class="ui-check"><input type="radio" name="plan" checked><span>Basic</span></label>…</fieldset>`.
3773
+ One minmax(0, 1fr) column: an auto column grows to the longest unbroken word in any label (0.9.0). */
3774
+ .ui-choices { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; margin: 0; padding: 0; border: 0; min-width: 0; }
3666
3775
  .ui-choices > legend { padding: 0; }
3667
3776
 
3668
3777
  /* Switch: a checkbox that reads as on / off, for a setting that applies at once (no Save button).
3669
- `<label class="ui-check ui-switch"><input type="checkbox" role="switch" checked> Send Monday digest</label>`.
3778
+ `<label class="ui-check ui-switch"><input type="checkbox" role="switch" checked><span>Send Monday digest</span></label>`.
3670
3779
  Track 32×20, thumb 16; the row stays the checkbox's 44px target. */
3671
3780
  .ui-switch > input[type="checkbox"] {
3672
3781
  appearance: none; -webkit-appearance: none; position: relative; width: 32px; height: 20px; margin: 0;
@@ -3806,6 +3915,26 @@ button.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-off
3806
3915
  /* Phone: 11 targets of 44px wrapped into 3-4 rows at 320 (measured). Keep Previous · current · Next; the range wraps under. */
3807
3916
  @media (max-width: 35.9975em) { .ui-pager > a:not([rel]):not([aria-current]):not([aria-disabled]), .ui-pager__gap { display: none; } }
3808
3917
 
3918
+ /* 0.12.0: filter chip and card stack, both drawn by hand on every surface the 05/10/2026 density audit read.
3919
+ Filter chip: `<div class="ui-chips"><button type="button" class="ui-chip" aria-pressed="true">Shopify Plus</button>
3920
+ <a class="ui-chip" href="?country=us" aria-current="page">US</a></div>`. Polaris FilterPill's shape (radius 8, a 1px line,
3921
+ dashed until applied) at the Button's density: 28px, 12px/550, so a filter row and a button row are one height.
3922
+ Applied = `aria-pressed="true"` (in place) or `aria-current="page"` (a URL), solid line and the selected tint. */
3923
+ .ui-chip {
3924
+ display: inline-flex; align-items: center; justify-content: center; gap: 4px; box-sizing: border-box;
3925
+ height: var(--ui-btn-h, 28px); padding: 0 12px; border: 1px dashed var(--ui-border); border-radius: var(--ui-radius-md);
3926
+ background: var(--ui-surface); color: var(--ui-text); font: inherit; font-size: var(--ui-fs-caption); font-weight: 550;
3927
+ line-height: 16px; white-space: nowrap; text-decoration: none; cursor: pointer;
3928
+ }
3929
+ .ui-chip:hover { border-style: solid; background: var(--ui-surface-2); }
3930
+ .ui-chip:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 1px; }
3931
+ .ui-chip:is([aria-pressed="true"], [aria-current="page"]) { border-style: solid; border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-accent-strong); }
3932
+ @media (pointer: coarse), (max-width: 47.9975em) { .ui-chip { height: auto; min-height: 44px; min-width: 44px; } }
3933
+ /* Card stack: `<div class="ui-stack"><div class="ui-card">…</div><div class="ui-card">…</div></div>`. 16px between blocks,
3934
+ Polaris BlockStack gap 400, the admin's spacing between cards on a page. `--tight` is 8 (inside a card). */
3935
+ .ui-stack { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--ui-space-4); }
3936
+ .ui-stack--tight { gap: var(--ui-space-2); }
3937
+
3809
3938
  /* ═══ @gemboss/ui src/chat.css ═══ */
3810
3939
  /*
3811
3940
  * The chat BODY — what an answer looks like, wherever a GemBoss surface puts a chat (01/10/2026).
@@ -4552,6 +4681,7 @@ dialog.ui-modal.gb-search { width: min(640px, 100%); margin-top: 8vh; max-height
4552
4681
  --gemboss-danger-border: #5c2a24;
4553
4682
  --ui-accent-fg: #0d0d12;
4554
4683
  --ui-warning-fg: #e8c063;
4684
+ --ui-card-shadow: none; /* a dark drop under a dark card is noise */
4555
4685
  --gb-brand-ink: #f2f3f5;
4556
4686
  }
4557
4687
  /* Avatar initials in dark: the tone's light end, so the text clears 4.5:1 on its tinted disc. */