@gemboss/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/auth.css ADDED
@@ -0,0 +1,606 @@
1
+ /* The GemBoss sign-in screen — @gemboss/ui (28/09/2026).
2
+ *
3
+ * Moved here VERBATIM from the admin's styled-jsx block (apps/admin/src/app/login/LoginClient.tsx),
4
+ * which was the original every other sign-in copied by hand: gemcommunity's .auth-wrap,
5
+ * content-radar's and gemwatcher's login.css, gemcare's inbox, partner-radar. Class names took a
6
+ * `gb-auth` prefix (gemcare and gempress already have a `.login-card` of their own); the panel's
7
+ * colours became tokens; nothing else changed. The admin's /login was screenshot at nine sizes in
8
+ * both themes before and after the move and compared pixel for pixel.
9
+ *
10
+ * Rendered by AuthLayout / AuthForm / PasswordField (auth.tsx). What only the admin has — the Google
11
+ * button, the "or" divider — stays in the admin.
12
+ */
13
+
14
+ /* dvh, not vh: on a phone 100vh is the height with the URL bar HIDDEN, so a centred card sat
15
+ under the toolbar. The card centres with margin:auto rather than align-items:center — auto
16
+ margins collapse to 0 when the content is taller than the screen, so the top of the card
17
+ can never be pushed above the fold (short landscape phone, keyboard open). */
18
+ /* FLUID, not stepped. Every vertical gap is clamp(compact, N cqh, roomy): N is picked so the roomy
19
+ value lands at ~800px of height, and the compact floor at ~400px. Between the two the page
20
+ breathes in proportion to the screen instead of jumping at a breakpoint — the stepped version
21
+ snapped its card from 379px to 469px between 640 and 680 tall, and still overflowed below
22
+ 400. Controls never scale: inputs and buttons keep min-height 44px at every size, only the
23
+ AIR between them gives. cqh is the SHELL's height, which is the visible height — URL bar and
24
+ on-screen keyboard included (see --vvh above the return). */
25
+ /* The shell is exactly the visible height and is the size container every rule below measures
26
+ (cqh, @container gb-auth). It scrolls itself if even the shed layout does not fit (< ~260px). */
27
+ /* ONE PALETTE, TWO REPOS. Every colour on this screen is a --gb-* token, and gemcommunity's
28
+ sign-in (app/globals.css, the block scoped to .auth-wrap) declares the SAME names with the
29
+ SAME values. That is what keeps the two sign-in screens identical: a colour changed in one
30
+ place is a colour that can be changed in the other, by name, instead of two piles of hex
31
+ drifting apart — which is what they had been doing (card 380 vs 400, radius 14 vs 12,
32
+ wordmark 22 vs 24, and this screen with no dark mode at all while that one had it).
33
+ Dark follows the OS. There is no toggle here, unlike gemcommunity: that app persists a
34
+ data-theme the admin has no reader for, and a switch that only half-works is worse than
35
+ none. The [data-theme] hook is still honoured so a person who set it over there, in the
36
+ same browser, is not thrown back to white when they land here. */
37
+ .gb-auth {
38
+ --gb-surface: #ffffff;
39
+ /* The admin's own near-black, by name — the colour guard asks for the token and it is the
40
+ right answer here anyway: light mode IS the admin's palette, and only the dark block
41
+ below has values the admin has no token for. */
42
+ --gb-ink: var(--gemboss-text-strong, #1b1c1e);
43
+ --gb-muted: #6d7175;
44
+ --gb-line: #e1e3e5;
45
+ --gb-field-line: #c9cccf;
46
+ --gb-field-bg: #ffffff;
47
+ --gb-field-hover: #fafafa;
48
+ --gb-placeholder: #868b94;
49
+ --gb-accent: #5661f5;
50
+ --gb-btn: linear-gradient(120deg, #1b1c1e 0%, #000000 100%);
51
+ --gb-btn-ink: #ffffff;
52
+ --gb-wordmark: linear-gradient(120deg, #1b1c1e 0%, #000000 100%);
53
+ --gb-hover: #f7f7f5;
54
+ --gb-hover-line: #b9bbc0;
55
+ --gb-err-bg: #fee2e2;
56
+ --gb-err-ink: #b91c1c;
57
+ --gb-err-line: transparent;
58
+ --gb-card-shadow: 0 24px 64px rgba(15, 23, 42, 0.08);
59
+ --gb-mark-bg: #0b0b0c;
60
+ /* The brand's own facet geometry as a texture for the panel, rather than a stock diagonal
61
+ hatch: it is the same path the mark draws, blown up and held at 7% opacity. Inline so it
62
+ costs no request on the screen that must render before anything else exists. */
63
+ --gb-facet: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 120'%3E%3Cg fill='none' stroke='%23ffffff' stroke-width='0.45' stroke-linejoin='round'%3E%3Cpath d='M26,14 74,14 96,50 50,110 4,50Z'/%3E%3Cpath d='M4,50H96M26,14 34,50M74,14 66,50M34,50 50,110M66,50 50,110'/%3E%3C/g%3E%3C/svg%3E");
64
+ /* The composition is the gemboss one and stays the gemboss one in dark: ink wash top-left,
65
+ iris wash bottom-right, flat paper underneath. Only the three colours flip. */
66
+ --gb-page:
67
+ radial-gradient(1200px 600px at 20% 10%, rgba(27, 28, 30, 0.05), transparent 60%),
68
+ radial-gradient(1000px 500px at 80% 90%, rgba(86, 97, 245, 0.06), transparent 60%),
69
+ #fafafb;
70
+ --gb-page-flat: #ffffff;
71
+ /* The brand panel, as tokens (28/09/2026, @gemboss/ui): the admin's is INK with iris used
72
+ sparingly, because its mark is an outline on ink. A surface whose mark is filled with iris
73
+ (gemcommunity, content-radar) redefines these five on its own root; nothing else changes. */
74
+ --gb-panel-bg:
75
+ radial-gradient(900px 520px at 78% 12%, rgba(86, 97, 245, 0.26), transparent 62%),
76
+ radial-gradient(700px 480px at 12% 96%, rgba(86, 97, 245, 0.12), transparent 60%),
77
+ #0b0b0c;
78
+ --gb-panel-ink: #f4f4f2;
79
+ --gb-panel-muted: rgba(244, 244, 242, 0.72);
80
+ --gb-panel-eyebrow: #8a93ff;
81
+ --gb-panel-facet-o: 0.05;
82
+ color-scheme: light dark;
83
+ position: fixed;
84
+ top: var(--vvt, 0px);
85
+ left: 0;
86
+ right: 0;
87
+ height: 100vh;
88
+ height: var(--vvh, 100dvh);
89
+ container: gb-auth / size;
90
+ overflow-y: auto;
91
+ /* The recolor swapped the lower wash to iris but missed this one, which was
92
+ rgba(194,65,12,.08) — retired coral — so the sign-in screen kept a peach cast in the
93
+ top-left corner. Ink for depth, iris for the single accent: B&W ground, one colour. */
94
+ background: var(--gb-page);
95
+ color: var(--gb-ink);
96
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
97
+ }
98
+ /* ONE COLUMN BY DEFAULT. The split is opt-IN, at a size where a second column is a gift
99
+ rather than a squeeze — and the form column keeps every clamp and shed step it had, so
100
+ nothing about the phone or the short-landscape case changes. */
101
+ .gb-auth-split {
102
+ display: grid;
103
+ grid-template-columns: 1fr;
104
+ min-height: 100%;
105
+ }
106
+ .gb-auth-stage {
107
+ box-sizing: border-box;
108
+ display: flex;
109
+ min-height: 100%;
110
+ padding: clamp(12px, 3cqh, 24px) clamp(16px, 4vw, 24px);
111
+ }
112
+ .gb-auth-panel-in {
113
+ position: relative;
114
+ box-sizing: border-box;
115
+ width: 100%;
116
+ max-width: 520px;
117
+ padding: 48px clamp(32px, 5vw, 64px);
118
+ }
119
+ /* The eyebrow is the one place a monospace face earns its keep here: it labels the panel
120
+ without competing with the headline, and the letterspacing does the work the size would
121
+ otherwise have to. */
122
+ .gb-auth-eyebrow {
123
+ margin: 0 0 20px;
124
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
125
+ font-size: 12px;
126
+ font-weight: 500;
127
+ letter-spacing: 0.12em;
128
+ text-transform: uppercase;
129
+ color: var(--gb-panel-eyebrow);
130
+ }
131
+ .gb-auth-panel-h {
132
+ margin: 0 0 16px;
133
+ font-size: clamp(28px, 3.2vw, 40px);
134
+ line-height: 1.15;
135
+ letter-spacing: -0.02em;
136
+ font-weight: 700;
137
+ color: var(--gb-panel-ink);
138
+ }
139
+ .gb-auth-panel-p {
140
+ /* This used to be 0 0 32px, the gap down to the CTA. The button is gone, so that 32px was
141
+ dangling off the panel's last line with nothing to balance it. */
142
+ margin: 0;
143
+ font-size: 15px;
144
+ line-height: 1.6;
145
+ /* 0.72 of #f4f4f2 on #0b0b0c measures 8.9:1 — well past AA, and quiet enough that the
146
+ headline still leads. */
147
+ color: var(--gb-panel-muted);
148
+ }
149
+ /* THE PANEL IS OFF UNTIL EARNED. Hidden by default rather than shown-then-hidden: a panel
150
+ that paints first and disappears at a breakpoint is a panel that flashes on every phone. */
151
+ .gb-auth-panel {
152
+ display: none;
153
+ }
154
+ /* Wide AND tall. Width alone is not enough — a 1440x420 window is a wide letterbox, and a
155
+ column of headline plus button in 420px of height is the crush this whole screen spent its
156
+ shed ladder avoiding. 900 wide is where 380px of card plus a readable second column stop
157
+ fighting.
158
+ 520 tall, not 620 (26/09/2026): a 1366x768 laptop with a bookmarks bar leaves under 620px
159
+ of viewport, so the most common laptop never saw this column. Its content measures 286px
160
+ padding included, so 520 still leaves over 200px spare. The form sheds on height whether
161
+ or not the panel is there, so the panel costs it nothing. Same number as the gemcommunity
162
+ sign-in (app/globals.css); change both or neither. */
163
+ @container gb-auth (min-width: 900px) and (min-height: 520px) {
164
+ .gb-auth-split {
165
+ grid-template-columns: minmax(420px, 1fr) minmax(0, 1.05fr);
166
+ }
167
+ /* The column IS the structure now, so the card stops drawing a second one. A border and
168
+ a 64px shadow around a form that already sits in its own half of the screen is chrome
169
+ about chrome. The card keeps every bit of this below 900px, where it is the only thing
170
+ giving the form an edge. */
171
+ /* SCOPED UNDER .gb-auth-split ON PURPOSE — (0,2,0) against the base rules' (0,1,0), so this
172
+ wins wherever it is written. The first version of these two relied on coming LATER in
173
+ the sheet than the card's own rule, and it did not: a container query carries the specificity
174
+ of the selectors inside it, nothing more, and the tie went to the rule written first.
175
+ Measured then: card still 380 wide with its border and 64px shadow intact. */
176
+ .gb-auth-split .gb-auth-stage {
177
+ background: var(--gb-surface);
178
+ }
179
+ .gb-auth-split .gb-auth-card {
180
+ border: none;
181
+ box-shadow: none;
182
+ max-width: 400px;
183
+ }
184
+ .gb-auth-panel {
185
+ display: flex;
186
+ align-items: center;
187
+ position: relative;
188
+ overflow: hidden;
189
+ /* INK-DOMINANT, because the gemboss mark is an OUTLINE on ink. Iris appears twice on
190
+ this panel and both times it is small: the eyebrow and the button. */
191
+ background: var(--gb-panel-bg);
192
+ color: var(--gb-panel-ink);
193
+ }
194
+ /* The facet lines of the mark, blown up and laid over the ground — the brand's own geometry
195
+ instead of a stock diagonal hatch. Tiny opacity on purpose: at 1920 it should read as
196
+ texture you notice second, not a pattern you notice first. */
197
+ .gb-auth-panel::before {
198
+ content: "";
199
+ position: absolute;
200
+ /* Deliberately OVERSIZED and pushed off two edges, so what lands on the panel is a few
201
+ facet lines crossing it — texture. Sized to fit, it read as a big grey triangle in the
202
+ corner: a picture of a logo rather than a ground for one. */
203
+ inset: -45% -38% auto auto;
204
+ width: 132%;
205
+ aspect-ratio: 1;
206
+ background-image: var(--gb-facet);
207
+ background-size: contain;
208
+ background-repeat: no-repeat;
209
+ opacity: var(--gb-panel-facet-o);
210
+ pointer-events: none;
211
+ }
212
+ }
213
+ .gb-auth-card {
214
+ width: 100%;
215
+ max-width: 380px;
216
+ margin: auto;
217
+ background: var(--gb-surface);
218
+ border: 1px solid var(--gb-line);
219
+ border-radius: 14px;
220
+ padding: clamp(18px, 3.5cqh, 28px) 28px;
221
+ box-shadow: var(--gb-card-shadow);
222
+ }
223
+ .gb-auth-header {
224
+ text-align: center;
225
+ margin-bottom: clamp(12px, 3cqh, 24px);
226
+ }
227
+ /* The header gives up height LAST, with no layout switch: the tagline goes in ONE step under
228
+ 600px of height, then the mark shrinks from 56px at 560 to 32px at 480.
229
+ The tagline used to fold CONTINUOUSLY (max-height ramping from 20px at 600 to 0 at 560), which
230
+ meant every height in that band showed the line cut through the middle: 4px of a 16px line at
231
+ 320x568, the iPhone SE size, flagged text_clipped by scripts/visual-gate on 28/09/2026. A line of
232
+ text has no useful partial state, so it is shown whole or not at all (content-radar's copy of this
233
+ screen had already made the same fix, 27/09/2026). The mark CAN shrink, so it still does.
234
+ A mark has a floor: shrunk continuously to zero it passed through an 11px smudge that read as
235
+ a rendering bug (screenshot at 390x480). So under 480 it goes in one step (~38px) and a
236
+ landscape phone gets the wordmark and the form. The old breakpoint moved the mark beside the
237
+ name and made the card jump 60px between two heights 40px apart. */
238
+ .gb-auth-mark {
239
+ display: inline-flex;
240
+ margin-bottom: clamp(6px, calc(6px + (100cqh - 480px) * 0.075), 12px);
241
+ }
242
+ /* `img` too (29/09/2026): a surface whose CSP forbids inline `style` (gemcare: `style-src 'self'`)
243
+ cannot use GemMark, which paints its tile with style="fill:var(--gb-mark-bg)". It passes its
244
+ vendored gemboss-mark.svg as an <img> through `mark`, and that must shrink on a short screen too. */
245
+ .gb-auth-mark svg,
246
+ .gb-auth-mark img {
247
+ width: clamp(32px, calc(32px + (100cqh - 480px) * 0.3), 56px);
248
+ height: clamp(32px, calc(32px + (100cqh - 480px) * 0.3), 56px);
249
+ }
250
+ @container gb-auth (max-height: 479.98px) {
251
+ .gb-auth-mark {
252
+ display: none;
253
+ }
254
+ }
255
+ .gb-auth-header h1 {
256
+ font-size: clamp(19px, 2.75cqh, 22px);
257
+ font-weight: 700;
258
+ margin: 0 0 clamp(0px, calc((100cqh - 560px) * 0.1), 4px);
259
+ background: var(--gb-wordmark);
260
+ -webkit-background-clip: text;
261
+ -webkit-text-fill-color: transparent;
262
+ background-clip: text;
263
+ }
264
+ .gb-auth-header p {
265
+ margin: 0;
266
+ font-size: 13px;
267
+ color: var(--gb-muted);
268
+ }
269
+ /* Out of sight, not out of the page: the same visually-hidden pattern the header uses under 400,
270
+ so a screen reader still hears the tagline, as it did when the fold took it to 0px. */
271
+ @container gb-auth (max-height: 599.98px) {
272
+ .gb-auth-header p {
273
+ position: absolute;
274
+ width: 1px;
275
+ height: 1px;
276
+ margin: -1px;
277
+ overflow: hidden;
278
+ clip: rect(0 0 0 0);
279
+ white-space: nowrap;
280
+ }
281
+ }
282
+ .gb-auth-form {
283
+ display: flex;
284
+ flex-direction: column;
285
+ gap: clamp(8px, 1.5cqh, 12px);
286
+ }
287
+ .gb-auth-form label {
288
+ display: flex;
289
+ flex-direction: column;
290
+ gap: clamp(4px, 0.75cqh, 6px);
291
+ font-size: 12px;
292
+ font-weight: 500;
293
+ color: var(--gb-ink);
294
+ }
295
+ /* 38px → 44px. §9.1 wants a CONTROL at 44 (Apple HIG / WCAG 2.5.5 AAA); measured on the
296
+ live page 2026-09-09 these were 38, the sign-in button 40, the Google button 42, the
297
+ password toggle 13 and the Google connect link 15. All five were pre-existing, on the
298
+ first screen a merchant sees — and now the screen the MCP connect flow sends strangers to,
299
+ which is why they are fixed here rather than filed. min-height, not height, so a
300
+ browser that needs more room for the text still gets it. */
301
+ /* THE FIELD'S SHAPE. In the admin it is the shared `gemboss-field gemboss-field--tall` class
302
+ (packages/ui/src/gemboss-field.css), which the admin's inputs keep wearing. Every other
303
+ surface has no such sheet, so `PasswordField`/`AuthField` default to `gb-auth-input`: the same
304
+ numbers, resolved from that file's Polaris fallbacks — 32px box raised to a 44px floor, 12px
305
+ side padding, 13px on 20px, radius 8, 1px border; 16px text under 768px (iOS zooms any field
306
+ under 16px on focus and does not zoom back); height:auto under a coarse pointer. */
307
+ .gb-auth-input {
308
+ box-sizing: border-box; height: 32px; min-height: 44px; min-width: 0; padding: 0 12px;
309
+ font: inherit; font-size: 13px; line-height: 20px;
310
+ border: 1px solid; border-radius: 8px;
311
+ }
312
+ @media (max-width: 47.9975em) { .gb-auth-input { font-size: 16px; } }
313
+ @media (pointer: coarse) { .gb-auth-input { height: auto; } }
314
+ /* Colour only, over whichever class drew the shape. */
315
+ .gb-auth-form input {
316
+ width: 100%;
317
+ box-sizing: border-box;
318
+ background-color: var(--gb-field-bg);
319
+ border-color: var(--gb-field-line);
320
+ color: var(--gb-ink);
321
+ }
322
+ .gb-auth-form input::placeholder {
323
+ color: var(--gb-placeholder);
324
+ }
325
+ /* HOVER AND FOCUS HAVE TO BE RESTATED HERE, and finding that out is the reason this block
326
+ exists. gemboss-field.css owns them, at (0,3,0) for the hover; the rule above out-specifies
327
+ the class — (0,3,1) when it lived in the admin's styled-jsx, (0,1,1) here, still above
328
+ (0,1,0) — so it took the hover background with it and the field stopped reacting to the
329
+ pointer (measured then: rest and hover identical in both themes). A token that wins the
330
+ cascade silently takes the states with it.
331
+ The focus ring is restated for a different reason: gemboss-field.css paints it Polaris
332
+ blue (#005bd3), which is not a colour this screen uses anywhere. This screen is B&W plus
333
+ iris, deliberately outside Polaris, and its own password toggle already rings in near-
334
+ black. Ink, in both themes, and the same ink gemcommunity's sign-in rings in. */
335
+ .gb-auth-form input:hover:not(:disabled) {
336
+ background-color: var(--gb-field-hover);
337
+ border-color: var(--gb-hover-line);
338
+ }
339
+ .gb-auth-form input:focus-visible {
340
+ outline: 2px solid var(--gb-ink);
341
+ outline-offset: 1px;
342
+ border-color: var(--gb-ink);
343
+ }
344
+ /* Same shape as a .gb-auth-form label, for the one field whose input had to leave the label.
345
+ Declared rather than reused so a change to labels cannot silently un-match this one. */
346
+ .gb-auth-field {
347
+ display: flex;
348
+ flex-direction: column;
349
+ gap: clamp(4px, 0.75cqh, 6px);
350
+ font-size: 12px;
351
+ font-weight: 500;
352
+ color: var(--gb-ink);
353
+ }
354
+ .gb-auth-pw {
355
+ position: relative;
356
+ display: flex;
357
+ }
358
+ .gb-auth-pw input {
359
+ width: 100%;
360
+ /* Room for the button. Without it the last characters of a revealed password sit UNDER the
361
+ eye — which is only visible in the one state the button exists to produce. */
362
+ padding-right: 48px;
363
+ }
364
+ .gb-auth-pw-toggle {
365
+ /* min-WIDTH as well as height. #2144 gave this 44px of height and stopped, because the
366
+ script I checked it with only read heights — the repo's own gate reads both and caught
367
+ it at 41.22px wide, one check away from shipping "fixed" twice. */
368
+ min-height: 44px;
369
+ min-width: 44px;
370
+ position: absolute;
371
+ right: 0;
372
+ top: 0;
373
+ bottom: 0;
374
+ display: inline-flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ padding: 0;
378
+ margin: 0;
379
+ background: none;
380
+ border: none;
381
+ border-radius: 8px;
382
+ cursor: pointer;
383
+ color: var(--gb-muted);
384
+ transition: none;
385
+ }
386
+ .gb-auth-pw-toggle:hover {
387
+ color: var(--gb-ink);
388
+ }
389
+ /* focus-visible, not focus: a mouse click on the eye should not leave a ring sitting inside
390
+ the field. §9.2 wants the state to exist, not to fire for pointer users. */
391
+ .gb-auth-pw-toggle:focus-visible {
392
+ outline: 2px solid var(--gemboss-primary, #1b1c1e);
393
+ outline-offset: -2px;
394
+ }
395
+ .gb-auth-error {
396
+ background: var(--gb-err-bg);
397
+ border: 1px solid var(--gb-err-line);
398
+ color: var(--gb-err-ink);
399
+ padding: 8px 10px;
400
+ font-size: 13px;
401
+ border-radius: 6px;
402
+ }
403
+ /* Scoped to the submit button BY ITS OWN CLASS, NOT a bare "button". This rule paints a filled
404
+ black pill, and as ".login-form button" it was specificity 0,1,1 against ".pw-toggle" at 0,1,0 —
405
+ so the eye INSIDE the password field was painted as a second Sign-in button: black
406
+ gradient, white icon, and a 6px margin-top that pushed it out of the input. Measured in a
407
+ production build at all six widths (SS9.1): backgroundImage=GRADIENT,
408
+ color=rgb(255,255,255), marginTop=6px.
409
+ Second time in this repo — composer-owns-only-its-own-buttons.test.ts bans the same shape
410
+ in the chat composer. SS9.3: one visual owner, and the owner names its own element. */
411
+ .gb-auth-form > .gb-auth-submit {
412
+ margin-top: clamp(2px, 0.75cqh, 6px);
413
+ min-height: 44px;
414
+ font-size: 14px;
415
+ font-weight: 600;
416
+ border: none;
417
+ border-radius: 8px;
418
+ background: var(--gb-btn);
419
+ color: var(--gb-btn-ink);
420
+ cursor: pointer;
421
+ font-family: inherit;
422
+ transition: opacity 0.15s ease, transform 0.15s ease;
423
+ }
424
+ .gb-auth-form > .gb-auth-submit:hover:not(:disabled) {
425
+ transform: translateY(-1px);
426
+ }
427
+ .gb-auth-form > .gb-auth-submit:disabled {
428
+ opacity: 0.6;
429
+ cursor: not-allowed;
430
+ }
431
+ /* The signup line. 24px of target, not 44: WCAG 2.5.8 is the threshold that has a standard
432
+ behind it for a LINK in a line of text, and inline-block is what makes the padding a target
433
+ at all — on an inline element it paints and measures nothing. */
434
+ .gb-auth-foot {
435
+ margin: clamp(10px, 2cqh, 16px) 0 0;
436
+ font-size: 13px;
437
+ color: var(--gb-muted);
438
+ text-align: center;
439
+ line-height: 1.5;
440
+ }
441
+ .gb-auth-foot a {
442
+ display: inline-block;
443
+ min-height: 24px;
444
+ line-height: 24px;
445
+ padding: 0 2px;
446
+ color: var(--gb-accent);
447
+ font-weight: 600;
448
+ text-decoration: none;
449
+ }
450
+ .gb-auth-foot a:hover,
451
+ .gb-auth-foot a:focus-visible {
452
+ text-decoration: underline;
453
+ }
454
+ .gb-auth-card .gb-auth-help {
455
+ margin: clamp(8px, 1.75cqh, 14px) 0 0;
456
+ font-size: 12px;
457
+ color: var(--gb-muted);
458
+ text-align: center;
459
+ line-height: 1.5;
460
+ }
461
+ /* The "trouble getting in?" line. min-height 24px: WCAG 2.5.8 for a link. */
462
+ .gb-auth-card .gb-auth-help a {
463
+ display: inline-block;
464
+ min-height: 24px;
465
+ line-height: 24px;
466
+ /* The page's link colour, not the grey of the text around it (#3129): the reader of this line
467
+ is stuck and scanning for a way out. #5661f5 on white is 4.75:1, above the 4.5:1 12px needs. */
468
+ color: var(--gb-accent);
469
+ font-weight: 600;
470
+ text-decoration: none;
471
+ }
472
+ .gb-auth-card .gb-auth-help a:hover,
473
+ .gb-auth-card .gb-auth-help a:focus-visible {
474
+ text-decoration: underline;
475
+ }
476
+ /* PHONE: the card IS the page. A floating card inside a 16px gutter spends ~90px of a 320px
477
+ screen on border, shadow and double padding for nothing — the pattern every major sign-in
478
+ page (Google, Apple, Shopify, Stripe) uses below tablet width is a full-bleed white sheet.
479
+ Inputs go to 16px because iOS Safari zooms the page on focus into any field under 16px,
480
+ and the zoom does not undo itself after sign-in. Safe-area padding keeps the form out of
481
+ the notch/home indicator in the desktop-app and standalone webviews. LAST in the sheet on
482
+ purpose: a short phone matches the max-height block too, and this one must win the padding. */
483
+ @media (max-width: 480px) {
484
+ .gb-auth {
485
+ background: var(--gb-page-flat);
486
+ }
487
+ .gb-auth-stage {
488
+ padding: max(clamp(10px, 3cqh, 20px), env(safe-area-inset-top)) max(16px, env(safe-area-inset-right))
489
+ max(clamp(10px, 3cqh, 20px), env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
490
+ }
491
+ .gb-auth-card {
492
+ border: none;
493
+ border-radius: 0;
494
+ box-shadow: none;
495
+ padding: 0;
496
+ }
497
+ /* (no font-size here: the field's shape class — gemboss-field in the admin, gb-auth-input
498
+ elsewhere — reads 16px under this same breakpoint, which is what stops iOS zooming.) */
499
+ }
500
+ /* ALWAYS INSIDE THE VIEWPORT. Once the clamps above are at their floors, the only height left
501
+ is in what can go. Shed in order of what a person trying to sign in needs least, each step
502
+ at the height where the one before stopped fitting (measured, 390 and 1280 wide):
503
+ < 440 the "or" divider — the Google button is self-explanatory without it
504
+ < 400 the GemBoss wordmark — kept for screen readers, it is still the page heading
505
+ < 360 the signup and help lines, and the desktop card trims its padding
506
+ Never shed: email, password, Sign in, Google. Those are the page. The signup line survives
507
+ to 360 rather than going with the wordmark at 400: a stranger with no account is exactly
508
+ who this screen strands, and below 360 there is a scrollbar to reach it with. LAST in the sheet so
509
+ it wins over the phone block above. */
510
+ @container gb-auth (max-height: 399.98px) {
511
+ .gb-auth-header {
512
+ position: absolute;
513
+ width: 1px;
514
+ height: 1px;
515
+ margin: -1px;
516
+ overflow: hidden;
517
+ clip: rect(0 0 0 0);
518
+ white-space: nowrap;
519
+ }
520
+ }
521
+ @container gb-auth (max-height: 359.98px) {
522
+ .gb-auth-card .gb-auth-help,
523
+ .gb-auth-foot {
524
+ display: none;
525
+ }
526
+ .gb-auth-stage {
527
+ padding-top: max(8px, env(safe-area-inset-top));
528
+ padding-bottom: max(8px, env(safe-area-inset-bottom));
529
+ }
530
+ }
531
+ /* The card only has padding to trim where it IS a card; on a phone it is already 0. */
532
+ @container gb-auth (max-height: 359.98px) and (min-width: 481px) {
533
+ .gb-auth-card {
534
+ padding-top: 14px;
535
+ padding-bottom: 14px;
536
+ }
537
+ }
538
+ /* DARK — a pure token flip. Not one rule above changes; every colour on this screen reads a
539
+ --gb-* token, so the whole screen inverts by redefining eighteen values here. The same
540
+ eighteen, with the same values, are what gemcommunity's .auth-wrap block declares.
541
+ Two selectors on purpose: the OS ask, ignored when a data-theme of light is set, and an
542
+ explicit data-theme of dark. LAST in the sheet so it beats the phone block, which sets a
543
+ flat page colour of its own.
544
+ When this sheet lived in the admin's styled-jsx, the root part needed :global() — styled-jsx
545
+ glued its scope class onto ":root", no html element carries one, and the whole dark block
546
+ matched nothing (measured 23/09/2026: --gb-surface still #ffffff on a dark-OS browser, with
547
+ typecheck and the text-reading tests all green). As a plain stylesheet it needs nothing; the
548
+ dark screenshots taken for the move are what show it still applies.
549
+ WHAT IT IS NOT: a dark mode for the admin. Nothing behind /login has one yet, so signing in
550
+ moves a dark-OS person to a light app. That is the same handoff gemcommunity has shipped
551
+ since its auth skin landed, and the alternative was leaving this screen the one white
552
+ rectangle on a dark machine. */
553
+ @media (prefers-color-scheme: dark) {
554
+ :root:not([data-theme="light"]) .gb-auth {
555
+ --gb-surface: #141517;
556
+ --gb-ink: #f4f4f3;
557
+ --gb-muted: #969c97;
558
+ --gb-line: rgba(255, 255, 255, 0.11);
559
+ --gb-field-line: rgba(255, 255, 255, 0.17);
560
+ --gb-field-bg: #1b1c1f;
561
+ --gb-field-hover: #212226;
562
+ --gb-placeholder: #7f838c;
563
+ --gb-accent: #8a93ff;
564
+ --gb-btn: linear-gradient(120deg, #f4f4f3 0%, #dfe0e4 100%);
565
+ --gb-btn-ink: #0b0b0c;
566
+ --gb-wordmark: linear-gradient(120deg, #f4f4f3 0%, #c9cbd2 100%);
567
+ --gb-hover: rgba(255, 255, 255, 0.06);
568
+ --gb-hover-line: rgba(255, 255, 255, 0.28);
569
+ --gb-err-bg: rgba(215, 44, 13, 0.16);
570
+ --gb-err-ink: #ff9d88;
571
+ --gb-err-line: rgba(245, 120, 95, 0.4);
572
+ --gb-card-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
573
+ --gb-mark-bg: #202127;
574
+ --gb-page:
575
+ radial-gradient(1200px 600px at 20% 10%, rgba(255, 255, 255, 0.05), transparent 60%),
576
+ radial-gradient(1000px 500px at 80% 90%, rgba(138, 147, 255, 0.1), transparent 60%),
577
+ #0b0b0c;
578
+ --gb-page-flat: #0b0b0c;
579
+ }
580
+ }
581
+ :root[data-theme="dark"] .gb-auth {
582
+ --gb-surface: #141517;
583
+ --gb-ink: #f4f4f3;
584
+ --gb-muted: #969c97;
585
+ --gb-line: rgba(255, 255, 255, 0.11);
586
+ --gb-field-line: rgba(255, 255, 255, 0.17);
587
+ --gb-field-bg: #1b1c1f;
588
+ --gb-field-hover: #212226;
589
+ --gb-placeholder: #7f838c;
590
+ --gb-accent: #8a93ff;
591
+ --gb-btn: linear-gradient(120deg, #f4f4f3 0%, #dfe0e4 100%);
592
+ --gb-btn-ink: #0b0b0c;
593
+ --gb-wordmark: linear-gradient(120deg, #f4f4f3 0%, #c9cbd2 100%);
594
+ --gb-hover: rgba(255, 255, 255, 0.06);
595
+ --gb-hover-line: rgba(255, 255, 255, 0.28);
596
+ --gb-err-bg: rgba(215, 44, 13, 0.16);
597
+ --gb-err-ink: #ff9d88;
598
+ --gb-err-line: rgba(245, 120, 95, 0.4);
599
+ --gb-card-shadow: 0 24px 64px rgba(0, 0, 0, 0.55);
600
+ --gb-mark-bg: #202127;
601
+ --gb-page:
602
+ radial-gradient(1200px 600px at 20% 10%, rgba(255, 255, 255, 0.05), transparent 60%),
603
+ radial-gradient(1000px 500px at 80% 90%, rgba(138, 147, 255, 0.1), transparent 60%),
604
+ #0b0b0c;
605
+ --gb-page-flat: #0b0b0c;
606
+ }