@webority/theme 0.3.0 → 0.5.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.
@@ -14,3728 +14,24 @@
14
14
  //
15
15
  // Plain CSS is valid SCSS; nothing here has been re-nested or renamed.
16
16
  // ============================================================================
17
-
18
- // ── Checkbox & Radio ──
19
- // Selection controls: a 20px glyph centred in a 36px circular hit area
20
- // that shows a soft primary-tinted ripple on hover. Unchecked = muted
21
- // grey-blue outline; checked = solid primary. Checkbox = squircle box + white
22
- // tick; radio = ring + filled centre dot. Pure CSS, no JS — drives the native
23
- // input via `appearance: none`, so it stays keyboard- and screen-reader-
24
- // accessible.
25
17
  //
26
- // Markup:
27
- // <label class="lx-control">
28
- // <input type="checkbox" class="lx-check"> (or type="radio" class="lx-radio")
29
- // <span class="lx-control-label">Full-time</span>
30
- // </label>
31
- // Group rows in a `.lx-control-group` (horizontal, wraps) or stack them.
32
- .lx-control {
33
- display: inline-flex;
34
- align-items: center;
35
- gap: 6px;
36
- cursor: pointer;
37
- user-select: none;
38
- font-size: 14px;
39
- color: var(--color-text);
40
- line-height: 1.4;
41
- }
42
- .lx-control-group {
43
- display: flex;
44
- flex-wrap: wrap;
45
- align-items: center;
46
- gap: 8px 20px;
47
- }
48
- .lx-control-label {
49
- padding-right: 2px;
50
- }
51
-
52
- // The control glyph: 20px box inside a 36px circular tap target (8px ring).
53
- .lx-check,
54
- .lx-radio {
55
- appearance: none;
56
- -webkit-appearance: none;
57
- flex: none;
58
- box-sizing: border-box;
59
- width: 20px;
60
- height: 20px;
61
- // 8px transparent ring → 36px hit area + room for the hover ripple
62
- margin: 8px;
63
- position: relative;
64
- border: 1.6px solid var(--color-border-strong);
65
- background: transparent;
66
- cursor: pointer;
67
- transition:
68
- border-color 0.15s ease,
69
- background-color 0.15s ease;
70
- }
71
- .lx-check {
72
- border-radius: var(--radius-sm);
73
- corner-shape: squircle;
74
- }
75
- .lx-radio {
76
- border-radius: 50%;
77
- }
78
-
79
- // Soft primary circular ripple behind the glyph on hover (the Minimals halo).
80
- .lx-check::after,
81
- .lx-radio::after {
82
- content: "";
83
- position: absolute;
84
- left: 50%;
85
- top: 50%;
86
- width: 36px;
87
- height: 36px;
88
- border-radius: 50%;
89
- transform: translate(-50%, -50%) scale(0.5);
90
- background: var(--color-primary-soft);
91
- opacity: 0;
92
- z-index: -1;
93
- pointer-events: none;
94
- transition:
95
- opacity 0.15s ease,
96
- transform 0.15s ease;
97
- }
98
- .lx-control:hover .lx-check::after,
99
- .lx-control:hover .lx-radio::after,
100
- .lx-check:hover::after,
101
- .lx-radio:hover::after {
102
- opacity: 1;
103
- transform: translate(-50%, -50%) scale(1);
104
- }
105
-
106
- // Checked — primary fill (checkbox) / primary ring + dot (radio)
107
- .lx-check:checked {
108
- border-color: var(--color-primary);
109
- background: var(--color-primary);
110
- // white tick drawn with a rotated box-shadow-free pseudo
111
- 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");
112
- background-size: 15px 15px;
113
- background-position: center;
114
- background-repeat: no-repeat;
115
- }
116
- .lx-radio:checked {
117
- border-color: var(--color-primary);
118
- }
119
- .lx-radio:checked::before {
120
- content: "";
121
- position: absolute;
122
- left: 50%;
123
- top: 50%;
124
- width: 10px;
125
- height: 10px;
126
- border-radius: 50%;
127
- background: var(--color-primary);
128
- transform: translate(-50%, -50%);
129
- }
130
-
131
- // Keyboard focus ring (only on focus-visible, not mouse)
132
- .lx-check:focus-visible,
133
- .lx-radio:focus-visible {
134
- outline: none;
135
- box-shadow: 0 0 0 3px var(--color-primary-soft);
136
- }
137
-
138
- // Disabled
139
- .lx-check:disabled,
140
- .lx-radio:disabled,
141
- .lx-control:has(:disabled) {
142
- cursor: not-allowed;
143
- }
144
- .lx-check:disabled,
145
- .lx-radio:disabled {
146
- border-color: var(--color-border);
147
- background-color: var(--color-bg-muted);
148
- }
149
- .lx-check:disabled:checked,
150
- .lx-radio:disabled:checked {
151
- border-color: var(--color-border-strong);
152
- }
153
- .lx-check:disabled:checked {
154
- background-color: var(--color-border-strong);
155
- }
156
- .lx-radio:disabled:checked::before {
157
- background: var(--color-border-strong);
158
- }
159
- .lx-control:has(:disabled) {
160
- color: var(--color-text-subtle);
161
- }
162
- .lx-check:disabled::after,
163
- .lx-radio:disabled::after {
164
- display: none;
165
- }
166
-
167
- @media (prefers-reduced-motion: reduce) {
168
- .lx-check,
169
- .lx-radio,
170
- .lx-check::after,
171
- .lx-radio::after {
172
- transition: none;
173
- }
174
- }
175
-
176
- // Row-selection checkbox variant used inside a .table-ledger select column.
177
- .lx-check {
178
- width: 16px;
179
- height: 16px;
180
- accent-color: var(--color-primary);
181
- cursor: pointer;
182
- vertical-align: middle;
183
- }
184
-
185
- // ── Field / Input ──
186
- // Inputs — always-white fill, 16px squircle, soft brand border tint on focus
187
- // (no glow).
188
- .input,
189
- .select,
190
- .textarea {
191
- box-sizing: border-box;
192
- width: 100%;
193
- height: var(--wui-control-height);
194
- padding: 12px;
195
- border-radius: var(--radius);
196
- border: 1px solid var(--color-border);
197
- background: var(--color-surface);
198
- font-size: 14px;
199
- line-height: 1.5;
200
- color: var(--color-text);
201
- transition:
202
- border-color 0.12s,
203
- box-shadow 0.12s;
204
- font-family: inherit;
205
- }
206
- .textarea {
207
- height: auto;
208
- min-height: 120px;
209
- padding: 12px 15px 16px;
210
- line-height: 1.5;
211
- resize: vertical;
212
- }
213
- .input:hover:not(:focus):not(:disabled),
214
- .select:hover:not(:focus):not(:disabled),
215
- .textarea:hover:not(:focus):not(:disabled) {
216
- background-color: var(--color-bg-muted);
217
- border-color: var(--color-border-strong);
218
- }
219
- .input:focus,
220
- .select:focus,
221
- .textarea:focus {
222
- outline: none;
223
- background-color: var(--color-bg-muted);
224
- color: var(--color-text);
225
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
226
- box-shadow: none;
227
- }
228
- // Textarea renders as .form-control.textarea (react-bootstrap). Match the
229
- // shared theme's .form-control:hover specificity so it hovers to
230
- // --color-border-strong too.
231
- textarea.textarea.form-control:hover:not(:focus):not(:disabled):not([readonly]) {
232
- border-color: var(--color-border-strong);
233
- }
234
- textarea.textarea:focus {
235
- outline: none;
236
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
237
- box-shadow: none;
238
- }
239
- .input::placeholder,
240
- .textarea::placeholder {
241
- color: var(--color-text-subtle);
242
- }
243
- // Edge/IE add a native password reveal (and clear) button — hide them so only
244
- // the app's own eye toggle shows.
245
- .input::-ms-reveal,
246
- .input::-ms-clear {
247
- display: none;
248
- }
249
- .input:disabled,
250
- .select:disabled,
251
- .textarea:disabled {
252
- background: var(--color-bg-muted);
253
- border-color: var(--color-border);
254
- color: var(--color-text-subtle);
255
- cursor: not-allowed;
256
- }
257
- .field:has(.input:disabled) .label {
258
- color: var(--color-text-subtle);
259
- }
260
-
261
- // Read-only inputs: darker muted fill to signal non-editable.
262
- .input[readonly],
263
- .select[readonly],
264
- .textarea[readonly] {
265
- background: var(--color-bg-muted);
266
- color: var(--color-text-muted);
267
- cursor: not-allowed;
268
- }
269
-
270
- // Invalid field state (required-field validation) — soft danger-tinted
271
- // border on the surface.
272
- // `[aria-invalid="true"]` is styled alongside `.is-invalid`: React sets the ARIA
273
- // attribute (it is the accessible source of truth) and does not always add the
274
- // Bootstrap class, so keying on the class alone left React error states unstyled.
275
- .input.is-invalid,
276
- .select.is-invalid,
277
- .textarea.is-invalid,
278
- .input.is-invalid:focus,
279
- .select.is-invalid:focus,
280
- .textarea.is-invalid:focus,
281
- .input[aria-invalid="true"],
282
- .select[aria-invalid="true"],
283
- .textarea[aria-invalid="true"],
284
- .input[aria-invalid="true"]:focus,
285
- .select[aria-invalid="true"]:focus,
286
- .textarea[aria-invalid="true"]:focus {
287
- background: var(--color-surface);
288
- border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-surface));
289
- box-shadow: none;
290
- }
291
- .input.is-invalid,
292
- .input[aria-invalid="true"] {
293
- padding-right: 41px;
294
- }
295
- .field-error {
296
- font-size: 12px;
297
- line-height: normal;
298
- color: var(--color-danger);
299
- }
300
-
301
- // Native select with a rounded squircle, custom chevron, off-surface fill.
302
- .select {
303
- appearance: none;
304
- -webkit-appearance: none;
305
- padding-right: 40px;
306
- 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");
307
- background-repeat: no-repeat;
308
- background-position: right 14px center;
309
- background-size: 16px;
310
- cursor: pointer;
311
- }
312
-
313
- // Date inputs — a brand calendar glyph that fills the field so a click
314
- // anywhere opens the native picker; muted text until a value is chosen.
315
- .input[type="date"] {
316
- position: relative;
317
- cursor: pointer;
318
- }
319
- .input[type="date"]:not(.has-value):not(:focus) {
320
- color: var(--color-text-subtle);
321
- }
322
- .input[type="date"]::-webkit-calendar-picker-indicator {
323
- position: absolute;
324
- inset: 0;
325
- width: auto;
326
- height: auto;
327
- margin: 0;
328
- padding: 0;
329
- cursor: pointer;
330
- opacity: 0;
331
- }
332
- .input[type="date"]::-webkit-datetime-edit {
333
- padding-right: 28px;
334
- }
335
- .input-date-wrap {
336
- position: relative;
337
- }
338
-
339
- // Number input: hide the native spinner so a custom stepper can be rendered.
340
- .input[type="number"]::-webkit-inner-spin-button,
341
- .input[type="number"]::-webkit-outer-spin-button {
342
- -webkit-appearance: none;
343
- margin: 0;
344
- }
345
- .input[type="number"] {
346
- -moz-appearance: textfield;
347
- appearance: textfield;
348
- }
349
-
350
- // Custom number stepper — up/down arrows, no background.
351
- .num-stepper {
352
- position: relative;
353
- width: 100%;
354
- }
355
- .field > .num-stepper {
356
- align-self: stretch;
357
- }
358
- .num-stepper .input {
359
- width: 100%;
360
- padding-right: 36px;
361
- }
362
- .num-stepper-btns {
363
- position: absolute;
364
- top: 50%;
365
- right: 8px;
366
- transform: translateY(-50%);
367
- display: flex;
368
- flex-direction: column;
369
- gap: 0;
370
- }
371
- .num-stepper-btns button {
372
- display: grid;
373
- place-items: center;
374
- width: 20px;
375
- height: 15px;
376
- border: none;
377
- background: transparent;
378
- padding: 0;
379
- cursor: pointer;
380
- color: var(--color-text-muted);
381
- }
382
- .num-stepper-btns button:hover {
383
- color: var(--color-text);
384
- }
385
- .num-stepper-btns button:active {
386
- color: var(--color-primary);
387
- }
388
- .input-date-wrap > .input {
389
- padding-right: 38px;
390
- }
391
- .input-date-icon {
392
- position: absolute;
393
- right: 12px;
394
- top: 50%;
395
- transform: translateY(-50%);
396
- color: var(--color-text-muted);
397
- pointer-events: none;
398
- }
399
-
400
- // Error state — danger-tinted border + room for a trailing affix.
401
- .input.error,
402
- .input.error:focus,
403
- .textarea.error,
404
- .textarea.error:focus {
405
- background: var(--color-surface);
406
- border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-surface));
407
- box-shadow: none;
408
- }
409
- .input.error {
410
- padding-right: 41px;
411
- }
412
- // Dark mode: keep the value in an errored field legible.
413
- :root[data-theme="dark"] .input.error,
414
- :root[data-theme="dark"] .input.error:focus,
415
- :root[data-theme="dark"] .input.error:-webkit-autofill,
416
- :root[data-theme="dark"] .input.error:focus:-webkit-autofill {
417
- color: var(--color-text);
418
- -webkit-text-fill-color: var(--color-text);
419
- }
420
-
421
- // Field-relative wrap for trailing affixes (error icon, password toggle).
422
- .input-wrap {
423
- position: relative;
424
- width: 100%;
425
- align-self: stretch;
426
- }
427
- .input-wrap .input {
428
- width: 100%;
429
- }
430
- .input-affix {
431
- position: absolute;
432
- right: 14px;
433
- top: 22.5px;
434
- transform: translateY(-50%);
435
- display: grid;
436
- place-items: center;
437
- pointer-events: none;
438
- color: var(--color-danger);
439
- }
440
-
441
- // Leading-icon input — icon turns brand on focus.
442
- .input-icon {
443
- position: relative;
444
- display: flex;
445
- align-items: center;
446
- width: 100%;
447
- }
448
- .input-icon > .ic {
449
- position: absolute;
450
- left: 13px;
451
- top: 50%;
452
- transform: translateY(-50%);
453
- display: grid;
454
- place-items: center;
455
- color: var(--color-text-subtle);
456
- pointer-events: none;
457
- }
458
- .input-icon .input {
459
- padding-left: 40px;
460
- }
461
- .input-icon:focus-within > .ic {
462
- color: var(--color-primary);
463
- }
464
-
465
- // Password reveal toggle (place inside .input-wrap).
466
- .input.pw {
467
- padding-right: 44px;
468
- }
469
- .input.pw[type="password"]::placeholder {
470
- letter-spacing: normal;
471
- color: var(--color-text-subtle);
472
- }
473
- .pw-toggle {
474
- position: absolute;
475
- right: 7px;
476
- top: 22px;
477
- transform: translateY(-50%);
478
- display: grid;
479
- place-items: center;
480
- width: 32px;
481
- height: 32px;
482
- border: none;
483
- background: transparent;
484
- color: var(--color-text-subtle);
485
- border-radius: var(--radius);
486
- cursor: pointer;
487
- transition:
488
- color 0.12s ease,
489
- background 0.12s ease;
490
- }
491
- .pw-toggle:hover {
492
- color: var(--color-primary);
493
- background: transparent;
494
- }
495
-
496
- .hint,
497
- .field-hint {
498
- margin-top: 2px;
499
- font-size: 12px;
500
- color: var(--color-text-subtle);
501
- }
502
- .label {
503
- display: block;
504
- font-size: 14px;
505
- line-height: normal;
506
- font-weight: 500;
507
- color: var(--color-text);
508
- margin-bottom: 6px;
509
- }
510
- .label-hint {
511
- color: var(--color-text-subtle);
512
- font-weight: 400;
513
- margin-left: 6px;
514
- font-size: 12px;
515
- }
516
- .label-optional {
517
- font-weight: 400;
518
- font-size: 14px;
519
- color: var(--color-text-subtle);
520
- }
521
- .required {
522
- color: var(--color-danger);
523
- }
524
- .field {
525
- display: flex;
526
- flex-direction: column;
527
- align-items: flex-start;
528
- gap: 6px;
529
- margin-bottom: 16px;
530
- }
531
- .field > .label,
532
- .field label {
533
- margin-bottom: 0;
534
- }
535
- .field-row {
536
- display: grid;
537
- grid-template-columns: 1fr 1fr;
538
- gap: 16px;
539
- margin-bottom: 16px;
540
- }
541
- .field-row-3 {
542
- grid-template-columns: 1fr 1fr 1fr;
543
- }
544
- // min-width:0 — grid/flex items default to min-width:auto, which lets a long
545
- // control (e.g. a search-select label) push its track past 1fr and defeat
546
- // text truncation. Reset to 0 so the ellipsis engages instead of overflowing.
547
- .field-row .field {
548
- margin-bottom: 0;
549
- min-width: 0;
550
- }
551
- // Controls fill their field even though .field aligns children flex-start.
552
- .field > .search-select,
553
- .field > .input-with-unit,
554
- .field > .input,
555
- .field > .datepicker,
556
- .field > .field-error {
557
- align-self: stretch;
558
- }
559
- // Doubled selectors — see the lg-tier block below: a media query carries no
560
- // extra specificity, so a consumer's later single-class rule would win.
561
- @media (max-width: 575.98px) {
562
- .field-row.field-row,
563
- .field-row-3.field-row-3 {
564
- grid-template-columns: 1fr;
565
- }
566
- }
567
-
568
- // red border around a non-input group (e.g. a structure-form card row)
569
- .group-invalid {
570
- border: 1px solid var(--color-danger);
571
- border-radius: var(--radius);
572
- padding: 8px;
573
- }
574
-
575
- // One field, one border: the WRAPPER owns the border + hover/focus/error ring
576
- // so the numeric input and its trailing unit pill light up together.
577
- .input-with-unit {
578
- display: flex;
579
- align-items: stretch;
580
- border: 1px solid var(--color-border);
581
- border-radius: var(--radius-lg);
582
- corner-shape: squircle;
583
- background: var(--color-surface);
584
- transition:
585
- border-color 0.12s,
586
- background-color 0.12s;
587
- overflow: hidden;
588
- }
589
- .input-with-unit:hover:not(:focus-within) {
590
- background: var(--color-bg-muted);
591
- border-color: var(--color-border-strong);
592
- }
593
- .input-with-unit:focus-within {
594
- background: var(--color-bg-muted);
595
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
596
- }
597
- .input-with-unit:has(.num-input.error) {
598
- border-color: var(--color-danger);
599
- }
600
-
601
- // Numeric inputs are plain text fields — no native spinner, no stepper. The
602
- // field flows seamlessly into the trailing unit pill; the wrapper carries the
603
- // border.
604
- .input-with-unit .num-input {
605
- border: none;
606
- border-radius: 0;
607
- background: transparent;
608
- }
609
- .input-with-unit .num-input:focus {
610
- box-shadow: none;
611
- }
612
- .input-with-unit .unit {
613
- background: var(--color-bg-muted);
614
- border: none;
615
- border-left: 1px solid var(--color-border);
616
- padding: 9px 14px;
617
- font-size: 14px;
618
- font-weight: 500;
619
- color: var(--color-text-muted);
620
- font-family: "Inter", sans-serif;
621
- display: inline-flex;
622
- align-items: center;
623
- white-space: nowrap;
624
- }
625
-
626
- // ── Status badge ──
627
- .badge {
628
- display: inline-flex;
629
- align-items: center;
630
- gap: 4px;
631
- padding: 3px 9px;
632
- border-radius: 999px;
633
- font-size: 12px;
634
- font-weight: 500;
635
- background: var(--color-bg-muted);
636
- color: var(--color-text-muted);
637
- border: 1px solid var(--color-border);
638
- }
639
- .badge-primary {
640
- background: var(--color-primary-soft);
641
- color: var(--color-primary);
642
- border-color: color-mix(in srgb, var(--color-primary) 25%, transparent);
643
- }
644
- .badge-success {
645
- background: var(--color-success-soft);
646
- color: var(--color-success-strong);
647
- border-color: transparent;
648
- }
649
- .badge-warn {
650
- background: var(--color-warning-soft);
651
- color: var(--color-warning-strong);
652
- border-color: transparent;
653
- }
654
- .badge-danger {
655
- background: var(--color-danger-soft);
656
- color: var(--color-danger-strong);
657
- border-color: transparent;
658
- }
659
- .badge-info {
660
- background: var(--color-info-soft);
661
- color: var(--color-info-strong);
662
- border-color: transparent;
663
- }
664
- .badge-dot::before {
665
- content: "";
666
- width: 6px;
667
- height: 6px;
668
- border-radius: 999px;
669
- background: currentColor;
670
- }
671
-
672
- // "Soft" status badge — pill, 12/600, muted tone fill.
673
- .status-label {
674
- display: inline-flex;
675
- align-items: center;
676
- gap: 5px;
677
- padding: 0 10px;
678
- height: 24px;
679
- border-radius: 999px;
680
- font-size: 12px;
681
- font-weight: 600;
682
- line-height: 1;
683
- white-space: nowrap;
684
- }
685
- // Tones are SEMANTIC (success/warning/danger/info/neutral) — the same
686
- // vocabulary .chip and .badge use — and derive from the same semantic ramp.
687
- // The colour-named `.tone-*` classes are kept as deprecated aliases so older
688
- // Razor call sites keep working; author new markup with the semantic names.
689
- .status-label.success,
690
- .status-label.tone-green {
691
- background: color-mix(in srgb, var(--color-success) 16%, transparent);
692
- color: var(--color-success-strong);
693
- }
694
- .status-label.warning,
695
- .status-label.tone-amber {
696
- background: color-mix(in srgb, var(--color-warning) 16%, transparent);
697
- color: var(--color-warning-strong);
698
- }
699
- .status-label.danger,
700
- .status-label.tone-red {
701
- background: color-mix(in srgb, var(--color-danger) 16%, transparent);
702
- color: var(--color-danger-strong);
703
- }
704
- .status-label.info,
705
- .status-label.tone-blue {
706
- background: color-mix(in srgb, var(--color-info) 16%, transparent);
707
- color: var(--color-info-strong);
708
- }
709
- .status-label.primary {
710
- background: var(--color-primary-soft);
711
- color: var(--color-primary);
712
- }
713
- // Default/neutral — also the bare `.status-label` look, so an unknown or
714
- // omitted tone still renders as a proper pill instead of transparent text.
715
- .status-label,
716
- .status-label.neutral,
717
- .status-label.tone-grey {
718
- background: color-mix(in srgb, var(--color-text-subtle) 16%, transparent);
719
- color: var(--color-text-subtle);
720
- }
721
-
722
- // ── Choice chip ──
723
- // Inline selectable chip — native control hidden, pill adopts the primary
724
- // accent when checked.
725
- .choice-chip {
726
- display: inline-flex;
727
- align-items: center;
728
- gap: 8px;
729
- padding: 9px 14px;
730
- border: 1.5px solid var(--color-border);
731
- border-radius: 999px;
732
- font-size: 14px;
733
- font-weight: 500;
734
- color: var(--color-text);
735
- background: var(--color-surface);
736
- cursor: pointer;
737
- user-select: none;
738
- transition:
739
- border-color 0.12s ease,
740
- background 0.12s ease,
741
- color 0.12s ease;
742
- }
743
- .choice-chip:hover {
744
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
745
- }
746
- .choice-chip input {
747
- position: absolute;
748
- opacity: 0;
749
- pointer-events: none;
750
- }
751
- .choice-chip.checked {
752
- border-color: var(--color-primary);
753
- background: var(--color-primary-soft);
754
- color: var(--color-primary);
755
- }
756
-
757
- // ── Structure card (struct-card*) ──
758
- // Single-select radio-card control (e.g. "type of structure" picker).
759
- .struct-card {
760
- position: relative;
761
- display: flex;
762
- align-items: center;
763
- gap: 11px;
764
- padding: 20px;
765
- border: 1.5px solid var(--color-border);
766
- border-radius: var(--radius);
767
- background: var(--color-surface);
768
- cursor: pointer;
769
- transition:
770
- border-color 0.12s ease,
771
- background 0.12s ease;
772
- }
773
- .struct-card:hover {
774
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
775
- }
776
- .struct-card.checked {
777
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
778
- background: color-mix(in srgb, var(--color-primary) 5%, transparent);
779
- }
780
- .struct-card.checked .struct-card-title {
781
- color: var(--color-primary);
782
- }
783
- .struct-card-input {
784
- position: absolute;
785
- opacity: 0;
786
- pointer-events: none;
787
- }
788
- .struct-card-ic {
789
- width: 44px;
790
- height: 44px;
791
- border-radius: 99px;
792
- flex-shrink: 0;
793
- display: inline-flex;
794
- align-items: center;
795
- justify-content: center;
796
- background: var(--color-bg-muted);
797
- color: var(--color-text-subtle);
798
- transition:
799
- background 0.12s,
800
- color 0.12s;
801
- }
802
- .struct-card.checked .struct-card-ic {
803
- background: var(--color-surface);
804
- color: var(--color-primary);
805
- }
806
- // Icon illustrations are coloured PNGs: greyscale them until the card is
807
- // selected, then show the full-colour (blue) art.
808
- .struct-card-ic img {
809
- filter: grayscale(1);
810
- opacity: 0.6;
811
- transition:
812
- filter 0.12s,
813
- opacity 0.12s;
814
- }
815
- .struct-card.checked .struct-card-ic img {
816
- filter: none;
817
- opacity: 1;
818
- }
819
- .struct-card-body {
820
- display: flex;
821
- flex-direction: column;
822
- min-width: 0;
823
- }
824
- .struct-card-title {
825
- font-weight: 600;
826
- font-size: 16px;
827
- color: var(--color-text);
828
- }
829
- .struct-card-desc {
830
- font-size: 14px;
831
- font-weight: 400;
832
- color: var(--color-text-muted);
833
- line-height: 1.35;
834
- margin-top: 2px;
835
- }
836
- .struct-card-check {
837
- position: absolute;
838
- top: 10px;
839
- right: 10px;
840
- display: inline-flex;
841
- align-items: center;
842
- justify-content: center;
843
- background: transparent;
844
- color: var(--color-primary);
845
- opacity: 0;
846
- transform: scale(0.5);
847
- transition:
848
- opacity 0.12s ease,
849
- transform 0.12s ease;
850
- }
851
- .struct-card.checked .struct-card-check {
852
- opacity: 1;
853
- transform: scale(1);
854
- }
855
-
856
- // Tile variant: big centered illustration + label. Opt in with
857
- // className="struct-card-tile".
858
- .struct-card-tile {
859
- flex-direction: column;
860
- align-items: center;
861
- text-align: center;
862
- gap: 16px;
863
- padding: 26px 18px 20px;
864
- }
865
- // Drop the icon disc — the illustration stands on its own, sized large.
866
- .struct-card-tile .struct-card-ic,
867
- .struct-card-tile.checked .struct-card-ic {
868
- width: auto;
869
- height: auto;
870
- border-radius: 0;
871
- background: transparent;
872
- }
873
- // Full-colour at all times (override the greyscale-until-selected treatment).
874
- .struct-card-tile .struct-card-ic img,
875
- .struct-card-tile.checked .struct-card-ic img {
876
- width: 100px;
877
- height: 100px;
878
- filter: none;
879
- opacity: 1;
880
- }
881
- .struct-card-tile .struct-card-body {
882
- align-items: center;
883
- }
884
- .struct-card-tile .struct-card-title {
885
- font-size: 17px;
886
- }
887
- .struct-card-tile .struct-card-desc {
888
- margin-top: 4px;
889
- }
890
- .struct-card-tile .struct-card-check {
891
- display: none;
892
- }
893
-
894
- // ── Search select ──
895
- .search-select {
896
- position: relative;
897
- min-width: 0;
898
- }
899
- .search-select-control {
900
- display: flex;
901
- align-items: center;
902
- justify-content: space-between;
903
- gap: 8px;
904
- text-align: left;
905
- cursor: pointer;
906
- width: 100%;
907
- min-width: 0;
908
- }
909
- .search-select-control > span {
910
- min-width: 0;
911
- overflow: hidden;
912
- text-overflow: ellipsis;
913
- white-space: nowrap;
914
- }
915
- .search-select-control:disabled {
916
- cursor: not-allowed;
917
- opacity: 0.6;
918
- }
919
- .search-select-control:hover:not(:disabled) {
920
- border-color: var(--color-border-strong);
921
- }
922
- .search-select-control.open {
923
- background: var(--color-bg-muted);
924
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
925
- }
926
- .search-select-caret {
927
- flex-shrink: 0;
928
- color: var(--color-text-muted);
929
- transition: transform 0.15s ease;
930
- }
931
- .search-select-caret path {
932
- stroke-width: 2px;
933
- }
934
- .search-select-control.open .search-select-caret {
935
- transform: rotate(180deg);
936
- }
937
- .search-select-placeholder {
938
- color: var(--color-text-subtle);
939
- }
940
- // Menu matches the themed-select dropdown: 16px squircle, 4px padding, soft
941
- // shadow.
942
- .search-select-menu {
943
- position: absolute;
944
- z-index: 40;
945
- top: calc(100% + 6px);
946
- left: 0;
947
- right: 0;
948
- background: var(--color-surface);
949
- border: 1px solid var(--color-border);
950
- border-radius: var(--radius-lg);
951
- corner-shape: squircle;
952
- box-shadow: var(--shadow-overlay);
953
- padding: 4px;
954
- overflow: hidden;
955
- }
956
- // Portalled to <body>: fixed-positioned (top/left set inline from the
957
- // trigger rect) so the menu overlays instead of being clipped by a
958
- // scrollable modal / table card. Sits above the modal layer.
959
- .search-select-menu-fixed {
960
- position: fixed;
961
- z-index: 1600;
962
- right: auto;
963
- top: auto;
964
- left: auto;
965
- }
966
- // Search box matches the search bar: rounded field, leading icon turns
967
- // primary.
968
- .search-select-search {
969
- display: flex;
970
- align-items: center;
971
- gap: 8px;
972
- margin: 4px;
973
- padding: 0 12px;
974
- height: 40px;
975
- border: 1px solid var(--color-border);
976
- border-radius: var(--radius);
977
- corner-shape: squircle;
978
- background: var(--color-surface);
979
- color: var(--color-text-subtle);
980
- }
981
- .search-select-search {
982
- position: relative;
983
- }
984
- .search-select-search:focus-within {
985
- background: var(--color-bg-muted);
986
- }
987
- .search-select-search:focus-within svg {
988
- color: var(--color-primary);
989
- }
990
- .search-select-clear {
991
- display: grid;
992
- place-items: center;
993
- width: 24px;
994
- height: 24px;
995
- border: none;
996
- background: transparent;
997
- padding: 0;
998
- cursor: pointer;
999
- color: var(--color-text-subtle);
1000
- }
1001
- .search-select-clear:hover {
1002
- color: var(--color-text);
1003
- }
1004
- .search-select-input {
1005
- flex: 1;
1006
- border: none;
1007
- outline: none;
1008
- font-size: 14px;
1009
- font-family: inherit;
1010
- color: var(--color-text);
1011
- background: transparent;
1012
- }
1013
- .search-select-options {
1014
- max-height: 300px;
1015
- overflow-y: auto;
1016
- padding: 4px 4px 4px;
1017
- display: flex;
1018
- flex-direction: column;
1019
- gap: 4px;
1020
- scrollbar-gutter: stable;
1021
- }
1022
- .search-select-option {
1023
- display: flex;
1024
- align-items: center;
1025
- justify-content: space-between;
1026
- gap: 8px;
1027
- width: 100%;
1028
- text-align: left;
1029
- padding: 9px 14px;
1030
- line-height: 1.4;
1031
- border: none;
1032
- border-radius: var(--radius);
1033
- corner-shape: squircle;
1034
- background: transparent;
1035
- font-size: 14px;
1036
- color: var(--color-text);
1037
- cursor: pointer;
1038
- transition:
1039
- background 0.12s ease,
1040
- color 0.12s ease;
1041
- }
1042
- .search-select-option:hover,
1043
- .search-select-option.is-active {
1044
- background: var(--color-primary-soft);
1045
- color: var(--color-primary);
1046
- }
1047
- .search-select-option.selected {
1048
- background: var(--color-primary-soft);
1049
- color: var(--color-primary);
1050
- font-weight: 500;
1051
- }
1052
- .search-select-empty {
1053
- padding: 12px 10px;
1054
- font-size: 13px;
1055
- color: var(--color-text-subtle);
1056
- text-align: center;
1057
- }
1058
-
1059
- // ── Date picker ──
1060
- // Custom DatePicker — themed popover calendar (the native popup can't be
1061
- // styled).
1062
- .datepicker {
1063
- position: relative;
1064
- }
1065
- .datepicker-trigger {
1066
- text-align: left;
1067
- cursor: pointer;
1068
- }
1069
- .datepicker-trigger:not(.has-value) {
1070
- color: var(--color-text-subtle);
1071
- }
1072
- .datepicker.open .datepicker-trigger {
1073
- background-color: var(--color-bg-muted);
1074
- border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
1075
- box-shadow: none;
1076
- }
1077
- // Portalled to <body> and fixed-positioned from JS so a modal's
1078
- // overflow:hidden can't clip it; top/left come from inline styles. z-index
1079
- // sits above the modal layer.
1080
- .datepicker-pop {
1081
- position: fixed;
1082
- z-index: 1600;
1083
- width: 312px;
1084
- max-width: calc(100vw - 16px);
1085
- padding: 18px;
1086
- background: var(--color-surface);
1087
- border: 1px solid var(--color-border);
1088
- border-radius: var(--radius-xl);
1089
- corner-shape: squircle;
1090
- box-shadow: var(--shadow-overlay);
1091
- animation: datepicker-in 0.14s ease;
1092
- }
1093
- .datepicker-pop.drop-up {
1094
- animation: datepicker-in-up 0.14s ease;
1095
- }
1096
- @keyframes datepicker-in {
1097
- from {
1098
- opacity: 0;
1099
- transform: translateY(-4px);
1100
- }
1101
- to {
1102
- opacity: 1;
1103
- transform: translateY(0);
1104
- }
1105
- }
1106
- @keyframes datepicker-in-up {
1107
- from {
1108
- opacity: 0;
1109
- transform: translateY(4px);
1110
- }
1111
- to {
1112
- opacity: 1;
1113
- transform: translateY(0);
1114
- }
1115
- }
1116
- .datepicker-head {
1117
- display: flex;
1118
- align-items: center;
1119
- justify-content: space-between;
1120
- margin-bottom: 14px;
1121
- }
1122
- .datepicker-month {
1123
- font-size: 15px;
1124
- font-weight: 600;
1125
- color: var(--color-text);
1126
- letter-spacing: -0.01em;
1127
- }
1128
- .datepicker-nav {
1129
- display: inline-flex;
1130
- gap: 6px;
1131
- }
1132
- .datepicker-nav-btn {
1133
- display: inline-flex;
1134
- align-items: center;
1135
- justify-content: center;
1136
- width: 30px;
1137
- height: 30px;
1138
- border: 1px solid var(--color-border);
1139
- border-radius: 999px;
1140
- background: transparent;
1141
- color: var(--color-text-muted);
1142
- cursor: pointer;
1143
- transition:
1144
- background 0.12s,
1145
- color 0.12s,
1146
- border-color 0.12s;
1147
- }
1148
- .datepicker-nav-btn:hover {
1149
- background: var(--color-primary-soft, color-mix(in srgb, var(--color-primary) 8%, transparent));
1150
- border-color: var(--color-primary);
1151
- color: var(--color-primary);
1152
- }
1153
- .datepicker-weekdays {
1154
- display: grid;
1155
- grid-template-columns: repeat(7, 1fr);
1156
- gap: 4px;
1157
- margin-bottom: 6px;
1158
- }
1159
- .datepicker-grid {
1160
- display: flex;
1161
- flex-direction: column;
1162
- gap: 4px;
1163
- }
1164
- .datepicker-row {
1165
- display: grid;
1166
- grid-template-columns: repeat(7, 1fr);
1167
- gap: 4px;
1168
- }
1169
- .datepicker-weekdays span {
1170
- text-align: center;
1171
- font-size: 11px;
1172
- font-weight: 600;
1173
- text-transform: uppercase;
1174
- letter-spacing: 0.04em;
1175
- color: var(--color-text-subtle);
1176
- padding: 4px 0;
1177
- }
1178
- .datepicker-day {
1179
- display: inline-flex;
1180
- align-items: center;
1181
- justify-content: center;
1182
- aspect-ratio: 1;
1183
- width: 100%;
1184
- border: none;
1185
- border-radius: 50%;
1186
- background: transparent;
1187
- font-size: 13px;
1188
- font-weight: 500;
1189
- color: var(--color-text);
1190
- cursor: pointer;
1191
- transition:
1192
- background 0.12s,
1193
- color 0.12s,
1194
- box-shadow 0.12s;
1195
- }
1196
- .datepicker-day:hover:not(:disabled):not(.is-selected) {
1197
- background: var(--color-primary-soft, color-mix(in srgb, var(--color-primary) 8%, transparent));
1198
- color: var(--color-primary);
1199
- }
1200
- .datepicker-day.is-outside {
1201
- color: var(--color-text-subtle);
1202
- font-weight: 400;
1203
- }
1204
- .datepicker-day.is-today {
1205
- font-weight: 700;
1206
- color: var(--color-primary);
1207
- box-shadow: inset 0 0 0 1.5px var(--color-primary);
1208
- }
1209
- .datepicker-day.is-selected {
1210
- background: var(--color-primary);
1211
- color: #fff;
1212
- font-weight: 600;
1213
- }
1214
- .datepicker-day:disabled {
1215
- color: var(--color-border-strong);
1216
- cursor: not-allowed;
1217
- }
1218
- .datepicker-foot {
1219
- display: flex;
1220
- align-items: center;
1221
- justify-content: space-between;
1222
- margin-top: 14px;
1223
- padding-top: 14px;
1224
- border-top: 1px solid var(--color-border);
1225
- }
1226
- .datepicker-link {
1227
- border: none;
1228
- background: transparent;
1229
- padding: 6px 12px;
1230
- border-radius: var(--radius);
1231
- font-size: 13px;
1232
- font-weight: 600;
1233
- color: var(--color-primary);
1234
- cursor: pointer;
1235
- transition: background 0.12s;
1236
- }
1237
- .datepicker-link:hover:not(:disabled) {
1238
- background: var(--color-primary-soft, color-mix(in srgb, var(--color-primary) 8%, transparent));
1239
- }
1240
- .datepicker-link:disabled {
1241
- color: var(--color-text-subtle);
1242
- cursor: not-allowed;
1243
- }
1244
-
1245
- // ── Phone input ──
1246
- .app-phone-number-input {
1247
- width: 100%;
1248
- }
1249
- // One field, one border: the WRAPPER owns the chrome (same pattern as
1250
- // .input-with-unit) so the dial-code picker and the number light up together.
1251
- // The adornment used to be absolutely positioned over the input, which let the
1252
- // placeholder run underneath it — masked only by the UA button's grey fill.
1253
- .app-phone-number-input .phone-input-wrap {
1254
- display: flex;
1255
- align-items: center;
1256
- width: 100%;
1257
- height: var(--wui-control-height);
1258
- border: 1px solid var(--color-border);
1259
- border-radius: var(--radius);
1260
- corner-shape: squircle;
1261
- background: var(--color-surface);
1262
- transition:
1263
- border-color 0.12s,
1264
- background-color 0.12s;
1265
- }
1266
- .app-phone-number-input .phone-input-wrap:hover:not(:focus-within) {
1267
- background: var(--color-bg-muted);
1268
- border-color: var(--color-border-strong);
1269
- }
1270
- .app-phone-number-input .phone-input-wrap:focus-within {
1271
- background: var(--color-bg-muted);
1272
- border-color: var(--color-primary);
1273
- }
1274
- .app-phone-number-input .phone-input-wrap:has(> .input:disabled) {
1275
- background: var(--color-bg-muted);
1276
- cursor: not-allowed;
1277
- }
1278
- .app-phone-number-input .phone-input-wrap:has(.input.error),
1279
- .app-phone-number-input .phone-input-wrap:has(.input.is-invalid) {
1280
- border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-surface));
1281
- }
1282
- // The inner control drops its own chrome — the shell carries it.
1283
- .app-phone-number-input .phone-input-wrap > .input,
1284
- .app-phone-number-input .phone-input-wrap > .input:hover,
1285
- .app-phone-number-input .phone-input-wrap > .input:focus {
1286
- flex: 1 1 auto;
1287
- min-width: 0;
1288
- width: auto;
1289
- height: 100%;
1290
- border: 0;
1291
- background: transparent;
1292
- box-shadow: none;
1293
- }
1294
- // `relative` so the menu anchors here (it is NOT positioned by JS — it used to
1295
- // rely on the adornment being absolute, which is why it appeared to float).
1296
- .app-phone-number-input .phone-country-adornment {
1297
- position: relative;
1298
- flex: none;
1299
- display: flex;
1300
- align-items: center;
1301
- padding-left: 12px;
1302
- z-index: 5;
1303
- }
1304
- // NOT scoped under `.country-dropdown` — <wui-phone> renders the toggle inside
1305
- // `.phone-country-adornment`, with no such wrapper. Scoping it there left the
1306
- // button on raw UA styling (2px outset border + grey fill) inside the field.
1307
- .country-toggle {
1308
- display: flex;
1309
- align-items: center;
1310
- gap: 6px;
1311
- padding: 0;
1312
- border: 0;
1313
- background: transparent;
1314
- box-shadow: none;
1315
- color: var(--color-text);
1316
- cursor: pointer;
1317
- font: inherit;
1318
- line-height: 1;
1319
- }
1320
- .country-toggle::after {
1321
- display: none;
1322
- }
1323
- .country-toggle:hover,
1324
- .country-toggle:focus,
1325
- .country-toggle:active {
1326
- background: transparent;
1327
- box-shadow: none;
1328
- color: var(--color-text);
1329
- }
1330
- .country-toggle:hover .phone-chevron {
1331
- color: var(--color-text);
1332
- }
1333
- .country-toggle:focus {
1334
- outline: none;
1335
- }
1336
- .country-toggle:focus-visible {
1337
- outline: 2px solid var(--color-primary);
1338
- outline-offset: 2px;
1339
- border-radius: var(--radius-sm);
1340
- }
1341
- .country-toggle:disabled {
1342
- cursor: not-allowed;
1343
- opacity: 0.6;
1344
- }
1345
- // Flag glyphs come from the generated .wui-flag-* classes (_flags.scss).
1346
- .app-phone-number-input .phone-chevron {
1347
- color: var(--color-text-muted);
1348
- transition: transform 0.2s ease;
1349
- }
1350
- // Keyed off the element's own ARIA state — <wui-phone> toggles aria-expanded,
1351
- // it never adds react-bootstrap's `.show`.
1352
- .country-toggle[aria-expanded="true"] .phone-chevron {
1353
- transform: rotate(180deg);
1354
- }
1355
- .app-phone-number-input .phone-dial {
1356
- font-size: 13px;
1357
- font-weight: 500;
1358
- color: var(--color-text);
1359
- }
1360
-
1361
- // Menu, search field and rows all share the .search-select language so the
1362
- // country picker reads as the same component family as every other picker.
1363
- // Fixed, with top/left set from the trigger rect by <wui-phone>, so an
1364
- // overflow-hidden card/modal/table wrapper can't clip the list. Sits above the
1365
- // modal layer, matching .search-select-menu-fixed and .datepicker-pop.
1366
- .country-dropdown-menu {
1367
- position: fixed;
1368
- z-index: 1600;
1369
- width: 320px;
1370
- max-width: calc(100vw - 16px);
1371
- padding: 4px;
1372
- border: 1px solid var(--color-border);
1373
- border-radius: var(--radius-lg);
1374
- corner-shape: squircle;
1375
- background: var(--color-surface);
1376
- box-shadow: var(--shadow-overlay);
1377
- overflow: hidden;
1378
- }
1379
- .dropdown-search-wrapper {
1380
- position: relative;
1381
- margin: 4px;
1382
- }
1383
- .dropdown-search-icon {
1384
- position: absolute;
1385
- left: 12px;
1386
- top: 50%;
1387
- transform: translateY(-50%);
1388
- display: flex;
1389
- color: var(--color-text-subtle);
1390
- pointer-events: none;
1391
- transition: color 0.12s ease;
1392
- }
1393
- .dropdown-search-wrapper:focus-within .dropdown-search-icon {
1394
- color: var(--color-primary);
1395
- }
1396
- .dropdown-search-input {
1397
- width: 100%;
1398
- height: 40px;
1399
- padding: 0 12px 0 36px;
1400
- border: 1px solid var(--color-border);
1401
- border-radius: var(--radius);
1402
- corner-shape: squircle;
1403
- font-size: 14px;
1404
- color: var(--color-text);
1405
- background: var(--color-surface);
1406
- outline: none;
1407
- }
1408
- .dropdown-search-input:hover:not(:focus) {
1409
- background: var(--color-bg-muted);
1410
- border-color: var(--color-border-strong);
1411
- }
1412
- // Border is the focus cue everywhere else in the system — no glow ring here.
1413
- .dropdown-search-input:focus {
1414
- background: var(--color-bg-muted);
1415
- border-color: var(--color-primary);
1416
- }
1417
- .country-list-scroll {
1418
- max-height: 250px;
1419
- overflow-y: auto;
1420
- overflow-x: hidden;
1421
- padding: 4px;
1422
- display: flex;
1423
- flex-direction: column;
1424
- gap: 4px;
1425
- scrollbar-width: thin;
1426
- scrollbar-gutter: stable;
1427
- }
1428
- .country-list-scroll::-webkit-scrollbar {
1429
- width: 6px;
1430
- }
1431
- .country-list-scroll::-webkit-scrollbar-thumb {
1432
- background: var(--color-border);
1433
- border-radius: 999px;
1434
- }
1435
- // Rounded, inset rows with a primary-tinted hover — identical to
1436
- // .search-select-option. (!important is required: react-bootstrap's
1437
- // .dropdown-item ships its own colour/padding at equal specificity.)
1438
- .country-dropdown-item {
1439
- display: flex !important;
1440
- align-items: center;
1441
- gap: 10px;
1442
- padding: 9px 14px !important;
1443
- border-radius: var(--radius) !important;
1444
- corner-shape: squircle;
1445
- cursor: pointer;
1446
- color: var(--color-text) !important;
1447
- background: transparent !important;
1448
- border: 0 !important;
1449
- transition:
1450
- background 0.12s ease,
1451
- color 0.12s ease;
1452
- }
1453
- .country-dropdown-item:hover,
1454
- .country-dropdown-item:focus {
1455
- background: var(--color-primary-soft) !important;
1456
- color: var(--color-primary) !important;
1457
- }
1458
- .country-dropdown-item.selected {
1459
- background: var(--color-primary-soft) !important;
1460
- color: var(--color-primary) !important;
1461
- font-weight: 500;
1462
- }
1463
- .country-dropdown-menu .wui-flag {
1464
- flex-shrink: 0;
1465
- }
1466
- .country-name-text {
1467
- font-size: 14px;
1468
- font-weight: 500;
1469
- color: var(--color-text);
1470
- }
1471
- .country-code-text {
1472
- margin-left: 6px;
1473
- font-weight: 400;
1474
- color: var(--color-text-muted);
1475
- }
1476
- // On a tinted row the label must ride the row colour, not stay grey.
1477
- .country-dropdown-item:hover .country-name-text,
1478
- .country-dropdown-item:focus .country-name-text,
1479
- .country-dropdown-item.selected .country-name-text,
1480
- .country-dropdown-item:hover .country-code-text,
1481
- .country-dropdown-item:focus .country-code-text,
1482
- .country-dropdown-item.selected .country-code-text {
1483
- color: currentColor;
1484
- }
1485
- .selected-check-icon {
1486
- margin-left: auto;
1487
- display: flex;
1488
- color: var(--color-primary);
1489
- }
1490
- .country-empty {
1491
- padding: 10px 16px;
1492
- text-align: center;
1493
- color: var(--color-text-subtle);
1494
- font-size: 14px;
1495
- }
1496
-
1497
- // ── Avatar ──
1498
- // A photo fills the avatar disc edge to edge; the circle is the frame, so
1499
- // the image is cropped to it rather than letterboxed.
1500
- .avatar > .avatar-img {
1501
- width: 100%;
1502
- height: 100%;
1503
- border-radius: inherit;
1504
- object-fit: cover;
1505
- display: block;
1506
- }
1507
- .avatar {
1508
- width: 32px;
1509
- height: 32px;
1510
- border-radius: 999px;
1511
- background: color-mix(in srgb, var(--color-primary) 15%, var(--color-surface));
1512
- color: var(--color-primary);
1513
- font-size: 12px;
1514
- font-weight: 600;
1515
- display: inline-flex;
1516
- align-items: center;
1517
- justify-content: center;
1518
- }
1519
- .avatar-lg {
1520
- width: 48px;
1521
- height: 48px;
1522
- font-size: 16px;
1523
- }
1524
- .avatar-xl {
1525
- width: 64px;
1526
- height: 64px;
1527
- font-size: 22px;
1528
- }
1529
- .avatar-sm {
1530
- width: 24px;
1531
- height: 24px;
1532
- font-size: 10px;
1533
- }
1534
-
1535
- // ── File attach ──
1536
- // Support-form attachment strip: drop zone → attached-image preview.
1537
- .attach-drop {
1538
- display: flex;
1539
- width: 100%;
1540
- align-items: center;
1541
- gap: 12px;
1542
- padding: 12px 14px;
1543
- border: 1px dashed var(--color-border);
1544
- border-radius: var(--radius-lg);
1545
- background: var(--color-surface);
1546
- cursor: pointer;
1547
- transition:
1548
- border-color 0.3s cubic-bezier(0.22, 1, 0.36, 1),
1549
- background 0.3s cubic-bezier(0.22, 1, 0.36, 1);
1550
- }
1551
- .attach-drop:hover {
1552
- border-color: var(--color-primary);
1553
- background: var(--color-primary-soft);
1554
- }
1555
- .attach-drop-icon {
1556
- flex: 0 0 auto;
1557
- width: 36px;
1558
- height: 36px;
1559
- display: inline-flex;
1560
- align-items: center;
1561
- justify-content: center;
1562
- border-radius: 999px;
1563
- background: var(--color-primary-soft);
1564
- color: var(--color-primary);
1565
- }
1566
- .attach-drop:hover .attach-drop-icon {
1567
- background: var(--color-surface);
1568
- }
1569
- .attach-drop-copy {
1570
- display: flex;
1571
- flex-direction: column;
1572
- gap: 2px;
1573
- flex: 1;
1574
- min-width: 0;
1575
- }
1576
- .attach-drop-title {
1577
- font-weight: 600;
1578
- font-size: 14px;
1579
- line-height: 1.4;
1580
- color: var(--color-text);
1581
- }
1582
- .attach-drop-hint {
1583
- font-size: 12px;
1584
- color: var(--color-text-subtle);
1585
- }
1586
- .attach-drop-cta {
1587
- flex: 0 0 auto;
1588
- font-size: 12px;
1589
- font-weight: 600;
1590
- color: var(--color-primary);
1591
- border: 1px solid var(--color-border);
1592
- border-radius: 999px;
1593
- padding: 5px 14px;
1594
- background: var(--color-surface);
1595
- }
1596
- // Attached image renders full width so the user can actually check what
1597
- // they're sending.
1598
- .attach-preview {
1599
- width: 100%;
1600
- border: 1px solid var(--color-border);
1601
- border-radius: var(--radius-lg);
1602
- background: var(--color-surface);
1603
- overflow: hidden;
1604
- }
1605
- .attach-figure {
1606
- position: relative;
1607
- // A lighter elevated surface behind the (contained) image so the
1608
- // letterbox reads as a surface, not near-black — dark mode uses the
1609
- // muted/elevated step instead.
1610
- background: var(--color-bg-muted);
1611
- }
1612
- .attach-figure img {
1613
- display: block;
1614
- width: 100%;
1615
- max-height: 260px;
1616
- object-fit: contain;
1617
- }
1618
- .attach-meta {
1619
- display: flex;
1620
- align-items: baseline;
1621
- justify-content: space-between;
1622
- gap: 12px;
1623
- padding: 10px 14px;
1624
- border-top: 1px solid var(--color-border);
1625
- min-width: 0;
1626
- }
1627
- .attach-name {
1628
- font-size: 13px;
1629
- font-weight: 500;
1630
- color: var(--color-text);
1631
- overflow: hidden;
1632
- text-overflow: ellipsis;
1633
- white-space: nowrap;
1634
- }
1635
- .attach-size {
1636
- font-size: 12px;
1637
- color: var(--color-text-subtle);
1638
- flex: 0 0 auto;
1639
- }
1640
- .attach-remove {
1641
- position: absolute;
1642
- top: 8px;
1643
- right: 8px;
1644
- display: inline-flex;
1645
- align-items: center;
1646
- justify-content: center;
1647
- width: 28px;
1648
- height: 28px;
1649
- border: 0;
1650
- background: rgba(0, 0, 0, 0.62);
1651
- backdrop-filter: blur(10px);
1652
- -webkit-backdrop-filter: blur(10px);
1653
- color: #fff;
1654
- border-radius: 999px;
1655
- cursor: pointer;
1656
- transition: background 0.2s;
1657
- }
1658
- .attach-remove:hover {
1659
- background: var(--color-danger);
1660
- }
1661
- // Iconsax hardcodes both stroke colour and width on its paths, so force
1662
- // white here — otherwise the glyph vanishes into the button in dark mode.
1663
- .attach-remove svg {
1664
- width: 18px;
1665
- height: 18px;
1666
- display: block;
1667
- }
1668
- .attach-remove svg path {
1669
- stroke: #fff;
1670
- stroke-width: 2px;
1671
- }
1672
- @media (prefers-reduced-motion: reduce) {
1673
- .attach-drop,
1674
- .attach-remove {
1675
- transition: none;
1676
- }
1677
- }
1678
-
1679
- // ── File picker ──
1680
- // File input — segmented "Upload" button + placeholder, matches the .input
1681
- // shell.
1682
- .file-input {
1683
- box-sizing: border-box;
1684
- display: flex;
1685
- align-items: stretch;
1686
- width: 100%;
1687
- border-radius: var(--radius-lg);
1688
- corner-shape: squircle;
1689
- border: 1px solid var(--color-border);
1690
- background: var(--color-surface);
1691
- overflow: hidden;
1692
- cursor: pointer;
1693
- transition:
1694
- border-color 0.12s,
1695
- box-shadow 0.12s;
1696
- }
1697
- .file-input:hover {
1698
- border-color: var(--color-border-strong);
1699
- }
1700
- .file-input-btn {
1701
- display: inline-flex;
1702
- align-items: center;
1703
- flex-shrink: 0;
1704
- padding: 12px 20px;
1705
- font-size: 14px;
1706
- font-weight: 600;
1707
- color: var(--color-text);
1708
- background: var(--color-bg-muted);
1709
- border-right: 1px solid var(--color-border);
1710
- }
1711
- .file-input-text {
1712
- display: inline-flex;
1713
- align-items: center;
1714
- min-width: 0;
1715
- padding: 12px 16px;
1716
- font-size: 14px;
1717
- color: var(--color-text-subtle);
1718
- white-space: nowrap;
1719
- overflow: hidden;
1720
- text-overflow: ellipsis;
1721
- }
1722
- .file-input-text.has-file {
1723
- color: var(--color-text);
1724
- }
1725
-
1726
- // Tall empty state (AppFilePicker): a drag-and-drop dropzone — dashed
1727
- // border, circular icon badge, click-or-drop copy. Highlights on hover and
1728
- // while a file is dragged over.
1729
- .file-input.file-input-tall {
1730
- position: relative;
1731
- flex-direction: column;
1732
- align-items: center;
1733
- justify-content: center;
1734
- gap: 10px;
1735
- height: 350px;
1736
- padding: 20px;
1737
- text-align: center;
1738
- background: var(--color-bg-subtle);
1739
- border: 1px dashed var(--color-border-strong);
1740
- transition:
1741
- border-color 0.15s ease,
1742
- background-color 0.15s ease;
1743
- }
1744
- .file-input.file-input-tall:hover,
1745
- .file-input.file-input-tall.dragging {
1746
- border-color: var(--color-primary);
1747
- background: var(--color-bg-muted);
1748
- }
1749
- // Fill-label associates click with the hidden file input; children don't
1750
- // steal drag hits.
1751
- .file-input.file-input-tall > .file-input-zone-label {
1752
- position: absolute;
1753
- inset: 0;
1754
- display: flex;
1755
- flex-direction: column;
1756
- align-items: center;
1757
- justify-content: center;
1758
- gap: 10px;
1759
- margin: 0;
1760
- cursor: pointer;
1761
- }
1762
- .file-input.file-input-tall > .file-input-zone-label > * {
1763
- pointer-events: none;
1764
- }
1765
- .file-input.file-input-tall .file-input-icon {
1766
- display: inline-flex;
1767
- align-items: center;
1768
- justify-content: center;
1769
- width: 54px;
1770
- height: 54px;
1771
- border-radius: 50%;
1772
- background: var(--color-primary-soft);
1773
- color: var(--color-primary);
1774
- transition: transform 0.15s ease;
1775
- }
1776
- .file-input.file-input-tall.dragging .file-input-icon {
1777
- transform: translateY(-2px) scale(1.06);
1778
- }
1779
- .file-input-title {
1780
- font-size: 14px;
1781
- font-weight: 600;
1782
- color: var(--color-text);
1783
- }
1784
- .file-input-title .browse {
1785
- color: var(--color-primary);
1786
- }
1787
- .file-input.file-input-tall .file-input-text {
1788
- padding: 0;
1789
- font-size: 12px;
1790
- }
1791
-
1792
- // Large preview shown once an image exists; hover reveals a "Change image"
1793
- // overlay.
1794
- .file-preview {
1795
- position: relative;
1796
- box-sizing: border-box;
1797
- display: block;
1798
- width: 100%;
1799
- height: 350px;
1800
- border-radius: var(--radius-lg);
1801
- corner-shape: squircle;
1802
- border: 1px solid var(--color-border);
1803
- overflow: hidden;
1804
- cursor: pointer;
1805
- background: var(--color-bg-subtle);
1806
- transition: border-color 0.12s;
1807
- }
1808
- .file-preview:hover {
1809
- border-color: var(--color-border-strong);
1810
- }
1811
- .file-preview.error {
1812
- border-color: var(--color-danger);
1813
- }
1814
- // Open-picker control fills the shell (remove button is a sibling, not
1815
- // nested).
1816
- .file-preview-open {
1817
- display: block;
1818
- box-sizing: border-box;
1819
- width: 100%;
1820
- height: 100%;
1821
- padding: 0;
1822
- border: none;
1823
- background: transparent;
1824
- cursor: pointer;
1825
- position: relative;
1826
- font: inherit;
1827
- color: inherit;
1828
- text-align: left;
1829
- }
1830
- .file-preview-img {
1831
- display: block;
1832
- width: 100%;
1833
- height: 100%;
1834
- object-fit: cover;
1835
- transition: transform 0.25s ease;
1836
- }
1837
- // Gentle zoom on hover so the field feels interactive.
1838
- .file-preview:hover .file-preview-img {
1839
- transform: scale(1.03);
1840
- }
1841
- .file-preview-overlay {
1842
- position: absolute;
1843
- inset: 0;
1844
- display: flex;
1845
- align-items: center;
1846
- justify-content: center;
1847
- // Subtle dim so the glassy CTA reads on any image, without hiding it.
1848
- background: rgba(16, 24, 40, 0.22);
1849
- opacity: 0;
1850
- transition: opacity 0.15s ease;
1851
- }
1852
- .file-preview:hover .file-preview-overlay,
1853
- .file-preview:focus-within .file-preview-overlay {
1854
- opacity: 1;
1855
- }
1856
- // The "Change image" call-to-action as a glassy pill button.
1857
- .file-preview-cta {
1858
- display: inline-flex;
1859
- align-items: center;
1860
- gap: 8px;
1861
- padding: 9px 18px;
1862
- background: rgba(255, 255, 255, 0.96);
1863
- color: var(--color-text);
1864
- font-size: 14px;
1865
- font-weight: 600;
1866
- border-radius: 999px;
1867
- box-shadow: var(--shadow-overlay);
1868
- }
1869
- .file-preview-remove {
1870
- position: absolute;
1871
- top: 8px;
1872
- right: 8px;
1873
- z-index: 2;
1874
- box-sizing: border-box;
1875
- display: inline-flex;
1876
- align-items: center;
1877
- justify-content: center;
1878
- width: 30px;
1879
- height: 30px;
1880
- padding: 0;
1881
- border: 2px solid transparent;
1882
- border-radius: 50%;
1883
- background: rgba(16, 24, 40, 0.55);
1884
- color: #fff;
1885
- cursor: pointer;
1886
- opacity: 0;
1887
- transition:
1888
- border-color 0.12s,
1889
- opacity 0.15s ease;
1890
- }
1891
- .file-preview:hover .file-preview-remove,
1892
- .file-preview:focus-within .file-preview-remove {
1893
- opacity: 1;
1894
- }
1895
- // Hovering the cross: keep it plain — just a slightly deeper disc, no
1896
- // coloured ring.
1897
- .file-preview-remove:hover {
1898
- background: rgba(16, 24, 40, 0.78);
1899
- color: #fff;
1900
- }
1901
-
1902
- // ── Logo picker ──
1903
- // Round dropzone uploader (workspace/brand logo) and the settings-page photo
1904
- // ring share this same interactive-uploader pattern.
1905
- .set-uploader {
1906
- display: inline-flex;
1907
- padding: 0;
1908
- border: none;
1909
- background: transparent;
1910
- font: inherit;
1911
- color: inherit;
1912
- cursor: pointer;
1913
- }
1914
- .set-uploader:disabled {
1915
- cursor: progress;
1916
- }
1917
- .set-uploader-img {
1918
- width: 100%;
1919
- height: 100%;
1920
- border-radius: 50%;
1921
- object-fit: cover;
1922
- }
1923
- .set-uploader-ring {
1924
- position: relative;
1925
- width: 120px;
1926
- height: 120px;
1927
- border-radius: 50%;
1928
- padding: 8px;
1929
- border: 1px dashed color-mix(in srgb, var(--color-border-strong) 75%, transparent);
1930
- cursor: pointer;
1931
- transition: border-color 0.18s;
1932
- }
1933
- .set-uploader-ring:hover,
1934
- .set-uploader:hover .set-uploader-ring,
1935
- .set-uploader:focus-visible .set-uploader-ring {
1936
- border-color: var(--color-primary);
1937
- }
1938
- .set-uploader-logo {
1939
- width: 100%;
1940
- height: 100%;
1941
- border-radius: 50%;
1942
- display: grid;
1943
- place-items: center;
1944
- font-size: 38px;
1945
- font-weight: 700;
1946
- letter-spacing: -0.02em;
1947
- color: var(--color-primary);
1948
- background: var(--color-primary-soft);
1949
- }
1950
- .set-uploader-overlay {
1951
- position: absolute;
1952
- inset: 8px;
1953
- border-radius: 50%;
1954
- display: flex;
1955
- flex-direction: column;
1956
- align-items: center;
1957
- justify-content: center;
1958
- gap: 4px;
1959
- background: rgba(20, 26, 33, 0.62);
1960
- color: #fff;
1961
- font-size: 11px;
1962
- font-weight: 600;
1963
- opacity: 0;
1964
- transition: opacity 0.18s;
1965
- }
1966
- .set-uploader-ring:hover .set-uploader-overlay,
1967
- .set-uploader:hover .set-uploader-overlay,
1968
- .set-uploader:focus-visible .set-uploader-overlay {
1969
- opacity: 1;
1970
- }
1971
- // While uploading: keep the overlay (spinner) visible and signal it's not
1972
- // clickable.
1973
- .set-uploader-overlay.is-loading {
1974
- opacity: 1;
1975
- }
1976
- .set-uploader[aria-busy="true"] {
1977
- cursor: progress;
1978
- }
1979
- .set-uploader-hint {
1980
- margin: 18px auto 0;
1981
- max-width: 240px;
1982
- font-size: 12px;
1983
- line-height: 1.5;
1984
- color: var(--color-text-subtle);
1985
- }
1986
-
1987
- // Minimals-style rounded logo dropzone (workspace branding form).
1988
- .logo-upload {
1989
- display: flex;
1990
- align-items: center;
1991
- gap: 20px;
1992
- flex-wrap: wrap;
1993
- }
1994
- .logo-dropzone {
1995
- position: relative;
1996
- width: 104px;
1997
- height: 104px;
1998
- flex: none;
1999
- border-radius: var(--radius-lg);
2000
- border: 1.5px dashed var(--color-border-strong);
2001
- background: var(--color-bg-subtle);
2002
- display: grid;
2003
- place-items: center;
2004
- overflow: hidden;
2005
- cursor: pointer;
2006
- padding: 6px;
2007
- transition:
2008
- border-color 0.15s ease,
2009
- background 0.15s ease;
2010
- }
2011
- .logo-dropzone:hover,
2012
- .logo-dropzone:focus-visible {
2013
- border-color: var(--color-primary);
2014
- background: var(--color-primary-soft);
2015
- outline: none;
2016
- }
2017
- .logo-dropzone.has-image {
2018
- border-style: solid;
2019
- background: var(--color-surface);
2020
- }
2021
- .logo-dropzone-empty {
2022
- display: grid;
2023
- place-items: center;
2024
- color: var(--color-text-subtle);
2025
- }
2026
- .logo-dropzone-img {
2027
- max-width: 100%;
2028
- max-height: 100%;
2029
- object-fit: contain;
2030
- border-radius: var(--radius);
2031
- }
2032
- // hover overlay (camera + label), like the Minimals avatar uploader
2033
- .logo-dropzone-overlay {
2034
- position: absolute;
2035
- inset: 0;
2036
- display: flex;
2037
- flex-direction: column;
2038
- align-items: center;
2039
- justify-content: center;
2040
- gap: 4px;
2041
- background: var(--color-scrim);
2042
- color: var(--color-on-scrim);
2043
- font-size: 12px;
2044
- font-weight: 600;
2045
- opacity: 0;
2046
- transition: opacity 0.15s ease;
2047
- }
2048
- .logo-dropzone:hover .logo-dropzone-overlay,
2049
- .logo-dropzone:focus-visible .logo-dropzone-overlay {
2050
- opacity: 1;
2051
- }
2052
- .logo-upload-body {
2053
- flex: 1;
2054
- min-width: 200px;
2055
- }
2056
-
2057
- // ── Drawer / Sidebar menu ──
2058
- // AppSidebarMenu — react-bootstrap Offcanvas mapped onto the app tokens plus
2059
- // a flex column layout for the header / scrolling body / sticky footer.
2060
- .app-sidebar-menu.offcanvas {
2061
- --bs-offcanvas-width: var(--asm-width, 420px);
2062
- --bs-offcanvas-bg: var(--color-surface);
2063
- --bs-offcanvas-color: var(--color-text);
2064
- --bs-offcanvas-border-color: var(--color-border);
2065
- --bs-offcanvas-border-width: 1px;
2066
- --bs-offcanvas-padding-x: 0;
2067
- --bs-offcanvas-padding-y: 0;
2068
- max-width: 100vw;
2069
- display: flex;
2070
- flex-direction: column;
2071
- }
2072
- .app-sidebar-menu .offcanvas-body {
2073
- padding: 0;
2074
- display: flex;
2075
- flex-direction: column;
2076
- flex: 1;
2077
- min-height: 0;
2078
- }
2079
- .app-sidebar-menu .asm-footer {
2080
- padding: 16px 20px;
2081
- border-top: 1px solid var(--color-border);
2082
- background: var(--color-surface);
2083
- }
2084
-
2085
- .drawer {
2086
- position: fixed;
2087
- top: 0;
2088
- right: 0;
2089
- width: 420px;
2090
- max-width: 100vw;
2091
- height: 100vh;
2092
- background: var(--color-surface);
2093
- border-left: 1px solid var(--color-border);
2094
- z-index: 200;
2095
- transform: translateX(100%);
2096
- transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
2097
- display: flex;
2098
- flex-direction: column;
2099
- }
2100
- .drawer.open {
2101
- transform: translateX(0);
2102
- }
2103
- .drawer-backdrop {
2104
- position: fixed;
2105
- inset: 0;
2106
- background: rgba(15, 23, 42, 0.4);
2107
- opacity: 0;
2108
- pointer-events: none;
2109
- transition: opacity 0.2s;
2110
- z-index: 150;
2111
- }
2112
- .drawer-backdrop.open {
2113
- opacity: 1;
2114
- pointer-events: auto;
2115
- }
2116
- .drawer-header {
2117
- padding: 18px 20px;
2118
- border-bottom: 1px solid var(--color-border);
2119
- display: flex;
2120
- align-items: center;
2121
- justify-content: space-between;
2122
- background: var(--color-surface);
2123
- }
2124
- .drawer-title {
2125
- font-weight: 600;
2126
- font-size: 16px;
2127
- letter-spacing: -0.01em;
2128
- display: flex;
2129
- align-items: center;
2130
- gap: 10px;
2131
- color: var(--color-text);
2132
- }
2133
- .drawer-title-ic {
2134
- flex: none;
2135
- display: grid;
2136
- place-items: center;
2137
- width: 30px;
2138
- height: 30px;
2139
- border-radius: var(--radius);
2140
- corner-shape: squircle;
2141
- background: var(--color-primary-soft);
2142
- color: var(--color-primary);
2143
- }
2144
- // Sticky tools zone: search + section meta stay put while the list scrolls.
2145
- .drawer-tools {
2146
- padding: 16px 20px 12px;
2147
- border-bottom: 1px solid var(--color-border);
2148
- background: var(--color-surface);
2149
- }
2150
- .drawer-body {
2151
- flex: 1;
2152
- overflow-y: auto;
2153
- padding: 14px 16px 20px;
2154
- }
2155
-
2156
- // ── Tabs ──
2157
- // The tab strip both surfaces render. React drives it as a controlled strip and
2158
- // Razor additionally renders Bootstrap panels beneath it, but the strip markup —
2159
- // and therefore the look — is identical.
2160
- .app-tabs {
2161
- display: flex;
2162
- align-items: center;
2163
- gap: 2px;
2164
- border-bottom: 1px solid var(--color-border);
2165
- overflow-x: auto;
2166
- scrollbar-width: none;
2167
- // Declared explicitly because the React AppTabs renders a react-bootstrap
2168
- // <Nav>, so the element also carries `.nav` — whose `flex-wrap: wrap` would
2169
- // otherwise apply unopposed (equal specificity, and we declared nothing),
2170
- // wrapping the tabs onto a second line instead of scrolling.
2171
- flex-wrap: nowrap;
2172
- }
2173
- .app-tabs::-webkit-scrollbar {
2174
- display: none;
2175
- }
2176
- .app-tab {
2177
- display: inline-flex;
2178
- align-items: center;
2179
- gap: 8px;
2180
- padding: 10px 14px;
2181
- border: 0;
2182
- // Sits on the strip's own hairline rather than adding a second one.
2183
- border-bottom: 2px solid transparent;
2184
- margin-bottom: -1px;
2185
- background: transparent;
2186
- font-family: inherit;
2187
- font-size: 14px;
2188
- font-weight: 500;
2189
- line-height: 1.4;
2190
- color: var(--color-text-muted);
2191
- white-space: nowrap;
2192
- cursor: pointer;
2193
- transition:
2194
- color 0.12s ease,
2195
- border-color 0.12s ease;
2196
- }
2197
- .app-tab:hover:not(:disabled):not(.active) {
2198
- color: var(--color-text);
2199
- }
2200
- .app-tab.active {
2201
- color: var(--color-primary);
2202
- border-bottom-color: var(--color-primary);
2203
- // The active tab carries weight as well as colour — colour alone is a weak
2204
- // signal for anyone who reads the strip at a glance or cannot distinguish it.
2205
- font-weight: 600;
2206
- }
2207
- .app-tab:disabled {
2208
- color: var(--color-text-subtle);
2209
- cursor: not-allowed;
2210
- }
2211
- .app-tab:focus-visible {
2212
- outline: 2px solid var(--color-primary);
2213
- outline-offset: -2px;
2214
- border-radius: var(--radius-sm);
2215
- }
2216
- // A destructive tab reads as destructive whether or not it is the active one.
2217
- .app-tab.danger {
2218
- color: var(--color-danger);
2219
- }
2220
- .app-tab.danger:hover:not(:disabled):not(.active) {
2221
- color: var(--color-danger-active);
2222
- }
2223
- .app-tab.danger.active {
2224
- color: var(--color-danger);
2225
- border-bottom-color: var(--color-danger);
2226
- }
2227
- .app-tab.danger .app-tab-count {
2228
- background: var(--color-danger-soft);
2229
- color: var(--color-danger-strong);
2230
- }
2231
- // Trailing count pill.
2232
- .app-tab-count {
2233
- display: inline-flex;
2234
- align-items: center;
2235
- justify-content: center;
2236
- min-width: 20px;
2237
- padding: 0 6px;
2238
- height: 20px;
2239
- border-radius: 999px;
2240
- background: var(--color-bg-muted);
2241
- color: var(--color-text-subtle);
2242
- font-size: 12px;
2243
- font-weight: 600;
2244
- }
2245
- .app-tab.active .app-tab-count {
2246
- background: var(--color-primary-soft);
2247
- color: var(--color-primary);
2248
- }
2249
-
2250
- // Wrapper for a self-contained tabs widget (strip + panels), as <app-tabs>
2251
- // renders server-side. React composes the strip and its panels itself.
2252
- .app-tabs-group > .tab-content {
2253
- padding-top: 16px;
2254
- }
2255
-
2256
- // Pills variant — a segmented control rather than an underline.
2257
- .app-tabs-pills {
2258
- gap: 4px;
2259
- border-bottom: 0;
2260
- }
2261
- .app-tabs-pills .app-tab {
2262
- border-bottom: 0;
2263
- margin-bottom: 0;
2264
- border-radius: var(--radius);
2265
- }
2266
- .app-tabs-pills .app-tab.active {
2267
- background: var(--color-primary-soft);
2268
- }
2269
-
2270
- // ── Search field ──
2271
- // The search control both surfaces render: a rounded field with a leading
2272
- // magnifier that takes the brand colour on focus, plus a clear button that
2273
- // appears once there's a value.
2274
- .search-wrap {
2275
- position: relative;
2276
- display: flex;
2277
- align-items: center;
2278
- width: 100%;
2279
- }
2280
- .search-wrap > .search-icon {
2281
- position: absolute;
2282
- left: 13px;
2283
- top: 50%;
2284
- transform: translateY(-50%);
2285
- display: grid;
2286
- place-items: center;
2287
- color: var(--color-text-subtle);
2288
- pointer-events: none;
2289
- transition: color 0.12s ease;
2290
- }
2291
- .search-wrap:focus-within > .search-icon {
2292
- color: var(--color-primary);
2293
- }
2294
- .search-field,
2295
- .search-input {
2296
- width: 100%;
2297
- padding-left: 40px;
2298
- padding-right: 36px;
2299
- }
2300
- .search-clear {
2301
- position: absolute;
2302
- right: 8px;
2303
- top: 50%;
2304
- transform: translateY(-50%);
2305
- display: grid;
2306
- place-items: center;
2307
- width: 26px;
2308
- height: 26px;
2309
- padding: 0;
2310
- border: 0;
2311
- border-radius: 999px;
2312
- background: transparent;
2313
- color: var(--color-text-subtle);
2314
- cursor: pointer;
2315
- opacity: 0;
2316
- pointer-events: none;
2317
- transition:
2318
- opacity 0.12s ease,
2319
- color 0.12s ease;
2320
- }
2321
- .search-wrap.has-value .search-clear,
2322
- .search-wrap:focus-within .search-clear {
2323
- opacity: 1;
2324
- pointer-events: auto;
2325
- }
2326
- .search-clear:hover {
2327
- color: var(--color-text);
2328
- }
2329
-
2330
- // ── Empty state ──
2331
- .empty-state {
2332
- text-align: center;
2333
- padding: 48px 24px;
2334
- color: var(--color-text-subtle);
2335
- }
2336
- .empty-state-title {
2337
- font-size: 16px;
2338
- font-weight: 600;
2339
- line-height: 1.35;
2340
- color: var(--color-text);
2341
- }
2342
- .empty-state-message {
2343
- margin: 6px 0 0;
2344
- font-size: 14px;
2345
- line-height: 1.5;
2346
- color: var(--color-text-muted);
2347
- }
2348
- .empty-state-action {
2349
- margin-top: 16px;
2350
- }
2351
- // Tighter scale for a widget/tile, as opposed to a full card or page.
2352
- .empty-state-sm {
2353
- padding: 28px 16px;
2354
- }
2355
- .empty-state-sm .empty-state-title {
2356
- font-size: 14px;
2357
- }
2358
- .empty-state-sm .empty-state-message {
2359
- font-size: 12px;
2360
- }
2361
- .empty-state-icon {
2362
- width: 48px;
2363
- height: 48px;
2364
- border-radius: var(--radius-lg);
2365
- background: var(--color-primary-soft);
2366
- color: var(--color-primary);
2367
- display: inline-flex;
2368
- align-items: center;
2369
- justify-content: center;
2370
- margin-bottom: 14px;
2371
- }
2372
-
2373
- // The empty state a DataTable renders in place of its rows. Distinct from
2374
- // the card-level .empty-state above: a 50px primary-tinted disc, then a 32px
2375
- // gap to the title and 12px to the subtitle.
2376
- .tbl-empty {
2377
- display: flex;
2378
- flex-direction: column;
2379
- align-items: center;
2380
- text-align: center;
2381
- padding: 40px 24px;
2382
- }
2383
- .tbl-empty-icon {
2384
- display: inline-grid;
2385
- place-items: center;
2386
- flex: none;
2387
- width: 50px;
2388
- height: 50px;
2389
- border-radius: 50%;
2390
- background: color-mix(in srgb, var(--color-primary) 10%, transparent);
2391
- color: var(--color-primary);
2392
- margin-bottom: 16px;
2393
- }
2394
- .tbl-empty-title {
2395
- font-size: 18px;
2396
- font-weight: 600;
2397
- line-height: 1.35;
2398
- color: var(--color-text);
2399
- }
2400
- .tbl-empty-text {
2401
- margin-top: 8px;
2402
- font-size: 16px;
2403
- font-weight: 400;
2404
- line-height: 1.5;
2405
- color: var(--color-text-muted);
2406
- }
2407
- // An action (e.g. "New assessment") sits under the subtitle when the caller
2408
- // supplies one.
2409
- .tbl-empty-action {
2410
- margin-top: 20px;
2411
- }
2412
-
2413
- .lx-table-empty {
2414
- padding: 36px 24px;
2415
- text-align: center;
2416
- color: var(--color-text-muted);
2417
- }
2418
- // The empty state owns its own padding and is not a row you can act on — no
2419
- // cell padding, no border, and no hover fill from .table / .table-ledger.
2420
- .table td.lx-table-empty:has(> .tbl-empty) {
2421
- padding: 0;
2422
- border-bottom: 0;
2423
- }
2424
- .table tbody tr:hover td.lx-table-empty:has(> .tbl-empty),
2425
- .table-ledger tbody tr:hover td.lx-table-empty:has(> .tbl-empty) {
2426
- background: transparent;
2427
- }
2428
-
2429
- // ── Table (base) ──
2430
- // The shared data-table look, on the plain `.table` class Bootstrap already
2431
- // gives every table. Without this a product's tables fall back to stock
2432
- // Bootstrap and drift from the design system; `.table-ledger` below is the
2433
- // denser, card-inset variant, not a replacement for having a base.
2434
- .table {
2435
- --bs-table-bg: transparent;
2436
- --bs-table-color: var(--color-text);
2437
- width: 100%;
2438
- margin-bottom: 0;
2439
- color: var(--color-text);
2440
- vertical-align: middle;
2441
- border-color: var(--color-border);
2442
- // `separate` is required for a cell to render a border-radius at all —
2443
- // Bootstrap reboot sets `collapse`, under which .table-ledger's rounded
2444
- // header corners silently do nothing. border-spacing 0 keeps cells flush.
2445
- border-collapse: separate;
2446
- border-spacing: 0;
2447
- }
2448
-
2449
- // The cell rules are wrapped in :where() so they carry ZERO class specificity
2450
- // — `:where(.table) > thead > tr > th` is (0,0,3), which any single-class
2451
- // variant selector such as `.table-ledger thead th` (0,1,2) outranks.
18
+ // This file is now an INDEX. The rules live in ./wui/*.scss, split by domain
19
+ // because one 5,000-line stylesheet made finding a component a grep.
2452
20
  //
2453
- // Without this the base out-ranks its own variant: written plainly,
2454
- // `.table > thead > tr > th` is (0,1,3) and beats `.table-ledger thead th`,
2455
- // so the ledger lost its own border-bottom/line-height/font-family to the
2456
- // base. A base layer must always sit *under* the variants built on it.
2457
- :where(.table) > thead > tr > th {
2458
- height: 42px;
2459
- padding: 0 12px;
2460
- background: var(--color-bg-muted);
2461
- border-bottom: 1px solid var(--color-border);
2462
- font-size: 12px;
2463
- font-weight: 500;
2464
- letter-spacing: 0.04em;
2465
- text-transform: uppercase;
2466
- color: var(--color-text-subtle);
2467
- white-space: nowrap;
2468
- vertical-align: middle;
2469
- transition:
2470
- background-color 0.12s ease,
2471
- color 0.12s ease;
2472
- }
2473
- :where(.table) > tbody > tr > td {
2474
- padding: 12px;
2475
- border-bottom: 1px solid var(--color-border);
2476
- font-size: 14px;
2477
- color: var(--color-text-muted);
2478
- vertical-align: middle;
2479
- transition: background-color 0.12s ease;
2480
- }
2481
- :where(.table) > tbody > tr:last-child > td {
2482
- border-bottom: 0;
2483
- }
2484
- :where(.table) > tbody > tr:hover > td {
2485
- background: var(--color-bg-subtle);
2486
- }
2487
- @media (prefers-reduced-motion: reduce) {
2488
- :where(.table) > thead > tr > th,
2489
- :where(.table) > tbody > tr > td {
2490
- transition: none;
2491
- }
2492
- }
2493
- // Numeric / action columns align to the trailing edge.
2494
- .table .cell-end,
2495
- .table thead th.cell-end {
2496
- text-align: right;
2497
- }
2498
-
2499
- // ── Table + pagination ──
2500
- // DataTable cell helpers
2501
- .table-ledger .lx-cell-end,
2502
- .table-ledger thead th.lx-cell-end {
2503
- text-align: right;
2504
- }
2505
-
2506
- // Ledger chrome — the shared table look. Applies to every .table-ledger: a
2507
- // floating 42px header band, airy rows on a hairline rule, and an inset from
2508
- // the card edge. Column geometry is NOT set here.
2509
- .table-wrap:has(> .table-ledger) {
2510
- padding: 20px 20px 0;
2511
- }
2512
- .table-ledger {
2513
- border-spacing: 0;
2514
- }
2515
- .table-ledger thead th {
2516
- height: 42px;
2517
- padding: 0 12px;
2518
- background: var(--color-bg-muted);
2519
- border-bottom: none;
2520
- font-family: "Inter", var(--font-sans, sans-serif);
2521
- font-size: 12px;
2522
- font-weight: 500;
2523
- line-height: 17px;
2524
- letter-spacing: 0.04em;
2525
- text-transform: uppercase;
2526
- color: var(--color-text-subtle);
2527
- vertical-align: middle;
2528
- }
2529
- .table-ledger thead th:first-child {
2530
- padding-left: 20px;
2531
- border-radius: var(--radius-sm) 0 0 var(--radius-sm);
2532
- }
2533
- .table-ledger thead th:last-child {
2534
- padding-right: 20px;
2535
- border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
2536
- }
2537
- .table-ledger tbody td {
2538
- height: 54px;
2539
- padding: 12px;
2540
- border-bottom: 0.5px solid var(--color-border);
2541
- font-family: "Inter", var(--font-sans, sans-serif);
2542
- font-size: 12px;
2543
- font-weight: 500;
2544
- line-height: 15px;
2545
- color: var(--color-text-muted);
2546
- }
2547
- .table-ledger tbody td:first-child {
2548
- padding-left: 20px;
2549
- }
2550
- .table-ledger tbody td:last-child {
2551
- padding-right: 20px;
2552
- }
2553
- .table-ledger tbody tr:last-child td {
2554
- border-bottom: none;
2555
- }
2556
- .table-ledger tbody tr:hover td {
2557
- background: var(--color-bg-subtle);
2558
- }
2559
- .table-ledger .u-meta {
2560
- gap: 2px;
2561
- }
2562
- .table-ledger .u-name {
2563
- font-size: 14px;
2564
- font-weight: 500;
2565
- line-height: 19px;
2566
- color: var(--color-text);
2567
- }
2568
- @media (max-width: 575.98px) {
2569
- .table-ledger .u-name {
2570
- font-size: 12px;
2571
- line-height: normal;
2572
- margin-bottom: 2px;
2573
- }
2574
- }
2575
- // An identity-cell name is often a link, so it inherits Bootstrap's
2576
- // underline-on-hover. The colour shift is the affordance here.
2577
- .table-ledger a.u-name,
2578
- .table-ledger a.u-name:hover {
2579
- text-decoration: none;
2580
- }
2581
- // Identity-cell subline. `.u-email` is in the group because an identity cell
2582
- // renders the secondary line as either a generic .lx-text-xs or a .u-email.
2583
- .table-ledger .u-meta > .u-email,
2584
- .table-ledger .u-meta > .lx-text-xs {
2585
- font-size: 12px;
2586
- font-weight: 400;
2587
- line-height: 15px;
2588
- color: var(--color-text-subtle);
2589
- }
2590
- .table-ledger .u-row-actions {
2591
- gap: 6px;
2592
- }
2593
-
2594
- // Ledger tables: 20px edge padding so header labels + first/last cells align
2595
- // with the card edge — shared across every .table-ledger surface.
2596
- .table-ledger thead th:first-child,
2597
- .table-ledger tbody td:first-child {
2598
- padding-left: 20px;
2599
- }
2600
- .table-ledger thead th:last-child,
2601
- .table-ledger tbody td:last-child {
2602
- padding-right: 20px;
2603
- }
2604
-
2605
- // Pagination footer — rows-per-page + range + prev/next, sits flush under a
2606
- // table.
2607
- .table-pagination {
2608
- display: flex;
2609
- align-items: center;
2610
- justify-content: flex-end;
2611
- gap: 22px;
2612
- padding: 12px 16px;
2613
- border-top: 1px solid var(--color-border);
2614
- font-size: 13px;
2615
- color: var(--color-text-muted);
2616
- flex-wrap: wrap;
2617
- }
2618
- // Rows-per-page sits at the leading edge; range + nav stay grouped on the
2619
- // right.
2620
- .tpg-rows {
2621
- display: inline-flex;
2622
- align-items: center;
2623
- gap: 8px;
2624
- white-space: nowrap;
2625
- margin-right: auto;
2626
- font-size: 12px;
2627
- font-weight: 500;
2628
- text-transform: uppercase;
2629
- color: var(--color-text-subtle);
2630
- }
2631
- .select.tpg-select {
2632
- width: auto;
2633
- min-width: 64px;
2634
- height: 32px;
2635
- min-height: 32px;
2636
- padding: 0 28px 0 12px;
2637
- font-size: 13px;
2638
- font-weight: 600;
2639
- border-radius: var(--radius);
2640
- background-position: right 8px center;
2641
- background-size: 14px;
2642
- }
2643
- .tpg-range {
2644
- font-variant-numeric: tabular-nums;
2645
- white-space: nowrap;
2646
- font-size: 14px;
2647
- font-weight: 500;
2648
- color: var(--color-text-muted);
2649
- }
2650
- .tpg-nav {
2651
- display: inline-flex;
2652
- align-items: center;
2653
- gap: 4px;
2654
- }
2655
- .tpg-arrow {
2656
- display: grid;
2657
- place-items: center;
2658
- width: 32px;
2659
- height: 32px;
2660
- border: none;
2661
- background: transparent;
2662
- color: var(--color-text-muted);
2663
- border-radius: 50%;
2664
- cursor: pointer;
2665
- transition:
2666
- background 0.14s ease,
2667
- color 0.14s ease;
2668
- }
2669
- .tpg-arrow:hover:not(:disabled) {
2670
- background: color-mix(in srgb, var(--color-primary) 10%, transparent);
2671
- color: var(--color-primary);
2672
- }
2673
- // Text-5 already reads as "unavailable" — no opacity dimming on top of it.
2674
- .tpg-arrow:disabled {
2675
- color: var(--color-border-strong);
2676
- cursor: not-allowed;
2677
- }
2678
-
2679
- // Horizontal scroll guard + flush-fit inside a card. Wrapping the table here
2680
- // also stops `.card > .table` header-corner rounding from applying mid-card.
2681
- .table-wrap {
2682
- overflow-x: auto;
2683
- max-width: 100%;
2684
- // Keep the horizontal swipe inside the table — don't let it drag the
2685
- // page — and give touch devices momentum scrolling. Scrollbar is
2686
- // thinned/hidden so the scroll feels native.
2687
- overscroll-behavior-x: contain;
2688
- -webkit-overflow-scrolling: touch;
2689
- scrollbar-width: thin;
2690
- scrollbar-color: var(--color-border-strong) transparent;
2691
- }
2692
- .table-wrap::-webkit-scrollbar {
2693
- height: 6px;
2694
- }
2695
- .table-wrap::-webkit-scrollbar-track {
2696
- background: transparent;
2697
- }
2698
- .table-wrap::-webkit-scrollbar-thumb {
2699
- background: var(--color-border-strong);
2700
- border-radius: 999px;
2701
- }
2702
-
2703
- // Row-selection checkbox column.
2704
- .table-ledger th.lx-select-cell,
2705
- .table-ledger td.lx-select-cell {
2706
- width: 44px;
2707
- min-width: 44px;
2708
- max-width: 44px;
2709
- text-align: center;
2710
- padding-left: 12px;
2711
- padding-right: 0;
2712
- }
2713
- // Selection is a brand-tinted state on the CELLS, not a grey fill on the row:
2714
- // cells carry their own background (hover sets it too), so a background on the
2715
- // <tr> is painted over and never shows. Grey also reads as "disabled" rather
2716
- // than "chosen".
2717
- .table-ledger tbody tr.is-selected > td {
2718
- background: var(--color-primary-soft);
2719
- }
2720
- .table-ledger tbody tr.is-selected:hover > td {
2721
- background: color-mix(in srgb, var(--color-primary) 14%, transparent);
2722
- }
2723
-
2724
- // Row status affordance — a colour bar on the leading edge, so a row's state is
2725
- // legible without relying on a tint alone.
2726
- .table-ledger tbody tr[data-status] > td:first-child {
2727
- box-shadow: inset 3px 0 0 0 var(--color-border-strong);
2728
- }
2729
- .table-ledger tbody tr[data-status="success"] > td:first-child {
2730
- box-shadow: inset 3px 0 0 0 var(--color-success);
2731
- }
2732
- .table-ledger tbody tr[data-status="warning"] > td:first-child {
2733
- box-shadow: inset 3px 0 0 0 var(--color-warning);
2734
- }
2735
- .table-ledger tbody tr[data-status="danger"] > td:first-child {
2736
- box-shadow: inset 3px 0 0 0 var(--color-danger);
2737
- }
2738
- .table-ledger tbody tr[data-status="info"] > td:first-child {
2739
- box-shadow: inset 3px 0 0 0 var(--color-info);
2740
- }
2741
-
2742
- // Row actions — icon-only buttons replacing a kebab dropdown.
2743
- .u-row-actions {
2744
- display: flex;
2745
- align-items: center;
2746
- justify-content: flex-end;
2747
- gap: 6px;
2748
- // Action cells never wrap — a status hint stays on one line.
2749
- white-space: nowrap;
2750
- }
2751
- .u-action {
2752
- display: inline-grid;
2753
- place-items: center;
2754
- flex: none;
2755
- width: 36px;
2756
- height: 36px;
2757
- padding: 0;
2758
- border: none;
2759
- background: var(--color-bg-subtle);
2760
- border-radius: 999px;
2761
- color: var(--color-text-subtle);
2762
- cursor: pointer;
2763
- transition:
2764
- background 0.12s ease,
2765
- color 0.12s ease;
2766
- position: relative;
2767
- }
2768
- // Touch pointers need a 44px minimum target. Grow the hit area, not the
2769
- // ink, so row density and alignment are untouched.
2770
- @media (pointer: coarse) {
2771
- .u-action::after {
2772
- content: "";
2773
- position: absolute;
2774
- top: 50%;
2775
- left: 50%;
2776
- transform: translate(-50%, -50%);
2777
- width: 44px;
2778
- height: 44px;
2779
- }
2780
- }
2781
- .u-action svg {
2782
- flex: none;
2783
- }
2784
- .u-action:hover:not(:disabled) {
2785
- background: color-mix(in srgb, var(--color-primary) 10%, transparent);
2786
- color: var(--color-primary);
2787
- }
2788
- .u-action:disabled {
2789
- opacity: 0.4;
2790
- cursor: not-allowed;
2791
- }
2792
- .u-action.danger {
2793
- color: var(--color-danger);
2794
- }
2795
- .u-action.danger:hover:not(:disabled) {
2796
- background: color-mix(in srgb, var(--color-danger) 10%, transparent);
2797
- color: var(--color-danger);
2798
- }
2799
- .u-action.u-action-preview {
2800
- background: var(--color-primary-soft);
2801
- color: var(--color-primary);
2802
- }
2803
- .u-action.u-action-download {
2804
- background: var(--color-success-soft);
2805
- color: var(--color-success-active);
2806
- }
2807
- .u-action.u-action-share {
2808
- background: var(--color-warning-soft);
2809
- color: var(--color-warning-active);
2810
- }
2811
- .u-action.u-action-preview:hover:not(:disabled) {
2812
- background: var(--color-primary-soft);
2813
- color: var(--color-primary);
2814
- }
2815
- .u-action.u-action-download:hover:not(:disabled) {
2816
- background: var(--color-success-soft);
2817
- color: var(--color-success-active);
2818
- }
2819
- .u-action.u-action-share:hover:not(:disabled) {
2820
- background: var(--color-warning-soft);
2821
- color: var(--color-warning-active);
2822
- }
2823
- .u-action.u-action-danger:hover {
2824
- color: var(--color-danger);
2825
- background: color-mix(in srgb, var(--color-danger) 10%, transparent);
2826
- }
2827
-
2828
- // Identity cell — avatar + stacked name/email.
2829
- .u-cell {
2830
- display: flex;
2831
- align-items: center;
2832
- gap: 14px;
2833
- }
2834
- .u-avatar {
2835
- width: 40px;
2836
- height: 40px;
2837
- border-radius: 50%;
2838
- flex: none;
2839
- display: grid;
2840
- place-items: center;
2841
- font-size: 16px;
2842
- font-weight: 600;
2843
- color: var(--color-primary);
2844
- background: color-mix(in srgb, var(--color-primary) 20%, transparent);
2845
- }
2846
- .u-meta {
2847
- display: flex;
2848
- flex-direction: column;
2849
- min-width: 0;
2850
- }
2851
- .u-name {
2852
- display: inline-flex;
2853
- align-items: center;
2854
- gap: 8px;
2855
- font-size: 14px;
2856
- font-weight: 600;
2857
- color: var(--color-text);
2858
- }
2859
- .u-email {
2860
- font-size: 14px;
2861
- font-weight: 400;
2862
- color: var(--color-text-muted);
2863
- }
2864
- .u-you {
2865
- padding: 1px 7px;
2866
- font-size: 10.5px;
2867
- }
2868
-
2869
- // ── Modal ──
2870
- // Chrome stays on `.lx-modal-*`. The shell is react-bootstrap <Modal>:
2871
- // .modal.lx-modal-shell ← className (layout container)
2872
- // .modal-backdrop.lx-modal-dim ← backdropClassName (dim only)
2873
- // .modal-dialog.lx-rb-dialog ← dialogClassName
2874
- // .modal-content.lx-modal ← contentClassName (the card)
2875
- // Do NOT put layout on backdropClassName — that targets a sibling, not
2876
- // .modal.
2877
- .modal-backdrop.lx-modal-dim {
2878
- background: rgba(15, 23, 42, 0.45);
2879
- z-index: 1500;
2880
- }
2881
- .modal.lx-modal-shell {
2882
- --bs-modal-bg: transparent;
2883
- display: flex !important;
2884
- align-items: center;
2885
- justify-content: center;
2886
- z-index: 1505;
2887
- padding: 20px;
2888
- }
2889
- // RB dialog shell: no Bootstrap max-width/margin so `.lx-modal` owns sizing.
2890
- .modal.lx-modal-shell .modal-dialog.lx-rb-dialog {
2891
- margin: 0;
2892
- max-width: none;
2893
- width: auto;
2894
- pointer-events: none;
2895
- display: flex;
2896
- align-items: center;
2897
- justify-content: center;
2898
- }
2899
- .modal.lx-modal-shell .modal-dialog.lx-rb-dialog .modal-content {
2900
- pointer-events: auto;
2901
- border: none;
2902
- // Lift off the dim backdrop: a hairline ring defines the edge (carries
2903
- // elevation in dark mode where a drop shadow barely reads) + a soft,
2904
- // deep drop shadow.
2905
- box-shadow:
2906
- 0 0 0 1px color-mix(in srgb, var(--color-text) 10%, transparent),
2907
- var(--shadow-overlay-lg);
2908
- background: var(--color-surface);
2909
- }
2910
- .lx-modal {
2911
- background: var(--color-surface);
2912
- border-radius: var(--radius-xl);
2913
- corner-shape: squircle;
2914
- border: none;
2915
- width: 100%;
2916
- max-width: 440px;
2917
- max-height: calc(100vh - 40px);
2918
- display: flex;
2919
- flex-direction: column;
2920
- overflow: hidden;
2921
- }
2922
- .lx-modal-wide {
2923
- max-width: 1240px;
2924
- }
2925
-
2926
- // Side drawer — the dialog docks to the right edge at 30% of the viewport
2927
- // and slides in, instead of sitting centred. Same chrome, different
2928
- // geometry.
2929
- .modal.lx-modal-shell.is-side {
2930
- align-items: stretch;
2931
- justify-content: flex-end;
2932
- padding: 0;
2933
- }
2934
- .modal.lx-modal-shell.is-side .modal-dialog.lx-rb-dialog {
2935
- height: 100%;
2936
- width: auto;
2937
- max-width: none;
2938
- align-items: stretch;
2939
- justify-content: flex-end;
2940
- }
2941
- .lx-modal-side {
2942
- width: 30vw;
2943
- min-width: 420px;
2944
- max-width: none;
2945
- max-height: 100vh;
2946
- height: 100vh;
2947
- border-radius: 0;
2948
- corner-shape: normal;
2949
- animation: lx-drawer-in 0.32s cubic-bezier(0.22, 1, 0.36, 1);
2950
- }
2951
- @media (max-width: 991.98px) {
2952
- .lx-modal-side {
2953
- width: 70vw;
2954
- }
2955
- }
2956
- @media (max-width: 575.98px) {
2957
- .lx-modal-side {
2958
- width: 100vw;
2959
- }
2960
- }
2961
- @keyframes lx-drawer-in {
2962
- from {
2963
- transform: translateX(100%);
2964
- }
2965
- to {
2966
- transform: translateX(0);
2967
- }
2968
- }
2969
- @media (prefers-reduced-motion: reduce) {
2970
- .lx-modal-side {
2971
- animation: none;
2972
- }
2973
- }
2974
- .lx-modal-header {
2975
- flex: 0 0 auto;
2976
- display: flex;
2977
- align-items: center;
2978
- justify-content: space-between;
2979
- gap: 12px;
2980
- padding: 16px 20px;
2981
- border-bottom: 1px solid var(--color-border);
2982
- }
2983
- .lx-modal-body {
2984
- flex: 1 1 auto;
2985
- min-height: 0;
2986
- overflow-y: auto;
2987
- overscroll-behavior: contain;
2988
- scroll-behavior: smooth;
2989
- -webkit-overflow-scrolling: touch;
2990
- scrollbar-width: thin;
2991
- scrollbar-color: var(--color-border-strong) transparent;
2992
- padding: 20px 22px 24px;
2993
- }
2994
- .lx-modal-body::-webkit-scrollbar {
2995
- width: 8px;
2996
- }
2997
- .lx-modal-body::-webkit-scrollbar-track {
2998
- background: transparent;
2999
- }
3000
- .lx-modal-body::-webkit-scrollbar-thumb {
3001
- background: transparent;
3002
- border-radius: 999px;
3003
- border: 2px solid transparent;
3004
- background-clip: padding-box;
3005
- }
3006
- .lx-modal-body:hover::-webkit-scrollbar-thumb {
3007
- background: var(--color-border-strong);
3008
- background-clip: padding-box;
3009
- }
3010
- @media (prefers-reduced-motion: reduce) {
3011
- .lx-modal-body {
3012
- scroll-behavior: auto;
3013
- }
3014
- }
3015
- // Modal heading group: optional contextual (Bulk) icon tile + title/subtitle.
3016
- .lx-modal-heading {
3017
- display: flex;
3018
- align-items: center;
3019
- gap: 10px;
3020
- min-width: 0;
3021
- }
3022
- .lx-modal-headtext {
3023
- min-width: 0;
3024
- }
3025
- .lx-modal-headicon {
3026
- display: inline-flex;
3027
- align-items: center;
3028
- justify-content: center;
3029
- width: 34px;
3030
- height: 34px;
3031
- border-radius: var(--radius);
3032
- corner-shape: squircle;
3033
- flex: none;
3034
- background: var(--color-primary-soft);
3035
- color: var(--color-primary);
3036
- }
3037
-
3038
- // Break long unbroken names so they wrap within the heading instead of
3039
- // running under the close button.
3040
- .lx-modal-title {
3041
- font-size: 20px;
3042
- font-weight: 600;
3043
- color: var(--color-text);
3044
- overflow-wrap: anywhere;
3045
- }
3046
- .lx-modal-subtitle {
3047
- margin-top: 1px;
3048
- font-size: 12px;
3049
- font-weight: 400;
3050
- color: var(--color-text-muted);
3051
- overflow-wrap: anywhere;
3052
- }
3053
- .lx-form-section {
3054
- margin-top: 24px;
3055
- }
3056
- .lx-form-section:first-child {
3057
- margin-top: 0;
3058
- }
3059
- // Stretch controls to fill the column even though .field aligns to
3060
- // flex-start.
3061
- .lx-modal-body .field > .input,
3062
- .lx-modal-body .field > .search-select,
3063
- .lx-modal-body .field > .input-date-wrap,
3064
- .lx-modal-body .field > .datepicker,
3065
- .lx-modal-body .field > .phone-input-wrap,
3066
- .lx-modal-body .field > .field-error {
3067
- align-self: stretch;
3068
- }
3069
- .lx-form-section-head {
3070
- margin-bottom: 16px;
3071
- }
3072
- .lx-form-section-title {
3073
- font-size: 16px;
3074
- font-weight: 600;
3075
- text-transform: uppercase;
3076
- letter-spacing: 0.04em;
3077
- color: var(--color-text-subtle);
3078
- }
3079
- .lx-modal-footer {
3080
- flex: 0 0 auto;
3081
- display: flex;
3082
- align-items: center;
3083
- justify-content: flex-end;
3084
- gap: 10px;
3085
- padding: 16px 22px;
3086
- border-top: 1px solid var(--color-border);
3087
- }
3088
-
3089
- // Modal close — a squircle at rest that morphs into a circle on hover while
3090
- // taking a danger tint, so dismissing reads as the destructive-ish gesture
3091
- // it is. Slower on the way in than the way out.
3092
- .lx-modal-close {
3093
- margin-left: auto;
3094
- display: grid;
3095
- place-items: center;
3096
- flex: none;
3097
- width: 36px;
3098
- height: 36px;
3099
- padding: 0;
3100
- border: none;
3101
- cursor: pointer;
3102
- line-height: 1;
3103
- background: var(--color-bg-subtle);
3104
- color: var(--color-text-muted);
3105
- border-radius: 999px;
3106
- corner-shape: squircle;
3107
- transition:
3108
- background 180ms cubic-bezier(0.4, 0, 0.2, 1),
3109
- color 180ms cubic-bezier(0.4, 0, 0.2, 1),
3110
- corner-shape 180ms cubic-bezier(0.4, 0, 0.2, 1);
3111
- }
3112
- .lx-modal-close:hover {
3113
- background: var(--color-danger-soft);
3114
- color: var(--color-danger);
3115
- corner-shape: round;
3116
- transition:
3117
- background 280ms cubic-bezier(0.16, 1, 0.3, 1),
3118
- color 280ms cubic-bezier(0.16, 1, 0.3, 1),
3119
- corner-shape 280ms cubic-bezier(0.16, 1, 0.3, 1);
3120
- }
3121
- .lx-modal-close:focus-visible {
3122
- outline: 2px solid var(--color-primary);
3123
- outline-offset: 2px;
3124
- }
3125
- // The shape morph is decorative; hold the colour cue for reduced-motion
3126
- // users.
3127
- @media (prefers-reduced-motion: reduce) {
3128
- .lx-modal-close,
3129
- .lx-modal-close:hover {
3130
- corner-shape: squircle;
3131
- transition:
3132
- background 1ms linear,
3133
- color 1ms linear;
3134
- }
3135
- }
3136
-
3137
- // ── Confirm dialog ──
3138
- // Confirmation dialog — centred status icon + title + message, two equal
3139
- // side-by-side CTAs. Runs headerless (Modal hideHeader), so the body
3140
- // carries the spacing and the footer drops its divider.
3141
- .lx-confirm-content {
3142
- display: flex;
3143
- flex-direction: column;
3144
- align-items: center;
3145
- text-align: center;
3146
- padding: 16px 12px 6px;
3147
- }
3148
- .lx-confirm-icon {
3149
- line-height: 0;
3150
- margin-bottom: 22px;
3151
- }
3152
- .lx-confirm-title {
3153
- margin: 0 0 10px;
3154
- font-size: 22px;
3155
- font-weight: 700;
3156
- letter-spacing: -0.01em;
3157
- color: var(--color-text);
3158
- }
3159
- .lx-confirm-msg {
3160
- margin: 0;
3161
- font-size: 15px;
3162
- line-height: 1.55;
3163
- color: var(--color-text-muted);
3164
- max-width: 40ch;
3165
- }
3166
- .lx-confirm-actions {
3167
- display: flex;
3168
- gap: 12px;
3169
- width: 100%;
3170
- }
3171
- .lx-modal-footer:has(.lx-confirm-actions) {
3172
- border-top: none;
3173
- padding-top: 0;
3174
- }
3175
- .lx-confirm-actions .btn {
3176
- flex: 1;
3177
- min-height: var(--wui-control-height);
3178
- height: var(--wui-control-height);
3179
- border-radius: var(--radius);
3180
- corner-shape: squircle;
3181
- padding-left: 16px;
3182
- padding-right: 16px;
3183
- font-size: 14px;
3184
- font-weight: 500;
3185
- }
3186
-
3187
- // ── Grid / layout utils ──
3188
- // Responsive card grids (Bootstrap has no grid-template utility).
3189
- .lx-grid {
3190
- display: grid;
3191
- grid-template-columns: 1fr;
3192
- }
3193
- .lx-cols-2 {
3194
- grid-template-columns: repeat(2, minmax(0, 1fr));
3195
- }
3196
- .lx-cols-3 {
3197
- grid-template-columns: repeat(3, minmax(0, 1fr));
3198
- }
3199
- .lx-cols-4 {
3200
- grid-template-columns: repeat(4, minmax(0, 1fr));
3201
- }
3202
- @media (min-width: 768px) {
3203
- .lx-grid-md-2 {
3204
- grid-template-columns: repeat(2, minmax(0, 1fr));
3205
- }
3206
- .lx-grid-md-3 {
3207
- grid-template-columns: repeat(3, minmax(0, 1fr));
3208
- }
3209
- .lx-grid-md-4 {
3210
- grid-template-columns: repeat(4, minmax(0, 1fr));
3211
- }
3212
- }
3213
- @media (min-width: 992px) {
3214
- .lx-grid-lg-2 {
3215
- grid-template-columns: repeat(2, minmax(0, 1fr));
3216
- }
3217
- .lx-grid-lg-3 {
3218
- grid-template-columns: repeat(3, minmax(0, 1fr));
3219
- }
3220
- .lx-grid-lg-4 {
3221
- grid-template-columns: repeat(4, minmax(0, 1fr));
3222
- }
3223
- }
3224
- .lx-col-span-2 {
3225
- grid-column: span 2 / span 2;
3226
- }
3227
- .lx-col-span-3 {
3228
- grid-column: span 3 / span 3;
3229
- }
3230
- .lx-col-span-full {
3231
- grid-column: 1 / -1;
3232
- }
3233
-
3234
- // ── Info tip ──
3235
- // Inline term help (InfoTip).
3236
- .infotip {
3237
- position: relative;
3238
- display: inline-flex;
3239
- vertical-align: middle;
3240
- margin-left: 5px;
3241
- }
3242
- .infotip-btn {
3243
- display: inline-flex;
3244
- align-items: center;
3245
- justify-content: center;
3246
- width: 16px;
3247
- height: 16px;
3248
- padding: 0;
3249
- border: none;
3250
- border-radius: 999px;
3251
- background: transparent;
3252
- color: var(--color-text-subtle);
3253
- cursor: help;
3254
- transform: scaleY(-1);
3255
- transition: color 0.15s;
3256
- }
3257
- .infotip-btn:hover,
3258
- .infotip-btn:focus-visible {
3259
- color: var(--color-primary);
3260
- outline: none;
3261
- }
3262
- .infotip-pop {
3263
- position: absolute;
3264
- bottom: calc(100% + 8px);
3265
- left: 50%;
3266
- transform: translateX(-50%);
3267
- z-index: 220;
3268
- display: flex;
3269
- flex-direction: column;
3270
- gap: 4px;
3271
- width: 280px;
3272
- max-width: 78vw;
3273
- padding: 12px 14px;
3274
- background: var(--color-surface);
3275
- border: none;
3276
- border-radius: var(--radius-lg);
3277
- box-shadow: var(--shadow-overlay);
3278
- text-align: left;
3279
- white-space: normal;
3280
- cursor: default;
3281
- }
3282
- .infotip-pop::after {
3283
- content: "";
3284
- position: absolute;
3285
- top: 100%;
3286
- left: 50%;
3287
- transform: translateX(-50%);
3288
- border: 6px solid transparent;
3289
- border-top-color: var(--color-surface);
3290
- }
3291
- // Portalled to <body>: position is set inline (fixed top/left), so drop the
3292
- // anchored offsets and the centering transform, and hide the arrow. z-index
3293
- // sits above the modal backdrop so it shows over the modal, not behind it.
3294
- .infotip-pop.is-portal {
3295
- bottom: auto;
3296
- transform: none;
3297
- z-index: 1600;
3298
- }
3299
- .infotip-pop.is-portal::after {
3300
- display: none;
3301
- }
3302
- .infotip-term {
3303
- display: flex;
3304
- align-items: center;
3305
- gap: 8px;
3306
- font-size: 13px;
3307
- font-weight: 600;
3308
- color: var(--color-text);
3309
- }
3310
- .infotip-full {
3311
- font-size: 12px;
3312
- font-weight: 500;
3313
- color: var(--color-text);
3314
- }
3315
- .infotip-def {
3316
- font-size: 12px;
3317
- color: var(--color-text-muted);
3318
- line-height: 1.5;
3319
- }
3320
-
3321
- // ── Page header ──
3322
- .page-header {
3323
- margin-bottom: 24px;
3324
- }
3325
- .page-title {
3326
- font-size: 24px;
3327
- font-weight: 600;
3328
- letter-spacing: -0.02em;
3329
- color: var(--color-text);
3330
- }
3331
- .page-subtitle {
3332
- font-size: 14px;
3333
- font-weight: 400;
3334
- color: var(--color-text-muted);
3335
- margin-top: 2px;
3336
- }
3337
- .page-actions {
3338
- display: flex;
3339
- gap: 8px;
3340
- align-items: center;
3341
- }
3342
-
3343
- // Tablet and below: the masthead stops being a single row — the title block and
3344
- // its actions stack, and the actions stretch to the full width rather than
3345
- // crowding the title. Bootstrap's lg tier (991.98px), not an invented breakpoint.
3346
- //
3347
- // Selectors are DOUBLED (`.page-header.page-header`) on purpose. A media query
3348
- // adds no specificity, so a consumer's own single-class rule — written later in
3349
- // the cascade, as a consuming app's stylesheet always is — silently beats the
3350
- // responsive rule and the layout never collapses. Doubling puts these at (0,2,0)
3351
- // so they hold, while still leaving a consumer free to override deliberately.
3352
- @media (max-width: 991.98px) {
3353
- .page-header.page-header {
3354
- flex-direction: column;
3355
- align-items: stretch;
3356
- gap: 12px;
3357
- }
3358
- .page-actions.page-actions {
3359
- flex-wrap: wrap;
3360
- width: 100%;
3361
- }
3362
- // A two-up field row is too tight for a tablet column.
3363
- .field-row.field-row,
3364
- .field-row-3.field-row-3 {
3365
- grid-template-columns: 1fr;
3366
- }
3367
- // A fixed 420px drawer leaves too little of the page visible on a tablet.
3368
- .drawer.drawer {
3369
- width: 100vw;
3370
- max-width: 420px;
3371
- }
3372
- }
3373
-
3374
- // ── Row-action / icon buttons ──
3375
- .btn-icon {
3376
- min-height: 0;
3377
- padding: 8px;
3378
- border-radius: 999px;
3379
- corner-shape: round;
3380
- }
3381
-
3382
- // Circular icon buttons in the top bar (minimal dashboard chrome).
3383
- .topbar-icon-btn {
3384
- position: relative;
3385
- display: inline-flex;
3386
- align-items: center;
3387
- justify-content: center;
3388
- width: 36px;
3389
- height: 36px;
3390
- flex: 0 0 auto;
3391
- border: none;
3392
- border-radius: 999px;
3393
- background: transparent;
3394
- color: var(--color-text-muted);
3395
- cursor: pointer;
3396
- transition:
3397
- background 0.15s ease,
3398
- color 0.15s ease;
3399
- }
3400
- .topbar-icon-btn:hover {
3401
- background: var(--color-primary-soft);
3402
- color: var(--color-primary);
3403
- }
3404
- .topbar-icon-btn.has-badge::after {
3405
- content: "";
3406
- position: absolute;
3407
- top: 9px;
3408
- right: 9px;
3409
- width: 8px;
3410
- height: 8px;
3411
- border-radius: 999px;
3412
- background: var(--color-danger);
3413
- border: 2px solid var(--color-bg);
3414
- }
3415
-
3416
- // ── Spinner / shimmer ──
3417
- .lx-spinner {
3418
- width: 18px;
3419
- height: 18px;
3420
- border: 2.5px solid var(--color-border);
3421
- border-top-color: var(--color-primary);
3422
- border-radius: 999px;
3423
- animation: lx-spin 0.7s linear infinite;
3424
- display: inline-block;
3425
- flex-shrink: 0;
3426
- }
3427
- @keyframes lx-spin {
3428
- to {
3429
- transform: rotate(360deg);
3430
- }
3431
- }
3432
- // Small (14px) spinner variant.
3433
- .lx-spinner-sm {
3434
- width: 14px;
3435
- height: 14px;
3436
- border-width: 2px;
3437
- }
3438
- // Spinner for the Button loading state.
3439
- .lx-spin {
3440
- animation: lx-spin 0.8s linear infinite;
3441
- }
3442
-
3443
- // Skeleton shimmer — used ONLY while loading the first data (loading &&
3444
- // empty), to tell "loading first data" apart from "loaded but empty".
3445
- .skeleton {
3446
- position: relative;
3447
- overflow: hidden;
3448
- display: block;
3449
- background: var(--color-bg-muted);
3450
- border-radius: var(--radius-sm);
3451
- }
3452
- .skeleton::after {
3453
- content: "";
3454
- position: absolute;
3455
- inset: 0;
3456
- transform: translateX(-100%);
3457
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent);
3458
- animation: skeleton-shimmer 1.4s ease-in-out infinite;
3459
- }
3460
- @keyframes skeleton-shimmer {
3461
- 100% {
3462
- transform: translateX(100%);
3463
- }
3464
- }
3465
- :root[data-theme="dark"] .skeleton::after {
3466
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);
3467
- }
3468
- @media (prefers-reduced-motion: reduce) {
3469
- .skeleton::after {
3470
- animation: none;
3471
- }
3472
- }
3473
-
3474
- // ── Text-scale utilities (lx-text-*) ──
3475
- .lx-text-xs {
3476
- font-size: 12px;
3477
- line-height: 1.4;
3478
- }
3479
- .lx-text-sm {
3480
- font-size: 13.5px;
3481
- line-height: 1.5;
3482
- }
3483
- .lx-text-base {
3484
- font-size: 15px;
3485
- line-height: 1.5;
3486
- }
3487
- .lx-text-lg {
3488
- font-size: 17px;
3489
- line-height: 1.4;
3490
- }
3491
- .lx-text-xl {
3492
- font-size: 20px;
3493
- line-height: 1.4;
3494
- }
3495
- .lx-text-2xl {
3496
- font-size: 24px;
3497
- line-height: 1.3;
3498
- }
3499
- .lx-text-3xl {
3500
- font-size: 30px;
3501
- line-height: 1.2;
3502
- }
3503
- .lx-text-4xl {
3504
- font-size: 36px;
3505
- line-height: 1.15;
3506
- }
3507
-
3508
- /* ── Chip (token / tag) ── */
3509
- .chip {
3510
- display: inline-flex;
3511
- align-items: center;
3512
- gap: 6px;
3513
- max-width: 100%;
3514
- padding: 4px 10px;
3515
- border: 1px solid var(--color-border);
3516
- border-radius: 999px;
3517
- background: var(--color-bg-muted);
3518
- color: var(--color-text);
3519
- font-size: 0.8125rem;
3520
- font-weight: 500;
3521
- line-height: 1.2;
3522
- vertical-align: middle;
3523
- }
3524
- .chip-sm {
3525
- gap: 4px;
3526
- padding: 2px 8px;
3527
- font-size: 0.75rem;
3528
- }
3529
- .chip-label {
3530
- overflow: hidden;
3531
- text-overflow: ellipsis;
3532
- white-space: nowrap;
3533
- }
3534
- .chip-ic {
3535
- display: inline-flex;
3536
- color: var(--color-text-subtle);
3537
- }
3538
- .chip-avatar {
3539
- display: inline-flex;
3540
- width: 18px;
3541
- height: 18px;
3542
- margin-left: -2px;
3543
- border-radius: 999px;
3544
- overflow: hidden;
3545
- flex: none;
3546
- }
3547
- .chip-avatar img {
3548
- width: 100%;
3549
- height: 100%;
3550
- object-fit: cover;
3551
- }
3552
- .chip-body {
3553
- display: inline-flex;
3554
- align-items: center;
3555
- gap: inherit;
3556
- padding: 0;
3557
- border: 0;
3558
- background: none;
3559
- font: inherit;
3560
- color: inherit;
3561
- cursor: pointer;
3562
- }
3563
- .chip-clickable {
3564
- cursor: pointer;
3565
- }
3566
- .chip-clickable:hover {
3567
- border-color: var(--color-border-strong);
3568
- }
3569
- .chip-remove {
3570
- display: inline-flex;
3571
- align-items: center;
3572
- justify-content: center;
3573
- margin: 0 -4px 0 2px;
3574
- padding: 2px;
3575
- border: 0;
3576
- border-radius: 999px;
3577
- background: none;
3578
- color: var(--color-text-subtle);
3579
- line-height: 0;
3580
- cursor: pointer;
3581
- }
3582
- .chip-remove:hover {
3583
- background: color-mix(in srgb, var(--color-text) 8%, transparent);
3584
- color: var(--color-text);
3585
- }
3586
- .chip.is-disabled {
3587
- opacity: 0.55;
3588
- pointer-events: none;
3589
- }
3590
-
3591
- /* Tones — tinted surface + on-tint text; icon/remove inherit the tone colour. */
3592
- .chip.primary {
3593
- border-color: transparent;
3594
- background: var(--color-primary-soft);
3595
- color: var(--color-primary);
3596
- }
3597
- .chip.success {
3598
- border-color: transparent;
3599
- background: var(--color-success-soft);
3600
- color: var(--color-success-active);
3601
- }
3602
- .chip.warning {
3603
- border-color: transparent;
3604
- background: var(--color-warning-soft);
3605
- color: var(--color-warning-active);
3606
- }
3607
- .chip.danger {
3608
- border-color: transparent;
3609
- background: var(--color-danger-soft);
3610
- color: var(--color-danger-active);
3611
- }
3612
- .chip.info {
3613
- border-color: transparent;
3614
- background: var(--color-info-soft);
3615
- color: var(--color-info-active);
3616
- }
3617
- .chip.primary .chip-ic,
3618
- .chip.success .chip-ic,
3619
- .chip.warning .chip-ic,
3620
- .chip.danger .chip-ic,
3621
- .chip.info .chip-ic,
3622
- .chip.primary .chip-remove,
3623
- .chip.success .chip-remove,
3624
- .chip.warning .chip-remove,
3625
- .chip.danger .chip-remove,
3626
- .chip.info .chip-remove {
3627
- color: currentColor;
3628
- }
3629
- .chip.primary .chip-remove:hover,
3630
- .chip.success .chip-remove:hover,
3631
- .chip.warning .chip-remove:hover,
3632
- .chip.danger .chip-remove:hover,
3633
- .chip.info .chip-remove:hover {
3634
- background: color-mix(in srgb, currentColor 16%, transparent);
3635
- color: currentColor;
3636
- }
3637
-
3638
- /* ── Autocomplete (input-first combobox; extends .search-select) ── */
3639
- .search-select-ac {
3640
- justify-content: flex-start;
3641
- cursor: text;
3642
- padding-right: 8px;
3643
- }
3644
- .search-select-ac-input {
3645
- flex: 1 1 auto;
3646
- min-width: 0;
3647
- padding: 0;
3648
- border: 0;
3649
- background: none;
3650
- color: inherit;
3651
- font: inherit;
3652
- outline: none;
3653
- }
3654
- .search-select-ac-input::placeholder {
3655
- color: var(--color-text-subtle);
3656
- }
3657
- .search-select-spinner {
3658
- flex: none;
3659
- width: 1rem;
3660
- height: 1rem;
3661
- color: var(--color-text-subtle);
3662
- border-width: 2px;
3663
- }
3664
- .search-select-hint {
3665
- padding: 10px 12px;
3666
- color: var(--color-text-subtle);
3667
- font-size: 0.8125rem;
3668
- }
3669
-
3670
- /* ── Multi-select (chips in the control; check-list dropdown; extends .search-select) ── */
3671
- .search-select-multi {
3672
- align-items: flex-start;
3673
- gap: 6px;
3674
- min-height: 40px;
3675
- height: auto;
3676
- padding-top: 5px;
3677
- padding-bottom: 5px;
3678
- flex-wrap: nowrap;
3679
- }
3680
- .ms-chips {
3681
- display: flex;
3682
- flex-wrap: wrap;
3683
- gap: 6px;
3684
- flex: 1 1 auto;
3685
- min-width: 0;
3686
- align-items: center;
3687
- }
3688
- .search-select-multi .search-select-caret {
3689
- align-self: center;
3690
- }
3691
- .ms-check {
3692
- display: inline-flex;
3693
- align-items: center;
3694
- justify-content: center;
3695
- width: 16px;
3696
- flex: none;
3697
- color: var(--color-primary);
3698
- opacity: 0;
3699
- }
3700
- .search-select-option[aria-selected='true'] .ms-check {
3701
- opacity: 1;
3702
- }
3703
- .ms-opt-label {
3704
- min-width: 0;
3705
- overflow: hidden;
3706
- text-overflow: ellipsis;
3707
- }
3708
- .search-select-multi .search-select-option {
3709
- display: flex;
3710
- align-items: center;
3711
- gap: 8px;
3712
- }
3713
-
3714
- /* ── Choice card: image / custom-template variant ── */
3715
- .struct-card-img-card {
3716
- flex-direction: column;
3717
- align-items: stretch;
3718
- padding: 0;
3719
- overflow: hidden;
3720
- }
3721
- .struct-card-media {
3722
- display: block;
3723
- width: 100%;
3724
- aspect-ratio: 16 / 9;
3725
- background: var(--color-bg-muted);
3726
- overflow: hidden;
3727
- }
3728
- .struct-card-img {
3729
- width: 100%;
3730
- height: 100%;
3731
- object-fit: cover;
3732
- display: block;
3733
- }
3734
- .struct-card-img-card .struct-card-body {
3735
- padding: 12px 14px;
3736
- }
3737
- .struct-card-img-card .struct-card-check {
3738
- position: absolute;
3739
- top: 8px;
3740
- right: 8px;
3741
- }
21
+ // The order below is LOAD-BEARING. A later partial may deliberately override an
22
+ // earlier one, so reordering these imports silently changes the cascade — and
23
+ // nothing in the build will complain. `pnpm check:theme` measures the rules that
24
+ // have to win against Bootstrap; the rest is guarded by review.
25
+ @import "wui/controls";
26
+ @import "wui/choice";
27
+ @import "wui/media";
28
+ @import "wui/navigation";
29
+ @import "wui/card";
30
+ @import "wui/data";
31
+ @import "wui/overlays";
32
+ @import "wui/utilities";
33
+ @import "wui/typography";
34
+ @import "wui/disclosure";
35
+ @import "wui/display";
36
+ @import "wui/flow";
37
+ @import "wui/shell";