@webority/theme 0.0.10 → 0.3.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.
@@ -1,9 +1,9 @@
1
1
  // ============================================================================
2
2
  // _wui-components.scss
3
3
  // ----------------------------------------------------------------------------
4
- // Extracted portal component layer — verbatim CSS pulled from the Lumex
5
- // customer portal's `design-system.css`, `components.css`, and
6
- // `portal-helpers.css` (Lumex.Web.Customer/src/assets/styles/).
4
+ // Extracted portal component layer — the shared, reusable UI-component
5
+ // CSS pulled from the customer portal's `design-system.css`,
6
+ // `components.css`, and `portal-helpers.css`.
7
7
  //
8
8
  // Only rules whose subject is a genuinely shared, reusable UI-component class
9
9
  // were kept (fields, badges, modals, tables, drawers, phone/date/search
@@ -16,7 +16,7 @@
16
16
  // ============================================================================
17
17
 
18
18
  // ── Checkbox & Radio ──
19
- // Lumex selection controls: a 20px glyph centred in a 36px circular hit area
19
+ // Selection controls: a 20px glyph centred in a 36px circular hit area
20
20
  // that shows a soft primary-tinted ripple on hover. Unchecked = muted
21
21
  // grey-blue outline; checked = solid primary. Checkbox = squircle box + white
22
22
  // tick; radio = ring + filled centre dot. Pure CSS, no JS — drives the native
@@ -69,7 +69,7 @@
69
69
  background-color 0.15s ease;
70
70
  }
71
71
  .lx-check {
72
- border-radius: 6px;
72
+ border-radius: var(--radius-sm);
73
73
  corner-shape: squircle;
74
74
  }
75
75
  .lx-radio {
@@ -190,9 +190,9 @@
190
190
  .textarea {
191
191
  box-sizing: border-box;
192
192
  width: 100%;
193
- height: 45px;
193
+ height: var(--wui-control-height);
194
194
  padding: 12px;
195
- border-radius: 8px;
195
+ border-radius: var(--radius);
196
196
  border: 1px solid var(--color-border);
197
197
  background: var(--color-surface);
198
198
  font-size: 14px;
@@ -222,7 +222,7 @@
222
222
  outline: none;
223
223
  background-color: var(--color-bg-muted);
224
224
  color: var(--color-text);
225
- border-color: rgba(47, 80, 228, 0.3);
225
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
226
226
  box-shadow: none;
227
227
  }
228
228
  // Textarea renders as .form-control.textarea (react-bootstrap). Match the
@@ -233,7 +233,7 @@ textarea.textarea.form-control:hover:not(:focus):not(:disabled):not([readonly])
233
233
  }
234
234
  textarea.textarea:focus {
235
235
  outline: none;
236
- border-color: rgba(47, 80, 228, 0.3);
236
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
237
237
  box-shadow: none;
238
238
  }
239
239
  .input::placeholder,
@@ -269,17 +269,27 @@ textarea.textarea:focus {
269
269
 
270
270
  // Invalid field state (required-field validation) — soft danger-tinted
271
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.
272
275
  .input.is-invalid,
273
276
  .select.is-invalid,
274
277
  .textarea.is-invalid,
275
278
  .input.is-invalid:focus,
276
279
  .select.is-invalid:focus,
277
- .textarea.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 {
278
287
  background: var(--color-surface);
279
288
  border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-surface));
280
289
  box-shadow: none;
281
290
  }
282
- .input.is-invalid {
291
+ .input.is-invalid,
292
+ .input[aria-invalid="true"] {
283
293
  padding-right: 41px;
284
294
  }
285
295
  .field-error {
@@ -472,7 +482,7 @@ textarea.textarea:focus {
472
482
  border: none;
473
483
  background: transparent;
474
484
  color: var(--color-text-subtle);
475
- border-radius: 8px;
485
+ border-radius: var(--radius);
476
486
  cursor: pointer;
477
487
  transition:
478
488
  color 0.12s ease,
@@ -546,9 +556,11 @@ textarea.textarea:focus {
546
556
  .field > .field-error {
547
557
  align-self: stretch;
548
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.
549
561
  @media (max-width: 575.98px) {
550
- .field-row,
551
- .field-row-3 {
562
+ .field-row.field-row,
563
+ .field-row-3.field-row-3 {
552
564
  grid-template-columns: 1fr;
553
565
  }
554
566
  }
@@ -556,7 +568,7 @@ textarea.textarea:focus {
556
568
  // red border around a non-input group (e.g. a structure-form card row)
557
569
  .group-invalid {
558
570
  border: 1px solid var(--color-danger);
559
- border-radius: 10px;
571
+ border-radius: var(--radius);
560
572
  padding: 8px;
561
573
  }
562
574
 
@@ -566,7 +578,7 @@ textarea.textarea:focus {
566
578
  display: flex;
567
579
  align-items: stretch;
568
580
  border: 1px solid var(--color-border);
569
- border-radius: 16px;
581
+ border-radius: var(--radius-lg);
570
582
  corner-shape: squircle;
571
583
  background: var(--color-surface);
572
584
  transition:
@@ -580,7 +592,7 @@ textarea.textarea:focus {
580
592
  }
581
593
  .input-with-unit:focus-within {
582
594
  background: var(--color-bg-muted);
583
- border-color: rgba(47, 80, 228, 0.3);
595
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
584
596
  }
585
597
  .input-with-unit:has(.num-input.error) {
586
598
  border-color: var(--color-danger);
@@ -657,7 +669,7 @@ textarea.textarea:focus {
657
669
  background: currentColor;
658
670
  }
659
671
 
660
- // Keystone "soft" status badge — pill, 12/600, muted tone fill.
672
+ // "Soft" status badge — pill, 12/600, muted tone fill.
661
673
  .status-label {
662
674
  display: inline-flex;
663
675
  align-items: center;
@@ -670,24 +682,40 @@ textarea.textarea:focus {
670
682
  line-height: 1;
671
683
  white-space: nowrap;
672
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,
673
690
  .status-label.tone-green {
674
- background: rgba(34, 197, 94, 0.16);
691
+ background: color-mix(in srgb, var(--color-success) 16%, transparent);
675
692
  color: var(--color-success-strong);
676
693
  }
694
+ .status-label.warning,
677
695
  .status-label.tone-amber {
678
- background: rgba(255, 171, 0, 0.16);
696
+ background: color-mix(in srgb, var(--color-warning) 16%, transparent);
679
697
  color: var(--color-warning-strong);
680
698
  }
699
+ .status-label.danger,
681
700
  .status-label.tone-red {
682
- background: rgba(255, 86, 48, 0.16);
701
+ background: color-mix(in srgb, var(--color-danger) 16%, transparent);
683
702
  color: var(--color-danger-strong);
684
703
  }
704
+ .status-label.info,
685
705
  .status-label.tone-blue {
686
- background: rgba(0, 184, 217, 0.16);
706
+ background: color-mix(in srgb, var(--color-info) 16%, transparent);
687
707
  color: var(--color-info-strong);
688
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,
689
717
  .status-label.tone-grey {
690
- background: rgba(145, 158, 171, 0.16);
718
+ background: color-mix(in srgb, var(--color-text-subtle) 16%, transparent);
691
719
  color: var(--color-text-subtle);
692
720
  }
693
721
 
@@ -735,7 +763,7 @@ textarea.textarea:focus {
735
763
  gap: 11px;
736
764
  padding: 20px;
737
765
  border: 1.5px solid var(--color-border);
738
- border-radius: 10px;
766
+ border-radius: var(--radius);
739
767
  background: var(--color-surface);
740
768
  cursor: pointer;
741
769
  transition:
@@ -743,10 +771,10 @@ textarea.textarea:focus {
743
771
  background 0.12s ease;
744
772
  }
745
773
  .struct-card:hover {
746
- border-color: rgba(47, 80, 228, 0.3);
774
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
747
775
  }
748
776
  .struct-card.checked {
749
- border-color: rgba(47, 80, 228, 0.3);
777
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
750
778
  background: color-mix(in srgb, var(--color-primary) 5%, transparent);
751
779
  }
752
780
  .struct-card.checked .struct-card-title {
@@ -893,7 +921,7 @@ textarea.textarea:focus {
893
921
  }
894
922
  .search-select-control.open {
895
923
  background: var(--color-bg-muted);
896
- border-color: rgba(47, 80, 228, 0.3);
924
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
897
925
  }
898
926
  .search-select-caret {
899
927
  flex-shrink: 0;
@@ -919,9 +947,9 @@ textarea.textarea:focus {
919
947
  right: 0;
920
948
  background: var(--color-surface);
921
949
  border: 1px solid var(--color-border);
922
- border-radius: 16px;
950
+ border-radius: var(--radius-lg);
923
951
  corner-shape: squircle;
924
- box-shadow: 0 12px 32px rgba(7, 20, 55, 0.12);
952
+ box-shadow: var(--shadow-overlay);
925
953
  padding: 4px;
926
954
  overflow: hidden;
927
955
  }
@@ -945,7 +973,7 @@ textarea.textarea:focus {
945
973
  padding: 0 12px;
946
974
  height: 40px;
947
975
  border: 1px solid var(--color-border);
948
- border-radius: 14px;
976
+ border-radius: var(--radius);
949
977
  corner-shape: squircle;
950
978
  background: var(--color-surface);
951
979
  color: var(--color-text-subtle);
@@ -1001,7 +1029,7 @@ textarea.textarea:focus {
1001
1029
  padding: 9px 14px;
1002
1030
  line-height: 1.4;
1003
1031
  border: none;
1004
- border-radius: 12px;
1032
+ border-radius: var(--radius);
1005
1033
  corner-shape: squircle;
1006
1034
  background: transparent;
1007
1035
  font-size: 14px;
@@ -1043,7 +1071,7 @@ textarea.textarea:focus {
1043
1071
  }
1044
1072
  .datepicker.open .datepicker-trigger {
1045
1073
  background-color: var(--color-bg-muted);
1046
- border-color: rgba(47, 80, 228, 0.3);
1074
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
1047
1075
  box-shadow: none;
1048
1076
  }
1049
1077
  // Portalled to <body> and fixed-positioned from JS so a modal's
@@ -1057,8 +1085,9 @@ textarea.textarea:focus {
1057
1085
  padding: 18px;
1058
1086
  background: var(--color-surface);
1059
1087
  border: 1px solid var(--color-border);
1060
- border-radius: 18px;
1088
+ border-radius: var(--radius-xl);
1061
1089
  corner-shape: squircle;
1090
+ box-shadow: var(--shadow-overlay);
1062
1091
  animation: datepicker-in 0.14s ease;
1063
1092
  }
1064
1093
  .datepicker-pop.drop-up {
@@ -1198,7 +1227,7 @@ textarea.textarea:focus {
1198
1227
  border: none;
1199
1228
  background: transparent;
1200
1229
  padding: 6px 12px;
1201
- border-radius: 8px;
1230
+ border-radius: var(--radius);
1202
1231
  font-size: 13px;
1203
1232
  font-weight: 600;
1204
1233
  color: var(--color-primary);
@@ -1217,24 +1246,65 @@ textarea.textarea:focus {
1217
1246
  .app-phone-number-input {
1218
1247
  width: 100%;
1219
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.
1220
1253
  .app-phone-number-input .phone-input-wrap {
1221
- position: relative;
1254
+ display: flex;
1255
+ align-items: center;
1222
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;
1223
1265
  }
1224
- .app-phone-number-input .phone-input-wrap > .input {
1225
- width: 100%;
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);
1226
1269
  }
1227
- .app-phone-number-input .phone-country-adornment {
1228
- position: absolute;
1229
- left: 0;
1230
- top: 0;
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;
1231
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;
1232
1299
  display: flex;
1233
1300
  align-items: center;
1234
1301
  padding-left: 12px;
1235
1302
  z-index: 5;
1236
1303
  }
1237
- .country-dropdown .country-toggle {
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 {
1238
1308
  display: flex;
1239
1309
  align-items: center;
1240
1310
  gap: 6px;
@@ -1243,29 +1313,43 @@ textarea.textarea:focus {
1243
1313
  background: transparent;
1244
1314
  box-shadow: none;
1245
1315
  color: var(--color-text);
1316
+ cursor: pointer;
1317
+ font: inherit;
1318
+ line-height: 1;
1246
1319
  }
1247
- .country-dropdown .country-toggle::after {
1320
+ .country-toggle::after {
1248
1321
  display: none;
1249
1322
  }
1250
- .country-dropdown .country-toggle:hover,
1251
- .country-dropdown .country-toggle:focus,
1252
- .country-dropdown .country-toggle:active {
1253
- background: transparent !important;
1254
- box-shadow: none !important;
1255
- color: var(--color-text) !important;
1323
+ .country-toggle:hover,
1324
+ .country-toggle:focus,
1325
+ .country-toggle:active {
1326
+ background: transparent;
1327
+ box-shadow: none;
1328
+ color: var(--color-text);
1256
1329
  }
1257
- .app-phone-number-input .phone-flag {
1258
- width: 22px;
1259
- height: 15px;
1260
- border-radius: 2px;
1261
- object-fit: cover;
1262
- box-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);
1330
+ .country-toggle:hover .phone-chevron {
1331
+ color: var(--color-text);
1263
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).
1264
1346
  .app-phone-number-input .phone-chevron {
1265
1347
  color: var(--color-text-muted);
1266
1348
  transition: transform 0.2s ease;
1267
1349
  }
1268
- .country-dropdown.show .phone-chevron {
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 {
1269
1353
  transform: rotate(180deg);
1270
1354
  }
1271
1355
  .app-phone-number-input .phone-dial {
@@ -1274,34 +1358,48 @@ textarea.textarea:focus {
1274
1358
  color: var(--color-text);
1275
1359
  }
1276
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.
1277
1366
  .country-dropdown-menu {
1367
+ position: fixed;
1368
+ z-index: 1600;
1278
1369
  width: 320px;
1279
- padding: 8px 0;
1370
+ max-width: calc(100vw - 16px);
1371
+ padding: 4px;
1280
1372
  border: 1px solid var(--color-border);
1281
- border-radius: 12px;
1373
+ border-radius: var(--radius-lg);
1374
+ corner-shape: squircle;
1282
1375
  background: var(--color-surface);
1283
- box-shadow: 0 10px 24px -4px rgba(15, 42, 81, 0.14);
1376
+ box-shadow: var(--shadow-overlay);
1284
1377
  overflow: hidden;
1285
1378
  }
1286
1379
  .dropdown-search-wrapper {
1287
1380
  position: relative;
1288
- padding: 4px 12px 8px;
1381
+ margin: 4px;
1289
1382
  }
1290
1383
  .dropdown-search-icon {
1291
1384
  position: absolute;
1292
- left: 24px;
1293
- top: calc(50% - 3px);
1385
+ left: 12px;
1386
+ top: 50%;
1294
1387
  transform: translateY(-50%);
1295
1388
  display: flex;
1296
- color: var(--color-text-muted);
1389
+ color: var(--color-text-subtle);
1297
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);
1298
1395
  }
1299
1396
  .dropdown-search-input {
1300
1397
  width: 100%;
1301
- height: 38px;
1398
+ height: 40px;
1302
1399
  padding: 0 12px 0 36px;
1303
1400
  border: 1px solid var(--color-border);
1304
- border-radius: 999px;
1401
+ border-radius: var(--radius);
1402
+ corner-shape: squircle;
1305
1403
  font-size: 14px;
1306
1404
  color: var(--color-text);
1307
1405
  background: var(--color-surface);
@@ -1311,47 +1409,58 @@ textarea.textarea:focus {
1311
1409
  background: var(--color-bg-muted);
1312
1410
  border-color: var(--color-border-strong);
1313
1411
  }
1412
+ // Border is the focus cue everywhere else in the system — no glow ring here.
1314
1413
  .dropdown-search-input:focus {
1315
1414
  background: var(--color-bg-muted);
1316
1415
  border-color: var(--color-primary);
1317
- box-shadow: 0 0 0 3px var(--color-primary-soft);
1318
1416
  }
1319
1417
  .country-list-scroll {
1320
1418
  max-height: 250px;
1321
1419
  overflow-y: auto;
1322
1420
  overflow-x: hidden;
1421
+ padding: 4px;
1422
+ display: flex;
1423
+ flex-direction: column;
1424
+ gap: 4px;
1323
1425
  scrollbar-width: thin;
1426
+ scrollbar-gutter: stable;
1324
1427
  }
1325
1428
  .country-list-scroll::-webkit-scrollbar {
1326
1429
  width: 6px;
1327
1430
  }
1328
1431
  .country-list-scroll::-webkit-scrollbar-thumb {
1329
1432
  background: var(--color-border);
1330
- border-radius: 3px;
1433
+ border-radius: 999px;
1331
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.)
1332
1438
  .country-dropdown-item {
1333
1439
  display: flex !important;
1334
1440
  align-items: center;
1335
1441
  gap: 10px;
1336
- padding: 9px 16px !important;
1442
+ padding: 9px 14px !important;
1443
+ border-radius: var(--radius) !important;
1444
+ corner-shape: squircle;
1337
1445
  cursor: pointer;
1338
1446
  color: var(--color-text) !important;
1339
1447
  background: transparent !important;
1340
1448
  border: 0 !important;
1449
+ transition:
1450
+ background 0.12s ease,
1451
+ color 0.12s ease;
1341
1452
  }
1342
1453
  .country-dropdown-item:hover,
1343
1454
  .country-dropdown-item:focus {
1344
- background: var(--color-bg-subtle) !important;
1455
+ background: var(--color-primary-soft) !important;
1456
+ color: var(--color-primary) !important;
1345
1457
  }
1346
1458
  .country-dropdown-item.selected {
1347
1459
  background: var(--color-primary-soft) !important;
1460
+ color: var(--color-primary) !important;
1461
+ font-weight: 500;
1348
1462
  }
1349
- .country-dropdown-menu .phone-flag-lg {
1350
- width: 22px;
1351
- height: 15px;
1352
- border-radius: 2px;
1353
- object-fit: cover;
1354
- box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
1463
+ .country-dropdown-menu .wui-flag {
1355
1464
  flex-shrink: 0;
1356
1465
  }
1357
1466
  .country-name-text {
@@ -1364,6 +1473,15 @@ textarea.textarea:focus {
1364
1473
  font-weight: 400;
1365
1474
  color: var(--color-text-muted);
1366
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
+ }
1367
1485
  .selected-check-icon {
1368
1486
  margin-left: auto;
1369
1487
  display: flex;
@@ -1423,7 +1541,7 @@ textarea.textarea:focus {
1423
1541
  gap: 12px;
1424
1542
  padding: 12px 14px;
1425
1543
  border: 1px dashed var(--color-border);
1426
- border-radius: 12px;
1544
+ border-radius: var(--radius-lg);
1427
1545
  background: var(--color-surface);
1428
1546
  cursor: pointer;
1429
1547
  transition:
@@ -1480,7 +1598,7 @@ textarea.textarea:focus {
1480
1598
  .attach-preview {
1481
1599
  width: 100%;
1482
1600
  border: 1px solid var(--color-border);
1483
- border-radius: 12px;
1601
+ border-radius: var(--radius-lg);
1484
1602
  background: var(--color-surface);
1485
1603
  overflow: hidden;
1486
1604
  }
@@ -1566,7 +1684,7 @@ textarea.textarea:focus {
1566
1684
  display: flex;
1567
1685
  align-items: stretch;
1568
1686
  width: 100%;
1569
- border-radius: 16px;
1687
+ border-radius: var(--radius-lg);
1570
1688
  corner-shape: squircle;
1571
1689
  border: 1px solid var(--color-border);
1572
1690
  background: var(--color-surface);
@@ -1679,7 +1797,7 @@ textarea.textarea:focus {
1679
1797
  display: block;
1680
1798
  width: 100%;
1681
1799
  height: 350px;
1682
- border-radius: 16px;
1800
+ border-radius: var(--radius-lg);
1683
1801
  corner-shape: squircle;
1684
1802
  border: 1px solid var(--color-border);
1685
1803
  overflow: hidden;
@@ -1746,7 +1864,7 @@ textarea.textarea:focus {
1746
1864
  font-size: 14px;
1747
1865
  font-weight: 600;
1748
1866
  border-radius: 999px;
1749
- box-shadow: 0 6px 18px rgba(16, 24, 40, 0.22);
1867
+ box-shadow: var(--shadow-overlay);
1750
1868
  }
1751
1869
  .file-preview-remove {
1752
1870
  position: absolute;
@@ -2018,7 +2136,7 @@ textarea.textarea:focus {
2018
2136
  place-items: center;
2019
2137
  width: 30px;
2020
2138
  height: 30px;
2021
- border-radius: 9px;
2139
+ border-radius: var(--radius);
2022
2140
  corner-shape: squircle;
2023
2141
  background: var(--color-primary-soft);
2024
2142
  color: var(--color-primary);
@@ -2035,16 +2153,215 @@ textarea.textarea:focus {
2035
2153
  padding: 14px 16px 20px;
2036
2154
  }
2037
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
+
2038
2330
  // ── Empty state ──
2039
2331
  .empty-state {
2040
2332
  text-align: center;
2041
2333
  padding: 48px 24px;
2042
2334
  color: var(--color-text-subtle);
2043
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
+ }
2044
2361
  .empty-state-icon {
2045
2362
  width: 48px;
2046
2363
  height: 48px;
2047
- border-radius: 12px;
2364
+ border-radius: var(--radius-lg);
2048
2365
  background: var(--color-primary-soft);
2049
2366
  color: var(--color-primary);
2050
2367
  display: inline-flex;
@@ -2109,6 +2426,76 @@ textarea.textarea:focus {
2109
2426
  background: transparent;
2110
2427
  }
2111
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.
2452
+ //
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
+
2112
2499
  // ── Table + pagination ──
2113
2500
  // DataTable cell helpers
2114
2501
  .table-ledger .lx-cell-end,
@@ -2141,11 +2528,11 @@ textarea.textarea:focus {
2141
2528
  }
2142
2529
  .table-ledger thead th:first-child {
2143
2530
  padding-left: 20px;
2144
- border-radius: 6px 0 0 6px;
2531
+ border-radius: var(--radius-sm) 0 0 var(--radius-sm);
2145
2532
  }
2146
2533
  .table-ledger thead th:last-child {
2147
2534
  padding-right: 20px;
2148
- border-radius: 0 6px 6px 0;
2535
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
2149
2536
  }
2150
2537
  .table-ledger tbody td {
2151
2538
  height: 54px;
@@ -2191,7 +2578,9 @@ textarea.textarea:focus {
2191
2578
  .table-ledger a.u-name:hover {
2192
2579
  text-decoration: none;
2193
2580
  }
2194
- // Identity-cell subline.
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,
2195
2584
  .table-ledger .u-meta > .lx-text-xs {
2196
2585
  font-size: 12px;
2197
2586
  font-weight: 400;
@@ -2247,7 +2636,7 @@ textarea.textarea:focus {
2247
2636
  padding: 0 28px 0 12px;
2248
2637
  font-size: 13px;
2249
2638
  font-weight: 600;
2250
- border-radius: 16px;
2639
+ border-radius: var(--radius);
2251
2640
  background-position: right 8px center;
2252
2641
  background-size: 14px;
2253
2642
  }
@@ -2321,8 +2710,33 @@ textarea.textarea:focus {
2321
2710
  padding-left: 12px;
2322
2711
  padding-right: 0;
2323
2712
  }
2324
- .table-ledger tbody tr.is-selected {
2325
- background: var(--color-bg-muted);
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);
2326
2740
  }
2327
2741
 
2328
2742
  // Row actions — icon-only buttons replacing a kebab dropdown.
@@ -2384,27 +2798,27 @@ textarea.textarea:focus {
2384
2798
  }
2385
2799
  .u-action.u-action-preview {
2386
2800
  background: var(--color-primary-soft);
2387
- color: #2f50e4;
2801
+ color: var(--color-primary);
2388
2802
  }
2389
2803
  .u-action.u-action-download {
2390
2804
  background: var(--color-success-soft);
2391
- color: #04b440;
2805
+ color: var(--color-success-active);
2392
2806
  }
2393
2807
  .u-action.u-action-share {
2394
2808
  background: var(--color-warning-soft);
2395
- color: #dfa000;
2809
+ color: var(--color-warning-active);
2396
2810
  }
2397
2811
  .u-action.u-action-preview:hover:not(:disabled) {
2398
2812
  background: var(--color-primary-soft);
2399
- color: #2f50e4;
2813
+ color: var(--color-primary);
2400
2814
  }
2401
2815
  .u-action.u-action-download:hover:not(:disabled) {
2402
2816
  background: var(--color-success-soft);
2403
- color: #04b440;
2817
+ color: var(--color-success-active);
2404
2818
  }
2405
2819
  .u-action.u-action-share:hover:not(:disabled) {
2406
2820
  background: var(--color-warning-soft);
2407
- color: #dfa000;
2821
+ color: var(--color-warning-active);
2408
2822
  }
2409
2823
  .u-action.u-action-danger:hover {
2410
2824
  color: var(--color-danger);
@@ -2490,12 +2904,12 @@ textarea.textarea:focus {
2490
2904
  // deep drop shadow.
2491
2905
  box-shadow:
2492
2906
  0 0 0 1px color-mix(in srgb, var(--color-text) 10%, transparent),
2493
- 0 24px 70px -18px rgba(0, 0, 0, 0.55);
2907
+ var(--shadow-overlay-lg);
2494
2908
  background: var(--color-surface);
2495
2909
  }
2496
2910
  .lx-modal {
2497
2911
  background: var(--color-surface);
2498
- border-radius: 24px;
2912
+ border-radius: var(--radius-xl);
2499
2913
  corner-shape: squircle;
2500
2914
  border: none;
2501
2915
  width: 100%;
@@ -2614,7 +3028,7 @@ textarea.textarea:focus {
2614
3028
  justify-content: center;
2615
3029
  width: 34px;
2616
3030
  height: 34px;
2617
- border-radius: 10px;
3031
+ border-radius: var(--radius);
2618
3032
  corner-shape: squircle;
2619
3033
  flex: none;
2620
3034
  background: var(--color-primary-soft);
@@ -2760,9 +3174,9 @@ textarea.textarea:focus {
2760
3174
  }
2761
3175
  .lx-confirm-actions .btn {
2762
3176
  flex: 1;
2763
- min-height: 45px;
2764
- height: 45px;
2765
- border-radius: 16px;
3177
+ min-height: var(--wui-control-height);
3178
+ height: var(--wui-control-height);
3179
+ border-radius: var(--radius);
2766
3180
  corner-shape: squircle;
2767
3181
  padding-left: 16px;
2768
3182
  padding-right: 16px;
@@ -2859,8 +3273,8 @@ textarea.textarea:focus {
2859
3273
  padding: 12px 14px;
2860
3274
  background: var(--color-surface);
2861
3275
  border: none;
2862
- border-radius: 10px;
2863
- box-shadow: 0 8px 28px rgba(7, 20, 55, 0.14);
3276
+ border-radius: var(--radius-lg);
3277
+ box-shadow: var(--shadow-overlay);
2864
3278
  text-align: left;
2865
3279
  white-space: normal;
2866
3280
  cursor: default;
@@ -2926,6 +3340,37 @@ textarea.textarea:focus {
2926
3340
  align-items: center;
2927
3341
  }
2928
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
+
2929
3374
  // ── Row-action / icon buttons ──
2930
3375
  .btn-icon {
2931
3376
  min-height: 0;
@@ -3002,7 +3447,7 @@ textarea.textarea:focus {
3002
3447
  overflow: hidden;
3003
3448
  display: block;
3004
3449
  background: var(--color-bg-muted);
3005
- border-radius: 6px;
3450
+ border-radius: var(--radius-sm);
3006
3451
  }
3007
3452
  .skeleton::after {
3008
3453
  content: "";