@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/assets/hattisar-wordmark-dark.png +0 -0
- package/assets/hattisar-wordmark-light.png +0 -0
- package/package.json +17 -0
- package/tokens.css +1067 -0
|
Binary file
|
|
Binary file
|
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
|
+
}
|