@nextlyhq/ui 0.0.2-alpha.6 → 0.0.2-alpha.62

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/dist/theme.css ADDED
@@ -0,0 +1,1249 @@
1
+ /**
2
+ * Nextly design-system theme.
3
+ *
4
+ * Single source of truth for tokens, the Tailwind `@theme inline` mappings, the
5
+ * dark variant, and the animation keyframes. It does NOT include a base reset —
6
+ * that lives in `index.css` and ships in the pre-compiled `styles.css` bundle.
7
+ * Consumed two ways:
8
+ * - Source: `@import "@nextlyhq/ui/theme.css"` after `@import "tailwindcss"`
9
+ * to compile against the token contract (admin, power-user plugins).
10
+ * - Pre-compiled: `@nextlyhq/ui/styles.css` bundles this behind a Tailwind
11
+ * build so plugins get styled components with zero setup.
12
+ *
13
+ * Selectors are unscoped (`:root` / `.dark`); a consumer that embeds the UI in a
14
+ * host page scopes them at build time (admin rewrites to `.nextly-admin`).
15
+ */
16
+
17
+ /**
18
+ * Activate `dark:` utilities from the `.dark` class rather than the OS
19
+ * `prefers-color-scheme`, so an explicit light choice wins over system dark.
20
+ */
21
+ @custom-variant dark (&:where(.dark, .dark *));
22
+
23
+ @theme inline {
24
+ /* ============================================
25
+ * ANIMATIONS (Tailwind v4.1)
26
+ * ============================================ */
27
+ --animate-accordion-down: accordion-down 0.3s cubic-bezier(0.87, 0, 0.13, 1);
28
+ --animate-accordion-up: accordion-up 0.3s cubic-bezier(0.87, 0, 0.13, 1);
29
+ --animate-fade-in: fade-in 0.2s ease-out;
30
+ --animate-fade-out: fade-out 0.2s ease-in;
31
+ --animate-zoom-in-95: zoom-in-95 0.1s ease-out;
32
+ --animate-zoom-out-95: zoom-out-95 0.1s ease-in;
33
+ --animate-slide-in-from-top-2: slide-in-from-top-2 0.2s ease-out;
34
+ --animate-slide-in-from-bottom-2: slide-in-from-bottom-2 0.2s ease-out;
35
+ --animate-slide-in-from-left-2: slide-in-from-left-2 0.2s ease-out;
36
+ --animate-slide-in-from-right-2: slide-in-from-right-2 0.2s ease-out;
37
+ --animate-brand-orbit: brand-orbit 2.4s linear infinite;
38
+ --animate-brand-pulse: brand-pulse 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
39
+
40
+ /* ============================================
41
+ * COLOR PALETTE (Tailwind v4.1)
42
+ * Monochrome design system, WCAG 2.2 AA compliant.
43
+ *
44
+ * Color Format Strategy:
45
+ * - Reference scales (blue/slate/etc.): HEX, kept for utility classes.
46
+ * - Semantic tokens: OKLCH full-color values defined in `:root` / `.dark`,
47
+ * mapped here via `@theme inline` with bare `var()` so scoped/dark
48
+ * overrides resolve at the consuming element.
49
+ * ============================================ */
50
+
51
+ /* Blue Scale - Primary Brand Color */
52
+ --color-blue-50: #eff6ff;
53
+ --color-blue-100: #dbeafe;
54
+ --color-blue-200: #bfdbfe;
55
+ --color-blue-300: #93c5fd;
56
+ --color-blue-400: #60a5fa;
57
+ --color-blue-500: #3b82f6;
58
+ --color-blue-600: #2563eb;
59
+ --color-blue-700: #1d4ed8;
60
+ --color-blue-800: #1e40af;
61
+ --color-blue-900: #1e3a8a;
62
+ --color-blue-950: #172554;
63
+
64
+ /* Cyan Scale - Accent Color */
65
+ --color-cyan-50: #ecfeff;
66
+ --color-cyan-100: #cffafe;
67
+ --color-cyan-200: #a5f3fc;
68
+ --color-cyan-300: #67e8f9;
69
+ --color-cyan-400: #22d3ee;
70
+ --color-cyan-500: #06b6d4;
71
+ --color-cyan-600: #0891b2;
72
+ --color-cyan-700: #0e7490;
73
+ --color-cyan-800: #155e75;
74
+ --color-cyan-900: #164e63;
75
+ --color-cyan-950: #083344;
76
+
77
+ /* Green Scale - Success Color */
78
+ --color-green-50: #f0fdf4;
79
+ --color-green-100: #dcfce7;
80
+ --color-green-200: #bbf7d0;
81
+ --color-green-300: #86efac;
82
+ --color-green-400: #4ade80;
83
+ --color-green-500: #22c55e;
84
+ --color-green-600: #16a34a;
85
+ --color-green-700: #15803d;
86
+ --color-green-800: #166534;
87
+ --color-green-900: #14532d;
88
+ --color-green-950: #052e16;
89
+
90
+ /* Amber Scale - Warning Color */
91
+ --color-amber-50: #fffbeb;
92
+ --color-amber-100: #fef3c7;
93
+ --color-amber-200: #fde68a;
94
+ --color-amber-300: #fcd34d;
95
+ --color-amber-400: #fbbf24;
96
+ --color-amber-500: #f59e0b;
97
+ --color-amber-600: #d97706;
98
+ --color-amber-700: #b45309;
99
+ --color-amber-800: #92400e;
100
+ --color-amber-900: #78350f;
101
+ --color-amber-950: #451a03;
102
+
103
+ /* Red Scale - Error/Destructive Color */
104
+ --color-red-50: #fef2f2;
105
+ --color-red-100: #fee2e2;
106
+ --color-red-200: #fecaca;
107
+ --color-red-300: #fca5a5;
108
+ --color-red-400: #f87171;
109
+ --color-red-500: #ef4444;
110
+ --color-red-600: #dc2626;
111
+ --color-red-700: #b91c1c;
112
+ --color-red-800: #991b1b;
113
+ --color-red-900: #7f1d1d;
114
+ --color-red-950: #450a0a;
115
+
116
+ /* Slate Scale - Neutral Colors */
117
+ --color-slate-50: #f8fafc;
118
+ --color-slate-100: #f1f5f9;
119
+ --color-slate-200: #e2e8f0;
120
+ --color-slate-300: #cbd5e1;
121
+ --color-slate-400: #94a3b8;
122
+ --color-slate-500: #64748b;
123
+ --color-slate-600: #475569;
124
+ --color-slate-700: #334155;
125
+ --color-slate-800: #1e293b;
126
+ --color-slate-900: #0f172a;
127
+ --color-slate-950: #020617;
128
+
129
+ /* ============================================
130
+ * SEMANTIC COLOR TOKENS (Tailwind v4.1)
131
+ * Reference color scales defined above
132
+ * ============================================ */
133
+
134
+ /* shadcn/ui semantic colors - reference CSS variables defined below */
135
+ --color-overlay: var(--nx-overlay);
136
+ --color-overlay-soft: var(--nx-overlay-soft);
137
+ --color-overlay-strong: var(--nx-overlay-strong);
138
+ --color-border: var(--nx-border);
139
+ --color-border-subtle: var(--nx-border-subtle);
140
+ --color-border-strong: var(--nx-border-strong);
141
+ --color-input: var(--nx-input);
142
+ --color-control-border: var(--nx-control-border);
143
+ --color-ring: var(--nx-ring);
144
+ --color-background: var(--nx-background);
145
+ --color-foreground: var(--nx-foreground);
146
+
147
+ --color-primary: var(--nx-primary);
148
+ --color-primary-50: color-mix(in srgb, var(--nx-primary), white 95%);
149
+ --color-primary-100: color-mix(in srgb, var(--nx-primary), white 90%);
150
+ --color-primary-200: color-mix(in srgb, var(--nx-primary), white 70%);
151
+ --color-primary-300: color-mix(in srgb, var(--nx-primary), white 50%);
152
+ --color-primary-400: color-mix(in srgb, var(--nx-primary), white 30%);
153
+ --color-primary-500: var(--nx-primary);
154
+ --color-primary-600: color-mix(in srgb, var(--nx-primary), black 10%);
155
+ --color-primary-700: color-mix(in srgb, var(--nx-primary), black 30%);
156
+ --color-primary-800: color-mix(in srgb, var(--nx-primary), black 50%);
157
+ --color-primary-900: color-mix(in srgb, var(--nx-primary), black 70%);
158
+ --color-primary-950: color-mix(in srgb, var(--nx-primary), black 85%);
159
+ --color-primary-foreground: var(--nx-primary-foreground);
160
+
161
+ --color-secondary: var(--nx-secondary);
162
+ --color-secondary-foreground: var(--nx-secondary-foreground);
163
+
164
+ /* Status scales, derived the same way as the primary scale above.
165
+ *
166
+ * A status role is one token; the scale is mixed from it, so retheming
167
+ * `--nx-success` moves every shade with it and they cannot drift apart. The
168
+ * shades exist because a status needs more than its solid colour: a tinted
169
+ * surface, a border on that surface, and text dark enough to read on it.
170
+ * Mixing toward white and black keeps 50 pale and 900 deep in both modes,
171
+ * which is what a `dark:` pair at the call site already assumes. */
172
+ --color-destructive: var(--nx-destructive);
173
+ --color-destructive-50: color-mix(in srgb, var(--nx-destructive), white 95%);
174
+ --color-destructive-100: color-mix(in srgb, var(--nx-destructive), white 90%);
175
+ --color-destructive-200: color-mix(in srgb, var(--nx-destructive), white 70%);
176
+ --color-destructive-300: color-mix(in srgb, var(--nx-destructive), white 50%);
177
+ --color-destructive-400: color-mix(in srgb, var(--nx-destructive), white 30%);
178
+ --color-destructive-500: var(--nx-destructive);
179
+ --color-destructive-600: color-mix(in srgb, var(--nx-destructive), black 10%);
180
+ --color-destructive-700: color-mix(in srgb, var(--nx-destructive), black 30%);
181
+ --color-destructive-800: color-mix(in srgb, var(--nx-destructive), black 50%);
182
+ --color-destructive-900: color-mix(in srgb, var(--nx-destructive), black 70%);
183
+ --color-destructive-950: color-mix(in srgb, var(--nx-destructive), black 85%);
184
+ /* Saturated fill for solid buttons, kept separate from the text-tuned base
185
+ * so each can be solved for its own job. The ink that lands on it is
186
+ * `-foreground`, which is white in light mode and black in dark. */
187
+ --color-destructive-solid: var(--nx-destructive-solid);
188
+ --color-destructive-foreground: var(--nx-destructive-foreground);
189
+
190
+ --color-success: var(--nx-success);
191
+ --color-success-50: color-mix(in srgb, var(--nx-success), white 95%);
192
+ --color-success-100: color-mix(in srgb, var(--nx-success), white 90%);
193
+ --color-success-200: color-mix(in srgb, var(--nx-success), white 70%);
194
+ --color-success-300: color-mix(in srgb, var(--nx-success), white 50%);
195
+ --color-success-400: color-mix(in srgb, var(--nx-success), white 30%);
196
+ --color-success-500: var(--nx-success);
197
+ --color-success-600: color-mix(in srgb, var(--nx-success), black 10%);
198
+ --color-success-700: color-mix(in srgb, var(--nx-success), black 30%);
199
+ --color-success-800: color-mix(in srgb, var(--nx-success), black 50%);
200
+ --color-success-900: color-mix(in srgb, var(--nx-success), black 70%);
201
+ --color-success-950: color-mix(in srgb, var(--nx-success), black 85%);
202
+ /* Saturated fill for solid usage; distinct from the text-tuned base. */
203
+ --color-success-solid: var(--nx-success-solid);
204
+ --color-success-foreground: var(--nx-success-foreground);
205
+
206
+ --color-warning: var(--nx-warning);
207
+ --color-warning-50: color-mix(in srgb, var(--nx-warning), white 95%);
208
+ --color-warning-100: color-mix(in srgb, var(--nx-warning), white 90%);
209
+ --color-warning-200: color-mix(in srgb, var(--nx-warning), white 70%);
210
+ --color-warning-300: color-mix(in srgb, var(--nx-warning), white 50%);
211
+ --color-warning-400: color-mix(in srgb, var(--nx-warning), white 30%);
212
+ --color-warning-500: var(--nx-warning);
213
+ --color-warning-600: color-mix(in srgb, var(--nx-warning), black 10%);
214
+ --color-warning-700: color-mix(in srgb, var(--nx-warning), black 30%);
215
+ --color-warning-800: color-mix(in srgb, var(--nx-warning), black 50%);
216
+ --color-warning-900: color-mix(in srgb, var(--nx-warning), black 70%);
217
+ --color-warning-950: color-mix(in srgb, var(--nx-warning), black 85%);
218
+ --color-warning-foreground: var(--nx-warning-foreground);
219
+
220
+ /* The editor's highlight mark. A token rather than a literal because it
221
+ * styles authored content, which an integrator restyles to their own site
222
+ * and which has to stay readable on a dark page. */
223
+ --color-highlight: var(--nx-highlight);
224
+ --color-highlight-foreground: var(--nx-highlight-foreground);
225
+
226
+ /* Syntax colors, so code tokens are styled the same way as everything else
227
+ * (text-code-keyword, bg-code-bg) instead of through bespoke CSS. */
228
+ --color-code-bg: var(--nx-code-bg);
229
+ --color-code-fg: var(--nx-code-fg);
230
+ --color-code-comment: var(--nx-code-comment);
231
+ --color-code-keyword: var(--nx-code-keyword);
232
+ --color-code-string: var(--nx-code-string);
233
+ --color-code-number: var(--nx-code-number);
234
+ --color-code-function: var(--nx-code-function);
235
+ --color-code-operator: var(--nx-code-operator);
236
+ --color-code-punctuation: var(--nx-code-punctuation);
237
+ --color-code-variable: var(--nx-code-variable);
238
+ --color-code-tag: var(--nx-code-tag);
239
+ --color-code-deleted: var(--nx-code-deleted);
240
+ --color-code-inserted: var(--nx-code-inserted);
241
+
242
+ --color-muted: var(--nx-muted);
243
+ --color-muted-foreground: var(--nx-muted-foreground);
244
+
245
+ --color-accent: var(--nx-accent);
246
+ --color-accent-50: color-mix(in srgb, var(--nx-accent), white 95%);
247
+ --color-accent-100: color-mix(in srgb, var(--nx-accent), white 90%);
248
+ --color-accent-200: color-mix(in srgb, var(--nx-accent), white 70%);
249
+ --color-accent-300: color-mix(in srgb, var(--nx-accent), white 50%);
250
+ --color-accent-400: color-mix(in srgb, var(--nx-accent), white 30%);
251
+ --color-accent-500: var(--nx-accent);
252
+ --color-accent-600: color-mix(in srgb, var(--nx-accent), black 10%);
253
+ --color-accent-700: color-mix(in srgb, var(--nx-accent), black 30%);
254
+ --color-accent-800: color-mix(in srgb, var(--nx-accent), black 50%);
255
+ --color-accent-900: color-mix(in srgb, var(--nx-accent), black 70%);
256
+ --color-accent-950: color-mix(in srgb, var(--nx-accent), black 85%);
257
+ --color-accent-foreground: var(--nx-accent-foreground);
258
+
259
+ --color-card: var(--nx-card);
260
+ --color-card-foreground: var(--nx-card-foreground);
261
+
262
+ --color-popover: var(--nx-popover);
263
+ --color-popover-foreground: var(--nx-popover-foreground);
264
+
265
+ /* Sidebar semantic tokens */
266
+ --color-sidebar: var(--nx-sidebar-background);
267
+ --color-sidebar-background: var(--nx-sidebar-background);
268
+ --color-sidebar-foreground: var(--nx-sidebar-foreground);
269
+ --color-sidebar-primary: var(--nx-sidebar-primary);
270
+ --color-sidebar-primary-foreground: var(--nx-sidebar-primary-foreground);
271
+ --color-sidebar-accent: var(--nx-sidebar-accent);
272
+ --color-sidebar-accent-background: var(--nx-sidebar-accent);
273
+ --color-sidebar-accent-foreground: var(--nx-sidebar-accent-foreground);
274
+ --color-sidebar-border: var(--nx-sidebar-border);
275
+ --color-sidebar-ring: var(--nx-sidebar-ring);
276
+
277
+ /* ============================================
278
+ * TYPOGRAPHY (Tailwind v4.1)
279
+ * Perfect Fourth Scale (1.333 ratio) - WCAG 2.2 AA Compliant
280
+ * ============================================ */
281
+
282
+ /* Font Families live in the non-inline @theme block below. */
283
+
284
+ /* Type Scale - Base Size: 16px (1rem) */
285
+ --text-xs: 0.75rem;
286
+ /* 12px */
287
+ --text-sm: 0.875rem;
288
+ /* 14px */
289
+ --text-base: 1rem;
290
+ /* 16px - body text (WCAG minimum) */
291
+ --text-lg: 1.125rem;
292
+ /* 18px */
293
+ --text-xl: 1.25rem;
294
+ /* 20px */
295
+ --text-2xl: 1.5rem;
296
+ /* 24px */
297
+ --text-3xl: 1.875rem;
298
+ /* 30px */
299
+ --text-4xl: 2.25rem;
300
+ /* 36px */
301
+ --text-5xl: 3rem;
302
+ /* 48px */
303
+ --text-6xl: 3.75rem;
304
+ /* 60px */
305
+
306
+ /* Line Heights (Leading) */
307
+ --leading-none: 1;
308
+ /* Large display text */
309
+ --leading-tight: 1.25;
310
+ /* Headings */
311
+ --leading-snug: 1.375;
312
+ /* Tight UI elements */
313
+ --leading-normal: 1.5;
314
+ /* Default for UI text */
315
+ --leading-relaxed: 1.625;
316
+ /* Long-form content */
317
+ --leading-loose: 2;
318
+ /* Very spacious text */
319
+
320
+ /* Letter Spacing (Tracking) */
321
+ --tracking-tighter: -0.05em;
322
+ /* -0.8px at 16px */
323
+ --tracking-tight: -0.025em;
324
+ /* -0.4px at 16px - for large headings */
325
+ --tracking-normal: 0em;
326
+ /* Default */
327
+ --tracking-wide: 0.025em;
328
+ /* +0.4px at 16px - for small text */
329
+ --tracking-wider: 0.05em;
330
+ /* +0.8px at 16px - for uppercase */
331
+ --tracking-widest: 0.1em;
332
+ /* +1.6px at 16px */
333
+
334
+ /* ============================================
335
+ * SPACING & LAYOUT (Tailwind v4.1)
336
+ * 8px Grid System - Industry Standard
337
+ * ============================================ */
338
+
339
+ /* Spacing Scale (8px base unit) */
340
+ --space-0: 0rem;
341
+ /* 0px */
342
+ --space-px: 0.0625rem;
343
+ /* 1px - for fine borders */
344
+ --space-0-5: 0.125rem;
345
+ /* 2px */
346
+ --space-1: 0.25rem;
347
+ /* 4px - icon padding, tiny gaps */
348
+ --space-1-5: 0.375rem;
349
+ /* 6px */
350
+ --space-2: 0.5rem;
351
+ /* 8px - tight spacing, inline elements */
352
+ --space-2-5: 0.625rem;
353
+ /* 10px */
354
+ --space-3: 0.75rem;
355
+ /* 12px - between related elements */
356
+ --space-3-5: 0.875rem;
357
+ /* 14px */
358
+ --space-4: 1rem;
359
+ /* 16px - standard element spacing */
360
+ --space-5: 1.25rem;
361
+ /* 20px */
362
+ --space-6: 1.5rem;
363
+ /* 24px - between form fields */
364
+ --space-7: 1.75rem;
365
+ /* 28px */
366
+ --space-8: 2rem;
367
+ /* 32px - between sections */
368
+ --space-9: 2.25rem;
369
+ /* 36px */
370
+ --space-10: 2.5rem;
371
+ /* 40px */
372
+ --space-11: 2.75rem;
373
+ /* 44px - minimum touch target */
374
+ --space-12: 3rem;
375
+ /* 48px - between major sections */
376
+ --space-14: 3.5rem;
377
+ /* 56px */
378
+ --space-16: 4rem;
379
+ /* 64px - page margins */
380
+ --space-20: 5rem;
381
+ /* 80px */
382
+ --space-24: 6rem;
383
+ /* 96px - large section breaks */
384
+
385
+ /* ============================================
386
+ * BORDER RADIUS (Tailwind v4.1)
387
+ *
388
+ * The contract is three steps — sm, md, lg — all derived from the single
389
+ * --radius knob (shadcn pattern), so the whole admin re-rounds from one
390
+ * declaration. Those three are also exactly what `tailwind-preset.ts` exports,
391
+ * so a plugin built against the published preset gets the same scale.
392
+ *
393
+ * That only holds if components pick a TIER BY ELEMENT CATEGORY instead of
394
+ * naming a length. A component that hardcodes a corner value — `rounded-none`
395
+ * as much as `rounded-[6px]` — pins itself at that value and silently opts
396
+ * out of the knob. The tiers, and what the shipped components put in each:
397
+ *
398
+ * rounded-lg (--radius-lg, the knob itself)
399
+ * Floating and grouping containers: cards, dialogs, alert dialogs,
400
+ * popovers, tooltips, dropdown / select / command panels, empty-state
401
+ * panels.
402
+ *
403
+ * rounded-md (--radius-md, one step tighter)
404
+ * Controls and the inline surfaces that sit among them: buttons, inputs,
405
+ * textareas, select triggers, icon buttons, alerts, table wrappers and
406
+ * image frames. A control nested in a container reads as inset when its
407
+ * corner is tighter than the surface holding it.
408
+ *
409
+ * rounded-sm (--radius-sm, tighter again)
410
+ * Small controls and adornments: menu and list items, badges, chips,
411
+ * tags, checkboxes, toolbar affordances — anything already inside a
412
+ * control or a dense row.
413
+ *
414
+ * Circles sit deliberately outside the scale: avatars, status dots, spinners,
415
+ * progress tracks and switch tracks and thumbs use `rounded-full`, which is
416
+ * 9999px and not a --radius step. They are round because of their shape, not
417
+ * because of the theme, so a retheme must not flatten them.
418
+ *
419
+ * rounded-xl / rounded-2xl are NOT tiers of this knob. `tailwind-preset.ts`
420
+ * exports only lg/md/sm, so a plugin using the preset gets Tailwind's fixed
421
+ * 0.75rem / 1rem for them, detached from --radius; and the definitions below
422
+ * ADD to the knob rather than subtracting, so they always sit rounder than
423
+ * the container tier and drift further as the knob grows. Nothing in the repo
424
+ * uses them. Treat them as off-contract and pick from sm/md/lg.
425
+ *
426
+ * At the shipped 0.5rem the three tiers are 4px, 6px and 8px. All positive,
427
+ * which matters: `sm` and `md` SUBTRACT from the knob, so a knob under 4px
428
+ * makes them negative. They still render square, because border-radius clamps
429
+ * a negative length to 0 at used-value time — but the token keeps its
430
+ * negative value, so reading `--radius-sm` for a padding, an inset, or
431
+ * through getComputedStyle yields -4px rather than 0. Anything consuming a
432
+ * step as a LENGTH rather than as a corner must clamp it itself, and that
433
+ * obligation returns the moment someone dials the knob back down.
434
+ *
435
+ * A site that genuinely must stay square keeps `rounded-none` WITH an inline
436
+ * comment saying why — overlapped-border strips, full-bleed fills inside an
437
+ * already-rounded parent, underline tabs (which is why Tabs is square, not a
438
+ * `rounded-sm` adornment). Square with a stated reason is a decision; square
439
+ * without one is an element that was never wired to the knob. Sheet is the
440
+ * standing example of the latter: it is anchored to the viewport edge and
441
+ * carries no radius class at all.
442
+ *
443
+ * Corollary for containers: a rounded box whose child paints a background to
444
+ * its edge must either clip that child (`overflow-hidden`) or give the child
445
+ * a matching corner (`rounded-b-[inherit]` and friends). Otherwise the child's
446
+ * fill paints square across the parent's curve at any nonzero --radius.
447
+ * ============================================ */
448
+ --radius-sm: calc(var(--radius) - 4px);
449
+ --radius-md: calc(var(--radius) - 2px);
450
+ --radius-lg: var(--radius);
451
+ --radius-xl: calc(var(--radius) + 4px);
452
+ --radius-2xl: calc(var(--radius) + 8px);
453
+
454
+ /* ============================================
455
+ * BOX SHADOWS (Tailwind v4.1)
456
+ * The ramp's colour comes from --nx-shadow-color, mixed down to each step's
457
+ * alpha. `@theme inline` copies a declaration's value into the generated
458
+ * utility, so a literal rgba() would be frozen at build time while this
459
+ * var() stays live and re-resolves per mode / per scope at the element.
460
+ * ============================================ */
461
+ --shadow-sm: 0 1px 2px 0
462
+ color-mix(in srgb, var(--nx-shadow-color) 5%, transparent);
463
+ --shadow-md:
464
+ 0 4px 6px -1px color-mix(in srgb, var(--nx-shadow-color) 10%, transparent),
465
+ 0 2px 4px -1px color-mix(in srgb, var(--nx-shadow-color) 6%, transparent);
466
+ --shadow-lg:
467
+ 0 10px 15px -3px color-mix(in srgb, var(--nx-shadow-color) 10%, transparent),
468
+ 0 4px 6px -2px color-mix(in srgb, var(--nx-shadow-color) 5%, transparent);
469
+ --shadow-xl:
470
+ 0 20px 25px -5px color-mix(in srgb, var(--nx-shadow-color) 10%, transparent),
471
+ 0 10px 10px -5px color-mix(in srgb, var(--nx-shadow-color) 4%, transparent);
472
+
473
+ /* Custom Functional Shadows (Fixes build warnings from arbitrary values) */
474
+ --shadow-inner-subtle: 0 -4px 6px -1px
475
+ color-mix(in srgb, var(--nx-shadow-color) 5%, transparent);
476
+ --shadow-glow-success: 0 0 10px
477
+ color-mix(in srgb, var(--nx-success) 10%, transparent);
478
+ --shadow-glow-primary: 0 0 15px
479
+ color-mix(in srgb, var(--nx-primary) 50%, transparent);
480
+ --shadow-elevation-primary: 0 8px 20px -8px
481
+ color-mix(in srgb, var(--nx-primary) 40%, transparent);
482
+ --shadow-soft-primary: 0 8px 30px
483
+ color-mix(in srgb, var(--nx-primary) 6%, transparent);
484
+ --shadow-neo: 4px 4px 0px 0px
485
+ color-mix(in srgb, var(--nx-shadow-color) 10%, transparent);
486
+
487
+ /* ============================================
488
+ * FLUID TYPOGRAPHY (Tailwind v4.1)
489
+ * Fixes build warnings from arbitrary values
490
+ * ============================================ */
491
+ --text-fluid-2xs: clamp(0.6rem, 0.55rem + 0.1vw, 0.65rem);
492
+ --text-fluid-xs: clamp(0.65rem, 0.6rem + 0.1vw, 0.7rem);
493
+ --text-fluid-sm: clamp(0.7rem, 0.65rem + 0.1vw, 0.75rem);
494
+ --text-fluid-base: clamp(0.875rem, 0.8rem + 0.2vw, 1rem);
495
+ --text-fluid-lg: clamp(1rem, 2vw, 1.125rem);
496
+ --text-fluid-xl: clamp(1.25rem, 3vw, 1.5rem);
497
+ --text-fluid-2xl: clamp(1.5rem, 5vw, 2.25rem);
498
+ --text-fluid-3xl: clamp(1.5rem, 5vw, 2.5rem);
499
+ --text-fluid-4xl: clamp(1.75rem, 5vw, 2.5rem);
500
+
501
+ /* Custom Transitions */
502
+ --ease-premium: cubic-bezier(0.2, 0.8, 0.2, 1);
503
+ }
504
+
505
+ /* ============================================
506
+ * FONT FAMILIES & WEIGHTS (Tailwind v4.1)
507
+ *
508
+ * Deliberately a non-inline `@theme`. `@theme inline` copies a declaration's
509
+ * value into whatever consumes it, which froze these stacks into
510
+ * `--default-font-family` / `--default-mono-font-family` and into the
511
+ * `font-mono` utility at build time, so the variables existed but nothing read
512
+ * them and re-declaring one on a scope changed nothing. A plain `@theme`
513
+ * emits each token as a real custom property and makes its consumers
514
+ * reference it with `var()`, so the base rule and the `font-*` utilities
515
+ * re-resolve wherever the token is redeclared. The weights sit here for the
516
+ * same reason, and to match the weights Tailwind ships (`--font-weight-black`
517
+ * and friends), which the `font-*` utilities also reach through `var()`.
518
+ *
519
+ * `--font-*` is the font-FAMILY namespace and `--font-weight-*` is the weight
520
+ * one, so a weight declared as `--font-bold` registers a family literally named
521
+ * "bold": `font-bold` then compiles to `font-family: 700`, which browsers drop,
522
+ * and it shadows the real weight utility rather than adding to it.
523
+ * ============================================ */
524
+ @theme {
525
+ /* The host app supplies the face through the variable; the family name is the
526
+ * var()'s own FALLBACK ARGUMENT, covering a host that installed the font
527
+ * without `next/font`, and the generic stack after it covers a host that did
528
+ * neither. Naming only the family would resolve to nothing under `next/font`,
529
+ * which self-hosts the face and exposes it ONLY as a variable -- a bare
530
+ * "Geist" there silently falls through to the system sans.
531
+ *
532
+ * The fallback has to sit INSIDE the parentheses. A family listed after the
533
+ * closing `)` is not reached when the variable is undefined: an unresolved
534
+ * `var()` with no fallback makes the whole declaration invalid at
535
+ * computed-value time, which for an inherited property like font-family means
536
+ * the element inherits its parent's face and the entire stack is discarded.
537
+ * The comma list looks like a fallback chain and is not one, so a host that
538
+ * skipped the variable rendered in whatever the page already used rather than
539
+ * in the generic sans this stack names.
540
+ *
541
+ * These are declared in a non-inline `@theme`, so they are emitted into
542
+ * `:root` and substitution happens THERE, not on the consuming element. A
543
+ * host must expose the variable at or above that scope -- `next/font`'s
544
+ * generated class belongs on `<html>`. On `<body>` the variable is invisible
545
+ * to this declaration no matter how it is spelled. */
546
+ --font-sans:
547
+ var(--font-geist, Geist), ui-sans-serif, system-ui, -apple-system,
548
+ "Segoe UI", sans-serif;
549
+ --font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
550
+ --font-mono:
551
+ var(--font-geist-mono, "Geist Mono"), ui-monospace, SFMono-Regular, Menlo,
552
+ Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
553
+
554
+ /* Body text, captions */
555
+ --font-weight-normal: 400;
556
+ /* Subheadings, emphasis */
557
+ --font-weight-medium: 500;
558
+ /* Headings, buttons */
559
+ --font-weight-semibold: 600;
560
+ /* Strong emphasis (rarely used) */
561
+ --font-weight-bold: 700;
562
+ }
563
+
564
+ /* ============================================
565
+ * RESPONSIVE TYPOGRAPHY (Desktop)
566
+ * Scale up larger text sizes on desktop screens
567
+ * Must be outside @theme block due to Tailwind v4.1 restrictions
568
+ * ============================================ */
569
+ @media (min-width: 768px) {
570
+ @theme {
571
+ --text-2xl: 1.75rem;
572
+ /* 28px on desktop (from 24px) */
573
+ --text-3xl: 2.25rem;
574
+ /* 36px on desktop (from 30px) */
575
+ --text-4xl: 3rem;
576
+ /* 48px on desktop (from 36px) */
577
+ --text-5xl: 4rem;
578
+ /* 64px on desktop (from 48px) */
579
+ }
580
+ }
581
+
582
+ /* Animation keyframes referenced by the --animate-* tokens above. */
583
+ /* Keyframes for custom animations */
584
+ @keyframes brand-orbit {
585
+ from {
586
+ transform: rotate(0deg);
587
+ }
588
+ to {
589
+ transform: rotate(360deg);
590
+ }
591
+ }
592
+
593
+ @keyframes brand-pulse {
594
+ 0%,
595
+ 100% {
596
+ transform: scale(0.97);
597
+ filter: drop-shadow(
598
+ 0 0 10px color-mix(in srgb, var(--nx-primary) 8%, transparent)
599
+ );
600
+ }
601
+ 50% {
602
+ transform: scale(1.03);
603
+ filter: drop-shadow(
604
+ 0 0 20px color-mix(in srgb, var(--nx-primary) 22%, transparent)
605
+ );
606
+ }
607
+ }
608
+
609
+ @keyframes accordion-down {
610
+ from {
611
+ height: 0;
612
+ }
613
+
614
+ to {
615
+ height: var(--radix-accordion-content-height);
616
+ }
617
+ }
618
+
619
+ @keyframes accordion-up {
620
+ from {
621
+ height: var(--radix-accordion-content-height);
622
+ }
623
+
624
+ to {
625
+ height: 0;
626
+ }
627
+ }
628
+
629
+ @keyframes fade-in {
630
+ from {
631
+ opacity: 0;
632
+ }
633
+
634
+ to {
635
+ opacity: 1;
636
+ }
637
+ }
638
+
639
+ @keyframes fade-out {
640
+ from {
641
+ opacity: 1;
642
+ }
643
+
644
+ to {
645
+ opacity: 0;
646
+ }
647
+ }
648
+
649
+ @keyframes zoom-in-95 {
650
+ from {
651
+ opacity: 0;
652
+ transform: scale(0.95);
653
+ }
654
+
655
+ to {
656
+ opacity: 1;
657
+ transform: scale(1);
658
+ }
659
+ }
660
+
661
+ @keyframes zoom-out-95 {
662
+ from {
663
+ opacity: 1;
664
+ transform: scale(1);
665
+ }
666
+
667
+ to {
668
+ opacity: 0;
669
+ transform: scale(0.95);
670
+ }
671
+ }
672
+
673
+ @keyframes slide-in-from-top-2 {
674
+ from {
675
+ transform: translateY(-0.5rem);
676
+ }
677
+
678
+ to {
679
+ transform: translateY(0);
680
+ }
681
+ }
682
+
683
+ @keyframes slide-in-from-bottom-2 {
684
+ from {
685
+ transform: translateY(0.5rem);
686
+ }
687
+
688
+ to {
689
+ transform: translateY(0);
690
+ }
691
+ }
692
+
693
+ @keyframes slide-in-from-left-2 {
694
+ from {
695
+ transform: translateX(-0.5rem);
696
+ }
697
+
698
+ to {
699
+ transform: translateX(0);
700
+ }
701
+ }
702
+
703
+ @keyframes slide-in-from-right-2 {
704
+ from {
705
+ transform: translateX(0.5rem);
706
+ }
707
+
708
+ to {
709
+ transform: translateX(0);
710
+ }
711
+ }
712
+
713
+ :root {
714
+ /* Base colors - using Slate neutral scale */
715
+ --nx-background: oklch(0.99 0 0);
716
+ /* Pure white for weightless aesthetic */
717
+ --nx-page-background: oklch(0.97 0 0);
718
+ /* #f9f9f9 for page content area */
719
+ --nx-foreground: oklch(0 0 0);
720
+
721
+ /* Component colors */
722
+ --nx-card: oklch(1 0 0);
723
+ /* pure white for cards to pop on off-white bg */
724
+ --nx-card-foreground: oklch(0 0 0);
725
+ --nx-popover: oklch(0.99 0 0);
726
+ /* white */
727
+ --nx-popover-foreground: oklch(0 0 0);
728
+ /* Table headers, table footers and the pagination bar. A surface, so it lives
729
+ with the other surfaces: declared next to the component that paints it, a
730
+ palette change would move everything around it and leave it behind. */
731
+ --nx-table-header-bg: oklch(1 0 0);
732
+
733
+ /* Brand colors - Blue-Cyan Professional Palette */
734
+ --nx-primary: oklch(0 0 0);
735
+ /* Black for premium minimalist aesthetic */
736
+ --nx-primary-foreground: oklch(1 0 0);
737
+ /* white */
738
+
739
+ /* Secondary colors - using slate-100 for subtle backgrounds */
740
+ --nx-secondary: oklch(0.94 0 0);
741
+ /* slate-100 */
742
+ --nx-secondary-foreground: oklch(0 0 0);
743
+
744
+ /* Utility colors */
745
+ --nx-muted: oklch(0.97 0 0);
746
+ /* Darkened so muted text clears WCAG 4.5:1 on the muted surface (was 4.35:1). */
747
+ --nx-muted-foreground: oklch(0.44 0 0);
748
+ /* A hover and selection SURFACE, not ink: one step off the page, with
749
+ `--nx-accent-foreground` as the only thing meant to be read on it. */
750
+ --nx-accent: oklch(0.94 0 0);
751
+ --nx-accent-foreground: oklch(0 0 0);
752
+
753
+ /* State colors - WCAG 2.2 AA Compliant.
754
+ * Each status has two roles kept as separate tokens, the industry-standard
755
+ * split (Radix/Primer): the base token is the readable TEXT color used by
756
+ * `text-destructive` and derived shades, and `-solid` is the saturated FILL
757
+ * used by buttons under the on-color ink. In light mode both roles can be
758
+ * the same value and the ink is white. In dark mode they cannot: a fill
759
+ * light enough to read as text on the page is too light for white text, so
760
+ * success splits the fill and destructive flips its ink to black instead.
761
+ * The on-color token is what varies, not just the fill. */
762
+ /* red-500 itself, the reference palette's red. It is lighter than the 4.5:1
763
+ * text minimum allows; darkening it to pass reads as a different colour, and
764
+ * the palette is built around this one. `contrast/accepted.ts` records the
765
+ * affected pairings with their measured ratios. */
766
+ --nx-destructive: oklch(0.63 0.19 23.03);
767
+ --nx-destructive-solid: oklch(0.63 0.19 23.03);
768
+ --nx-destructive-foreground: oklch(1 0 0);
769
+ /* white */
770
+ /* Darkened from green-600; renders as #00852c, ~4.8:1 as text on white. */
771
+ --nx-success: oklch(0.4981 0.17 149.2);
772
+ --nx-success-solid: oklch(0.53 0.17 149.2);
773
+ --nx-success-foreground: oklch(1 0 0);
774
+ /* white */
775
+ /* Darkened from amber-500 so it reads as text on white at WCAG 4.5:1 (was
776
+ * 2.15:1); its light shades still tint badge/alert surfaces. */
777
+ --nx-warning: oklch(0.529 0.1646 70.11);
778
+ --nx-warning-foreground: oklch(0.2079 0.0399 265.73);
779
+ /* slate-900 - REQUIRED for AA contrast (warning-500 on white = 3.4:1, fails AA) */
780
+
781
+ /* The highlighter mark in authored content. Kept a light yellow in both
782
+ * modes on purpose: it stands for a marker pen drawn over the text, and the
783
+ * pairing below is what keeps that readable rather than the page's own
784
+ * foreground, which would be white-on-yellow in dark mode. */
785
+ --nx-highlight: oklch(0.9297 0.1638 101.06);
786
+ /* yellow-200 */
787
+ --nx-highlight-foreground: oklch(0.2079 0.0399 265.73);
788
+ /* slate-900 — the mark keeps its own dark text, whatever the page does */
789
+
790
+ /* Syntax colors. The tokenizer reports what a token *is*; these decide how
791
+ * it looks, so a code block carries no colour of its own and renders from
792
+ * whichever mode is active. Kept muted against the monochrome chrome, and
793
+ * held above 4.5:1 on --nx-code-bg since code is small text. */
794
+ --nx-code-bg: oklch(0.9761 0.0035 247.86);
795
+ --nx-code-fg: oklch(0.2079 0.0399 265.73);
796
+ /* Darkened slightly to clear WCAG 4.5:1 on the code surface (was 4.44:1). */
797
+ --nx-code-comment: oklch(0.5264 0.0407 257.44);
798
+ --nx-code-keyword: oklch(0.4882 0.2172 303.9);
799
+ --nx-code-string: oklch(0.4478 0.1189 152.1);
800
+ --nx-code-number: oklch(0.5106 0.1518 44.2);
801
+ --nx-code-function: oklch(0.4757 0.1444 254.6);
802
+ --nx-code-operator: oklch(0.4936 0.1418 8.4);
803
+ /* Darkened slightly to clear WCAG 4.5:1 on the code surface (was 4.44:1). */
804
+ --nx-code-punctuation: oklch(0.5264 0.0407 257.44);
805
+ --nx-code-variable: oklch(0.4694 0.1173 62.3);
806
+ --nx-code-tag: oklch(0.5054 0.1905 27.5);
807
+ --nx-code-deleted: oklch(0.5054 0.1905 27.5);
808
+ --nx-code-inserted: oklch(0.4478 0.1189 152.1);
809
+
810
+ /* Element colors — 3-tier translucent border scale (monochrome).
811
+ * The first two tiers are decorative and are tuned by eye: subtle = faint
812
+ * dividers, default = the container edge on cards, groups, and tables
813
+ * (composites to a light grey on white, a hairline rather than a rule). The third
814
+ * is not decorative — strong marks the edge of a control that has no other
815
+ * boundary, so it is held to a contrast minimum instead. */
816
+ /* The scrim behind a modal. A token rather than a literal because it is the
817
+ * one surface colour that must differ between the two modes for the same
818
+ * reason a border does: it composites over what is beneath it, and what is
819
+ * beneath it is white here and near-black in dark. It was written inline as
820
+ * `bg-black/80`, identical in both modes, at four different strengths across
821
+ * six components — drift rather than intent. */
822
+ --nx-overlay: oklch(0 0 0 / 0.55);
823
+ /* A scrim you READ ON rather than look through. `--nx-overlay` sits under a
824
+ * modal, which supplies its own surface for text; a full-screen state screen
825
+ * puts its message straight onto the scrim, where the MUTED line decides the
826
+ * strength, not the heading: `text-white/60` over 55% on a white page is
827
+ * 2.81:1, and over 80% it is 5.66:1. Pure white clears AA on either (4.76:1
828
+ * and 12.63:1), so the heading alone would not have justified this token. */
829
+ --nx-overlay-strong: oklch(0 0 0 / 0.8);
830
+ /* For a scrim over CONTENT rather than the page: an image's hover actions,
831
+ * where the content beneath still has to read through. */
832
+ --nx-overlay-soft: oklch(0 0 0 / 0.4);
833
+ --nx-border-subtle: color-mix(in srgb, oklch(0.92 0 0), transparent 60%);
834
+ --nx-border: oklch(0.92 0 0);
835
+ /* These two sit BELOW the WCAG 1.4.11 3:1 minimum for a control boundary,
836
+ * deliberately, to keep the light border weight the palette is designed
837
+ * around. `contrast/accepted.ts` records each affected pairing with its
838
+ * measured ratio and is what keeps that visible rather than silent; the
839
+ * contrast suite reads that list, so neither token can drift further without
840
+ * failing. */
841
+ --nx-border-strong: color-mix(in srgb, oklch(0.92 0 0), black 25%);
842
+ --nx-input: oklch(0.94 0 0);
843
+ /* The boundary of a control that has NOTHING ELSE to identify it.
844
+ *
845
+ * A text field at --nx-input is identifiable without its edge: it carries a
846
+ * label, a placeholder, a value, and a focus ring the moment it is used. An
847
+ * unchecked checkbox or radio is only the box. Its border is not decoration
848
+ * around the control, it IS the control, so 1.4.11's 3:1 applies with nothing
849
+ * to fall back on -- and at the reference palette's border weight the box is
850
+ * effectively invisible.
851
+ *
852
+ * These were one token, which is why the reference weight could not be taken
853
+ * without losing the checkbox. They are two questions -- "how heavy is a field
854
+ * edge" and "can this control be seen at all" -- and they only shared an
855
+ * answer by accident. Splitting them is what lets the field borders match the
856
+ * reference while the controls stay identifiable.
857
+ *
858
+ * Solved to the LIGHTEST value that still clears 3:1 with the margin the
859
+ * contrast suite requires, against the darkest surface a control lands on, so
860
+ * it sits as close to the reference weight as the criterion permits. */
861
+ --nx-control-border: oklch(0.62 0 0);
862
+ --nx-ring: var(--nx-primary);
863
+
864
+ /* The base colour of the elevation ramp (--shadow-sm..xl, -inner-subtle,
865
+ * -neo). Each step mixes this down to its own alpha, so retinting every
866
+ * shadow at once is a single token change. */
867
+ --nx-shadow-color: oklch(0 0 0);
868
+
869
+ /* Chart colors for data visualization */
870
+ --nx-chart-1: var(--nx-primary);
871
+ --nx-chart-2: oklch(0.7399 0.1277 209.34);
872
+ --nx-chart-3: oklch(0.6273 0.17 149.2);
873
+ /* green-600 */
874
+ --nx-chart-4: oklch(0.7686 0.1646 70.11);
875
+ /* amber-500 */
876
+ --nx-chart-5: oklch(0.6368 0.2078 25.33);
877
+ /* red-500 */
878
+
879
+ /* Design tokens */
880
+ /* The one corner knob. Every tier below derives from it, so this single
881
+ * declaration re-rounds the whole admin and a plugin built against the
882
+ * published preset re-rounds with it. */
883
+ --radius: 0.5rem;
884
+
885
+ /* Control sizing — derived from a single height knob so control density
886
+ * scales from one place. Steps map to the control size variants:
887
+ * sm 32px, md 36px, default 40px, lg 44px. */
888
+ --nx-control-height: 2.5rem;
889
+ --nx-control-height-sm: calc(var(--nx-control-height) - 0.5rem);
890
+ --nx-control-height-md: calc(var(--nx-control-height) - 0.25rem);
891
+ --nx-control-height-lg: calc(var(--nx-control-height) + 0.25rem);
892
+
893
+ /* Field width caps — bound how wide a form control grows so a short value
894
+ * (a slug, a toggle, a single date) does not stretch to the full content
895
+ * width. `FieldShell` reads these with a literal fallback, so a consumer
896
+ * theme that predates these tokens still renders correctly; the token is
897
+ * what lets a theme retune the caps centrally instead. A length, not a
898
+ * colour, so it is declared once here rather than per light/dark block —
899
+ * a field's width does not change with the theme. */
900
+ --nx-field-half: 380px;
901
+ --nx-field-full: 760px;
902
+
903
+ /* Page shell — the inset from the content panel's edge to the page's content,
904
+ * and the measure that content is bounded to. `PageShell` spends these as
905
+ * GRID COLUMNS rather than as padding, because padding cannot be cancelled by
906
+ * a descendant: that is why full-bleed content previously had to be rendered
907
+ * OUTSIDE the wrapper to escape the measure, and why two wrappers each
908
+ * applying their own inset silently doubled it.
909
+ *
910
+ * A length rather than a colour, so it is declared once here rather than per
911
+ * light/dark block — a page's inset does not change with the theme. */
912
+ --nx-gutter: 2rem;
913
+ --nx-measure-form: 56rem;
914
+ --nx-measure-wide: 72rem;
915
+
916
+ /* The measure the content column is bounded to, declared here so a theme can
917
+ * retune it centrally and so the token resolves to a real value rather than
918
+ * to a `var()` fallback. `PageShell` overrides it INLINE per its `width`
919
+ * prop, which is what makes the three widths one grid template instead of
920
+ * three near-identical ones; this declaration is the value that applies when
921
+ * the grid is used without that component. */
922
+ --nx-shell-measure: var(--nx-measure-form);
923
+
924
+ /* The single vertical-rhythm token for a form section. `FormSection` applies
925
+ * it to every DIRECT child, so the card's top and bottom breathing room and
926
+ * the gap between two fields are one decision and cannot drift apart — the
927
+ * first child's top padding IS the card's top inset.
928
+ *
929
+ * The rhythm belongs to the section rather than to the rows it is handed. A
930
+ * section renders whatever children it is given, and cannot tell one row
931
+ * idiom from another; leaving each row to supply its own vertical padding
932
+ * makes the card's inset depend on which idiom the caller happened to reach
933
+ * for, and a row that supplies none renders flush against the borders. */
934
+ --nx-field-gap: 1.25rem;
935
+
936
+ /* Focus ring colors */
937
+ --nx-focus-ring: var(--nx-primary);
938
+ --nx-focus-ring-offset: oklch(1 0 0);
939
+ /* white */
940
+
941
+ /* Sidebar specific colors - using Slate scale */
942
+ --nx-sidebar-background: oklch(0.99 0 0);
943
+ /* flat with the content surface; nav is separated by borders, not fill */
944
+ /* Resting and active nav ink are the same value in light mode: nav labels sit
945
+ * at body-text weight rather than a step back from it, which is the reference
946
+ * palette's arrangement.
947
+ *
948
+ * So the active row is marked by its FILL (--nx-sidebar-accent) TOGETHER WITH
949
+ * a font-weight change, and the pair is load-bearing rather than decorative.
950
+ * The fill alone is about 1.11:1, far under the 3:1 WCAG 1.4.11 asks of
951
+ * information identifying a component's state, and there is no exemption for
952
+ * a large filled region -- that is 1.4.3, and it is about text. The weight
953
+ * change is what satisfies the criterion, being a difference that is not a
954
+ * colour and so carries no ratio at all.
955
+ *
956
+ * Removing `data-[active=true]:font-medium` from the sidebar menu buttons
957
+ * therefore leaves the nav with no conforming active state, and
958
+ * `contrast/__tests__/sidebar-ink-hierarchy.test.ts` fails per variant if
959
+ * either loses it.
960
+ *
961
+ * Dark mode keeps the two-step ink relationship instead, where a near-white
962
+ * active ink on a dark row is what separates the states. */
963
+ --nx-sidebar-foreground: oklch(0 0 0);
964
+ --nx-sidebar-primary: oklch(0 0 0);
965
+ --nx-sidebar-primary-foreground: oklch(1 0 0);
966
+ /* slate-50 */
967
+ --nx-sidebar-accent: oklch(0.94 0 0);
968
+ /* slate-100 */
969
+ --nx-sidebar-accent-foreground: oklch(0 0 0);
970
+ /* Below the 3:1 boundary minimum, deliberately, to match the border weight of
971
+ * the rest of the palette. Recorded in `contrast/accepted.ts`. */
972
+ --nx-sidebar-border: oklch(0.94 0 0);
973
+ --nx-sidebar-ring: var(--nx-primary);
974
+
975
+ /* Data table specific colors */
976
+ --nx-table-row-hover: oklch(0.97 0 0);
977
+ --nx-table-row-selected: var(--nx-primary);
978
+ --nx-table-border: var(--nx-border);
979
+ }
980
+
981
+ /* Dark theme overrides when using class-based theming */
982
+ .dark {
983
+ /* Base colors - using Slate dark scale */
984
+ --nx-background: oklch(0 0 0);
985
+ /* ultra deep dark background (darker than slate-950) */
986
+ --nx-page-background: oklch(0.23 0 0);
987
+ /* Matches background in dark mode */
988
+ --nx-foreground: oklch(1 0 0);
989
+ /* slate-50 */
990
+
991
+ /* Component colors */
992
+ --nx-card: oklch(0.14 0 0);
993
+ /* raised one tonal step above --nx-background (222 47% 4%) so panels/cards
994
+ * read as distinct surfaces in dark mode instead of merging into the page */
995
+ --nx-card-foreground: oklch(1 0 0);
996
+ /* slate-50 */
997
+ /* Darkened toward the card elevation so borders, inputs, and muted text
998
+ * placed on a popover clear their WCAG minimums (border was 2.85:1, input
999
+ * 1.73:1); the surface was too light for its own contents. */
1000
+ --nx-popover: oklch(0.18 0 0);
1001
+ --nx-popover-foreground: oklch(1 0 0);
1002
+ /* slate-50 */
1003
+ /* A step above the card beneath it, carrying no chroma of its own: a hue here
1004
+ would tint the header band independently of every surface around it. */
1005
+ --nx-table-header-bg: oklch(0.1573 0 0);
1006
+
1007
+ /* Brand colors - Blue-Cyan Professional Palette (Dark Mode) */
1008
+ --nx-primary: oklch(1 0 0); /* High contrast White for dark mode */
1009
+ --nx-primary-foreground: oklch(0 0 0);
1010
+
1011
+ /* Secondary colors - using slate-800 for dark backgrounds */
1012
+ --nx-secondary: oklch(0.25 0 0);
1013
+ /* slate-800 */
1014
+ --nx-secondary-foreground: oklch(1 0 0);
1015
+ /* slate-50 */
1016
+
1017
+ /* Utility colors */
1018
+ --nx-muted: oklch(0.23 0 0);
1019
+ --nx-muted-foreground: oklch(0.72 0 0);
1020
+ /* The hover and selected surface: one step lighter than --nx-muted so a row
1021
+ under the pointer separates from a merely quiet one. Neutral, not the blue
1022
+ it was once labelled -- a tinted accent reads as a status on a surface that
1023
+ carries none. */
1024
+ --nx-accent: oklch(0.32 0 0);
1025
+ --nx-accent-foreground: oklch(1 0 0);
1026
+ /* white */
1027
+
1028
+ /* State colors - WCAG 2.2 AA Compliant (Dark Mode).
1029
+ * The base token is lightened from the fill color so it reads as text on the
1030
+ * dark page at 4.5:1. The two statuses then resolve the on-color problem
1031
+ * differently, because a fill light enough to read as text on a dark page is
1032
+ * too light for white text: destructive keeps ONE value for base and fill
1033
+ * and flips its ink to black (6.90:1), while success keeps a darker separate
1034
+ * fill and keeps its ink white (4.80:1). Warning is already light enough to
1035
+ * read on dark, so its base is unchanged here. */
1036
+ /* Lightened red so it reads as text at WCAG 4.5:1 on the dark page and on the
1037
+ * lighter popover surface (was 4.17:1 on the page, 4.25:1 on a popover). */
1038
+ --nx-destructive: oklch(0.69 0.2 23.91);
1039
+ /* Same value as the base: the button fill needs no darkening here because
1040
+ its ink is black rather than white. */
1041
+ --nx-destructive-solid: oklch(0.69 0.2 23.91);
1042
+ /* Black, not white. The dark-mode fill is light enough that black on-color
1043
+ text reads at 6.90:1 where white would fall below the minimum. */
1044
+ --nx-destructive-foreground: oklch(0 0 0);
1045
+ /* Lightened green so it reads as text at WCAG 4.5:1 on the dark page and on
1046
+ * the lighter popover surface (was 4.17:1 on the page, 4.29:1 on a popover). */
1047
+ /* Lightened a step so success ink clears 4.5:1 on --nx-muted with the margin
1048
+ * the suite requires. Muted is the binding surface here, being the lightest
1049
+ * of the dark surfaces this ink lands on; against card or page it was never
1050
+ * close. */
1051
+ --nx-success: oklch(0.61 0.1921 149.58);
1052
+ /* Darker green fill so white on-color button text clears 4.5:1. */
1053
+ --nx-success-solid: oklch(0.5225 0.1921 149.58);
1054
+ --nx-success-foreground: oklch(1 0 0);
1055
+ /* white */
1056
+ --nx-warning: oklch(0.7686 0.1646 70.11);
1057
+ /* amber-500 */
1058
+ --nx-warning-foreground: oklch(0.2079 0.0399 265.73);
1059
+ /* slate-900 */
1060
+
1061
+ /* Dimmed so the mark reads as highlighted rather than as a lamp on a dark
1062
+ * page, while its own foreground stays dark enough to read against it. */
1063
+ --nx-highlight: oklch(0.8 0.1425 101.06);
1064
+ /* yellow-300-ish, pulled back for the dark surface */
1065
+ --nx-highlight-foreground: oklch(0.2079 0.0399 265.73);
1066
+ /* slate-900 */
1067
+
1068
+ /* Syntax colors (dark). Lightened rather than reused from light mode: the
1069
+ * light hues sit near 0.45-0.5 lightness and disappear on the dark surface.
1070
+ * Chroma is pulled back slightly so they don't vibrate against it. */
1071
+ --nx-code-bg: oklch(0.1916 0.0228 266.36);
1072
+ --nx-code-fg: oklch(0.9838 0.0035 247.86);
1073
+ --nx-code-comment: oklch(0.6626 0.0364 256.79);
1074
+ --nx-code-keyword: oklch(0.7482 0.1235 303.9);
1075
+ --nx-code-string: oklch(0.7654 0.1476 152.1);
1076
+ --nx-code-number: oklch(0.7807 0.1189 44.2);
1077
+ --nx-code-function: oklch(0.7365 0.1163 254.6);
1078
+ --nx-code-operator: oklch(0.7549 0.1234 8.4);
1079
+ --nx-code-punctuation: oklch(0.6626 0.0364 256.79);
1080
+ --nx-code-variable: oklch(0.7938 0.1052 62.3);
1081
+ --nx-code-tag: oklch(0.7118 0.1476 27.5);
1082
+ --nx-code-deleted: oklch(0.7118 0.1476 27.5);
1083
+ --nx-code-inserted: oklch(0.7654 0.1476 152.1);
1084
+
1085
+ /* Element colors — 3-tier translucent border scale (white-alpha for dark).
1086
+ * Recomputed as white-over-dark alpha (not a flipped hex) so dividers read
1087
+ * on the deep background without glowing. The decorative tiers carry the same
1088
+ * alphas as light mode, which lands the container edge at a matched 1.41:1 on
1089
+ * the page (1.38:1 light), so a hairline weighs the same in both modes. */
1090
+ /* Stronger than light: the surfaces beneath are mid-tone rather than white,
1091
+ * so the same alpha suppresses less of them. Still black rather than a tinted
1092
+ * scrim, because the modal above it supplies the contrast. */
1093
+ --nx-overlay: oklch(0 0 0 / 0.7);
1094
+ --nx-overlay-strong: oklch(0 0 0 / 0.85);
1095
+ --nx-overlay-soft: oklch(0 0 0 / 0.5);
1096
+ --nx-border-subtle: color-mix(in srgb, oklch(0.26 0 0), transparent 60%);
1097
+ --nx-border: oklch(0.26 0 0);
1098
+ /* Alpha raised so the composited strong border clears WCAG 3:1 / ~4:1 (was 3.00:1). */
1099
+ --nx-border-strong: oklch(0.5444 0 0);
1100
+ /* Brightened so the field border clears WCAG 3:1 on the lighter popover
1101
+ * surface as well as the page and card (was 1.73:1 on a popover). */
1102
+ --nx-input: oklch(0.5445 0 0);
1103
+ /* Dark mode never gave up the boundary minimum, so the control border simply
1104
+ tracks the field border here and the split costs nothing. */
1105
+ --nx-control-border: oklch(0.5445 0 0);
1106
+ --nx-ring: var(--nx-primary);
1107
+
1108
+ /* Black in dark mode too: the ramp reads as depth against the deep surfaces
1109
+ * rather than as a glow, which a lightened shadow colour would produce.
1110
+ * One --shadow-* ramp serves both modes and inlines a single set of
1111
+ * color-mix opacities, so this token is the only place elevation is
1112
+ * retuned -- overriding it resolves the whole ramp to that hue. */
1113
+ --nx-shadow-color: oklch(0 0 0);
1114
+
1115
+ /* Chart colors for data visualization (same as light mode) */
1116
+ --nx-chart-1: var(--nx-primary);
1117
+ --nx-chart-2: oklch(0.7399 0.1277 209.34);
1118
+ --nx-chart-3: oklch(0.7229 0.1921 149.58);
1119
+ /* green-500 */
1120
+ --nx-chart-4: oklch(0.7686 0.1646 70.11);
1121
+ /* amber-500 */
1122
+ --nx-chart-5: oklch(0.6368 0.2078 25.33);
1123
+ /* red-500 */
1124
+
1125
+ /* Sidebar specific colors - dark mode */
1126
+ --nx-sidebar-background: oklch(0.18 0 0);
1127
+ /* flat with the content surface; nav is separated by borders, not fill */
1128
+ /* Resting nav ink, dimmed off slate-50 so it sits a step below
1129
+ * --nx-sidebar-accent-foreground the way the light pair does: the lightness
1130
+ * gap here (0.164) is the same one light mode puts between slate-600 and
1131
+ * slate-900, so an active row reads as emphasised by its ink and not only by
1132
+ * its fill. Chroma and hue track the theme's other secondary ink
1133
+ * (--nx-muted-foreground) so lightness alone carries the hierarchy. */
1134
+ --nx-sidebar-foreground: oklch(0.72 0 0);
1135
+ --nx-sidebar-primary: oklch(1 0 0);
1136
+ /* slate-50 */
1137
+ --nx-sidebar-primary-foreground: oklch(0 0 0);
1138
+ --nx-sidebar-accent: oklch(0.32 0 0);
1139
+ --nx-sidebar-accent-foreground: oklch(1 0 0);
1140
+ /* slate-50 */
1141
+ /* Lightened so the sidebar border clears WCAG 3:1 on its surface (was 1.19:1). */
1142
+ --nx-sidebar-border: oklch(0.519 0 0);
1143
+ --nx-sidebar-ring: var(--nx-primary);
1144
+
1145
+ /* Data table specific colors - dark mode */
1146
+ --nx-table-row-hover: oklch(0.23 0 0);
1147
+ --nx-table-row-selected: var(--nx-primary);
1148
+ --nx-table-border: var(--nx-border);
1149
+ /* inherit the retuned white-alpha border (was a fixed faint slate) */
1150
+ }
1151
+
1152
+ /* ============================================================
1153
+ * Page shell
1154
+ * ============================================================
1155
+ *
1156
+ * The gutter and the measure are ONE grid definition, spent as columns rather
1157
+ * than as padding. That is load-bearing rather than stylistic:
1158
+ *
1159
+ * - Padding cannot be cancelled by a descendant, so a block that must run
1160
+ * edge-to-edge previously had to be rendered OUTSIDE the wrapper imposing
1161
+ * the measure. `.nx-bleed` turns that from an accident into a declaration.
1162
+ * - Two wrappers that each apply an inset silently add. As columns there is
1163
+ * one declaration, so double-padding is unrepresentable rather than merely
1164
+ * discouraged.
1165
+ * - Every direct child defaults to the `content` column, so a page header and
1166
+ * the card beneath it cannot disagree about their left edge.
1167
+ *
1168
+ * Centring comes from the two outer tracks being equal rather than from
1169
+ * `margin-inline: auto` on a max-width box, which does nothing at panel widths
1170
+ * where the cap does not bind.
1171
+ */
1172
+ @layer components {
1173
+ /* A form section's vertical rhythm, applied to every DIRECT child.
1174
+ *
1175
+ * Plain CSS rather than a Tailwind utility on the element, for two reasons.
1176
+ * The parenthesised custom-property shorthand is v4-only, so the same class
1177
+ * compiles to a rule under one Tailwind major and to nothing under the other,
1178
+ * and the section renders flush wherever it compiles to nothing — silently,
1179
+ * because the markup is identical either way. And an arbitrary-value token is
1180
+ * EXTRACTED from any file the scanner reads, a comment or a test string
1181
+ * included, so merely naming one of these utilities in prose emits its rule;
1182
+ * a malformed spelling anywhere in the repository fails the whole stylesheet.
1183
+ *
1184
+ * A rule shipped in the theme has neither property: it is the same CSS under
1185
+ * both majors, and there is no token for a scanner to find.
1186
+ *
1187
+ * Children must not pad themselves. The two paddings are additive, so a row
1188
+ * carrying its own `py-*` doubles the rhythm on the sections that are already
1189
+ * correct. */
1190
+ .nx-form-section-rows > * {
1191
+ padding-block: var(--nx-field-gap);
1192
+ }
1193
+
1194
+ .nx-page-shell {
1195
+ display: grid;
1196
+ /* The outer tracks are `minmax(gutter, 1fr)` rather than a fixed gutter so
1197
+ * the grid always spans its container: a fixed pair would total only
1198
+ * `measure + 2 * gutter`, leaving `full-start`/`full-end` short of the
1199
+ * panel's edges in anything wider, and a `Bleed` child short with them. The
1200
+ * `1fr` maximum absorbs the surplus while `gutter` holds as the minimum.
1201
+ *
1202
+ * Their being equal is also what centres the content column, so no
1203
+ * `justify-content` is declared: with flexible tracks the grid fills its
1204
+ * container and there is no free space for it to distribute. */
1205
+ grid-template-columns:
1206
+ [full-start] minmax(var(--nx-gutter), 1fr)
1207
+ [content-start] minmax(0, var(--nx-shell-measure))
1208
+ [content-end] minmax(var(--nx-gutter), 1fr)
1209
+ [full-end];
1210
+ align-content: start;
1211
+ }
1212
+
1213
+ /* `min-width: 0` because a grid item's default `min-width: auto` refuses to
1214
+ * shrink below its content, which lets one wide descendant — a long code
1215
+ * string, a table — push the column past the measure it was given. */
1216
+ .nx-page-shell > * {
1217
+ grid-column: content;
1218
+ min-width: 0;
1219
+ }
1220
+
1221
+ /* Scoped to a DIRECT child: the named lines exist on this grid alone, so a
1222
+ * `.nx-bleed` nested deeper resolves against a grid that never declared them.
1223
+ * Writing the rule this way means such a case is inert rather than
1224
+ * mysteriously half-working. */
1225
+ .nx-page-shell > .nx-bleed {
1226
+ grid-column: full;
1227
+ }
1228
+ }
1229
+
1230
+ /* The gutter steps with the content PANEL rather than the viewport: the panel's
1231
+ * width changes when a sidebar opens, and the panel is what the page sits in.
1232
+ * `content` is the container the admin's <main> already names. The step values
1233
+ * match Payload's --gutter-h ladder (32 / 24 / 16).
1234
+ *
1235
+ * The override lands on `.nx-page-shell`, a DESCENDANT of that container. A
1236
+ * container query styles only elements inside its container, so a rule naming
1237
+ * `:root` or `.nextly-admin` — both ancestors of the `<main>` that declares it —
1238
+ * never matches, and the gutter keeps its widest value at every panel size. */
1239
+ @container content (max-width: 1024px) {
1240
+ .nx-page-shell {
1241
+ --nx-gutter: 1.5rem;
1242
+ }
1243
+ }
1244
+
1245
+ @container content (max-width: 768px) {
1246
+ .nx-page-shell {
1247
+ --nx-gutter: 1rem;
1248
+ }
1249
+ }