@webority/theme 0.81.0 → 0.82.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webority/theme",
3
- "version": "0.81.0",
3
+ "version": "0.82.0",
4
4
  "description": "Webority shared design system — Bootstrap 5.3 SCSS overrides, --wui-* tokens, and shared component CSS. The single source of brand chrome for React portals and Razor sites.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "publishConfig": {
@@ -135,12 +135,9 @@
135
135
  border-color: var(--color-primary);
136
136
  }
137
137
 
138
- // Display type: Webority Serif Demi Bold on titles, Webority Sans on everything else.
138
+ // Display type: Webority Serif Demi Bold only at 24px and above (page and result titles); every
139
+ // smaller title stays in Webority Sans.
139
140
  .page-title,
140
- .card-title,
141
- .wui-modal-title,
142
- .empty-state-title,
143
- .tbl-empty-title,
144
141
  .result-title {
145
142
  font-family: var(--wui-font-display);
146
143
  font-weight: 600;
@@ -148,6 +145,11 @@
148
145
  letter-spacing: -0.01em;
149
146
  color: var(--color-text);
150
147
  }
148
+ // The compact page titles (size sm and the page header bar) are 20px, so they keep the sans face.
149
+ .page-header-sm .page-title,
150
+ .page-header.page-header-bar .page-title {
151
+ font-family: inherit;
152
+ }
151
153
 
152
154
  @mixin wui-signal-dark {
153
155
  --wui-signal-heading: #C4AEFF;
@@ -212,9 +214,6 @@
212
214
  :where(&) .compare-table td.is-highlight {
213
215
  background: color-mix(in srgb, #FFFFFF 8%, var(--color-surface));
214
216
  }
215
- :where(&) .segmented {
216
- --segmented-selected-bg: color-mix(in srgb, #FFFFFF 8%, var(--color-surface));
217
- }
218
217
  :where(&) .table-ledger tbody tr.is-selected:hover {
219
218
  --bs-table-bg-state: color-mix(in srgb, #FFFFFF 12%, transparent);
220
219
  }
@@ -6,7 +6,7 @@
6
6
  // ---- Button polish (used by both Razor + React) ---------------------
7
7
  // Flat buttons — no drop shadow. Active-press feedback (1px nudge) is
8
8
  // retained because it's a tactile signal, not a depth cue.
9
- // A button is exactly as tall as a field of the same size (the --wui-control-height ladder, 36/44/52px), so a
9
+ // A button is exactly as tall as a field of the same size (the --wui-control-height ladder, 32/36/44px), so a
10
10
  // button beside an input lines up in every form row. Heights are min-height with the label flex-centred; sm/lg
11
11
  // set theirs in their size blocks below.
12
12
  .btn:not(.btn-sm):not(.btn-lg):not(.btn-pill):not(.btn-pill-sm):not(.u-action):not(.btn-icon) {
@@ -270,14 +270,14 @@
270
270
  // sizes step font-size, horizontal padding and radius together (vertical size
271
271
  // comes from --wui-control-height, so a button lines up with an input). Weight
272
272
  // is 600 on every rung — buttons read as a deliberate action, not body text.
273
- // Medium (default): 14px / 600, about 20px side padding, 18px squircle radius, --wui-control-height tall.
273
+ // Medium (default): 14px / 600, about 12px side padding, 14px squircle radius, --wui-control-height tall.
274
274
  // Side padding is always twice the vertical padding. A button is its field's height with the label
275
275
  // line centred, so its vertical padding is (height - both borders - one line) / 2 and its side
276
- // padding the height less both borders and one line: about 7.7/15.4, 10.2/20.3 and 12.6/25.2px at sm,
276
+ // padding the height less both borders and one line: about 5.7/11.4, 6.2/12.3 and 8.6/17.2px at sm,
277
277
  // md and lg with the theme's line height.
278
278
  .btn {
279
279
  --wui-btn-height: var(--wui-control-height);
280
- --bs-btn-border-radius: 18px;
280
+ --bs-btn-border-radius: 14px;
281
281
  --bs-btn-font-size: 0.875rem;
282
282
  --bs-btn-font-weight: 600;
283
283
  --bs-btn-padding-x: calc(var(--wui-btn-height) - 2 * var(--bs-btn-border-width) - var(--bs-btn-font-size) * var(--bs-btn-line-height));
@@ -285,24 +285,24 @@
285
285
  // keep a plain border-radius arc — not a cross-browser design contract.
286
286
  corner-shape: squircle;
287
287
  }
288
- // Small: 12px, about 15px side padding, 14px squircle radius (proportional to md), as tall as a small field.
288
+ // Small: 12px, about 11px side padding, 12px squircle radius (proportional to md), as tall as a small field.
289
289
  .btn-sm:not(.btn-icon),
290
290
  .btn-group-sm > .btn:not(.btn-icon) {
291
291
  --wui-btn-height: var(--wui-control-height-sm);
292
292
  --bs-btn-padding-y: 0;
293
- --bs-btn-border-radius: 14px;
293
+ --bs-btn-border-radius: 12px;
294
294
  --bs-btn-font-size: 0.75rem;
295
295
  display: inline-flex;
296
296
  align-items: center;
297
297
  justify-content: center;
298
298
  min-height: var(--wui-control-height-sm);
299
299
  }
300
- // Large: 16px, about 25px side padding, 22px squircle radius (proportional to md), as tall as a large field.
300
+ // Large: 16px, about 17px side padding, 18px squircle radius (proportional to md), as tall as a large field.
301
301
  .btn-lg:not(.btn-icon),
302
302
  .btn-group-lg > .btn:not(.btn-icon) {
303
303
  --wui-btn-height: var(--wui-control-height-lg);
304
304
  --bs-btn-padding-y: 0;
305
- --bs-btn-border-radius: 22px;
305
+ --bs-btn-border-radius: 18px;
306
306
  --bs-btn-font-size: 1rem;
307
307
  display: inline-flex;
308
308
  align-items: center;
@@ -719,7 +719,7 @@
719
719
  background-color: transparent;
720
720
  border-color: transparent;
721
721
  color: var(--color-primary-ink);
722
- opacity: 0.55;
722
+ opacity: var(--wui-disabled-opacity);
723
723
  }
724
724
  .btn.btn-ghost.btn-ghost.is-loading:disabled,
725
725
  .btn.btn-ghost.btn-ghost.is-loading.disabled {
@@ -792,7 +792,7 @@
792
792
  background-color: transparent;
793
793
  border-color: transparent;
794
794
  color: var(--color-primary-ink);
795
- opacity: 0.55;
795
+ opacity: var(--wui-disabled-opacity);
796
796
  }
797
797
 
798
798
  // ---- Card hover-lift (name is historical) -----------------------------
package/scss/_tokens.scss CHANGED
@@ -81,14 +81,12 @@
81
81
  --wui-shadow-thumb: 0 2px 6px rgba(0, 0, 0, 0.1);
82
82
  --wui-shadow-thumb-hover: 0 2px 12px rgba(0, 0, 0, 0.2);
83
83
 
84
- // One height for every form control. Buttons, inputs and selects share form
85
- // rows, so they must line up — they were 46.8px and 45px, which reads as a
86
- // misalignment on every filter bar. 44px is also the WCAG touch-target floor.
87
- --wui-control-height: 44px;
88
- // Optional size steps (sm/lg) for dense toolbars / marketing-scale fields. md is the
89
- // default above; a control opts into a step with .input-sm / .input-lg.
90
- --wui-control-height-sm: 36px;
91
- --wui-control-height-lg: 52px;
84
+ // The control height ladder: sm 32, md 36, lg 44. A button, field, select, segmented control, tab
85
+ // or chip of one size shares its height, so a row of mixed controls lines up. On a phone-width or
86
+ // touch screen md rises to 44px, the touch-target floor (media rule after this block).
87
+ --wui-control-height: 36px;
88
+ --wui-control-height-sm: 32px;
89
+ --wui-control-height-lg: 44px;
92
90
 
93
91
  // Scrollbar thumb — one quiet grey-blue at three interaction strengths.
94
92
  --wui-scrollbar: rgba(145, 158, 171, 0.45);
@@ -361,6 +359,14 @@
361
359
  --wui-first-day: 1;
362
360
  }
363
361
 
362
+ // A finger needs 44px: on a phone-width or touch screen the default control takes the lg rung. The
363
+ // same query sizes the modal and drawer close buttons (wui/_overlays.scss).
364
+ @media (max-width: 575.98px), (pointer: coarse) {
365
+ :root {
366
+ --wui-control-height: 44px;
367
+ }
368
+ }
369
+
364
370
  // Bootstrap md / xl (not invented px)
365
371
  @media (min-width: 768px) {
366
372
  :root { --wui-container-pad: 64px; }
@@ -291,7 +291,7 @@ $input-border-color: $border-color;
291
291
  // post-CSS focus uses var(--color-primary) so runtime rebrand tracks.
292
292
  $input-focus-box-shadow: none; // border IS the focus cue
293
293
  $input-focus-color: $body-color;
294
- $input-padding-y: 0.5rem; // filler inside --wui-control-height (44px)
294
+ $input-padding-y: 0.5rem; // filler inside --wui-control-height (36px at md)
295
295
  $input-padding-x: 0.875rem; // 14px
296
296
  $input-border-radius: $border-radius;
297
297
  // Themed, so it flips in dark mode. The old fixed #99A1B7 was 2.58:1 on white and read as typed text on dark.
@@ -342,7 +342,7 @@ $link-hover-decoration: underline;
342
342
  // colour. A runtime var also avoids capturing the library's compile-time $primary.
343
343
  // $focus-ring-blur matches BS (0) — not restated.
344
344
  $focus-ring-color: var(--color-focus-ring);
345
- $focus-ring-width: 3px; // BS 0.25rem (4px)
345
+ $focus-ring-width: 2px; // matches --wui-focus-ring-width; Bootstrap builds box-shadow strings from it at compile time
346
346
 
347
347
 
348
348
  // ---- Z-index ----------------------------------------------------------
@@ -57,6 +57,11 @@
57
57
  --color-pressed-overlay: color-mix(in srgb, currentColor 12%, transparent);
58
58
  --wui-press-scale: 0.96;
59
59
  --wui-press-duration: 80ms;
60
+ // Disabled: one opacity for every control that dims as a whole (a checkbox, a segmented option, a
61
+ // chip, a tab, an icon button). A text field does not dim; it takes the deeper disabled fill.
62
+ --wui-disabled-opacity: 0.5;
63
+ // The raised thumb that marks the selected option of a segmented control: white in light mode.
64
+ --color-thumb: var(--wui-bg-light);
60
65
  --color-text-muted: var(--wui-text-2);
61
66
  --color-text-subtle: var(--wui-text-3);
62
67
 
@@ -181,6 +186,8 @@
181
186
  // light-mode field-bg override lives on --wui-field-bg.
182
187
  --color-field-bg: var(--wui-dark-bg-muted);
183
188
  --color-surface: var(--wui-dark-bg-surface);
189
+ // A raised thumb is lighter than the muted track it sits on.
190
+ --color-thumb: color-mix(in srgb, var(--wui-dark-text) 12%, var(--wui-dark-bg-muted));
184
191
 
185
192
  --color-border: var(--wui-dark-border);
186
193
  --color-border-light: color-mix(in srgb, var(--wui-dark-border) 50%, transparent);
@@ -87,7 +87,7 @@
87
87
  }
88
88
  // Disabled by aria so a focused button at the end of the row keeps focus.
89
89
  &[aria-disabled="true"] {
90
- opacity: 0.4;
90
+ opacity: var(--wui-disabled-opacity);
91
91
  cursor: default;
92
92
  background: var(--color-surface);
93
93
  color: var(--color-text);
@@ -130,7 +130,7 @@ wui-chart[aria-busy="true"] .wui-chart-surface {
130
130
  }
131
131
  .wui-chart-tooltip-title {
132
132
  margin-bottom: 6px;
133
- font-size: 11.5px;
133
+ font-size: 12px;
134
134
  font-weight: 500;
135
135
  color: var(--color-text-subtle);
136
136
  }
@@ -165,7 +165,7 @@ wui-chart[aria-busy="true"] .wui-chart-surface {
165
165
  margin-top: 6px;
166
166
  padding-top: 6px;
167
167
  border-top: 1px solid var(--color-border);
168
- font-size: 11.5px;
168
+ font-size: 12px;
169
169
  color: var(--color-text-subtle);
170
170
  }
171
171
 
@@ -38,7 +38,7 @@
38
38
 
39
39
  // A card that cannot change: its input is disabled directly or by a disabled fieldset.
40
40
  .struct-card:has(.struct-card-input:disabled) {
41
- opacity: 0.55;
41
+ opacity: var(--wui-disabled-opacity);
42
42
  cursor: not-allowed;
43
43
  }
44
44
  .struct-card:has(.struct-card-input:disabled):hover {
@@ -257,7 +257,7 @@
257
257
  }
258
258
  .search-select-control:disabled {
259
259
  cursor: not-allowed;
260
- opacity: 0.6;
260
+ opacity: var(--wui-disabled-opacity);
261
261
  }
262
262
  .search-select-control:hover:not(:disabled):not(.open):not([aria-invalid="true"]):not(:has(.search-select-ac-input:disabled)):not([aria-disabled="true"]) {
263
263
  // Not while open — an open control keeps its active (primary) border, not the grey hover one.
@@ -287,8 +287,8 @@
287
287
  .search-select-control:focus-visible,
288
288
  .search-select-control:has(> .search-select-input:focus-visible),
289
289
  .search-select-control:has(.ms-input:focus-visible) {
290
- outline: 2px solid var(--color-focus-ring);
291
- outline-offset: 2px;
290
+ outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
291
+ outline-offset: var(--wui-focus-ring-offset);
292
292
  }
293
293
  .search-select-caret {
294
294
  flex-shrink: 0;
@@ -325,16 +325,16 @@
325
325
  .search-select-lg .search-select-multi {
326
326
  height: auto;
327
327
  }
328
- // AppSelectMini is <wui-select compact> (a 32px picker). Its sizes are compact-scaled —
329
- // the compound selector outranks both the plain size rule and the compact rule.
328
+ // AppSelectMini is <wui-select compact>: sm sits on the 32px sm rung, lg one rung below the lg
329
+ // field, never under 32px. The compound selector outranks both the plain size rule and the compact rule.
330
330
  .search-select-compact.search-select-sm .search-select-control {
331
- height: 28px;
332
- min-height: 28px;
331
+ height: var(--wui-control-height-sm);
332
+ min-height: var(--wui-control-height-sm);
333
333
  font-size: 12px;
334
334
  }
335
335
  .search-select-compact.search-select-lg .search-select-control {
336
- height: 38px;
337
- min-height: 38px;
336
+ height: var(--wui-control-height);
337
+ min-height: var(--wui-control-height);
338
338
  font-size: 14px;
339
339
  }
340
340
 
@@ -398,7 +398,7 @@
398
398
  //
399
399
  // <wui-select compact> emitted this class and NOTHING styled it, so AppSelectMini rendered
400
400
  // byte-identical to a full-size select on both surfaces — the catalogue told people to
401
- // reach for it when they wanted something compact, and got a 44px control. It was invisible
401
+ // reach for it when they wanted something compact, and got a full-height control. It was invisible
402
402
  // to every gate because the class check only ever scanned the Razor tag helpers, never the
403
403
  // elements package that actually emits it.
404
404
  //
@@ -406,8 +406,8 @@
406
406
  // pagination) so a compact wui select and a compact native select read as one control
407
407
  // rather than two sizes of the same idea.
408
408
  .search-select-compact .search-select-control {
409
- height: 32px;
410
- min-height: 32px;
409
+ height: var(--wui-control-height-sm);
410
+ min-height: var(--wui-control-height-sm);
411
411
  min-width: 64px;
412
412
  padding: 0 10px;
413
413
  font-size: 13px;
@@ -445,6 +445,7 @@
445
445
  // Search box matches the search bar: rounded field, leading icon turns
446
446
  // primary.
447
447
  .search-select-search {
448
+ position: relative;
448
449
  display: flex;
449
450
  align-items: center;
450
451
  gap: 8px;
@@ -457,9 +458,6 @@
457
458
  background: var(--color-field-bg);
458
459
  color: var(--color-text-subtle);
459
460
  }
460
- .search-select-search {
461
- position: relative;
462
- }
463
461
  // Hover and focus follow AppInput, as the phone country search does: a stronger border on hover,
464
462
  // then a clean surface fill + soft primary border on focus.
465
463
  .search-select-search:hover:not(:focus-within) {
@@ -743,7 +741,7 @@ button.datepicker-month:hover {
743
741
  }
744
742
  .datepicker-weekdays span {
745
743
  text-align: center;
746
- font-size: 11px;
744
+ font-size: 12px;
747
745
  font-weight: 600;
748
746
  text-transform: uppercase;
749
747
  letter-spacing: 0.04em;
@@ -1253,7 +1251,7 @@ wui-timepicker[aria-invalid="true"] .timepicker.open .timepicker-trigger {
1253
1251
  }
1254
1252
  .country-toggle:disabled {
1255
1253
  cursor: not-allowed;
1256
- opacity: 0.6;
1254
+ opacity: var(--wui-disabled-opacity);
1257
1255
  }
1258
1256
  // Flag glyphs come from the generated .wui-flag-* classes (_flags.scss).
1259
1257
  .app-phone-number-input .phone-chevron {
@@ -1633,7 +1631,7 @@ wui-timepicker[aria-invalid="true"] .timepicker.open .timepicker-trigger {
1633
1631
  }
1634
1632
  .timepicker-option:disabled {
1635
1633
  color: var(--color-text-subtle);
1636
- opacity: 0.5;
1634
+ opacity: var(--wui-disabled-opacity);
1637
1635
  cursor: not-allowed;
1638
1636
  }
1639
1637
  .timepicker-option:focus-visible {
@@ -162,7 +162,7 @@ button.wui-cmd-crumb:focus-visible {
162
162
  .wui-cmd-group-label {
163
163
  padding: 8px 10px 4px;
164
164
  color: var(--color-text-muted);
165
- font-size: 11px;
165
+ font-size: 12px;
166
166
  font-weight: 600;
167
167
  letter-spacing: 0.04em;
168
168
  text-transform: uppercase;
@@ -191,7 +191,7 @@ button.wui-cmd-crumb:focus-visible {
191
191
  .wui-cmd-item[aria-disabled="true"] {
192
192
  color: var(--color-text-muted);
193
193
  cursor: not-allowed;
194
- opacity: 0.6;
194
+ opacity: var(--wui-disabled-opacity);
195
195
  }
196
196
  .wui-cmd-item-icon {
197
197
  display: inline-grid;
@@ -290,7 +290,7 @@ button.wui-cmd-crumb:focus-visible {
290
290
  background: var(--color-bg-muted);
291
291
  color: var(--color-text);
292
292
  font-family: inherit;
293
- font-size: 11px;
293
+ font-size: 12px;
294
294
  }
295
295
 
296
296
  @media (prefers-reduced-motion: no-preference) {
@@ -272,7 +272,7 @@
272
272
  .wui-check:disabled,
273
273
  .wui-radio:disabled {
274
274
  cursor: not-allowed;
275
- opacity: 0.55;
275
+ opacity: var(--wui-disabled-opacity);
276
276
  box-shadow: none;
277
277
  }
278
278
  // A disabled box that is ticked keeps its primary fill, only dimmed; the muted fill is for an
@@ -386,7 +386,7 @@
386
386
  box-sizing: border-box;
387
387
  width: 100%;
388
388
  height: var(--wui-control-height);
389
- padding: 12px;
389
+ padding: 6px 12px;
390
390
  border-radius: var(--wui-field-radius);
391
391
  border: 1px solid var(--color-field-border);
392
392
  background: var(--color-field-bg);
@@ -398,20 +398,20 @@
398
398
  box-shadow 0.12s;
399
399
  font-family: inherit;
400
400
  }
401
- // Size steps — sm/lg opt out of the shared 44px control height. App* emits
401
+ // Size steps: sm/lg step to the 32px and 44px rungs. App* emits
402
402
  // .input-sm/.input-lg; Bootstrap's form-control-sm/lg are included so a raw
403
403
  // BS field can match without the .input alias.
404
404
  .form-control-sm,
405
405
  .input-sm {
406
406
  height: var(--wui-control-height-sm);
407
- padding: 6px 10px;
407
+ padding: 5px 10px;
408
408
  font-size: 13px;
409
409
  border-radius: var(--wui-field-radius-sm);
410
410
  }
411
411
  .form-control-lg,
412
412
  .input-lg {
413
413
  height: var(--wui-control-height-lg);
414
- padding: 14px 16px;
414
+ padding: 9px 16px;
415
415
  font-size: 16px;
416
416
  border-radius: var(--wui-field-radius-lg);
417
417
  }
@@ -1352,8 +1352,8 @@ wui-select[compact] {
1352
1352
  border-radius: 0;
1353
1353
  background: transparent;
1354
1354
  // The wrapper owns the 1px border, so the input must shed those 2px. The input
1355
- // is a border-box control-height box (44px at md); nesting it in a 1px-bordered
1356
- // wrapper stacks the wrapper's 2px on top, so a 44px field renders at 46px with a
1355
+ // is a border-box control-height box (36px at md); nesting it in a 1px-bordered
1356
+ // wrapper stacks the wrapper's 2px on top, so a 36px field renders at 38px with a
1357
1357
  // unit. Subtracting the border here keeps input + wrapper border == the control
1358
1358
  // height at every size.
1359
1359
  height: calc(var(--wui-control-height) - 2px);
@@ -1451,8 +1451,8 @@ wui-select[compact] {
1451
1451
  .num-btn {
1452
1452
  display: grid;
1453
1453
  place-items: center;
1454
- // A 44px cell that shrinks before the value does in a narrow field.
1455
- flex: 0 1 44px;
1454
+ // A control-height cell that shrinks before the value does in a narrow field.
1455
+ flex: 0 1 var(--wui-control-height);
1456
1456
  min-width: 0;
1457
1457
  overflow: hidden;
1458
1458
  // Fill the field height and centre the glyph. `height: 100%` resolved to auto
@@ -1483,7 +1483,7 @@ wui-select[compact] {
1483
1483
  .num-btn:disabled {
1484
1484
  color: var(--color-text-subtle);
1485
1485
  cursor: not-allowed;
1486
- opacity: 0.5;
1486
+ opacity: var(--wui-disabled-opacity);
1487
1487
  }
1488
1488
  // Segment dividers: minus divides on its right, plus on its left — so the value
1489
1489
  // cell is bounded on both sides. (DOM order is minus, input, [unit], plus on both
@@ -1497,12 +1497,12 @@ wui-select[compact] {
1497
1497
 
1498
1498
  // ── Size variants ── the inner input carries .input-sm / .input-lg (height +
1499
1499
  // font), the wrapper carries .num-wrap-sm / .num-wrap-lg to scale the button
1500
- // cells to match. Default (md) uses the base 44px cell above.
1500
+ // cells to match. Default (md) uses the control-height cell above.
1501
1501
  .num-wrap-sm {
1502
1502
  border-radius: var(--wui-field-radius-sm);
1503
1503
  }
1504
1504
  .num-wrap-sm .num-btn {
1505
- flex-basis: 36px;
1505
+ flex-basis: var(--wui-control-height-sm);
1506
1506
  }
1507
1507
  .num-wrap-sm .num-input {
1508
1508
  font-size: 13px;
@@ -1511,7 +1511,7 @@ wui-select[compact] {
1511
1511
  border-radius: var(--wui-field-radius-lg);
1512
1512
  }
1513
1513
  .num-wrap-lg .num-btn {
1514
- flex-basis: 52px;
1514
+ flex-basis: var(--wui-control-height-lg);
1515
1515
  }
1516
1516
  .num-wrap-lg .num-input {
1517
1517
  font-size: 16px;
@@ -1613,17 +1613,19 @@ wui-select[compact] {
1613
1613
  }
1614
1614
 
1615
1615
  // ── Choice chip ──
1616
- // Inline selectable chip — native control hidden, pill adopts the primary
1617
- // accent when checked.
1616
+ // Inline selectable chip: the native input is hidden and the pill shows its state. The look reads
1617
+ // the input itself, so a server-rendered chip changes when it is clicked, with no script.
1618
1618
  .choice-chip {
1619
1619
  display: inline-flex;
1620
1620
  align-items: center;
1621
1621
  gap: 8px;
1622
- padding: 9px 14px;
1622
+ min-height: var(--wui-control-height);
1623
+ padding: 0 14px;
1623
1624
  border: 1px solid var(--color-border);
1624
1625
  border-radius: 999px;
1625
1626
  font-size: 14px;
1626
1627
  font-weight: 500;
1628
+ line-height: 1.2;
1627
1629
  color: var(--color-text);
1628
1630
  background: var(--color-surface);
1629
1631
  cursor: pointer;
@@ -1633,7 +1635,7 @@ wui-select[compact] {
1633
1635
  background 0.12s ease,
1634
1636
  color 0.12s ease;
1635
1637
  }
1636
- .choice-chip:hover {
1638
+ .choice-chip:hover:not(:has(input:disabled)) {
1637
1639
  border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
1638
1640
  }
1639
1641
  .choice-chip:has(input:focus-visible) {
@@ -1645,62 +1647,68 @@ wui-select[compact] {
1645
1647
  opacity: 0;
1646
1648
  pointer-events: none;
1647
1649
  }
1648
- .choice-chip.checked {
1649
- // Match AppChoiceCard's checked border: a soft primary-30 edge, not the
1650
- // full-strength primary line (they read as the same selection language).
1650
+ // Match AppChoiceCard's checked border: a soft primary-30 edge, not a full-strength line.
1651
+ .choice-chip:has(input:checked) {
1651
1652
  border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
1652
1653
  background: var(--color-primary-soft);
1653
1654
  color: var(--color-primary-ink);
1654
1655
  }
1656
+ // A tick before the label, so selection does not rest on the tint alone. Its slot is kept on every
1657
+ // chip, so picking one never widens it and a wrapping row never shifts.
1658
+ .choice-chip::before {
1659
+ content: "";
1660
+ flex: none;
1661
+ width: 14px;
1662
+ height: 14px;
1663
+ background: transparent;
1664
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7'/%3E%3C/svg%3E") center / contain no-repeat;
1665
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7'/%3E%3C/svg%3E") center / contain no-repeat;
1666
+ }
1667
+ .choice-chip:has(input:checked)::before {
1668
+ background: currentColor;
1669
+ }
1670
+ .choice-chip:has(input:disabled) {
1671
+ opacity: var(--wui-disabled-opacity);
1672
+ cursor: not-allowed;
1673
+ }
1655
1674
 
1656
- // A count badge inside a selectable chip — "Active 12". Inverts on selection for
1657
- // the same reason the nav badge does: the chip already carries the soft tint, so a
1658
- // tinted badge on top of it would disappear.
1675
+ // A count badge inside a selectable chip ("Active 12"). It inverts on selection: the chip already
1676
+ // carries the soft tint, so a tinted badge on top of it would disappear.
1659
1677
  .choice-chip .count {
1660
1678
  padding: 1px 6px;
1661
1679
  border-radius: 999px;
1662
1680
  background: var(--color-bg-muted);
1663
- font-size: 11px;
1681
+ font-size: 12px;
1664
1682
  font-variant-numeric: tabular-nums;
1665
1683
  color: var(--color-text-subtle);
1666
1684
  }
1667
- .choice-chip.checked .count {
1685
+ .choice-chip:has(input:checked) .count {
1668
1686
  background: var(--color-surface);
1669
1687
  color: var(--color-primary-ink);
1670
1688
  }
1671
1689
 
1672
- // Size scale (`size`): padding, gap and font step together; md is the base above.
1690
+ // Size scale (`size`): height, padding, gap and font step together; md is the base above.
1673
1691
  .choice-chip-sm {
1674
1692
  gap: 6px;
1675
- padding: 6px 11px;
1693
+ min-height: var(--wui-control-height-sm);
1694
+ padding: 0 11px;
1676
1695
  font-size: 13px;
1677
1696
  }
1678
- .choice-chip-sm .count {
1679
- font-size: 10px;
1680
- }
1681
1697
  .choice-chip-lg {
1682
1698
  gap: 10px;
1683
- padding: 11px 18px;
1699
+ min-height: var(--wui-control-height-lg);
1700
+ padding: 0 18px;
1684
1701
  font-size: 15px;
1685
1702
  }
1686
- .choice-chip-lg .count {
1687
- font-size: 12px;
1688
- }
1689
1703
 
1690
1704
  // A count beside an AppSegmented option's label: a small muted figure with tabular digits, the
1691
1705
  // same treatment as a choice chip's count. Plain colour, no opacity, so it keeps its contrast.
1692
1706
  .wui-segmented-option-count {
1693
- font-size: 11px;
1707
+ font-size: 12px;
1694
1708
  font-weight: 500;
1695
1709
  font-variant-numeric: tabular-nums;
1696
1710
  color: var(--color-text-muted);
1697
1711
  }
1698
- // Keyboard focus on the selected segment: the ring sits inside the pill, over its selected ring,
1699
- // so the focus cue and the selection read as one shape and not as two rings. The selector outranks
1700
- // the unscoped focus rule in _display.scss, which loads after this partial.
1701
- .segmented .segmented-option:has(.segmented-input:checked:focus-visible) {
1702
- outline-offset: var(--wui-focus-ring-inset);
1703
- }
1704
1712
 
1705
1713
  // Form section heading — a branded accent bar rather than a full-width hairline,
1706
1714
  // so a long form reads as designed sections instead of raw rules.
@@ -1789,7 +1797,7 @@ wui-otp {
1789
1797
  width: 44px;
1790
1798
  min-width: 22px;
1791
1799
  flex: 0 1 auto;
1792
- height: var(--wui-control-height);
1800
+ height: var(--wui-control-height-lg);
1793
1801
  // Colour model matches AppInput exactly: muted resting fill + soft border, same
1794
1802
  // field radius. State rules below mirror AppInput's hover/focus/invalid/disabled.
1795
1803
  border: 1px solid var(--color-field-border);
@@ -1936,7 +1944,7 @@ wui-otp[aria-invalid="true"] .otp-slot {
1936
1944
  .slider:has([disabled]) .slider-thumb {
1937
1945
  border-color: var(--color-text-subtle);
1938
1946
  cursor: not-allowed;
1939
- opacity: 0.6;
1947
+ opacity: var(--wui-disabled-opacity);
1940
1948
  }
1941
1949
  .slider:has([disabled]) .slider-fill {
1942
1950
  background-color: var(--color-text-subtle);