@human-synthesis/norns-ui 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +32 -20
  2. package/package.json +7 -3
  3. package/src/auto-import.js +1 -4
  4. package/src/components/Accordion.n +24 -23
  5. package/src/components/Autocomplete.n +108 -33
  6. package/src/components/Collapsible.n +8 -11
  7. package/src/components/ContextMenu.n +55 -36
  8. package/src/components/DatePicker.n +19 -31
  9. package/src/components/DateRangePicker.n +17 -29
  10. package/src/components/Dialog.n +35 -22
  11. package/src/components/Dropdown.n +54 -23
  12. package/src/components/MultiSelect.n +138 -36
  13. package/src/components/Popover.n +30 -15
  14. package/src/components/ScrollArea.n +9 -20
  15. package/src/components/Sheet.n +36 -22
  16. package/src/components/Tabs.n +39 -13
  17. package/src/components/TimePicker.n +52 -65
  18. package/src/components/ToggleButton.n +18 -12
  19. package/src/components/ToggleButtonGroup.n +31 -11
  20. package/src/components/Tooltip.n +52 -19
  21. package/src/index.js +1 -4
  22. package/src/lib/behaviors/clickOutside.svelte.js +54 -0
  23. package/src/lib/behaviors/escape.svelte.js +36 -0
  24. package/src/lib/behaviors/floating.svelte.js +137 -0
  25. package/src/lib/behaviors/focusTrap.svelte.js +80 -0
  26. package/src/lib/behaviors/index.js +14 -0
  27. package/src/lib/behaviors/portal.svelte.js +32 -0
  28. package/src/lib/behaviors/rovingTabindex.svelte.js +76 -0
  29. package/src/lib/behaviors/scrollLock.svelte.js +56 -0
  30. package/src/styles/atoms.css +628 -920
  31. package/src/styles/tokens.css +33 -0
  32. package/src/types/Collapsible.d.ts +1 -0
  33. package/src/types/ContextMenu.d.ts +6 -1
  34. package/src/types/DatePicker.d.ts +7 -2
  35. package/src/types/DateRangePicker.d.ts +3 -3
  36. package/src/types/Dialog.d.ts +4 -0
  37. package/src/types/Dropdown.d.ts +2 -1
  38. package/src/types/MultiSelect.d.ts +3 -0
  39. package/src/types/ScrollArea.d.ts +1 -1
  40. package/src/types/Sheet.d.ts +3 -0
  41. package/src/types/Tabs.d.ts +2 -0
  42. package/src/types/ToggleButton.d.ts +2 -1
  43. package/src/types/ToggleButtonGroup.d.ts +2 -1
  44. package/src/types/Tooltip.d.ts +3 -0
  45. package/src/components/Drawer.n +0 -49
  46. package/src/components/PreviewCard.n +0 -30
  47. package/src/components/RichTooltip.n +0 -33
  48. package/src/types/Drawer.d.ts +0 -18
  49. package/src/types/PreviewCard.d.ts +0 -16
  50. package/src/types/RichTooltip.d.ts +0 -16
@@ -1,17 +1,26 @@
1
1
  /*
2
2
  * Norns UI — component atom classes.
3
3
  *
4
- * Pure Tailwind utility-composition via `@layer components`. No JS, no Svelte.
5
- * All atoms reference *role tokens* from tokens.css never raw color shades.
6
- * That's how light/dark theme swap works through a single point of change.
4
+ * Pure Tailwind utility composition via `@layer components`. No JS, no Svelte.
5
+ * Atoms reference role tokens (and the new elevation/motion/icon-btn tokens
6
+ * in tokens.css), never raw color shades. That's how light/dark theme swap
7
+ * works through a single point of change.
7
8
  *
8
- * Naming convention: `<atom>-<variant>` and `<atom>-<size>`. AI-predictable.
9
+ * Naming: `<atom>-<variant>` and `<atom>-<size>`. Predictable, AI-friendly.
10
+ *
11
+ * Design rules — every atom obeys these unless it has a documented reason:
12
+ * 1. Radius: `--ui-radius` for floating panels & full-size controls,
13
+ * `--ui-radius-sm` for items inside menus, `--ui-radius-lg` only for
14
+ * Dialog and HeroBanner.
15
+ * 2. Hover on any interactive surface uses `bg-bg-muted`. No exceptions.
16
+ * 3. Focus uses the `--ui-focus-ring` shadow on `:focus-visible`.
17
+ * 4. Transitions use `--ui-motion-fast` + `--ui-motion-ease`.
18
+ * 5. Shadows use `--ui-elev-{1,2,3}`. No raw shadow utilities.
9
19
  */
10
20
 
11
21
  @reference 'tailwindcss';
12
22
 
13
23
  @layer base {
14
- /* Body */
15
24
  html {
16
25
  font-family: var(--font-sans);
17
26
  color: var(--color-fg);
@@ -23,98 +32,172 @@
23
32
  background-color: var(--color-bg);
24
33
  }
25
34
 
26
- /* Reduced-motion: globally short-circuit our transitions/animations.
27
- * Components using prefers-reduced-motion: reduce explicitly will get this.
28
- */
35
+ /* Respect prefers-reduced-motion without nuking decorative animations
36
+ * entirely: cap iterations to 1 (so infinite loops like the shiny
37
+ * sweep, sparkle, gradient shift play once and stop instead of
38
+ * looping), and shrink incidental transitions. Finite animations
39
+ * (ripple, copy-pop, toast-in, banner-in) keep their natural duration
40
+ * so users still see the state change they signal. */
29
41
  @media (prefers-reduced-motion: reduce) {
30
42
  *,
31
43
  *::before,
32
44
  *::after {
33
- animation-duration: 0.01ms !important;
34
45
  animation-iteration-count: 1 !important;
35
46
  transition-duration: 0.01ms !important;
36
47
  scroll-behavior: auto !important;
37
48
  }
38
49
  }
39
50
 
40
- /* Heading hierarchy — applied to bare h1..h6 unless overridden via Tailwind. */
41
- h1 {
42
- @apply text-4xl font-bold tracking-tight leading-tight;
43
- }
44
- h2 {
45
- @apply text-3xl font-semibold tracking-tight leading-tight;
46
- }
47
- h3 {
48
- @apply text-2xl font-semibold tracking-tight leading-snug;
49
- }
50
- h4 {
51
- @apply text-xl font-semibold leading-snug;
52
- }
53
- h5 {
54
- @apply text-lg font-semibold leading-snug;
55
- }
56
- h6 {
57
- @apply text-base font-semibold;
58
- }
51
+ /* Heading hierarchy */
52
+ h1 { @apply text-4xl font-bold tracking-tight leading-tight; }
53
+ h2 { @apply text-3xl font-semibold tracking-tight leading-tight; }
54
+ h3 { @apply text-2xl font-semibold tracking-tight leading-snug; }
55
+ h4 { @apply text-xl font-semibold leading-snug; }
56
+ h5 { @apply text-lg font-semibold leading-snug; }
57
+ h6 { @apply text-base font-semibold; }
59
58
 
60
- /* Inline code + pre */
61
59
  code {
62
60
  font-family: var(--font-mono);
63
- @apply text-[0.9em] px-1 py-0.5 rounded;
61
+ @apply text-[0.9em] px-1 py-0.5;
64
62
  background-color: var(--color-bg-muted);
65
63
  color: var(--color-fg);
64
+ border-radius: var(--ui-radius-sm);
66
65
  }
67
66
  pre {
68
67
  font-family: var(--font-mono);
69
- @apply text-sm p-3 rounded overflow-x-auto;
68
+ @apply text-sm p-3 overflow-x-auto;
70
69
  background-color: var(--color-bg-muted);
71
70
  color: var(--color-fg);
71
+ border-radius: var(--ui-radius);
72
72
  }
73
73
  pre code {
74
74
  @apply p-0 bg-transparent;
75
75
  }
76
76
 
77
- /* Keyboard */
78
77
  kbd {
79
78
  font-family: var(--font-mono);
80
- @apply text-xs px-1.5 py-0.5 rounded border;
79
+ @apply text-xs px-1.5 py-0.5 border;
81
80
  background-color: var(--color-bg-muted);
82
81
  border-color: var(--color-border);
83
82
  color: var(--color-fg);
83
+ border-radius: var(--ui-radius-sm);
84
84
  }
85
85
 
86
- /* Default form-control reset on dark */
87
- hr {
88
- border-color: var(--color-border);
89
- }
86
+ hr { border-color: var(--color-border); }
90
87
  }
91
88
 
92
89
  @layer components {
90
+ /* ============================================================ */
91
+ /* Shared mixins */
92
+ /* ============================================================ */
93
+
94
+ /* `.ui-ring` — single canonical focus indicator. Components compose:
95
+ * .btn { ... }
96
+ * .btn:focus-visible { box-shadow: var(--ui-focus-ring); }
97
+ * or apply this helper class directly. */
98
+ .ui-ring:focus-visible {
99
+ outline: none;
100
+ box-shadow: var(--ui-focus-ring);
101
+ }
102
+
103
+ /* `.surface-elevated` — public class for Popover, Dropdown, Tooltip-rich,
104
+ * Dialog, Sheet, Toast, nav-menu-content, etc. Each of those atoms also
105
+ * extends this rule via selector lists below, so consumers can apply
106
+ * either name. */
107
+ .surface-elevated,
108
+ .popover-content,
109
+ .dropdown-content,
110
+ .tooltip-rich,
111
+ .dialog-content,
112
+ .sheet-content,
113
+ .toast,
114
+ .nav-menu-content {
115
+ background-color: var(--color-bg-elevated);
116
+ color: var(--color-fg);
117
+ border: 1px solid var(--color-border);
118
+ border-radius: var(--ui-radius);
119
+ box-shadow: var(--ui-elev-2);
120
+ }
121
+
122
+ /* `.control-base` — public alias. Form-control atoms below extend this
123
+ * rule via selector lists, so `.input`, `.textarea`, `.select`, etc. all
124
+ * share one source of truth for size/padding/border/focus/disabled. */
125
+ .control-base,
126
+ .input,
127
+ .textarea,
128
+ .select,
129
+ .color-picker-text,
130
+ .date-picker-trigger {
131
+ @apply block w-full text-sm leading-none;
132
+ background-color: var(--color-bg);
133
+ color: var(--color-fg);
134
+ border: 1px solid var(--color-border-strong);
135
+ border-radius: var(--ui-radius);
136
+ height: var(--ui-h-md);
137
+ padding-inline: 0.75rem;
138
+ transition: border-color var(--ui-motion-fast) var(--ui-motion-ease),
139
+ background-color var(--ui-motion-fast) var(--ui-motion-ease),
140
+ box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
141
+ }
142
+ .control-base::placeholder,
143
+ .input::placeholder,
144
+ .textarea::placeholder,
145
+ .select::placeholder { color: var(--color-fg-subtle); }
146
+ .control-base:focus-visible,
147
+ .input:focus-visible,
148
+ .textarea:focus-visible,
149
+ .select:focus-visible,
150
+ .date-picker-trigger:focus-visible {
151
+ outline: none;
152
+ border-color: var(--color-ring);
153
+ box-shadow: var(--ui-focus-ring);
154
+ }
155
+ .control-base:disabled,
156
+ .input:disabled,
157
+ .textarea:disabled,
158
+ .select:disabled {
159
+ @apply cursor-not-allowed opacity-70;
160
+ background-color: var(--color-bg-muted);
161
+ }
162
+ .control-err,
163
+ .input-err,
164
+ .textarea-err,
165
+ .select-err {
166
+ border-color: var(--color-danger-500);
167
+ }
168
+ .control-err:focus-visible,
169
+ .input-err:focus-visible,
170
+ .textarea-err:focus-visible,
171
+ .select-err:focus-visible {
172
+ border-color: var(--color-danger-500);
173
+ box-shadow: 0 0 0 2px var(--color-bg), 0 0 0 4px var(--color-danger-500);
174
+ }
175
+
93
176
  /* ============================================================ */
94
177
  /* Button */
95
178
  /* ============================================================ */
96
179
  .btn {
97
180
  @apply inline-flex items-center justify-center gap-2 text-sm font-medium leading-none whitespace-nowrap;
98
- @apply transition-[background-color,color,border-color,transform,opacity] duration-150 ease-out;
99
181
  @apply select-none cursor-pointer relative overflow-hidden;
100
182
  @apply disabled:cursor-not-allowed disabled:opacity-60;
101
- @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
102
- --tw-ring-color: var(--color-ring);
103
- --tw-ring-offset-color: var(--color-bg);
104
183
  border-radius: var(--ui-radius);
105
184
  height: var(--ui-h-md);
106
185
  padding-inline: 0.875rem;
186
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease),
187
+ color var(--ui-motion-fast) var(--ui-motion-ease),
188
+ border-color var(--ui-motion-fast) var(--ui-motion-ease),
189
+ transform var(--ui-motion-fast) var(--ui-motion-ease),
190
+ box-shadow var(--ui-motion-fast) var(--ui-motion-ease),
191
+ opacity var(--ui-motion-fast) var(--ui-motion-ease);
107
192
  }
108
- .btn:not(:disabled):active {
109
- transform: scale(0.97);
110
- }
193
+ .btn:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
194
+ .btn:not(:disabled):active { transform: scale(0.97); }
111
195
 
112
196
  .btn-sm {
113
197
  @apply text-xs;
114
198
  height: var(--ui-h-sm);
115
199
  padding-inline: 0.625rem;
116
200
  }
117
-
118
201
  .btn-lg {
119
202
  @apply text-base;
120
203
  height: var(--ui-h-lg);
@@ -125,23 +208,19 @@
125
208
  background-color: var(--color-primary-600);
126
209
  color: var(--color-fg-on-primary);
127
210
  }
128
- .btn-primary:hover {
129
- background-color: var(--color-primary-700);
130
- }
131
- .btn-primary:active {
132
- background-color: var(--color-primary-800);
133
- }
211
+ .btn-primary:hover { background-color: var(--color-primary-700); }
212
+ .btn-primary:active { background-color: var(--color-primary-800); }
134
213
 
135
214
  .btn-secondary {
136
215
  background-color: var(--color-bg-muted);
137
216
  color: var(--color-fg);
138
217
  border: 1px solid var(--color-border);
139
218
  }
140
- .btn-secondary:hover {
141
- background-color: var(--color-bg-subtle);
142
- }
143
- .btn-secondary:active {
219
+ .btn-secondary:hover { background-color: var(--color-bg-subtle); }
220
+ .btn-secondary:active { background-color: var(--color-border); }
221
+ .btn-secondary[data-pressed='true'] {
144
222
  background-color: var(--color-border);
223
+ border-color: var(--color-border-strong);
145
224
  }
146
225
 
147
226
  .btn-ghost {
@@ -152,19 +231,19 @@
152
231
  background-color: var(--color-bg-muted);
153
232
  color: var(--color-fg);
154
233
  }
234
+ .btn-ghost[data-pressed='true'] {
235
+ background-color: var(--color-bg-muted);
236
+ color: var(--color-fg);
237
+ }
155
238
 
156
239
  .btn-danger {
157
240
  background-color: var(--color-danger-600);
158
241
  color: white;
159
242
  }
160
- .btn-danger:hover {
161
- background-color: var(--color-danger-700);
162
- }
163
- .btn-danger:active {
164
- background-color: var(--color-danger-800);
165
- }
243
+ .btn-danger:hover { background-color: var(--color-danger-700); }
244
+ .btn-danger:active { background-color: var(--color-danger-800); }
166
245
  .btn-danger:focus-visible {
167
- --tw-ring-color: var(--color-danger-500);
246
+ box-shadow: 0 0 0 2px var(--color-bg), 0 0 0 4px var(--color-danger-500);
168
247
  }
169
248
 
170
249
  .btn-link {
@@ -176,38 +255,56 @@
176
255
  color: var(--color-primary-400);
177
256
  }
178
257
 
179
- /* ============================================================ */
180
- /* Spinner (used inside Btn loading state) */
181
- /* ============================================================ */
258
+ /* Square icon-button — close buttons, calendar nav, chevron triggers, etc.
259
+ * The selector list extends `.btn-icon` to consumer atoms so they share
260
+ * one source of truth without `@apply`-ing across custom classes. */
261
+ .btn-icon,
262
+ .dialog-close,
263
+ .toast-close,
264
+ .combobox-trigger,
265
+ .calendar-nav,
266
+ .pagination-item {
267
+ @apply inline-flex items-center justify-center;
268
+ width: var(--ui-icon-btn-md);
269
+ height: var(--ui-icon-btn-md);
270
+ padding: 0;
271
+ border-radius: var(--ui-radius-sm);
272
+ color: var(--color-fg-muted);
273
+ background: transparent;
274
+ border: none;
275
+ cursor: pointer;
276
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease),
277
+ color var(--ui-motion-fast) var(--ui-motion-ease);
278
+ }
279
+ .btn-icon:hover,
280
+ .dialog-close:hover,
281
+ .combobox-trigger:hover,
282
+ .calendar-nav:hover,
283
+ .pagination-item:hover {
284
+ background-color: var(--color-bg-muted);
285
+ color: var(--color-fg);
286
+ }
287
+ .btn-icon:focus-visible,
288
+ .dialog-close:focus-visible,
289
+ .toast-close:focus-visible,
290
+ .combobox-trigger:focus-visible,
291
+ .calendar-nav:focus-visible,
292
+ .pagination-item:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
293
+ .btn-icon-sm,
294
+ .dialog-close,
295
+ .toast-close,
296
+ .combobox-trigger,
297
+ .calendar-nav { width: var(--ui-icon-btn-sm); height: var(--ui-icon-btn-sm); }
298
+ .btn-icon-lg { width: var(--ui-icon-btn-lg); height: var(--ui-icon-btn-lg); }
299
+
300
+ /* Spinner used inside Btn loading state */
182
301
  .ui-spinner {
183
302
  @apply inline-block size-4 animate-spin rounded-full border-2 border-current border-t-transparent;
184
303
  }
185
304
 
186
305
  /* ============================================================ */
187
- /* Inputs */
306
+ /* Form controls — base shared with .control-base above */
188
307
  /* ============================================================ */
189
- .input {
190
- @apply block w-full text-sm leading-none;
191
- @apply transition-[border-color,background-color] duration-150 ease-out;
192
- @apply focus:outline-none focus:ring-2;
193
- @apply disabled:cursor-not-allowed disabled:opacity-70;
194
- background-color: var(--color-bg);
195
- color: var(--color-fg);
196
- border: 1px solid var(--color-border-strong);
197
- border-radius: var(--ui-radius);
198
- height: var(--ui-h-md);
199
- padding-inline: 0.75rem;
200
- --tw-ring-color: color-mix(in oklab, var(--color-ring) 30%, transparent);
201
- }
202
- .input::placeholder {
203
- color: var(--color-fg-subtle);
204
- }
205
- .input:focus {
206
- border-color: var(--color-ring);
207
- }
208
- .input:disabled {
209
- background-color: var(--color-bg-muted);
210
- }
211
308
  .input-sm {
212
309
  @apply text-xs;
213
310
  height: var(--ui-h-sm);
@@ -218,123 +315,53 @@
218
315
  height: var(--ui-h-lg);
219
316
  padding-inline: 0.875rem;
220
317
  }
221
- .input-err {
222
- border-color: var(--color-danger-500);
223
- }
224
- .input-err:focus {
225
- border-color: var(--color-danger-500);
226
- --tw-ring-color: color-mix(in oklab, var(--color-danger-500) 30%, transparent);
227
- }
228
318
 
229
- /* Textarea */
230
319
  .textarea {
231
- @apply block w-full text-sm leading-snug px-3 py-2;
232
- @apply transition-[border-color,background-color] duration-150 ease-out;
233
- @apply focus:outline-none focus:ring-2;
234
- @apply disabled:cursor-not-allowed disabled:opacity-70;
235
- background-color: var(--color-bg);
236
- color: var(--color-fg);
237
- border: 1px solid var(--color-border-strong);
238
- border-radius: var(--ui-radius);
320
+ @apply leading-snug;
321
+ padding-block: 0.5rem;
239
322
  min-height: calc(var(--ui-h-md) * 2);
240
- --tw-ring-color: color-mix(in oklab, var(--color-ring) 30%, transparent);
241
- }
242
- .textarea::placeholder {
243
- color: var(--color-fg-subtle);
244
- }
245
- .textarea:focus {
246
- border-color: var(--color-ring);
247
- }
248
- .textarea:disabled {
249
- background-color: var(--color-bg-muted);
250
- }
251
- .textarea-err {
252
- border-color: var(--color-danger-500);
253
- }
254
- .textarea-err:focus {
255
- border-color: var(--color-danger-500);
256
- --tw-ring-color: color-mix(in oklab, var(--color-danger-500) 30%, transparent);
257
- }
258
-
259
- /* Select */
260
- .select {
261
- @apply block w-full text-sm leading-none;
262
- @apply transition-[border-color,background-color] duration-150 ease-out;
263
- @apply focus:outline-none focus:ring-2;
264
- @apply disabled:cursor-not-allowed disabled:opacity-70;
265
- background-color: var(--color-bg);
266
- color: var(--color-fg);
267
- border: 1px solid var(--color-border-strong);
268
- border-radius: var(--ui-radius);
269
- height: var(--ui-h-md);
270
- padding-inline: 0.75rem;
271
- --tw-ring-color: color-mix(in oklab, var(--color-ring) 30%, transparent);
272
- }
273
- .select:focus {
274
- border-color: var(--color-ring);
275
- }
276
- .select:disabled {
277
- background-color: var(--color-bg-muted);
278
- }
279
- .select-err {
280
- border-color: var(--color-danger-500);
323
+ height: auto;
281
324
  }
282
325
 
283
- /* Checkbox / Radio */
284
- .checkbox {
285
- @apply size-4 cursor-pointer rounded;
286
- @apply focus:outline-none focus:ring-2 focus:ring-offset-1;
287
- @apply disabled:cursor-not-allowed disabled:opacity-60;
288
- border: 1px solid var(--color-border-strong);
289
- accent-color: var(--color-primary-600);
290
- --tw-ring-color: color-mix(in oklab, var(--color-ring) 30%, transparent);
291
- --tw-ring-offset-color: var(--color-bg);
292
- }
293
- .checkbox-err {
294
- border-color: var(--color-danger-500);
295
- }
326
+ /* Checkbox / Radio — small square/round controls. Native `accent-color`
327
+ * does the colored fill once checked. */
328
+ .checkbox,
296
329
  .radio {
297
- @apply size-4 cursor-pointer rounded-full;
298
- @apply focus:outline-none focus:ring-2 focus:ring-offset-1;
330
+ @apply size-4 cursor-pointer;
299
331
  @apply disabled:cursor-not-allowed disabled:opacity-60;
300
332
  border: 1px solid var(--color-border-strong);
301
333
  accent-color: var(--color-primary-600);
302
- --tw-ring-color: color-mix(in oklab, var(--color-ring) 30%, transparent);
303
- --tw-ring-offset-color: var(--color-bg);
304
- }
305
- .radio-err {
306
- border-color: var(--color-danger-500);
334
+ transition: box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
307
335
  }
336
+ .checkbox { border-radius: var(--ui-radius-sm); }
337
+ .radio { border-radius: var(--ui-radius-full); }
338
+ .checkbox:focus-visible,
339
+ .radio:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
340
+ .checkbox-err,
341
+ .radio-err { border-color: var(--color-danger-500); }
308
342
 
309
343
  /* Switch */
310
344
  .switch {
311
345
  @apply relative inline-flex h-5 w-9 shrink-0 cursor-pointer appearance-none rounded-full;
312
- @apply transition-colors duration-150 ease-out;
313
- @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1;
314
346
  @apply disabled:cursor-not-allowed disabled:opacity-60;
315
347
  background-color: var(--color-border-strong);
316
- --tw-ring-color: color-mix(in oklab, var(--color-ring) 30%, transparent);
317
- --tw-ring-offset-color: var(--color-bg);
318
- }
319
- .switch:checked {
320
- background-color: var(--color-primary-600);
348
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease),
349
+ box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
321
350
  }
351
+ .switch:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
352
+ .switch:checked { background-color: var(--color-primary-600); }
322
353
  .switch::before {
323
354
  content: '';
324
355
  @apply absolute left-0.5 top-0.5 size-4 rounded-full;
325
- @apply transition-transform duration-150 ease-out;
326
356
  background-color: white;
357
+ transition: transform var(--ui-motion-fast) var(--ui-motion-ease);
327
358
  }
328
- .switch:checked::before {
329
- transform: translateX(1rem);
330
- }
359
+ .switch:checked::before { transform: translateX(1rem); }
331
360
 
332
361
  /* ============================================================ */
333
362
  /* Field */
334
363
  /* ============================================================ */
335
- .field {
336
- @apply flex flex-col;
337
- }
364
+ .field { @apply flex flex-col; }
338
365
  .field-label {
339
366
  @apply mb-1 block text-sm font-medium;
340
367
  color: var(--color-fg);
@@ -351,9 +378,7 @@
351
378
  @apply mt-1 text-xs;
352
379
  color: var(--color-danger-600);
353
380
  }
354
- .field-row {
355
- @apply flex items-center gap-2;
356
- }
381
+ .field-row { @apply flex items-center gap-2; }
357
382
 
358
383
  .field-group {
359
384
  @apply space-y-3 p-4;
@@ -365,31 +390,37 @@
365
390
  color: var(--color-fg);
366
391
  }
367
392
 
368
- /* Form */
369
- .form {
370
- @apply space-y-4;
371
- }
393
+ .form { @apply space-y-4; }
372
394
 
373
395
  /* ============================================================ */
374
396
  /* Dialog */
375
397
  /* ============================================================ */
376
398
  .dialog-overlay {
377
- @apply fixed inset-0 z-50 backdrop-blur-sm opacity-0 transition-opacity duration-150;
399
+ @apply fixed inset-0 z-50 backdrop-blur-sm;
378
400
  background-color: oklch(0% 0 0 / 0.5);
401
+ opacity: 0;
402
+ transition: opacity var(--ui-motion-fast) var(--ui-motion-ease);
379
403
  }
380
- .dialog-overlay[data-state='open'] {
381
- @apply opacity-100;
382
- }
404
+ .dialog-overlay[data-state='open'] { opacity: 1; }
405
+
383
406
  .dialog-content {
384
- @apply fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 p-6;
385
- @apply opacity-0 scale-95 transition duration-150 shadow-xl;
386
- background-color: var(--color-bg-elevated);
387
- color: var(--color-fg);
388
- border: 1px solid var(--color-border);
407
+ /* Centered via translate longhand so it doesn't compose with the
408
+ * scale animation below. Tailwind v4 emits `translate:` for its
409
+ * `-translate-x-*` utilities; mixing them with `transform:` here
410
+ * would double the offset. */
411
+ @apply fixed left-1/2 top-1/2 z-50 w-full max-w-lg p-6;
412
+ translate: -50% -50%;
413
+ /* base surface comes from `.surface-elevated` selector list */
389
414
  border-radius: var(--ui-radius-lg);
415
+ box-shadow: var(--ui-elev-3);
416
+ opacity: 0;
417
+ scale: 0.96;
418
+ transition: opacity var(--ui-motion-fast) var(--ui-motion-ease),
419
+ scale var(--ui-motion-fast) var(--ui-motion-ease);
390
420
  }
391
421
  .dialog-content[data-state='open'] {
392
- @apply opacity-100 scale-100;
422
+ opacity: 1;
423
+ scale: 1;
393
424
  }
394
425
  .dialog-title {
395
426
  @apply text-lg font-semibold leading-tight tracking-tight;
@@ -399,92 +430,75 @@
399
430
  @apply mt-1 text-sm;
400
431
  color: var(--color-fg-muted);
401
432
  }
402
- .dialog-body {
403
- @apply mt-4;
404
- }
405
- .dialog-actions {
406
- @apply mt-6 flex items-center justify-end gap-2;
407
- }
408
- .dialog-close {
409
- @apply absolute right-3 top-3 inline-flex size-7 items-center justify-center rounded-md;
410
- @apply focus-visible:outline-none focus-visible:ring-2;
411
- color: var(--color-fg-muted);
412
- --tw-ring-color: var(--color-ring);
413
- }
414
- .dialog-close:hover {
415
- background-color: var(--color-bg-muted);
416
- color: var(--color-fg);
417
- }
433
+ .dialog-body { @apply mt-4; }
434
+ .dialog-actions { @apply mt-6 flex items-center justify-end gap-2; }
435
+ .dialog-close { @apply absolute right-3 top-3; }
418
436
 
419
437
  /* ============================================================ */
420
- /* Sheet */
438
+ /* Sheet (and Drawer, which is just Sheet with side="left") */
421
439
  /* ============================================================ */
422
440
  .sheet-content {
423
- @apply fixed z-50 transition-transform duration-200 ease-out shadow-xl;
441
+ @apply fixed z-50 p-6;
424
442
  background-color: var(--color-bg-elevated);
425
443
  color: var(--color-fg);
426
444
  border: 1px solid var(--color-border);
445
+ box-shadow: var(--ui-elev-3);
446
+ /* Same precaution as `.dialog-content`: drive the slide via the
447
+ * `translate:` longhand and avoid mixing with `transform:` so they
448
+ * don't compose. */
449
+ transition: translate var(--ui-motion-med) var(--ui-motion-ease);
427
450
  }
428
- .sheet-right {
429
- @apply right-0 top-0 h-full w-full max-w-md translate-x-full p-6;
430
- }
431
- .sheet-right[data-state='open'] {
432
- @apply translate-x-0;
433
- }
434
- .sheet-left {
435
- @apply left-0 top-0 h-full w-full max-w-md -translate-x-full p-6;
436
- }
437
- .sheet-left[data-state='open'] {
438
- @apply translate-x-0;
439
- }
440
- .sheet-top {
441
- @apply left-0 top-0 w-full -translate-y-full p-6;
442
- }
443
- .sheet-top[data-state='open'] {
444
- @apply translate-y-0;
445
- }
446
- .sheet-bottom {
447
- @apply bottom-0 left-0 w-full translate-y-full p-6;
448
- }
449
- .sheet-bottom[data-state='open'] {
450
- @apply translate-y-0;
451
- }
452
- .sheet-body {
453
- @apply mt-4;
454
- }
451
+ .sheet-right { @apply right-0 top-0 h-full w-full max-w-md; translate: 100% 0; }
452
+ .sheet-left { @apply left-0 top-0 h-full w-full max-w-md; translate: -100% 0; }
453
+ .sheet-top { @apply left-0 top-0 w-full; translate: 0 -100%; }
454
+ .sheet-bottom { @apply bottom-0 left-0 w-full; translate: 0 100%; }
455
+ .sheet-right[data-state='open'],
456
+ .sheet-left[data-state='open'],
457
+ .sheet-top[data-state='open'],
458
+ .sheet-bottom[data-state='open'] { translate: 0 0; }
459
+ .sheet-body { @apply mt-4; }
455
460
 
456
461
  /* ============================================================ */
457
- /* Popover / Dropdown / Tooltip */
462
+ /* Popover / Dropdown / Tooltip — all surface-elevated */
458
463
  /* ============================================================ */
459
464
  .popover-content {
460
- @apply z-50 p-3 text-sm shadow-md;
461
- @apply opacity-0 scale-95 transition duration-150;
462
- background-color: var(--color-bg-elevated);
463
- color: var(--color-fg);
464
- border: 1px solid var(--color-border);
465
- border-radius: var(--ui-radius);
465
+ @apply z-50 p-3 text-sm;
466
+ /* base surface comes from `.surface-elevated` selector list */
467
+ opacity: 0;
468
+ transform: scale(0.96);
469
+ transition: opacity var(--ui-motion-fast) var(--ui-motion-ease),
470
+ transform var(--ui-motion-fast) var(--ui-motion-ease);
466
471
  }
467
472
  .popover-content[data-state='open'] {
468
- @apply opacity-100 scale-100;
473
+ opacity: 1;
474
+ transform: scale(1);
469
475
  }
470
476
 
471
477
  .dropdown-content {
472
- @apply z-50 min-w-[8rem] p-1 text-sm shadow-md;
473
- @apply opacity-0 scale-95 transition duration-150;
474
- background-color: var(--color-bg-elevated);
475
- color: var(--color-fg);
476
- border: 1px solid var(--color-border);
477
- border-radius: var(--ui-radius);
478
+ @apply z-50 text-sm;
479
+ /* base surface comes from `.surface-elevated` selector list */
480
+ min-width: 8rem;
481
+ padding: 0.25rem;
482
+ opacity: 0;
483
+ transform: scale(0.96);
484
+ transition: opacity var(--ui-motion-fast) var(--ui-motion-ease),
485
+ transform var(--ui-motion-fast) var(--ui-motion-ease);
478
486
  }
479
487
  .dropdown-content[data-state='open'] {
480
- @apply opacity-100 scale-100;
488
+ opacity: 1;
489
+ transform: scale(1);
481
490
  }
482
491
  .dropdown-item {
483
- @apply flex cursor-pointer select-none items-center px-2 py-1.5 text-sm outline-none rounded-sm;
492
+ @apply flex cursor-pointer select-none items-center gap-2 px-2 py-1.5 text-sm outline-none;
484
493
  color: var(--color-fg);
494
+ border-radius: var(--ui-radius-sm);
495
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
485
496
  }
486
- .dropdown-item[data-highlighted] {
497
+ .dropdown-item:hover:not([data-disabled]),
498
+ .dropdown-item:focus-visible,
499
+ .dropdown-item[data-active='true'] {
487
500
  background-color: var(--color-bg-muted);
501
+ outline: none;
488
502
  }
489
503
  .dropdown-item[data-disabled] {
490
504
  @apply cursor-not-allowed opacity-50;
@@ -495,23 +509,29 @@
495
509
  }
496
510
 
497
511
  .tooltip-content {
498
- @apply z-50 overflow-hidden px-2 py-1 text-xs shadow-md;
499
- @apply opacity-0 scale-95 transition duration-150;
512
+ @apply z-50 px-2 py-1 text-xs;
500
513
  background-color: var(--color-bg-inverse);
501
514
  color: var(--color-bg);
502
515
  border-radius: var(--ui-radius-sm);
516
+ box-shadow: var(--ui-elev-2);
517
+ opacity: 0;
518
+ transform: scale(0.96);
519
+ transition: opacity var(--ui-motion-fast) var(--ui-motion-ease),
520
+ transform var(--ui-motion-fast) var(--ui-motion-ease);
521
+ }
522
+ .tooltip-content[data-state='open'] {
523
+ opacity: 1;
524
+ transform: scale(1);
503
525
  }
504
- .tooltip-content[data-state='delayed-open'],
505
- .tooltip-content[data-state='instant-open'] {
506
- @apply opacity-100 scale-100;
526
+ /* `.tooltip-rich` is in the `.surface-elevated` selector list above. */
527
+ .tooltip-rich {
528
+ @apply p-3 text-sm max-w-xs;
507
529
  }
508
530
 
509
531
  /* ============================================================ */
510
532
  /* Tabs */
511
533
  /* ============================================================ */
512
- .tabs-root {
513
- @apply space-y-3;
514
- }
534
+ .tabs-root { @apply space-y-3; }
515
535
  .tabs-list {
516
536
  @apply inline-flex h-9 items-center justify-center p-1;
517
537
  background-color: var(--color-bg-muted);
@@ -519,19 +539,23 @@
519
539
  border-radius: var(--ui-radius);
520
540
  }
521
541
  .tabs-trigger {
522
- @apply inline-flex items-center justify-center whitespace-nowrap px-3 py-1 text-sm font-medium rounded-sm;
523
- @apply transition-colors focus-visible:outline-none focus-visible:ring-2;
542
+ @apply inline-flex items-center justify-center whitespace-nowrap px-3 py-1 text-sm font-medium;
524
543
  @apply disabled:cursor-not-allowed disabled:opacity-50;
525
544
  color: var(--color-fg-muted);
526
- --tw-ring-color: var(--color-ring);
545
+ background: transparent;
546
+ border: none;
547
+ cursor: pointer;
548
+ border-radius: var(--ui-radius-sm);
549
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease),
550
+ color var(--ui-motion-fast) var(--ui-motion-ease);
527
551
  }
552
+ .tabs-trigger:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
553
+ .tabs-trigger[aria-selected='true'],
528
554
  .tabs-trigger[data-state='active'] {
529
555
  background-color: var(--color-bg-elevated);
530
556
  color: var(--color-fg);
531
557
  }
532
- .tabs-content {
533
- @apply focus-visible:outline-none;
534
- }
558
+ .tabs-content:focus-visible { outline: none; }
535
559
 
536
560
  /* ============================================================ */
537
561
  /* Toast */
@@ -540,22 +564,13 @@
540
564
  @apply pointer-events-none fixed bottom-4 right-4 z-[100] flex w-full max-w-sm flex-col gap-2;
541
565
  }
542
566
  .toast {
543
- @apply pointer-events-auto flex items-start gap-3 p-3 shadow-md;
544
- background-color: var(--color-bg-elevated);
545
- color: var(--color-fg);
546
- border: 1px solid var(--color-border);
547
- border-radius: var(--ui-radius);
548
- animation: norns-toast-in 200ms ease-out;
567
+ @apply pointer-events-auto flex items-start gap-3 p-3;
568
+ /* base surface comes from `.surface-elevated` selector list */
569
+ animation: norns-toast-in var(--ui-motion-med) var(--ui-motion-ease);
549
570
  }
550
571
  @keyframes norns-toast-in {
551
- from {
552
- opacity: 0;
553
- transform: translateX(1rem);
554
- }
555
- to {
556
- opacity: 1;
557
- transform: translateX(0);
558
- }
572
+ from { opacity: 0; transform: translateX(1rem); }
573
+ to { opacity: 1; transform: translateX(0); }
559
574
  }
560
575
  .toast-info {
561
576
  background-color: var(--color-info-bg);
@@ -577,22 +592,16 @@
577
592
  color: var(--color-danger-fg);
578
593
  border-color: var(--color-danger-border);
579
594
  }
580
- .toast-message {
581
- @apply flex-1 text-sm leading-snug;
582
- }
595
+ .toast-message { @apply flex-1 text-sm leading-snug; }
583
596
  .toast-close {
584
- @apply -mr-1 -mt-0.5 inline-flex size-6 shrink-0 items-center justify-center rounded;
585
- @apply focus-visible:outline-none focus-visible:ring-2;
597
+ @apply -mr-1 -mt-0.5;
586
598
  color: currentColor;
587
599
  opacity: 0.6;
588
- --tw-ring-color: var(--color-ring);
589
- }
590
- .toast-close:hover {
591
- opacity: 1;
592
600
  }
601
+ .toast-close:hover { opacity: 1; background-color: transparent; color: currentColor; }
593
602
 
594
603
  /* ============================================================ */
595
- /* Card / Surface */
604
+ /* Surface / Card */
596
605
  /* ============================================================ */
597
606
  .surface {
598
607
  background-color: var(--color-bg-elevated);
@@ -610,21 +619,15 @@
610
619
  color: var(--color-fg);
611
620
  border: 1px solid var(--color-border);
612
621
  border-radius: var(--ui-radius-lg);
613
- /* No overflow:hidden — would clip popovers / menu dropdowns inside.
614
- * Header & footer round their own corners explicitly. */
615
- }
616
- .card-padded {
617
- @apply p-5;
618
622
  }
623
+ .card-padded { @apply p-5; }
619
624
  .card-header {
620
625
  @apply px-5 pt-5 pb-3;
621
626
  border-bottom: 1px solid var(--color-border);
622
627
  border-top-left-radius: var(--ui-radius-lg);
623
628
  border-top-right-radius: var(--ui-radius-lg);
624
629
  }
625
- .card-body {
626
- @apply p-5;
627
- }
630
+ .card-body { @apply p-5; }
628
631
  .card-footer {
629
632
  @apply px-5 py-3;
630
633
  border-top: 1px solid var(--color-border);
@@ -635,16 +638,18 @@
635
638
  .card-interactive {
636
639
  @apply cursor-pointer no-underline;
637
640
  color: inherit;
638
- transition: border-color 150ms ease-out, transform 150ms ease-out, box-shadow 150ms ease-out;
641
+ transition: border-color var(--ui-motion-fast) var(--ui-motion-ease),
642
+ transform var(--ui-motion-fast) var(--ui-motion-ease),
643
+ box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
639
644
  }
640
645
  .card-interactive:hover {
641
646
  border-color: var(--color-border-strong);
642
647
  transform: translateY(-2px);
643
- box-shadow: 0 12px 24px -8px oklch(0% 0 0 / 0.10);
648
+ box-shadow: var(--ui-elev-2);
644
649
  }
645
650
  .card-interactive:focus-visible {
646
- outline: 2px solid var(--color-ring);
647
- outline-offset: 2px;
651
+ outline: none;
652
+ box-shadow: var(--ui-focus-ring);
648
653
  }
649
654
 
650
655
  /* ============================================================ */
@@ -654,17 +659,11 @@
654
659
  @apply flex items-start gap-3 p-3 text-sm;
655
660
  border: 1px solid;
656
661
  border-radius: var(--ui-radius);
657
- animation: norns-banner-in 250ms cubic-bezier(0.21, 1.02, 0.73, 1) both;
662
+ animation: norns-banner-in var(--ui-motion-med) var(--ui-motion-ease) both;
658
663
  }
659
664
  @keyframes norns-banner-in {
660
- from {
661
- opacity: 0;
662
- transform: translateY(-4px);
663
- }
664
- to {
665
- opacity: 1;
666
- transform: translateY(0);
667
- }
665
+ from { opacity: 0; transform: translateY(-4px); }
666
+ to { opacity: 1; transform: translateY(0); }
668
667
  }
669
668
  .banner-info {
670
669
  background-color: var(--color-info-bg);
@@ -686,18 +685,12 @@
686
685
  color: var(--color-danger-fg);
687
686
  border-color: var(--color-danger-border);
688
687
  }
689
- .banner-icon {
690
- @apply shrink-0 mt-0.5;
691
- }
692
- .banner-body {
693
- @apply flex-1;
694
- }
695
- .banner-actions {
696
- @apply shrink-0 flex gap-2 ml-auto;
697
- }
688
+ .banner-icon { @apply shrink-0 mt-0.5; }
689
+ .banner-body { @apply flex-1; }
690
+ .banner-actions { @apply shrink-0 flex gap-2 ml-auto; }
698
691
 
699
692
  /* ============================================================ */
700
- /* Badge */
693
+ /* Badge / Chip */
701
694
  /* ============================================================ */
702
695
  .badge {
703
696
  @apply inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium leading-none;
@@ -713,29 +706,12 @@
713
706
  background-color: var(--color-primary-950);
714
707
  color: var(--color-primary-200);
715
708
  }
716
- .badge-success {
717
- background-color: var(--color-success-bg);
718
- color: var(--color-success-fg);
719
- }
720
- .badge-warning {
721
- background-color: var(--color-warning-bg);
722
- color: var(--color-warning-fg);
723
- }
724
- .badge-danger {
725
- background-color: var(--color-danger-bg);
726
- color: var(--color-danger-fg);
727
- }
728
- .badge-info {
729
- background-color: var(--color-info-bg);
730
- color: var(--color-info-fg);
731
- }
732
- .badge-sm {
733
- @apply text-[10px] px-1.5;
734
- }
709
+ .badge-success { background-color: var(--color-success-bg); color: var(--color-success-fg); }
710
+ .badge-warning { background-color: var(--color-warning-bg); color: var(--color-warning-fg); }
711
+ .badge-danger { background-color: var(--color-danger-bg); color: var(--color-danger-fg); }
712
+ .badge-info { background-color: var(--color-info-bg); color: var(--color-info-fg); }
713
+ .badge-sm { @apply text-[10px] px-1.5; }
735
714
 
736
- /* ============================================================ */
737
- /* Chip */
738
- /* ============================================================ */
739
715
  .chip {
740
716
  @apply inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium;
741
717
  background-color: var(--color-bg-muted);
@@ -746,14 +722,16 @@
746
722
  .chip-remove {
747
723
  @apply -mr-1 inline-flex size-4 items-center justify-center rounded-full;
748
724
  opacity: 0.6;
725
+ transition: opacity var(--ui-motion-fast) var(--ui-motion-ease),
726
+ background-color var(--ui-motion-fast) var(--ui-motion-ease);
749
727
  }
750
728
  .chip-remove:hover {
751
729
  opacity: 1;
752
- background-color: var(--color-border);
730
+ background-color: var(--color-bg-muted);
753
731
  }
754
732
 
755
733
  /* ============================================================ */
756
- /* Avatar */
734
+ /* Avatar */
757
735
  /* ============================================================ */
758
736
  .avatar {
759
737
  @apply relative inline-flex shrink-0 items-center justify-center overflow-hidden font-medium;
@@ -761,48 +739,35 @@
761
739
  color: var(--color-fg);
762
740
  border-radius: var(--ui-radius-full);
763
741
  }
764
- .avatar-sm {
765
- @apply size-6 text-xs;
766
- }
767
- .avatar-md {
768
- @apply size-8 text-sm;
769
- }
770
- .avatar-lg {
771
- @apply size-12 text-base;
772
- }
773
- .avatar-xl {
774
- @apply size-16 text-lg;
775
- }
776
- .avatar img {
777
- @apply h-full w-full object-cover;
778
- }
742
+ .avatar-sm { @apply size-6 text-xs; }
743
+ .avatar-md { @apply size-8 text-sm; }
744
+ .avatar-lg { @apply size-12 text-base; }
745
+ .avatar-xl { @apply size-16 text-lg; }
746
+ .avatar img { @apply h-full w-full object-cover; }
779
747
 
780
- /* AvatarGroup */
781
- .avatar-group {
782
- @apply inline-flex items-center;
783
- }
748
+ .avatar-group { @apply inline-flex items-center; }
784
749
  .avatar-group-item,
785
750
  .avatar-group-overflow {
786
- @apply relative inline-flex transition-transform duration-150 ease-out;
751
+ @apply relative inline-flex;
787
752
  margin-left: -0.5rem;
788
753
  box-shadow: 0 0 0 2px var(--color-bg);
789
754
  border-radius: var(--ui-radius-full);
755
+ transition: transform var(--ui-motion-fast) var(--ui-motion-ease);
790
756
  }
791
757
  .avatar-group-item:first-child,
792
758
  .avatar-group:has(.avatar-group-item:first-child) > .avatar-group-item:first-child {
793
759
  margin-left: 0;
794
760
  }
795
- .avatar-group-item:hover {
796
- transform: translateY(-2px);
797
- z-index: 1;
798
- }
761
+ .avatar-group-item:hover { transform: translateY(-2px); z-index: 1; }
799
762
  .avatar-group-overflow {
800
763
  @apply text-xs font-medium;
801
764
  background-color: var(--color-bg-muted);
802
765
  color: var(--color-fg-muted);
803
766
  }
804
767
 
805
- /* GradientText */
768
+ /* ============================================================ */
769
+ /* Decorative buttons (CSS-only motion) */
770
+ /* ============================================================ */
806
771
  .gradient-text {
807
772
  background-clip: text;
808
773
  -webkit-background-clip: text;
@@ -810,29 +775,20 @@
810
775
  background-size: 200% 200%;
811
776
  background-position: 0% 50%;
812
777
  }
813
- .gradient-text-anim {
814
- animation: norns-gradient-shift 6s ease-in-out infinite;
815
- }
778
+ .gradient-text-anim { animation: norns-gradient-shift 6s ease-in-out infinite; }
816
779
  @keyframes norns-gradient-shift {
817
780
  0%, 100% { background-position: 0% 50%; }
818
781
  50% { background-position: 100% 50%; }
819
782
  }
820
783
 
821
- /* CopyButton */
822
- .copy-btn-check {
823
- animation: norns-copy-pop 250ms ease-out;
824
- }
784
+ .copy-btn-check { animation: norns-copy-pop 250ms ease-out; }
825
785
  @keyframes norns-copy-pop {
826
786
  0% { transform: scale(0.5); opacity: 0; }
827
787
  70% { transform: scale(1.15); }
828
788
  100% { transform: scale(1); opacity: 1; }
829
789
  }
830
790
 
831
- /* ShinyButton sweeping shine overlay */
832
- .shiny-btn {
833
- position: relative;
834
- isolation: isolate;
835
- }
791
+ .shiny-btn { position: relative; isolation: isolate; }
836
792
  .shiny-btn-shine {
837
793
  position: absolute;
838
794
  inset: 0;
@@ -854,33 +810,21 @@
854
810
  100% { background-position: -150% 0; }
855
811
  }
856
812
 
857
- /* RippleButton */
858
- .ripple-btn {
859
- position: relative;
860
- overflow: hidden;
861
- }
813
+ .ripple-btn { position: relative; overflow: hidden; }
862
814
  .ripple-btn-ripple {
863
815
  position: absolute;
864
816
  pointer-events: none;
865
- border-radius: 9999px;
817
+ border-radius: var(--ui-radius-full);
866
818
  background-color: color-mix(in oklab, currentColor 35%, transparent);
867
819
  transform: scale(0);
868
820
  opacity: 0.6;
869
821
  animation: norns-ripple 600ms ease-out forwards;
870
822
  }
871
823
  @keyframes norns-ripple {
872
- to {
873
- transform: scale(1);
874
- opacity: 0;
875
- }
824
+ to { transform: scale(1); opacity: 0; }
876
825
  }
877
826
 
878
- /* LiquidButton (motion subpath) */
879
- .liquid-btn {
880
- position: relative;
881
- isolation: isolate;
882
- overflow: hidden;
883
- }
827
+ .liquid-btn { position: relative; isolation: isolate; overflow: hidden; }
884
828
  .liquid-btn-blob {
885
829
  position: absolute;
886
830
  inset: -25%;
@@ -900,17 +844,12 @@
900
844
  z-index: 1;
901
845
  }
902
846
 
903
- /* Sparkles (motion subpath) */
904
- .sparkles-wrap {
905
- @apply relative inline-block;
906
- }
907
- .sparkles-overlay {
908
- @apply pointer-events-none absolute inset-0 overflow-hidden;
909
- }
847
+ .sparkles-wrap { @apply relative inline-block; }
848
+ .sparkles-overlay { @apply pointer-events-none absolute inset-0 overflow-hidden; }
910
849
  .sparkle {
911
850
  position: absolute;
912
851
  display: block;
913
- border-radius: 9999px;
852
+ border-radius: var(--ui-radius-full);
914
853
  background: var(--sparkle-color, var(--color-primary-400));
915
854
  opacity: 0;
916
855
  transform: scale(0);
@@ -918,16 +857,13 @@
918
857
  animation-fill-mode: forwards;
919
858
  filter: drop-shadow(0 0 4px var(--sparkle-color, var(--color-primary-400)));
920
859
  }
921
- .sparkles-paused .sparkle {
922
- animation-play-state: paused;
923
- }
860
+ .sparkles-paused .sparkle { animation-play-state: paused; }
924
861
  @keyframes norns-sparkle {
925
862
  0%, 100% { opacity: 0; transform: scale(0) rotate(0deg); }
926
863
  20%, 80% { opacity: 1; }
927
864
  50% { transform: scale(1) rotate(180deg); }
928
865
  }
929
866
 
930
- /* GradientBackground (motion subpath) */
931
867
  .gradient-bg {
932
868
  @apply relative overflow-hidden;
933
869
  background-image: linear-gradient(
@@ -945,7 +881,7 @@
945
881
  }
946
882
 
947
883
  /* ============================================================ */
948
- /* Skeleton */
884
+ /* Skeleton / Progress */
949
885
  /* ============================================================ */
950
886
  .skeleton {
951
887
  @apply block;
@@ -953,38 +889,26 @@
953
889
  border-radius: var(--ui-radius-sm);
954
890
  animation: norns-skeleton-pulse 1.6s ease-in-out infinite;
955
891
  }
956
- .skeleton-circle {
957
- border-radius: var(--ui-radius-full);
958
- }
892
+ .skeleton-circle { border-radius: var(--ui-radius-full); }
959
893
  @keyframes norns-skeleton-pulse {
960
894
  0%, 100% { opacity: 1; }
961
895
  50% { opacity: 0.5; }
962
896
  }
963
897
 
964
- /* ============================================================ */
965
- /* Progress */
966
- /* ============================================================ */
967
898
  .progress {
968
899
  @apply relative h-2 w-full overflow-hidden;
969
900
  background-color: var(--color-bg-muted);
970
901
  border-radius: var(--ui-radius-full);
971
902
  }
972
903
  .progress-bar {
973
- @apply h-full transition-[width] duration-300 ease-out;
904
+ @apply h-full;
974
905
  background-color: var(--color-primary-600);
975
906
  border-radius: var(--ui-radius-full);
907
+ transition: width var(--ui-motion-slow) var(--ui-motion-ease);
976
908
  }
977
- .progress-bar-success {
978
- background-color: var(--color-success-600);
979
- }
980
- .progress-bar-warning {
981
- background-color: var(--color-warning-600);
982
- }
983
- .progress-bar-danger {
984
- background-color: var(--color-danger-600);
985
- }
986
-
987
- /* Indeterminate (uses CSS keyframes only) */
909
+ .progress-bar-success { background-color: var(--color-success-600); }
910
+ .progress-bar-warning { background-color: var(--color-warning-600); }
911
+ .progress-bar-danger { background-color: var(--color-danger-600); }
988
912
  .progress-indeterminate .progress-bar {
989
913
  width: 35%;
990
914
  animation: norns-progress-indeterminate 1.4s ease-in-out infinite;
@@ -995,7 +919,7 @@
995
919
  }
996
920
 
997
921
  /* ============================================================ */
998
- /* Header (composite) */
922
+ /* Header / Hero / Stepper / Breadcrumbs / Pagination */
999
923
  /* ============================================================ */
1000
924
  .norns-header {
1001
925
  @apply flex items-center justify-between gap-4 px-6 py-3;
@@ -1011,25 +935,16 @@
1011
935
  @apply flex items-center gap-2 font-semibold;
1012
936
  color: var(--color-fg);
1013
937
  }
1014
- .norns-header-nav {
1015
- @apply flex items-center gap-1;
1016
- }
1017
- .norns-header-actions {
1018
- @apply flex items-center gap-2;
1019
- }
938
+ .norns-header-nav { @apply flex items-center gap-1; }
939
+ .norns-header-actions { @apply flex items-center gap-2; }
1020
940
 
1021
- /* ============================================================ */
1022
- /* HeroBanner (composite) */
1023
- /* ============================================================ */
1024
941
  .hero {
1025
942
  @apply relative overflow-hidden p-8 md:p-12;
1026
943
  background-color: var(--color-bg-elevated);
1027
944
  border: 1px solid var(--color-border);
1028
945
  border-radius: var(--ui-radius-lg);
1029
946
  }
1030
- .hero-center {
1031
- @apply text-center;
1032
- }
947
+ .hero-center { @apply text-center; }
1033
948
  .hero-title {
1034
949
  @apply text-3xl font-bold tracking-tight md:text-4xl;
1035
950
  color: var(--color-fg);
@@ -1038,41 +953,26 @@
1038
953
  @apply mt-3 text-base md:text-lg max-w-prose;
1039
954
  color: var(--color-fg-muted);
1040
955
  }
1041
- .hero-center .hero-description {
1042
- @apply mx-auto;
1043
- }
1044
- .hero-actions {
1045
- @apply mt-6 flex flex-wrap items-center gap-3;
1046
- }
1047
- .hero-center .hero-actions {
1048
- @apply justify-center;
1049
- }
956
+ .hero-center .hero-description { @apply mx-auto; }
957
+ .hero-actions { @apply mt-6 flex flex-wrap items-center gap-3; }
958
+ .hero-center .hero-actions { @apply justify-center; }
1050
959
  .hero-image {
1051
960
  @apply pointer-events-none absolute inset-y-0 right-0 w-1/2 object-cover opacity-20;
1052
961
  }
1053
962
 
1054
- /* ============================================================ */
1055
- /* Stepper */
1056
- /* ============================================================ */
1057
- .stepper {
1058
- @apply flex items-center gap-3;
1059
- }
1060
- .stepper-vertical {
1061
- @apply flex-col items-stretch;
1062
- }
963
+ .stepper { @apply flex items-center gap-3; }
964
+ .stepper-vertical { @apply flex-col items-stretch; }
1063
965
  .stepper-step {
1064
966
  @apply flex items-center gap-2 text-sm;
1065
967
  color: var(--color-fg-muted);
1066
968
  }
1067
- .stepper-step-current {
1068
- color: var(--color-fg);
1069
- font-weight: 600;
1070
- }
1071
- .stepper-step-complete {
1072
- color: var(--color-fg);
1073
- }
969
+ .stepper-step-current { color: var(--color-fg); font-weight: 600; }
970
+ .stepper-step-complete { color: var(--color-fg); }
1074
971
  .stepper-marker {
1075
- @apply inline-flex size-7 shrink-0 items-center justify-center rounded-full text-xs font-semibold;
972
+ @apply inline-flex shrink-0 items-center justify-center text-xs font-semibold;
973
+ width: var(--ui-icon-btn-md);
974
+ height: var(--ui-icon-btn-md);
975
+ border-radius: var(--ui-radius-full);
1076
976
  background-color: var(--color-bg-muted);
1077
977
  color: var(--color-fg-muted);
1078
978
  border: 1px solid var(--color-border);
@@ -1087,145 +987,126 @@
1087
987
  color: white;
1088
988
  border-color: var(--color-success-600);
1089
989
  }
1090
- .stepper-divider {
1091
- @apply h-px flex-1;
1092
- background-color: var(--color-border);
1093
- }
1094
- .stepper-vertical .stepper-divider {
1095
- @apply h-6 w-px ml-3;
1096
- }
990
+ .stepper-divider { @apply h-px flex-1; background-color: var(--color-border); }
991
+ .stepper-vertical .stepper-divider { @apply h-6 w-px ml-3; }
1097
992
 
1098
- /* ============================================================ */
1099
- /* Breadcrumbs */
1100
- /* ============================================================ */
1101
993
  .breadcrumbs {
1102
994
  @apply flex items-center gap-1 text-sm;
1103
995
  color: var(--color-fg-muted);
1104
996
  }
1105
- .breadcrumb-item {
1106
- @apply inline-flex items-center gap-1;
1107
- }
1108
- .breadcrumb-link {
1109
- color: var(--color-fg-muted);
1110
- }
1111
- .breadcrumb-link:hover {
1112
- color: var(--color-fg);
1113
- }
1114
- .breadcrumb-current {
1115
- color: var(--color-fg);
1116
- font-weight: 500;
1117
- }
997
+ .breadcrumb-item { @apply inline-flex items-center gap-1; }
998
+ .breadcrumb-link { color: var(--color-fg-muted); }
999
+ .breadcrumb-link:hover { color: var(--color-fg); }
1000
+ .breadcrumb-current { color: var(--color-fg); font-weight: 500; }
1118
1001
  .breadcrumb-separator {
1119
1002
  @apply mx-1 select-none;
1120
1003
  color: var(--color-fg-subtle);
1121
1004
  }
1122
1005
 
1123
- /* ============================================================ */
1124
- /* Pagination */
1125
- /* ============================================================ */
1126
- .pagination {
1127
- @apply inline-flex items-center gap-1;
1128
- }
1006
+ .pagination { @apply inline-flex items-center gap-1; }
1129
1007
  .pagination-item {
1130
- @apply inline-flex size-8 items-center justify-center text-sm;
1131
- color: var(--color-fg-muted);
1132
- border-radius: var(--ui-radius-sm);
1133
- }
1134
- .pagination-item:hover {
1135
- background-color: var(--color-bg-muted);
1136
- color: var(--color-fg);
1137
- }
1138
- .pagination-item-active {
1139
- background-color: var(--color-primary-600);
1140
- color: white;
1008
+ /* base + hover come from `.btn-icon` selector list above. The pagination
1009
+ * variant is full-md icon-button size (8x8 in original), so override
1010
+ * the small default. */
1011
+ width: var(--ui-icon-btn-md);
1012
+ height: var(--ui-icon-btn-md);
1141
1013
  }
1014
+ .pagination-item-active,
1142
1015
  .pagination-item-active:hover {
1143
- background-color: var(--color-primary-700);
1016
+ background-color: var(--color-primary-600);
1144
1017
  color: white;
1145
1018
  }
1146
1019
  .pagination-item:disabled {
1147
1020
  @apply cursor-not-allowed opacity-40;
1148
1021
  }
1149
1022
  .pagination-ellipsis {
1150
- @apply inline-flex size-8 items-center justify-center text-sm select-none;
1023
+ @apply inline-flex items-center justify-center text-sm select-none;
1024
+ width: var(--ui-icon-btn-md);
1025
+ height: var(--ui-icon-btn-md);
1151
1026
  color: var(--color-fg-subtle);
1152
1027
  }
1153
1028
 
1154
1029
  /* ============================================================ */
1155
- /* Accordion */
1030
+ /* Accordion / Collapsible — both built on <details> */
1156
1031
  /* ============================================================ */
1157
1032
  .accordion {
1158
- @apply divide-y;
1159
1033
  border: 1px solid var(--color-border);
1160
1034
  border-radius: var(--ui-radius);
1161
1035
  }
1162
- .accordion :where(.divide-y) > * {
1163
- border-color: var(--color-border);
1164
- }
1165
- .accordion-item {
1166
- border-bottom: 1px solid var(--color-border);
1167
- }
1168
- .accordion-item:last-child {
1169
- border-bottom: none;
1170
- }
1036
+ .accordion-item { border-bottom: 1px solid var(--color-border); }
1037
+ .accordion-item:last-child { border-bottom: none; }
1171
1038
  .accordion-trigger {
1172
- @apply flex w-full items-center justify-between gap-2 px-4 py-3 text-left text-sm font-medium;
1173
- @apply transition-colors focus-visible:outline-none focus-visible:ring-2;
1039
+ @apply flex w-full items-center justify-between gap-2 px-4 py-3 text-left text-sm font-medium cursor-pointer;
1174
1040
  color: var(--color-fg);
1175
- background-color: transparent;
1176
- --tw-ring-color: var(--color-ring);
1177
- }
1178
- .accordion-trigger:hover {
1179
- background-color: var(--color-bg-muted);
1041
+ background: transparent;
1042
+ list-style: none;
1043
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
1180
1044
  }
1181
- .accordion-trigger[data-state='open'] .accordion-chevron {
1045
+ .accordion-trigger::-webkit-details-marker { display: none; }
1046
+ .accordion-trigger:hover { background-color: var(--color-bg-muted); }
1047
+ .accordion-trigger:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
1048
+ .accordion-item[open] > .accordion-trigger > .accordion-chevron {
1182
1049
  transform: rotate(180deg);
1183
1050
  }
1184
1051
  .accordion-chevron {
1185
- @apply size-4 shrink-0 transition-transform duration-150;
1052
+ @apply size-4 shrink-0;
1186
1053
  color: var(--color-fg-muted);
1054
+ transition: transform var(--ui-motion-fast) var(--ui-motion-ease);
1187
1055
  }
1188
1056
  .accordion-content {
1189
1057
  @apply px-4 pb-3 text-sm;
1190
1058
  color: var(--color-fg-muted);
1191
1059
  }
1192
1060
 
1193
- /* ============================================================ */
1194
- /* Separator */
1195
- /* ============================================================ */
1196
- .separator {
1197
- background-color: var(--color-border);
1061
+ .collapsible {
1062
+ border: 1px solid var(--color-border);
1063
+ border-radius: var(--ui-radius);
1064
+ background-color: var(--color-bg-elevated);
1198
1065
  }
1199
- .separator-horizontal {
1200
- @apply h-px w-full;
1066
+ .collapsible-trigger {
1067
+ @apply flex w-full items-center gap-2 px-3 py-2 text-left text-sm font-medium cursor-pointer;
1068
+ color: var(--color-fg);
1069
+ background: transparent;
1070
+ list-style: none;
1071
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
1201
1072
  }
1202
- .separator-vertical {
1203
- @apply w-px h-full self-stretch;
1073
+ .collapsible-trigger::-webkit-details-marker { display: none; }
1074
+ .collapsible-trigger:hover { background-color: var(--color-bg-muted); }
1075
+ .collapsible-trigger:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
1076
+ .collapsible[open] > .collapsible-trigger > .collapsible-chevron {
1077
+ transform: rotate(90deg);
1078
+ }
1079
+ .collapsible-chevron {
1080
+ @apply size-4 shrink-0;
1081
+ color: var(--color-fg-muted);
1082
+ transition: transform var(--ui-motion-fast) var(--ui-motion-ease);
1083
+ }
1084
+ .collapsible-title { @apply flex-1; }
1085
+ .collapsible-content {
1086
+ @apply px-3 pb-3 pt-1 text-sm;
1087
+ color: var(--color-fg-muted);
1204
1088
  }
1205
1089
 
1206
1090
  /* ============================================================ */
1207
- /* Toolbar */
1091
+ /* Separator / Toolbar */
1208
1092
  /* ============================================================ */
1093
+ .separator { background-color: var(--color-border); }
1094
+ .separator-horizontal { @apply h-px w-full; }
1095
+ .separator-vertical { @apply w-px h-full self-stretch; }
1096
+
1209
1097
  .toolbar {
1210
1098
  @apply flex items-center gap-1 p-1;
1211
1099
  background-color: var(--color-bg-muted);
1212
1100
  border: 1px solid var(--color-border);
1213
1101
  border-radius: var(--ui-radius);
1214
1102
  }
1215
- .toolbar-vertical {
1216
- @apply flex-col items-stretch;
1217
- }
1103
+ .toolbar-vertical { @apply flex-col items-stretch; }
1218
1104
 
1219
1105
  /* ============================================================ */
1220
- /* ButtonGroup */
1106
+ /* ButtonGroup — covers `.btn`, `.btn-secondary[data-pressed]` */
1221
1107
  /* ============================================================ */
1222
- .btn-group {
1223
- @apply inline-flex;
1224
- }
1225
- .btn-group > .btn,
1226
- .btn-group > .toggle-btn {
1227
- border-radius: 0;
1228
- }
1108
+ .btn-group { @apply inline-flex; }
1109
+ .btn-group > * { border-radius: 0; }
1229
1110
  .btn-group > :first-child {
1230
1111
  border-top-left-radius: var(--ui-radius);
1231
1112
  border-bottom-left-radius: var(--ui-radius);
@@ -1234,95 +1115,19 @@
1234
1115
  border-top-right-radius: var(--ui-radius);
1235
1116
  border-bottom-right-radius: var(--ui-radius);
1236
1117
  }
1237
- .btn-group > * + * {
1238
- margin-left: -1px;
1239
- }
1240
- .btn-group-vertical {
1241
- @apply flex-col;
1242
- }
1243
- .btn-group-vertical > * {
1244
- width: 100%;
1245
- }
1118
+ .btn-group > * + * { margin-left: -1px; }
1119
+
1120
+ .btn-group-vertical { @apply flex-col; }
1121
+ .btn-group-vertical > * { width: 100%; border-radius: 0; }
1246
1122
  .btn-group-vertical > :first-child {
1247
- border-radius: 0;
1248
1123
  border-top-left-radius: var(--ui-radius);
1249
1124
  border-top-right-radius: var(--ui-radius);
1250
1125
  }
1251
1126
  .btn-group-vertical > :last-child {
1252
- border-radius: 0;
1253
1127
  border-bottom-left-radius: var(--ui-radius);
1254
1128
  border-bottom-right-radius: var(--ui-radius);
1255
1129
  }
1256
- .btn-group-vertical > * + * {
1257
- margin-left: 0;
1258
- margin-top: -1px;
1259
- }
1260
-
1261
- /* ============================================================ */
1262
- /* ToggleButton */
1263
- /* ============================================================ */
1264
- .toggle-btn {
1265
- @apply inline-flex items-center justify-center gap-1.5 text-sm font-medium;
1266
- @apply transition-colors focus-visible:outline-none focus-visible:ring-2;
1267
- @apply disabled:cursor-not-allowed disabled:opacity-60;
1268
- height: var(--ui-h-md);
1269
- padding-inline: 0.625rem;
1270
- background-color: transparent;
1271
- color: var(--color-fg-muted);
1272
- border: 1px solid var(--color-border);
1273
- border-radius: var(--ui-radius);
1274
- --tw-ring-color: var(--color-ring);
1275
- }
1276
- .toggle-btn:hover {
1277
- background-color: var(--color-bg-muted);
1278
- color: var(--color-fg);
1279
- }
1280
- .toggle-btn[data-state='on'] {
1281
- background-color: var(--color-bg-muted);
1282
- color: var(--color-fg);
1283
- border-color: var(--color-border-strong);
1284
- }
1285
- .toggle-btn-sm {
1286
- height: var(--ui-h-sm);
1287
- @apply text-xs px-2;
1288
- }
1289
- .toggle-btn-lg {
1290
- height: var(--ui-h-lg);
1291
- @apply text-base px-3;
1292
- }
1293
-
1294
- /* ============================================================ */
1295
- /* Collapsible */
1296
- /* ============================================================ */
1297
- .collapsible {
1298
- border: 1px solid var(--color-border);
1299
- border-radius: var(--ui-radius);
1300
- background-color: var(--color-bg-elevated);
1301
- }
1302
- .collapsible-trigger {
1303
- @apply flex w-full items-center gap-2 px-3 py-2 text-left text-sm font-medium;
1304
- @apply transition-colors focus-visible:outline-none focus-visible:ring-2;
1305
- color: var(--color-fg);
1306
- background-color: transparent;
1307
- --tw-ring-color: var(--color-ring);
1308
- }
1309
- .collapsible-trigger:hover {
1310
- background-color: var(--color-bg-muted);
1311
- }
1312
- .collapsible-chevron {
1313
- @apply size-4 shrink-0 transition-transform duration-150;
1314
- color: var(--color-fg-muted);
1315
- }
1316
- .collapsible-chevron-open {
1317
- transform: rotate(90deg);
1318
- }
1319
- .collapsible-title {
1320
- @apply flex-1;
1321
- }
1322
- .collapsible-content {
1323
- @apply px-3 pb-3 pt-1 text-sm;
1324
- color: var(--color-fg-muted);
1325
- }
1130
+ .btn-group-vertical > * + * { margin-left: 0; margin-top: -1px; }
1326
1131
 
1327
1132
  /* ============================================================ */
1328
1133
  /* NumberInput */
@@ -1335,15 +1140,22 @@
1335
1140
  @apply text-center;
1336
1141
  border-radius: 0;
1337
1142
  min-width: 4rem;
1143
+ -moz-appearance: textfield;
1144
+ }
1145
+ .number-input-field::-webkit-outer-spin-button,
1146
+ .number-input-field::-webkit-inner-spin-button {
1147
+ -webkit-appearance: none;
1148
+ margin: 0;
1338
1149
  }
1339
1150
  .number-step {
1340
- @apply inline-flex items-center justify-center px-2;
1151
+ @apply inline-flex items-center justify-center px-2 cursor-pointer;
1341
1152
  background-color: var(--color-bg-muted);
1342
1153
  color: var(--color-fg);
1343
1154
  border: 1px solid var(--color-border-strong);
1344
- --tw-ring-color: var(--color-ring);
1345
1155
  height: var(--ui-h-md);
1156
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
1346
1157
  }
1158
+ .number-step:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
1347
1159
  .number-step:first-child {
1348
1160
  border-top-left-radius: var(--ui-radius);
1349
1161
  border-bottom-left-radius: var(--ui-radius);
@@ -1354,44 +1166,28 @@
1354
1166
  border-bottom-right-radius: var(--ui-radius);
1355
1167
  border-left: none;
1356
1168
  }
1357
- .number-step:hover:not(:disabled) {
1358
- background-color: var(--color-bg-subtle);
1359
- }
1360
- .number-step:disabled {
1361
- @apply cursor-not-allowed opacity-40;
1362
- }
1363
- .number-input-field {
1364
- border-radius: 0;
1365
- }
1366
- /* Hide native spinners since we have our own buttons */
1367
- .number-input-field::-webkit-outer-spin-button,
1368
- .number-input-field::-webkit-inner-spin-button {
1369
- -webkit-appearance: none;
1370
- margin: 0;
1371
- }
1372
- .number-input-field {
1373
- -moz-appearance: textfield;
1374
- }
1169
+ .number-step:hover:not(:disabled) { background-color: var(--color-bg-subtle); }
1170
+ .number-step:disabled { @apply cursor-not-allowed opacity-40; }
1375
1171
 
1376
1172
  /* ============================================================ */
1377
1173
  /* OtpField */
1378
1174
  /* ============================================================ */
1379
- .otp-field {
1380
- @apply inline-flex gap-2;
1381
- }
1175
+ .otp-field { @apply inline-flex gap-2; }
1382
1176
  .otp-cell {
1383
1177
  @apply text-center font-mono text-lg;
1384
- @apply focus:outline-none focus:ring-2;
1385
1178
  width: 2.75rem;
1386
1179
  height: 3rem;
1387
1180
  background-color: var(--color-bg);
1388
1181
  color: var(--color-fg);
1389
1182
  border: 1px solid var(--color-border-strong);
1390
1183
  border-radius: var(--ui-radius);
1391
- --tw-ring-color: color-mix(in oklab, var(--color-ring) 30%, transparent);
1184
+ transition: border-color var(--ui-motion-fast) var(--ui-motion-ease),
1185
+ box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
1392
1186
  }
1393
- .otp-cell:focus {
1187
+ .otp-cell:focus-visible {
1188
+ outline: none;
1394
1189
  border-color: var(--color-ring);
1190
+ box-shadow: var(--ui-focus-ring);
1395
1191
  }
1396
1192
 
1397
1193
  /* ============================================================ */
@@ -1399,44 +1195,40 @@
1399
1195
  /* ============================================================ */
1400
1196
  .tags-input {
1401
1197
  @apply flex w-full flex-wrap items-center gap-1.5 px-2 py-1.5;
1402
- @apply transition-colors focus-within:ring-2;
1403
1198
  min-height: var(--ui-h-md);
1404
1199
  background-color: var(--color-bg);
1405
1200
  color: var(--color-fg);
1406
1201
  border: 1px solid var(--color-border-strong);
1407
1202
  border-radius: var(--ui-radius);
1408
- --tw-ring-color: color-mix(in oklab, var(--color-ring) 30%, transparent);
1203
+ transition: border-color var(--ui-motion-fast) var(--ui-motion-ease),
1204
+ box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
1409
1205
  }
1410
1206
  .tags-input:focus-within {
1207
+ outline: none;
1411
1208
  border-color: var(--color-ring);
1209
+ box-shadow: var(--ui-focus-ring);
1412
1210
  }
1413
- .tags-input-err {
1414
- border-color: var(--color-danger-500);
1415
- }
1211
+ .tags-input-err { border-color: var(--color-danger-500); }
1416
1212
  .tags-input-field {
1417
1213
  @apply min-w-[6rem] flex-1 text-sm outline-none;
1418
1214
  background: transparent;
1419
1215
  color: var(--color-fg);
1420
1216
  }
1421
- .tags-input-field::placeholder {
1422
- color: var(--color-fg-subtle);
1423
- }
1217
+ .tags-input-field::placeholder { color: var(--color-fg-subtle); }
1424
1218
 
1425
1219
  /* ============================================================ */
1426
1220
  /* Timeline */
1427
1221
  /* ============================================================ */
1428
- .timeline {
1429
- @apply list-none p-0 m-0 relative;
1430
- }
1222
+ .timeline { @apply list-none p-0 m-0 relative; }
1431
1223
  .timeline-item {
1432
1224
  @apply relative grid pl-10 pb-6;
1433
1225
  grid-template-columns: 1fr;
1434
1226
  }
1435
- .timeline-item:last-child {
1436
- @apply pb-0;
1437
- }
1227
+ .timeline-item:last-child { @apply pb-0; }
1438
1228
  .timeline-dot {
1439
- @apply absolute left-0 inline-flex size-7 items-center justify-center rounded-full text-white;
1229
+ @apply absolute left-0 inline-flex items-center justify-center rounded-full text-white;
1230
+ width: var(--ui-icon-btn-md);
1231
+ height: var(--ui-icon-btn-md);
1440
1232
  background-color: var(--color-primary-600);
1441
1233
  top: 0;
1442
1234
  }
@@ -1451,85 +1243,94 @@
1451
1243
  bottom: -0.5rem;
1452
1244
  background-color: var(--color-border);
1453
1245
  }
1454
- .timeline-time {
1455
- @apply text-xs;
1456
- color: var(--color-fg-subtle);
1457
- }
1458
- .timeline-title {
1459
- @apply text-sm font-semibold;
1460
- color: var(--color-fg);
1461
- }
1462
- .timeline-description {
1463
- @apply text-sm mt-0.5;
1464
- color: var(--color-fg-muted);
1465
- }
1246
+ .timeline-time { @apply text-xs; color: var(--color-fg-subtle); }
1247
+ .timeline-title { @apply text-sm font-semibold; color: var(--color-fg); }
1248
+ .timeline-description { @apply text-sm mt-0.5; color: var(--color-fg-muted); }
1466
1249
 
1467
1250
  /* ============================================================ */
1468
1251
  /* Combobox / Autocomplete / MultiSelect */
1469
- /* ============================================================ */
1470
- .combobox-input-wrap {
1471
- @apply relative inline-flex w-full items-stretch;
1252
+ /* (Listbox styling — uses dropdown-content + dropdown-item) */
1253
+ /* ============================================================ */
1254
+ .combobox-input-wrap { @apply relative inline-flex w-full items-stretch; }
1255
+ .combobox-trigger { @apply absolute right-1 top-1/2 -translate-y-1/2; }
1256
+ /* Combobox-listbox is rendered with class="dropdown-content combobox-listbox"
1257
+ * so it inherits the elevated surface; this rule adds size constraints. */
1258
+ .combobox-listbox {
1259
+ max-height: 16rem;
1260
+ overflow-y: auto;
1261
+ min-width: 12rem;
1472
1262
  }
1473
- .combobox-input {
1474
- @apply pr-9;
1263
+ .combobox-empty {
1264
+ @apply px-2 py-1.5 text-sm;
1265
+ color: var(--color-fg-muted);
1475
1266
  }
1476
- .combobox-trigger {
1477
- @apply absolute right-1 top-1/2 -translate-y-1/2 inline-flex size-7 items-center justify-center rounded;
1267
+
1268
+ /* MultiSelect uses `.tags-input` as the wrapper so selected chips
1269
+ * render inline with the typed input. The `.multi-select` modifier
1270
+ * makes the wrapper a click target that focuses the inner input. */
1271
+ .multi-select { cursor: text; }
1272
+ .multi-select-toggle {
1273
+ @apply inline-flex items-center justify-center;
1274
+ width: var(--ui-icon-btn-sm);
1275
+ height: var(--ui-icon-btn-sm);
1276
+ padding: 0;
1277
+ border-radius: var(--ui-radius-sm);
1478
1278
  color: var(--color-fg-muted);
1279
+ background: transparent;
1280
+ border: none;
1281
+ cursor: pointer;
1282
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease),
1283
+ color var(--ui-motion-fast) var(--ui-motion-ease);
1479
1284
  }
1480
- .combobox-trigger:hover {
1285
+ .multi-select-toggle:hover {
1481
1286
  background-color: var(--color-bg-muted);
1287
+ color: var(--color-fg);
1482
1288
  }
1289
+ .multi-select-toggle:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
1483
1290
 
1484
1291
  /* ============================================================ */
1485
- /* Navigation menu (Hierarchical / Mega) — CSS-only hover/focus */
1486
- /* No JS, no Bits-UI dep. Submenus open on hover OR keyboard */
1487
- /* focus inside the parent <li>. */
1292
+ /* Navigation menu (Hierarchical / Mega) — CSS-only hover/focus */
1488
1293
  /* ============================================================ */
1489
- .nav-menu {
1490
- @apply relative z-10;
1491
- }
1492
- .nav-menu-list {
1493
- @apply flex items-center gap-0 list-none p-0 m-0 flex-wrap;
1494
- }
1495
- .nav-menu-item {
1496
- @apply relative;
1497
- }
1294
+ .nav-menu { @apply relative z-10; }
1295
+ .nav-menu-list { @apply flex items-center gap-0 list-none p-0 m-0 flex-wrap; }
1296
+ .nav-menu-item { @apply relative; }
1498
1297
  .nav-menu-trigger {
1499
- @apply inline-flex items-center gap-1 px-3 text-sm font-medium rounded-md;
1500
- @apply transition-colors focus-visible:outline-none focus-visible:ring-2;
1501
- @apply cursor-pointer select-none;
1298
+ @apply inline-flex items-center gap-1 px-3 text-sm font-medium cursor-pointer select-none;
1502
1299
  height: var(--ui-h-md);
1503
1300
  color: var(--color-fg-muted);
1504
1301
  background-color: transparent;
1505
- --tw-ring-color: var(--color-ring);
1302
+ border-radius: var(--ui-radius-sm);
1506
1303
  text-decoration: none;
1507
1304
  border: none;
1508
1305
  font-family: inherit;
1306
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease),
1307
+ color var(--ui-motion-fast) var(--ui-motion-ease);
1509
1308
  }
1309
+ .nav-menu-trigger:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
1510
1310
  .nav-menu-trigger:hover {
1511
1311
  color: var(--color-fg);
1512
1312
  background-color: var(--color-bg-muted);
1513
1313
  }
1514
1314
  .nav-menu-chevron {
1515
- @apply transition-transform duration-200 shrink-0;
1315
+ @apply shrink-0;
1316
+ transition: transform var(--ui-motion-med) var(--ui-motion-ease);
1516
1317
  }
1517
1318
  .nav-menu-content {
1518
- @apply absolute top-full left-0 mt-1 z-50;
1519
- @apply opacity-0 invisible -translate-y-1 transition-[opacity,transform,visibility] duration-200 ease-out;
1520
- background-color: var(--color-bg-elevated);
1521
- border: 1px solid var(--color-border);
1522
- border-radius: var(--ui-radius);
1523
- box-shadow:
1524
- 0 10px 32px -8px oklch(0% 0 0 / 0.18),
1525
- 0 4px 8px -4px oklch(0% 0 0 / 0.06);
1319
+ @apply absolute top-full left-0 mt-1 z-50 invisible;
1320
+ /* base surface comes from `.surface-elevated` selector list */
1526
1321
  min-width: 14rem;
1527
1322
  padding: 0.25rem;
1323
+ opacity: 0;
1324
+ transform: translateY(-4px);
1325
+ transition: opacity var(--ui-motion-med) var(--ui-motion-ease),
1326
+ transform var(--ui-motion-med) var(--ui-motion-ease),
1327
+ visibility var(--ui-motion-med) var(--ui-motion-ease);
1528
1328
  }
1529
- /* Open on hover, keyboard focus inside, OR aria-expanded=true. */
1530
1329
  .nav-menu-item:hover > .nav-menu-content,
1531
1330
  .nav-menu-item:focus-within > .nav-menu-content {
1532
- @apply opacity-100 visible translate-y-0;
1331
+ @apply visible;
1332
+ opacity: 1;
1333
+ transform: translateY(0);
1533
1334
  }
1534
1335
  .nav-menu-item:hover > .nav-menu-trigger,
1535
1336
  .nav-menu-item:focus-within > .nav-menu-trigger {
@@ -1541,15 +1342,14 @@
1541
1342
  transform: rotate(180deg);
1542
1343
  }
1543
1344
  .nav-menu-link {
1544
- @apply flex items-start gap-2 px-3 py-2 text-sm rounded transition-colors;
1345
+ @apply flex items-start gap-2 px-3 py-2 text-sm;
1545
1346
  color: var(--color-fg);
1546
1347
  text-decoration: none;
1348
+ border-radius: var(--ui-radius-sm);
1349
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
1547
1350
  }
1548
- .nav-menu-link:hover {
1549
- background-color: var(--color-bg-muted);
1550
- }
1351
+ .nav-menu-link:hover { background-color: var(--color-bg-muted); }
1551
1352
 
1552
- /* MegaMenu — wider, multi-column, anchored to the row not the trigger. */
1553
1353
  .mega-menu-grid {
1554
1354
  @apply grid gap-6 p-5;
1555
1355
  min-width: 32rem;
@@ -1557,86 +1357,46 @@
1557
1357
  .mega-menu-grid.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1558
1358
  .mega-menu-grid.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
1559
1359
  .mega-menu-grid.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
1560
- .mega-menu-column {
1561
- @apply flex flex-col gap-1;
1562
- }
1360
+ .mega-menu-column { @apply flex flex-col gap-1; }
1563
1361
  .mega-menu-title {
1564
1362
  @apply text-[11px] uppercase tracking-wider px-3 mb-1 font-semibold;
1565
1363
  color: var(--color-fg-subtle);
1566
1364
  }
1567
1365
 
1568
- /* ============================================================ */
1569
- /* ScrollArea */
1570
- /* ============================================================ */
1571
- .scroll-area-viewport {
1572
- @apply h-full w-full rounded-[inherit];
1573
- }
1574
- .scroll-area-scrollbar {
1575
- @apply flex select-none touch-none p-0.5 transition-colors;
1576
- background-color: transparent;
1577
- }
1578
- .scroll-area-scrollbar[data-orientation='vertical'] {
1579
- width: 0.5rem;
1580
- }
1581
- .scroll-area-scrollbar[data-orientation='horizontal'] {
1582
- height: 0.5rem;
1583
- }
1584
- .scroll-area-thumb {
1585
- @apply relative flex-1 rounded-full;
1586
- background-color: var(--color-border-strong);
1587
- }
1588
- .scroll-area-thumb:hover {
1589
- background-color: var(--color-fg-subtle);
1590
- }
1591
-
1592
1366
  /* ============================================================ */
1593
1367
  /* ColorPicker */
1594
1368
  /* ============================================================ */
1595
- .color-picker {
1596
- @apply inline-flex items-center gap-2;
1597
- }
1369
+ .color-picker { @apply inline-flex items-center gap-2; }
1598
1370
  .color-picker-swatch {
1599
1371
  @apply inline-block size-9 cursor-pointer overflow-hidden;
1600
1372
  border: 1px solid var(--color-border-strong);
1601
1373
  border-radius: var(--ui-radius);
1602
1374
  }
1603
- .color-picker-text {
1604
- @apply font-mono;
1605
- width: 8.5rem;
1606
- }
1375
+ .color-picker-text { @apply font-mono; width: 8.5rem; }
1607
1376
 
1608
1377
  /* ============================================================ */
1609
1378
  /* Uploader */
1610
1379
  /* ============================================================ */
1611
1380
  .uploader-root {
1612
1381
  @apply flex w-full flex-col items-center justify-center gap-2 p-6 text-center cursor-pointer;
1613
- @apply transition-colors focus-visible:outline-none focus-visible:ring-2;
1614
1382
  background-color: var(--color-bg-subtle);
1615
1383
  color: var(--color-fg);
1616
1384
  border: 2px dashed var(--color-border-strong);
1617
1385
  border-radius: var(--ui-radius);
1618
- --tw-ring-color: var(--color-ring);
1619
- }
1620
- .uploader-root:hover {
1621
- background-color: var(--color-bg-muted);
1386
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease),
1387
+ border-color var(--ui-motion-fast) var(--ui-motion-ease),
1388
+ box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
1622
1389
  }
1390
+ .uploader-root:hover { background-color: var(--color-bg-muted); }
1391
+ .uploader-root:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
1623
1392
  .uploader-dragging {
1624
1393
  background-color: color-mix(in oklab, var(--color-primary-500) 6%, var(--color-bg-muted));
1625
1394
  border-color: var(--color-primary-500);
1626
1395
  }
1627
- .uploader-text {
1628
- @apply flex flex-col items-center gap-0.5;
1629
- }
1630
- .uploader-primary {
1631
- @apply text-sm font-medium;
1632
- }
1633
- .uploader-helper {
1634
- @apply text-xs;
1635
- color: var(--color-fg-muted);
1636
- }
1637
- .uploader-list {
1638
- @apply mt-2 flex flex-col gap-1 list-none p-0;
1639
- }
1396
+ .uploader-text { @apply flex flex-col items-center gap-0.5; }
1397
+ .uploader-primary { @apply text-sm font-medium; }
1398
+ .uploader-helper { @apply text-xs; color: var(--color-fg-muted); }
1399
+ .uploader-list { @apply mt-2 flex flex-col gap-1 list-none p-0; }
1640
1400
  .uploader-item {
1641
1401
  @apply flex items-center gap-2 px-2 py-1.5;
1642
1402
  background-color: var(--color-bg-muted);
@@ -1644,107 +1404,75 @@
1644
1404
  }
1645
1405
 
1646
1406
  /* ============================================================ */
1647
- /* Date pickers (Popover-based) + custom Calendar */
1407
+ /* Date / Time pickers + custom Calendar */
1648
1408
  /* ============================================================ */
1649
1409
  .date-picker-trigger {
1650
1410
  @apply inline-flex items-center gap-2 cursor-pointer text-left w-full;
1651
1411
  min-width: 12rem;
1652
1412
  }
1653
- .date-picker-trigger[disabled] {
1654
- @apply opacity-60 cursor-not-allowed;
1655
- }
1413
+ .date-picker-trigger[disabled] { @apply opacity-60 cursor-not-allowed; }
1656
1414
 
1657
- .calendar {
1658
- @apply select-none;
1659
- }
1415
+ .calendar { @apply select-none; }
1660
1416
  .calendar-header {
1661
1417
  @apply flex items-center justify-between gap-1 mb-2;
1662
1418
  }
1663
1419
  .calendar-heading-btn {
1664
- @apply flex-1 text-sm font-semibold rounded py-1 px-2 transition-colors;
1420
+ @apply flex-1 text-sm font-semibold py-1 px-2 cursor-pointer;
1665
1421
  color: var(--color-fg);
1666
1422
  background: transparent;
1667
1423
  border: none;
1668
- cursor: pointer;
1669
- }
1670
- .calendar-heading-btn:hover {
1671
- background-color: var(--color-bg-muted);
1672
- }
1673
- .calendar-nav {
1674
- @apply inline-flex size-7 items-center justify-center rounded transition-colors;
1675
- color: var(--color-fg-muted);
1676
- background: transparent;
1677
- border: none;
1678
- cursor: pointer;
1679
- }
1680
- .calendar-nav:hover {
1681
- background-color: var(--color-bg-muted);
1682
- color: var(--color-fg);
1424
+ border-radius: var(--ui-radius-sm);
1425
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
1683
1426
  }
1427
+ .calendar-heading-btn:hover { background-color: var(--color-bg-muted); }
1428
+ .calendar-heading-btn:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
1429
+ /* `.calendar-nav` is in the `.btn-icon` selector list above. */
1684
1430
  .calendar-row {
1685
1431
  @apply grid gap-0.5;
1686
1432
  grid-template-columns: repeat(7, minmax(0, 1fr));
1687
1433
  }
1688
- .calendar-weekdays {
1689
- @apply mb-1;
1690
- }
1434
+ .calendar-weekdays { @apply mb-1; }
1691
1435
  .calendar-weekday {
1692
1436
  @apply text-center text-[11px] font-medium uppercase py-1;
1693
1437
  color: var(--color-fg-subtle);
1694
1438
  }
1695
1439
  .calendar-day {
1696
- @apply h-9 w-full inline-flex items-center justify-center rounded-md text-sm transition-colors;
1440
+ @apply h-9 w-full inline-flex items-center justify-center text-sm cursor-pointer;
1697
1441
  color: var(--color-fg);
1698
1442
  background: transparent;
1699
1443
  border: none;
1700
- cursor: pointer;
1701
- }
1702
- .calendar-day:hover:not(:disabled) {
1703
- background-color: var(--color-bg-muted);
1704
- }
1705
- .calendar-day:focus-visible {
1706
- outline: 2px solid var(--color-ring);
1707
- outline-offset: -2px;
1708
- }
1709
- .calendar-day-outside {
1710
- color: var(--color-fg-subtle);
1444
+ border-radius: var(--ui-radius-sm);
1445
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
1711
1446
  }
1447
+ .calendar-day:hover:not(:disabled) { background-color: var(--color-bg-muted); }
1448
+ .calendar-day:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
1449
+ .calendar-day-outside { color: var(--color-fg-subtle); }
1712
1450
  .calendar-day-today {
1713
1451
  font-weight: 600;
1714
1452
  box-shadow: inset 0 0 0 1px var(--color-border-strong);
1715
1453
  }
1716
- .calendar-day-selected {
1454
+ .calendar-day-selected,
1455
+ .calendar-day-selected:hover:not(:disabled) {
1717
1456
  background-color: var(--color-primary-600);
1718
1457
  color: white;
1719
1458
  font-weight: 600;
1720
1459
  }
1721
- .calendar-day-selected:hover:not(:disabled) {
1722
- background-color: var(--color-primary-700);
1723
- }
1460
+ .calendar-day-selected:hover:not(:disabled) { background-color: var(--color-primary-700); }
1724
1461
  .calendar-day-in-range {
1725
1462
  background-color: color-mix(in oklab, var(--color-primary-500) 12%, transparent);
1726
1463
  border-radius: 0;
1727
1464
  }
1728
- .calendar-day:disabled {
1729
- @apply opacity-30 cursor-not-allowed;
1730
- }
1465
+ .calendar-day:disabled { @apply opacity-30 cursor-not-allowed; }
1731
1466
 
1732
- /* TimePicker scrollable hour/minute columns */
1733
- .time-picker-popover {
1734
- min-width: 14rem;
1735
- }
1736
- .time-picker-grid {
1737
- @apply flex items-stretch gap-1;
1738
- }
1467
+ .time-picker-popover { min-width: 14rem; }
1468
+ .time-picker-grid { @apply flex items-stretch gap-1; }
1739
1469
  .time-picker-col {
1740
1470
  @apply flex-1 flex flex-col gap-0.5 overflow-y-auto py-1;
1741
1471
  max-height: 14rem;
1742
1472
  scroll-snap-type: y mandatory;
1743
1473
  scrollbar-width: thin;
1744
1474
  }
1745
- .time-picker-col::-webkit-scrollbar {
1746
- width: 4px;
1747
- }
1475
+ .time-picker-col::-webkit-scrollbar { width: 4px; }
1748
1476
  .time-picker-col::-webkit-scrollbar-thumb {
1749
1477
  background-color: var(--color-border-strong);
1750
1478
  border-radius: 2px;
@@ -1754,25 +1482,23 @@
1754
1482
  color: var(--color-fg-muted);
1755
1483
  }
1756
1484
  .time-picker-cell {
1757
- @apply h-8 inline-flex items-center justify-center rounded text-sm font-mono tabular-nums transition-colors;
1485
+ @apply h-8 inline-flex items-center justify-center text-sm font-mono tabular-nums cursor-pointer;
1758
1486
  color: var(--color-fg);
1759
1487
  background: transparent;
1760
1488
  border: none;
1761
- cursor: pointer;
1489
+ border-radius: var(--ui-radius-sm);
1762
1490
  scroll-snap-align: center;
1763
1491
  flex-shrink: 0;
1492
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
1764
1493
  }
1765
- .time-picker-cell:hover {
1766
- background-color: var(--color-bg-muted);
1767
- }
1768
- .time-picker-cell-selected {
1494
+ .time-picker-cell:hover { background-color: var(--color-bg-muted); }
1495
+ .time-picker-cell-selected,
1496
+ .time-picker-cell-selected:hover {
1769
1497
  background-color: var(--color-primary-600);
1770
1498
  color: white;
1771
1499
  font-weight: 600;
1772
1500
  }
1773
- .time-picker-cell-selected:hover {
1774
- background-color: var(--color-primary-700);
1775
- }
1501
+ .time-picker-cell-selected:hover { background-color: var(--color-primary-700); }
1776
1502
 
1777
1503
  /* ============================================================ */
1778
1504
  /* DataTable */
@@ -1782,42 +1508,25 @@
1782
1508
  border: 1px solid var(--color-border);
1783
1509
  border-radius: var(--ui-radius);
1784
1510
  }
1785
- .data-table {
1786
- @apply w-full text-sm;
1787
- border-collapse: collapse;
1788
- }
1511
+ .data-table { @apply w-full text-sm; border-collapse: collapse; }
1789
1512
  .data-table th,
1790
- .data-table td {
1791
- @apply px-3 py-2 text-left align-middle;
1792
- }
1513
+ .data-table td { @apply px-3 py-2 text-left align-middle; }
1793
1514
  .data-table thead th {
1794
1515
  @apply font-semibold;
1795
1516
  color: var(--color-fg);
1796
1517
  background-color: var(--color-bg-muted);
1797
1518
  border-bottom: 1px solid var(--color-border);
1798
1519
  }
1799
- .data-table-sticky thead th {
1800
- @apply sticky top-0 z-10;
1801
- }
1802
- .data-table tbody tr {
1803
- border-bottom: 1px solid var(--color-border);
1804
- }
1805
- .data-table tbody tr:last-child {
1806
- border-bottom: none;
1807
- }
1808
- .data-table tbody tr:hover {
1809
- background-color: var(--color-bg-muted);
1810
- }
1520
+ .data-table-sticky thead th { @apply sticky top-0 z-10; }
1521
+ .data-table tbody tr { border-bottom: 1px solid var(--color-border); }
1522
+ .data-table tbody tr:last-child { border-bottom: none; }
1523
+ .data-table tbody tr:hover { background-color: var(--color-bg-muted); }
1811
1524
  .data-table-striped tbody tr:nth-child(even) {
1812
1525
  background-color: var(--color-bg-subtle);
1813
1526
  }
1814
1527
  .data-table-dense th,
1815
- .data-table-dense td {
1816
- @apply py-1;
1817
- }
1818
- .data-table-row-clickable {
1819
- @apply cursor-pointer;
1820
- }
1528
+ .data-table-dense td { @apply py-1; }
1529
+ .data-table-row-clickable { @apply cursor-pointer; }
1821
1530
  .data-table-empty td {
1822
1531
  @apply text-center py-6;
1823
1532
  color: var(--color-fg-muted);
@@ -1829,49 +1538,53 @@
1829
1538
  font: inherit;
1830
1539
  cursor: pointer;
1831
1540
  }
1832
- .data-table-sort:hover {
1833
- color: var(--color-primary-600);
1834
- }
1541
+ .data-table-sort:hover { color: var(--color-primary-600); }
1835
1542
 
1836
1543
  /* ============================================================ */
1837
- /* Tree (recursive) */
1544
+ /* Tree */
1838
1545
  /* ============================================================ */
1839
- .tree {
1840
- @apply text-sm;
1841
- color: var(--color-fg);
1842
- }
1843
- .tree ul {
1844
- @apply list-none p-0 m-0;
1845
- }
1546
+ .tree { @apply text-sm; color: var(--color-fg); }
1547
+ .tree ul { @apply list-none p-0 m-0; }
1846
1548
  .tree-row {
1847
- @apply flex w-full items-center gap-2 py-1 pr-2 text-left rounded-sm;
1848
- @apply transition-colors focus-visible:outline-none focus-visible:ring-2;
1549
+ @apply flex w-full items-center gap-2 py-1 pr-2 text-left cursor-pointer;
1849
1550
  color: var(--color-fg);
1850
1551
  background-color: transparent;
1851
- --tw-ring-color: var(--color-ring);
1852
- }
1853
- .tree-row:hover {
1854
- background-color: var(--color-bg-muted);
1552
+ border-radius: var(--ui-radius-sm);
1553
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
1855
1554
  }
1856
- .tree-row-selected {
1555
+ .tree-row:hover { background-color: var(--color-bg-muted); }
1556
+ .tree-row:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
1557
+ .tree-row-selected,
1558
+ .tree-row-selected:hover {
1857
1559
  background-color: color-mix(in oklab, var(--color-primary-500) 12%, transparent);
1858
1560
  color: var(--color-fg);
1859
1561
  }
1860
- .tree-row-selected:hover {
1861
- background-color: color-mix(in oklab, var(--color-primary-500) 18%, transparent);
1862
- }
1863
1562
  .tree-chevron {
1864
- @apply inline-flex size-4 shrink-0 items-center justify-center transition-transform duration-150;
1563
+ @apply inline-flex size-4 shrink-0 items-center justify-center;
1865
1564
  color: var(--color-fg-muted);
1565
+ transition: transform var(--ui-motion-fast) var(--ui-motion-ease);
1866
1566
  }
1867
- .tree-chevron-open {
1868
- transform: rotate(90deg);
1567
+ .tree-chevron-open { transform: rotate(90deg); }
1568
+ .tree-chevron-spacer { @apply inline-block size-4 shrink-0; }
1569
+ .tree-label { @apply truncate; }
1570
+
1571
+ /* ============================================================ */
1572
+ /* ScrollArea */
1573
+ /* Atom-defined so the overflow rules ship in atoms.css; cannot */
1574
+ /* rely on Tailwind utility generation inside library `cn()` */
1575
+ /* calls because Tailwind v4 doesn't scan workspace-dep sources */
1576
+ /* by default. */
1577
+ /* ============================================================ */
1578
+ .norns-scroll-area {
1579
+ overflow-y: auto;
1580
+ overflow-x: hidden;
1869
1581
  }
1870
- .tree-chevron-spacer {
1871
- @apply inline-block size-4 shrink-0;
1582
+ .norns-scroll-area-horizontal {
1583
+ overflow-x: auto;
1584
+ overflow-y: hidden;
1872
1585
  }
1873
- .tree-label {
1874
- @apply truncate;
1586
+ .norns-scroll-area-both {
1587
+ overflow: auto;
1875
1588
  }
1876
1589
 
1877
1590
  /* ============================================================ */
@@ -1883,37 +1596,32 @@
1883
1596
  background-color: var(--color-bg-muted);
1884
1597
  }
1885
1598
  .carousel-track {
1886
- @apply flex h-full transition-transform duration-500 ease-out;
1887
- }
1888
- .carousel-slide {
1889
- @apply h-full w-full shrink-0;
1599
+ @apply flex h-full;
1600
+ transition: transform 500ms var(--ui-motion-ease);
1890
1601
  }
1602
+ .carousel-slide { @apply h-full w-full shrink-0; }
1891
1603
  .carousel-arrow {
1892
- @apply absolute top-1/2 -translate-y-1/2 inline-flex size-9 items-center justify-center;
1893
- @apply transition-colors focus-visible:outline-none focus-visible:ring-2;
1604
+ @apply absolute top-1/2 -translate-y-1/2 inline-flex items-center justify-center cursor-pointer;
1605
+ width: var(--ui-icon-btn-lg);
1606
+ height: var(--ui-icon-btn-lg);
1894
1607
  background-color: color-mix(in oklab, var(--color-bg) 70%, transparent);
1895
1608
  color: var(--color-fg);
1896
1609
  border: 1px solid var(--color-border);
1897
1610
  border-radius: var(--ui-radius-full);
1898
- --tw-ring-color: var(--color-ring);
1899
- }
1900
- .carousel-arrow:hover {
1901
- background-color: var(--color-bg);
1902
- }
1903
- .carousel-arrow-prev {
1904
- @apply left-3;
1905
- }
1906
- .carousel-arrow-next {
1907
- @apply right-3;
1611
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
1908
1612
  }
1613
+ .carousel-arrow:hover { background-color: var(--color-bg); }
1614
+ .carousel-arrow:focus-visible { outline: none; box-shadow: var(--ui-focus-ring); }
1615
+ .carousel-arrow-prev { @apply left-3; }
1616
+ .carousel-arrow-next { @apply right-3; }
1909
1617
  .carousel-dots {
1910
1618
  @apply absolute bottom-3 left-1/2 -translate-x-1/2 inline-flex items-center gap-1.5;
1911
1619
  }
1912
1620
  .carousel-dot {
1913
- @apply size-2 rounded-full transition-colors;
1621
+ @apply size-2 rounded-full cursor-pointer;
1914
1622
  background-color: color-mix(in oklab, var(--color-bg-inverse) 50%, transparent);
1623
+ border: none;
1624
+ transition: background-color var(--ui-motion-fast) var(--ui-motion-ease);
1915
1625
  }
1916
- .carousel-dot-active {
1917
- background-color: var(--color-bg-inverse);
1918
- }
1626
+ .carousel-dot-active { background-color: var(--color-bg-inverse); }
1919
1627
  }