@webority/theme 0.9.10 → 0.9.12

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.9.10",
3
+ "version": "0.9.12",
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": {
@@ -419,6 +419,31 @@
419
419
  --bs-dropdown-link-active-color: var(--color-primary-hover);
420
420
  --bs-dropdown-link-active-bg: var(--color-primary-soft);
421
421
  box-shadow: var(--wui-shadow-overlay);
422
+ // A long menu (account / workspace lists) must never spill past the screen and clip its
423
+ // last rows: cap the panel to the viewport and scroll INSIDE it. The room subtracted
424
+ // leaves space for a topbar-height trigger + margin — the common anchor for these long
425
+ // lists — so a top-anchored menu fits; Popper flips/shifts the panel for other anchors.
426
+ // The thin thumb matches the shared rail (--wui-scrollbar*), like .wui-scroll.
427
+ max-height: calc(100vh - 5rem);
428
+ overflow-y: auto;
429
+ scrollbar-width: thin;
430
+ scrollbar-color: var(--wui-scrollbar) transparent;
431
+ }
432
+ .dropdown-menu::-webkit-scrollbar {
433
+ width: 8px;
434
+ }
435
+ .dropdown-menu::-webkit-scrollbar-track {
436
+ background: transparent;
437
+ }
438
+ .dropdown-menu::-webkit-scrollbar-thumb {
439
+ background-color: var(--wui-scrollbar);
440
+ border-radius: 9999px;
441
+ border: 2px solid transparent;
442
+ background-clip: padding-box;
443
+ }
444
+ .dropdown-menu::-webkit-scrollbar-thumb:hover {
445
+ background-color: var(--wui-scrollbar-hover);
446
+ background-clip: padding-box;
422
447
  }
423
448
 
424
449
  .dropdown-item {
@@ -59,6 +59,7 @@
59
59
  @import "flags";
60
60
  @import "wui/shell";
61
61
  @import "wui/dock";
62
+ @import "wui/feedback";
62
63
  @import "wui/toolbar";
63
64
  // Last: restates the settled state of menus other partials style, so it must win.
64
65
  @import "wui/menu-motion";
@@ -13,14 +13,16 @@
13
13
  color: var(--color-text-subtle);
14
14
  }
15
15
  .empty-state-title {
16
- font-size: 16px;
16
+ // Matches .tbl-empty-title (the DataTable empty state) so the two read as one family.
17
+ font-size: 18px;
17
18
  font-weight: 600;
18
19
  line-height: 1.35;
19
20
  color: var(--color-text);
20
21
  }
21
22
  .empty-state-message {
22
23
  margin: 6px 0 0;
23
- font-size: 14px;
24
+ // Matches .tbl-empty-text.
25
+ font-size: 16px;
24
26
  line-height: 1.5;
25
27
  color: var(--color-text-muted);
26
28
  }
@@ -112,39 +114,26 @@
112
114
  .table-ledger tbody tr:hover td.wui-table-empty:has(> .tbl-empty) {
113
115
  background: transparent;
114
116
  }
115
- // Keep the empty state on screen. An empty table has no data to scroll to, yet its
116
- // header row can still be wider than the viewport (many columns, or wide header
117
- // labels), so `.table-wrap` would scroll horizontally and the centred empty message —
118
- // spanning that full width in a colSpan cell — would slide off screen on a narrow
119
- // screen. Keyed off the empty body itself (`.tbl-empty` for a rich AppTableEmptyState,
120
- // `.tbl-empty-plain` for a plain string — both surfaces render one), lay the empty
121
- // table out at the visible wrap width and drop the scroll, so the message stays centred
122
- // and fully in view. This is the "do it without turning the scroll container into a
123
- // query container" path the pin note on `.table-wrap:has(> .table-ledger)` calls for —
124
- // it replaces the earlier `container-type` + `100cqw` pin, which collapsed the wrap as a
125
- // flex/grid item. Scoped to the empty body, so a table WITH rows is untouched and still
126
- // scrolls its columns.
127
- // Keyed on the empty CELL (`td.wui-table-empty`, the one element unique to the empty
128
- // state on both surfaces) with CHILD combinators, NOT a loose descendant `:has(.tbl-empty)`.
129
- // The child chain matches only a table's OWN empty body — so an expandable row whose
130
- // nested table happens to be empty can never force the outer, data-bearing table into
131
- // this layout. A data table (no empty cell of its own) is untouched and still scrolls.
132
- .table-wrap:has(> table > tbody > tr > td.wui-table-empty) {
133
- overflow-x: hidden;
134
- }
135
- .table:has(> tbody > tr > td.wui-table-empty) {
117
+ // The React AppDataTable renders its empty state as a SIBLING of the table, INSIDE `.table-wrap`
118
+ // (the horizontal scroller), in this `.tbl-empty-block`. Two things fall out of that placement:
119
+ // 1. the horizontal scrollbar sits at the BOTTOM of the wrap — below this message — instead of
120
+ // being wedged between the header band and the message (which is what an outside-the-wrap
121
+ // block produced), and
122
+ // 2. `position: sticky; left: 0` + `width: 100%` pins the message to the wrap's VISIBLE width and
123
+ // holds it there while a wide header scrolls behind it: `width: 100%` on a child of an
124
+ // overflow container resolves to the container's CONTENT (visible) width, not its scroll width,
125
+ // so the message never grows the scroll region and never slides off on a narrow screen.
126
+ //
127
+ // This deliberately does NOT use `container-type` + `100cqw` — the query container collapses
128
+ // `.table-wrap` to a sliver when it is a flex/grid item (see the `.table-wrap` note below). Sticky
129
+ // sidesteps that entirely. (The `.wui-table-empty` cell rules above remain for Razor, where the
130
+ // empty state is author-placed inside a `<td colSpan>`.)
131
+ .tbl-empty-block {
132
+ position: sticky;
133
+ left: 0;
134
+ // = the wrap's visible width (content box of the overflow container), never its scroll width.
136
135
  width: 100%;
137
- min-width: 0;
138
- table-layout: fixed;
139
- }
140
- // Under the fixed layout above the columns are narrow, so a long header label would
141
- // spill out of its cell and back into horizontal overflow — the very thing we just
142
- // removed. Clip each header to its column (ellipsis) in the empty state; short labels
143
- // are unaffected, and the data table keeps its natural header sizing.
144
- .table:has(> tbody > tr > td.wui-table-empty) > thead th {
145
- overflow: hidden;
146
- white-space: nowrap;
147
- text-overflow: ellipsis;
136
+ // `.tbl-empty` / `.tbl-empty-plain` inside own the centring and padding.
148
137
  }
149
138
 
150
139
  // ── Table (base) ──
@@ -300,8 +289,9 @@
300
289
  // ITEM (a very common product layout), forcing the whole table into a tiny horizontal
301
290
  // scroll. It was briefly added for a `100cqw` empty-state pin; the collapse is far worse
302
291
  // than the empty message not being perfectly centred on a narrow scrolling table, so the
303
- // pin was removed. If the empty state ever needs viewport-relative sizing again, do it
304
- // without turning the scroll container into a query container.
292
+ // pin was removed. The empty state now just sizes like a normal table (see the empty-state
293
+ // block above) — if it ever needs viewport-relative sizing again, do it without turning
294
+ // the scroll container into a query container.
305
295
  }
306
296
  .table-ledger {
307
297
  border-spacing: 0;
@@ -700,7 +700,8 @@ a.info-row:hover .info-value {
700
700
  font-size: 12.5px;
701
701
  color: var(--color-text-muted);
702
702
  }
703
- .timeline-at {
703
+ .timeline-at,
704
+ .timeline-time {
704
705
  color: var(--color-text-subtle);
705
706
  font-variant-numeric: tabular-nums;
706
707
  }
@@ -820,12 +821,30 @@ a.info-row:hover .info-value {
820
821
  justify-content: space-between;
821
822
  gap: 12px;
822
823
  }
823
- .timeline--progress .timeline-head .timeline-at {
824
+ // Date OVER time — the stamp stacks vertically so the time reads on its own line beneath the
825
+ // date. `.timeline-when` groups the two wherever a date+time stamp appears (both variants), so
826
+ // the pair moves and aligns as one unit.
827
+ .timeline-when {
828
+ display: inline-flex;
829
+ flex-direction: column;
830
+ gap: 1px;
831
+ }
832
+ // Progress: the stamp is pinned top-right, so the stack is right-aligned under the title row.
833
+ .timeline--progress .timeline-head .timeline-when {
824
834
  flex: none;
835
+ align-items: flex-end;
836
+ text-align: right;
837
+ }
838
+ .timeline--progress .timeline-head .timeline-at,
839
+ .timeline--progress .timeline-head .timeline-time {
825
840
  font-size: 12.5px;
826
841
  color: var(--color-text-subtle);
827
842
  white-space: nowrap;
828
843
  }
844
+ // The time-of-day line is a touch smaller/lighter than the date it sits under.
845
+ .timeline-time {
846
+ font-size: 12px;
847
+ }
829
848
  // An upcoming step greys down — it hasn't happened yet.
830
849
  .timeline--progress .timeline-item.is-upcoming .timeline-title {
831
850
  color: var(--color-text-muted);
@@ -0,0 +1,259 @@
1
+ // ── In-product feedback ──
2
+ // AppFeedback / <app-feedback> / <wui-feedback> — a floating trigger pinned to a page
3
+ // corner that opens a small panel with a message box. The element authors the markup and
4
+ // owns the states (`open` / `busy` on the host, `hidden` on the panel, error and thank-you
5
+ // blocks); everything visual lives here.
6
+ //
7
+ // Part of the wui-components layer — see _wui-components.scss for load order.
8
+
9
+ wui-feedback {
10
+ position: fixed;
11
+ bottom: 20px;
12
+ z-index: var(--wui-z-fixed);
13
+ display: block;
14
+ }
15
+ wui-feedback,
16
+ wui-feedback[position="bottom-right"] {
17
+ right: 20px;
18
+ left: auto;
19
+ }
20
+ wui-feedback[position="bottom-left"] {
21
+ left: 20px;
22
+ right: auto;
23
+ }
24
+
25
+ // ── Trigger ──
26
+ .feedback-trigger {
27
+ display: inline-flex;
28
+ align-items: center;
29
+ gap: 8px;
30
+ height: var(--wui-control-height-sm);
31
+ padding: 0 16px;
32
+ border: none;
33
+ border-radius: 999px;
34
+ background: var(--color-primary);
35
+ color: var(--color-primary-text);
36
+ font-size: 14px;
37
+ font-weight: 600;
38
+ line-height: 1;
39
+ cursor: pointer;
40
+ box-shadow: var(--wui-shadow-overlay);
41
+ transition:
42
+ background-color 160ms var(--wui-ease-ui),
43
+ transform 160ms var(--wui-ease-ui);
44
+ }
45
+ .feedback-trigger:hover {
46
+ background: var(--color-primary-hover);
47
+ }
48
+ .feedback-trigger:focus-visible {
49
+ outline: 2px solid var(--color-primary);
50
+ outline-offset: 2px;
51
+ }
52
+ .feedback-trigger-icon {
53
+ display: grid;
54
+ place-items: center;
55
+ line-height: 0;
56
+ }
57
+ .feedback-trigger-label {
58
+ white-space: nowrap;
59
+ }
60
+
61
+ // ── Panel ──
62
+ // Anchored to the same corner as the trigger, opening upward — the trigger sits at the
63
+ // bottom of the viewport, so there is only ever room above it.
64
+ .feedback-panel {
65
+ position: absolute;
66
+ bottom: calc(100% + 12px);
67
+ width: 340px;
68
+ max-width: calc(100vw - 40px);
69
+ padding: 16px;
70
+ background: var(--color-surface);
71
+ border: 1px solid var(--color-border);
72
+ border-radius: var(--wui-radius-lg);
73
+ box-shadow: var(--wui-shadow-overlay-lg);
74
+ color: var(--color-text);
75
+ text-align: start;
76
+ }
77
+ wui-feedback[position="bottom-right"] .feedback-panel {
78
+ right: 0;
79
+ }
80
+ wui-feedback[position="bottom-left"] .feedback-panel {
81
+ left: 0;
82
+ }
83
+ // `hidden` on a custom element's child is honoured, but a later display rule would win —
84
+ // state it explicitly so the panel cannot be left visible by cascade order.
85
+ .feedback-panel[hidden] {
86
+ display: none;
87
+ }
88
+
89
+ .feedback-head {
90
+ display: flex;
91
+ align-items: flex-start;
92
+ justify-content: space-between;
93
+ gap: 12px;
94
+ margin-bottom: 12px;
95
+ }
96
+ .feedback-heading {
97
+ margin: 0;
98
+ font-size: 15px;
99
+ font-weight: 600;
100
+ line-height: 1.3;
101
+ color: var(--color-text);
102
+ }
103
+ .feedback-close {
104
+ display: grid;
105
+ place-items: center;
106
+ width: 28px;
107
+ height: 28px;
108
+ flex: 0 0 auto;
109
+ padding: 0;
110
+ border: none;
111
+ border-radius: var(--wui-radius-sm);
112
+ background: transparent;
113
+ color: var(--color-text-muted);
114
+ cursor: pointer;
115
+ }
116
+ .feedback-close:hover {
117
+ background: var(--color-bg-muted);
118
+ color: var(--color-text);
119
+ }
120
+ .feedback-close:focus-visible {
121
+ outline: 2px solid var(--color-primary);
122
+ outline-offset: 1px;
123
+ }
124
+
125
+ .feedback-body[hidden],
126
+ .feedback-actions[hidden],
127
+ .feedback-thanks[hidden] {
128
+ display: none;
129
+ }
130
+
131
+ // ── Fields ──
132
+ .feedback-field {
133
+ display: block;
134
+ margin-bottom: 12px;
135
+ }
136
+ .feedback-field-label {
137
+ display: block;
138
+ margin-bottom: 6px;
139
+ font-size: 13px;
140
+ font-weight: 500;
141
+ color: var(--color-text-muted);
142
+ }
143
+ .feedback-message,
144
+ .feedback-email {
145
+ display: block;
146
+ width: 100%;
147
+ padding: 10px 12px;
148
+ background: var(--color-surface);
149
+ border: 1px solid var(--color-border);
150
+ border-radius: var(--wui-radius-sm);
151
+ color: var(--color-text);
152
+ font-family: inherit;
153
+ font-size: 14px;
154
+ line-height: 1.45;
155
+ }
156
+ .feedback-message {
157
+ min-height: 96px;
158
+ resize: vertical;
159
+ }
160
+ .feedback-message:focus-visible,
161
+ .feedback-email:focus-visible {
162
+ outline: none;
163
+ border-color: var(--color-primary);
164
+ box-shadow: 0 0 0 3px var(--color-primary-soft);
165
+ }
166
+ .feedback-email {
167
+ height: var(--wui-control-height-sm);
168
+ }
169
+
170
+ .feedback-error {
171
+ margin: 0 0 4px;
172
+ font-size: 13px;
173
+ line-height: 1.4;
174
+ color: var(--color-danger-text);
175
+ }
176
+ .feedback-error[hidden] {
177
+ display: none;
178
+ }
179
+
180
+ // ── Actions ──
181
+ .feedback-actions {
182
+ display: flex;
183
+ justify-content: flex-end;
184
+ gap: 8px;
185
+ }
186
+ .feedback-cancel,
187
+ .feedback-submit {
188
+ height: var(--wui-control-height-sm);
189
+ padding: 0 14px;
190
+ border-radius: var(--wui-radius-sm);
191
+ font-size: 14px;
192
+ font-weight: 600;
193
+ line-height: 1;
194
+ cursor: pointer;
195
+ }
196
+ .feedback-cancel {
197
+ border: 1px solid var(--color-border);
198
+ background: transparent;
199
+ color: var(--color-text);
200
+ }
201
+ .feedback-cancel:hover {
202
+ background: var(--color-bg-muted);
203
+ }
204
+ .feedback-submit {
205
+ border: none;
206
+ background: var(--color-primary);
207
+ color: var(--color-primary-text);
208
+ }
209
+ .feedback-submit:hover:not(:disabled) {
210
+ background: var(--color-primary-hover);
211
+ }
212
+ .feedback-submit:disabled {
213
+ opacity: 0.65;
214
+ cursor: default;
215
+ }
216
+ .feedback-cancel:focus-visible,
217
+ .feedback-submit:focus-visible {
218
+ outline: 2px solid var(--color-primary);
219
+ outline-offset: 2px;
220
+ }
221
+
222
+ // ── Thank-you ──
223
+ .feedback-thanks {
224
+ display: flex;
225
+ align-items: center;
226
+ gap: 10px;
227
+ padding: 8px 0 4px;
228
+ font-size: 14px;
229
+ color: var(--color-success-text);
230
+ }
231
+ .feedback-thanks-icon {
232
+ display: grid;
233
+ place-items: center;
234
+ line-height: 0;
235
+ }
236
+ .feedback-thanks-text {
237
+ font-weight: 500;
238
+ line-height: 1.4;
239
+ }
240
+
241
+ // ── Small screens ──
242
+ // The panel is nearly the width of the phone, so pin it to both edges rather than to the
243
+ // trigger's corner — a 340px card anchored to one side overhangs on the narrowest devices.
244
+ @include media-breakpoint-down(sm) {
245
+ .feedback-panel {
246
+ position: fixed;
247
+ right: 12px;
248
+ left: 12px;
249
+ bottom: calc(var(--wui-control-height-sm) + 32px);
250
+ width: auto;
251
+ max-width: none;
252
+ }
253
+ }
254
+
255
+ @media (prefers-reduced-motion: reduce) {
256
+ .feedback-trigger {
257
+ transition: none;
258
+ }
259
+ }
@@ -127,6 +127,11 @@
127
127
 
128
128
  // Optional overflow wrapper: the scroll chevrons live HERE, outside the scrolling strip,
129
129
  // so they stay pinned while the tabs scroll under them.
130
+ // A little horizontal padding insets the strip from its container's edges, so the first/last
131
+ // tab — and its full-width active underline — never lands on a rounded container's corner (a
132
+ // tab strip placed flush in a card read as the underline hanging outside it). Horizontal only:
133
+ // both surfaces render this wrapper for horizontal strips and skip it for vertical, so the two
134
+ // stay identical.
130
135
  .app-tabs-wrap {
131
136
  position: relative;
132
137
  display: flex;
@@ -146,10 +151,12 @@
146
151
  // Auto-scrolling the active tab into view (and the chevrons) glides rather than jumps.
147
152
  scroll-behavior: smooth;
148
153
  }
149
- // Doubled to (0,2,0) so the strip keeps scrolling even if a consumer loads Bootstrap
150
- // after the theme, which would otherwise let `.nav`'s flex-wrap:wrap win on order.
154
+ // Doubled to (0,2,0) so the strip keeps scrolling AND keeps its gutters even if a consumer
155
+ // loads Bootstrap after the theme, which would otherwise let `.nav`'s flex-wrap:wrap and
156
+ // padding-left:0 win on source order.
151
157
  .app-tabs.app-tabs {
152
158
  flex-wrap: nowrap;
159
+ padding: 0 12px;
153
160
  }
154
161
  .app-tabs::-webkit-scrollbar {
155
162
  display: none;
@@ -164,12 +164,11 @@
164
164
  align-items: center;
165
165
  justify-content: space-between;
166
166
  gap: 16px;
167
- // Content-driven wrap: while the title block and actions both fit, space-between keeps the
168
- // actions pinned right; once a long title needs the full width they can no longer share the
169
- // row, so the actions drop onto their own line — left-aligned, since space-between places a
170
- // lone wrapped item at flex-start — instead of being crushed against the title. The gap above
171
- // doubles as the row-gap to that dropped line. (Below lg they stack unconditionally, further
172
- // down; this is the any-width, content-driven case.)
167
+ // Content-driven wrap at EVERY width — there is no breakpoint stack. While the title block
168
+ // and actions both fit, space-between keeps the actions pinned right; once a long title needs
169
+ // the full width they can no longer share the row, so the actions drop onto their own line —
170
+ // left-aligned, since space-between places a lone wrapped item at flex-start — instead of
171
+ // being crushed against the title. The gap above doubles as the row-gap to that dropped line.
173
172
  flex-wrap: wrap;
174
173
  }
175
174
  // Left block: an optional leading icon tile + the title stack, kept together.
@@ -188,8 +187,8 @@
188
187
  flex: none;
189
188
  display: inline-grid;
190
189
  place-items: center;
191
- width: 40px;
192
- height: 40px;
190
+ width: 55px;
191
+ height: 55px;
193
192
  border-radius: var(--radius);
194
193
  corner-shape: squircle;
195
194
  background: var(--color-primary-soft);
@@ -262,6 +261,14 @@
262
261
  display: flex;
263
262
  gap: 8px;
264
263
  align-items: center;
264
+ // Several action buttons wrap among themselves if their own line still can't hold them all
265
+ // — but they never force the block full-width, so a single button stays its natural size.
266
+ flex-wrap: wrap;
267
+ // Don't let the actions be squeezed. The title block (`.page-header-lead`, min-width: 0)
268
+ // absorbs the shrink instead, so the actions keep their natural size and the row stays on
269
+ // one line while it fits — then `.page-header-bar`'s flex-wrap drops the WHOLE actions
270
+ // block onto its own line only once the content genuinely can't share the row.
271
+ flex: none;
265
272
  }
266
273
 
267
274
  // ── Size scale ── (`size`): the title and the block's rhythm scale together.
@@ -272,8 +279,8 @@
272
279
  font-size: 20px;
273
280
  }
274
281
  .page-header-sm .page-header-icon {
275
- width: 34px;
276
- height: 34px;
282
+ width: 49px;
283
+ height: 49px;
277
284
  }
278
285
  .page-header-lg {
279
286
  margin-bottom: 32px;
@@ -282,8 +289,8 @@
282
289
  font-size: 30px;
283
290
  }
284
291
  .page-header-lg .page-header-icon {
285
- width: 48px;
286
- height: 48px;
292
+ width: 63px;
293
+ height: 63px;
287
294
  }
288
295
 
289
296
  // ── Centre alignment ── (`align="center"`): for empty-state and wizard mastheads.
@@ -318,21 +325,18 @@
318
325
  // its actions stack, and the actions stretch to the full width rather than
319
326
  // crowding the title. Bootstrap's lg tier (991.98px), not an invented breakpoint.
320
327
  //
321
- // Selectors are DOUBLED (`.page-header-bar.page-header-bar`) on purpose. A media
322
- // query adds no specificity, so a consumer's own single-class rule — written later
323
- // in the cascade, as a consuming app's stylesheet always is — silently beats the
324
- // responsive rule and the layout never collapses. Doubling puts these at (0,2,0)
325
- // so they hold, while still leaving a consumer free to override deliberately.
328
+ // The masthead bar is NOT stacked at a breakpoint. It wraps by CONTENT: `.page-header-bar`
329
+ // carries `flex-wrap: wrap` and its actions `flex: none`, so the title and actions share one
330
+ // row wherever they fit and the actions drop below only when the row genuinely can't hold both
331
+ // — a hard `max-width: 991.98px` column-stack pushed the actions under the title even on a
332
+ // 768px tablet that had ample room for them side by side.
333
+ //
334
+ // Selectors below are DOUBLED (`.field-row.field-row`) on purpose. A media query adds no
335
+ // specificity, so a consumer's own single-class rule — written later in the cascade, as a
336
+ // consuming app's stylesheet always is — silently beats the responsive rule and the layout
337
+ // never collapses. Doubling puts these at (0,2,0) so they hold, while still leaving a consumer
338
+ // free to override deliberately.
326
339
  @media (max-width: 991.98px) {
327
- .page-header-bar.page-header-bar {
328
- flex-direction: column;
329
- align-items: stretch;
330
- gap: 12px;
331
- }
332
- .page-actions.page-actions {
333
- flex-wrap: wrap;
334
- width: 100%;
335
- }
336
340
  // A two-up field row is too tight for a tablet column.
337
341
  .field-row.field-row,
338
342
  .field-row-3.field-row-3 {
@@ -422,6 +426,62 @@
422
426
  display: inline-block;
423
427
  }
424
428
 
429
+ // Password-requirements checklist — the content of the popover AppInput (type="password" +
430
+ // passwordRequirements) opens on focus. Rendered identically by React (a JSX <ul>) and Razor
431
+ // (the same <ul> built in webority-ui.js), so both surfaces read the same. `.pw-popover` is the
432
+ // custom class both put on Bootstrap's `.popover` panel.
433
+ .pw-popover .popover-body {
434
+ padding: 12px 14px;
435
+ }
436
+ .pw-criteria {
437
+ list-style: none;
438
+ margin: 0;
439
+ padding: 0;
440
+ display: flex;
441
+ flex-direction: column;
442
+ gap: 8px;
443
+ }
444
+ .pw-criteria .pw-rule {
445
+ display: flex;
446
+ align-items: center;
447
+ gap: 8px;
448
+ font-size: 13px;
449
+ line-height: 1.3;
450
+ }
451
+ // The state mark is a baked-colour circled ✓/✕ — matched to the AA status ramp. The list TEXT
452
+ // uses the -text token for contrast; the mark itself needs less and uses the vivid ramp value.
453
+ .pw-criteria .pw-mark {
454
+ flex: none;
455
+ width: 18px;
456
+ height: 18px;
457
+ background: center / contain no-repeat;
458
+ }
459
+ .pw-criteria .pw-rule-text {
460
+ min-width: 0;
461
+ }
462
+ .pw-criteria .pw-rule.met {
463
+ color: var(--color-success-text);
464
+ }
465
+ .pw-criteria .pw-rule.met .pw-mark {
466
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2317C653' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='m8 12 2.5 2.5 5.5-6'/%3E%3C/svg%3E");
467
+ }
468
+ .pw-criteria .pw-rule.unmet {
469
+ color: var(--color-danger-text);
470
+ }
471
+ .pw-criteria .pw-rule.unmet .pw-mark {
472
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23F8285A' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='m9 9 6 6m0-6-6 6'/%3E%3C/svg%3E");
473
+ }
474
+ // The checklist opens with a PLAIN FADE only — no drop / zoom / scale. It opts out of the shared
475
+ // menu-open motion (_menu-motion.scss animates every `.popover.show`) and lets Bootstrap's own
476
+ // `.fade` opacity transition carry the fade in and out. Both surfaces render `.popover.pw-popover`
477
+ // (React via react-bootstrap's Popover, Razor via Bootstrap's popover), so this makes the two
478
+ // identical. Higher specificity than `.popover.show` so it wins regardless of partial load order.
479
+ .popover.pw-popover.show {
480
+ animation: none;
481
+ scale: none;
482
+ translate: none;
483
+ }
484
+
425
485
  // Page/system-level announcement bar (AppBanner / <app-banner>). Full-width and persistent —
426
486
  // distinct from an inline .alert or a transient toast. Tones reuse the shared soft/text ramp.
427
487
  .wui-banner {