@gemboss/ui 0.2.0 → 0.3.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: 62b85f8a0c756bf63d370bff60e6debaaf4ea4df
5
- * sources sha256: fd64ea38ae59bb5ecf1c38da341be89f963c9fef11bd3189e83433588f4d88b5
4
+ * gemboss commit: 7d7968ae7238c5f4e7643d54dbac372f32ef5775
5
+ * sources sha256: 2807a6d2a8b27730eaa31cbe6195b633cbba9467b0ab62b9244a7f814bb8d205
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
  * + 180 rules from packages/ui/src/shell.css (the shared shell)
@@ -337,7 +337,7 @@
337
337
  background-color: var(--gemboss-field-bg-hover);
338
338
  }
339
339
  .ui-select:focus { outline: none; }
340
- .ui-select:focus-visible {
340
+ .ui-select:focus-visible:not(:disabled) {
341
341
  outline: 2px solid #005bd3; outline-offset: 1px;
342
342
  border-color: var(--gemboss-field-line-active);
343
343
  }
@@ -483,12 +483,35 @@ a.ui-metric:focus-visible { outline: 2px solid var(--ui-focus); outline-offset:
483
483
  consumer belongs in that consumer's chunk (see SettingsDialog.module.css). */
484
484
  .ui-modal--lg { position: relative; }
485
485
 
486
+ /* The kit's multi-line field joins the ONE field family (01/10/2026, Chris: "form control đồng bộ"):
487
+ same input-border token, hover and focus ring as `gemboss-field` and `.ui-select`. Before, it drew a
488
+ lighter --ui-border box at 14px with no focus ring, so on GemFactory's compose form a textarea sat
489
+ beside selects of a darker border and text fields of a third look (measured on prod). */
486
490
  .ui-textarea {
487
491
  width: 100%; min-height: 96px; resize: vertical; box-sizing: border-box;
488
- background: var(--ui-surface); border: 1px solid var(--ui-border); border-radius: var(--ui-radius-md);
489
- padding: 12px; color: var(--ui-text); font-size: 14px; font-family: inherit; outline: none;
492
+ background: var(--gemboss-field-bg);
493
+ border: 1px solid var(--gemboss-field-line); border-radius: var(--ui-radius-md);
494
+ padding: 12px; color: var(--gemboss-field-ink); font-size: var(--ui-fs-body); line-height: 1.5; font-family: inherit;
495
+ }
496
+ .ui-textarea::placeholder { color: var(--gemboss-field-placeholder); }
497
+ .ui-textarea:hover:not(:disabled) {
498
+ border-color: var(--gemboss-field-line-hover);
499
+ background-color: var(--gemboss-field-bg-hover);
500
+ }
501
+ .ui-textarea:focus { outline: none; }
502
+ .ui-textarea:focus-visible:not(:disabled) {
503
+ outline: 2px solid #005bd3; outline-offset: 1px;
504
+ border-color: var(--gemboss-field-line-active);
505
+ }
506
+ .ui-textarea:disabled { opacity: .5; cursor: not-allowed; }
507
+ /* On a PHONE-WIDTH screen every field of the family reads 16px (iOS zooms the page on tap below that)
508
+ and a select is a 44px target — by WIDTH, as `gemboss-field` does, because a desktop browser at 320px
509
+ still reports pointer: fine. Measured on GemFactory at 390px before this: select 13px, textarea 14px. */
510
+ @media (max-width: 47.9975em) {
511
+ .ui-select.ui-select, .ui-textarea { font-size: 16px; }
512
+ /* Height only for the full select: `--slim` is a density decision (table rows), as the coarse block keeps it. */
513
+ .ui-select:not(.ui-select--slim) { min-height: 44px; }
490
514
  }
491
- .ui-textarea:focus { border-color: var(--ui-accent); }
492
515
 
493
516
  /* ── Segmented control ────────────────────────────────────────────────────────
494
517
  Grouped toggle: icon squares (.ui-seg button) or text tabs (.is-text). The
@@ -768,6 +791,10 @@ body.ui-resizing::after { content: ""; position: fixed; inset: 0; z-index: 9999;
768
791
  border: 1px solid var(--gemboss-field-line);
769
792
  border-radius: 8px;
770
793
  }
794
+ /* WebKit draws `input[type=search]` as `-webkit-appearance: searchfield`: its own padding and rounding, so in Safari
795
+ (macOS and iOS, the platform the 16px rule exists for) a search box did not match the fields beside it. Same box for
796
+ every text type; the clear button stays. (GemFactory #98 review, 01/10/2026.) */
797
+ input[type="search"]:is(.gemboss-field, .gemboss-field__bare) { -webkit-appearance: none; appearance: none; }
771
798
  .gemboss-field::placeholder { color: var(--gemboss-field-placeholder); }
772
799
  /* A NARROW field — the inspector's T/R/B/L boxes and its number spinners are 40-68px wide, and the
773
800
  shared 12px side padding left 38px of content box for a three-digit value (measured in Chromium:
@@ -882,7 +909,8 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
882
909
  z-index: 320;
883
910
  overflow: hidden;
884
911
  }
885
- .gemboss-account-menu-item {
912
+ .gemboss-account-menu-item,
913
+ .ui-menu__item {
886
914
  display: block;
887
915
  width: 100%;
888
916
  min-height: 44px;
@@ -897,15 +925,18 @@ textarea.gemboss-field { height: auto; padding: 6px 12px; resize: vertical; }
897
925
  cursor: pointer;
898
926
  transition: background var(--gemboss-transition-fast) ease, color var(--gemboss-transition-fast) ease;
899
927
  }
900
- .gemboss-account-menu-item:hover {
928
+ .gemboss-account-menu-item:hover,
929
+ .ui-menu__item:hover {
901
930
  background: var(--gemboss-hover);
902
931
  }
903
- .gemboss-account-menu-item:focus-visible {
932
+ .gemboss-account-menu-item:focus-visible,
933
+ .ui-menu__item:focus-visible {
904
934
  outline: 2px solid var(--gemboss-primary);
905
935
  outline-offset: -2px;
906
936
  background: var(--gemboss-hover);
907
937
  }
908
- .gemboss-account-menu-item.danger {
938
+ .gemboss-account-menu-item.danger,
939
+ .ui-menu__item--danger {
909
940
  color: var(--gemboss-danger-strong);
910
941
  }
911
942
  .gemboss-account-chip {
@@ -2709,6 +2740,7 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
2709
2740
  degrading to instant is the right failure for chrome nobody asked to watch. */
2710
2741
  @starting-style {
2711
2742
  .gb-dock-dock, .gb-dock-mini { transform: translateY(10px); opacity: 0; }
2743
+ .gb-dock-side { transform: translateX(12px); opacity: 0; }
2712
2744
  }
2713
2745
  /* Nothing animates while a pointer is setting the numbers — see `dragging` in the host. */
2714
2746
  .gb-dock-dragging, .gb-dock-dragging * { transition: none !important; }
@@ -2747,6 +2779,66 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
2747
2779
  background: var(--gemboss-app-bg);
2748
2780
  }
2749
2781
 
2782
+ /* ── Side: a full-height panel pinned to the right edge (01/10/2026) ─────────────────────────────
2783
+ The third working shape, beside the popup and full screen: a long conversation next to the screen
2784
+ it is about. Opt-in per surface (`<AssistantHost allowSide>`), because unlike the popup it TAKES
2785
+ ROOM — the page beside it moves over (rule below), so nothing under it is hidden. That is why the
2786
+ surface is solid rather than frosted: nothing sits behind it to show through.
2787
+ Above `.gemboss-route-shell` (5), so the panel stays put while routes change — a sidebar that
2788
+ vanishes on every click is a popup with extra steps. BELOW the topbar (300): the account menu
2789
+ opens down out of the topbar's right end, exactly where this panel starts, and at the popup's 320
2790
+ the menu fell behind the panel (review). 290 still clears the desktop invite (260/280). The host
2791
+ is rendered inside `main` (AppShell's slot), which works because `main` makes no stacking context
2792
+ or containing block; a surface that gives its `main` a transform or z-index must mount it outside. */
2793
+ .gb-dock-side {
2794
+ z-index: 290;
2795
+ top: 56px;
2796
+ right: 0;
2797
+ bottom: 0;
2798
+ width: var(--gb-dock-side-w, 400px);
2799
+ background: var(--gemboss-surface);
2800
+ -webkit-backdrop-filter: none;
2801
+ backdrop-filter: none;
2802
+ border: 0;
2803
+ border-left: 1px solid var(--gemboss-divider);
2804
+ border-radius: 0;
2805
+ box-shadow: none;
2806
+ overflow: visible; /* the seam's touch band reaches 14px past the left edge */
2807
+ }
2808
+ .gb-dock-side .gb-dock-body, .gb-dock-side .gb-dock-pane { overflow: hidden; }
2809
+ /* The header does not drag a panel that is pinned. */
2810
+ .gb-dock-side .gb-dock-header { cursor: default; touch-action: auto; }
2811
+ /* The word for what the dot already says stands down here, as it does in a popup under 420px: the panel has
2812
+ five controls in its header, and the title needs the room (measured: "Gem assi…" / "Re…" at 400). Visually
2813
+ only — the dot is aria-hidden, so the word is the one place a screen reader hears "Working" (review #3622). */
2814
+ .gb-dock-side .gb-dock-status {
2815
+ flex: 0 0 auto; position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
2816
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
2817
+ }
2818
+ .gb-dock-side .gb-dock-header:active { cursor: default; }
2819
+ /* The seam: the panel's left edge, the fleet's one `.ui-resizer` (ui.css draws the grip). A 5px strip
2820
+ centred on the border, ABOVE the page beside it. */
2821
+ /* `.gb-dock-side >` is not decoration: `.ui-resizer { position: relative }` (ui.css) has the weight of one
2822
+ class too, and which file a bundler loads last would decide whether the seam is a 5px strip or a 0px
2823
+ line nobody can grab. */
2824
+ .gb-dock-side > .gb-dock-sidegrip {
2825
+ position: absolute;
2826
+ top: 0;
2827
+ bottom: 0;
2828
+ left: -3px;
2829
+ width: 5px;
2830
+ z-index: 3;
2831
+ }
2832
+ /* The page makes room. `margin-right`, not padding: main's padding is set by four rules in shell.css
2833
+ and a fifth here would fight them. The provider puts the class and the width on <body>; the default
2834
+ `bodyClassPrefix` is `gemboss-assistant` — a surface with another prefix writes its own copy of this
2835
+ one rule. No transition: the seam is set from a pointer, and a page that trails the finger is the
2836
+ bug `.gb-dock` already avoids. */
2837
+ @media (min-width: 1100px) {
2838
+ body.gemboss-assistant-side .gemboss-platform-main { margin-right: var(--gb-assistant-side-w, 400px); }
2839
+ }
2840
+
2841
+
2750
2842
  /* ── Full: the one control full screen needs — the way back to the popup ──────────────────────
2751
2843
  Floated over the chat's own header, whose right half is empty, rather than given a bar of its
2752
2844
  own: a whole strip of chrome for one button would push the conversation down on every screen. */
@@ -2896,7 +2988,10 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
2896
2988
  white-space: nowrap;
2897
2989
  }
2898
2990
  .gb-dock-header:active { cursor: grabbing; }
2899
- .gb-dock-actions { display: flex; align-items: center; gap: 2px; cursor: default; }
2991
+ /* `margin-left: auto`: the status word was the only thing that stretched, so wherever it stands down (a popup under
2992
+ 420px, the side panel) the controls followed the title instead of the right edge — 350px adrift in a 720px panel
2993
+ (review #3622). With the word showing, it still takes the free space first and nothing moves. */
2994
+ .gb-dock-actions { display: flex; align-items: center; gap: 2px; cursor: default; margin-left: auto; }
2900
2995
  /* One ACTION, then the three WINDOW controls — ordered by how much each changes the window: bigger,
2901
2996
  smaller, gone. The gap is what says they are two different kinds of button; without it four icons
2902
2997
  in a row read as one undifferentiated strip, which is what "các icon đang không đồng đều" was
@@ -3039,4 +3134,682 @@ body.gemboss-assistant-unread .gnav-item.gnav-assistant::after {
3039
3134
  bottom: 0;
3040
3135
  border-radius: 0;
3041
3136
  }
3137
+ /* No room beside a phone screen: the side panel takes the screen, the way full does, and there is
3138
+ nothing to drag. The page is not pushed (the rule above stops at 761). */
3139
+ .gb-dock-side {
3140
+ left: 0;
3141
+ right: 0;
3142
+ width: auto;
3143
+ border-left: 0;
3144
+ border-radius: 0; /* the phone .gb-dock rule above rounds the top; this panel is the screen */
3145
+ }
3146
+ .gb-dock-sidegrip { display: none; }
3147
+ /* …so the button that pins it there would only say "full screen" a second time. */
3148
+ .gb-dock-pinside { display: none; }
3149
+ }
3150
+
3151
+ /* 1100 = rail 240 + the panel's narrowest 380 + the page's narrowest 480 (SIDE_MIN_WIDTH, SIDE_PAGE_MIN in
3152
+ assistant/dock.ts). Below it the two do not fit side by side — MEASURED at 768: pushed, the page kept
3153
+ 120px and its table ran under the panel. So between the phone and 1100 the panel covers the content
3154
+ area the way full screen does, nothing is pushed, there is no seam to drag, and the popup does not
3155
+ offer to pin.
3156
+ AT THE END OF THE FILE on purpose: `.gb-dock-icon { display: grid }` comes later than the side
3157
+ section and has the same weight, so placed there this block lost and the pin still showed at 768
3158
+ (measured). */
3159
+ @media (min-width: 761px) and (max-width: 1099.98px) {
3160
+ .gb-dock-side { left: 240px; width: auto; }
3161
+ .gb-dock-side .gb-dock-sidegrip, .gb-dock-pinside { display: none; }
3162
+ }
3163
+
3164
+ /* ═══ @gemboss/ui src/kit-plain.css ═══ */
3165
+ /* kit-plain.css — kit primitives that only the NON-React surfaces use (GemFactory, gemcare, gempress write the
3166
+ markup by hand). They ship inside gemboss-ui.css (scripts/build-css.mjs `own`), but the admin does NOT import
3167
+ this file: `ui.css` is loaded by the admin's root layout, so every rule there is paid on every admin screen,
3168
+ and adding these to it pushed the admin shell 0.2 KB over its first-load budget (CI of #3591, 01/10/2026).
3169
+ The admin has Polaris for menus, checkboxes and description lists. */
3170
+
3171
+ /* ── Icon button · Menu · Key-value list ──────────────────────────────────────
3172
+ Added 01/10/2026 for GemFactory's item dialog, which had seven actions in one header row; the
3173
+ secondary five now sit behind one "⋯" button. Plain CSS, same contract as the tabs and tables in ui.css.
3174
+
3175
+ Icon button: `<button class="ui-btn ui-btn--icon" aria-label="…"><svg…></button>` — square, the
3176
+ label is REQUIRED (there is no text for a screen reader to read otherwise).
3177
+
3178
+ Menu: a `<details>` so it opens with no script. `<details class="ui-menu ui-menu--end">
3179
+ <summary class="ui-btn ui-btn--icon" aria-label="More actions">…</summary><div class="ui-menu__list">
3180
+ <button class="ui-menu__item">Copy #12<span class="ui-menu__hint">for Claude Code</span></button>
3181
+ <hr class="ui-menu__sep"><button class="ui-menu__item ui-menu__item--danger">Delete…</button></div>
3182
+ </details>`. `--end` aligns the list to the right edge of its button. Closing on an outside click,
3183
+ on an item, and on Escape is the page's job (a few lines; GemFactory's ui-kit.mjs has them).
3184
+ Items are 44px under EVERY pointer, not only coarse: they touch each other (0px apart), and §9.1
3185
+ only allows a gap under 8px between targets that are already 44px.
3186
+
3187
+ Key-value list: `<dl class="ui-kv"><dt>Owner</dt><dd>Lily</dd>…</dl>` — label column sized to
3188
+ its longest label, value column takes the rest and wraps long keys and URLs. */
3189
+ .ui-btn--icon { flex: none; width: var(--ui-control-h); padding: 0; }
3190
+ .ui-btn--icon.ui-btn--sm { width: 28px; }
3191
+ .ui-btn--icon svg { width: 18px; height: 18px; }
3192
+ /* Menu: the SURFACE is the shell's `gemboss-popover-surface` and the rows are the account menu's row (shell.css
3193
+ declares `.gemboss-account-menu-item, .ui-menu__item` together), so a kit menu and the account menu on one page
3194
+ look the same and a fix to one reaches the other (§9.3). Markup: `<div class="ui-menu__list gemboss-popover-surface">`.
3195
+ Here: only placement, the hint, the separator and the disabled state. */
3196
+ .ui-menu { position: relative; display: inline-block; }
3197
+ .ui-menu > summary { list-style: none; cursor: pointer; }
3198
+ .ui-menu > summary::-webkit-details-marker { display: none; }
3199
+ .ui-menu__list { position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; display: grid; min-width: 220px; padding: var(--ui-space-1); box-sizing: border-box; }
3200
+ .ui-menu--end .ui-menu__list { left: auto; right: 0; }
3201
+ .ui-menu__item { display: flex; align-items: center; gap: var(--ui-space-2); white-space: nowrap; border-radius: var(--ui-radius-sm); }
3202
+ .ui-menu__item:is([disabled], [aria-disabled="true"]) { opacity: .5; cursor: not-allowed; pointer-events: none; }
3203
+ .ui-menu__hint { margin-left: auto; padding-left: var(--ui-space-4); color: var(--ui-text-muted); font-size: var(--ui-fs-caption); }
3204
+ .ui-menu__sep { height: 1px; margin: var(--ui-space-1) 0; border: 0; background: var(--ui-border); }
3205
+ /* Checkbox: the LABEL is the target; disabled turns both the label and the box to not-allowed. Focus ring in the
3206
+ kit's own token (no --p-* here: this file is appended after the export's Polaris resolver runs). */
3207
+ .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); }
3208
+ .ui-check > input { flex: none; width: 18px; height: 18px; margin: 0; accent-color: var(--ui-accent); cursor: inherit; }
3209
+ .ui-check > input:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
3210
+ .ui-check:has(> input:disabled) { opacity: .5; cursor: not-allowed; }
3211
+ .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); }
3212
+ .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); }
3213
+ .ui-kv > dt { color: var(--ui-text-muted); }
3214
+ .ui-kv > dd { margin: 0; min-width: 0; overflow-wrap: anywhere; color: var(--ui-text); }
3215
+
3216
+ /* Under a thumb the icon-only button is 44 WIDE as well (the kit's coarse block already raises every .ui-btn to 44 tall). */
3217
+ @media (pointer: coarse) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; } }
3218
+ /* …and at phone WIDTH, as gemboss-field does: a desktop browser at 320px still reports pointer: fine. */
3219
+ @media (max-width: 47.9975em) { .ui-btn--icon:not(.ui-btn--sm) { min-width: 44px; min-height: 44px; } }
3220
+
3221
+ /* ── Link · Media card (02/10/2026) ──────────────────────────────────────────
3222
+ GemFactory, gemcare and gempress left text links in the browser's default blue-and-purple, and each drew its own
3223
+ thumbnail card. The admin uses Polaris' Link and has no media card, so both live here, not in ui.css.
3224
+
3225
+ Link: `<a class="ui-link" href>` for a link inside text. Iris (--gemboss-accent-strong, 6.2:1 on white), underlined
3226
+ so it is not told by colour alone (WCAG 1.4.1); visited looks the same (these are tools, not articles). Markdown or
3227
+ other generated HTML you cannot add classes to: wrap it in `.ui-prose` and its bare `<a>` get the same look.
3228
+
3229
+ Media card: `<article class="ui-card ui-card--media"><a|button class="ui-card__media"><img alt=""></…>
3230
+ <div class="ui-card__body">…</div><div class="ui-card__foot">…</div></article>`. The picture is 16:10 and
3231
+ cropped to fill; the card has no padding of its own, the body and foot do. */
3232
+ .ui-link,
3233
+ .ui-prose a:not([class]) {
3234
+ color: var(--gemboss-accent-strong, #3a44de);
3235
+ text-decoration: underline;
3236
+ text-decoration-thickness: from-font;
3237
+ text-underline-offset: 2px;
3238
+ }
3239
+ .ui-link:visited,
3240
+ .ui-prose a:not([class]):visited { color: var(--gemboss-accent-strong, #3a44de); }
3241
+ .ui-link:hover,
3242
+ .ui-prose a:not([class]):hover { color: var(--gemboss-accent, #5661f5); text-decoration-thickness: 2px; }
3243
+ .ui-link:focus-visible,
3244
+ .ui-prose a:not([class]):focus-visible { outline: 2px solid var(--ui-focus, #005bd3); outline-offset: 2px; border-radius: 2px; }
3245
+
3246
+ .ui-card--media { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
3247
+ .ui-card__media {
3248
+ display: block; width: 100%; aspect-ratio: 16 / 10; margin: 0; padding: 0; border: 0;
3249
+ border-bottom: 1px solid var(--ui-border); background: var(--ui-surface-2); overflow: hidden; color: inherit;
3250
+ }
3251
+ .ui-card__media > img { display: block; width: 100%; height: 100%; object-fit: cover; }
3252
+ button.ui-card__media, a.ui-card__media { cursor: pointer; }
3253
+ .ui-card__media:focus-visible { outline: 2px solid var(--ui-focus, #005bd3); outline-offset: -2px; }
3254
+ /* justify-items: start: a tag or a button in the body keeps its own width instead of stretching across the card. */
3255
+ .ui-card__body { display: grid; justify-items: start; gap: var(--ui-space-2, 8px); padding: var(--ui-space-3, 12px) var(--ui-space-4, 16px); min-width: 0; }
3256
+ .ui-card__foot {
3257
+ display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--ui-space-2, 8px);
3258
+ margin-top: auto; padding: 0 var(--ui-space-4, 16px) var(--ui-space-3, 12px);
3259
+ color: var(--ui-text-muted); font-size: var(--ui-fs-caption, 13px);
3260
+ }
3261
+
3262
+ /* Popup body and foot: `<div class="ui-modal__body">…</div><div class="ui-modal__foot"><button class="ui-btn">…</button></div>`.
3263
+ The admin's Modal.tsx pads its own children, so ui.css never styled these; every non-React surface padded them by
3264
+ hand (GemFactory: 16/20 and a hairline above the actions). kit-plain.js's openModal writes exactly this shape. */
3265
+ .ui-modal__body { padding: 16px 20px; }
3266
+ .ui-modal__foot {
3267
+ display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end;
3268
+ padding: 12px 20px; border-top: 1px solid var(--ui-border);
3269
+ }
3270
+ /* A popup taller than the screen: the body scrolls, the head and foot stay, so the actions are always reachable
3271
+ (320×568 with a long form put Huỷ/Xoá below the fold, unreachable under the locked page; review #3633). */
3272
+ .ui-overlay > .ui-modal { display: flex; flex-direction: column; max-height: calc(100dvh - 40px); }
3273
+ .ui-modal > .ui-modal__head, .ui-modal > .ui-modal__foot { flex: none; }
3274
+ .ui-modal > .ui-modal__body { flex: 1 1 auto; min-height: 0; overflow: auto; }
3275
+
3276
+ /* ═══ @gemboss/ui src/chat.css ═══ */
3277
+ /*
3278
+ * The chat BODY — what an answer looks like, wherever a GemBoss surface puts a chat (01/10/2026).
3279
+ *
3280
+ * Chris: every surface's AI assistant (gemcare, content-radar, partner-radar, GemCommunity) must be built from the
3281
+ * GemBoss UI framework. Until today only the FRAME was shared (assistant.css: popup / bar / full screen; shell.css:
3282
+ * the thread rail); the body lived in the admin's gemboss-shell.css and the exporter left it out on purpose, so every
3283
+ * surface drew its own bubbles, markdown and composer. These are those rules, moved here VERBATIM (same selectors,
3284
+ * same values, same order) — the admin @imports this file at the top of its shell, so it is still the one owner.
3285
+ *
3286
+ * What is here: the header (`.gemboss-ec-head/title/sub`), the message list (`.gemboss-ec-scroll`), messages and
3287
+ * bubbles (`.gemboss-ec-msg.user|.assistant`, `.gemboss-ec-bubble`), the working indicator and the streaming cursor
3288
+ * (`.gemboss-ec-pending*`, `.gemboss-ec-cursor`), markdown (`.gemboss-md-*`, `.gemboss-ec-md-link`), step narration
3289
+ * (`.gemboss-narr*`), starters and option chips (`.gemboss-ec-starter(s)`, `.gemboss-ec-chip(s)`), and the composer
3290
+ * (`.gemboss-ec-input` with its textarea and submit button).
3291
+ * What is NOT: the panel's geometry (`.gemboss-ec` width, editor columns, tabs), attachments, CTA / action cards,
3292
+ * the engine picker and work mode — those are still the admin's, and a surface brings its own. ONE exception, on
3293
+ * purpose: the chat panel's touch / mouse TARGET blocks (`@media (pointer: coarse|fine)` below) name every control
3294
+ * of the panel, the admin-only ones (tabs, upload, engine picker, work mode, attach, stop) included. They are one
3295
+ * rule per pointer kind; split in two they wrote their declarations twice and the admin's shell budget had no room.
3296
+ * Those selectors match nothing on a surface without those controls. (`.gemboss-ec-workmode`: the click-area
3297
+ * `::before` is here, its label-sizing `::after` stays the admin's.)
3298
+ *
3299
+ * Markup: see the catalog's "Thân chat" section. Behaviour (streaming, stop, keyboard) belongs to the surface.
3300
+ */
3301
+
3302
+ .gemboss-ec-head { padding: 16px 16px; border-bottom: 1px solid var(--gemboss-divider); display: flex; flex-direction: column; gap: 2px; }
3303
+ .gemboss-ec-title { font-family: var(--gemboss-serif); font-size: 15px; font-weight: 600; }
3304
+ .gemboss-ec-sub { font-size: 12px; color: var(--gemboss-text-muted); }
3305
+ .gemboss-ec-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 16px; scrollbar-gutter: stable; }
3306
+ /* Visible thin scrollbar on the chat message list — the default macOS overlay bar is
3307
+ hidden until you actively scroll, so the list reads as "can't scroll" (esp. on the
3308
+ dedicated Assistant page, a fixed-height chat where the page itself doesn't scroll).
3309
+ A persistent thin bar makes the scroll affordance obvious; matches the composer input. */
3310
+ .gemboss-ec-scroll { scrollbar-width: thin; scrollbar-color: rgba(31,30,29,0.22) transparent; }
3311
+ .gemboss-ec-scroll::-webkit-scrollbar { width: 9px; }
3312
+ .gemboss-ec-scroll::-webkit-scrollbar-track { background: transparent; }
3313
+ .gemboss-ec-scroll::-webkit-scrollbar-thumb { background: rgba(31,30,29,0.2); border-radius: 6px; border: 2px solid transparent; background-clip: content-box; }
3314
+ .gemboss-ec-msg { display: flex; flex-direction: column; gap: 8px; max-width: 100%; }
3315
+ .gemboss-ec-msg.user { align-items: flex-end; }
3316
+ .gemboss-ec-msg.assistant { align-items: flex-start; }
3317
+ .gemboss-ec-bubble {
3318
+ /* Cap reading measure: a chat reply stays at a readable line length, not the bubble's full 90%. */
3319
+ font-size: var(--gemboss-fs-body); line-height: 1.5; padding: 12px 12px; border-radius: 16px; max-width: min(90%, 68ch);
3320
+ white-space: pre-wrap; word-break: break-word;
3321
+ }
3322
+ .gemboss-ec-msg.user .gemboss-ec-bubble { background: var(--gemboss-primary); color: #fff; border-bottom-right-radius: 4px; }
3323
+ .gemboss-ec-msg.assistant .gemboss-ec-bubble { background: var(--gemboss-surface); color: var(--gemboss-text); border: 1px solid var(--gemboss-divider); border-bottom-left-radius: 4px; }
3324
+ .gemboss-ec-input button[type="submit"] svg { width: 18px; height: 18px; fill: currentColor; }
3325
+ /* Live "working" heartbeat — the Claude-Code-style running asterisk shown before the first token.
3326
+ Slim, muted, single line so the chat stays clean; the motion (pulsing ✳, crossfading verb, live
3327
+ counter) is what makes the wait feel fast instead of frozen. */
3328
+ .gemboss-ec-pending { display: inline-flex; align-items: center; gap: 8px; padding: 6px 2px; font-size: var(--gemboss-fs-caption); color: var(--gemboss-text-muted); }
3329
+ .gemboss-ec-pending-star { color: var(--gemboss-primary); font-size: 12px; line-height: 1; animation: gemboss-star-pulse 1.1s ease-in-out infinite; }
3330
+ .gemboss-ec-pending-verb { color: var(--gemboss-text); animation: gemboss-verb-in 0.4s ease-out; }
3331
+ .gemboss-ec-pending-meta { font-variant-numeric: tabular-nums; opacity: 0.7; font-size: var(--gemboss-fs-caption); }
3332
+ @keyframes gemboss-star-pulse { 0%,100% { transform: scale(0.85); opacity: 0.55; } 50% { transform: scale(1.15); opacity: 1; } }
3333
+ @keyframes gemboss-verb-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: none; } }
3334
+ /* Typewriter cursor — a thin blinking bar trailing the answer while tokens stream in. */
3335
+ .gemboss-ec-cursor { display: inline-block; width: 2px; height: 1.05em; margin-left: 2px; vertical-align: -2px; background: var(--gemboss-primary); border-radius: 2px; animation: gemboss-cursor-blink 1s steps(1) infinite; }
3336
+ @keyframes gemboss-cursor-blink { 0%,45% { opacity: 1; } 50%,95% { opacity: 0.15; } 100% { opacity: 1; } }
3337
+ @media (prefers-reduced-motion: reduce) {
3338
+ .gemboss-ec-pending-star, .gemboss-ec-cursor { animation: none; }
3339
+ }
3340
+ /* Markdown link inside the answer bubble — a friendly inline link, never a raw path. */
3341
+ .gemboss-ec-md-link { color: var(--gemboss-primary-dark); font-weight: 600; text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--gemboss-primary) 35%, transparent); cursor: pointer; }
3342
+ .gemboss-ec-md-link:hover { border-bottom-color: var(--gemboss-primary); }
3343
+ /* Suggestion starters = compact wrapping CHIPS (pills), not stacked full-width boxes —
3344
+ reads as a chat's quick-reply chips and stops one prompt eating a whole row. */
3345
+ .gemboss-ec-starters { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
3346
+ .gemboss-ec-starter {
3347
+ text-align: left; font-size: 13px; padding: 6px 12px; border-radius: var(--gemboss-radius-full); cursor: pointer;
3348
+ background: var(--gemboss-surface); border: 1px solid var(--gemboss-divider); color: var(--gemboss-text);
3349
+ white-space: nowrap; max-width: 100%;
3350
+ }
3351
+ .gemboss-ec-starter:hover { border-color: var(--gemboss-primary); background: rgba(86, 97, 245, 0.06); }
3352
+ /* (removed) dead legacy rail styles — `.icon-nav-assistant` + the hover-flyout submenu
3353
+ (`.icon-nav-flyout*`). The current nav (IconNav.tsx) is a persistent 240px labeled
3354
+ sidebar using `.gnav-*` classes with route-driven sub-nav; there is no 72px icon rail
3355
+ and no hover flyout, so these selectors matched nothing. */
3356
+
3357
+ /* Composer = ONE bordered rounded shell (ChatGPT/Claude-style): the textarea sits borderless
3358
+ inside it and the attach (📎) + send (↑) buttons float within the same box — cleaner than the
3359
+ old three-separate-boxes row. The shell is inset from the panel edges (margin) and lifts its
3360
+ border to primary on focus-within, so the whole composer reads as one focused field. */
3361
+ /* TWO ROWS when there are chips (ChatComposer): the textarea on its own full-width row, the toolbar
3362
+ under it. The toolbar is where chips of any length live, and when they shared a row with the text
3363
+ box the send button was the thing that gave way — measured 22/09/2026 at 320px, it wrapped to a
3364
+ THIRD line. With no chips at all, ONE row — see `:has` below.
3365
+ 8px inside, not 6: at 6 the text sat 13px from the border and the box read as cramped. What the
3366
+ toolbar SAYS at each width is ChatComposer's job (data-density), measured rather than guessed. */
3367
+ .gemboss-ec-input {
3368
+ display: flex; flex-wrap: wrap; align-items: center; gap: 4px;
3369
+ margin: 12px; padding: 8px;
3370
+ border: 1px solid var(--gemboss-divider); border-radius: 16px;
3371
+ background: var(--gemboss-surface);
3372
+ transition: border-color var(--gemboss-transition-fast) ease, box-shadow var(--gemboss-transition-fast) ease;
3373
+ }
3374
+ .gemboss-ec-input:focus-within {
3375
+ border-color: var(--gemboss-primary);
3376
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--gemboss-primary) 12%, transparent);
3377
+ }
3378
+ .gemboss-ec-input textarea {
3379
+ flex: 1 0 100%; min-width: 0; width: 100%; box-sizing: border-box; resize: none; border-radius: 0; background: transparent;
3380
+ /* The CHIP's box model — a transparent 1px border and 8px of padding — so the first letter typed
3381
+ lines up with the first letter of "Claude Code" below it. It was 6px with no border: the text
3382
+ started 3px left of the chips, measured on every host at every width, and the box read as two
3383
+ columns. (Box model rather than nine pixels of padding: nine is off the spacing scale the CI guard holds.) */
3384
+ border: 1px solid transparent;
3385
+ padding: 6px 8px; font-size: var(--gemboss-fs-body); font-family: inherit;
3386
+ /* Starts at one line; GemChat's auto-grow JS expands it up to ~3 lines (max-height),
3387
+ then scrolls inside (ChatGPT/Gemini-style). overflow-y is hidden until the JS flips
3388
+ it to auto past the cap, so a 1–2 line message never shows a (rounding) scrollbar. */
3389
+ min-height: 0; max-height: 84px; line-height: 1.5; overflow-y: hidden;
3390
+ scrollbar-width: thin; scrollbar-color: rgba(31,30,29,0.22) transparent; /* thin, not the OS bar */
3391
+ }
3392
+ .gemboss-ec-input textarea::-webkit-scrollbar { width: 7px; }
3393
+ .gemboss-ec-input textarea::-webkit-scrollbar-track { background: transparent; }
3394
+ .gemboss-ec-input textarea::-webkit-scrollbar-thumb { background: rgba(31,30,29,0.2); border-radius: 4px; border: 2px solid transparent; background-clip: content-box; }
3395
+ .gemboss-ec-input textarea:focus { outline: none; }
3396
+ /* Send button — a filled primary circle floating at the shell's bottom-right. inline-flex centers
3397
+ the ↑ SVG (the old rule had no flex-centering, so the icon sat high).
3398
+
3399
+ SCOPED TO THE SUBMIT BUTTON, and that word is load-bearing. This rule used to read
3400
+ `.gemboss-ec-input button`, which reached EVERY button in the composer — including whatever a host
3401
+ passes in through `composerLeading`, a slot that exists precisely so hosts can put their own
3402
+ controls here. The 📎 attach button already had to fight it back with a class of its own (see the
3403
+ comment above `.gemboss-ec-attach-btn`), which was the first sign that this selector owns more than
3404
+ it should. The second was the engine picker: a Polaris Button, rendered into the slot, came out as a
3405
+ 34px black circle with its label ("Claude Code · Opus") clipped to three characters. Nothing was
3406
+ wrong with the picker — this rule was painting it.
3407
+
3408
+ §9.3: one visual owner per component. The composer owns its own send button and nothing that is
3409
+ handed to it. Guarded by composer-owns-only-its-own-buttons.test.ts.
3410
+
3411
+ 44px, not 34: it is a CONTROL (§9.1, Apple HIG / WCAG 2.5.5 AAA). This branch had already raised
3412
+ it — but on the OLD unscoped selector, the very rule main had just narrowed. The two changes were
3413
+ fixing different faults in one declaration, so both are kept: main's scope, this branch's size. */
3414
+ .gemboss-ec-input button[type="submit"] {
3415
+ flex: 0 0 auto; width: 44px; height: 44px; min-width: 44px; min-height: 44px; border-radius: 50%; border: 0; cursor: pointer;
3416
+ display: inline-flex; align-items: center; justify-content: center;
3417
+ background: var(--gemboss-primary); color: #fff; font-size: 16px; font-weight: 700;
3418
+ transition: background var(--gemboss-transition-fast) ease, filter var(--gemboss-transition-fast) ease;
3419
+ }
3420
+ .gemboss-ec-input button[type="submit"]:hover:not(:disabled) { background: var(--gemboss-primary-dark); }
3421
+ .gemboss-ec-input button[type="submit"]:active:not(:disabled) { background: var(--gemboss-primary-dark); filter: brightness(0.94); }
3422
+ .gemboss-ec-input button[type="submit"]:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: 2px; }
3423
+ /* Disabled (empty input): a clear neutral grey circle with a muted arrow — reads as
3424
+ "inactive" rather than a washed-out faint-primary blob. */
3425
+ .gemboss-ec-input button[type="submit"]:disabled { background: var(--gemboss-divider); color: var(--gemboss-text-muted); opacity: 1; cursor: default; }
3426
+ /* ── Narration timeline ── */
3427
+ .gemboss-narr { list-style: none; margin: 0; padding: 8px 0 0; display: flex; flex-direction: column; gap: 8px; }
3428
+ .gemboss-narr-row { display: flex; align-items: center; gap: 8px; font-size: var(--gemboss-fs-caption); color: var(--gemboss-text-muted); }
3429
+ .gemboss-narr-dot { width: 16px; height: 16px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; flex: 0 0 auto; }
3430
+ .gemboss-narr-row.ok .gemboss-narr-dot { background: var(--gemboss-active-bg); color: var(--gemboss-active-fg); }
3431
+ .gemboss-narr-row.failed .gemboss-narr-dot { background: #fde8e6; color: var(--gemboss-danger); }
3432
+ .gemboss-narr-row.running .gemboss-narr-dot { background: transparent; }
3433
+ /* THE ENGINE'S MARK, in the dot's slot. No pill behind it: the ✓/✕ dots earn their background by
3434
+ being a glyph in a circle, and a logo in a coloured circle reads as a badge the engine awarded
3435
+ itself. It takes the ROW's colour, so a failed turn's mark goes red with everything else — a mark
3436
+ that stayed brand-coloured over a failure would be the one cheerful thing on a broken row. */
3437
+ .gemboss-narr-dot-engine { background: transparent !important; color: inherit; }
3438
+ .gemboss-narr-row.ok .gemboss-narr-dot-engine { color: var(--gemboss-text); }
3439
+ .gemboss-narr-row.failed .gemboss-narr-dot-engine { color: var(--gemboss-danger); }
3440
+ .gemboss-narr-row.ok .gemboss-narr-text, .gemboss-narr-row.failed .gemboss-narr-text { color: var(--gemboss-text); }
3441
+ /* A message the seller typed INTO the running turn. It reads as THEIR line, not as a step the agent
3442
+ took: full-strength text (the timeline is muted by default) against the page's own text colour, and
3443
+ a `❯` prompt caret instead of a status dot. It must be legible at a glance while the agent is still
3444
+ writing above it — a seller scanning a long timeline needs to find the moment they interrupted. */
3445
+ .gemboss-narr-row.steer .gemboss-narr-dot { background: transparent; color: var(--gemboss-text-muted); }
3446
+ /* `anywhere`, not `normal`: a seller correcting a turn pastes URLs and product handles, and one
3447
+ unbroken 60-character token in a 320px chat column is a row that pushes the panel sideways. Measured
3448
+ 2026-09-23 — the default `overflow-wrap: normal` on this row does not break such a token. */
3449
+ .gemboss-narr-steer { color: var(--gemboss-text); font-weight: 500; overflow-wrap: anywhere; }
3450
+ .gemboss-narr-spin { width: 11px; height: 11px; border: 2px solid var(--gemboss-divider-soft); border-top-color: var(--gemboss-primary); border-radius: 50%; animation: gemboss-spin 0.7s linear infinite; }
3451
+ @keyframes gemboss-spin { to { transform: rotate(360deg); } }
3452
+ /* Live-reasoning ("thinking") row: muted + italic while streaming, collapsible once settled. */
3453
+ .gemboss-narr-row.thinking { align-items: flex-start; }
3454
+ .gemboss-narr-row.thinking .gemboss-narr-dot { color: var(--gemboss-text-muted); background: transparent; }
3455
+ .gemboss-narr-thinking-live { font-style: italic; color: var(--gemboss-text-muted); white-space: pre-wrap; opacity: 0.9; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
3456
+ .gemboss-narr-thinking { font-size: var(--gemboss-fs-caption); }
3457
+ .gemboss-narr-thinking > summary { cursor: pointer; color: var(--gemboss-text-muted); list-style: none; user-select: none; }
3458
+ .gemboss-narr-thinking > summary::-webkit-details-marker { display: none; }
3459
+ .gemboss-narr-thinking > summary::before { content: "▸ "; }
3460
+ .gemboss-narr-thinking[open] > summary::before { content: "▾ "; }
3461
+ .gemboss-narr-thinking-body { margin-top: 6px; padding: 8px 8px; border-left: 2px solid var(--gemboss-divider-soft); color: var(--gemboss-text-muted); font-style: italic; white-space: pre-wrap; line-height: 1.5; }
3462
+ /* TOOL TRANSCRIPT rows — the Claude-Code shape: a monospace call line, its return value folded
3463
+ behind a disclosure. Muted on purpose: the transcript is evidence you can audit, not the answer. */
3464
+ .gemboss-narr-row.trace code { font-family: var(--gemboss-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: var(--gemboss-fs-caption); word-break: break-word; }
3465
+ .gemboss-narr-trace-call { color: var(--gemboss-text-muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
3466
+ .gemboss-narr-trace { font-size: var(--gemboss-fs-caption); min-width: 0; }
3467
+ .gemboss-narr-trace > summary { cursor: pointer; color: var(--gemboss-text-muted); list-style: none; user-select: none;
3468
+ /* A tool call with real arguments is long and the chat panel is narrow: a full payload wrapped to five
3469
+ lines buries the rows around it. Clamp while closed; opening the row shows the call in full. */
3470
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
3471
+ .gemboss-narr-trace[open] > summary { display: block; overflow: visible; }
3472
+ .gemboss-narr-trace > summary::-webkit-details-marker { display: none; }
3473
+ /* The fold is now the ONLY way to see a payload or a finished thought (both start closed), so its
3474
+ summary is a target: ≥24px tall, the list-link tier of Gems/CLAUDE.md §9.1 (WCAG 2.5.8). Measured
3475
+ 21px before, 13/09. */
3476
+ .gemboss-narr-trace > summary, .gemboss-narr-thinking > summary { min-height: 24px; box-sizing: border-box; padding-block: 2px; }
3477
+ .gemboss-narr-trace > summary::before { content: "▸ "; }
3478
+ .gemboss-narr-trace[open] > summary::before { content: "▾ "; }
3479
+ .gemboss-narr-trace-body { margin-top: 6px; padding: 8px; border-left: 2px solid var(--gemboss-divider-soft); color: var(--gemboss-text-muted); white-space: pre-wrap; line-height: 1.5; overflow-x: auto; }
3480
+ @media (max-width: 640px) {
3481
+ /* Composer auto-grows from one line (see GemChat) — no fixed floor needed here. */
3482
+ .gemboss-ec-input textarea { min-height: 0; }
3483
+ .gemboss-ec-scroll { padding: 12px; }
3484
+ }
3485
+ /* ── Chat option chips (persona/goal polling) ── */
3486
+ .gemboss-ec-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 4px; }
3487
+ /* 44px because a chip is a CONTROL, not a link in a list — Apple HIG / WCAG 2.5.5 AAA, Gems CLAUDE.md
3488
+ §9.1. Measured 2026-09-09 at 390px and 1366px: padding alone rendered these at 34px, ten short, and
3489
+ they are the only way to answer a poll or resume an unfinished run on a phone. `min-height` rather
3490
+ than `height` so a wrapped two-line label still grows instead of clipping. */
3491
+ .gemboss-ec-chip {
3492
+ background: var(--gemboss-surface); border: 1px solid var(--gemboss-divider); border-radius: var(--gemboss-radius-full);
3493
+ padding: 8px 16px; font-size: 13px; cursor: pointer; color: var(--gemboss-text);
3494
+ min-height: 44px;
3495
+ }
3496
+ .gemboss-ec-chip:hover { border-color: var(--gemboss-primary); color: var(--gemboss-active-fg); }
3497
+ .gemboss-ec-chip:disabled { opacity: 0.5; cursor: default; }
3498
+ /* ════ Mouse targets — the same 44px, bought with an overlay instead of a bigger box ═══════
3499
+ §9.1 asks 44px of a CONTROL, and it does not say "on touch only". The chat panel's controls are
3500
+ 28-37px under a mouse, which clears WCAG 2.5.8 AA (24px) and not 2.5.5 AAA (44px). The dense look
3501
+ is deliberate and worth keeping, so the TARGET grows and the ink does not — the same trick the
3502
+ editor topbar already uses on its device buttons.
3503
+
3504
+ WHICH ONES, AND WHY NOT THE REST. A target that reaches into a neighbour steals clicks meant for
3505
+ it, which is worse than a small one. So this was decided by measuring the gap between every pair
3506
+ of vertically adjacent controls at 390 / 768 / 1366 and asking whether BOTH could grow into it —
3507
+ a control's own clearance is not enough, because its neighbour is growing too. Measured
3508
+ 2026-09-09, three pairs could not:
3509
+
3510
+ starter chips, row to row gap 8px, both need 7 -> the ROW GAP goes to 16px below, which buys
3511
+ it without changing how the chips look
3512
+ textarea / engine picker gap 8px, needs 10 -> the textarea does not grow (see below)
3513
+ textarea / send gap 6px, needs 9 -> same
3514
+
3515
+ THE MESSAGE BOX IS EXCLUDED ON PURPOSE. It is a full-width field 37px tall: its AREA is already
3516
+ enormous, the only dimension short of 44 is its height, and it is the one control here you reach
3517
+ by clicking INTO text rather than at a point. Growing it also collides with both of its
3518
+ neighbours. With it left alone, everything else fits: send needs 5 of the 6px above it, the
3519
+ picker needs 8 of 8.
3520
+
3521
+ Height only. Horizontally these controls are already 34-196px wide, and an overlay that also grew
3522
+ sideways would collide with the chip beside it, which is 8px away. */
3523
+ @media (pointer: fine) {
3524
+ .gemboss-ec-starters { row-gap: 16px; }
3525
+ .gemboss-ec-tab,
3526
+ .gemboss-ec-starter,
3527
+ .gemboss-ec-upload,
3528
+ .gemboss-ec-enginepick button,
3529
+ .gemboss-ec-workmode,
3530
+ .gemboss-ec-input .gemboss-ec-attach-btn,
3531
+ .gemboss-ec-stop,
3532
+ /* Shares the toolbar row with Stop, which is already 44px tall — so a 44px overlay here grows into
3533
+ a row that is that tall already, and reaches into nothing above or below it. */
3534
+ .gemboss-ec-stop-send,
3535
+ .gemboss-ec-input button[type="submit"] { position: relative; }
3536
+ .gemboss-ec-tab::after,
3537
+ .gemboss-ec-starter::after,
3538
+ .gemboss-ec-upload::after,
3539
+ .gemboss-ec-enginepick button::after,
3540
+ /* `::before` on the work-mode wrapper: its `::after` is the invisible label that SIZES it, and a
3541
+ second use of the same pseudo-element would silently replace that. */
3542
+ .gemboss-ec-workmode::before,
3543
+ .gemboss-ec-input .gemboss-ec-attach-btn::after,
3544
+ .gemboss-ec-stop::after,
3545
+ .gemboss-ec-stop-send::after,
3546
+ .gemboss-ec-input button[type="submit"]::after {
3547
+ content: "";
3548
+ position: absolute;
3549
+ left: 0; right: 0;
3550
+ top: 50%;
3551
+ transform: translateY(-50%);
3552
+ height: 44px;
3553
+ /* No background at all: a pseudo-element is transparent already, and the `background` shorthand
3554
+ is banned in this family because it resets `background-image` — the work-mode caret. My own
3555
+ guard from #2158 caught this line, which is the guard doing exactly what it was written for. */
3556
+ pointer-events: auto;
3557
+ /* Above the panel chrome it overhangs — the chrome is not interactive, so a click landing on the
3558
+ control the overlay belongs to is the point.
3559
+ This does NOT rescue a control sitting flush against the top of `.gemboss-ec-scroll`: an
3560
+ overflow container clips its absolutely-positioned descendants, and z-index cannot cross that.
3561
+ Measured at 768px, where the first row of starter chips begins at the exact pixel the header
3562
+ ends: their target is 37px until the list scrolls, and 44px after. A scroll position, not a
3563
+ layout constant — the same chips measure 44px at 390, 1366 and 1920. Left alone rather than
3564
+ bought with padding on the scroll area, which every seller would see all the time to fix a
3565
+ target in one transient position. */
3566
+ z-index: 1;
3567
+ }
3568
+ }
3569
+ @media (pointer: coarse) {
3570
+ .gemboss-ec-input button[type="submit"] { width: 44px; height: 44px; font-size: 17px; }
3571
+ .gemboss-ec-starter, .gemboss-ec-opt { min-height: 44px; }
3572
+ .gemboss-ec-input textarea { min-height: 44px; }
3573
+ }
3574
+ /* ── The answer bubble's markdown ────────────────────────────────────────────────────────────────
3575
+ *
3576
+ * The bubble understood bold, links and line breaks. The models writing into it do not restrict
3577
+ * themselves to three things: a question about pages came back as a markdown TABLE and rendered as
3578
+ * literal pipes down the page. These are the styles for what it actually writes. */
3579
+
3580
+ .gemboss-md-p { margin: 0 0 8px; }
3581
+ .gemboss-md-p:last-child { margin-bottom: 0; }
3582
+ /* Demoted one level in the markup (h1 → h3) because the bubble sits under the page's own headings.
3583
+ * The SIZE hierarchy is kept here, because that is what a reader uses to skim. */
3584
+ .gemboss-md-h { margin: 16px 0 6px; font-weight: 650; line-height: 1.3; }
3585
+ .gemboss-md-h:first-child { margin-top: 0; }
3586
+ /* RESTORED after #3370 deleted these three. MarkdownLite.tsx builds the class by interpolation —
3587
+ * `gemboss-md-h gemboss-md-h${b.level}` — so the literal `gemboss-md-h1` exists in no source file
3588
+ * and in no compiled chunk, and all three of that PR's detection passes read them as dead. The gate
3589
+ * now carries the prefix rule that catches this shape. Do not "clean" these up. */
3590
+ .gemboss-md-h1 { font-size: 17px; }
3591
+ .gemboss-md-h2 { font-size: 15px; }
3592
+ .gemboss-md-h3 { font-size: var(--gemboss-fs-body); color: var(--gemboss-text-subdued, #616161); }
3593
+ .gemboss-md-list { margin: 0 0 8px; padding-left: 20px; }
3594
+ .gemboss-md-list li { margin: 2px 0; }
3595
+ .gemboss-md-quote {
3596
+ margin: 0 0 8px;
3597
+ padding: 2px 0 2px 12px;
3598
+ border-left: 3px solid var(--gemboss-border, #e3e3e3);
3599
+ color: var(--gemboss-text-subdued, #616161);
3600
+ }
3601
+ .gemboss-md-rule { margin: 12px 0; border: 0; border-top: 1px solid var(--gemboss-border, #e3e3e3); }
3602
+ /* Inline code: a chip, so an identifier stops reading as prose. */
3603
+ .gemboss-md-code {
3604
+ padding: 1px 6px;
3605
+ border-radius: 6px;
3606
+ background: var(--gemboss-surface-subdued, #f1f1f1);
3607
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3608
+ font-size: 0.9em;
3609
+ word-break: break-word;
3610
+ }
3611
+ /* Tables scroll INSIDE their own box. The chat lives in a 380px sidebar in the theme editor, and a
3612
+ * five-column table there would otherwise push the whole panel sideways — §9.1's rule that wide
3613
+ * content scrolls in its own container, not the page. */
3614
+ .gemboss-md-tablewrap { margin: 0 0 12px; overflow-x: auto; }
3615
+ .gemboss-md-table { border-collapse: collapse; font-size: var(--gemboss-fs-caption); min-width: 100%; }
3616
+ .gemboss-md-table th,
3617
+ .gemboss-md-table td {
3618
+ border: 1px solid var(--gemboss-border, #e3e3e3);
3619
+ padding: 6px 8px;
3620
+ text-align: left;
3621
+ vertical-align: top;
3622
+ white-space: nowrap;
3623
+ }
3624
+ .gemboss-md-table th { background: var(--gemboss-surface-subdued, #f7f7f7); font-weight: 600; }
3625
+ /* A picture in an answer (only from hosts markdown-blocks.ts trusts). Never wider than the bubble — the
3626
+ * chat can sit in a 380px sidebar — and capped in height so five thumbnails read as a shortlist, not
3627
+ * five screens. The link around it is the tap target: keyboard focus has to be visible on it. */
3628
+ .gemboss-md-imglink { display: inline-block; max-width: 100%; margin: 4px 0 8px; border-radius: 8px; line-height: 0; }
3629
+ .gemboss-md-imglink:focus-visible { outline: 2px solid var(--gemboss-focus, #005bd3); outline-offset: 2px; }
3630
+ .gemboss-md-img {
3631
+ display: block;
3632
+ max-width: 100%;
3633
+ max-height: 320px;
3634
+ height: auto;
3635
+ border: 1px solid var(--gemboss-border, #e3e3e3);
3636
+ border-radius: 8px;
3637
+ background: var(--gemboss-surface-subdued, #f7f7f7);
3638
+ object-fit: contain;
3639
+ }
3640
+ /* A code block, with the fold. A 200-line answer should not push everything after it off screen. */
3641
+ .gemboss-md-codeblock {
3642
+ position: relative;
3643
+ margin: 0 0 12px;
3644
+ border: 1px solid var(--gemboss-border, #e3e3e3);
3645
+ border-radius: 8px;
3646
+ background: var(--gemboss-surface-subdued, #f7f7f7);
3647
+ overflow: hidden;
3648
+ }
3649
+ .gemboss-md-codelang {
3650
+ display: block;
3651
+ padding: 4px 12px 0;
3652
+ color: var(--gemboss-text-subdued, #8a8a8a);
3653
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3654
+ font-size: 11px;
3655
+ }
3656
+ .gemboss-md-codeblock pre { margin: 0; padding: 8px 12px; overflow-x: auto; }
3657
+ .gemboss-md-codeblock code {
3658
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
3659
+ font-size: 12px;
3660
+ line-height: 1.5;
3661
+ white-space: pre;
3662
+ }
3663
+ .gemboss-md-codemore {
3664
+ display: block;
3665
+ width: 100%;
3666
+ padding: 6px 12px;
3667
+ border: 0;
3668
+ border-top: 1px solid var(--gemboss-border, #e3e3e3);
3669
+ background: transparent;
3670
+ color: var(--gemboss-text-subdued, #616161);
3671
+ font-size: 12px;
3672
+ text-align: left;
3673
+ cursor: pointer;
3674
+ }
3675
+ .gemboss-md-codemore:hover { background: rgba(0, 0, 0, 0.03); }
3676
+ /* ── The payload, live, in the chat ──────────────────────────────────────────────────────────────
3677
+ A trace row said `apply_section_edit(section_id:"hero")` — that something happened, and nothing about
3678
+ what. This is what. Open by default: folding it would put the answer back behind a click. */
3679
+ .gemboss-narr-row.source { align-items: flex-start; }
3680
+ .gemboss-narr-source { min-width: 0; flex: 1; }
3681
+ .gemboss-narr-source-head { font-size: 11px; color: var(--gemboss-text-muted, #686b71); margin-bottom: 4px; }
3682
+ .gemboss-narr-source-head code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
3683
+ .gemboss-narr-source-code {
3684
+ margin: 0; padding: 8px 12px; border-radius: 8px; max-height: 220px; overflow: auto;
3685
+ background: var(--gemboss-primary-soft, #f2f3f5); border: 1px solid var(--gemboss-divider, #e5e7ea);
3686
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: var(--gemboss-fs-micro); line-height: 17px;
3687
+ white-space: pre; tab-size: 2;
3688
+ }
3689
+ /* Light-ground syntax colours for the payload rows (the code-highlight.ts `c-*` spans). Contrast checked
3690
+ against #fff: every one clears 4.5:1. (The page editor's Code tab shared them until it was removed, #2651.) */
3691
+ .gemboss-narr-source-code .c-com { color: #6a7a86; font-style: italic; }
3692
+ .gemboss-narr-source-code .c-str { color: #9a3412; }
3693
+ .gemboss-narr-source-code .c-num { color: #1d6fa5; }
3694
+ .gemboss-narr-source-code .c-kw { color: #7c3aed; font-weight: 600; }
3695
+ .gemboss-narr-source-code .c-key { color: #0b6b3a; }
3696
+ .gemboss-narr-source-code .c-tag { color: #b0197a; }
3697
+ .gemboss-narr-source-code .c-attr{ color: #7c3aed; }
3698
+ .gemboss-narr-source-code .c-pun { color: #6a6d73; }
3699
+ @media (max-width: 520px) {
3700
+ .gemboss-narr-source-code { max-height: 160px; font-size: 11px; }
3701
+ }
3702
+ /* A checklist row's box IS its marker; the dot column stays for alignment and shows nothing. */
3703
+ .gemboss-narr-dot-quiet { visibility: hidden; }
3704
+
3705
+ /* ═══ @gemboss/ui src/chat-composer.css ═══ */
3706
+ /*
3707
+ * The chat COMPOSER — @gemboss/ui (moved from apps/admin/src/components/chat/chat-composer.css, 02/10/2026).
3708
+ *
3709
+ * The toolbar row under the text box (chips lead, tools, status, the one action pinned right), the one-row
3710
+ * layout when there are no chips, the engine picker's layout and the Stop button. A second surface needs the
3711
+ * toolbar — gemcare's assistant composer is a text box and a send button, i.e. the one-row case — and a copy
3712
+ * would be a second owner. The box itself (`form.gemboss-ec-input`, its textarea and send button) is in chat.css.
3713
+ *
3714
+ * A SEPARATE file from chat.css on purpose, as it was a separate file in the admin: chat.css rides the shell's
3715
+ * first load on every screen, while this sheet follows the chat chunk — the admin imports it from ChatComposer
3716
+ * and EnginePicker, not from the layout (the shell sat at 239.1 / 240 KB brotli when this moved). Other surfaces
3717
+ * get it inside gemboss-ui.css. Rules and their order are unchanged.
3718
+ *
3719
+ * Every selector here names a composer-only class (chiplabel · chipmark · chipsub · enginepick · enginepick-btn ·
3720
+ * stop · toolbar · toolbar-lead · toolbar-tools), so a rule only matches while a composer is on screen.
3721
+ */
3722
+
3723
+ /* The toolbar row: chips on the left, then status, then the one action button pinned right.
3724
+ `align-items: center` — the old row was `flex-end`, so 28px chips sat on the BOTTOM edge of a 44px
3725
+ button and their centres ran 8px below its centre. Measured at 1366: chips cy 819, send cy 811.
3726
+ The lead group is the only thing that shrinks. Send is `flex: none` and the chips inside the lead
3727
+ carry `min-width: 0`, so a narrow composer ellipsises a label instead of pushing send off the row. */
3728
+ .gemboss-ec-toolbar { flex: 1 0 100%; display: flex; align-items: center; gap: 4px; min-width: 0; }
3729
+ .gemboss-ec-toolbar-lead { flex: 1 1 auto; display: flex; align-items: center; gap: 4px; min-width: 0; }
3730
+ /* The lead GROWS, and that is what pins status + action to the right edge. With nothing in it (a
3731
+ browser tab has no engine picker, a driver without work mode has no select) it is removed, and
3732
+ whatever follows takes the auto margin instead. */
3733
+ .gemboss-ec-toolbar-lead:empty { display: none; }
3734
+ .gemboss-ec-toolbar-lead:empty + * { margin-left: auto; }
3735
+ /* TOOLS ARE NOT CHIPS. The 📎 lives here, never in the lead: the composer gate reads `leading` as the
3736
+ chip strip twice over — `edge` wants the first chip's text flush with the typed text, and `oneRow`
3737
+ only runs when the lead is EMPTY. A button parked in the lead pushed the chips 47px right AND left
3738
+ `oneRow` unable to fail at all; the gate's own negative control said so out loud
3739
+ ("MÙ với oneRow — đối chứng ĐÃ QUAN SÁT ĐƯỢC và vẫn xanh") once the breakage stopped registering.
3740
+ `flex: none` so it never steals the shrink budget the lead needs to ellipsise a long chip label. */
3741
+ .gemboss-ec-toolbar-tools { display: flex; align-items: center; gap: 4px; flex: none; min-width: 0; }
3742
+ .gemboss-ec-toolbar-tools:empty { display: none; }
3743
+ /* NO CHIPS ⇒ ONE ROW. Product Studio has nothing in its lead, and it still spent a whole 44px row on
3744
+ a lone send button beside empty space — 99-120px tall for a one-line box. The text box shares the
3745
+ row now and send sits at its bottom edge, where a growing message keeps it. */
3746
+ .gemboss-ec-input:has(> .gemboss-ec-toolbar > .gemboss-ec-toolbar-lead:empty) { flex-wrap: nowrap; align-items: flex-end; }
3747
+ .gemboss-ec-input:has(> .gemboss-ec-toolbar > .gemboss-ec-toolbar-lead:empty) > textarea { flex: 1 1 0; }
3748
+ .gemboss-ec-input:has(> .gemboss-ec-toolbar > .gemboss-ec-toolbar-lead:empty) > .gemboss-ec-toolbar { flex: 0 0 auto; }
3749
+ /* The engine picker is a `gemboss-ec-chipctl` like the select beside it — its colour, weight, padding
3750
+ and hover all come from that one rule. What is left here is layout: Popover wraps the activator in an
3751
+ element of its own, and the chip can only shrink if every box between it and the toolbar can.
3752
+ DESCENDANT, not child, for that reason — `>` matched nothing when the button sat inside Polaris'
3753
+ wrapper, measured, after the first attempt left it unstyled. */
3754
+ .gemboss-ec-enginepick { flex: 0 3 auto; min-width: 0; display: inline-flex; }
3755
+ .gemboss-ec-enginepick > * { min-width: 0; max-width: 100%; display: flex; }
3756
+ .gemboss-ec-enginepick button {
3757
+ min-width: 0; max-width: 100%;
3758
+ /* The same caret the work-mode select draws, at the same inset, so the two chips read as one type. */
3759
+ padding-right: 24px;
3760
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
3761
+ background-repeat: no-repeat;
3762
+ background-position: right 7px center;
3763
+ }
3764
+ .gemboss-ec-chiplabel { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3765
+ .gemboss-ec-chipmark { display: none; }
3766
+ .gemboss-ec-chipmark .gemboss-engine-mark { display: block; width: 16px; height: 16px; margin: 2px 0; }
3767
+ /* The steps ChatComposer measured its way down to (data-density on the form). The ellipsis above stays
3768
+ as the floor for an engine with no mark, never as the plan. */
3769
+ .gemboss-ec-input:not([data-density="full"]) .gemboss-ec-chipsub { display: none; }
3770
+ .gemboss-ec-input:is([data-density="icon"], [data-density="tight"]) .gemboss-ec-enginepick-btn.has-mark .gemboss-ec-chiplabel { display: none; }
3771
+ .gemboss-ec-input:is([data-density="icon"], [data-density="tight"]) .gemboss-ec-enginepick-btn.has-mark .gemboss-ec-chipmark { display: block; }
3772
+ /* A mark does not ellipsise, so once the chip IS the mark it holds its width — shrunk further it slid
3773
+ under the mode chip beside it. The overflow that leaves is what tells ChatComposer to step on. */
3774
+ .gemboss-ec-input:is([data-density="icon"], [data-density="tight"]) .gemboss-ec-enginepick:has(.has-mark) { flex-shrink: 0; }
3775
+ /* Open: it is showing a popover, so it stops being quiet. */
3776
+ .gemboss-ec-enginepick button[aria-expanded="true"] {
3777
+ color: var(--gemboss-text); background-color: var(--gemboss-hover, rgba(0,0,0,0.05));
3778
+ }
3779
+ @media (pointer: coarse) {
3780
+ /* SCOPED, and that word is load-bearing — this line was DEAD as `.gemboss-ec-stop`.
3781
+ A media query adds no specificity, so the unscoped rule for the same class, declared ~1800 lines
3782
+ LATER at its mouse size, beat this one on source order alone. Measured
3783
+ 2026-09-09 in a real coarse-pointer context: `matchMedia('(pointer: coarse)').matches === true`
3784
+ and the computed width unchanged at its mouse size. Every phone got a Stop button ten pixels
3785
+ under the target, while the rule meant to
3786
+ fix that sat here reading as done — and the source test beside this block could not see it,
3787
+ because a name being PRESENT is not the same fact as a rule WINNING.
3788
+ `.gemboss-ec-input` is the same idiom the attach button above already uses for the same reason. */
3789
+ .gemboss-ec-input .gemboss-ec-stop { width: 44px; height: 44px; min-width: 44px; }
3790
+ /* SCOPED for the reason above: this control's own look is declared later, in the shell sheet. */
3791
+ .gemboss-ec-input .gemboss-ec-stop-send { min-height: 44px; }
3792
+ .gemboss-ec-enginepick button { min-height: 44px; }
3793
+ }
3794
+ /* Sized like the send button it replaces, so the composer does not reflow when a turn starts —
3795
+ * a control that moves under the cursor is a control people misclick. So it is THE SAME CIRCLE AS SEND
3796
+ * — same 44px, same 50% radius, only the colour and the glyph change. It used to be a 32px red
3797
+ * rounded square beside a 44px round send: two shapes, two sizes, one slot. */
3798
+ .gemboss-ec-stop {
3799
+ flex: 0 0 auto;
3800
+ display: inline-flex;
3801
+ align-items: center;
3802
+ justify-content: center;
3803
+ width: 44px;
3804
+ height: 44px;
3805
+ min-width: 44px;
3806
+ border: none;
3807
+ border-radius: 50%;
3808
+ background: var(--gemboss-critical, #d72c0d);
3809
+ color: #fff;
3810
+ cursor: pointer;
3042
3811
  }
3812
+ .gemboss-ec-stop svg { width: 12px; height: 12px; fill: currentColor; }
3813
+ .gemboss-ec-stop:disabled { opacity: 0.6; cursor: default; }
3814
+ .gemboss-ec-stop:hover { filter: brightness(1.08); }
3815
+ .gemboss-ec-stop:focus-visible { outline: 2px solid var(--gemboss-focus, #005bd3); outline-offset: 2px; }