@gemboss/ui 0.12.0 → 0.21.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/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@gemboss/ui",
3
- "version": "0.12.0",
3
+ "version": "0.21.0",
4
4
  "description": "GemBoss design system for every surface in the fleet: the admin shell (topbar + rail + drawer) as React components, and the admin stylesheet exported as one CSS file.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "sideEffects": [
8
8
  "*.css",
9
- "./src/kit-plain.js"
9
+ "./src/kit-plain.js",
10
+ "./src/editor-plain.js"
10
11
  ],
11
12
  "main": "./src/index.ts",
12
13
  "types": "./src/index.ts",
@@ -21,6 +22,14 @@
21
22
  "./chat.css": "./src/chat.css",
22
23
  "./chat-composer.css": "./src/chat-composer.css",
23
24
  "./editor.css": "./src/editor.css",
25
+ "./collections.css": "./src/collections.css",
26
+ "./reactions.css": "./src/reactions.css",
27
+ "./reactions-data": "./src/reactions-data.ts",
28
+ "./editor": "./src/editor/index.ts",
29
+ "./editor/swatch": "./src/editor/swatch.ts",
30
+ "./segmented": "./src/segmented.tsx",
31
+ "./editor/css-url": "./src/editor/css-url.ts",
32
+ "./editor-plain.js": "./src/editor-plain.js",
24
33
  "./auth.css": "./src/auth.css",
25
34
  "./kit-plain.css": "./src/kit-plain.css",
26
35
  "./kit-plain.js": "./src/kit-plain.js",
@@ -37,18 +46,21 @@
37
46
  "src/**/*.tsx",
38
47
  "src/*.css",
39
48
  "src/kit-plain.js",
49
+ "src/editor-plain.js",
40
50
  "src/icon.svg",
41
51
  "!src/**/*.test.*",
42
52
  "gemboss-ui.css",
43
53
  "catalog/index.html",
54
+ "catalog/editor.html",
44
55
  "README.md",
45
56
  "CHANGELOG.md"
46
57
  ],
47
58
  "scripts": {
48
59
  "build:css": "node scripts/build-css.mjs",
49
- "prepack": "node scripts/build-css.mjs && node scripts/build-catalog.mjs",
60
+ "stage": "node scripts/stage.mjs",
61
+ "weight": "node scripts/weight-gate.mjs",
50
62
  "typecheck": "tsc -p tsconfig.json --noEmit",
51
- "catalog": "node scripts/build-css.mjs && node scripts/build-catalog.mjs",
63
+ "catalog": "node scripts/build-css.mjs && node scripts/build-catalog.mjs && node scripts/build-catalog-editor.mjs",
52
64
  "fleet": "node scripts/fleet.mjs"
53
65
  },
54
66
  "peerDependencies": {
package/src/app-shell.css CHANGED
@@ -1,256 +1 @@
1
- /* Styles for the parts of the shell the PACKAGE owns — `AppShell`'s three topbar slots,
2
- * `RailHeading`, `RailCount`. Everything else (rail, topbar, drawer, tokens, kit) is the admin's
3
- * own CSS, exported by apps/admin/scripts/export-ui-css.mjs and concatenated ahead of this file into
4
- * `gemboss-ui.css`. The admin does not load this file: it composes the shell itself and has no
5
- * slots, headings or pills of this kind.
6
- *
7
- * Built only from --gemboss-* tokens, so a palette change in gemboss-tokens.css reaches it. */
8
-
9
- /* The base font. The admin's comes from Polaris, which no other surface loads, so without this rule
10
- * every surface on gemboss-ui.css rendered in the browser's default serif. Same elements Polaris
11
- * sets (html, body, button), same stack (--gemboss-font-sans resolves to Polaris's
12
- * --p-font-family-sans fallback). `:where()` keeps it at (0,0,0): any rule a surface writes for
13
- * these elements wins, wherever it sits in the cascade. Re-theme with the token, not a body rule.
14
- * The admin does not load this file, so its own rendering is untouched. */
15
- :where(html, body, button) { font-family: var(--gemboss-font-sans); }
16
- /* …and the rest of Polaris' body text (02/10/2026): bodyMd, 13px on a 20px line, weight 450, the text colour,
17
- * antialiased. With the face alone, every surface picked its own size — measured on the sign-in pages that day:
18
- * 14, 15 and 16px bodies, one in Lora serif — so every line outside a kit class read differently from the admin.
19
- * Same (0,0,0) as above: a surface that sets its own body text still wins, and should delete that rule.
20
- * BODY only, never `html`: the root size is what `rem` means, and gemwatcher alone has ~200 rem lengths that a 13px
21
- * root would shrink by a fifth (review #3665). Polaris does the same: html stays at the browser's 100%. */
22
- :where(body) {
23
- font-size: 13px; line-height: 20px; font-weight: 450; color: var(--gemboss-text);
24
- -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
25
- }
26
- /* Headings get their own line (Polaris heading line-heights are ~1.25-1.4): inheriting the body's 20px made a wrapped 22px
27
- * title overlap itself (gempress, 0.5.1). */
28
- :where(h1, h2, h3) { letter-spacing: -0.01em; line-height: 1.3; }
29
- /* 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,
30
- * because Polaris' global sheet sets `h1…h6, p { margin: 0; font-size: 1em; font-weight: regular }` and every heading
31
- * that looks like one is a Text variant. Surfaces had the browser's instead: a bare <h2> at 19.5px bold with 16px margins
32
- * sat in GemFactory and taskdesk pages beside kit cards titled 14px. Here, only INSIDE the shell (`.gemboss-platform-
33
- * shell`, the AppShell root every surface on the kit renders): a login page or a public page keeps its own type. Never
34
- * inside an article the surface shows: `.ui-prose` or `[data-content]` (the contract), and the wrappers the fleet already
35
- * uses for rendered HTML, read on 05/10/2026, by exact name (gemcommunity `md-body` and `recap-md`,
36
- * 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
37
- * `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
38
- * browser's bold and margins (a zero-specificity author rule still beats the browser's own sheet, review #3787). */
39
- :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) *) {
40
- margin: 0; font-size: 1em; font-weight: inherit; line-height: inherit;
41
- }
42
-
43
- .gemboss-topbar-start { display: flex; align-items: center; gap: 8px; min-width: 0; }
44
- .gemboss-topbar-center { display: flex; justify-content: center; min-width: 0; }
45
- .gemboss-topbar-end { display: flex; align-items: center; justify-content: flex-end; gap: 8px; min-width: 0; }
46
- @media (max-width: 760px) {
47
- /* The topbar is one flex row here: the middle slot takes what is left. Basis 0, not auto: with `auto` the search's own
48
- * width (up to 520px) was its claim, so the other slots had to give way to it (0.6.0 pinned them instead, see below). With
49
- * 0 it only takes what the others leave, down to a 96px floor: an input with its icon still reads as search. */
50
- .gemboss-topbar-center { flex: 1 1 0; min-width: 96px; justify-content: flex-start; }
51
- .gemboss-topbar-center:empty { min-width: 0; }
52
- /* No actions: the empty slot would still take one flex `gap` (8px) off the search at 320px. */
53
- .gemboss-topbar-end:empty { display: none; }
54
- /* The actions never give: with `min-width: 0` the slot shrank and its buttons (justify flex-end) spilled LEFT over the
55
- * search (assistant + account, 15px over partner-radar's search at 320 and 360, 0.6.0).
56
- * The start slot gives LAST, after the search reaches its floor: the brand name ellipsizes (.gb-brand-name) instead of
57
- * the account chip leaving the screen. 0.6.0 pinned it (`flex: 0 0 auto`) and a 30-character brand at 461px pushed the
58
- * chip 44px past the right edge (review #3690). The menu button and the mark keep their 44px. */
59
- .gemboss-topbar-end { flex: 0 0 auto; }
60
- .gemboss-topbar-start { flex: 0 1 auto; }
61
- .gemboss-topbar-start > .gemboss-mobile-menu-btn { flex: 0 0 auto; }
62
- .gemboss-topbar-start > .gb-brand { min-width: 44px; }
63
- }
64
-
65
- .gnav-heading {
66
- padding: 10px 12px 4px;
67
- font-size: var(--gemboss-fs-micro); font-weight: 600; line-height: 1.2;
68
- letter-spacing: 0.08em; text-transform: uppercase;
69
- color: var(--gemboss-text-muted);
70
- }
71
- .gnav-scroll > .gnav-heading:first-child,
72
- .gnav-scroll > .gnav-section:first-child > .gnav-heading:first-child { padding-top: 2px; }
73
-
74
- .gnav-count {
75
- margin-left: auto; flex: 0 0 auto; min-width: 20px; padding: 0 6px;
76
- border-radius: var(--gemboss-radius-full);
77
- background: var(--gemboss-hover); color: var(--gemboss-text-muted);
78
- font-size: var(--gemboss-fs-micro); font-weight: 700; line-height: 18px;
79
- text-align: center; font-variant-numeric: tabular-nums;
80
- }
81
- .gnav-item.active .gnav-count { background: var(--gemboss-primary); color: var(--gemboss-on-primary); }
82
-
83
- /* ── Navigation pending (0.10.0): a click answers on the first frame ─────────────────────────────────────────────
84
- * Chris 05/10/2026: content-radar's rail (the icon turns into a spinner the moment you click, sub items too) is the
85
- * standard for every surface. Measured 04/10 on content-radar with a server ~280 ms away: nothing on screen changed
86
- * for 321-503 ms after a click; with this, 6-8 ms.
87
- *
88
- * ONE state, three ways to set it, so a React rail, a Next rail and a server-rendered rail look the same:
89
- * - `aria-busy="true"` on the item: RailItem / RailSubItem `pending` (React), kit-plain.js on a followed link;
90
- * - a `.gnav-pending` child: NavLink from `@gemboss/ui/next` (Next's useLinkStatus is only readable INSIDE the
91
- * link, so the link cannot set its own attribute; it renders this status text and `:has()` reads it).
92
- * `:is()` is forgiving: a browser without `:has()` still honours the aria-busy half of each rule.
93
- * The icon keeps its box (visibility, not display), so the label never moves. Here and not in shell.css: the
94
- * admin's shell sheet has <1 KB of first-load headroom, and the admin answers a click with its own holding screen.
95
- */
96
- .gnav-item:is([aria-busy="true"], :has(> .gnav-pending)),
97
- .gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending)) { cursor: progress; }
98
- .gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico > * { visibility: hidden; }
99
- .gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico::after,
100
- .gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after,
101
- .gb-pending-mark {
102
- content: ""; display: inline-block; vertical-align: middle; grid-area: 1 / 1; flex: none; box-sizing: border-box;
103
- width: 20px; height: 20px; border-radius: 50%;
104
- /* the item's own ink, so the spinner follows the active and hover colours */
105
- border: 2px solid color-mix(in srgb, currentColor 18%, transparent); border-top-color: currentColor;
106
- animation: gnav-spin 0.7s linear infinite;
107
- }
108
- /* 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
109
- * it would be a third item in a space-between row and push a trailing count to the middle (review #3777). */
110
- .gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after {
111
- position: absolute; left: 22px; top: 50%; margin-top: -6px; width: 12px; height: 12px; border-width: 1.5px;
112
- }
113
- @keyframes gnav-spin { to { transform: rotate(360deg); } }
114
- /* Slower, never off: it is the only sign that the click was heard. */
115
- @media (prefers-reduced-motion: reduce) {
116
- .gnav-item:is([aria-busy="true"], :has(> .gnav-pending)) > .gnav-ico::after,
117
- .gnav-subitem:is([aria-busy="true"], :has(> .gnav-pending))::after,
118
- .gb-pending-mark { animation-duration: 2s; }
119
- }
120
- /* A link in the PAGE (a list row, a card, a tab) opts in with `data-gb-pending`: PendingLink (`@gemboss/ui/next`) or
121
- * kit-plain.js put `span.gb-pending-mark` (role=status) inside it while the page loads. "inline" (default): 12px after
122
- * the text. "corner": 16px in the top right corner, out of the flow, for a link that wraps a whole card (a grid card
123
- * gains no column). Marked by attribute, not class: a surface that colours bare links with `a:not([class])` keeps it. */
124
- a[data-gb-pending]:has(> .gb-pending-mark), a[data-gb-pending][aria-busy="true"] { cursor: progress; }
125
- a[data-gb-pending] > .gb-pending-mark { display: inline-block; width: 12px; height: 12px; border-width: 1.5px; margin-left: 6px; vertical-align: -1px; }
126
- a[data-gb-pending="corner"] { position: relative; }
127
- a[data-gb-pending="corner"] > .gb-pending-mark { position: absolute; top: 12px; right: 12px; width: 16px; height: 16px; margin: 0; }
128
- /* RouteHolding (./route-holding.tsx): a route group's loading boundary. The kit's skeleton lines in the content column,
129
- * one shape for every route: a title bar, then four lines of falling length (the admin's holding screen). */
130
- .gb-route-holding { padding-top: 4px; }
131
- .gb-route-holding .ui-skeleton { gap: 16px; }
132
- .gb-route-holding__heading { margin: 0 0 20px; font-size: 20px; font-weight: 650; line-height: 1.3; color: var(--gemboss-text); }
133
- .gb-route-holding .ui-skeleton__line { height: 10px; }
134
- .gb-route-holding .ui-skeleton__line.gb-route-holding__title { height: 20px; width: 34%; margin-bottom: 8px; }
135
- .gb-route-holding .ui-skeleton__line:nth-last-child(4) { width: 46%; }
136
- .gb-route-holding .ui-skeleton__line:nth-last-child(3) { width: 92%; }
137
- .gb-route-holding .ui-skeleton__line:nth-last-child(2) { width: 78%; }
138
- .gb-route-holding .ui-skeleton__line:nth-last-child(1) { width: 61%; }
139
- /* RouteCover: the same frame over the content area from the click until the navigation commits. Shown only while
140
- * `html[data-gb-navigating]` (set by the @gemboss/ui/next links while pending). No grace delay: the target is the new
141
- * 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
142
- * 80 ms fade is what a router cache hit (20-90 ms) sees of it: a faint, partial frame, not a full flash.
143
- * z 5: above the outgoing page, below the rail, the topbar (300) and the assistant (290). */
144
- .gb-route-cover { display: none; }
145
- html[data-gb-navigating] .gb-route-cover {
146
- display: block; position: fixed; inset: 56px 0 0 240px; z-index: 5; overflow: hidden; box-sizing: border-box;
147
- padding: 24px; background: var(--gemboss-app-bg); pointer-events: none;
148
- animation: gb-route-cover-in 0.08s ease-out both;
149
- }
150
- @keyframes gb-route-cover-in { from { opacity: 0; } to { opacity: 1; } }
151
- @media (max-width: 760px) { html[data-gb-navigating] .gb-route-cover { left: 0; padding: 16px; } }
152
- @media (prefers-reduced-motion: reduce) { html[data-gb-navigating] .gb-route-cover { animation-duration: 0s; } }
153
- /* The status text a NavLink renders inside a rail item: read, not seen. */
154
- .gnav-pending {
155
- position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
156
- overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
157
- }
158
-
159
- /* ── BrandLockup (./brand.tsx) ─────────────────────────────────────────────────────────────────
160
- * The admin's `.gemboss-brand-lockup` box (40px tall, 8px padding, hover wash) around the wordmark's
161
- * own proportions. Not that class itself: its ≤460px rule swaps in a background image at an
162
- * admin-only path, which every other host would 404. Ink is the logo file's #14151a. */
163
- .gb-brand {
164
- min-width: 0; height: 40px; margin: 0; padding: 0 8px;
165
- border-radius: var(--gemboss-radius-md);
166
- display: inline-flex; align-items: center; gap: 4.5px;
167
- color: var(--gb-brand-ink, #14151a); text-decoration: none;
168
- transition: background-color var(--gemboss-transition-base) ease;
169
- }
170
- .gb-brand:hover { background: var(--gemboss-hover); }
171
- .gb-brand:focus-visible { outline: 2px solid var(--gemboss-primary); outline-offset: 2px; }
172
- .gb-brand-gem { display: block; flex: 0 0 auto; }
173
- .gb-brand-name {
174
- font-family: Inter, var(--gemboss-font-sans); font-size: 13.3px; font-weight: 700;
175
- letter-spacing: -0.13px; line-height: 1; white-space: nowrap;
176
- overflow: hidden; text-overflow: ellipsis; min-width: 0;
177
- }
178
- /* The admin shows only its mark below 460px; so does this. */
179
- @media (max-width: 460px) {
180
- .gb-brand-name { display: none; }
181
- /* The mark alone is ~21px wide: a 44px target, centred (visual-gate tap_target_link on content-radar at 320, 0.5.1). */
182
- .gb-brand { min-width: 44px; height: 44px; justify-content: center; }
183
- }
184
-
185
- /* ── AccountMenu (./account.tsx): its rules live in shell.css since 29/09/2026, because the admin
186
- * renders AccountMenu too now and loads shell.css, not this file. */
187
-
188
- /* ── Topbar search (TopbarSearch, search.tsx; 0.6.3) ────────────────────────────────────────────────────────
189
- The admin's "Search ⌘K" box as a real form for AppShell's `center` slot. Same metrics as the admin's
190
- .gemboss-command-search (gemboss-shell.css): 36px under a mouse, 44px under a finger (pinned in the admin's
191
- kit-controls-reach-44.test.ts), 18px icon, 13px text, the shortcut hint at the right. Field tokens, so it
192
- wears the same box as every other text field in the kit. */
193
- .gemboss-topbar-search {
194
- width: 100%;
195
- max-width: 560px;
196
- height: 36px;
197
- min-width: 0;
198
- margin: 0 auto;
199
- border: 1px solid var(--gemboss-field-line);
200
- border-radius: var(--gemboss-radius-md);
201
- background: var(--gemboss-field-bg);
202
- padding: 0 8px 0 12px;
203
- display: grid;
204
- grid-template-columns: 18px minmax(0, 1fr) auto;
205
- gap: 8px;
206
- align-items: center;
207
- color: var(--gemboss-text-muted);
208
- }
209
- .gemboss-topbar-search:hover { border-color: var(--gemboss-field-line-hover); background: var(--gemboss-field-bg-hover); }
210
- .gemboss-topbar-search:focus-within { border-color: var(--gemboss-field-line-active); background: var(--gemboss-surface); }
211
- .gemboss-topbar-search-icon { width: 18px; height: 18px; color: currentColor; }
212
- /* The INPUT is the tap target, not the box: the box is a <form>, and a tap on its icon or border does not focus
213
- anything. Inside a 1px border, `height: 100%` left the input 42 of the box's 44 (visual-gate tap_target_size at
214
- 320/390/768 on GemFactory, 03/10/2026). It reaches over both border lines; transparent, so the border still shows. */
215
- .gemboss-topbar-search-input {
216
- min-width: 0; height: calc(100% + 2px); border: 0; background: transparent; padding: 0; margin: -1px 0;
217
- font: inherit; font-size: var(--gemboss-fs-body); line-height: 18px; color: var(--gemboss-field-ink); outline: none;
218
- }
219
- .gemboss-topbar-search-input::placeholder { color: var(--gemboss-field-placeholder); opacity: 1; }
220
- .gemboss-topbar-search-kbd {
221
- border: 1px solid var(--gemboss-field-line); border-radius: var(--gemboss-radius-sm); background: var(--gemboss-surface);
222
- padding: 1px 6px; color: var(--gemboss-text-muted); font-family: inherit; font-size: var(--gemboss-fs-micro); line-height: 16px;
223
- }
224
- .gemboss-topbar-search:focus-within .gemboss-topbar-search-kbd { display: none; }
225
- /* 44 under a finger AND at phone width (0.6.4, 03/10/2026): the same pair of conditions the kit's own controls use
226
- (kit-plain.css, `max-width: 47.9975em`). Under `pointer: coarse` alone, a narrow desktop window and every visual-gate
227
- run measured the box at 36 and failed `tap_target_size` at 320/390/768 on GemFactory /framework. */
228
- @media (pointer: coarse), (max-width: 47.9975em) { .gemboss-topbar-search { height: 44px; } }
229
- /* A phone has no ⌘ key, and the box needs every pixel the bar has left. */
230
- @media (max-width: 460px) { .gemboss-topbar-search-kbd { display: none; } }
231
-
232
- /* ── Page width (0.8.1) ──────────────────────────────────────────────────────────────────────────────
233
- * Chris 04/10/2026: the rail on the left, and on the right a content column with ONE max width across every
234
- * service, not edge to edge. The admin's tiers (gemboss-shell.css, `.gemboss-w-*` around a Polaris Page), same
235
- * numbers; `page-width.test.tsx` fails if the two disagree:
236
- * .ui-page → --ui-content-max (900px, ui.css): lists, tables, card grids, dashboards
237
- * .ui-page--feed → 720px: read top to bottom (activity, logs, timelines)
238
- * .ui-page--form → 662px: one column of fields (Polaris narrowWidth)
239
- * .ui-page--canvas → 1440px: content measured to be wider than a reading column (admin: the funnel path)
240
- * .ui-page--full → no cap: a board or an editor that is the screen. Say why where you use it.
241
- * A cap only exists where it is SMALLER than the room it sits in. Right of the 240px rail with main's 24px padding,
242
- * 900 bites from 1188px up (1280 → 992 of room); 1440 bites only from ~1730px. Below that the column is simply the
243
- * room, so the tier never causes a horizontal scroll.
244
- * Here, not in ui.css: ui.css is exported verbatim into the admin under a 90 KB budget (89.8 KB at 0.7.3), and the
245
- * admin draws its tiers on Polaris' Page instead. Fixed px, not %: a % cap grows the line length with the monitor. */
246
- .ui-page {
247
- box-sizing: border-box;
248
- width: 100%;
249
- min-width: 0;
250
- max-width: var(--ui-content-max, 900px);
251
- margin-inline: auto;
252
- }
253
- .ui-page--feed { max-width: 720px; }
254
- .ui-page--form { max-width: 662px; }
255
- .ui-page--canvas { max-width: 1440px; }
256
- .ui-page--full { max-width: none; }
1
+ :where(html,body,button){font-family:var(--gemboss-font-sans)}:where(body){font-size:13px;line-height:20px;font-weight:450;color:var(--gemboss-text);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}:where(h1,h2,h3){letter-spacing:-.01em;line-height:1.3}:where(.gemboss-platform-shell) :where(h1,h2,h3,h4,h5,h6,p):where(:not([class])):not(:where(.ui-prose,[data-content],[class*=prose i],[class*=markdown i],.md-body,.recap-md,.asst-md,.article-body) *){margin:0;font-size:1em;font-weight:inherit;line-height:inherit}.gemboss-topbar-start{display:flex;align-items:center;gap:8px;min-width:0}.gemboss-topbar-center{display:flex;justify-content:center;min-width:0}.gemboss-topbar-end{display:flex;align-items:center;justify-content:flex-end;gap:8px;min-width:0}@media (max-width: 760px){.gemboss-topbar-center{flex:1 1 0;min-width:96px;justify-content:flex-start}.gemboss-topbar-center:empty{min-width:0}.gemboss-topbar-end:empty{display:none}.gemboss-topbar-end{flex:0 0 auto}.gemboss-topbar-start{flex:0 1 auto}.gemboss-topbar-start>.gemboss-mobile-menu-btn{flex:0 0 auto}.gemboss-topbar-start>.gb-brand{min-width:44px}}.gnav-heading{padding:10px 12px 4px;font-size:var(--gemboss-fs-micro);font-weight:600;line-height:1.2;letter-spacing:.08em;text-transform:uppercase;color:var(--gemboss-text-muted)}.gnav-scroll>.gnav-heading:first-child,.gnav-scroll>.gnav-section:first-child>.gnav-heading:first-child{padding-top:2px}.gnav-count{margin-left:auto;flex:0 0 auto;min-width:20px;padding:0 6px;border-radius:var(--gemboss-radius-full);background:var(--gemboss-hover);color:var(--gemboss-text-muted);font-size:var(--gemboss-fs-micro);font-weight:700;line-height:18px;text-align:center;font-variant-numeric:tabular-nums}.gnav-item.active .gnav-count{background:var(--gemboss-primary);color:var(--gemboss-on-primary)}.gnav-item:is([aria-busy=true],:has(>.gnav-pending)),.gnav-subitem:is([aria-busy=true],:has(>.gnav-pending)){cursor:progress}.gnav-item:is([aria-busy=true],:has(>.gnav-pending))>.gnav-ico>*{visibility:hidden}.gnav-item:is([aria-busy=true],:has(>.gnav-pending))>.gnav-ico:after,.gnav-subitem:is([aria-busy=true],:has(>.gnav-pending)):after,.gb-pending-mark{content:"";display:inline-block;vertical-align:middle;grid-area:1 / 1;flex:none;box-sizing:border-box;width:20px;height:20px;border-radius:50%;border:2px solid color-mix(in srgb,currentColor 18%,transparent);border-top-color:currentColor;animation:gnav-spin .7s linear infinite}.gnav-subitem:is([aria-busy=true],:has(>.gnav-pending)):after{position:absolute;left:22px;top:50%;margin-top:-6px;width:12px;height:12px;border-width:1.5px}@keyframes gnav-spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion: reduce){.gnav-item:is([aria-busy=true],:has(>.gnav-pending))>.gnav-ico:after,.gnav-subitem:is([aria-busy=true],:has(>.gnav-pending)):after,.gb-pending-mark{animation-duration:2s}}a[data-gb-pending]:has(>.gb-pending-mark),a[data-gb-pending][aria-busy=true]{cursor:progress}a[data-gb-pending]>.gb-pending-mark{display:inline-block;width:12px;height:12px;border-width:1.5px;margin-left:6px;vertical-align:-1px}a[data-gb-pending=corner]{position:relative}a[data-gb-pending=corner]>.gb-pending-mark{position:absolute;top:12px;right:12px;width:16px;height:16px;margin:0}.gb-route-holding{padding-top:4px}.gb-route-holding .ui-skeleton{gap:16px}.gb-route-holding__heading{margin:0 0 20px;font-size:20px;font-weight:650;line-height:1.3;color:var(--gemboss-text)}.gb-route-holding .ui-skeleton__line{height:10px}.gb-route-holding .ui-skeleton__line.gb-route-holding__title{height:20px;width:34%;margin-bottom:8px}.gb-route-holding .ui-skeleton__line:nth-last-child(4){width:46%}.gb-route-holding .ui-skeleton__line:nth-last-child(3){width:92%}.gb-route-holding .ui-skeleton__line:nth-last-child(2){width:78%}.gb-route-holding .ui-skeleton__line:nth-last-child(1){width:61%}.gb-route-cover{display:none}html[data-gb-navigating] .gb-route-cover{display:block;position:fixed;inset:56px 0 0 240px;z-index:5;overflow:hidden;box-sizing:border-box;padding:24px;background:var(--gemboss-app-bg);pointer-events:none;animation:gb-route-cover-in .08s ease-out both}@keyframes gb-route-cover-in{0%{opacity:0}to{opacity:1}}@media (max-width: 760px){html[data-gb-navigating] .gb-route-cover{left:0;padding:16px}}@media (prefers-reduced-motion: reduce){html[data-gb-navigating] .gb-route-cover{animation-duration:0s}}.gnav-pending{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.gb-brand{min-width:0;height:40px;margin:0;padding:0 8px;border-radius:var(--gemboss-radius-md);display:inline-flex;align-items:center;gap:4.5px;color:var(--gb-brand-ink, #14151a);text-decoration:none;transition:background-color var(--gemboss-transition-base) ease}.gb-brand:hover{background:var(--gemboss-hover)}.gb-brand:focus-visible{outline:2px solid var(--gemboss-primary);outline-offset:2px}.gb-brand-gem{display:block;flex:0 0 auto}.gb-brand-name{font-family:Inter,var(--gemboss-font-sans);font-size:13.3px;font-weight:700;letter-spacing:-.13px;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}@media (max-width: 460px){.gb-brand-name{display:none}.gb-brand{min-width:44px;height:44px;justify-content:center}}.gemboss-topbar-search{width:100%;max-width:560px;height:36px;min-width:0;margin:0 auto;border:1px solid var(--gemboss-field-line);border-radius:var(--gemboss-radius-md);background:var(--gemboss-field-bg);padding:0 8px 0 12px;display:grid;grid-template-columns:18px minmax(0,1fr) auto;gap:8px;align-items:center;color:var(--gemboss-text-muted)}.gemboss-topbar-search:hover{border-color:var(--gemboss-field-line-hover);background:var(--gemboss-field-bg-hover)}.gemboss-topbar-search:focus-within{border-color:var(--gemboss-field-line-active);background:var(--gemboss-surface)}.gemboss-topbar-search-icon{width:18px;height:18px;color:currentColor}.gemboss-topbar-search-input{min-width:0;height:calc(100% + 2px);border:0;background:transparent;padding:0;margin:-1px 0;font:inherit;font-size:var(--gemboss-fs-body);line-height:18px;color:var(--gemboss-field-ink);outline:none}.gemboss-topbar-search-input::placeholder{color:var(--gemboss-field-placeholder);opacity:1}.gemboss-topbar-search-kbd{border:1px solid var(--gemboss-field-line);border-radius:var(--gemboss-radius-sm);background:var(--gemboss-surface);padding:1px 6px;color:var(--gemboss-text-muted);font-family:inherit;font-size:var(--gemboss-fs-micro);line-height:16px}.gemboss-topbar-search:focus-within .gemboss-topbar-search-kbd{display:none}@media (pointer: coarse),(max-width: 47.9975em){.gemboss-topbar-search{height:44px}}@media (max-width: 460px){.gemboss-topbar-search-kbd{display:none}}.ui-page{box-sizing:border-box;width:100%;min-width:0;max-width:var(--ui-content-max, 900px);margin-inline:auto}.ui-page--feed{max-width:720px}.ui-page--form{max-width:662px}.ui-page--canvas{max-width:1440px}.ui-page--full{max-width:none}