@webority/theme 0.3.0 → 0.4.0

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.
@@ -0,0 +1,808 @@
1
+ // Checkbox & radio, field/input, status badge, choice chip.
2
+ //
3
+ // Part of the wui-components layer — see _wui-components.scss for the whole
4
+ // picture and the load order. Order within the layer is LOAD-BEARING: these
5
+ // partials are a split of one stylesheet, not independent modules, so a rule
6
+ // here may be overridden by a later partial. Never reorder the imports.
7
+
8
+ // ── Checkbox & Radio ──
9
+ // Selection controls: a 20px glyph centred in a 36px circular hit area
10
+ // that shows a soft primary-tinted ripple on hover. Unchecked = muted
11
+ // grey-blue outline; checked = solid primary. Checkbox = squircle box + white
12
+ // tick; radio = ring + filled centre dot. Pure CSS, no JS — drives the native
13
+ // input via `appearance: none`, so it stays keyboard- and screen-reader-
14
+ // accessible.
15
+ //
16
+ // Markup:
17
+ // <label class="lx-control">
18
+ // <input type="checkbox" class="lx-check"> (or type="radio" class="lx-radio")
19
+ // <span class="lx-control-label">Full-time</span>
20
+ // </label>
21
+ // Group rows in a `.lx-control-group` (horizontal, wraps) or stack them.
22
+ .lx-control {
23
+ display: inline-flex;
24
+ align-items: center;
25
+ gap: 6px;
26
+ cursor: pointer;
27
+ user-select: none;
28
+ font-size: 14px;
29
+ color: var(--color-text);
30
+ line-height: 1.4;
31
+ }
32
+ .lx-control-group {
33
+ display: flex;
34
+ flex-wrap: wrap;
35
+ align-items: center;
36
+ gap: 8px 20px;
37
+ }
38
+ .lx-control-label {
39
+ padding-right: 2px;
40
+ }
41
+
42
+ // The control glyph: 20px box inside a 36px circular tap target (8px ring).
43
+ .lx-check,
44
+ .lx-radio {
45
+ appearance: none;
46
+ -webkit-appearance: none;
47
+ flex: none;
48
+ box-sizing: border-box;
49
+ width: 20px;
50
+ height: 20px;
51
+ // 8px transparent ring → 36px hit area + room for the hover ripple
52
+ margin: 8px;
53
+ position: relative;
54
+ border: 1.6px solid var(--color-border-strong);
55
+ background: transparent;
56
+ cursor: pointer;
57
+ transition:
58
+ border-color 0.15s ease,
59
+ background-color 0.15s ease;
60
+ }
61
+ .lx-check {
62
+ border-radius: var(--radius-sm);
63
+ corner-shape: squircle;
64
+ }
65
+ .lx-radio {
66
+ border-radius: 50%;
67
+ }
68
+
69
+ // Soft primary circular ripple behind the glyph on hover (the Minimals halo).
70
+ .lx-check::after,
71
+ .lx-radio::after {
72
+ content: "";
73
+ position: absolute;
74
+ left: 50%;
75
+ top: 50%;
76
+ width: 36px;
77
+ height: 36px;
78
+ border-radius: 50%;
79
+ transform: translate(-50%, -50%) scale(0.5);
80
+ background: var(--color-primary-soft);
81
+ opacity: 0;
82
+ z-index: -1;
83
+ pointer-events: none;
84
+ transition:
85
+ opacity 0.15s ease,
86
+ transform 0.15s ease;
87
+ }
88
+ .lx-control:hover .lx-check::after,
89
+ .lx-control:hover .lx-radio::after,
90
+ .lx-check:hover::after,
91
+ .lx-radio:hover::after {
92
+ opacity: 1;
93
+ transform: translate(-50%, -50%) scale(1);
94
+ }
95
+
96
+ // Checked — primary fill (checkbox) / primary ring + dot (radio)
97
+ .lx-check:checked {
98
+ border-color: var(--color-primary);
99
+ background: var(--color-primary);
100
+ // white tick drawn with a rotated box-shadow-free pseudo
101
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7'/%3E%3C/svg%3E");
102
+ background-size: 15px 15px;
103
+ background-position: center;
104
+ background-repeat: no-repeat;
105
+ }
106
+ .lx-radio:checked {
107
+ border-color: var(--color-primary);
108
+ }
109
+ .lx-radio:checked::before {
110
+ content: "";
111
+ position: absolute;
112
+ left: 50%;
113
+ top: 50%;
114
+ width: 10px;
115
+ height: 10px;
116
+ border-radius: 50%;
117
+ background: var(--color-primary);
118
+ transform: translate(-50%, -50%);
119
+ }
120
+
121
+ // Keyboard focus ring (only on focus-visible, not mouse)
122
+ .lx-check:focus-visible,
123
+ .lx-radio:focus-visible {
124
+ outline: none;
125
+ box-shadow: 0 0 0 3px var(--color-primary-soft);
126
+ }
127
+
128
+ // Disabled
129
+ .lx-check:disabled,
130
+ .lx-radio:disabled,
131
+ .lx-control:has(:disabled) {
132
+ cursor: not-allowed;
133
+ }
134
+ .lx-check:disabled,
135
+ .lx-radio:disabled {
136
+ border-color: var(--color-border);
137
+ background-color: var(--color-bg-muted);
138
+ }
139
+ .lx-check:disabled:checked,
140
+ .lx-radio:disabled:checked {
141
+ border-color: var(--color-border-strong);
142
+ }
143
+ .lx-check:disabled:checked {
144
+ background-color: var(--color-border-strong);
145
+ }
146
+ .lx-radio:disabled:checked::before {
147
+ background: var(--color-border-strong);
148
+ }
149
+ .lx-control:has(:disabled) {
150
+ color: var(--color-text-subtle);
151
+ }
152
+ .lx-check:disabled::after,
153
+ .lx-radio:disabled::after {
154
+ display: none;
155
+ }
156
+
157
+ @media (prefers-reduced-motion: reduce) {
158
+ .lx-check,
159
+ .lx-radio,
160
+ .lx-check::after,
161
+ .lx-radio::after {
162
+ transition: none;
163
+ }
164
+ }
165
+
166
+ // Row-selection checkbox variant used inside a .table-ledger select column.
167
+ .lx-check {
168
+ width: 16px;
169
+ height: 16px;
170
+ accent-color: var(--color-primary);
171
+ cursor: pointer;
172
+ vertical-align: middle;
173
+ }
174
+
175
+ // ── Field / Input ──
176
+ // Inputs — always-white fill, 16px squircle, soft brand border tint on focus
177
+ // (no glow).
178
+ .input,
179
+ .select,
180
+ .textarea {
181
+ box-sizing: border-box;
182
+ width: 100%;
183
+ height: var(--wui-control-height);
184
+ padding: 12px;
185
+ border-radius: var(--radius);
186
+ border: 1px solid var(--color-border);
187
+ background: var(--color-surface);
188
+ font-size: 14px;
189
+ line-height: 1.5;
190
+ color: var(--color-text);
191
+ transition:
192
+ border-color 0.12s,
193
+ box-shadow 0.12s;
194
+ font-family: inherit;
195
+ }
196
+ .textarea {
197
+ height: auto;
198
+ min-height: 120px;
199
+ padding: 12px 15px 16px;
200
+ line-height: 1.5;
201
+ resize: vertical;
202
+ }
203
+ .input:hover:not(:focus):not(:disabled),
204
+ .select:hover:not(:focus):not(:disabled),
205
+ .textarea:hover:not(:focus):not(:disabled) {
206
+ background-color: var(--color-bg-muted);
207
+ border-color: var(--color-border-strong);
208
+ }
209
+ .input:focus,
210
+ .select:focus,
211
+ .textarea:focus {
212
+ outline: none;
213
+ background-color: var(--color-bg-muted);
214
+ color: var(--color-text);
215
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
216
+ box-shadow: none;
217
+ }
218
+ // Textarea renders as .form-control.textarea (react-bootstrap). Match the
219
+ // shared theme's .form-control:hover specificity so it hovers to
220
+ // --color-border-strong too.
221
+ textarea.textarea.form-control:hover:not(:focus):not(:disabled):not([readonly]) {
222
+ border-color: var(--color-border-strong);
223
+ }
224
+ textarea.textarea:focus {
225
+ outline: none;
226
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
227
+ box-shadow: none;
228
+ }
229
+ .input::placeholder,
230
+ .textarea::placeholder {
231
+ color: var(--color-text-subtle);
232
+ }
233
+ // Edge/IE add a native password reveal (and clear) button — hide them so only
234
+ // the app's own eye toggle shows.
235
+ .input::-ms-reveal,
236
+ .input::-ms-clear {
237
+ display: none;
238
+ }
239
+ .input:disabled,
240
+ .select:disabled,
241
+ .textarea:disabled {
242
+ background: var(--color-bg-muted);
243
+ border-color: var(--color-border);
244
+ color: var(--color-text-subtle);
245
+ cursor: not-allowed;
246
+ }
247
+ .field:has(.input:disabled) .label {
248
+ color: var(--color-text-subtle);
249
+ }
250
+
251
+ // Read-only is NOT disabled, and the two must not look alike: a read-only value is
252
+ // still submitted, still selectable and still copyable, so a `not-allowed` cursor
253
+ // would lie about it. Only the surface says "not yours to edit".
254
+ //
255
+ // Match on `[readonly]`, never the `:read-only` PSEUDO-class — that also matches a
256
+ // DISABLED input (and any non-editable element), so a `:read-only` rule silently
257
+ // repaints disabled fields too.
258
+ .input[readonly],
259
+ .select[readonly],
260
+ .textarea[readonly] {
261
+ background: var(--color-bg-subtle);
262
+ color: var(--color-text-muted);
263
+ cursor: default;
264
+ }
265
+
266
+ // Invalid field state (required-field validation) — soft danger-tinted
267
+ // border on the surface.
268
+ // `[aria-invalid="true"]` is styled alongside `.is-invalid`: React sets the ARIA
269
+ // attribute (it is the accessible source of truth) and does not always add the
270
+ // Bootstrap class, so keying on the class alone left React error states unstyled.
271
+ .input.is-invalid,
272
+ .select.is-invalid,
273
+ .textarea.is-invalid,
274
+ .input.is-invalid:focus,
275
+ .select.is-invalid:focus,
276
+ .textarea.is-invalid:focus,
277
+ .input[aria-invalid="true"],
278
+ .select[aria-invalid="true"],
279
+ .textarea[aria-invalid="true"],
280
+ .input[aria-invalid="true"]:focus,
281
+ .select[aria-invalid="true"]:focus,
282
+ .textarea[aria-invalid="true"]:focus {
283
+ background: var(--color-surface);
284
+ border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-surface));
285
+ box-shadow: none;
286
+ }
287
+ .input.is-invalid,
288
+ .input[aria-invalid="true"] {
289
+ padding-right: 41px;
290
+ }
291
+ .field-error {
292
+ font-size: 12px;
293
+ line-height: normal;
294
+ color: var(--color-danger);
295
+ }
296
+
297
+ // Native select with a rounded squircle, custom chevron, off-surface fill.
298
+ .select {
299
+ appearance: none;
300
+ -webkit-appearance: none;
301
+ padding-right: 40px;
302
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19.92 8.95l-6.52 6.52c-.77.77-2.03.77-2.8 0L4.08 8.95' fill='none' stroke='%234B5675' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10'/%3E%3C/svg%3E");
303
+ background-repeat: no-repeat;
304
+ background-position: right 14px center;
305
+ background-size: 16px;
306
+ cursor: pointer;
307
+ }
308
+
309
+ // Date inputs — a brand calendar glyph that fills the field so a click
310
+ // anywhere opens the native picker; muted text until a value is chosen.
311
+ .input[type="date"] {
312
+ position: relative;
313
+ cursor: pointer;
314
+ }
315
+ .input[type="date"]:not(.has-value):not(:focus) {
316
+ color: var(--color-text-subtle);
317
+ }
318
+ .input[type="date"]::-webkit-calendar-picker-indicator {
319
+ position: absolute;
320
+ inset: 0;
321
+ width: auto;
322
+ height: auto;
323
+ margin: 0;
324
+ padding: 0;
325
+ cursor: pointer;
326
+ opacity: 0;
327
+ }
328
+ .input[type="date"]::-webkit-datetime-edit {
329
+ padding-right: 28px;
330
+ }
331
+ .input-date-wrap {
332
+ position: relative;
333
+ }
334
+
335
+ // Number input: hide the native spinner so a custom stepper can be rendered.
336
+ .input[type="number"]::-webkit-inner-spin-button,
337
+ .input[type="number"]::-webkit-outer-spin-button {
338
+ -webkit-appearance: none;
339
+ margin: 0;
340
+ }
341
+ .input[type="number"] {
342
+ -moz-appearance: textfield;
343
+ appearance: textfield;
344
+ }
345
+
346
+ // Custom number stepper — up/down arrows, no background.
347
+ .num-stepper {
348
+ position: relative;
349
+ width: 100%;
350
+ }
351
+ .field > .num-stepper {
352
+ align-self: stretch;
353
+ }
354
+ .num-stepper .input {
355
+ width: 100%;
356
+ padding-right: 36px;
357
+ }
358
+ .num-stepper-btns {
359
+ position: absolute;
360
+ top: 50%;
361
+ right: 8px;
362
+ transform: translateY(-50%);
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: 0;
366
+ }
367
+ .num-stepper-btns button {
368
+ display: grid;
369
+ place-items: center;
370
+ width: 20px;
371
+ height: 15px;
372
+ border: none;
373
+ background: transparent;
374
+ padding: 0;
375
+ cursor: pointer;
376
+ color: var(--color-text-muted);
377
+ }
378
+ .num-stepper-btns button:hover {
379
+ color: var(--color-text);
380
+ }
381
+ .num-stepper-btns button:active {
382
+ color: var(--color-primary);
383
+ }
384
+ .input-date-wrap > .input {
385
+ padding-right: 38px;
386
+ }
387
+ .input-date-icon {
388
+ position: absolute;
389
+ right: 12px;
390
+ top: 50%;
391
+ transform: translateY(-50%);
392
+ color: var(--color-text-muted);
393
+ pointer-events: none;
394
+ }
395
+
396
+ // Error state — danger-tinted border + room for a trailing affix.
397
+ .input.error,
398
+ .input.error:focus,
399
+ .textarea.error,
400
+ .textarea.error:focus {
401
+ background: var(--color-surface);
402
+ border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-surface));
403
+ box-shadow: none;
404
+ }
405
+ .input.error {
406
+ padding-right: 41px;
407
+ }
408
+ // Dark mode: keep the value in an errored field legible.
409
+ :root[data-theme="dark"] .input.error,
410
+ :root[data-theme="dark"] .input.error:focus,
411
+ :root[data-theme="dark"] .input.error:-webkit-autofill,
412
+ :root[data-theme="dark"] .input.error:focus:-webkit-autofill {
413
+ color: var(--color-text);
414
+ -webkit-text-fill-color: var(--color-text);
415
+ }
416
+
417
+ // Field-relative wrap for trailing affixes (error icon, password toggle).
418
+ .input-wrap {
419
+ position: relative;
420
+ width: 100%;
421
+ align-self: stretch;
422
+ }
423
+ .input-wrap .input {
424
+ width: 100%;
425
+ }
426
+ .input-affix {
427
+ position: absolute;
428
+ right: 14px;
429
+ top: 22.5px;
430
+ transform: translateY(-50%);
431
+ display: grid;
432
+ place-items: center;
433
+ pointer-events: none;
434
+ color: var(--color-danger);
435
+ }
436
+
437
+ // Leading-icon input — icon turns brand on focus.
438
+ .input-icon {
439
+ position: relative;
440
+ display: flex;
441
+ align-items: center;
442
+ width: 100%;
443
+ }
444
+ .input-icon > .ic {
445
+ position: absolute;
446
+ left: 13px;
447
+ top: 50%;
448
+ transform: translateY(-50%);
449
+ display: grid;
450
+ place-items: center;
451
+ color: var(--color-text-subtle);
452
+ pointer-events: none;
453
+ }
454
+ .input-icon .input {
455
+ padding-left: 40px;
456
+ }
457
+ .input-icon:focus-within > .ic {
458
+ color: var(--color-primary);
459
+ }
460
+
461
+ // Password reveal toggle (place inside .input-wrap).
462
+ .input.pw {
463
+ padding-right: 44px;
464
+ }
465
+ .input.pw[type="password"]::placeholder {
466
+ letter-spacing: normal;
467
+ color: var(--color-text-subtle);
468
+ }
469
+ .pw-toggle {
470
+ position: absolute;
471
+ right: 7px;
472
+ top: 22px;
473
+ transform: translateY(-50%);
474
+ display: grid;
475
+ place-items: center;
476
+ width: 32px;
477
+ height: 32px;
478
+ border: none;
479
+ background: transparent;
480
+ color: var(--color-text-subtle);
481
+ border-radius: var(--radius);
482
+ cursor: pointer;
483
+ transition:
484
+ color 0.12s ease,
485
+ background 0.12s ease;
486
+ }
487
+ .pw-toggle:hover {
488
+ color: var(--color-primary);
489
+ background: transparent;
490
+ }
491
+
492
+ .hint,
493
+ .field-hint {
494
+ margin-top: 2px;
495
+ font-size: 12px;
496
+ color: var(--color-text-subtle);
497
+ }
498
+ .label {
499
+ display: block;
500
+ font-size: 14px;
501
+ line-height: normal;
502
+ font-weight: 500;
503
+ color: var(--color-text);
504
+ margin-bottom: 6px;
505
+ }
506
+ .label-hint {
507
+ color: var(--color-text-subtle);
508
+ font-weight: 400;
509
+ margin-left: 6px;
510
+ font-size: 12px;
511
+ }
512
+ .label-optional {
513
+ font-weight: 400;
514
+ font-size: 14px;
515
+ color: var(--color-text-subtle);
516
+ }
517
+ .required {
518
+ color: var(--color-danger);
519
+ // The gap is CSS, not a significant space in the markup. RequiredMark used to render
520
+ // `{' '}*` while AppDatePicker and every Razor helper rendered a bare `*`, so the same
521
+ // asterisk sat 4px further out in some fields than others — and no check could see it,
522
+ // because HTML comparison collapses whitespace by design.
523
+ margin-inline-start: 0.25em;
524
+ }
525
+ .field {
526
+ display: flex;
527
+ flex-direction: column;
528
+ align-items: flex-start;
529
+ gap: 6px;
530
+ margin-bottom: 16px;
531
+ }
532
+ .field > .label,
533
+ .field label {
534
+ margin-bottom: 0;
535
+ }
536
+ .field-row {
537
+ display: grid;
538
+ grid-template-columns: 1fr 1fr;
539
+ gap: 16px;
540
+ margin-bottom: 16px;
541
+ }
542
+ .field-row-3 {
543
+ grid-template-columns: 1fr 1fr 1fr;
544
+ }
545
+ // min-width:0 — grid/flex items default to min-width:auto, which lets a long
546
+ // control (e.g. a search-select label) push its track past 1fr and defeat
547
+ // text truncation. Reset to 0 so the ellipsis engages instead of overflowing.
548
+ .field-row .field {
549
+ margin-bottom: 0;
550
+ min-width: 0;
551
+ }
552
+ // Controls fill their field even though .field aligns children flex-start.
553
+ .field > .search-select,
554
+ .field > .input-with-unit,
555
+ .field > .input,
556
+ .field > .datepicker,
557
+ .field > .field-error {
558
+ align-self: stretch;
559
+ }
560
+ // Doubled selectors — see the lg-tier block below: a media query carries no
561
+ // extra specificity, so a consumer's later single-class rule would win.
562
+ @media (max-width: 575.98px) {
563
+ .field-row.field-row,
564
+ .field-row-3.field-row-3 {
565
+ grid-template-columns: 1fr;
566
+ }
567
+ }
568
+
569
+ // red border around a non-input group (e.g. a structure-form card row)
570
+ .group-invalid {
571
+ border: 1px solid var(--color-danger);
572
+ border-radius: var(--radius);
573
+ padding: 8px;
574
+ }
575
+
576
+ // One field, one border: the WRAPPER owns the border + hover/focus/error ring
577
+ // so the numeric input and its trailing unit pill light up together.
578
+ .input-with-unit {
579
+ display: flex;
580
+ align-items: stretch;
581
+ border: 1px solid var(--color-border);
582
+ border-radius: var(--radius-lg);
583
+ corner-shape: squircle;
584
+ background: var(--color-surface);
585
+ transition:
586
+ border-color 0.12s,
587
+ background-color 0.12s;
588
+ overflow: hidden;
589
+ }
590
+ .input-with-unit:hover:not(:focus-within) {
591
+ background: var(--color-bg-muted);
592
+ border-color: var(--color-border-strong);
593
+ }
594
+ .input-with-unit:focus-within {
595
+ background: var(--color-bg-muted);
596
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
597
+ }
598
+ .input-with-unit:has(.num-input.error) {
599
+ border-color: var(--color-danger);
600
+ }
601
+
602
+ // Numeric inputs are plain text fields — no native spinner, no stepper. The
603
+ // field flows seamlessly into the trailing unit pill; the wrapper carries the
604
+ // border.
605
+ .input-with-unit .num-input {
606
+ border: none;
607
+ border-radius: 0;
608
+ background: transparent;
609
+ }
610
+ .input-with-unit .num-input:focus {
611
+ box-shadow: none;
612
+ }
613
+ .input-with-unit .unit {
614
+ background: var(--color-bg-muted);
615
+ border: none;
616
+ border-left: 1px solid var(--color-border);
617
+ padding: 9px 14px;
618
+ font-size: 14px;
619
+ font-weight: 500;
620
+ color: var(--color-text-muted);
621
+ font-family: "Inter", sans-serif;
622
+ display: inline-flex;
623
+ align-items: center;
624
+ white-space: nowrap;
625
+ }
626
+
627
+ // ── Status badge ──
628
+ .badge {
629
+ display: inline-flex;
630
+ align-items: center;
631
+ gap: 4px;
632
+ padding: 3px 9px;
633
+ border-radius: 999px;
634
+ font-size: 12px;
635
+ font-weight: 500;
636
+ background: var(--color-bg-muted);
637
+ color: var(--color-text-muted);
638
+ border: 1px solid var(--color-border);
639
+ }
640
+ .badge-primary {
641
+ background: var(--color-primary-soft);
642
+ color: var(--color-primary);
643
+ border-color: color-mix(in srgb, var(--color-primary) 25%, transparent);
644
+ }
645
+ .badge-success {
646
+ background: var(--color-success-soft);
647
+ color: var(--color-success-strong);
648
+ border-color: transparent;
649
+ }
650
+ .badge-warn {
651
+ background: var(--color-warning-soft);
652
+ color: var(--color-warning-strong);
653
+ border-color: transparent;
654
+ }
655
+ .badge-danger {
656
+ background: var(--color-danger-soft);
657
+ color: var(--color-danger-strong);
658
+ border-color: transparent;
659
+ }
660
+ .badge-info {
661
+ background: var(--color-info-soft);
662
+ color: var(--color-info-strong);
663
+ border-color: transparent;
664
+ }
665
+ .badge-dot::before {
666
+ content: "";
667
+ width: 6px;
668
+ height: 6px;
669
+ border-radius: 999px;
670
+ background: currentColor;
671
+ }
672
+
673
+ // "Soft" status badge — pill, 12/600, muted tone fill.
674
+ .status-label {
675
+ display: inline-flex;
676
+ align-items: center;
677
+ gap: 5px;
678
+ padding: 0 10px;
679
+ height: 24px;
680
+ border-radius: 999px;
681
+ font-size: 12px;
682
+ font-weight: 600;
683
+ line-height: 1;
684
+ white-space: nowrap;
685
+ }
686
+ // Tones are SEMANTIC — success / warning / danger / info / primary / neutral —
687
+ // the same vocabulary .chip, .badge and every other tone-bearing component uses.
688
+ // A semantic name survives a palette change; a colour name does not (a "green"
689
+ // tone is a lie the moment the brand's success colour stops being green).
690
+ //
691
+ // Two other vocabularies existed and are accepted as DEPRECATED ALIASES so a
692
+ // consumer can migrate without a flag day:
693
+ // - colour-named .tone-green / -amber / -red / -blue / -grey (was the library's)
694
+ // - short semantic .tone-ok / -warn / -bad (was lumex's)
695
+ // Author new markup with the semantic names; the aliases will be removed.
696
+ .status-label.success,
697
+ .status-label.tone-ok,
698
+ .status-label.tone-green {
699
+ background: color-mix(in srgb, var(--color-success) 16%, transparent);
700
+ color: var(--color-success-strong);
701
+ }
702
+ .status-label.warning,
703
+ .status-label.tone-warn,
704
+ .status-label.tone-amber {
705
+ background: color-mix(in srgb, var(--color-warning) 16%, transparent);
706
+ color: var(--color-warning-strong);
707
+ }
708
+ .status-label.danger,
709
+ .status-label.tone-bad,
710
+ .status-label.tone-red {
711
+ background: color-mix(in srgb, var(--color-danger) 16%, transparent);
712
+ color: var(--color-danger-strong);
713
+ }
714
+ .status-label.info,
715
+ .status-label.tone-blue {
716
+ background: color-mix(in srgb, var(--color-info) 16%, transparent);
717
+ color: var(--color-info-strong);
718
+ }
719
+ .status-label.primary {
720
+ background: var(--color-primary-soft);
721
+ color: var(--color-primary);
722
+ }
723
+ // Default/neutral — also the bare `.status-label` look, so an unknown or omitted
724
+ // tone still renders as a proper pill instead of transparent text.
725
+ .status-label,
726
+ .status-label.neutral,
727
+ .status-label.tone-grey {
728
+ background: color-mix(in srgb, var(--color-text-subtle) 16%, transparent);
729
+ color: var(--color-text-subtle);
730
+ }
731
+
732
+ // ── Choice chip ──
733
+ // Inline selectable chip — native control hidden, pill adopts the primary
734
+ // accent when checked.
735
+ .choice-chip {
736
+ display: inline-flex;
737
+ align-items: center;
738
+ gap: 8px;
739
+ padding: 9px 14px;
740
+ border: 1.5px solid var(--color-border);
741
+ border-radius: 999px;
742
+ font-size: 14px;
743
+ font-weight: 500;
744
+ color: var(--color-text);
745
+ background: var(--color-surface);
746
+ cursor: pointer;
747
+ user-select: none;
748
+ transition:
749
+ border-color 0.12s ease,
750
+ background 0.12s ease,
751
+ color 0.12s ease;
752
+ }
753
+ .choice-chip:hover {
754
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
755
+ }
756
+ .choice-chip input {
757
+ position: absolute;
758
+ opacity: 0;
759
+ pointer-events: none;
760
+ }
761
+ .choice-chip.checked {
762
+ border-color: var(--color-primary);
763
+ background: var(--color-primary-soft);
764
+ color: var(--color-primary);
765
+ }
766
+
767
+ // A count badge inside a selectable chip — "Active 12". Inverts on selection for
768
+ // the same reason the nav badge does: the chip already carries the soft tint, so a
769
+ // tinted badge on top of it would disappear.
770
+ .choice-chip .count {
771
+ padding: 1px 6px;
772
+ border-radius: 999px;
773
+ background: var(--color-bg-muted);
774
+ font-size: 11px;
775
+ font-variant-numeric: tabular-nums;
776
+ color: var(--color-text-subtle);
777
+ }
778
+ .choice-chip.checked .count {
779
+ background: var(--color-surface);
780
+ color: var(--color-primary);
781
+ }
782
+
783
+ // Form section heading — a branded accent bar rather than a full-width hairline,
784
+ // so a long form reads as designed sections instead of raw rules.
785
+ //
786
+ // Block, not flex: an inline subscript in the label (N(SG)) would otherwise be
787
+ // split out as a separate flex item and gain a gap.
788
+ .fld-section {
789
+ margin: 30px 0 10px;
790
+ font-size: 13px;
791
+ font-weight: 700;
792
+ text-transform: uppercase;
793
+ letter-spacing: 0.05em;
794
+ color: var(--color-text-muted);
795
+ }
796
+ .fld-section::before {
797
+ content: "";
798
+ display: inline-block;
799
+ width: 3px;
800
+ height: 12px;
801
+ margin-right: 9px;
802
+ border-radius: 2px;
803
+ background: var(--color-primary);
804
+ vertical-align: middle;
805
+ }
806
+ .fld-section:first-child {
807
+ margin-top: 0;
808
+ }