@wildmason/aegis 1.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/inputs.css ADDED
@@ -0,0 +1,1537 @@
1
+ /* ============================================================
2
+ Aegis Design System — inputs.css
3
+ Shared form control primitives for Works, Helm, Slate, Mortar.
4
+
5
+ Usage
6
+ -----
7
+ Import after themes.css so all --wm-* tokens are defined:
8
+
9
+ @import '@aegis/themes.css';
10
+ @import '@aegis/inputs.css';
11
+
12
+ Class reference
13
+ ---------------
14
+ Border-radius utilities (replaces Tailwind rounded-*):
15
+ .wm-r-xs border-radius: 4px — chips, badges, inline elements
16
+ .wm-r-sm border-radius: 6px — buttons, inputs, sidebar items
17
+ .wm-r-md border-radius: 8px — modals, dropdowns, popovers
18
+ .wm-r-lg border-radius: 12px — search overlays, large cards
19
+ .wm-r-full border-radius: 9999px — pills, avatars
20
+ .wm-r-l left corners only — left half of split button
21
+ .wm-r-r right corners only — right half of split button
22
+ .wm-r-t top corners only — top of stacked elements
23
+
24
+ Shadow utilities (replaces Tailwind shadow-*):
25
+ .wm-shadow box-shadow: --wm-shadow-sm — embedded widgets
26
+ .wm-shadow-md box-shadow: --wm-shadow-md — cards, dropdowns, popovers
27
+ .wm-shadow-lg box-shadow: --wm-shadow-lg — modals, overlays
28
+
29
+ Menu item:
30
+ .wm-menu-item Hover-background for buttons inside popovers/dropdowns
31
+
32
+ Form controls:
33
+ .wm-field Vertical field container (label + control + help)
34
+ .wm-label Field label
35
+ .wm-checkbox Custom-styled native checkbox (appearance:none)
36
+ .wm-check-label Label wrapper: flex row aligning .wm-checkbox + text
37
+ .wm-input Base input — text, textarea, native select
38
+ .wm-input-group Composite row: icon + .wm-input-bare (e.g. search)
39
+ .wm-input-bare Borderless inner input used inside .wm-input-group
40
+ .wm-input-icon Leading/trailing icon slot inside .wm-input-group
41
+ .wm-select-trigger Button-based custom select trigger (for app-select etc.)
42
+ .wm-number-input Number input variant (spinner chrome removed)
43
+ .wm-number-controls ± button strip appended to .wm-input-group
44
+ .wm-number-btn Individual +/− step button
45
+ .wm-ghost-field Ghost wrapper — chrome hidden at rest, shown on hover
46
+ .wm-error-msg Inline danger message block below a field
47
+
48
+ Keyboard:
49
+ .wm-kbd Single keyboard key badge (heavier bottom border = key depth)
50
+ .wm-kbd-combo Flex wrapper for key combinations — auto-injects '+' between keys
51
+
52
+ Spec
53
+ ----
54
+ - Standard height: 36px (use padding for textarea)
55
+ - Focus ring: 3px diffuse box-shadow — no layout shift
56
+ - Labels: 12px / font-weight 500 / text-muted
57
+ - Hover: bg → wm-bg-sunken, border → wm-border-hover
58
+ - Focus: border → wm-color-accent, animated ring
59
+ - Disabled: 50% opacity, not-allowed cursor
60
+ ============================================================ */
61
+
62
+
63
+ /* ============================================================
64
+ Chevron — per colour-scheme
65
+ ============================================================
66
+ SVG data URIs cannot reference CSS custom properties or
67
+ currentColor, so we bake a light and dark version and assign
68
+ via --wm-chevron. Used by select.wm-input and .wm-select-trigger.
69
+ ============================================================ */
70
+
71
+ :root,
72
+ [data-theme='warm-light'],
73
+ [data-theme='neutral-light'],
74
+ [data-theme='solarized-precision'] {
75
+ --wm-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23000000' stroke-opacity='0.6' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
76
+ }
77
+
78
+ [data-theme='arctic-night'],
79
+ [data-theme='vampires-kiss'],
80
+ [data-theme='twilight-peach'],
81
+ [data-theme='thistle-mocha'],
82
+ [data-theme='posh-sandalwood'],
83
+ [data-theme='crepuscular-sky'],
84
+ [data-theme='source-control-dark'] {
85
+ --wm-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-opacity='0.6' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
86
+ }
87
+
88
+
89
+ /* ============================================================
90
+ Focus ring animation
91
+ ============================================================
92
+ Shared by .wm-input:focus, .wm-input-group:focus-within, and
93
+ .wm-select-trigger[aria-expanded='true']. Expands from 0 then
94
+ settles at 3px — communicates focus without adding layout space.
95
+ ============================================================ */
96
+
97
+ @keyframes wm-focus-pulse {
98
+ 0% { box-shadow: 0 0 0 0px color-mix(in srgb, var(--wm-color-accent) 60%, transparent); }
99
+ 55% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--wm-color-accent) 6%, transparent); }
100
+ 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--wm-color-accent) 15%, transparent); }
101
+ }
102
+
103
+
104
+ /* ============================================================
105
+ Field wrapper — .wm-field
106
+ ============================================================
107
+ Vertical flex column holding a label, control, and optional
108
+ help/error text. Gap of 4px keeps label visually coupled to
109
+ its control without merging into it.
110
+
111
+ Usage:
112
+ <div class="wm-field">
113
+ <label class="wm-label">Name</label>
114
+ <input class="wm-input" type="text">
115
+ <span class="wm-field-help">Optional helper text</span>
116
+ </div>
117
+
118
+ Spacing helpers (.space-y / .space-x):
119
+ Apply to consecutive siblings to auto-gap them.
120
+ ============================================================ */
121
+
122
+ .wm-field {
123
+ display: flex;
124
+ flex-direction: column;
125
+ gap: 4px;
126
+ box-sizing: border-box;
127
+ }
128
+
129
+ /* Vertical and horizontal auto-spacing between consecutive fields */
130
+ .wm-field.space-y + .wm-field { margin-top: 0.75rem; }
131
+ .wm-field.space-x + .wm-field { margin-left: 0.75rem; }
132
+
133
+
134
+ /* ============================================================
135
+ Label — .wm-label
136
+ ============================================================
137
+ 12px / medium weight / text-muted. Rendered as a block so it
138
+ occupies its own row in the flex column. user-select:none
139
+ prevents accidental highlight when clicking adjacent controls.
140
+ ============================================================ */
141
+
142
+ .wm-label {
143
+ display: block;
144
+ font-size: 12px;
145
+ font-weight: 500;
146
+ color: var(--wm-text-muted);
147
+ line-height: 1;
148
+ user-select: none;
149
+ }
150
+
151
+
152
+ /* ============================================================
153
+ Help and error text
154
+ ============================================================
155
+ Sit below the control inside a .wm-field. Error text uses
156
+ danger color; help text uses muted.
157
+ ============================================================ */
158
+
159
+ .wm-field-help {
160
+ font-size: 11px;
161
+ color: var(--wm-text-muted);
162
+ line-height: 1.4;
163
+ }
164
+
165
+ .wm-field-error {
166
+ font-size: 11px;
167
+ color: var(--wm-color-danger);
168
+ line-height: 1.4;
169
+ }
170
+
171
+
172
+ /* ============================================================
173
+ Checkbox — .wm-checkbox + .wm-check-label
174
+ ============================================================
175
+ Custom-styled native checkbox. appearance:none hides the
176
+ native chrome; checked state renders via a baked-in white
177
+ SVG checkmark on the accent fill (works across all themes).
178
+
179
+ Usage:
180
+ <label class="wm-check-label">
181
+ <input type="checkbox" class="wm-checkbox">
182
+ <span>Enable feature</span>
183
+ </label>
184
+
185
+ Standalone (pair with aria-label):
186
+ <input type="checkbox" class="wm-checkbox" aria-label="…">
187
+ ============================================================ */
188
+
189
+ @keyframes wm-check-draw {
190
+ from { stroke-dashoffset: 20; }
191
+ to { stroke-dashoffset: 0; }
192
+ }
193
+
194
+ .wm-checkbox {
195
+ appearance: none;
196
+ -webkit-appearance: none;
197
+ flex-shrink: 0;
198
+ width: 16px;
199
+ height: 16px;
200
+ box-sizing: border-box;
201
+ border: 1.5px solid var(--wm-border-input);
202
+ border-radius: var(--wm-radius-xs);
203
+ background-color: var(--wm-bg-base);
204
+ cursor: pointer;
205
+ position: relative;
206
+ vertical-align: middle;
207
+ transition:
208
+ border-color 150ms ease,
209
+ background-color 150ms ease,
210
+ box-shadow 150ms ease;
211
+
212
+ --tw-ring-shadow: none;
213
+ --tw-ring-offset-shadow: none;
214
+ --tw-ring-color: transparent;
215
+ --tw-shadow: none;
216
+ }
217
+
218
+ .wm-checkbox:hover:not(:checked):not(:disabled) {
219
+ background-color: var(--wm-bg-sunken);
220
+ border-color: var(--wm-border-hover);
221
+ }
222
+
223
+ .wm-checkbox:checked {
224
+ background-color: var(--wm-color-accent);
225
+ border-color: var(--wm-color-accent);
226
+ }
227
+
228
+ /* Animated checkmark via pseudo-element SVG */
229
+ .wm-checkbox:checked::after {
230
+ content: '';
231
+ display: block;
232
+ position: absolute;
233
+ inset: 0;
234
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 8'%3E%3Cpolyline points='1,4 3.5,6.5 9,1' fill='none' stroke='%23ffffff' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round' stroke-dasharray='20' stroke-dashoffset='0'/%3E%3C/svg%3E");
235
+ background-repeat: no-repeat;
236
+ background-position: center;
237
+ background-size: 10px 8px;
238
+ animation: wm-check-draw 150ms ease-out forwards;
239
+ }
240
+
241
+ .wm-checkbox:focus-visible {
242
+ outline: none;
243
+ border-color: var(--wm-color-accent);
244
+ animation: wm-focus-pulse 400ms cubic-bezier(0.2, 0, 0.4, 1) forwards;
245
+ }
246
+
247
+ .wm-checkbox:disabled {
248
+ opacity: 0.5;
249
+ cursor: not-allowed;
250
+ }
251
+
252
+ /* Label wrapper */
253
+ .wm-check-label {
254
+ display: inline-flex;
255
+ align-items: center;
256
+ gap: 0.5rem;
257
+ cursor: pointer;
258
+ user-select: none;
259
+ font-size: 0.875rem;
260
+ color: var(--wm-text-primary);
261
+ line-height: 1.4;
262
+ }
263
+
264
+ .wm-check-label:has(.wm-checkbox:disabled) {
265
+ opacity: 0.5;
266
+ cursor: not-allowed;
267
+ }
268
+
269
+ @media (prefers-reduced-motion: reduce) {
270
+ .wm-checkbox:checked::after { animation: none; }
271
+ }
272
+
273
+
274
+ /* ============================================================
275
+ Menu item — .wm-menu-item
276
+ ============================================================
277
+ Hover-background style for buttons inside popovers, dropdowns,
278
+ and custom menus. Matches the pattern used by .ctx-menu-item
279
+ in the context-menu component.
280
+
281
+ Usage:
282
+ <button class="wm-menu-item w-full text-left px-3 py-1.5 text-xs"
283
+ style="color: var(--wm-text-primary)">
284
+ Menu action
285
+ </button>
286
+ ============================================================ */
287
+
288
+ .wm-menu-item {
289
+ transition: background 0.1s;
290
+ }
291
+ .wm-menu-item:hover {
292
+ background: var(--wm-bg-hover);
293
+ }
294
+ .wm-menu-item:focus-visible {
295
+ outline: 2px solid var(--wm-color-accent);
296
+ outline-offset: -2px;
297
+ }
298
+
299
+
300
+ /* ============================================================
301
+ Border-radius utilities — .wm-r-*
302
+ ============================================================
303
+ Named after the radius token scale. Use instead of Tailwind's
304
+ rounded-* so radius values stay in sync with the design token
305
+ scale across all apps (with or without Tailwind present).
306
+
307
+ Full-corner:
308
+ .wm-r-xs → --wm-radius-xs (4px) chips, badges, inline elements
309
+ .wm-r-sm → --wm-radius-sm (6px) buttons, inputs, sidebar items
310
+ .wm-r-md → --wm-radius-md (8px) modals, dropdowns, popovers
311
+ .wm-r-lg → --wm-radius-lg (12px) search overlays, large cards
312
+ .wm-r-full → --wm-radius-full (9999px) pills, avatars
313
+
314
+ Partial-corner (split buttons, stacked panels):
315
+ .wm-r-l left corners only
316
+ .wm-r-r right corners only
317
+ .wm-r-t top corners only
318
+
319
+ Usage:
320
+ <div class="wm-r-md"> … </div>
321
+ <button class="wm-btn wm-r-l">Left</button>
322
+ <button class="wm-btn wm-r-r">Right</button>
323
+ ============================================================ */
324
+
325
+ .wm-r-xs { border-radius: var(--wm-radius-xs); }
326
+ .wm-r-sm { border-radius: var(--wm-radius-sm); }
327
+ .wm-r-md { border-radius: var(--wm-radius-md); }
328
+ .wm-r-lg { border-radius: var(--wm-radius-lg); }
329
+ .wm-r-full { border-radius: var(--wm-radius-full); }
330
+
331
+ .wm-r-l { border-radius: var(--wm-radius-xs) 0 0 var(--wm-radius-xs); }
332
+ .wm-r-r { border-radius: 0 var(--wm-radius-xs) var(--wm-radius-xs) 0; }
333
+ .wm-r-t { border-radius: var(--wm-radius-xs) var(--wm-radius-xs) 0 0; }
334
+
335
+
336
+ /* ============================================================
337
+ Shadow utilities — .wm-shadow / .wm-shadow-md / .wm-shadow-lg
338
+ ============================================================
339
+ Named after the shadow token scale. Reserve for floating/elevated
340
+ elements only — borders (--wm-border) handle hierarchy; shadows
341
+ handle depth. Use instead of Tailwind's shadow-* utilities.
342
+
343
+ .wm-shadow → --wm-shadow-sm embedded widgets, subtle lift
344
+ .wm-shadow-md → --wm-shadow-md cards, dropdowns, popovers
345
+ .wm-shadow-lg → --wm-shadow-lg modals, search overlays, command palettes
346
+
347
+ Usage:
348
+ <div class="wm-r-md wm-shadow-md"> Dropdown </div>
349
+ <div class="wm-r-lg wm-shadow-lg"> Modal </div>
350
+ ============================================================ */
351
+
352
+ .wm-shadow { box-shadow: var(--wm-shadow-sm); }
353
+ .wm-shadow-md { box-shadow: var(--wm-shadow-md); }
354
+ .wm-shadow-lg { box-shadow: var(--wm-shadow-lg); }
355
+
356
+
357
+ /* ============================================================
358
+ Base input — .wm-input
359
+ ============================================================
360
+ Applies to: <input>, <textarea>, <select> (native).
361
+ 36px height for single-line; textarea overrides height.
362
+
363
+ States:
364
+ Hover — bg-inset, border-hover
365
+ Focus — accent border + animated ring
366
+ Disabled — 50% opacity, not-allowed cursor
367
+
368
+ Usage:
369
+ <input class="wm-input" type="text" placeholder="…">
370
+ <select class="wm-input"> … </select>
371
+ <textarea class="wm-input"></textarea>
372
+ ============================================================ */
373
+
374
+ .wm-input {
375
+ /* Dimensions */
376
+ display: block;
377
+ width: 100%;
378
+ height: 36px;
379
+ box-sizing: border-box;
380
+ padding: 0 12px;
381
+
382
+ /* Typography */
383
+ font-size: 14px;
384
+ font-family: inherit;
385
+ color: var(--wm-text-primary);
386
+ line-height: 36px;
387
+
388
+ /* Surface */
389
+ background-color: var(--wm-bg-base);
390
+ border: 1px solid var(--wm-border-input);
391
+ border-radius: var(--wm-radius-md);
392
+
393
+ /* Reset */
394
+ outline: none;
395
+ appearance: none;
396
+ -webkit-appearance: none;
397
+
398
+ /* Neutralise Tailwind ring variables so they can't bleed into box-shadow */
399
+ --tw-ring-shadow: none;
400
+ --tw-ring-offset-shadow: none;
401
+ --tw-ring-color: transparent;
402
+ --tw-shadow: none;
403
+
404
+ transition:
405
+ background-color 150ms ease,
406
+ border-color 150ms ease,
407
+ box-shadow 150ms ease;
408
+ }
409
+
410
+ /* Placeholder: uses --wm-text-placeholder (intentionally lower contrast —
411
+ exempt from WCAG 1.4.3 per WCAG 2.2; inputs must have a visible label). */
412
+ .wm-input::placeholder {
413
+ color: var(--wm-text-placeholder);
414
+ opacity: 1;
415
+ }
416
+
417
+ /* Hover: step to inset bg + secondary border */
418
+ .wm-input:hover:not(:focus):not(:disabled) {
419
+ background-color: var(--wm-bg-sunken);
420
+ border-color: var(--wm-border-hover);
421
+ }
422
+
423
+ /* Focus: accent border + animated diffuse ring */
424
+ .wm-input:focus {
425
+ outline: none;
426
+ border-color: var(--wm-color-accent);
427
+ animation: wm-focus-pulse 400ms cubic-bezier(0.2, 0, 0.4, 1) forwards;
428
+ }
429
+
430
+ /* Disabled */
431
+ .wm-input:disabled {
432
+ opacity: 0.5;
433
+ cursor: not-allowed;
434
+ }
435
+
436
+ /* Inherit color-scheme so date/time picker chrome (calendar icon, spinners)
437
+ matches the active theme's light/dark declaration. */
438
+ .wm-input[type='date'],
439
+ .wm-input[type='time'],
440
+ .wm-input[type='datetime-local'] {
441
+ color-scheme: inherit;
442
+ }
443
+
444
+ /* Keep native date/time picker indicator in sync with surrounding icon opacity */
445
+ .wm-input[type='date']::-webkit-calendar-picker-indicator,
446
+ .wm-input[type='time']::-webkit-calendar-picker-indicator,
447
+ .wm-input[type='datetime-local']::-webkit-calendar-picker-indicator {
448
+ color: FieldText;
449
+ opacity: 0.6;
450
+ cursor: pointer;
451
+ }
452
+
453
+
454
+ /* ============================================================
455
+ Textarea variant
456
+ ============================================================
457
+ Overrides height and line-height from .wm-input. Minimum 80px;
458
+ user can resize vertically. Padding shifts to 8px top/bottom
459
+ to align the first line of text with the 36px input baseline.
460
+ ============================================================ */
461
+
462
+ textarea.wm-input {
463
+ height: auto;
464
+ min-height: 80px;
465
+ padding: 8px 12px;
466
+ line-height: 1.5;
467
+ resize: vertical;
468
+ }
469
+
470
+
471
+ /* ============================================================
472
+ Native select variant
473
+ ============================================================
474
+ appearance:none strips the native chevron; we restore one via
475
+ --wm-chevron (light/dark SVG data URI defined at top of file).
476
+ ============================================================ */
477
+
478
+ select.wm-input {
479
+ background-image: var(--wm-chevron);
480
+ background-repeat: no-repeat;
481
+ background-position: right 12px center;
482
+ padding-right: 32px;
483
+ cursor: pointer;
484
+ }
485
+
486
+
487
+ /* ============================================================
488
+ Composite input group — .wm-input-group
489
+ ============================================================
490
+ Wraps a leading/trailing icon (.wm-input-icon) together with
491
+ a borderless inner input (.wm-input-bare). The border and
492
+ focus ring belong to the group, not the inner input.
493
+
494
+ Usage:
495
+ <div class="wm-input-group">
496
+ <span class="wm-input-icon">
497
+ <svg …/>
498
+ </span>
499
+ <input class="wm-input-bare" type="search" placeholder="Search…">
500
+ </div>
501
+ ============================================================ */
502
+
503
+ .wm-input-group {
504
+ position: relative;
505
+ display: flex;
506
+ align-items: center;
507
+ height: 36px;
508
+ box-sizing: border-box;
509
+ background-color: var(--wm-bg-base);
510
+ border: 1px solid var(--wm-border-input);
511
+ border-radius: var(--wm-radius-md);
512
+ transition:
513
+ background-color 150ms ease,
514
+ border-color 150ms ease,
515
+ box-shadow 150ms ease;
516
+
517
+ --tw-ring-shadow: none;
518
+ --tw-ring-offset-shadow: none;
519
+ --tw-ring-color: transparent;
520
+ --tw-shadow: none;
521
+ }
522
+
523
+ /* Hover: skip if the inner input is focused or the group is disabled */
524
+ .wm-input-group:hover:not(:has(.wm-input-bare:focus)):not(:has(:disabled)) {
525
+ background-color: var(--wm-bg-sunken);
526
+ border-color: var(--wm-border-hover);
527
+ }
528
+
529
+ /* Focus: border + animated ring on the group when inner input is focused */
530
+ .wm-input-group:focus-within {
531
+ outline: none;
532
+ border-color: var(--wm-color-accent);
533
+ animation: wm-focus-pulse 400ms cubic-bezier(0.2, 0, 0.4, 1) forwards;
534
+ }
535
+
536
+
537
+ /* ============================================================
538
+ Bare inner input — .wm-input-bare
539
+ ============================================================
540
+ Used exclusively inside .wm-input-group. No border or background
541
+ of its own — those belong to the group wrapper.
542
+ ============================================================ */
543
+
544
+ .wm-input-bare {
545
+ flex: 1;
546
+ min-width: 0;
547
+ height: 100%;
548
+ padding: 0 12px;
549
+ font-size: 14px;
550
+ font-family: inherit;
551
+ color: var(--wm-text-primary);
552
+ background: transparent;
553
+ border: none;
554
+ outline: none;
555
+ box-sizing: border-box;
556
+
557
+ --tw-ring-shadow: none;
558
+ --tw-ring-offset-shadow: none;
559
+ --tw-ring-color: transparent;
560
+ --tw-shadow: none;
561
+ }
562
+
563
+ .wm-input-bare::placeholder {
564
+ color: var(--wm-text-placeholder);
565
+ opacity: 1;
566
+ }
567
+
568
+
569
+ /* ============================================================
570
+ Icon slot — .wm-input-icon
571
+ ============================================================
572
+ Leading or trailing icon inside a .wm-input-group. Uses the
573
+ CSS system color FieldText so it matches native picker chrome
574
+ in light and dark color-scheme contexts. pointer-events:none
575
+ keeps clicks on the icon from stealing focus from the input.
576
+
577
+ Usage:
578
+ <!-- leading icon -->
579
+ <span class="wm-input-icon"><svg …/></span>
580
+ <input class="wm-input-bare" …>
581
+
582
+ <!-- trailing icon (e.g. clear button — add pointer-events:auto) -->
583
+ <input class="wm-input-bare" …>
584
+ <button class="wm-input-icon" style="pointer-events:auto"> … </button>
585
+ ============================================================ */
586
+
587
+ .wm-input-icon {
588
+ display: flex;
589
+ align-items: center;
590
+ justify-content: center;
591
+ flex-shrink: 0;
592
+ width: 36px;
593
+ color: FieldText;
594
+ opacity: 0.6;
595
+ pointer-events: none;
596
+ }
597
+
598
+ /* Remove the left padding gap when an icon directly precedes the bare input */
599
+ .wm-input-icon + .wm-input-bare {
600
+ padding-left: 0;
601
+ }
602
+
603
+
604
+ /* ============================================================
605
+ Custom select trigger — .wm-select-trigger
606
+ ============================================================
607
+ A <button> styled identically to .wm-input for use with
608
+ component-level select pickers (app-select, popover select).
609
+ Matches the 36px height and all hover/focus states of .wm-input.
610
+
611
+ Opened state: set aria-expanded="true" on the button to trigger
612
+ the focus-ring style while the popover is open.
613
+
614
+ Usage:
615
+ <button class="wm-select-trigger" aria-expanded="false">
616
+ <span>Selected value</span>
617
+ <svg … /> <!-- chevron -->
618
+ </button>
619
+
620
+ Direct-child icon rule keeps the chevron opacity consistent
621
+ without overriding nested icon components inside the trigger.
622
+ ============================================================ */
623
+
624
+ .wm-select-trigger {
625
+ display: flex;
626
+ align-items: center;
627
+ justify-content: space-between;
628
+ gap: 8px;
629
+ width: 100%;
630
+ height: 36px;
631
+ box-sizing: border-box;
632
+ padding: 0 12px;
633
+ font-size: 14px;
634
+ font-family: inherit;
635
+ color: var(--wm-text-primary);
636
+ background-color: var(--wm-bg-base);
637
+ border: 1px solid var(--wm-border-input);
638
+ border-radius: var(--wm-radius-md);
639
+ text-align: left;
640
+ cursor: pointer;
641
+ outline: none;
642
+ transition:
643
+ background-color 150ms ease,
644
+ border-color 150ms ease,
645
+ box-shadow 150ms ease;
646
+
647
+ --tw-ring-shadow: none;
648
+ --tw-ring-offset-shadow: none;
649
+ --tw-ring-color: transparent;
650
+ --tw-shadow: none;
651
+ }
652
+
653
+ /* Trailing chevron / icon — direct child only to avoid clobbering nested icons */
654
+ .wm-select-trigger > svg,
655
+ .wm-select-trigger > i {
656
+ color: FieldText;
657
+ opacity: 0.6;
658
+ font-size: 12px;
659
+ flex-shrink: 0;
660
+ }
661
+
662
+ /* Hover: skip when open (aria-expanded) or disabled */
663
+ .wm-select-trigger:hover:not([aria-expanded='true']):not(:disabled) {
664
+ background-color: var(--wm-bg-sunken);
665
+ border-color: var(--wm-border-hover);
666
+ }
667
+
668
+ /* Open state: same visual as focus */
669
+ .wm-select-trigger[aria-expanded='true'],
670
+ .wm-select-trigger:focus-visible {
671
+ outline: none;
672
+ border-color: var(--wm-color-accent);
673
+ animation: wm-focus-pulse 400ms cubic-bezier(0.2, 0, 0.4, 1) forwards;
674
+ }
675
+
676
+ .wm-select-trigger:disabled {
677
+ opacity: 0.5;
678
+ cursor: not-allowed;
679
+ }
680
+
681
+
682
+ /* ============================================================
683
+ Number input — .wm-number-input / .wm-number-controls
684
+ ============================================================
685
+ Removes native browser spinners and replaces them with a
686
+ custom ± button strip (.wm-number-controls) appended inside
687
+ a .wm-input-group.
688
+
689
+ Usage:
690
+ <div class="wm-input-group">
691
+ <input class="wm-input-bare wm-number-input" type="number" value="5">
692
+ <div class="wm-number-controls">
693
+ <button class="wm-number-btn" aria-label="Increase">+</button>
694
+ <button class="wm-number-btn" aria-label="Decrease">−</button>
695
+ </div>
696
+ </div>
697
+ ============================================================ */
698
+
699
+ /* Strip native spinners */
700
+ .wm-number-input::-webkit-inner-spin-button,
701
+ .wm-number-input::-webkit-outer-spin-button {
702
+ -webkit-appearance: none;
703
+ margin: 0;
704
+ }
705
+ .wm-number-input {
706
+ -moz-appearance: textfield;
707
+ }
708
+
709
+ /* ± strip: sits flush at the right edge of the group, stacked vertically */
710
+ .wm-number-controls {
711
+ display: flex;
712
+ flex-direction: column;
713
+ flex-shrink: 0;
714
+ gap: 1px;
715
+ padding: 3px 4px 3px 2px;
716
+ }
717
+
718
+ .wm-number-btn {
719
+ display: flex;
720
+ align-items: center;
721
+ justify-content: center;
722
+ width: 20px;
723
+ height: 14px;
724
+ padding: 0;
725
+ background: transparent;
726
+ border: none;
727
+ border-radius: var(--wm-radius-xs);
728
+ color: FieldText;
729
+ opacity: 0.6;
730
+ cursor: pointer;
731
+ transition: opacity 150ms ease, background-color 150ms ease;
732
+ outline: none;
733
+ font-family: inherit;
734
+
735
+ --tw-ring-shadow: none;
736
+ --tw-ring-offset-shadow: none;
737
+ --tw-ring-color: transparent;
738
+ --tw-shadow: none;
739
+ }
740
+
741
+ .wm-number-btn:hover:not(:disabled) {
742
+ opacity: 1;
743
+ background-color: color-mix(in srgb, FieldText 8%, transparent);
744
+ }
745
+
746
+ .wm-number-btn:disabled {
747
+ opacity: 0.25;
748
+ cursor: not-allowed;
749
+ }
750
+
751
+
752
+ /* ============================================================
753
+ Ghost field — .wm-ghost-field
754
+ ============================================================
755
+ Wraps any .wm-input, .wm-input-group, or .wm-select-trigger
756
+ to give it a "read-only until touched" appearance. At rest the
757
+ border and background are transparent so it looks like static
758
+ text. The 1px border box is preserved — zero layout shift when
759
+ chrome appears on hover.
760
+
761
+ Hover: same mid-tone bg + secondary border as a normal input hover.
762
+ Focus: standard .wm-input:focus / :focus-within takes over.
763
+
764
+ Usage:
765
+ <div class="wm-ghost-field">
766
+ <input class="wm-input" type="text" value="Editable value">
767
+ </div>
768
+
769
+ With number field (hides ± strip until hovered/focused):
770
+ <div class="wm-ghost-field">
771
+ <div class="wm-input-group">
772
+ <input class="wm-input-bare wm-number-input" type="number">
773
+ <div class="wm-number-controls"> … </div>
774
+ </div>
775
+ </div>
776
+ ============================================================ */
777
+
778
+ /* Resting: transparent chrome */
779
+ .wm-ghost-field .wm-input:not(:focus),
780
+ .wm-ghost-field .wm-input-group:not(:focus-within),
781
+ .wm-ghost-field .wm-select-trigger:not([aria-expanded='true']):not(:focus-visible) {
782
+ border-color: transparent;
783
+ background-color: transparent;
784
+ box-shadow: none;
785
+ }
786
+
787
+ /* Hover: reveal standard input hover chrome */
788
+ .wm-ghost-field:hover .wm-input:not(:focus),
789
+ .wm-ghost-field:hover .wm-input-group:not(:focus-within),
790
+ .wm-ghost-field:hover .wm-select-trigger:not([aria-expanded='true']):not(:focus-visible) {
791
+ background-color: var(--wm-bg-sunken);
792
+ border-color: var(--wm-border-hover);
793
+ }
794
+
795
+ /* Hide the ± strip on a ghost number field until hover or focus */
796
+ .wm-ghost-field .wm-input-group:not(:focus-within) .wm-number-controls {
797
+ visibility: hidden;
798
+ }
799
+ .wm-ghost-field:hover .wm-input-group:not(:focus-within) .wm-number-controls,
800
+ .wm-ghost-field .wm-input-group:focus-within .wm-number-controls {
801
+ visibility: visible;
802
+ }
803
+
804
+
805
+ /* ============================================================
806
+ Error message block — .wm-error-msg
807
+ ============================================================
808
+ Inline danger block displayed below a field (or at the top of
809
+ a form) to communicate validation or server errors. Uses the
810
+ per-theme danger tokens for background and border, keeping the
811
+ block readable across all 12 themes.
812
+
813
+ Usage:
814
+ <div class="wm-error-msg">Incorrect username or password.</div>
815
+ ============================================================ */
816
+
817
+ .wm-error-msg {
818
+ background-color: var(--wm-color-danger-bg);
819
+ color: var(--wm-color-danger);
820
+ border: 1px solid var(--wm-color-danger-border);
821
+ border-radius: var(--wm-radius-sm);
822
+ padding: 10px 12px;
823
+ font-size: 13px;
824
+ line-height: 1.4;
825
+ }
826
+
827
+ .wm-success-msg {
828
+ background-color: var(--wm-color-success-bg);
829
+ color: var(--wm-color-success);
830
+ border: 1px solid var(--wm-color-success-border);
831
+ border-radius: var(--wm-radius-sm);
832
+ padding: 10px 12px;
833
+ font-size: 13px;
834
+ line-height: 1.4;
835
+ }
836
+
837
+ .wm-warning-msg {
838
+ background-color: var(--wm-color-warning-bg);
839
+ color: var(--wm-color-warning);
840
+ border: 1px solid var(--wm-color-warning-border);
841
+ border-radius: var(--wm-radius-sm);
842
+ padding: 10px 12px;
843
+ font-size: 13px;
844
+ line-height: 1.4;
845
+ }
846
+
847
+ .wm-info-msg {
848
+ background-color: color-mix(in srgb, var(--wm-color-accent) 8%, transparent);
849
+ color: var(--wm-text-secondary);
850
+ border: 1px solid color-mix(in srgb, var(--wm-color-accent) 25%, transparent);
851
+ border-radius: var(--wm-radius-sm);
852
+ padding: 10px 12px;
853
+ font-size: 13px;
854
+ line-height: 1.4;
855
+ }
856
+
857
+
858
+ /* ============================================================
859
+ Loading spinner — .wm-spinner
860
+ ============================================================
861
+ A CSS-only spinner that uses the accent color. Must be paired
862
+ with aria-label or aria-busy on its container.
863
+
864
+ Usage:
865
+ <div class="wm-spinner" aria-label="Loading…" role="status"></div>
866
+
867
+ Prefers-reduced-motion: hides the spin, shows a static ring so
868
+ the presence of the loading state is still communicated.
869
+ ============================================================ */
870
+
871
+ @keyframes wm-spin {
872
+ to { transform: rotate(360deg); }
873
+ }
874
+
875
+ .wm-spinner {
876
+ width: 20px;
877
+ height: 20px;
878
+ border: 2px solid var(--wm-border);
879
+ border-top-color: var(--wm-color-accent);
880
+ border-radius: var(--wm-radius-full);
881
+ animation: wm-spin 600ms linear infinite;
882
+ flex-shrink: 0;
883
+ }
884
+
885
+ .wm-spinner--sm {
886
+ width: 14px;
887
+ height: 14px;
888
+ }
889
+
890
+ .wm-spinner--lg {
891
+ width: 28px;
892
+ height: 28px;
893
+ border-width: 3px;
894
+ }
895
+
896
+
897
+ /* ============================================================
898
+ Linear progress bar — .wm-progress
899
+ ============================================================
900
+ Two variants:
901
+ Indeterminate — use when duration is unknown (loading, fetching)
902
+ Determinate — use when progress 0–100 is known (upload, install)
903
+
904
+ Indeterminate usage:
905
+ <div class="wm-progress" role="progressbar" aria-label="Loading…">
906
+ <div class="wm-progress__bar wm-progress__bar--indeterminate"></div>
907
+ </div>
908
+
909
+ Determinate usage (CSS custom property drives width):
910
+ <div class="wm-progress" role="progressbar"
911
+ aria-valuenow="40" aria-valuemin="0" aria-valuemax="100"
912
+ aria-label="Uploading…">
913
+ <div class="wm-progress__bar" style="--wm-progress-value: 40%"></div>
914
+ </div>
915
+
916
+ Sizes: default 2px, --sm 1px, --lg 4px
917
+
918
+ Prefers-reduced-motion: indeterminate bar freezes at 30% width.
919
+ ============================================================ */
920
+
921
+ @keyframes wm-progress-slide {
922
+ 0% { transform: translateX(-100%) scaleX(0.25); }
923
+ 50% { transform: translateX(60%) scaleX(0.5); }
924
+ 100% { transform: translateX(400%) scaleX(0.25); }
925
+ }
926
+
927
+ .wm-progress {
928
+ position: relative;
929
+ width: 100%;
930
+ height: 2px;
931
+ background: var(--wm-border);
932
+ border-radius: var(--wm-radius-full);
933
+ overflow: hidden;
934
+ }
935
+
936
+ .wm-progress--sm { height: 1px; }
937
+ .wm-progress--lg { height: 4px; }
938
+
939
+ .wm-progress__bar {
940
+ position: absolute;
941
+ inset: 0;
942
+ background: var(--wm-color-accent);
943
+ border-radius: var(--wm-radius-full);
944
+ /* Determinate: width driven by --wm-progress-value (default 0%) */
945
+ width: var(--wm-progress-value, 0%);
946
+ transition: width 200ms ease;
947
+ transform-origin: left center;
948
+ }
949
+
950
+ .wm-progress__bar--indeterminate {
951
+ width: 40%;
952
+ animation: wm-progress-slide 1.4s ease-in-out infinite;
953
+ }
954
+
955
+ @media (prefers-reduced-motion: reduce) {
956
+ .wm-progress__bar--indeterminate {
957
+ animation: none;
958
+ width: 30%;
959
+ }
960
+ }
961
+
962
+
963
+ /* ============================================================
964
+ Circular progress — .wm-circle-progress
965
+ ============================================================
966
+ SVG-based ring. Both determinate and indeterminate modes.
967
+
968
+ The component is a <svg> element — use the mixin via a helper
969
+ element or build it inline.
970
+
971
+ Indeterminate:
972
+ <svg class="wm-circle-progress wm-circle-progress--indeterminate"
973
+ viewBox="0 0 36 36" role="progressbar" aria-label="Loading…">
974
+ <circle class="wm-circle-progress__track" cx="18" cy="18" r="15"/>
975
+ <circle class="wm-circle-progress__fill" cx="18" cy="18" r="15"/>
976
+ </svg>
977
+
978
+ Determinate (set --wm-circle-pct, 0–94.2 = 0–100%):
979
+ <svg class="wm-circle-progress"
980
+ viewBox="0 0 36 36" role="progressbar"
981
+ aria-valuenow="60" aria-valuemin="0" aria-valuemax="100"
982
+ aria-label="60% complete">
983
+ <circle class="wm-circle-progress__track" cx="18" cy="18" r="15"/>
984
+ <circle class="wm-circle-progress__fill" cx="18" cy="18" r="15"
985
+ style="--wm-circle-pct: 56.5"/>
986
+ </svg>
987
+
988
+ Sizes: default 36×36px, --sm 20px, --lg 56px
989
+ r=15 → circumference ≈ 94.25; --wm-circle-pct maps 0–94.25 to stroke progress.
990
+ ============================================================ */
991
+
992
+ @keyframes wm-circle-spin {
993
+ to { transform: rotate(360deg); }
994
+ }
995
+
996
+ @keyframes wm-circle-dash {
997
+ 0% { stroke-dashoffset: 94; }
998
+ 50% { stroke-dashoffset: 24; stroke-dasharray: 65 30; }
999
+ 100% { stroke-dashoffset: 94; }
1000
+ }
1001
+
1002
+ .wm-circle-progress {
1003
+ width: 36px;
1004
+ height: 36px;
1005
+ flex-shrink: 0;
1006
+ }
1007
+
1008
+ .wm-circle-progress--sm { width: 20px; height: 20px; }
1009
+ .wm-circle-progress--lg { width: 56px; height: 56px; }
1010
+
1011
+ .wm-circle-progress__track {
1012
+ fill: none;
1013
+ stroke: var(--wm-border);
1014
+ stroke-width: 3;
1015
+ }
1016
+
1017
+ .wm-circle-progress__fill {
1018
+ fill: none;
1019
+ stroke: var(--wm-color-accent);
1020
+ stroke-width: 3;
1021
+ stroke-linecap: round;
1022
+ /* Determinate: circumference=94.25; pct drives dashoffset */
1023
+ stroke-dasharray: 94.25;
1024
+ stroke-dashoffset: calc(94.25 - var(--wm-circle-pct, 0));
1025
+ transform: rotate(-90deg);
1026
+ transform-origin: 50% 50%;
1027
+ transition: stroke-dashoffset 300ms ease;
1028
+ }
1029
+
1030
+ .wm-circle-progress--indeterminate .wm-circle-progress__fill {
1031
+ stroke-dasharray: 60 34;
1032
+ stroke-dashoffset: 94;
1033
+ animation:
1034
+ wm-circle-spin 1.4s linear infinite,
1035
+ wm-circle-dash 1.4s ease-in-out infinite;
1036
+ transition: none;
1037
+ }
1038
+
1039
+ @media (prefers-reduced-motion: reduce) {
1040
+ .wm-circle-progress--indeterminate .wm-circle-progress__fill {
1041
+ animation: none;
1042
+ stroke-dasharray: 40 54;
1043
+ opacity: 0.5;
1044
+ }
1045
+ }
1046
+
1047
+
1048
+ /* ============================================================
1049
+ Button — .wm-btn
1050
+ ============================================================
1051
+ Base interactive button. Always pair with a variant modifier.
1052
+
1053
+ Variants: --primary (default), --secondary, --ghost, --destructive
1054
+ Sizes: --sm (28px), --md (36px, default), --lg (40px)
1055
+
1056
+ The focus ring on primary and destructive buttons uses a
1057
+ double-ring (canvas gap + colour ring) rather than the diffuse
1058
+ wm-focus-pulse, because those buttons have a solid fill that
1059
+ would visually merge with the pulse ring.
1060
+
1061
+ Usage:
1062
+ <button class="wm-btn wm-btn--primary">Save</button>
1063
+ <button class="wm-btn wm-btn--secondary wm-btn--sm">Cancel</button>
1064
+ <a class="wm-btn wm-btn--ghost" href="…">Back</a>
1065
+ ============================================================ */
1066
+
1067
+ .wm-btn {
1068
+ display: inline-flex;
1069
+ align-items: center;
1070
+ justify-content: center;
1071
+ gap: 6px;
1072
+ height: 36px;
1073
+ padding: 0 14px;
1074
+ font-size: 0.875rem;
1075
+ font-family: inherit;
1076
+ font-weight: 600;
1077
+ white-space: nowrap;
1078
+ line-height: 1;
1079
+ border: 1px solid transparent;
1080
+ border-radius: var(--wm-radius-md);
1081
+ cursor: pointer;
1082
+ outline: none;
1083
+ text-decoration: none;
1084
+ box-sizing: border-box;
1085
+ transition:
1086
+ background-color var(--wm-transition-fast),
1087
+ border-color var(--wm-transition-fast),
1088
+ color var(--wm-transition-fast),
1089
+ box-shadow var(--wm-transition-fast);
1090
+
1091
+ --tw-ring-shadow: none;
1092
+ --tw-ring-offset-shadow: none;
1093
+ --tw-ring-color: transparent;
1094
+ --tw-shadow: none;
1095
+ }
1096
+
1097
+ /* ── Variants ──────────────────────────────────────────────── */
1098
+
1099
+ .wm-btn--primary {
1100
+ background-color: var(--wm-color-accent);
1101
+ border-color: var(--wm-color-accent);
1102
+ color: var(--wm-color-accent-text);
1103
+ }
1104
+
1105
+ .wm-btn--primary:hover:not(:disabled) {
1106
+ background-color: var(--wm-color-accent-hover);
1107
+ border-color: var(--wm-color-accent-hover);
1108
+ }
1109
+
1110
+ .wm-btn--secondary {
1111
+ background-color: transparent;
1112
+ border-color: var(--wm-border-input);
1113
+ color: var(--wm-text-primary);
1114
+ }
1115
+
1116
+ .wm-btn--secondary:hover:not(:disabled) {
1117
+ background-color: var(--wm-bg-sunken);
1118
+ border-color: var(--wm-border-hover);
1119
+ }
1120
+
1121
+ .wm-btn--ghost {
1122
+ background-color: transparent;
1123
+ border-color: transparent;
1124
+ color: var(--wm-text-secondary);
1125
+ }
1126
+
1127
+ .wm-btn--ghost:hover:not(:disabled) {
1128
+ background-color: var(--wm-bg-hover);
1129
+ color: var(--wm-text-primary);
1130
+ }
1131
+
1132
+ .wm-btn--destructive {
1133
+ background-color: var(--wm-color-danger);
1134
+ border-color: var(--wm-color-danger);
1135
+ color: #ffffff;
1136
+ }
1137
+
1138
+ .wm-btn--destructive:hover:not(:disabled) {
1139
+ background-color: color-mix(in srgb, var(--wm-color-danger) 85%, black);
1140
+ border-color: color-mix(in srgb, var(--wm-color-danger) 85%, black);
1141
+ }
1142
+
1143
+ .wm-btn--flat {
1144
+ background-color: var(--wm-bg-foreground);
1145
+ border-color: transparent;
1146
+ color: var(--wm-text-primary);
1147
+ }
1148
+
1149
+ .wm-btn--flat:hover:not(:disabled) {
1150
+ background-color: var(--wm-bg-hover);
1151
+ }
1152
+
1153
+ /* ── Sizes ─────────────────────────────────────────────────── */
1154
+
1155
+ .wm-btn--xs {
1156
+ height: 22px;
1157
+ padding: 0 6px;
1158
+ font-size: 0.6875rem;
1159
+ border-radius: var(--wm-radius-xs);
1160
+ }
1161
+
1162
+ .wm-btn--sm {
1163
+ height: 28px;
1164
+ padding: 0 10px;
1165
+ font-size: 0.8125rem;
1166
+ border-radius: var(--wm-radius-sm);
1167
+ }
1168
+
1169
+ .wm-btn--lg {
1170
+ height: 40px;
1171
+ padding: 0 18px;
1172
+ font-size: 0.9375rem;
1173
+ }
1174
+
1175
+ /* ── Focus ─────────────────────────────────────────────────── */
1176
+
1177
+ /* Secondary and ghost: diffuse pulse (border changes from neutral → accent) */
1178
+ .wm-btn--secondary:focus-visible,
1179
+ .wm-btn--ghost:focus-visible,
1180
+ .wm-btn--flat:focus-visible {
1181
+ outline: none;
1182
+ border-color: var(--wm-color-accent);
1183
+ animation: wm-focus-pulse 400ms cubic-bezier(0.2, 0, 0.4, 1) forwards;
1184
+ }
1185
+
1186
+ /* Primary and destructive: double-ring so the ring contrasts against the fill */
1187
+ .wm-btn--primary:focus-visible {
1188
+ outline: none;
1189
+ box-shadow:
1190
+ 0 0 0 2px var(--wm-bg-base),
1191
+ 0 0 0 4px var(--wm-color-accent);
1192
+ }
1193
+
1194
+ .wm-btn--destructive:focus-visible {
1195
+ outline: none;
1196
+ box-shadow:
1197
+ 0 0 0 2px var(--wm-bg-base),
1198
+ 0 0 0 4px var(--wm-color-danger);
1199
+ }
1200
+
1201
+ /* ── States ────────────────────────────────────────────────── */
1202
+
1203
+ .wm-btn:disabled {
1204
+ opacity: 0.5;
1205
+ cursor: not-allowed;
1206
+ }
1207
+
1208
+ .wm-btn--loading {
1209
+ cursor: wait;
1210
+ pointer-events: none;
1211
+ opacity: 0.75;
1212
+ }
1213
+
1214
+ /* SVG icons inside buttons: inherit colour, fixed size */
1215
+ .wm-btn svg {
1216
+ flex-shrink: 0;
1217
+ width: 16px;
1218
+ height: 16px;
1219
+ }
1220
+
1221
+ .wm-btn--xs svg { width: 10px; height: 10px; }
1222
+ .wm-btn--sm svg { width: 14px; height: 14px; }
1223
+ .wm-btn--lg svg { width: 18px; height: 18px; }
1224
+
1225
+ /* Icon-only: square button, no padding, equal width/height */
1226
+ .btn-icon-only {
1227
+ padding: 0;
1228
+ width: 36px;
1229
+ }
1230
+ .wm-btn--xs.btn-icon-only { width: 22px; }
1231
+ .wm-btn--sm.btn-icon-only { width: 28px; }
1232
+ .wm-btn--lg.btn-icon-only { width: 40px; }
1233
+
1234
+ /* ── CSS Tooltip ───────────────────────────────────────────── */
1235
+ /* Apply data-tooltip="Label" to any element. Shows below by */
1236
+ /* default; flip with data-tooltip-pos="top" (see below). */
1237
+
1238
+ [data-tooltip] {
1239
+ position: relative;
1240
+ }
1241
+
1242
+ [data-tooltip]::after {
1243
+ content: attr(data-tooltip);
1244
+ position: absolute;
1245
+ top: calc(100% + 6px);
1246
+ left: 50%;
1247
+ transform: translateX(-50%) translateY(-2px);
1248
+ white-space: nowrap;
1249
+ padding: 3px 8px;
1250
+ background: var(--wm-text-primary);
1251
+ color: var(--wm-bg-base);
1252
+ font-family: var(--wm-font-sans);
1253
+ font-size: 11.5px;
1254
+ font-weight: 500;
1255
+ line-height: 1.4;
1256
+ border-radius: var(--wm-radius-sm);
1257
+ pointer-events: none;
1258
+ opacity: 0;
1259
+ transition:
1260
+ opacity var(--wm-transition-fast),
1261
+ transform var(--wm-transition-fast);
1262
+ z-index: 9000;
1263
+ }
1264
+
1265
+ [data-tooltip]:hover::after,
1266
+ [data-tooltip]:focus-visible::after {
1267
+ opacity: 1;
1268
+ transform: translateX(-50%) translateY(0);
1269
+ }
1270
+
1271
+ [data-tooltip-pos='top']::after {
1272
+ top: auto;
1273
+ bottom: calc(100% + 6px);
1274
+ transform: translateX(-50%) translateY(2px);
1275
+ }
1276
+
1277
+ [data-tooltip-pos='top']:hover::after,
1278
+ [data-tooltip-pos='top']:focus-visible::after {
1279
+ transform: translateX(-50%) translateY(0);
1280
+ }
1281
+
1282
+
1283
+ /* ============================================================
1284
+ Reduced motion
1285
+ ============================================================
1286
+ For users who have opted-in to reduced motion in their OS,
1287
+ all transitions and animations are replaced with instant
1288
+ equivalents. Focus states still show a visible ring — just
1289
+ without the expand-then-settle pulse.
1290
+ ============================================================ */
1291
+
1292
+ @media (prefers-reduced-motion: reduce) {
1293
+ .wm-input,
1294
+ .wm-input-group,
1295
+ .wm-select-trigger,
1296
+ .wm-number-btn,
1297
+ .wm-btn {
1298
+ transition: none;
1299
+ }
1300
+
1301
+ /* Replace the animated pulse with a static diffuse ring */
1302
+ .wm-input:focus,
1303
+ .wm-input-group:focus-within,
1304
+ .wm-select-trigger[aria-expanded='true'],
1305
+ .wm-select-trigger:focus-visible,
1306
+ .wm-btn--secondary:focus-visible,
1307
+ .wm-btn--ghost:focus-visible {
1308
+ animation: none;
1309
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--wm-color-accent) 25%, transparent);
1310
+ }
1311
+
1312
+ .wm-spinner {
1313
+ animation: none;
1314
+ opacity: 0.5;
1315
+ }
1316
+ }
1317
+
1318
+
1319
+ /* ============================================================
1320
+ Badge — item type labels
1321
+ Usage: <span class="wm-badge wm-badge--task">Task</span>
1322
+ ============================================================ */
1323
+
1324
+ .wm-badge {
1325
+ display: inline-flex;
1326
+ align-items: center;
1327
+ gap: 0.25rem;
1328
+ padding: 0.1875rem 0.5rem;
1329
+ border-radius: var(--wm-radius-sm);
1330
+ font-size: 0.6875rem;
1331
+ font-weight: 600;
1332
+ line-height: 1.4;
1333
+ white-space: nowrap;
1334
+ letter-spacing: 0.01em;
1335
+ }
1336
+
1337
+ .wm-badge--task { color: var(--wm-color-task); background: color-mix(in srgb, var(--wm-color-task) 12%, transparent); }
1338
+ .wm-badge--bug { color: var(--wm-color-bug); background: color-mix(in srgb, var(--wm-color-bug) 12%, transparent); }
1339
+ .wm-badge--story { color: var(--wm-color-story); background: color-mix(in srgb, var(--wm-color-story) 12%, transparent); }
1340
+ .wm-badge--epic { color: var(--wm-color-epic-text); background: var(--wm-color-epic); }
1341
+ .wm-badge--initiative { color: var(--wm-color-initiative-text); background: var(--wm-color-initiative); }
1342
+ .wm-badge--completed { color: var(--wm-color-completed); background: color-mix(in srgb, var(--wm-color-completed) 12%, transparent); }
1343
+
1344
+
1345
+ /* ============================================================
1346
+ Priority chip — urgency indicators
1347
+ Usage: <span class="wm-chip wm-chip--high">High</span>
1348
+ ============================================================ */
1349
+
1350
+ .wm-chip {
1351
+ display: inline-flex;
1352
+ align-items: center;
1353
+ gap: 0.25rem;
1354
+ padding: 0.1875rem 0.5rem;
1355
+ border-radius: var(--wm-radius-full);
1356
+ font-size: 0.6875rem;
1357
+ font-weight: 600;
1358
+ line-height: 1.4;
1359
+ white-space: nowrap;
1360
+ }
1361
+
1362
+ .wm-chip--critical { color: var(--wm-priority-critical-text); background: var(--wm-priority-critical-bg); }
1363
+ .wm-chip--high { color: var(--wm-priority-high-text); background: var(--wm-priority-high-bg); }
1364
+ .wm-chip--medium { color: var(--wm-priority-medium-text); background: var(--wm-priority-medium-bg); }
1365
+ .wm-chip--low { color: var(--wm-priority-low-text); background: var(--wm-priority-low-bg); }
1366
+ .wm-chip--none { color: var(--wm-text-muted); background: var(--wm-bg-sunken); }
1367
+
1368
+
1369
+ /* ============================================================
1370
+ Dialog footer utility
1371
+ Place inside [body] slot of <wm-dialog>:
1372
+ <div body>
1373
+ <p>Message</p>
1374
+ <div class="wm-dialog-footer">…buttons…</div>
1375
+ </div>
1376
+ ============================================================ */
1377
+
1378
+ .wm-dialog-footer {
1379
+ display: flex;
1380
+ justify-content: flex-end;
1381
+ gap: 0.5rem;
1382
+ padding-top: 1rem;
1383
+ margin-top: 1rem;
1384
+ border-top: 1px solid var(--wm-border);
1385
+ }
1386
+
1387
+
1388
+ /* ============================================================
1389
+ Action list — vertical list of full-width button actions
1390
+ Use for 4+ actions in narrow containers (splash, welcome, settings).
1391
+ Pair with a single primary CTA above the list.
1392
+
1393
+ Usage:
1394
+ <button class="wm-btn wm-btn--primary" style="width:100%">Primary action</button>
1395
+ <ul class="wm-action-list">
1396
+ <li><button class="wm-action-list-item">Action label</button></li>
1397
+ </ul>
1398
+ ============================================================ */
1399
+
1400
+ .wm-action-list {
1401
+ list-style: none;
1402
+ margin: 0.5rem 0 0;
1403
+ padding: 0;
1404
+ display: flex;
1405
+ flex-direction: column;
1406
+ gap: 2px;
1407
+ }
1408
+
1409
+ .wm-action-list-item {
1410
+ display: flex;
1411
+ align-items: center;
1412
+ gap: 0.5rem;
1413
+ width: 100%;
1414
+ padding: 0.5rem 0.75rem;
1415
+ background: transparent;
1416
+ border: none;
1417
+ border-radius: var(--wm-radius-md);
1418
+ font-size: 0.875rem;
1419
+ font-weight: 500;
1420
+ color: var(--wm-text-secondary);
1421
+ cursor: pointer;
1422
+ text-align: left;
1423
+ transition: background var(--wm-duration-fast) var(--wm-ease-standard),
1424
+ color var(--wm-duration-fast) var(--wm-ease-standard);
1425
+ }
1426
+
1427
+ .wm-action-list-item:hover {
1428
+ background: var(--wm-bg-hover);
1429
+ color: var(--wm-text-primary);
1430
+ }
1431
+
1432
+ .wm-action-list-item:focus-visible {
1433
+ outline: 2px solid var(--wm-color-accent);
1434
+ outline-offset: -1px;
1435
+ }
1436
+
1437
+ .wm-action-list-item svg {
1438
+ flex-shrink: 0;
1439
+ width: 16px;
1440
+ height: 16px;
1441
+ stroke: currentColor;
1442
+ }
1443
+
1444
+ @media (prefers-reduced-motion: reduce) {
1445
+ .wm-action-list-item { transition: none; }
1446
+ }
1447
+
1448
+
1449
+ /* ============================================================
1450
+ Keyboard key — .wm-kbd / .wm-kbd-combo
1451
+ ============================================================
1452
+ .wm-kbd renders a single key as a small badge resembling a
1453
+ physical keyboard key. The heavier bottom border simulates
1454
+ the key's lower edge depth.
1455
+
1456
+ .wm-kbd-combo wraps two or more adjacent .wm-kbd elements
1457
+ and injects a '+' connector between them via CSS so no extra
1458
+ markup is needed.
1459
+
1460
+ Usage — single key:
1461
+ <kbd class="wm-kbd">Esc</kbd>
1462
+
1463
+ Usage — key combination:
1464
+ <span class="wm-kbd-combo">
1465
+ <kbd class="wm-kbd">Ctrl</kbd>
1466
+ <kbd class="wm-kbd">K</kbd>
1467
+ </span>
1468
+
1469
+ Usage — longer combo:
1470
+ <span class="wm-kbd-combo">
1471
+ <kbd class="wm-kbd">Ctrl</kbd>
1472
+ <kbd class="wm-kbd">Shift</kbd>
1473
+ <kbd class="wm-kbd">P</kbd>
1474
+ </span>
1475
+ ============================================================ */
1476
+
1477
+ .wm-kbd {
1478
+ display: inline-flex;
1479
+ align-items: center;
1480
+ justify-content: center;
1481
+ padding: 1px 5px;
1482
+ min-width: 20px;
1483
+ height: 20px;
1484
+ box-sizing: border-box;
1485
+
1486
+ font-family: var(--wm-font-sans);
1487
+ font-size: 11px;
1488
+ font-weight: 500;
1489
+ font-style: normal;
1490
+ line-height: 1;
1491
+ white-space: nowrap;
1492
+
1493
+ color: var(--wm-text-secondary);
1494
+ background: var(--wm-bg-sunken);
1495
+
1496
+ /* Key face: uniform border on top/sides, heavier on bottom for depth */
1497
+ border: 1px solid var(--wm-border-input);
1498
+ border-bottom-width: 2px;
1499
+ border-radius: var(--wm-radius-xs);
1500
+
1501
+ user-select: none;
1502
+ pointer-events: none;
1503
+ }
1504
+
1505
+
1506
+ /* ----------------------------------------------------------------
1507
+ Combo wrapper — auto-injects '+' between adjacent .wm-kbd
1508
+ ----------------------------------------------------------------
1509
+ Each .wm-kbd after the first gets a left margin to create room
1510
+ for the generated '+' connector, which is positioned absolutely
1511
+ in the gap between the two keys.
1512
+ ---------------------------------------------------------------- */
1513
+
1514
+ .wm-kbd-combo {
1515
+ display: inline-flex;
1516
+ align-items: center;
1517
+ }
1518
+
1519
+ .wm-kbd-combo > .wm-kbd + .wm-kbd {
1520
+ position: relative;
1521
+ margin-left: 14px;
1522
+ }
1523
+
1524
+ .wm-kbd-combo > .wm-kbd + .wm-kbd::before {
1525
+ content: '+';
1526
+ position: absolute;
1527
+ /* sit in the 14px gap: 4px from the right edge of this key's left edge */
1528
+ right: calc(100% + 4px);
1529
+ top: 50%;
1530
+ transform: translateY(-50%);
1531
+ font-size: 9px;
1532
+ font-weight: 400;
1533
+ line-height: 1;
1534
+ font-family: var(--wm-font-sans);
1535
+ color: var(--wm-text-muted);
1536
+ pointer-events: none;
1537
+ }