@hattisar/design-tokens 0.3.5 → 0.3.7

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/tokens.css +372 -122
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hattisar/design-tokens",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Shared design tokens (colors, type, radius, spacing, motion) and base primitives (reset, buttons, inputs, cards, modals, auth shell) for every Hattisar frontend.",
5
5
  "main": "tokens.css",
6
6
  "files": [
package/tokens.css CHANGED
@@ -32,22 +32,49 @@
32
32
  >=4.5:1) and for a real 3-step surface ladder — surface-tint used
33
33
  to equal background-global exactly, which meant hover states had
34
34
  no visible target to land on. */
35
- --color-background-global: #f6f7f9;
35
+ --color-background-global: #f4f4f5;
36
36
  --color-background-card: #ffffff;
37
- --color-surface-tint: #eef0f3;
38
- --color-input: #fdfdfd;
39
- --color-text-primary: #1c2127;
37
+ --color-surface-tint: #e8e8ea;
38
+ --color-input: #ffffff;
39
+ --color-text-primary: #09090b;
40
40
  /* ~6.4:1 on card */
41
- --color-text-secondary: #56606f;
41
+ --color-text-secondary: #52525b;
42
42
  /* ~4.6:1 on card — was #8f99a8 at 2.9:1, failing AA body text */
43
- --color-text-muted: #6b7684;
43
+ --color-text-muted: #6b6b74;
44
44
  /* Derived from text-primary's own RGB (28,33,39) rather than a
45
45
  separately maintained literal, so border and text can never drift
46
46
  apart the way they used to (border was rgb(17,20,24), a different
47
47
  dark from text-primary's rgb(28,33,39)). */
48
- --color-border: rgba(28, 33, 39, 0.1);
49
- --color-border-strong: rgba(28, 33, 39, 0.2);
48
+ --color-border: #e4e4e7;
49
+ --color-border-strong: #d4d4d8;
50
+
51
+ /* Anything that floats over the page: dropdowns, menus, popovers,
52
+ modals, tooltips.
53
+ Reported: "we might need to add little background shadow for all
54
+ dropdown, popup so it differentiate from background. hard to view
55
+ now."
56
+ They all had a shadow already. The problem was that they were
57
+ painted --color-background-card and opened *over* cards — white on
58
+ white, separated by a 9%-alpha hairline and a shadow tuned so
59
+ softly it read as nothing. A surface only looks raised against
60
+ something.
61
+ So a floating surface gets its own tier: in dark it is a real step
62
+ above the card, and in light — where a popover over a white card
63
+ has nowhere lighter to go — the separation comes from a stronger
64
+ edge and a shadow with actual weight instead. */
65
+ --color-surface-raised: #ffffff;
66
+ --color-border-raised: #d4d4d8;
67
+ --shadow-popover: 0 4px 6px -2px rgba(9, 9, 11, 0.08), 0 12px 28px -8px rgba(9, 9, 11, 0.18);
68
+
50
69
  --color-hover: var(--color-surface-tint);
70
+ --color-focus-ring: rgba(233, 235, 239, 0.2);
71
+ --color-focus-ring-danger: rgba(255, 138, 156, 0.24);
72
+ /* The ring drawn around a focused control. Ink at low alpha rather
73
+ than a saturated blue: this palette has no brand hue, and a blue
74
+ ring here would be the one place colour appeared without meaning
75
+ anything — the ten status tiers are what colour is for. */
76
+ --color-focus-ring: rgba(9, 9, 11, 0.14);
77
+ --color-focus-ring-danger: rgba(199, 10, 36, 0.2);
51
78
 
52
79
  /* Named accents — the same 8 hues most of the status tiers below
53
80
  already used, promoted into reusable tokens instead of each tier repeating
@@ -69,7 +96,7 @@
69
96
  --accent-amber: #8a6600;
70
97
  --accent-bronze: #a35200;
71
98
  --accent-blue: #005bd3;
72
- --accent-cyan: #007cb4;
99
+ --accent-cyan: #0a6d99;
73
100
  --accent-mint: #047b5d;
74
101
  --accent-fuchsia: #984062;
75
102
 
@@ -119,28 +146,28 @@
119
146
  computed. See StatusBadge.tsx for the status keyword -> tier
120
147
  mapping — ten distinct hues, deliberately no collisions. */
121
148
  --status-draft-text: var(--accent-slate);
122
- --status-draft-bg: #f0f4f9;
149
+ --status-draft-bg: #eef1f6;
123
150
  --status-backordered-text: var(--accent-violet);
124
- --status-backordered-bg: #f7ecfe;
151
+ --status-backordered-bg: #f3ecfd;
125
152
  --status-low-stock-text: var(--accent-amber);
126
- --status-low-stock-bg: #ffd289;
153
+ --status-low-stock-bg: #fff1d9;
127
154
  /* Near-empty stock — more urgent than low-stock's reorder-point
128
155
  nudge, not yet fully out. Bg sits between low-stock's punchy
129
156
  amber-gold and out-of-stock's pale peach; text is --color-critical. */
130
157
  --status-critical-stock-text: var(--color-critical);
131
- --status-critical-stock-bg: #ffe0c2;
158
+ --status-critical-stock-bg: #ffe7d1;
132
159
  --status-out-of-stock-text: var(--accent-bronze);
133
- --status-out-of-stock-bg: #ffede1;
160
+ --status-out-of-stock-bg: #ffeee4;
134
161
  --status-partially-fulfilled-text: var(--accent-blue);
135
- --status-partially-fulfilled-bg: #e7f2ff;
162
+ --status-partially-fulfilled-bg: #e8f1fe;
136
163
  --status-shipped-text: var(--accent-cyan);
137
- --status-shipped-bg: #92e5ff;
164
+ --status-shipped-bg: #ddf0fb;
138
165
  --status-delivered-text: var(--accent-mint);
139
- --status-delivered-bg: #ddf8d8;
166
+ --status-delivered-bg: #e1f5e6;
140
167
  --status-cancelled-text: var(--accent-fuchsia);
141
- --status-cancelled-bg: #ffcbda;
168
+ --status-cancelled-bg: #fde7ee;
142
169
  --status-overdue-text: var(--color-danger);
143
- --status-overdue-bg: #ffecee;
170
+ --status-overdue-bg: #feebec;
144
171
 
145
172
  /* Page-level warning/danger/success banners — distinct from the row
146
173
  badges above: a border plus a matching soft fill, no separate text
@@ -149,11 +176,11 @@
149
176
  repeating their own literal hex) so they can never drift out of
150
177
  sync the way they briefly did here. Success intentionally doesn't:
151
178
  it was never meant to mirror --color-success, it's its own shade. */
152
- --banner-warning-bg: #fff1e3;
179
+ --banner-warning-bg: #fff3e6;
153
180
  --banner-warning-border: var(--color-warning);
154
- --banner-danger-bg: #fee8eb;
181
+ --banner-danger-bg: #fdeaed;
155
182
  --banner-danger-border: var(--color-danger);
156
- --banner-success-bg: #cdfed4;
183
+ --banner-success-bg: #e2f7e6;
157
184
  --banner-success-border: var(--color-success);
158
185
 
159
186
  /* A restrained scale, not flat — every step deliberately small, since
@@ -165,17 +192,28 @@
165
192
  remove-buttons inside chips, the settings toggle switch) — never
166
193
  applied to a badge/tag/chip just for a rounder look; those use
167
194
  --radius-sm like everything else (see .status-badge and friends). */
168
- --radius-xs: 4px;
169
- --radius-sm: 6px;
170
- --radius-md: 8px;
171
- --radius-lg: 10px;
172
- --radius-xl: 12px;
195
+ --radius-xs: 5px;
196
+ --radius-sm: 7px;
197
+ --radius-md: 10px;
198
+ --radius-lg: 13px;
199
+ --radius-xl: 16px;
173
200
  --radius-full: 9999px;
174
201
 
175
- --shadow-xs: none;
176
- --shadow-sm: none;
177
- --shadow-md: none;
178
- --shadow-lg: none;
202
+ /* Depth, deliberately restrained. These were all `none`, which left
203
+ every surface separated by border alone — correct, legible, and
204
+ completely flat: a card, a dropdown and a modal all sat on the page
205
+ at exactly the same height. The 32 places already reaching for
206
+ these tokens were asking for a hierarchy the tokens declined to
207
+ give them.
208
+ Four tiers, each tied to a job: xs for a resting control, sm for a
209
+ card, md for anything that floats over the page (dropdowns,
210
+ popovers), lg for a modal. Tinted with the ink colour rather than
211
+ pure black so a shadow reads as the surface's own shade instead of
212
+ a grey smudge. */
213
+ --shadow-xs: 0 1px 2px rgba(9, 9, 11, 0.04);
214
+ --shadow-sm: 0 1px 2px rgba(9, 9, 11, 0.05), 0 2px 8px -3px rgba(9, 9, 11, 0.08);
215
+ --shadow-md: 0 2px 4px -1px rgba(9, 9, 11, 0.04), 0 8px 20px -4px rgba(9, 9, 11, 0.12);
216
+ --shadow-lg: 0 4px 8px -2px rgba(9, 9, 11, 0.06), 0 20px 48px -12px rgba(9, 9, 11, 0.22);
179
217
 
180
218
  /* One shared tier for every FloatingPanel-based dropdown/menu
181
219
  (row actions, column picker, combobox, select, table-actions,
@@ -204,18 +242,40 @@
204
242
  --space-8: 32px;
205
243
  --space-10: 40px;
206
244
  --space-12: 48px;
207
-
208
- /* Type scale — matches sizes already in use across the app (body
209
- 14.5px, h2 19px, h1 23px, ...) plus one new step (--text-3xl) for
210
- bento hero stats. */
211
- --text-xs: 12px;
212
- --text-sm: 13.5px;
213
- --text-base: 14.5px;
245
+ /* Two steps past the old ceiling, for the gaps that separate whole
246
+ regions of a page rather than elements inside one. Without them
247
+ every large gap in the app was a hardcoded number, and they were
248
+ all slightly different. */
249
+ /* The narrowest a form field may be before it stops being readable.
250
+ The document forms laid fields out as `flex: 1; minWidth: 140`,
251
+ which on a wide screen fits six controls onto one row at about
252
+ 130px each — narrow enough to clip "Customer's own reference"
253
+ mid-word. Every form field grid is built on this instead. */
254
+ --field-min-width: 260px;
255
+ /* For a control whose value is a word, not a sentence — a variant
256
+ name, a unit, a percentage — inside a line-item row. */
257
+ --field-min-width-narrow: 140px;
258
+
259
+ --space-16: 64px;
260
+ --space-20: 80px;
261
+
262
+ /* Type scale. The old one grew out of whatever each screen happened
263
+ to be using — 13.5, 14.5, 15 — three steps inside a point and a
264
+ half of each other, which is not a scale, it is three sizes that
265
+ could not agree. This is a proper ~1.2 ratio off a 14px body, so
266
+ the steps are actually distinguishable and a heading reads as a
267
+ heading rather than as slightly larger body text.
268
+ Body moves 14.5 -> 14: at Inter's x-height 14px is the more
269
+ comfortable long-session size, and it buys back the density the
270
+ roomier spacing below spends. */
271
+ --text-xs: 11.5px;
272
+ --text-sm: 13px;
273
+ --text-base: 14px;
214
274
  --text-md: 15px;
215
275
  --text-lg: 17px;
216
- --text-xl: 19px;
217
- --text-2xl: 23px;
218
- --text-3xl: 29px;
276
+ --text-xl: 20px;
277
+ --text-2xl: 24px;
278
+ --text-3xl: 30px;
219
279
 
220
280
  /* A real weight scale — headings, card titles, and KPI/stat values
221
281
  use these to stand apart from the body text around them; dense
@@ -244,10 +304,10 @@
244
304
  --hd-card-bg as every card (bordered, not tonally separate),
245
305
  mirrored by dark theme's own literal Hattisar Design values below. */
246
306
  --sidebar-bg: var(--color-background-card);
247
- --sidebar-text: #1c2127;
248
- --sidebar-text-muted: #5f6b7c;
307
+ --sidebar-text: var(--color-text-primary);
308
+ --sidebar-text-muted: var(--color-text-muted);
249
309
  --sidebar-active-bg: var(--color-surface-tint);
250
- --sidebar-active-text: #1c2127;
310
+ --sidebar-active-text: var(--color-text-primary);
251
311
  --sidebar-hover-bg: var(--color-surface-tint);
252
312
  /* Icon tint for the active nav row — not a full fill, so
253
313
  --sidebar-active-bg/text above stay the "you're here" pill and
@@ -289,22 +349,27 @@
289
349
  sits below the hover/tint tier. Modest steps between each (not a
290
350
  dramatic jump) plus real borders is what keeps a dark theme
291
351
  reading crisp instead of washed out — see --color-border below. */
292
- --color-background-global: #121214;
293
- --color-background-card: #1b1b1f;
294
- --color-surface-tint: #232328;
352
+ --color-background-global: #09090b;
353
+ --color-background-card: #18181b;
354
+ --color-surface-tint: #27272a;
295
355
  --color-input: var(--color-background-card);
296
356
  /* Not pure white — pure white text on a near-black surface causes
297
357
  halation (a glow/vibrate effect on the eye over long sessions).
298
358
  ~92% white instead. */
299
- --color-text-primary: #e9e9ec;
359
+ --color-text-primary: #fafafa;
300
360
  /* ~7:1 on card */
301
- --color-text-secondary: #9c9da5;
361
+ --color-text-secondary: #d4d4d8;
302
362
  /* ~4.7:1 on card — was #5f6b7c at 2.7:1, failing AA body text */
303
- --color-text-muted: #7e7f87;
363
+ --color-text-muted: #a1a1aa;
304
364
  /* Derived from text-primary's own RGB (233,233,236) — same
305
365
  single-source-of-truth reasoning as :root's own --color-border. */
306
- --color-border: rgba(233, 233, 236, 0.09);
307
- --color-border-strong: rgba(233, 233, 236, 0.16);
366
+ --color-border: #27272a;
367
+ --color-border-strong: #3f3f46;
368
+ /* A genuine step above the card, which is what makes a dropdown read
369
+ as floating rather than pasted on. */
370
+ --color-surface-raised: #1f1f23;
371
+ --color-border-raised: #3f3f46;
372
+ --shadow-popover: 0 4px 6px -2px rgba(0, 0, 0, 0.5), 0 16px 40px -10px rgba(0, 0, 0, 0.75);
308
373
  --color-hover: var(--color-surface-tint);
309
374
 
310
375
  /* Brighter counterparts of :root's accent set — a dark badge fill
@@ -317,7 +382,7 @@
317
382
  --accent-violet: #d4ccff;
318
383
  --accent-amber: #ffc879;
319
384
  --accent-bronze: #ffab73;
320
- --accent-blue: #4188ff;
385
+ --accent-blue: #7ba9ff;
321
386
  --accent-cyan: #91d0ff;
322
387
  --accent-mint: #2ced83;
323
388
  --accent-fuchsia: #ff9dbd;
@@ -329,10 +394,10 @@
329
394
  --color-brand: var(--color-text-primary);
330
395
  --color-brand-hover: color-mix(in srgb, var(--color-brand) 90%, black);
331
396
  --color-brand-active: color-mix(in srgb, var(--color-brand) 75%, black);
332
- --color-brand-text: #121212;
397
+ --color-brand-text: #09090b;
333
398
 
334
- --shadow-btn-primary: none;
335
- --shadow-btn-secondary: none;
399
+ --shadow-btn-primary: 0 1px 2px rgba(9, 9, 11, 0.12);
400
+ --shadow-btn-secondary: 0 1px 2px rgba(9, 9, 11, 0.05);
336
401
 
337
402
  --color-secondary-fill: var(--color-surface-tint);
338
403
  --color-secondary-border: var(--color-border-strong);
@@ -350,44 +415,51 @@
350
415
  --color-success: var(--accent-mint);
351
416
 
352
417
  --status-draft-text: var(--accent-slate);
353
- --status-draft-bg: #2a2d2f;
418
+ --status-draft-bg: #262a31;
354
419
  --status-backordered-text: var(--accent-violet);
355
- --status-backordered-bg: #35294a;
420
+ --status-backordered-bg: #312747;
356
421
  --status-low-stock-text: var(--accent-amber);
357
- --status-low-stock-bg: #43380f;
422
+ --status-low-stock-bg: #3f3411;
358
423
  --status-critical-stock-text: var(--color-critical);
359
- --status-critical-stock-bg: #4a3010;
424
+ --status-critical-stock-bg: #452c12;
360
425
  --status-out-of-stock-text: var(--accent-bronze);
361
- --status-out-of-stock-bg: #4a2c14;
426
+ --status-out-of-stock-bg: #452916;
362
427
  --status-partially-fulfilled-text: var(--accent-blue);
363
- --status-partially-fulfilled-bg: #1c2c4a;
428
+ --status-partially-fulfilled-bg: #1b2740;
364
429
  --status-shipped-text: var(--accent-cyan);
365
- --status-shipped-bg: #103846;
430
+ --status-shipped-bg: #0f3341;
366
431
  --status-delivered-text: var(--accent-mint);
367
- --status-delivered-bg: #163c22;
432
+ --status-delivered-bg: #133620;
368
433
  --status-cancelled-text: var(--accent-fuchsia);
369
- --status-cancelled-bg: #472233;
434
+ --status-cancelled-bg: #421f2f;
370
435
  --status-overdue-text: var(--color-danger);
371
- --status-overdue-bg: #452023;
436
+ --status-overdue-bg: #401d21;
372
437
 
373
- --banner-warning-bg: #412d00;
438
+ --banner-warning-bg: #3a2907;
374
439
  --banner-warning-border: var(--color-warning);
375
- --banner-danger-bg: #5f0716;
440
+ --banner-danger-bg: #4a1119;
376
441
  --banner-danger-border: var(--color-danger);
377
- --banner-success-bg: #073630;
442
+ --banner-success-bg: #0a3129;
378
443
  --banner-success-border: var(--color-success);
379
444
 
380
- --shadow-xs: none;
381
- --shadow-sm: none;
382
- --shadow-md: none;
383
- --shadow-lg: none;
384
-
385
- /* Deliberately NOT --color-background-card — the sidebar is now a
386
- distinct chrome rail, true black, sitting a full step below the
387
- page canvas rather than matching the card surface. Common
388
- enterprise-dark-theme pattern (VS Code's activity bar, Slack's
389
- rail): the frame reads as structurally separate from content. */
390
- --sidebar-bg: #000000;
445
+ /* A dark surface cannot be lifted by a soft grey shadow — there is
446
+ nothing lighter behind it to catch the light. So these go darker
447
+ and deeper than their light-theme counterparts, which is what
448
+ actually reads as elevation on a near-black page. */
449
+ --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.32);
450
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 8px -3px rgba(0, 0, 0, 0.5);
451
+ --shadow-md: 0 2px 4px -1px rgba(0, 0, 0, 0.4), 0 8px 22px -4px rgba(0, 0, 0, 0.6);
452
+ --shadow-lg: 0 4px 8px -2px rgba(0, 0, 0, 0.45), 0 24px 56px -12px rgba(0, 0, 0, 0.75);
453
+
454
+ /* A distinct chrome rail, still — but a step *above* the canvas now
455
+ rather than below it. It used to be true black under a #0d0f13
456
+ page, which read as a rail. Against zinc-950 the same black is
457
+ three values away from the page and the rail disappeared, so the
458
+ separation now comes from the other direction: zinc-900, the same
459
+ surface as a card, which is where shadcn puts a sidebar too.
460
+ The frame still reads as structurally separate from content, which
461
+ was the point of the original choice. */
462
+ --sidebar-bg: #18181b;
391
463
  --sidebar-text: var(--color-text-primary);
392
464
  --sidebar-text-muted: var(--color-text-muted);
393
465
  /* Two distinct white-overlay intensities on top of --sidebar-bg —
@@ -398,7 +470,7 @@
398
470
  --sidebar-active-text: var(--color-text-primary);
399
471
  --sidebar-hover-bg: rgba(255, 255, 255, 0.06);
400
472
  --sidebar-active-icon: var(--sidebar-active-text);
401
- --sidebar-rail-bg: #000000;
473
+ --sidebar-rail-bg: #18181b;
402
474
 
403
475
  color-scheme: dark;
404
476
  }
@@ -409,15 +481,18 @@
409
481
  takes the nearest declaration in the tree, so relying on :root
410
482
  alone would lose to a dark attribute set higher up. */
411
483
  [data-theme="light"] {
412
- --color-background-global: #f6f7f9;
484
+ --color-background-global: #f4f4f5;
413
485
  --color-background-card: #ffffff;
414
- --color-surface-tint: #eef0f3;
415
- --color-input: #fdfdfd;
416
- --color-text-primary: #1c2127;
417
- --color-text-secondary: #56606f;
418
- --color-text-muted: #6b7684;
419
- --color-border: rgba(28, 33, 39, 0.1);
420
- --color-border-strong: rgba(28, 33, 39, 0.2);
486
+ --color-surface-tint: #e8e8ea;
487
+ --color-input: #ffffff;
488
+ --color-text-primary: #09090b;
489
+ --color-text-secondary: #52525b;
490
+ --color-text-muted: #6b6b74;
491
+ --color-border: #e4e4e7;
492
+ --color-border-strong: #d4d4d8;
493
+ --color-surface-raised: #ffffff;
494
+ --color-border-raised: #d4d4d8;
495
+ --shadow-popover: 0 4px 6px -2px rgba(9, 9, 11, 0.08), 0 12px 28px -8px rgba(9, 9, 11, 0.18);
421
496
  --color-hover: var(--color-surface-tint);
422
497
 
423
498
  --color-brand: var(--color-text-primary);
@@ -425,8 +500,8 @@
425
500
  --color-brand-active: color-mix(in srgb, var(--color-brand) 70%, black);
426
501
  --color-brand-text: #ffffff;
427
502
 
428
- --shadow-btn-primary: none;
429
- --shadow-btn-secondary: none;
503
+ --shadow-btn-primary: 0 1px 2px rgba(0, 0, 0, 0.4);
504
+ --shadow-btn-secondary: 0 1px 2px rgba(0, 0, 0, 0.3);
430
505
 
431
506
  --color-secondary-fill: var(--color-surface-tint);
432
507
  --color-secondary-border: var(--color-border-strong);
@@ -443,43 +518,49 @@
443
518
  --color-success: #047b5d;
444
519
 
445
520
  --status-draft-text: var(--accent-slate);
446
- --status-draft-bg: #f0f4f9;
521
+ --status-draft-bg: #eef1f6;
447
522
  --status-backordered-text: var(--accent-violet);
448
- --status-backordered-bg: #f7ecfe;
523
+ --status-backordered-bg: #f3ecfd;
449
524
  --status-low-stock-text: var(--accent-amber);
450
- --status-low-stock-bg: #ffd289;
525
+ --status-low-stock-bg: #fff1d9;
451
526
  --status-critical-stock-text: var(--color-critical);
452
- --status-critical-stock-bg: #ffe0c2;
527
+ --status-critical-stock-bg: #ffe7d1;
453
528
  --status-out-of-stock-text: var(--accent-bronze);
454
- --status-out-of-stock-bg: #ffede1;
529
+ --status-out-of-stock-bg: #ffeee4;
455
530
  --status-partially-fulfilled-text: var(--accent-blue);
456
- --status-partially-fulfilled-bg: #e7f2ff;
531
+ --status-partially-fulfilled-bg: #e8f1fe;
457
532
  --status-shipped-text: var(--accent-cyan);
458
- --status-shipped-bg: #92e5ff;
533
+ --status-shipped-bg: #ddf0fb;
459
534
  --status-delivered-text: var(--accent-mint);
460
- --status-delivered-bg: #ddf8d8;
535
+ --status-delivered-bg: #e1f5e6;
461
536
  --status-cancelled-text: var(--accent-fuchsia);
462
- --status-cancelled-bg: #ffcbda;
537
+ --status-cancelled-bg: #fde7ee;
463
538
  --status-overdue-text: var(--color-danger);
464
- --status-overdue-bg: #ffecee;
539
+ --status-overdue-bg: #feebec;
465
540
 
466
- --banner-warning-bg: #fff1e3;
541
+ --banner-warning-bg: #fff3e6;
467
542
  --banner-warning-border: var(--color-warning);
468
- --banner-danger-bg: #fee8eb;
543
+ --banner-danger-bg: #fdeaed;
469
544
  --banner-danger-border: var(--color-danger);
470
- --banner-success-bg: #cdfed4;
545
+ --banner-success-bg: #e2f7e6;
471
546
  --banner-success-border: #047b5d;
472
547
 
548
+ /* Deliberately still flat, unlike :root's. The only thing that pins
549
+ itself to light is a print page, and a shadow on paper is a grey
550
+ smear. This block exists to restate the light palette inside a dark
551
+ ancestor — it is a copy of :root's values by necessity (custom
552
+ properties cannot be re-pointed at another block), so anything
553
+ changed above has to be changed here too. */
473
554
  --shadow-xs: none;
474
555
  --shadow-sm: none;
475
556
  --shadow-md: none;
476
557
  --shadow-lg: none;
477
558
 
478
559
  --sidebar-bg: var(--color-background-card);
479
- --sidebar-text: #1c2127;
480
- --sidebar-text-muted: #5f6b7c;
560
+ --sidebar-text: #09090b;
561
+ --sidebar-text-muted: var(--color-text-muted);
481
562
  --sidebar-active-bg: var(--color-surface-tint);
482
- --sidebar-active-text: #1c2127;
563
+ --sidebar-active-text: var(--color-text-primary);
483
564
  --sidebar-hover-bg: var(--color-surface-tint);
484
565
  --sidebar-active-icon: var(--sidebar-active-text);
485
566
  --sidebar-rail-bg: var(--color-background-card);
@@ -600,9 +681,24 @@ td a:hover {
600
681
  font-weight: var(--font-weight-semibold);
601
682
  }
602
683
 
684
+ /* A label should recede so the value it names is what gets scanned.
685
+ Medium weight at a step down from body, in secondary ink — quieter
686
+ than the data, still comfortably above AA. */
603
687
  label {
604
688
  font-size: var(--text-sm);
605
- font-weight: 400;
689
+ font-weight: var(--font-weight-medium);
690
+ color: var(--color-text-secondary);
691
+ line-height: 1.4;
692
+ }
693
+
694
+ /* A required field says so once, here, rather than each form inventing
695
+ its own asterisk-or-not. Marked on the label so it reads before the
696
+ field rather than after it. */
697
+ label[data-required]::after {
698
+ content: "*";
699
+ margin-left: 3px;
700
+ color: var(--color-danger);
701
+ font-weight: var(--font-weight-regular);
606
702
  }
607
703
 
608
704
  /* Primary action — highest-contrast neutral fill (--color-brand),
@@ -717,14 +813,24 @@ button:focus-visible {
717
813
  background: var(--color-destructive-hover);
718
814
  }
719
815
 
816
+ /* font-family is inherited; size, weight and colour are stated.
817
+ `font: inherit` alone meant a field took its type from whatever
818
+ wrapped it — and most fields in this app are wrapped in their own
819
+ <label>, which is a step smaller and now medium weight. So a field
820
+ inside a label rendered at 13px and one outside at 14px, and the
821
+ label change would have made the wrapped ones semibold. The value a
822
+ person typed should look the same everywhere it appears. */
720
823
  input,
721
824
  select,
722
825
  textarea {
723
- font: inherit;
826
+ font-family: inherit;
827
+ font-size: var(--text-base);
828
+ font-weight: var(--font-weight-regular);
829
+ line-height: 1.4;
724
830
  width: 100%;
725
- padding: 7px 10px;
726
- margin-top: 4px;
727
- border: 1px solid var(--color-border);
831
+ padding: 8px 12px;
832
+ margin-top: 6px;
833
+ border: 1px solid var(--color-border-strong);
728
834
  border-radius: var(--radius-sm);
729
835
  background: var(--color-input);
730
836
  color: var(--color-text-primary);
@@ -733,6 +839,11 @@ textarea {
733
839
  box-shadow 0.15s ease;
734
840
  }
735
841
 
842
+ input::placeholder,
843
+ textarea::placeholder {
844
+ color: var(--color-text-muted);
845
+ }
846
+
736
847
  /* The browser's native select arrow renders at its own baseline/size
737
848
  per-OS (a mismatched-color chevron and off-center text) —
738
849
  appearance:none strips it entirely and this draws a fixed, always-
@@ -740,21 +851,135 @@ textarea {
740
851
  bottom padding is what actually centers the selected value; the
741
852
  native arrow's box was throwing that off by consuming space
742
853
  asymmetrically. The open option list itself stays OS-native — no
743
- browser lets CSS reach inside it. */
744
- select {
854
+ browser lets CSS reach inside it.
855
+
856
+ `input[list]` gets the same chevron. It opens a dropdown of
857
+ suggestions exactly as a select does, but Chrome draws no affordance
858
+ for one at all — so a datalist field read as a plain text box while
859
+ sitting between two selects that both had an arrow. The difference
860
+ that remains, that you may also type a value not on the list, is the
861
+ right one to leave invisible: typing into a text box is
862
+ self-evidently allowed. */
863
+ /* ------------------------------------------------------------------
864
+ Choosing one of something: four controls, one appearance.
865
+
866
+ They are four because they do different jobs, and collapsing them
867
+ would cost something real each time:
868
+
869
+ select a closed set small enough to scroll. Cheap,
870
+ native, keyboard- and mobile-friendly.
871
+ Combobox a closed set too large to scroll — there are
872
+ 4,021 customers here, and a <select> of 4,021
873
+ options is not a control, it is a punishment.
874
+ input[type=date] a date. The native one carries the OS picker,
875
+ typed entry, locale order and mobile wheels;
876
+ a hand-built calendar loses all four and is a
877
+ well-known accessibility swamp.
878
+
879
+ Which to reach for:
880
+ under ~15 fixed options, closed -> select
881
+ anything else -> Combobox
882
+
883
+ Not input[list]. A datalist accepts typed values, which is why it
884
+ looked like the right control for an open set — but its suggestion
885
+ popup is drawn by the browser and no CSS reaches inside it, so the
886
+ field opens looking like nothing else on the page. Combobox's
887
+ `onCreate` covers the open set with the app's own dropdown: where
888
+ there is no table to insert into, "creating" is just accepting the
889
+ typed text (see ChannelSelect, VariantSelect).
890
+
891
+ What they share is the mark at the right-hand end that says "this
892
+ opens something", and it is the same mark in the same place on all
893
+ of them. It was not: the Combobox had none at all, so a customer or
894
+ a warehouse read as a plain text box beside fields that plainly
895
+ opened.
896
+ ------------------------------------------------------------------ */
897
+ select,
898
+ .combobox-input {
745
899
  appearance: none;
746
900
  -webkit-appearance: none;
747
- line-height: normal;
748
901
  padding-right: 32px;
749
902
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6' fill='none'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%23727676' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
750
903
  background-repeat: no-repeat;
751
904
  background-position: right 10px center;
752
905
  }
753
906
 
754
- /* Focus state: just the field's own border switching to brand color —
755
- no outline ring outside the field's edge (an outline-offset ring
756
- there used to double up as a second, floating rectangle around the
757
- input instead of reading as one focused box). */
907
+ /* Only a native select needs this, and sharing it was a bug worth
908
+ recording. `line-height: normal` is here to re-centre a select's
909
+ text after appearance:none strips the native box — a select does not
910
+ honour the 1.4 the other fields use. A .combobox-input is a real
911
+ <input> and does, so inheriting `normal` (about 1.2) made it roughly
912
+ three pixels shorter than the text field and the date field beside
913
+ it: same padding, same border, visibly smaller box.
914
+
915
+ The chevron above is what the two share. The line-height is not. */
916
+ select {
917
+ line-height: normal;
918
+ }
919
+
920
+ /* A date field's calendar button is drawn by the browser, not by us —
921
+ it cannot be swapped for the chevron above, but it can be given the
922
+ same weight and the same position, so a row of dates and selects
923
+ reads as one row of controls rather than two kinds. Sized and dimmed
924
+ to match; `color-scheme` (set on :root and both themes) is what
925
+ makes it and the popup follow the theme. */
926
+ input[type="date"]::-webkit-calendar-picker-indicator {
927
+ opacity: 0.55;
928
+ cursor: pointer;
929
+ padding: 0;
930
+ margin-right: 2px;
931
+ }
932
+
933
+ input[type="date"]::-webkit-calendar-picker-indicator:hover {
934
+ opacity: 1;
935
+ }
936
+
937
+ /* A button that carries no button chrome — a chevron toggle, a
938
+ "Forgot password?" link, an inline remove. Seven of these were made
939
+ bare by hand with `style={{ background: "none", border: "none",
940
+ padding: 0 }}`, and every one of them missed the same thing: the
941
+ bare `button` rule above also sets `box-shadow:
942
+ var(--shadow-btn-primary)`, which an inline background and border
943
+ do not clear.
944
+ That was invisible while every --shadow-* token was `none`. Turning
945
+ them on gave each of these buttons a floating rectangle its author
946
+ had explicitly tried to remove — the "extra layer" behind
947
+ "Additional information". Declared once here so the next one cannot
948
+ miss it. */
949
+ .btn-bare {
950
+ background: none;
951
+ border: none;
952
+ box-shadow: none;
953
+ padding: 0;
954
+ font: inherit;
955
+ color: inherit;
956
+ cursor: pointer;
957
+ text-align: inherit;
958
+ }
959
+
960
+ .btn-bare:hover:not(:disabled) {
961
+ background: none;
962
+ box-shadow: none;
963
+ transform: none;
964
+ }
965
+
966
+ /* Focus: the border goes to brand ink and a soft ring is drawn *by the
967
+ field itself*, with box-shadow rather than outline.
968
+ The border alone was the previous answer, after an `outline` with an
969
+ offset had read as a second rectangle floating around the input. A
970
+ box-shadow spread follows the border-radius exactly, so it reads as
971
+ one thickened edge — which fixes the original complaint without
972
+ leaving keyboard users hunting for which field they are in. */
973
+ input:focus-visible,
974
+ select:focus-visible,
975
+ textarea:focus-visible {
976
+ outline: none;
977
+ border-color: var(--color-brand);
978
+ box-shadow: 0 0 0 3px var(--color-focus-ring);
979
+ }
980
+
981
+ /* Kept for pointer focus too — a click into a field should still show
982
+ which one is live, just without the ring shouting. */
758
983
  input:focus,
759
984
  select:focus,
760
985
  textarea:focus {
@@ -762,6 +987,31 @@ textarea:focus {
762
987
  border-color: var(--color-brand);
763
988
  }
764
989
 
990
+ /* A field the browser or the form has judged invalid.
991
+ There was no error state at all: a rejected field looked exactly
992
+ like an accepted one, and the only signal was whatever the server
993
+ said afterwards. :user-invalid rather than :invalid deliberately —
994
+ it waits until somebody has actually interacted, so an untouched
995
+ form does not open covered in red. */
996
+ input[aria-invalid="true"],
997
+ select[aria-invalid="true"],
998
+ textarea[aria-invalid="true"],
999
+ input:user-invalid,
1000
+ select:user-invalid,
1001
+ textarea:user-invalid {
1002
+ border-color: var(--color-danger);
1003
+ }
1004
+
1005
+ input[aria-invalid="true"]:focus-visible,
1006
+ select[aria-invalid="true"]:focus-visible,
1007
+ textarea[aria-invalid="true"]:focus-visible,
1008
+ input:user-invalid:focus-visible,
1009
+ select:user-invalid:focus-visible,
1010
+ textarea:user-invalid:focus-visible {
1011
+ border-color: var(--color-danger);
1012
+ box-shadow: 0 0 0 3px var(--color-focus-ring-danger);
1013
+ }
1014
+
765
1015
  input:disabled,
766
1016
  select:disabled,
767
1017
  textarea:disabled {
@@ -1028,10 +1278,10 @@ a.card:hover {
1028
1278
  }
1029
1279
 
1030
1280
  .modal-panel {
1031
- background: var(--color-background-card);
1032
- border: 1px solid var(--color-border);
1281
+ background: var(--color-surface-raised);
1282
+ border: 1px solid var(--color-border-raised);
1033
1283
  border-radius: var(--radius-lg);
1034
- box-shadow: var(--shadow-lg);
1284
+ box-shadow: var(--shadow-popover);
1035
1285
  width: 100%;
1036
1286
  max-width: 640px;
1037
1287
  max-height: 80vh;