@hattisar/design-tokens 0.3.5

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 ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@hattisar/design-tokens",
3
+ "version": "0.3.5",
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
+ "main": "tokens.css",
6
+ "files": [
7
+ "tokens.css",
8
+ "assets"
9
+ ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/hattisar/hattisar-inventory.git"
16
+ }
17
+ }
package/tokens.css ADDED
@@ -0,0 +1,1067 @@
1
+ /**
2
+ * Mirrors shared/src/design-tokens.ts. Neutral scale carries structure
3
+ * and layout; --color-brand is charcoal ink (var(--color-text-primary)),
4
+ * not a separate hue — no indigo/brand accent color, primary actions
5
+ * are the same black-on-white (white-on-black in dark theme) as body
6
+ * text. Color is otherwise reserved for the ten inventory status tiers
7
+ * (see StatusBadge.tsx) plus the three page-level alert banners. Every
8
+ * text/background pairing here is verified >=4.5:1 (WCAG AA body text).
9
+ *
10
+ * :root holds the light theme (the default). [data-theme="dark"] on
11
+ * <html> overrides every color/status/banner/sidebar/shadow variable
12
+ * below for dark mode; [data-theme="light"] re-asserts the same light
13
+ * values so a subtree (print pages) can pin itself to light and win
14
+ * over an ancestor's dark attribute. Shape tokens (radius, spacing,
15
+ * type, motion) are theme-invariant and live in :root only.
16
+ *
17
+ * Canonical source for every Hattisar frontend (Inventory, Storefront,
18
+ * Console) — published as @hattisar/design-tokens so a change here
19
+ * propagates to all three on their own release schedule (version bump
20
+ * + redeploy), rather than three copies silently drifting apart. Holds
21
+ * only what's genuinely identical across all three: color/shape tokens,
22
+ * the base reset, typography, buttons, form controls, .card, the auth
23
+ * page shell, and the modal primitive. Anything app-specific (nav/
24
+ * sidebar shape, data tables, dashboards, chat UI, ...) stays local to
25
+ * each app's own stylesheet, loaded after this one.
26
+ */
27
+ :root {
28
+ /* Flat and opaque throughout — no gradient, no blur; color is
29
+ reserved for the status-tier badges and the brand accent below,
30
+ never the page chrome itself.
31
+ Neutral scale re-tuned for WCAG AA (every text/bg pair here hits
32
+ >=4.5:1) and for a real 3-step surface ladder — surface-tint used
33
+ to equal background-global exactly, which meant hover states had
34
+ no visible target to land on. */
35
+ --color-background-global: #f6f7f9;
36
+ --color-background-card: #ffffff;
37
+ --color-surface-tint: #eef0f3;
38
+ --color-input: #fdfdfd;
39
+ --color-text-primary: #1c2127;
40
+ /* ~6.4:1 on card */
41
+ --color-text-secondary: #56606f;
42
+ /* ~4.6:1 on card — was #8f99a8 at 2.9:1, failing AA body text */
43
+ --color-text-muted: #6b7684;
44
+ /* Derived from text-primary's own RGB (28,33,39) rather than a
45
+ separately maintained literal, so border and text can never drift
46
+ apart the way they used to (border was rgb(17,20,24), a different
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);
50
+ --color-hover: var(--color-surface-tint);
51
+
52
+ /* Named accents — the same 8 hues most of the status tiers below
53
+ already used, promoted into reusable tokens instead of each tier repeating
54
+ its own literal hex (removes the duplication, doesn't change the
55
+ colors). Tuned for small, dense badge text — text on a light tinted
56
+ pill needs a genuinely dark/saturated shade, not a vibrant mid-tone,
57
+ to stay readable at a glance across a long table or KPI tile alike.
58
+ Also not used for the primary button system below — that stays the
59
+ existing neutral "ink" fill by design (grays carry structure, color
60
+ is reserved for status/data). Reuses --color-text-primary rather
61
+ than its own literal hex, so "draft" badge text always tracks
62
+ whatever the page's own ink color is. */
63
+ --accent-slate: var(--color-text-primary);
64
+ --accent-violet: #5700d1;
65
+ /* Deepened from #956f00 (was 4.61:1 on white — technically passing
66
+ but with no margin); now ~5.2:1. --color-warning below reuses this
67
+ rather than repeating its own literal, same reasoning as
68
+ --color-success/--accent-mint further down. */
69
+ --accent-amber: #8a6600;
70
+ --accent-bronze: #a35200;
71
+ --accent-blue: #005bd3;
72
+ --accent-cyan: #007cb4;
73
+ --accent-mint: #047b5d;
74
+ --accent-fuchsia: #984062;
75
+
76
+ /* Primary button — charcoal brand ink. Reuses --color-text-primary
77
+ (not its own literal hex) so the button fill is exactly the same
78
+ ink as body text — these used to drift apart by a few points of
79
+ warmth/coolness whenever one was retuned without the other. */
80
+ --color-brand: var(--color-text-primary);
81
+ --color-brand-hover: color-mix(in srgb, var(--color-brand) 85%, black);
82
+ --color-brand-active: color-mix(in srgb, var(--color-brand) 70%, black);
83
+ --color-brand-text: #ffffff;
84
+
85
+ /* Secondary button — bordered light-gray fill. Reuses
86
+ --color-surface-tint/--color-border-strong/--color-text-primary
87
+ rather than their own literal hex, so the secondary button always
88
+ tracks the same neutral scale as the rest of the page. */
89
+ --color-secondary-fill: var(--color-surface-tint);
90
+ --color-secondary-border: var(--color-border-strong);
91
+ --color-secondary-text: var(--color-text-primary);
92
+ --color-secondary-hover: color-mix(in srgb, var(--color-text-primary) 6%, var(--color-secondary-fill));
93
+
94
+ /* Destructive button — irreversible actions only (delete, void). */
95
+ --color-destructive-fill: #c70a24;
96
+ --color-destructive-hover: #a30a24;
97
+ --color-destructive-text: #ffffff;
98
+
99
+ /* General-purpose semantic text/icon colors (form errors, savebar
100
+ warning icon, scan-log rows, ...) — distinct from the dedicated
101
+ --status-* fill/text pairs below, which StatusBadge uses directly.
102
+ Warning/success reuse the matching accent token below rather than
103
+ repeating their own literal, so the two can never drift apart the
104
+ way --color-warning and --accent-amber used to (same value, two
105
+ separately-maintained literals). */
106
+ --color-warning: var(--accent-amber);
107
+ /* Sits between warning and danger — near-empty stock, more urgent
108
+ than a plain low-stock reorder point but not yet actually out.
109
+ See --status-critical-stock-* below and StatusBadge.tsx's
110
+ "criticalStock" tier. */
111
+ --color-critical: #a83c00;
112
+ --color-danger: #c70a24;
113
+ --color-success: var(--accent-mint);
114
+
115
+ /* The ten inventory status tiers — the only color in the UI outside
116
+ the button system and these three banners. Each pairs a saturated
117
+ text color with its own soft fill, applied as-is (no color-mix
118
+ derivation) so the exact hue is controlled directly rather than
119
+ computed. See StatusBadge.tsx for the status keyword -> tier
120
+ mapping — ten distinct hues, deliberately no collisions. */
121
+ --status-draft-text: var(--accent-slate);
122
+ --status-draft-bg: #f0f4f9;
123
+ --status-backordered-text: var(--accent-violet);
124
+ --status-backordered-bg: #f7ecfe;
125
+ --status-low-stock-text: var(--accent-amber);
126
+ --status-low-stock-bg: #ffd289;
127
+ /* Near-empty stock — more urgent than low-stock's reorder-point
128
+ nudge, not yet fully out. Bg sits between low-stock's punchy
129
+ amber-gold and out-of-stock's pale peach; text is --color-critical. */
130
+ --status-critical-stock-text: var(--color-critical);
131
+ --status-critical-stock-bg: #ffe0c2;
132
+ --status-out-of-stock-text: var(--accent-bronze);
133
+ --status-out-of-stock-bg: #ffede1;
134
+ --status-partially-fulfilled-text: var(--accent-blue);
135
+ --status-partially-fulfilled-bg: #e7f2ff;
136
+ --status-shipped-text: var(--accent-cyan);
137
+ --status-shipped-bg: #92e5ff;
138
+ --status-delivered-text: var(--accent-mint);
139
+ --status-delivered-bg: #ddf8d8;
140
+ --status-cancelled-text: var(--accent-fuchsia);
141
+ --status-cancelled-bg: #ffcbda;
142
+ --status-overdue-text: var(--color-danger);
143
+ --status-overdue-bg: #ffecee;
144
+
145
+ /* Page-level warning/danger/success banners — distinct from the row
146
+ badges above: a border plus a matching soft fill, no separate text
147
+ color (banner text uses --color-text-primary as usual). Warning/
148
+ danger borders reference the semantic tokens directly (rather than
149
+ repeating their own literal hex) so they can never drift out of
150
+ sync the way they briefly did here. Success intentionally doesn't:
151
+ it was never meant to mirror --color-success, it's its own shade. */
152
+ --banner-warning-bg: #fff1e3;
153
+ --banner-warning-border: var(--color-warning);
154
+ --banner-danger-bg: #fee8eb;
155
+ --banner-danger-border: var(--color-danger);
156
+ --banner-success-bg: #cdfed4;
157
+ --banner-success-border: var(--color-success);
158
+
159
+ /* A restrained scale, not flat — every step deliberately small, since
160
+ most controls here are compact (30-36px buttons/inputs, smaller
161
+ badges); a radius sized for a big consumer-app card looks
162
+ disproportionately round on something this size. --radius-full is
163
+ the one exception, reserved strictly for shapes that are
164
+ geometrically circles/pills by function (dots, the small circular
165
+ remove-buttons inside chips, the settings toggle switch) — never
166
+ applied to a badge/tag/chip just for a rounder look; those use
167
+ --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;
173
+ --radius-full: 9999px;
174
+
175
+ --shadow-xs: none;
176
+ --shadow-sm: none;
177
+ --shadow-md: none;
178
+ --shadow-lg: none;
179
+
180
+ /* One shared tier for every FloatingPanel-based dropdown/menu
181
+ (row actions, column picker, combobox, select, table-actions,
182
+ "+ Create", report filters, global search, the header drawer,
183
+ the Save-button split menu, ...) — all identical position:fixed
184
+ portals to document.body (see FloatingPanel.tsx), so none of them
185
+ has a reason to out-rank another. Before this token they'd
186
+ accumulated six different literal values (30/70/90/95/110/unset)
187
+ purely by accident of whoever wrote each one, which meant a
188
+ dropdown opened from inside a Modal (--z-modal below, 100) could
189
+ silently render behind it depending on which component it was.
190
+ Set above --z-modal for exactly that reason — a menu must always
191
+ be able to open on top of a dialog it lives inside. */
192
+ --z-dropdown: 110;
193
+ --z-modal: 100;
194
+
195
+ /* 4px-based spacing scale — new markup (bento tiles, order cards)
196
+ should reach for these instead of new magic numbers. Existing
197
+ inline styles/hardcoded px values elsewhere are untouched. */
198
+ --space-1: 4px;
199
+ --space-2: 8px;
200
+ --space-3: 12px;
201
+ --space-4: 16px;
202
+ --space-5: 20px;
203
+ --space-6: 24px;
204
+ --space-8: 32px;
205
+ --space-10: 40px;
206
+ --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;
214
+ --text-md: 15px;
215
+ --text-lg: 17px;
216
+ --text-xl: 19px;
217
+ --text-2xl: 23px;
218
+ --text-3xl: 29px;
219
+
220
+ /* A real weight scale — headings, card titles, and KPI/stat values
221
+ use these to stand apart from the body text around them; dense
222
+ areas (table cells, form labels, buttons) stay on --font-weight-
223
+ regular so scanning a long list doesn't fight a page of bold text.
224
+ <strong>/<b> deliberately stay regular too (see the rule below) —
225
+ a separate, unrelated choice about semantic inline emphasis, not
226
+ part of this heading/value hierarchy. */
227
+ --font-weight-regular: 400;
228
+ --font-weight-medium: 500;
229
+ --font-weight-semibold: 600;
230
+ --font-weight-bold: 700;
231
+
232
+ /* Motion — formalizes the transition durations already scattered
233
+ through this file (mostly 0.1-0.15s) into shared tokens so new
234
+ micro-interactions (bento tiles, order cards, DataTable polish)
235
+ stay consistent with the existing feel. */
236
+ --ease-standard: cubic-bezier(0.2, 0, 0, 1);
237
+ --duration-fast: 100ms;
238
+ --duration-base: 150ms;
239
+ --duration-slow: 250ms;
240
+
241
+ --header-height: 52px;
242
+
243
+ /* Exact Hattisar Design match — aside/header sit at the same white
244
+ --hd-card-bg as every card (bordered, not tonally separate),
245
+ mirrored by dark theme's own literal Hattisar Design values below. */
246
+ --sidebar-bg: var(--color-background-card);
247
+ --sidebar-text: #1c2127;
248
+ --sidebar-text-muted: #5f6b7c;
249
+ --sidebar-active-bg: var(--color-surface-tint);
250
+ --sidebar-active-text: #1c2127;
251
+ --sidebar-hover-bg: var(--color-surface-tint);
252
+ /* Icon tint for the active nav row — not a full fill, so
253
+ --sidebar-active-bg/text above stay the "you're here" pill and
254
+ this just adds a distinct accent on top. Reuses --sidebar-active-
255
+ text rather than its own color, so the accent never drifts out of
256
+ sync with the text it's paired with. No left border (removed —
257
+ read as a stray dark mark down the row on top of the pill). */
258
+ --sidebar-active-icon: var(--sidebar-active-text);
259
+ --sidebar-rail-bg: var(--color-background-card);
260
+
261
+ /* Every left-hand nav rail in an app sharing this one width instead
262
+ of each picking its own. */
263
+ --sidebar-width: 240px;
264
+ --sidebar-width-collapsed: 64px;
265
+ --sidebar-height: calc(100vh - var(--header-height));
266
+
267
+ /* Three content widths, not one — simple field-only forms (Credit
268
+ Memos, Settings CRUD, ...), structured views with a sidebar or grid
269
+ (Dashboard, Customer/Vendor forms, Modules), and line-item-heavy
270
+ documents (Invoices, Purchase Orders, ...) don't read well at the
271
+ same width. Every page that opts into one of these also centers via
272
+ margin: 0 auto (see .record-form-layout > form, .wide-form-layout >
273
+ form, .dashboard-page-content, .settings-layout-content below) —
274
+ instead of each page picking its own guess at either. */
275
+ --page-width-small: 800px;
276
+ --page-width-medium: 1152px;
277
+ --page-width-large: 1600px;
278
+
279
+ color-scheme: light dark;
280
+ }
281
+
282
+ /* Dark theme — overrides every color/status/banner/sidebar/shadow
283
+ variable declared in :root above. Shape tokens (radius, spacing,
284
+ type, motion) are theme-invariant and not repeated here. */
285
+ [data-theme="dark"] {
286
+ /* Three-step dark surface ladder: sidebar/chrome sits deliberately
287
+ below the page canvas (true black, a distinct chrome rail rather
288
+ than "just another card"), page canvas sits below the card, card
289
+ sits below the hover/tint tier. Modest steps between each (not a
290
+ dramatic jump) plus real borders is what keeps a dark theme
291
+ 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;
295
+ --color-input: var(--color-background-card);
296
+ /* Not pure white — pure white text on a near-black surface causes
297
+ halation (a glow/vibrate effect on the eye over long sessions).
298
+ ~92% white instead. */
299
+ --color-text-primary: #e9e9ec;
300
+ /* ~7:1 on card */
301
+ --color-text-secondary: #9c9da5;
302
+ /* ~4.7:1 on card — was #5f6b7c at 2.7:1, failing AA body text */
303
+ --color-text-muted: #7e7f87;
304
+ /* Derived from text-primary's own RGB (233,233,236) — same
305
+ 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);
308
+ --color-hover: var(--color-surface-tint);
309
+
310
+ /* Brighter counterparts of :root's accent set — a dark badge fill
311
+ needs a light/saturated text color for contrast, the mirror image
312
+ of light mode's dark-on-pale text. Same 8 hues, same tier mapping,
313
+ just the dark-theme-appropriate shade of each. Reuses
314
+ --color-text-primary rather than its own literal hex, same
315
+ reasoning as :root's own --accent-slate. */
316
+ --accent-slate: var(--color-text-primary);
317
+ --accent-violet: #d4ccff;
318
+ --accent-amber: #ffc879;
319
+ --accent-bronze: #ffab73;
320
+ --accent-blue: #4188ff;
321
+ --accent-cyan: #91d0ff;
322
+ --accent-mint: #2ced83;
323
+ --accent-fuchsia: #ff9dbd;
324
+
325
+ /* Near-white fill for the primary action, with dark ink text. Reuses
326
+ --color-text-primary (not its own literal hex) so the button fill
327
+ is exactly the same "white ink" as body text — same reasoning as
328
+ :root's own --color-brand. */
329
+ --color-brand: var(--color-text-primary);
330
+ --color-brand-hover: color-mix(in srgb, var(--color-brand) 90%, black);
331
+ --color-brand-active: color-mix(in srgb, var(--color-brand) 75%, black);
332
+ --color-brand-text: #121212;
333
+
334
+ --shadow-btn-primary: none;
335
+ --shadow-btn-secondary: none;
336
+
337
+ --color-secondary-fill: var(--color-surface-tint);
338
+ --color-secondary-border: var(--color-border-strong);
339
+ --color-secondary-text: var(--color-text-primary);
340
+ --color-secondary-hover: color-mix(in srgb, white 6%, var(--color-secondary-fill));
341
+
342
+ --color-destructive-fill: #a30a24;
343
+ --color-destructive-hover: #c70a24;
344
+ --color-destructive-text: #ffffff;
345
+
346
+ --color-warning: var(--accent-amber);
347
+ /* Sits between warning and danger — see :root's --color-critical. */
348
+ --color-critical: #ffa057;
349
+ --color-danger: #f78086;
350
+ --color-success: var(--accent-mint);
351
+
352
+ --status-draft-text: var(--accent-slate);
353
+ --status-draft-bg: #2a2d2f;
354
+ --status-backordered-text: var(--accent-violet);
355
+ --status-backordered-bg: #35294a;
356
+ --status-low-stock-text: var(--accent-amber);
357
+ --status-low-stock-bg: #43380f;
358
+ --status-critical-stock-text: var(--color-critical);
359
+ --status-critical-stock-bg: #4a3010;
360
+ --status-out-of-stock-text: var(--accent-bronze);
361
+ --status-out-of-stock-bg: #4a2c14;
362
+ --status-partially-fulfilled-text: var(--accent-blue);
363
+ --status-partially-fulfilled-bg: #1c2c4a;
364
+ --status-shipped-text: var(--accent-cyan);
365
+ --status-shipped-bg: #103846;
366
+ --status-delivered-text: var(--accent-mint);
367
+ --status-delivered-bg: #163c22;
368
+ --status-cancelled-text: var(--accent-fuchsia);
369
+ --status-cancelled-bg: #472233;
370
+ --status-overdue-text: var(--color-danger);
371
+ --status-overdue-bg: #452023;
372
+
373
+ --banner-warning-bg: #412d00;
374
+ --banner-warning-border: var(--color-warning);
375
+ --banner-danger-bg: #5f0716;
376
+ --banner-danger-border: var(--color-danger);
377
+ --banner-success-bg: #073630;
378
+ --banner-success-border: var(--color-success);
379
+
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;
391
+ --sidebar-text: var(--color-text-primary);
392
+ --sidebar-text-muted: var(--color-text-muted);
393
+ /* Two distinct white-overlay intensities on top of --sidebar-bg —
394
+ hover lighter, active a touch stronger — so the current page's row
395
+ still reads as visibly different from a row you're just passing
396
+ the cursor over. */
397
+ --sidebar-active-bg: color-mix(in srgb, white 7%, transparent);
398
+ --sidebar-active-text: var(--color-text-primary);
399
+ --sidebar-hover-bg: rgba(255, 255, 255, 0.06);
400
+ --sidebar-active-icon: var(--sidebar-active-text);
401
+ --sidebar-rail-bg: #000000;
402
+
403
+ color-scheme: dark;
404
+ }
405
+
406
+ /* Explicit light theme — identical to :root's defaults. Exists so a
407
+ subtree can force light mode (print pages) and win over an
408
+ ancestor's [data-theme="dark"]: CSS custom-property resolution
409
+ takes the nearest declaration in the tree, so relying on :root
410
+ alone would lose to a dark attribute set higher up. */
411
+ [data-theme="light"] {
412
+ --color-background-global: #f6f7f9;
413
+ --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);
421
+ --color-hover: var(--color-surface-tint);
422
+
423
+ --color-brand: var(--color-text-primary);
424
+ --color-brand-hover: color-mix(in srgb, var(--color-brand) 85%, black);
425
+ --color-brand-active: color-mix(in srgb, var(--color-brand) 70%, black);
426
+ --color-brand-text: #ffffff;
427
+
428
+ --shadow-btn-primary: none;
429
+ --shadow-btn-secondary: none;
430
+
431
+ --color-secondary-fill: var(--color-surface-tint);
432
+ --color-secondary-border: var(--color-border-strong);
433
+ --color-secondary-text: var(--color-text-primary);
434
+ --color-secondary-hover: color-mix(in srgb, var(--color-text-primary) 6%, var(--color-secondary-fill));
435
+
436
+ --color-destructive-fill: #c70a24;
437
+ --color-destructive-hover: #a30a24;
438
+ --color-destructive-text: #ffffff;
439
+
440
+ --color-warning: #8a6600;
441
+ --color-critical: #a83c00;
442
+ --color-danger: #c70a24;
443
+ --color-success: #047b5d;
444
+
445
+ --status-draft-text: var(--accent-slate);
446
+ --status-draft-bg: #f0f4f9;
447
+ --status-backordered-text: var(--accent-violet);
448
+ --status-backordered-bg: #f7ecfe;
449
+ --status-low-stock-text: var(--accent-amber);
450
+ --status-low-stock-bg: #ffd289;
451
+ --status-critical-stock-text: var(--color-critical);
452
+ --status-critical-stock-bg: #ffe0c2;
453
+ --status-out-of-stock-text: var(--accent-bronze);
454
+ --status-out-of-stock-bg: #ffede1;
455
+ --status-partially-fulfilled-text: var(--accent-blue);
456
+ --status-partially-fulfilled-bg: #e7f2ff;
457
+ --status-shipped-text: var(--accent-cyan);
458
+ --status-shipped-bg: #92e5ff;
459
+ --status-delivered-text: var(--accent-mint);
460
+ --status-delivered-bg: #ddf8d8;
461
+ --status-cancelled-text: var(--accent-fuchsia);
462
+ --status-cancelled-bg: #ffcbda;
463
+ --status-overdue-text: var(--color-danger);
464
+ --status-overdue-bg: #ffecee;
465
+
466
+ --banner-warning-bg: #fff1e3;
467
+ --banner-warning-border: var(--color-warning);
468
+ --banner-danger-bg: #fee8eb;
469
+ --banner-danger-border: var(--color-danger);
470
+ --banner-success-bg: #cdfed4;
471
+ --banner-success-border: #047b5d;
472
+
473
+ --shadow-xs: none;
474
+ --shadow-sm: none;
475
+ --shadow-md: none;
476
+ --shadow-lg: none;
477
+
478
+ --sidebar-bg: var(--color-background-card);
479
+ --sidebar-text: #1c2127;
480
+ --sidebar-text-muted: #5f6b7c;
481
+ --sidebar-active-bg: var(--color-surface-tint);
482
+ --sidebar-active-text: #1c2127;
483
+ --sidebar-hover-bg: var(--color-surface-tint);
484
+ --sidebar-active-icon: var(--sidebar-active-text);
485
+ --sidebar-rail-bg: var(--color-background-card);
486
+
487
+ color-scheme: light;
488
+ }
489
+
490
+ *,
491
+ *::before,
492
+ *::after {
493
+ box-sizing: border-box;
494
+ }
495
+
496
+ /* Thin, understated scrollbar rather than the browser's default chunky
497
+ one. Applied globally (Firefox's two-value shorthand + WebKit's pseudo-elements)
498
+ rather than per scroll container, since scrollable areas exist
499
+ throughout an app and should all read the same way. */
500
+ * {
501
+ scrollbar-width: thin;
502
+ scrollbar-color: var(--color-border-strong) transparent;
503
+ }
504
+
505
+ *::-webkit-scrollbar {
506
+ width: 10px;
507
+ height: 10px;
508
+ }
509
+
510
+ *::-webkit-scrollbar-track {
511
+ background: transparent;
512
+ }
513
+
514
+ *::-webkit-scrollbar-thumb {
515
+ background: var(--color-border-strong);
516
+ border-radius: var(--radius-full);
517
+ border: 2px solid transparent;
518
+ background-clip: padding-box;
519
+ }
520
+
521
+ *::-webkit-scrollbar-thumb:hover {
522
+ background: var(--color-text-muted);
523
+ background-clip: padding-box;
524
+ }
525
+
526
+ body {
527
+ margin: 0;
528
+ font-family:
529
+ "Inter",
530
+ system-ui,
531
+ -apple-system,
532
+ "Segoe UI",
533
+ Roboto,
534
+ sans-serif;
535
+ font-size: var(--text-base);
536
+ line-height: 1.54;
537
+ background: var(--color-background-global);
538
+ color: var(--color-text-primary);
539
+ -webkit-font-smoothing: antialiased;
540
+ }
541
+
542
+ h1 {
543
+ margin: 0;
544
+ font-size: var(--text-xl);
545
+ font-weight: var(--font-weight-semibold);
546
+ line-height: 24px;
547
+ letter-spacing: -0.01em;
548
+ }
549
+
550
+ h2 {
551
+ margin: 0;
552
+ font-size: var(--text-lg);
553
+ font-weight: var(--font-weight-semibold);
554
+ letter-spacing: -0.01em;
555
+ }
556
+
557
+ h3 {
558
+ margin: 0;
559
+ font-size: var(--text-md);
560
+ font-weight: var(--font-weight-medium);
561
+ }
562
+
563
+ p {
564
+ margin: 0;
565
+ }
566
+
567
+ /* Inline semantic emphasis (<strong>/<b> used mid-sentence in body
568
+ copy) deliberately stays regular weight — unrelated to any heading/
569
+ value hierarchy, which uses the --font-weight-* tokens on purpose.
570
+ The browser's UA stylesheet bolds these by default regardless of any
571
+ class applied, so it's the one place a plain CSS rule (not a token)
572
+ is needed to actually reach them. Keeps the tag's semantic meaning
573
+ (screen readers still announce emphasis); only the visual weight
574
+ changes. */
575
+ strong,
576
+ b {
577
+ font-weight: 400;
578
+ }
579
+
580
+ a {
581
+ color: var(--color-text-primary);
582
+ text-decoration: none;
583
+ }
584
+
585
+ a:focus-visible {
586
+ outline: 1.5px dashed var(--color-brand);
587
+ outline-offset: 4px;
588
+ border-radius: var(--radius-full);
589
+ }
590
+
591
+ /* Table-cell identity links (item name, PO number, ...) go semibold on
592
+ hover instead of the row itself tinting. Weight rather than
593
+ underline, reserved for hover specifically (not the persistent
594
+ "you're on this page" signal a nav rail's [aria-current="page"] uses
595
+ semibold for too), so there's no confusion between a row you're
596
+ pointing at and one you're actually on. Scoped to table cells only,
597
+ not every <a> in an app (nav links, card links, etc. keep their own
598
+ existing hover treatments). */
599
+ td a:hover {
600
+ font-weight: var(--font-weight-semibold);
601
+ }
602
+
603
+ label {
604
+ font-size: var(--text-sm);
605
+ font-weight: 400;
606
+ }
607
+
608
+ /* Primary action — highest-contrast neutral fill (--color-brand),
609
+ paired text color (--color-brand-text). */
610
+ button {
611
+ font: inherit;
612
+ cursor: pointer;
613
+ border: none;
614
+ border-radius: var(--radius-sm);
615
+ padding: 5px 10px;
616
+ font-size: var(--text-base);
617
+ font-weight: 400;
618
+ background: var(--color-brand);
619
+ color: var(--color-brand-text);
620
+ box-shadow: var(--shadow-btn-primary);
621
+ transition:
622
+ transform 0.05s ease,
623
+ background 0.15s ease,
624
+ box-shadow 0.15s ease;
625
+ }
626
+
627
+ button:hover {
628
+ background: var(--color-brand-hover);
629
+ }
630
+
631
+ button:active {
632
+ background: var(--color-brand-active);
633
+ transform: scale(0.97);
634
+ }
635
+
636
+ button:disabled {
637
+ opacity: 0.4;
638
+ cursor: not-allowed;
639
+ transform: none;
640
+ }
641
+
642
+ /* A full-width toggle button (e.g. a collapsible card's header) spans
643
+ the whole card width (title left, chevron far right) — the global
644
+ button:active scale(0.97) above shrinks toward the button's center,
645
+ which barely moves title text near that center but visibly yanks a
646
+ far-edge chevron sideways on every click, reading as a "shake".
647
+ Full-width toggles don't need a pressed-scale cue the way compact
648
+ pill buttons do. */
649
+ .collapsible-card-toggle:active {
650
+ transform: none;
651
+ }
652
+
653
+ button:focus-visible {
654
+ outline: 2px solid var(--color-brand);
655
+ outline-offset: 2px;
656
+ }
657
+
658
+ /* Secondary action — white fill with the same layered-inset "3D"
659
+ bevel as the primary button, rather than a flat border. Use for
660
+ "Cancel" next to a primary "Save"/"Create" so the two don't compete
661
+ for the same visual weight.
662
+
663
+ Repeats the shape/typography properties from the bare `button {}`
664
+ rule above (rather than only overriding color/shadow and relying on
665
+ inheriting the rest) so this still renders correctly on a non-
666
+ <button> element carrying the class — an <a> styled as a secondary
667
+ button gets none of the bare `button` selector's box model
668
+ otherwise, and renders as an unpadded, unshaped sliver of color. */
669
+ .btn-secondary {
670
+ display: inline-flex;
671
+ align-items: center;
672
+ justify-content: center;
673
+ font: inherit;
674
+ cursor: pointer;
675
+ border: none;
676
+ border-radius: var(--radius-sm);
677
+ padding: 5px 10px;
678
+ font-size: var(--text-base);
679
+ font-weight: 400;
680
+ background: var(--color-secondary-fill);
681
+ color: var(--color-secondary-text);
682
+ box-shadow: var(--shadow-btn-secondary);
683
+ text-decoration: none;
684
+ }
685
+
686
+ .btn-secondary:hover {
687
+ background: var(--color-secondary-hover);
688
+ opacity: 1;
689
+ }
690
+
691
+ /* Destructive action — irreversible operations only (delete, void,
692
+ permanently remove). Not the default for every "Delete" button in
693
+ an app; opt in per call site where the action truly can't be
694
+ undone. Same self-contained-shape reasoning as .btn-secondary above. */
695
+ .btn-danger {
696
+ display: inline-flex;
697
+ align-items: center;
698
+ justify-content: center;
699
+ font: inherit;
700
+ cursor: pointer;
701
+ border: none;
702
+ border-radius: var(--radius-sm);
703
+ padding: 5px 10px;
704
+ font-size: var(--text-base);
705
+ font-weight: 400;
706
+ background: var(--color-destructive-fill);
707
+ /* Bare button{} above defaults every <button> to box-shadow:
708
+ var(--shadow-btn-primary), a layered bevel built around
709
+ --color-brand — on a red fill that shows up as a mismatched
710
+ black/white-ish ring instead of blending in. Flat, no bevel. */
711
+ box-shadow: none;
712
+ color: var(--color-destructive-text);
713
+ text-decoration: none;
714
+ }
715
+
716
+ .btn-danger:hover {
717
+ background: var(--color-destructive-hover);
718
+ }
719
+
720
+ input,
721
+ select,
722
+ textarea {
723
+ font: inherit;
724
+ width: 100%;
725
+ padding: 7px 10px;
726
+ margin-top: 4px;
727
+ border: 1px solid var(--color-border);
728
+ border-radius: var(--radius-sm);
729
+ background: var(--color-input);
730
+ color: var(--color-text-primary);
731
+ transition:
732
+ border-color 0.15s ease,
733
+ box-shadow 0.15s ease;
734
+ }
735
+
736
+ /* The browser's native select arrow renders at its own baseline/size
737
+ per-OS (a mismatched-color chevron and off-center text) —
738
+ appearance:none strips it entirely and this draws a fixed, always-
739
+ centered replacement instead. line-height:normal plus equal top/
740
+ bottom padding is what actually centers the selected value; the
741
+ native arrow's box was throwing that off by consuming space
742
+ asymmetrically. The open option list itself stays OS-native — no
743
+ browser lets CSS reach inside it. */
744
+ select {
745
+ appearance: none;
746
+ -webkit-appearance: none;
747
+ line-height: normal;
748
+ padding-right: 32px;
749
+ 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
+ background-repeat: no-repeat;
751
+ background-position: right 10px center;
752
+ }
753
+
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). */
758
+ input:focus,
759
+ select:focus,
760
+ textarea:focus {
761
+ outline: none;
762
+ border-color: var(--color-brand);
763
+ }
764
+
765
+ input:disabled,
766
+ select:disabled,
767
+ textarea:disabled {
768
+ color: color-mix(in srgb, var(--color-text-muted) 40%, transparent);
769
+ background: color-mix(in srgb, var(--color-surface-tint) 50%, transparent);
770
+ cursor: not-allowed;
771
+ }
772
+
773
+ /* Redraws the custom arrow after the disabled rule above's `background`
774
+ shorthand clears it (shorthand always wins over the plain select
775
+ rule's background-image, regardless of source order otherwise). */
776
+ select:disabled {
777
+ 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='%23c4c7c5' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
778
+ background-repeat: no-repeat;
779
+ background-position: right 10px center;
780
+ }
781
+
782
+ /* Checkboxes/radios are native controls, not text fields — the width:100%
783
+ above would stretch them full-width, and without an explicit
784
+ accent-color they render in the browser's own default blue instead
785
+ of this palette's own neutral --color-brand. */
786
+ input[type="checkbox"],
787
+ input[type="radio"] {
788
+ width: auto;
789
+ accent-color: var(--color-brand);
790
+ }
791
+
792
+ /* Shadow-based elevation, plus a hairline border — the card surface
793
+ and page canvas are close enough in lightness that shadow alone
794
+ doesn't read as a distinct edge, so the border carries definition
795
+ while the shadow carries depth. */
796
+ .card {
797
+ background: var(--color-background-card);
798
+ border: 0.5px solid var(--color-border);
799
+ border-radius: var(--radius-lg);
800
+ box-shadow: var(--shadow-sm);
801
+ padding: 16px;
802
+ }
803
+
804
+ /* A card whose entire content is a table — no inset padding, so the
805
+ table's own row/column lines run edge-to-edge to the card's outer
806
+ border instead of floating inside a padded frame. overflow: hidden
807
+ clips the table's square corners to the card's rounded ones (without
808
+ it, a header cell's background or a hovered row can square-corner
809
+ past the border-radius at the very first/last row). */
810
+ .card-table {
811
+ padding: 0;
812
+ overflow: hidden;
813
+ }
814
+
815
+ /* For a table embedded alongside other content in the same card (a
816
+ detail page's header/notes text above a line-items table, say) —
817
+ bleeds the table out to the card's own edges so it still reaches
818
+ edge-to-edge like .card-table does, without stripping padding from
819
+ the text around it. Matches the standard 16px .card padding, so
820
+ this bleed calculation and a card's own padding never drift apart. */
821
+ .table-bleed {
822
+ width: calc(100% + 32px);
823
+ margin: 0 -16px;
824
+ }
825
+
826
+ .login-footer {
827
+ position: absolute;
828
+ bottom: 28px;
829
+ left: 0;
830
+ right: 0;
831
+ text-align: center;
832
+ font-size: var(--text-sm);
833
+ color: var(--color-text-muted);
834
+ }
835
+
836
+ /* Full-page, single-column shell for every auth screen (Login, Signup,
837
+ AuthCallback, invite completion, OTP entry, ...) across every app —
838
+ the wordmark sits above the card, both centered on the page. */
839
+ .auth-page {
840
+ position: relative;
841
+ min-height: 100vh;
842
+ display: flex;
843
+ flex-direction: column;
844
+ align-items: center;
845
+ justify-content: center;
846
+ gap: 56px;
847
+ padding: 24px;
848
+ background: var(--color-background-global);
849
+ }
850
+
851
+ .auth-wordmark-wrap {
852
+ display: flex;
853
+ justify-content: center;
854
+ }
855
+
856
+ .auth-wordmark {
857
+ height: 28px;
858
+ width: auto;
859
+ }
860
+
861
+ /* Light theme (the default — no [data-theme] override needed) shows
862
+ the dark-text wordmark; dark theme flips to the white-text one. */
863
+ .auth-wordmark-dark {
864
+ display: none;
865
+ }
866
+
867
+ :root[data-theme="dark"] .auth-wordmark-light {
868
+ display: none;
869
+ }
870
+
871
+ :root[data-theme="dark"] .auth-wordmark-dark {
872
+ display: block;
873
+ }
874
+
875
+ .auth-page .login-footer {
876
+ position: absolute;
877
+ bottom: 24px;
878
+ }
879
+
880
+ /* Refined form chrome for auth screens — a small-caps label style and
881
+ taller, tinted inputs (Linear/Stripe-style), scoped to .login-form
882
+ rather than touching the app-wide input/label rules every other form
883
+ relies on. */
884
+ .login-form label {
885
+ display: block;
886
+ font-size: var(--text-xs);
887
+ font-weight: 400;
888
+ letter-spacing: 0.06em;
889
+ text-transform: uppercase;
890
+ color: var(--color-text-muted);
891
+ margin-bottom: 6px;
892
+ }
893
+
894
+ .login-form input {
895
+ height: 42px;
896
+ padding: 0 14px;
897
+ margin-top: 0;
898
+ border-radius: var(--radius-md);
899
+ background: var(--color-surface-tint);
900
+ /* A visible border regardless of theme — in light mode,
901
+ --color-surface-tint sits just a couple shades off
902
+ --color-background-card, so a borderless input was nearly
903
+ invisible against the card until focused. */
904
+ border-color: var(--color-border);
905
+ }
906
+
907
+ .login-form input:focus {
908
+ background: var(--color-input);
909
+ border-color: var(--color-border-strong);
910
+ }
911
+
912
+ /* Every button in the auth forms — not just the submit button — so a
913
+ plain .btn-secondary (e.g. "Continue with Google"), whose base
914
+ padding makes it noticeably thinner than the submit button, matches. */
915
+ .login-form button {
916
+ height: 44px;
917
+ border-radius: var(--radius-md);
918
+ font-size: var(--text-md);
919
+ letter-spacing: 0.01em;
920
+ }
921
+
922
+ /* Create-form layout wrapper — a create form's outer container. */
923
+ .record-form-layout {
924
+ display: flex;
925
+ align-items: flex-start;
926
+ gap: 12px;
927
+ margin-bottom: 12px;
928
+ }
929
+
930
+ /* Capped, NOT centered — small by default, the right width for a plain
931
+ field-only form (Credit Memos, Vendor Payments, most Settings CRUD
932
+ pages, ...). Flush left, same edge as the page's own <h2> title (a
933
+ separate sibling outside this box) and the same edge every list page
934
+ (.reports-layout-content/.settings-layout-content) already starts
935
+ from — centering via margin:0 auto instead used to drift this box's
936
+ left edge away from both of those, and away from itself between a
937
+ wide table and a two-field form, reading as content jumping around
938
+ from page to page instead of a page that's simply narrower. */
939
+ .record-form-layout > form {
940
+ flex: 1;
941
+ min-width: 0;
942
+ max-width: var(--page-width-small);
943
+ }
944
+
945
+ /* Structured forms that need more room than a plain field list but
946
+ aren't a line-item document — a sidebar panel (Customer/Vendor +
947
+ Notes/Tags) or a longer component list (Builds). Opt in with both
948
+ classes: className="record-form-layout medium-form-layout". */
949
+ .record-form-layout.medium-form-layout > form {
950
+ max-width: var(--page-width-medium);
951
+ }
952
+
953
+ /* Wide-table document forms (Invoices, Purchase Orders, Sales Orders,
954
+ ...) need more room than --page-width-medium for their line-items
955
+ table, but a truly unbounded max-width stretches those rows into
956
+ awkward proportions on an ultra-wide monitor — capped generously
957
+ instead, still full-bleed on any normal screen. Flush left, same
958
+ reasoning as .record-form-layout > form above. */
959
+ .wide-form-layout > form {
960
+ max-width: var(--page-width-large);
961
+ }
962
+
963
+ .wide-form-layout .party-form-columns {
964
+ max-width: none;
965
+ margin: 0;
966
+ }
967
+
968
+ /* Collapsible accordion sections — a plain chevron that rotates open
969
+ instead of the browser's own disclosure-triangle marker. */
970
+ .form-section-summary {
971
+ display: flex;
972
+ align-items: center;
973
+ gap: 8px;
974
+ cursor: pointer;
975
+ font-weight: 400;
976
+ padding: 8px 0;
977
+ user-select: none;
978
+ list-style: none;
979
+ }
980
+
981
+ .form-section-summary::-webkit-details-marker {
982
+ display: none;
983
+ }
984
+
985
+ .form-section-chevron {
986
+ flex-shrink: 0;
987
+ opacity: 0.55;
988
+ transition: transform 0.15s ease;
989
+ }
990
+
991
+ .form-section[open] > .form-section-summary .form-section-chevron {
992
+ transform: rotate(90deg);
993
+ }
994
+
995
+ a.card {
996
+ display: block;
997
+ /* A card-styled link is a clickable surface, not inline text — it
998
+ shouldn't inherit link-blue for its whole contents (every stat
999
+ number/label inside a dashboard tile would otherwise render blue). */
1000
+ color: var(--color-text-primary);
1001
+ text-decoration: none;
1002
+ transition:
1003
+ box-shadow 0.15s ease,
1004
+ transform 0.15s ease,
1005
+ border-color 0.15s ease;
1006
+ }
1007
+
1008
+ a.card:hover {
1009
+ box-shadow: var(--shadow-md);
1010
+ border-color: var(--color-border-strong);
1011
+ transform: translateY(-1px);
1012
+ text-decoration: none;
1013
+ }
1014
+
1015
+ /* ---------------------------------------------------------------- */
1016
+ /* Modal (item picker, etc.) */
1017
+ /* ---------------------------------------------------------------- */
1018
+
1019
+ .modal-overlay {
1020
+ position: fixed;
1021
+ inset: 0;
1022
+ background: rgba(26, 26, 26, 0.45);
1023
+ display: flex;
1024
+ align-items: flex-start;
1025
+ justify-content: center;
1026
+ padding: 60px 16px;
1027
+ z-index: var(--z-modal);
1028
+ }
1029
+
1030
+ .modal-panel {
1031
+ background: var(--color-background-card);
1032
+ border: 1px solid var(--color-border);
1033
+ border-radius: var(--radius-lg);
1034
+ box-shadow: var(--shadow-lg);
1035
+ width: 100%;
1036
+ max-width: 640px;
1037
+ max-height: 80vh;
1038
+ display: flex;
1039
+ flex-direction: column;
1040
+ overflow: hidden;
1041
+ }
1042
+
1043
+ .modal-panel-header {
1044
+ padding: 12px 16px;
1045
+ border-bottom: 1px solid var(--color-border);
1046
+ display: flex;
1047
+ align-items: center;
1048
+ gap: 12px;
1049
+ }
1050
+
1051
+ .modal-panel-header input {
1052
+ flex: 1;
1053
+ margin-top: 0;
1054
+ }
1055
+
1056
+ .modal-panel-body {
1057
+ overflow-y: auto;
1058
+ padding: 0;
1059
+ }
1060
+
1061
+ .modal-panel-footer {
1062
+ padding: 12px 16px;
1063
+ border-top: 1px solid var(--color-border);
1064
+ display: flex;
1065
+ justify-content: flex-end;
1066
+ gap: 10px;
1067
+ }