@webority/theme 0.64.0 → 0.66.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.64.0",
3
+ "version": "0.66.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": {
@@ -523,6 +523,21 @@
523
523
  margin-top: 0.25rem;
524
524
  }
525
525
 
526
+ // An item that hides from a width up (AppDropdown item `hiddenFrom`): the wide screen shows the
527
+ // same control in its own place, so the one overflow menu drops the row from that width on.
528
+ @media (min-width: 576px) {
529
+ .dropdown-item.wui-hidden-from-sm { display: none; }
530
+ }
531
+ @media (min-width: 768px) {
532
+ .dropdown-item.wui-hidden-from-md { display: none; }
533
+ }
534
+ @media (min-width: 992px) {
535
+ .dropdown-item.wui-hidden-from-lg { display: none; }
536
+ }
537
+ @media (min-width: 1200px) {
538
+ .dropdown-item.wui-hidden-from-xl { display: none; }
539
+ }
540
+
526
541
  // ---- Ghost button: the tertiary action --------------------------------
527
542
  // Brand-coloured, underlined text with no box in any state. It has no visible container,
528
543
  // so nothing may draw one: no hover fill, no sheen sweep, no lift or press nudge, and no
package/scss/_tokens.scss CHANGED
@@ -43,8 +43,10 @@
43
43
  --wui-field-bg: #F9FBFC;
44
44
  // Resting and hover border of every input-like control. They clear WCAG 1.4.11's 3:1
45
45
  // against the field fill and the surface, so a field's edge is visible without focus.
46
- --wui-field-border: #80899F;
47
- --wui-field-border-hover: #687088;
46
+ // Both sit just above the floor (3.11:1 and 3.63:1 on the fill) so the edge stays light;
47
+ // check-contrast fails if either drops under it.
48
+ --wui-field-border: #888FA4;
49
+ --wui-field-border-hover: #7C839A;
48
50
  --wui-border-light: #EBEDF3;
49
51
  --wui-border-soft: #F4F6F8;
50
52
  // The unfilled part of a progress bar / meter. Darker than every surface it can sit on
@@ -64,6 +64,7 @@
64
64
  @import "wui/carousel";
65
65
  @import "wui/onboarding-checklist";
66
66
  @import "wui/rating";
67
+ @import "wui/score-scale";
67
68
  @import "wui/relative-time";
68
69
  @import "wui/resizable";
69
70
  @import "wui/rolling-number";
@@ -72,6 +73,10 @@
72
73
  // After `navigation`/`overlays`: the switcher restates dropdown-item chrome those partials
73
74
  // set, so it has to win on equal specificity.
74
75
  @import "wui/workspace";
76
+ @import "wui/page-header-bar";
77
+ @import "wui/filter-popover";
78
+ @import "wui/metric-strip";
79
+ @import "wui/due-date";
75
80
 
76
81
  // Country flags are NOT part of this aggregate. The 245 `.wui-flag-*` rules are about 187KB raw
77
82
  // (38KB Brotli) of inlined SVG, and most sites never render a phone field or country picker.
@@ -11,6 +11,8 @@
11
11
  // engine draws inside .wui-chart-canvas with colours the element reads from --wui-chart-*.
12
12
  wui-chart {
13
13
  position: relative;
14
+ // Keeps the engine tooltip in the chart's own stacking layer, under sticky page chrome.
15
+ isolation: isolate;
14
16
  display: block;
15
17
  min-width: 0;
16
18
  color: var(--color-text-muted);
@@ -1471,6 +1471,21 @@ wui-select[compact] {
1471
1471
  font-size: 12px;
1472
1472
  }
1473
1473
 
1474
+ // A count beside an AppSegmented option's label: a small muted figure with tabular digits, the
1475
+ // same treatment as a choice chip's count. Plain colour, no opacity, so it keeps its contrast.
1476
+ .wui-segmented-option-count {
1477
+ font-size: 11px;
1478
+ font-weight: 500;
1479
+ font-variant-numeric: tabular-nums;
1480
+ color: var(--color-text-muted);
1481
+ }
1482
+ // Keyboard focus on the selected segment: the ring sits inside the pill, over its selected ring,
1483
+ // so the focus cue and the selection read as one shape and not as two rings. The selector outranks
1484
+ // the unscoped focus rule in _display.scss, which loads after this partial.
1485
+ .segmented .segmented-option:has(.segmented-input:checked:focus-visible) {
1486
+ outline-offset: var(--wui-focus-ring-inset);
1487
+ }
1488
+
1474
1489
  // Form section heading — a branded accent bar rather than a full-width hairline,
1475
1490
  // so a long form reads as designed sections instead of raw rules.
1476
1491
  //
@@ -194,7 +194,6 @@
194
194
  font-size: 12px;
195
195
  font-weight: 500;
196
196
  letter-spacing: 0.04em;
197
- text-transform: uppercase;
198
197
  white-space: nowrap;
199
198
  vertical-align: middle;
200
199
  transition:
@@ -293,6 +292,10 @@
293
292
  // block above) — if it ever needs viewport-relative sizing again, do it without turning
294
293
  // the scroll container into a query container.
295
294
  }
295
+ // Flush: no inset above the column header row, so the header sits at the top of the wrap.
296
+ .table-wrap.wui-table-flush:has(> .table-ledger) {
297
+ padding-top: 0;
298
+ }
296
299
  .table-ledger {
297
300
  border-spacing: 0;
298
301
  }
@@ -306,7 +309,6 @@
306
309
  font-weight: 500;
307
310
  line-height: 17px;
308
311
  letter-spacing: 0.04em;
309
- text-transform: uppercase;
310
312
  // --color-text-subtle (#78829D) is ~3.5:1 on the muted band, below WCAG AA's 4.5:1
311
313
  // for 12px text. A column header names the data under it — that is not decorative
312
314
  // text, so it takes the muted ramp (~7.5:1). Caught by the axe pass, on both surfaces.
@@ -884,6 +886,12 @@
884
886
  min-width: 0;
885
887
  }
886
888
 
889
+ // The identity column takes the width the other columns leave (its header carries wui-col-grow).
890
+ // Its name clamps to one line, and the full text is the tooltip.
891
+ .table-ledger th.wui-col-grow {
892
+ width: 100%;
893
+ }
894
+
887
895
  // Row-selection checkbox column.
888
896
  .table-ledger th.wui-select-cell,
889
897
  .table-ledger td.wui-select-cell {
@@ -953,7 +961,6 @@
953
961
  font-size: 12px;
954
962
  font-weight: 600;
955
963
  letter-spacing: 0.04em;
956
- text-transform: uppercase;
957
964
  color: var(--color-text-subtle);
958
965
  }
959
966
  .wui-column-menu-item {
@@ -1027,6 +1034,7 @@
1027
1034
  display: flex;
1028
1035
  align-items: center;
1029
1036
  justify-content: space-between;
1037
+ flex-wrap: wrap;
1030
1038
  gap: 12px;
1031
1039
  height: auto;
1032
1040
  min-height: 0;
@@ -1067,6 +1075,29 @@
1067
1075
  .table-ledger.wui-table-stack > * > tr > td:not(.wui-select-cell, .wui-expand-cell):is(:first-child, .wui-select-cell + td, .wui-expand-cell + td)::before {
1068
1076
  content: none;
1069
1077
  }
1078
+ // A title wraps in full on a card: a phone reader has no tooltip to read the rest from.
1079
+ .table-ledger.wui-table-stack .u-name {
1080
+ display: block;
1081
+ overflow: visible;
1082
+ }
1083
+ // A group header is a section label between the cards, not a card of its own.
1084
+ .table-ledger.wui-table-stack > tbody > tr.wui-group-row {
1085
+ margin-bottom: 0;
1086
+ padding: 0;
1087
+ border: 0;
1088
+ border-radius: 0;
1089
+ background: none;
1090
+ }
1091
+ .table-ledger.wui-table-stack > tbody > tr.wui-group-row > th.wui-group-cell {
1092
+ display: block;
1093
+ padding: 12px 0 4px;
1094
+ border: 0;
1095
+ background: none;
1096
+ }
1097
+ // A row with no actions leaves no empty band under its card.
1098
+ .table-ledger.wui-table-stack > tbody > tr > td:has(> .u-row-actions:empty) {
1099
+ display: none;
1100
+ }
1070
1101
  // Empty cells (a footer's blank columns) take no line.
1071
1102
  .table-ledger.wui-table-stack > * > tr > td:empty {
1072
1103
  display: none;
@@ -413,6 +413,40 @@ a.info-row:hover .info-value {
413
413
  --descs-cols: 1;
414
414
  }
415
415
  }
416
+ // Summary layout (React `layout="summary"`): one row per item, its label left and its value right.
417
+ // Declared after the column variants so the single column wins on equal specificity.
418
+ .wui-descs-summary .descs-list {
419
+ --descs-cols: 1;
420
+ }
421
+ .wui-descs-summary .descs-item {
422
+ flex-direction: row;
423
+ align-items: baseline;
424
+ justify-content: space-between;
425
+ gap: 16px;
426
+ grid-column: 1 / -1;
427
+ padding: 12px 16px;
428
+ }
429
+ .wui-descs-summary .descs-label,
430
+ .wui-descs-summary .descs-value {
431
+ padding: 0;
432
+ }
433
+ .wui-descs-summary .descs-value {
434
+ margin-left: auto;
435
+ text-align: right;
436
+ font-variant-numeric: tabular-nums;
437
+ }
438
+ // Emphasis (React item `emphasis`): the total row, bold, with a rule above it. The row before it
439
+ // drops its own bottom rule so the two do not stack into a double line.
440
+ .descs-item.wui-descs-emphasis {
441
+ border-top: 1px solid var(--color-text-muted);
442
+ }
443
+ .descs-item:has(+ .wui-descs-emphasis) {
444
+ border-bottom-color: transparent;
445
+ }
446
+ .descs-item.wui-descs-emphasis .descs-label,
447
+ .descs-item.wui-descs-emphasis .descs-value {
448
+ font-weight: 700;
449
+ }
416
450
 
417
451
  // ============================================================================
418
452
  // Segmented control — 2–4 mutually-exclusive options shown side by side.
@@ -0,0 +1,26 @@
1
+ // ── Due date ──
2
+ // AppDueDate / <app-due-date> (a <wui-due-date> element): a due date that never wraps. An overdue
3
+ // date reads in danger with its glyph, a date within soon-days reads "N days left" in warning.
4
+ //
5
+ // Part of the wui-components layer, see _wui-components.scss for load order.
6
+
7
+ .wui-due-date {
8
+ white-space: nowrap;
9
+ font-variant-numeric: tabular-nums;
10
+
11
+ > .wui-icon {
12
+ margin-right: 0.25rem;
13
+ }
14
+ }
15
+
16
+ .wui-due-date-danger {
17
+ color: var(--color-danger-text);
18
+ }
19
+
20
+ .wui-due-date-warning {
21
+ color: var(--color-warning-text);
22
+ }
23
+
24
+ .wui-due-date-empty {
25
+ color: var(--color-text-muted);
26
+ }
@@ -0,0 +1,63 @@
1
+ // The floating filter panel: <wui-filter-popover> with its Filters button, drawn by AppFilterBar
2
+ // `floating` and <app-filter-bar floating>. The element places the panel (anchorOverlay, fixed), so
3
+ // this partial only draws it: a 600 px panel of facets in two columns, and a foot with the Keep open
4
+ // switch and Clear all.
5
+
6
+ .wui-filter-popover {
7
+ display: inline-block;
8
+ }
9
+
10
+ .wui-filter-popover-trigger {
11
+ white-space: nowrap;
12
+ }
13
+
14
+ .wui-filter-popover-panel {
15
+ box-sizing: border-box;
16
+ width: 600px;
17
+ max-width: calc(100vw - 1rem);
18
+ max-height: 80vh;
19
+ overflow-y: auto;
20
+ padding: 1rem;
21
+ background: var(--color-surface);
22
+ border: 1px solid var(--color-border);
23
+ border-radius: var(--wui-radius-lg);
24
+ box-shadow: var(--wui-shadow-overlay);
25
+ z-index: var(--wui-z-popover);
26
+ }
27
+
28
+ .wui-filter-popover-panel[hidden] {
29
+ display: none;
30
+ }
31
+
32
+ .wui-filter-popover-grid {
33
+ display: grid;
34
+ grid-template-columns: repeat(2, minmax(0, 1fr));
35
+ gap: 0.75rem 1rem;
36
+ align-items: start;
37
+ }
38
+
39
+ @media (max-width: 575.98px) {
40
+ .wui-filter-popover-grid {
41
+ grid-template-columns: minmax(0, 1fr);
42
+ }
43
+ }
44
+
45
+ .wui-filter-popover-foot {
46
+ display: flex;
47
+ flex-wrap: wrap;
48
+ align-items: center;
49
+ justify-content: space-between;
50
+ gap: 0.5rem 1rem;
51
+ margin-top: 1rem;
52
+ padding-top: 0.75rem;
53
+ border-top: 1px solid var(--color-border);
54
+ }
55
+
56
+ .wui-filter-popover-count {
57
+ font-weight: 600;
58
+ }
59
+
60
+ .wui-filter-popover-more {
61
+ white-space: nowrap;
62
+ cursor: default;
63
+ }
@@ -145,6 +145,14 @@
145
145
  display: none;
146
146
  }
147
147
 
148
+ // head-layout="stacked": the stage name takes a line of its own and the tone dot goes.
149
+ .wui-kanban[data-head-layout="stacked"] .wui-kanban-dot {
150
+ display: none;
151
+ }
152
+ .wui-kanban[data-head-layout="stacked"] .wui-kanban-title {
153
+ flex: 1 0 100%;
154
+ }
155
+
148
156
  // The page's own controls at the right of the column head (a "+" that adds a card, say). They sit
149
157
  // before the collapse button, which then gives up its auto margin.
150
158
  .wui-kanban-actions {
@@ -256,6 +264,11 @@
256
264
  .wui-kanban-card:hover {
257
265
  border-color: var(--color-border-strong);
258
266
  }
267
+ // The card just added (active-card-key), tinted as the active row of a table is.
268
+ .wui-kanban-card[aria-current="true"] {
269
+ background: var(--color-primary-soft);
270
+ border-color: var(--color-primary);
271
+ }
259
272
  .wui-kanban-card:focus-visible {
260
273
  outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
261
274
  outline-offset: var(--wui-focus-ring-offset);
@@ -499,6 +512,11 @@
499
512
  overscroll-behavior-y: contain;
500
513
  padding-top: 4px;
501
514
  }
515
+ // The body is a flex column with a fixed height here, so a card would squash to fit; it keeps its
516
+ // own height and the column scrolls instead.
517
+ .wui-kanban[fill] .wui-kanban-card {
518
+ flex-shrink: 0;
519
+ }
502
520
  }
503
521
 
504
522
  // ── Phone column switcher and swipe hint (hidden from md up) ──
@@ -0,0 +1,85 @@
1
+ // ============================================================================
2
+ // Metric strip: a row of linked values, each a label over a value over a note.
3
+ // No card chrome. Markup and classes are the same for <app-metric-strip> and
4
+ // React AppMetricStrip.
5
+ // ============================================================================
6
+
7
+ .wui-metric-strip {
8
+ display: grid;
9
+ grid-template-columns: repeat(2, minmax(0, 1fr));
10
+ gap: 16px;
11
+ align-items: end;
12
+ }
13
+ // Two columns below 768px, the column count from the attribute above it.
14
+ @media (min-width: 768px) {
15
+ .wui-metric-strip.wui-metric-strip-cols-3 {
16
+ grid-template-columns: repeat(3, minmax(0, 1fr));
17
+ }
18
+ .wui-metric-strip.wui-metric-strip-cols-4 {
19
+ grid-template-columns: repeat(4, minmax(0, 1fr));
20
+ }
21
+ .wui-metric-strip.wui-metric-strip-cols-5 {
22
+ grid-template-columns: repeat(5, minmax(0, 1fr));
23
+ }
24
+ }
25
+
26
+ .wui-metric {
27
+ display: flex;
28
+ flex-direction: column;
29
+ min-width: 0;
30
+ color: inherit;
31
+ text-decoration: none;
32
+ }
33
+ a.wui-metric:focus-visible {
34
+ outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
35
+ outline-offset: var(--wui-focus-ring-offset);
36
+ }
37
+
38
+ .wui-metric-label {
39
+ font-size: 13px;
40
+ font-weight: 500;
41
+ color: var(--color-text-muted);
42
+ white-space: nowrap;
43
+ overflow: hidden;
44
+ text-overflow: ellipsis;
45
+ }
46
+ .wui-metric-value {
47
+ margin-top: 2px;
48
+ font-size: 20px;
49
+ font-weight: 600;
50
+ line-height: 1.2;
51
+ font-variant-numeric: tabular-nums;
52
+ color: var(--color-text);
53
+ white-space: nowrap;
54
+ }
55
+ a.wui-metric:hover .wui-metric-value {
56
+ text-decoration: underline;
57
+ text-underline-offset: 3px;
58
+ }
59
+ .wui-metric-note {
60
+ margin-top: 2px;
61
+ font-size: 12px;
62
+ color: var(--color-text-muted);
63
+ }
64
+
65
+ .wui-metric-strip-sm .wui-metric-label,
66
+ .wui-metric-strip-sm .wui-metric-note {
67
+ font-size: 12px;
68
+ }
69
+ .wui-metric-strip-sm .wui-metric-value {
70
+ font-size: 18px;
71
+ }
72
+
73
+ // The tone tints the value only; the label and note stay muted.
74
+ .wui-metric-tone-success .wui-metric-value {
75
+ color: var(--color-success-text);
76
+ }
77
+ .wui-metric-tone-warning .wui-metric-value {
78
+ color: var(--color-warning-text);
79
+ }
80
+ .wui-metric-tone-danger .wui-metric-value {
81
+ color: var(--color-danger-text);
82
+ }
83
+ .wui-metric-tone-info .wui-metric-value {
84
+ color: var(--color-info-text);
85
+ }
@@ -53,3 +53,37 @@
53
53
  .wui-modal-submit-error {
54
54
  margin-bottom: 16px;
55
55
  }
56
+
57
+ // The remote drawer: the same shell and card, docked to the end edge at full height. Width
58
+ // comes from `--wui-modal-max-width` (sm 360, md 480, lg 640), set on the shell by the script.
59
+ .modal.wui-modal-shell.wui-remote-drawer {
60
+ justify-content: flex-end;
61
+ align-items: stretch;
62
+ padding: 0;
63
+ }
64
+ .modal.wui-remote-drawer .modal-dialog.wui-remote-drawer-dialog {
65
+ margin: 0;
66
+ max-width: none;
67
+ width: min(var(--wui-modal-max-width, 480px), 100%);
68
+ height: 100%;
69
+ display: flex;
70
+ pointer-events: none;
71
+ transform: translateX(100%);
72
+ transition: transform 0.25s var(--wui-ease-ui);
73
+ }
74
+ .modal.wui-remote-drawer.show .modal-dialog.wui-remote-drawer-dialog {
75
+ transform: none;
76
+ }
77
+ .modal.wui-remote-drawer .wui-remote-drawer-dialog .wui-modal {
78
+ pointer-events: auto;
79
+ max-width: none;
80
+ max-height: none;
81
+ height: 100%;
82
+ border-radius: 0;
83
+ box-shadow: var(--shadow-overlay-lg);
84
+ }
85
+ @media (prefers-reduced-motion: reduce) {
86
+ .modal.wui-remote-drawer .modal-dialog.wui-remote-drawer-dialog {
87
+ transition: none;
88
+ }
89
+ }
@@ -0,0 +1,68 @@
1
+ // ── Bar ── (`size="bar"`): one 40px header row. The breadcrumbs lead as an inline prefix, the
2
+ // title takes the spare width and ends in an ellipsis, and the row never wraps from md up.
3
+ // Below md the row wraps, so the actions drop under the title instead of overlapping it.
4
+ // Selectors name `.page-header.page-header-bar` because `.page-header-bar` alone is also the
5
+ // inner row of every other size.
6
+ .page-header.page-header-bar {
7
+ flex-wrap: nowrap;
8
+ gap: 8px;
9
+ min-height: 40px;
10
+ // The bar sits 4px above the first content block.
11
+ margin-bottom: 4px;
12
+ }
13
+ .page-header.page-header-bar .page-title {
14
+ flex: 1 1 0;
15
+ min-width: 0;
16
+ font-size: 20px;
17
+ overflow: hidden;
18
+ text-overflow: ellipsis;
19
+ white-space: nowrap;
20
+ }
21
+ .page-header.page-header-bar .page-status,
22
+ .page-header-bar-views,
23
+ .page-header-bar-meta,
24
+ .page-header-bar-tools,
25
+ .page-header.page-header-bar .page-actions {
26
+ flex: none;
27
+ }
28
+ .page-header-bar-crumbs {
29
+ flex: none;
30
+ font-size: 13px;
31
+ color: var(--color-text-muted);
32
+ white-space: nowrap;
33
+ }
34
+ .page-header-bar-views,
35
+ .page-header-bar-tools,
36
+ .page-header-bar-bulk {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: 8px;
40
+ }
41
+ .page-header-bar-bulk[hidden] {
42
+ display: none;
43
+ }
44
+ .page-header-bar-meta {
45
+ font-size: 13px;
46
+ color: var(--color-text-muted);
47
+ white-space: nowrap;
48
+ }
49
+ // A meta line set below the bar takes a row of its own under the bar, with no row gap, so the
50
+ // detail budget (72px to the first section) holds: 12 + 40 + 16 + 4.
51
+ .page-header.wui-page-header-meta-below {
52
+ flex-wrap: wrap;
53
+ row-gap: 0;
54
+ }
55
+ .page-header.wui-page-header-meta-below > .page-header-bar-meta {
56
+ flex: 0 0 100%;
57
+ line-height: 16px;
58
+ }
59
+
60
+ @media (max-width: 767.98px) {
61
+ .page-header.page-header-bar {
62
+ flex-wrap: wrap;
63
+ }
64
+ // The title keeps its content width here, so a long row wraps the actions under it.
65
+ .page-header.page-header-bar .page-title {
66
+ flex: 0 1 auto;
67
+ }
68
+ }
@@ -0,0 +1,61 @@
1
+ // ── Score scale ──
2
+ // AppScoreScale / <app-score-scale>: one value per radio on a row of equal-width items that wraps on narrow screens.
3
+ // Each value is a label around a visually hidden native radio, so the arrow keys and the checked state come from the browser.
4
+ //
5
+ // Part of the wui-components layer, see _wui-components.scss for load order.
6
+
7
+ .wui-score-scale {
8
+ display: flex;
9
+ flex-wrap: wrap;
10
+ gap: 6px;
11
+ }
12
+ .wui-score-scale__item {
13
+ position: relative;
14
+ display: inline-flex;
15
+ flex: 1 1 2.75rem;
16
+ align-items: center;
17
+ justify-content: center;
18
+ min-height: 44px;
19
+ margin: 0;
20
+ padding: 0 6px;
21
+ border: 1px solid var(--color-border);
22
+ border-radius: var(--wui-radius-sm);
23
+ background: var(--color-surface);
24
+ color: var(--color-text);
25
+ font-variant-numeric: tabular-nums;
26
+ cursor: pointer;
27
+ transition:
28
+ border-color 0.12s ease,
29
+ background 0.12s ease,
30
+ color 0.12s ease;
31
+ }
32
+ .wui-score-scale__item:not(.wui-score-scale__item--selected):hover {
33
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
34
+ }
35
+ .wui-score-scale__item--selected {
36
+ border-color: var(--color-primary);
37
+ background: var(--color-primary);
38
+ color: var(--color-on-primary-ink);
39
+ }
40
+ .wui-score-scale__item:has(input:focus-visible) {
41
+ outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
42
+ outline-offset: var(--wui-focus-ring-offset);
43
+ }
44
+ .wui-score-scale__item:has(input:disabled) {
45
+ opacity: 0.55;
46
+ cursor: not-allowed;
47
+ }
48
+ .wui-score-scale__ends {
49
+ display: flex;
50
+ flex-basis: 100%;
51
+ justify-content: space-between;
52
+ gap: 12px;
53
+ color: var(--color-text-muted);
54
+ font-size: 13px;
55
+ }
56
+
57
+ @media (prefers-reduced-motion: reduce) {
58
+ .wui-score-scale__item {
59
+ transition: none;
60
+ }
61
+ }
@@ -734,9 +734,31 @@
734
734
  }
735
735
 
736
736
  .app-content {
737
+ --app-content-padding-top: 24px;
737
738
  flex: 1 1 auto;
738
739
  min-width: 0;
739
- padding: 24px 28px;
740
+ padding: var(--app-content-padding-top) 28px 24px;
741
+ }
742
+ [data-density="compact"] .app-content {
743
+ --app-content-padding-top: 12px;
744
+ }
745
+ // The skip link is the shell's first stop. It stays off screen until it takes focus, and being
746
+ // absolutely placed it takes no cell of the shell's grid.
747
+ .wui-skip-link {
748
+ position: absolute;
749
+ top: 8px;
750
+ left: 8px;
751
+ z-index: var(--wui-z-fixed);
752
+ padding: 8px 12px;
753
+ border-radius: var(--wui-radius);
754
+ background: var(--color-surface);
755
+ color: var(--color-text);
756
+ transform: translateY(-200%);
757
+ }
758
+ .wui-skip-link:focus-visible {
759
+ transform: none;
760
+ outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
761
+ outline-offset: var(--wui-focus-ring-offset);
740
762
  }
741
763
  .app-footer {
742
764
  position: fixed;
@@ -816,7 +838,11 @@
816
838
  padding-inline: 16px;
817
839
  }
818
840
  .app-shell.app-shell .app-content {
819
- padding-block: 16px 32px;
841
+ --app-content-padding-top: 16px;
842
+ padding-block: var(--app-content-padding-top) 32px;
843
+ }
844
+ .app-shell.app-shell[data-density="compact"] .app-content {
845
+ --app-content-padding-top: 12px;
820
846
  }
821
847
  }
822
848
  // Phones: the topbar's icon controls meet the 44px touch floor (36px is a pointer size).
@@ -125,6 +125,10 @@
125
125
  overflow-y: auto;
126
126
  overscroll-behavior: contain;
127
127
  }
128
+ .wui-split-items:focus-visible {
129
+ outline: var(--wui-focus-ring-width) solid var(--color-focus-ring);
130
+ outline-offset: var(--wui-focus-ring-inset);
131
+ }
128
132
 
129
133
  .wui-split-item {
130
134
  display: flex;
@@ -136,3 +136,11 @@
136
136
  transition: none;
137
137
  }
138
138
  }
139
+
140
+ // The filters slot of <app-table-toolbar> takes the spare width of its row from md up, so the filter
141
+ // bar and its floating panel sit beside the search and the actions rather than wrapping under them.
142
+ @media (min-width: 768px) {
143
+ .wui-table-toolbar-filters {
144
+ flex: 1 1 auto;
145
+ }
146
+ }