@webority/theme 0.7.3 → 0.7.5

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,70 +1,8 @@
1
1
  // =====================================================================
2
- // Webority UI — shared custom component CSS.
3
- // Imported AFTER Bootstrap. Anything that needs to render identically
4
- // on the Razor marketing site AND the React customer portal lives here.
5
- // Per-project styling (page layouts, route-specific tweaks) stays in
6
- // the consuming project, NOT in this shared theme.
2
+ // Webority UI — shared portal / control CSS (AFTER Bootstrap).
3
+ // Marketing section/hero/motion helpers: scss/_marketing.scss (opt-in).
7
4
  // =====================================================================
8
5
 
9
-
10
- // ---- Section helpers ------------------------------------------------
11
- // Marketing + portal both use vertically-padded sections with the
12
- // brand's spacing rhythm. Bootstrap's py-* gets you part-way; these
13
- // are the named variants the design system reaches for.
14
- .section-y-sm { padding-block: 3rem; }
15
- .section-y { padding-block: 5rem; }
16
- .section-y-lg { padding-block: 7rem; }
17
- @media (min-width: 768px) {
18
- .section-y-sm { padding-block: 4rem; }
19
- .section-y { padding-block: 6rem; }
20
- .section-y-lg { padding-block: 9rem; }
21
- }
22
-
23
-
24
- // ---- Eyebrow (accent-dot + label) -----------------------------------
25
- .eyebrow {
26
- display: inline-flex;
27
- align-items: center;
28
- gap: 0.5rem;
29
- font-size: 14px;
30
- font-weight: 700;
31
- line-height: 1;
32
- color: var(--wui-primary);
33
- }
34
- .eyebrow::before {
35
- content: "";
36
- width: 0.5rem;
37
- height: 0.5rem;
38
- border-radius: 9999px;
39
- background: var(--wui-primary);
40
- }
41
- .eyebrow--on-dark {
42
- color: rgba(255, 255, 255, 0.85);
43
- }
44
- .eyebrow--on-dark::before {
45
- background: #FFFFFF;
46
- }
47
-
48
-
49
- // ---- Display heading -------------------------------------------------
50
- // Bootstrap's .display-1..6 ladder skews bigger than our locked design.
51
- // .h-display drops in where the marketing hero needs the locked
52
- // font-weight 500 + tight letter-spacing. font-family reads --wui-font-display
53
- // so a product can swap in a serif/brand display face by overriding just that
54
- // token (default: the same body stack .font-display already uses).
55
- .h-display {
56
- font-family: var(--wui-font-display, var(--bs-body-font-family));
57
- font-weight: 500;
58
- letter-spacing: -0.03em;
59
- line-height: 1.05;
60
- }
61
-
62
-
63
- // ---- Gradient backgrounds -------------------------------------------
64
- .bg-hero-gradient { background: var(--wui-hero-gradient); }
65
- .bg-cta-gradient { background: var(--wui-cta-gradient); }
66
-
67
-
68
6
  // ---- Button polish (used by both Razor + React) ---------------------
69
7
  // Flat buttons — no drop shadow. Active-press feedback (1px nudge) is
70
8
  // retained because it's a tactile signal, not a depth cue.
@@ -88,18 +26,18 @@
88
26
  .btn-primary {
89
27
  // Pin hover/active to the pressed-primary token so a rebrand tracks
90
28
  // automatically instead of Bootstrap auto-shading $primary off-spec.
91
- --bs-btn-hover-bg: var(--wui-primary-pressed);
92
- --bs-btn-hover-border-color: var(--wui-primary-pressed);
93
- --bs-btn-active-bg: var(--wui-primary-pressed);
29
+ --bs-btn-hover-bg: var(--wui-primary-pressed);
30
+ --bs-btn-hover-border-color: var(--wui-primary-pressed);
31
+ --bs-btn-active-bg: var(--wui-primary-pressed);
94
32
  --bs-btn-active-border-color: var(--wui-primary-pressed);
95
33
  // Bootstrap computes this label colour at SCSS compile time from the library's
96
34
  // default $primary, so it can never react to a product overriding --wui-primary as a
97
35
  // runtime CSS var. Routing it through the token makes a light-primary brand's
98
36
  // --wui-primary-text override reach the button.
99
- --bs-btn-color: var(--wui-primary-text);
100
- --bs-btn-hover-color: var(--wui-primary-text);
101
- --bs-btn-active-color: var(--wui-primary-text);
102
- --bs-btn-disabled-color: var(--wui-primary-text);
37
+ --bs-btn-color: var(--wui-primary-text);
38
+ --bs-btn-hover-color: var(--wui-primary-text);
39
+ --bs-btn-active-color: var(--wui-primary-text);
40
+ --bs-btn-disabled-color: var(--wui-primary-text);
103
41
  }
104
42
 
105
43
  .btn-danger {
@@ -107,17 +45,17 @@
107
45
  // brighter danger fills (#F8285A → 3.86:1, #E82646 → 4.38:1), so the button uses the
108
46
  // deepened fill (5.80:1) that carries white cleanly. Token-driven so a rebrand tracks it;
109
47
  // the brighter --wui-danger stays for badges/dots that never carry white text.
110
- --bs-btn-color: #fff;
111
- --bs-btn-bg: var(--wui-danger-strong);
112
- --bs-btn-border-color: var(--wui-danger-strong);
113
- --bs-btn-hover-color: #fff;
114
- --bs-btn-hover-bg: var(--wui-danger-strong-hover);
115
- --bs-btn-hover-border-color: var(--wui-danger-strong-hover);
116
- --bs-btn-active-color: #fff;
117
- --bs-btn-active-bg: var(--wui-danger-strong-hover);
118
- --bs-btn-active-border-color: var(--wui-danger-strong-hover);
119
- --bs-btn-disabled-color: #fff;
120
- --bs-btn-disabled-bg: var(--wui-danger-strong);
48
+ --bs-btn-color: #fff;
49
+ --bs-btn-bg: var(--wui-danger-strong);
50
+ --bs-btn-border-color: var(--wui-danger-strong);
51
+ --bs-btn-hover-color: #fff;
52
+ --bs-btn-hover-bg: var(--wui-danger-strong-hover);
53
+ --bs-btn-hover-border-color: var(--wui-danger-strong-hover);
54
+ --bs-btn-active-color: #fff;
55
+ --bs-btn-active-bg: var(--wui-danger-strong-hover);
56
+ --bs-btn-active-border-color: var(--wui-danger-strong-hover);
57
+ --bs-btn-disabled-color: #fff;
58
+ --bs-btn-disabled-bg: var(--wui-danger-strong);
121
59
  --bs-btn-disabled-border-color: var(--wui-danger-strong);
122
60
  }
123
61
 
@@ -126,34 +64,65 @@
126
64
  // on a transparent fill; hover fills with the brand and flips the label to the on-brand text
127
65
  // token; active deepens to the pressed fill.
128
66
  .btn-outline-primary {
129
- --bs-btn-color: var(--color-primary);
130
- --bs-btn-border-color: var(--color-primary);
131
- --bs-btn-hover-color: var(--wui-primary-text);
132
- --bs-btn-hover-bg: var(--color-primary);
133
- --bs-btn-hover-border-color: var(--color-primary);
134
- --bs-btn-active-color: var(--wui-primary-text);
135
- --bs-btn-active-bg: var(--wui-primary-pressed);
136
- --bs-btn-active-border-color: var(--wui-primary-pressed);
137
- --bs-btn-disabled-color: var(--color-primary);
67
+ --bs-btn-color: var(--color-primary);
68
+ --bs-btn-border-color: var(--color-primary);
69
+ --bs-btn-hover-color: var(--wui-primary-text);
70
+ --bs-btn-hover-bg: var(--color-primary);
71
+ --bs-btn-hover-border-color: var(--color-primary);
72
+ --bs-btn-active-color: var(--wui-primary-text);
73
+ --bs-btn-active-bg: var(--wui-primary-pressed);
74
+ --bs-btn-active-border-color: var(--wui-primary-pressed);
75
+ --bs-btn-disabled-color: var(--color-primary);
138
76
  --bs-btn-disabled-border-color: var(--color-primary);
139
77
  }
140
78
 
141
79
  // Outline danger uses the deepened --wui-danger-strong (5.80:1, carries white) that solid
142
80
  // .btn-danger uses, so an outline-danger hover reads identically to solid danger.
143
81
  .btn-outline-danger {
144
- --bs-btn-color: var(--wui-danger-strong);
145
- --bs-btn-border-color: var(--wui-danger-strong);
146
- --bs-btn-hover-color: #fff;
147
- --bs-btn-hover-bg: var(--wui-danger-strong);
148
- --bs-btn-hover-border-color: var(--wui-danger-strong);
149
- --bs-btn-active-color: #fff;
150
- --bs-btn-active-bg: var(--wui-danger-strong-hover);
151
- --bs-btn-active-border-color: var(--wui-danger-strong-hover);
152
- --bs-btn-disabled-color: var(--wui-danger-strong);
82
+ --bs-btn-color: var(--wui-danger-strong);
83
+ --bs-btn-border-color: var(--wui-danger-strong);
84
+ --bs-btn-hover-color: #fff;
85
+ --bs-btn-hover-bg: var(--wui-danger-strong);
86
+ --bs-btn-hover-border-color: var(--wui-danger-strong);
87
+ --bs-btn-active-color: #fff;
88
+ --bs-btn-active-bg: var(--wui-danger-strong-hover);
89
+ --bs-btn-active-border-color: var(--wui-danger-strong-hover);
90
+ --bs-btn-disabled-color: var(--wui-danger-strong);
153
91
  --bs-btn-disabled-border-color: var(--wui-danger-strong);
154
92
  }
155
93
 
156
- // Themed disabled state (fleet default). Bootstrap's own disabled look is just the
94
+ // Soft neutral tinted button — a quieter alternative to .btn-light for a secondary
95
+ // action that shouldn't compete with a primary/danger CTA in the same row.
96
+ .btn-subtle {
97
+ --bs-btn-bg: var(--color-bg-muted);
98
+ --bs-btn-color: var(--color-text);
99
+ --bs-btn-border-color: transparent;
100
+ --bs-btn-hover-bg: var(--color-border);
101
+ --bs-btn-hover-color: var(--color-text);
102
+ --bs-btn-hover-border-color: transparent;
103
+ --bs-btn-active-bg: var(--color-border-strong);
104
+ --bs-btn-active-color: var(--color-text);
105
+ --bs-btn-active-border-color: transparent;
106
+ }
107
+
108
+ // Light outline for a dark banner/hero CTA, where .btn-outline-primary's brand-coloured
109
+ // border/text would disappear against a dark background. White is used directly (not a
110
+ // token) because it's the one colour that reads on any dark surface, brand-independent.
111
+ .btn-outline-light {
112
+ --bs-btn-color: #fff;
113
+ --bs-btn-bg: transparent;
114
+ --bs-btn-border-color: rgba(255, 255, 255, 0.6);
115
+ --bs-btn-hover-color: #fff;
116
+ --bs-btn-hover-bg: rgba(255, 255, 255, 0.15);
117
+ --bs-btn-hover-border-color: #fff;
118
+ --bs-btn-active-color: #fff;
119
+ --bs-btn-active-bg: rgba(255, 255, 255, 0.25);
120
+ --bs-btn-active-border-color: #fff;
121
+ --bs-btn-disabled-color: rgba(255, 255, 255, 0.5);
122
+ --bs-btn-disabled-border-color: rgba(255, 255, 255, 0.3);
123
+ }
124
+
125
+ // Themed disabled state (default). Bootstrap's own disabled look is just the
157
126
  // variant's colour at --bs-btn-disabled-opacity (.65) — barely different from the active
158
127
  // button on a tinted brand palette. Give the ordinary case a real desaturated/muted look
159
128
  // instead, reusing the same tokens .input:disabled already uses so a disabled button and
@@ -171,7 +140,6 @@
171
140
  cursor: not-allowed;
172
141
  }
173
142
 
174
-
175
143
  // ---- Component radii from tokens ------------------------------------
176
144
  // .btn/.card/.input capture Bootstrap's compiled $*-border-radius (the library
177
145
  // ladder) before a product loads, so a product's --wui-radius* override never
@@ -180,9 +148,8 @@
180
148
  // sm/lg are re-asserted so they keep their own rung instead of the base value.
181
149
  .btn {
182
150
  --bs-btn-border-radius: var(--wui-radius);
183
- // Smooth iOS-style corners so buttons match the squircle geometry every other
184
- // control in the system already uses (inputs, selects, pills). Older browsers
185
- // fall back to the plain border-radius arc — no breakage.
151
+ // Progressive enhancement only (Chrome/Safari recent). Unsupported engines
152
+ // keep a plain border-radius arc — not a cross-browser design contract.
186
153
  corner-shape: squircle;
187
154
  }
188
155
  .btn-sm,
@@ -194,7 +161,6 @@
194
161
  --bs-btn-border-radius: var(--wui-radius-lg);
195
162
  }
196
163
 
197
-
198
164
  // ---- Dropdown menu (AppDropdown / <app-dropdown>) -------------------
199
165
  // Bootstrap ships a bare .dropdown-item; the design system gives the menu real
200
166
  // padding, an icon–label gap, a rounded hover highlight tinted with the brand,
@@ -203,19 +169,19 @@
203
169
  // Elevated surface (like .card), not Bootstrap's default page --bs-body-bg: otherwise the
204
170
  // menu blends into a tinted page and, in dark mode, resolves to the page colour instead of
205
171
  // the elevated surface.
206
- --bs-dropdown-bg: var(--color-surface);
207
- --bs-dropdown-min-width: 12rem;
208
- --bs-dropdown-padding-x: 0.375rem;
209
- --bs-dropdown-padding-y: 0.375rem;
210
- --bs-dropdown-item-padding-y: 0.5rem;
211
- --bs-dropdown-item-padding-x: 0.625rem;
212
- --bs-dropdown-border-color: var(--wui-border-light);
213
- --bs-dropdown-border-radius: var(--wui-radius-lg);
214
- --bs-dropdown-link-color: var(--wui-text-1);
215
- --bs-dropdown-link-hover-color: var(--wui-primary-pressed);
216
- --bs-dropdown-link-hover-bg: var(--wui-primary-5);
172
+ --bs-dropdown-bg: var(--color-surface);
173
+ --bs-dropdown-min-width: 12rem;
174
+ --bs-dropdown-padding-x: 0.375rem;
175
+ --bs-dropdown-padding-y: 0.375rem;
176
+ --bs-dropdown-item-padding-y: 0.5rem;
177
+ --bs-dropdown-item-padding-x: 0.625rem;
178
+ --bs-dropdown-border-color: var(--wui-border-light);
179
+ --bs-dropdown-border-radius: var(--wui-radius-lg);
180
+ --bs-dropdown-link-color: var(--wui-text-1);
181
+ --bs-dropdown-link-hover-color: var(--wui-primary-pressed);
182
+ --bs-dropdown-link-hover-bg: var(--wui-primary-5);
217
183
  --bs-dropdown-link-active-color: var(--wui-primary-pressed);
218
- --bs-dropdown-link-active-bg: var(--wui-primary-10);
184
+ --bs-dropdown-link-active-bg: var(--wui-primary-10);
219
185
  box-shadow: var(--wui-shadow-overlay);
220
186
  }
221
187
 
@@ -253,87 +219,15 @@
253
219
 
254
220
  // Destructive row action — red label, red glyph, red-tinted hover.
255
221
  &.danger {
256
- --bs-dropdown-link-color: var(--wui-danger-text);
222
+ --bs-dropdown-link-color: var(--wui-danger-text);
257
223
  --bs-dropdown-link-hover-color: var(--wui-danger-text);
258
- --bs-dropdown-link-hover-bg: var(--wui-danger-10);
224
+ --bs-dropdown-link-hover-bg: var(--wui-danger-10);
259
225
  .bi, i, svg { color: var(--wui-danger); }
260
226
  &:hover .bi, &:hover i, &:hover svg,
261
227
  &:focus .bi, &:focus i, &:focus svg { color: var(--wui-danger); }
262
228
  }
263
229
  }
264
230
 
265
-
266
- // ---- Marketing pill buttons (locked design parity) -------------------
267
- // Pixel-locked to the standalone reference's `btn-squircle inline-flex
268
- // h-12 items-center text-sm font-medium` pattern. Each marketing CTA
269
- // layers .btn-pill on a Bootstrap variant — these classes only override
270
- // Bootstrap's btn CSS vars (height / padding / radius / font), so
271
- // every variant inherits the right size without redefining colors.
272
- //
273
- // Reference values:
274
- // - height : 48px (h-12 = 3rem)
275
- // - radius : 16px (--wui-radius-xl)
276
- // - font : 14px / 500 (text-sm font-medium)
277
- // - padding-x : 20px (px-5) default — Watch demo / outline-dark
278
- // Primary CTA overrides to 24px (px-6) inline
279
- // - corner-shape : squircle (iOS-style smooth curve)
280
- //
281
- // Example:
282
- // <a class="btn btn-primary btn-pill" style="--bs-btn-padding-x:1.5rem;">…</a>
283
- // <a class="btn btn-light btn-pill-sm">Learn more</a>
284
- .btn-pill {
285
- --bs-btn-padding-y: 0; // height driven by min-height + flex centering
286
- --bs-btn-padding-x: 1.25rem; // 20px (px-5)
287
- --bs-btn-font-size: 0.875rem; // 14px
288
- --bs-btn-font-weight: 500;
289
- --bs-btn-line-height: 1.25;
290
- --bs-btn-border-radius: var(--wui-radius-xl); // 16px
291
- min-height: 3rem; // 48px (h-12)
292
-
293
-
294
- display: inline-flex;
295
- align-items: center;
296
- gap: 0.375rem; // 6px — matches primary arrow's ml-1.5
297
- }
298
- .btn-pill-sm {
299
- --bs-btn-padding-y: 0;
300
- --bs-btn-padding-x: 0.875rem; // 14px (px-3.5)
301
- --bs-btn-font-size: 0.875rem; // 14px (text-sm)
302
- --bs-btn-font-weight: 500;
303
- --bs-btn-line-height: 1.25;
304
- --bs-btn-border-radius: 12px;
305
- min-height: 2.5rem; // 40px (h-10) — reference "Learn more" CTA
306
-
307
-
308
- display: inline-flex;
309
- align-items: center;
310
- gap: 0.375rem;
311
- }
312
-
313
-
314
- // ---- Ghost-dark button (frosted glass on light surface) -------------
315
- // Pixel-locked to the reference's `WatchDemoButton` "outline-dark" variant
316
- // (src/components/marketing/brand/watch-demo-button.tsx):
317
- // - border : rgba(13, 24, 43, 0.15) (foreground/15 in marketing-root)
318
- // - bg : rgba(255, 255, 255, 0.6) (surface/60)
319
- // - text : --wui-text-heading (text-foreground in marketing-root)
320
- // - blur : 4px (backdrop-blur-sm)
321
- // - hover bg: #ffffff (hover:bg-surface = full opacity)
322
- .btn-ghost-dark {
323
- --bs-btn-color: var(--wui-text-heading);
324
- --bs-btn-bg: rgba(255, 255, 255, 0.60);
325
- --bs-btn-border-color: rgba(13, 24, 43, 0.15);
326
- --bs-btn-hover-color: var(--wui-text-heading);
327
- --bs-btn-hover-bg: #ffffff;
328
- --bs-btn-hover-border-color: rgba(13, 24, 43, 0.20);
329
- --bs-btn-active-color: var(--wui-text-heading);
330
- --bs-btn-active-bg: #ffffff;
331
- --bs-btn-active-border-color: rgba(13, 24, 43, 0.20);
332
- backdrop-filter: blur(4px);
333
- -webkit-backdrop-filter: blur(4px);
334
- }
335
-
336
-
337
231
  // ---- Ghost button (transparent, brand-tinted hover) ------------------
338
232
  // A real ghost button: transparent at rest (no fill, no border), current text colour;
339
233
  // only the background lights up — a soft brand tint, the same recipe as a
@@ -346,23 +240,21 @@
346
240
  // is an ordinary .btn variant, so it sizes the same way .btn-primary/.btn-danger do —
347
241
  // Bootstrap's own .btn-sm/.btn-lg.
348
242
  .btn-ghost {
349
- --bs-btn-color: var(--color-text);
350
- --bs-btn-bg: transparent;
351
- --bs-btn-border-color: transparent;
352
- --bs-btn-hover-color: var(--color-text);
353
- --bs-btn-hover-bg: var(--wui-primary-5);
354
- --bs-btn-hover-border-color: transparent;
355
- --bs-btn-active-color: var(--color-text);
356
- --bs-btn-active-bg: var(--wui-primary-10);
243
+ --bs-btn-color: var(--color-text);
244
+ --bs-btn-bg: transparent;
245
+ --bs-btn-border-color: transparent;
246
+ --bs-btn-hover-color: var(--color-text);
247
+ --bs-btn-hover-bg: var(--wui-primary-5);
248
+ --bs-btn-hover-border-color: transparent;
249
+ --bs-btn-active-color: var(--color-text);
250
+ --bs-btn-active-bg: var(--wui-primary-10);
357
251
  --bs-btn-active-border-color: transparent;
358
252
  }
359
253
 
360
-
361
- // ---- Card "lifted" (shadowless) -------------------------------------
362
- // Previously a soft drop-shadow card; now flat. The `.card-lifted`
363
- // class is preserved so existing call sites don't break — it now
364
- // expresses elevation only via the 1px hover translate (kept as a
365
- // tactile signal, not a depth cue). No box-shadow, here or on hover.
254
+ // ---- Card hover-lift (name is historical) -----------------------------
255
+ // `.card-lifted` does NOT cast a shadow — in-page cards stay flat. It only
256
+ // nudges 1px on hover (tactile signal). API prop stays `lifted` / class
257
+ // `card-lifted` so call sites don't break.
366
258
  .card-lifted {
367
259
  transition: transform 0.2s var(--wui-ease-out-quart);
368
260
  &:hover {
@@ -370,7 +262,6 @@
370
262
  }
371
263
  }
372
264
 
373
-
374
265
  // ---- Card surface + radius (token-driven) ---------------------------
375
266
  // Radius follows the token ladder (see component-radii note) instead of the
376
267
  // compiled $card-border-radius. Background is an ELEVATED surface, not the page
@@ -396,53 +287,10 @@
396
287
  margin-left: auto;
397
288
  }
398
289
 
399
-
400
290
  // ---- Inputs ---------------------------------------------------------
401
- // 5 locked states — default / hover / focus / disabled / error —
402
- // matching the reference `<Input>` component. Bootstrap drives the base
403
- // look via $input-* vars in _variables.scss; these blocks add the
404
- // hover-border-darken cue and the leading/trailing-slot wrapper.
405
- //
406
- // The error state is driven by `aria-invalid="true"` (NOT by Bootstrap's
407
- // .is-invalid class) so accessibility tooling and visual cue stay in
408
- // sync without extra markup.
409
-
410
- .form-control,
411
- .form-select {
412
- // Radius follows the token ladder (see component-radii note) rather than the
413
- // compiled $input-border-radius, so a product's --wui-radius override reaches
414
- // the raw Bootstrap inputs the way it already reaches the .input primitive.
415
- border-radius: var(--wui-radius);
416
- transition: border-color 0.15s ease;
417
- }
418
- .form-control:hover:not(:focus):not(:disabled):not([readonly]):not([aria-invalid="true"]),
419
- .form-select:hover:not(:focus):not(:disabled):not([aria-invalid="true"]) {
420
- border-color: var(--color-border-strong); // --wui-text-3 @ 60%
421
- }
422
- .form-control[aria-invalid="true"],
423
- .form-select[aria-invalid="true"] {
424
- border-color: var(--bs-danger);
425
- }
426
- .form-control[aria-invalid="true"]:focus,
427
- .form-select[aria-invalid="true"]:focus {
428
- border-color: var(--bs-danger);
429
- box-shadow: none;
430
- }
431
-
432
- // Optional helper class for forms that prefer the explicit Bootstrap
433
- // validation pattern. Mirrors aria-invalid styling.
434
- .form-control.is-invalid,
435
- .form-select.is-invalid {
436
- border-color: var(--bs-danger);
437
- background-image: none; // kill Bootstrap's exclamation-mark SVG
438
- padding-right: 0.875rem;
439
- }
440
- .form-control.is-invalid:focus,
441
- .form-select.is-invalid:focus {
442
- border-color: var(--bs-danger);
443
- box-shadow: none;
444
- }
445
-
291
+ // Field chrome (height, hover, focus, invalid) lives in wui/_controls.scss on
292
+ // `.form-control` / `.form-select` with `.input` as an alias — one skin, not two
293
+ // This partial only keeps the marketing-side input-group shell.
446
294
 
447
295
  // ---- Input group (leading / trailing slots) ------------------------
448
296
  // Single outlined box that contains leading icon + input + trailing
@@ -450,47 +298,46 @@
450
298
  // / error states are visible regardless of which child has focus.
451
299
  //
452
300
  // Usage:
453
- // <div class="wui-input-group">
454
- // <span class="wui-input-leading"><i class="bi bi-currency-dollar"></i></span>
455
- // <input type="text" class="form-control" placeholder="0.00" />
456
- // </div>
301
+ // <div class="wui-input-group">
302
+ // <span class="wui-input-leading"><i class="bi bi-currency-dollar"></i></span>
303
+ // <input type="text" class="form-control" placeholder="0.00" />
304
+ // </div>
457
305
  //
458
- // <div class="wui-input-group">
459
- // <input type="password" class="form-control" placeholder="Password" />
460
- // <button type="button" class="wui-input-trailing"><i class="bi bi-eye"></i></button>
461
- // </div>
306
+ // <div class="wui-input-group">
307
+ // <input type="password" class="form-control" placeholder="Password" />
308
+ // <button type="button" class="wui-input-trailing"><i class="bi bi-eye"></i></button>
309
+ // </div>
462
310
  .wui-input-group {
463
311
  display: flex;
464
312
  align-items: center;
465
313
  gap: 0.5rem;
466
314
  padding-inline: 0.875rem;
467
- height: calc(2.5rem + 2px); // 40px box + 1px border each side
468
- background: var(--wui-bg-light, #FFFFFF);
469
- border: 1px solid var(--bs-border-color);
470
- border-radius: var(--radius); // matches the bare input radius
315
+ height: var(--wui-control-height);
316
+ background: var(--color-surface);
317
+ border: 1px solid var(--color-border);
318
+ border-radius: var(--radius);
471
319
  transition: border-color 0.15s ease;
472
-
473
-
474
320
  }
475
321
  .wui-input-group:hover:not(:focus-within):not(.is-disabled) {
476
- border-color: rgba(120, 130, 157, 0.6);
322
+ border-color: var(--color-border-strong);
477
323
  }
478
324
  .wui-input-group:focus-within {
479
- border-color: var(--wui-primary);
325
+ border-color: var(--color-primary);
480
326
  }
481
327
  .wui-input-group.is-disabled,
482
328
  .wui-input-group:has(input:disabled) {
483
- background: var(--wui-bg-muted);
329
+ background: var(--color-bg-muted);
484
330
  cursor: not-allowed;
485
- border-color: var(--bs-border-color);
331
+ border-color: var(--color-border);
486
332
  }
487
333
  .wui-input-group.has-error,
488
334
  .wui-input-group:has(input[aria-invalid="true"]) {
489
- border-color: var(--bs-danger);
335
+ border-color: color-mix(in srgb, var(--color-danger) 45%, var(--color-surface));
490
336
  }
491
337
 
492
338
  // Strip the bare input's own chrome — the wrapper carries it all.
493
- .wui-input-group > .form-control {
339
+ .wui-input-group > .form-control,
340
+ .wui-input-group > .input {
494
341
  flex: 1 1 auto;
495
342
  min-width: 0;
496
343
  height: 100%;
@@ -502,7 +349,9 @@
502
349
  border-radius: 0;
503
350
  }
504
351
  .wui-input-group > .form-control:focus,
505
- .wui-input-group > .form-control:hover {
352
+ .wui-input-group > .form-control:hover,
353
+ .wui-input-group > .input:focus,
354
+ .wui-input-group > .input:hover {
506
355
  border: 0;
507
356
  box-shadow: none !important;
508
357
  background: transparent;
@@ -519,7 +368,7 @@
519
368
  flex-shrink: 0;
520
369
  font-size: 1rem;
521
370
  line-height: 1;
522
- pointer-events: none; // icons are visual only by default
371
+ pointer-events: none; // icons are visual only by default
523
372
  }
524
373
 
525
374
  // When the trailing slot is a button (clear, toggle, etc.), reset
@@ -544,120 +393,24 @@ button.wui-input-leading:focus-visible {
544
393
  border-radius: 9999px;
545
394
  }
546
395
 
547
-
548
- // ---- Link with the marketing wipe underline (matches locked design) -
549
- // Uses background-image (not a pseudo-element with inline-block) so the link
550
- // stays inline. inline-block + overflow:hidden used to give the link its own
551
- // baseline, which dropped it below surrounding prose whenever the parent had
552
- // a non-default font-size (see Pricing footnote "Talk to sales" regression).
553
-
554
- .link-wipe {
555
- background-image: linear-gradient(currentColor, currentColor);
556
- background-repeat: no-repeat;
557
- background-position-x: 100%;
558
- background-position-y: 100%;
559
- background-size: 0 1px;
560
- transition: background-size 500ms cubic-bezier(0.4, 0, 0.2, 1);
561
- }
562
- @media (hover: hover) {
563
- .link-wipe:hover {
564
- background-position-x: 0%;
565
- background-size: 100% 1px;
566
- }
567
- }
568
- .link-wipe--accent {
569
- background-image: linear-gradient(var(--wui-primary), var(--wui-primary));
570
- }
571
-
572
-
573
396
  // ---- Tabular-numerals helper for KPIs / counters --------------------
574
397
  .tabular-nums { font-variant-numeric: tabular-nums; }
575
398
 
576
-
577
- // ---- Container with the marketing 1512px canvas ---------------------
578
- // Pair with --wui-container-pad (responsive). Distinct from Bootstrap's
579
- // .container (max-width steps at 540/720/960/1140/1320). Use this when a
580
- // section needs to breathe up to 1512px on wide monitors.
581
- .container-cloves {
582
- max-width: var(--wui-container-max, 1512px);
583
- margin-inline: auto;
584
- padding-inline: var(--wui-container-pad, 24px);
585
- width: 100%;
586
- }
587
-
588
-
589
399
  // ---- Radius step utilities ------------------------------------------
590
400
  // Every .btn is already a squircle (see base .btn above), so a button that
591
401
  // wants a larger corner only needs a bigger radius — hence .btn-radius-xl,
592
402
  // named for what it does (an xl radius step) rather than the corner shape.
593
403
  .btn-radius-xl {
594
- --bs-btn-border-radius: var(--wui-radius-xl); // 16px
404
+ --bs-btn-border-radius: var(--wui-radius-xl); // 16px
595
405
  }
596
406
 
597
- // Generic (non-button) squircle rounding. Unlike .btn, an arbitrary element
598
- // gets no corner-shape from the base rules, so this utility sets it itself —
599
- // otherwise the name would lie and it'd render a plain 32px arc. Older
600
- // browsers fall back to that arc (Chrome 138+ / Safari 16+ for the squircle).
407
+ // Generic (non-button) squircle rounding. Progressive enhancement only
408
+ //: unsupported engines keep a plain 32px arc — not a contract.
601
409
  .rounded-squircle {
602
410
  border-radius: 32px;
603
411
  corner-shape: squircle;
604
412
  }
605
413
 
606
-
607
- // ---- Hero radial wash ------------------------------------------------
608
- // Soft tinted glow over an elevated surface — sits behind hero copy.
609
- .hero-wash {
610
- background:
611
- radial-gradient(80% 50% at 50% 0%,
612
- color-mix(in srgb, var(--wui-primary) 8%, transparent), transparent 70%),
613
- var(--wui-bg-elevated);
614
- }
615
-
616
-
617
- // ---- Page entry fade -------------------------------------------------
618
- // Apply to <main> in _Layout so every page entry has a polite fade-up.
619
- .page-fade-in {
620
- animation: pageFadeIn 0.25s var(--wui-ease-ui) backwards;
621
- }
622
-
623
-
624
- // ---- Single-element fade-in ------------------------------------------
625
- .fade-in {
626
- animation: fadeIn 220ms var(--wui-ease-out-quart);
627
- }
628
-
629
-
630
- // ---- Stagger groups --------------------------------------------------
631
- // Wrap a container in `.stagger-children` and its direct children
632
- // cascade in 40ms apart, capped at 12 children. For bigger groups,
633
- // use the IntersectionObserver-driven `[data-reveal]` pattern below.
634
- .stagger-children > * {
635
- animation: staggerRise 360ms var(--wui-ease-out-quart) backwards;
636
- }
637
- .stagger-children > *:nth-child(1) { animation-delay: 0ms; }
638
- .stagger-children > *:nth-child(2) { animation-delay: 40ms; }
639
- .stagger-children > *:nth-child(3) { animation-delay: 80ms; }
640
- .stagger-children > *:nth-child(4) { animation-delay: 120ms; }
641
- .stagger-children > *:nth-child(5) { animation-delay: 160ms; }
642
- .stagger-children > *:nth-child(6) { animation-delay: 200ms; }
643
- .stagger-children > *:nth-child(7) { animation-delay: 240ms; }
644
- .stagger-children > *:nth-child(8) { animation-delay: 280ms; }
645
- .stagger-children > *:nth-child(9) { animation-delay: 320ms; }
646
- .stagger-children > *:nth-child(10) { animation-delay: 360ms; }
647
- .stagger-children > *:nth-child(11) { animation-delay: 400ms; }
648
- .stagger-children > *:nth-child(12) { animation-delay: 440ms; }
649
-
650
-
651
- // ---- Opt-in single-element stagger -----------------------------------
652
- // Use when you need granular control: set `--stagger: 240ms` inline on
653
- // individual items inside a non-stagger-children container.
654
- .stagger-item {
655
- opacity: 0;
656
- animation: rise 320ms var(--wui-ease-out-quart) forwards;
657
- animation-delay: var(--stagger, 0ms);
658
- }
659
-
660
-
661
414
  // ---- Press feedback --------------------------------------------------
662
415
  // Lightweight tactile alternative to a Material ripple. Add to any
663
416
  // pressable element (button, card link, chip). 80ms ease-out.
@@ -668,7 +421,6 @@ button.wui-input-leading:focus-visible {
668
421
  transform: scale(0.96);
669
422
  }
670
423
 
671
-
672
424
  // ---- Subtle hover lift -----------------------------------------------
673
425
  .lift-on-hover {
674
426
  transition: transform 0.2s var(--wui-ease-out-quart),
@@ -678,7 +430,6 @@ button.wui-input-leading:focus-visible {
678
430
  transform: translateY(-1px);
679
431
  }
680
432
 
681
-
682
433
  // ---- Loading shimmer skeleton ----------------------------------------
683
434
  .shimmer {
684
435
  position: relative;
@@ -697,124 +448,38 @@ button.wui-input-leading:focus-visible {
697
448
  animation: shimmer 1.4s infinite;
698
449
  }
699
450
 
700
-
701
- // ---- Marquee (customer-logo carousel etc.) ---------------------------
702
- // Pair with a flex wrapper that duplicates its children so the
703
- // translateX(-50%) loops seamlessly. Example:
704
- // <div class="overflow-hidden">
705
- // <div class="d-flex animate-marquee" style="width: max-content;">
706
- // <ul class="d-flex gap-5 mb-0 list-unstyled">...</ul>
707
- // <ul class="d-flex gap-5 mb-0 list-unstyled" aria-hidden="true">...</ul>
708
- // </div>
709
- // </div>
710
- .animate-marquee {
711
- animation: marketing-marquee 32s linear infinite;
712
- }
713
-
714
-
715
- // ---- Scroll-triggered reveals (paired with wwwroot/js/reveal.js) -----
716
- // Author opts in by adding `data-reveal` to a section. The JS module
717
- // adds `.is-visible` when the element scrolls into view; CSS transitions
718
- // fade + lift on the transition. Stagger descendants via
719
- // `[data-reveal-stagger] > *` with `--reveal-step` inline (default 40ms).
720
- [data-reveal] {
721
- opacity: 0;
722
- transform: translateY(8px);
723
- transition:
724
- opacity 540ms var(--wui-ease-out-quart),
725
- transform 540ms var(--wui-ease-out-quart);
726
- will-change: opacity, transform;
727
- }
728
- [data-reveal].is-visible {
729
- opacity: 1;
730
- transform: none;
731
- }
732
-
733
- [data-reveal-stagger] > * {
734
- opacity: 0;
735
- transform: translateY(8px);
736
- transition:
737
- opacity 540ms var(--wui-ease-out-quart),
738
- transform 540ms var(--wui-ease-out-quart);
739
- }
740
- [data-reveal-stagger].is-visible > * {
741
- opacity: 1;
742
- transform: none;
743
- }
744
- [data-reveal-stagger].is-visible > *:nth-child(1) { transition-delay: calc(var(--reveal-step, 40ms) * 0); }
745
- [data-reveal-stagger].is-visible > *:nth-child(2) { transition-delay: calc(var(--reveal-step, 40ms) * 1); }
746
- [data-reveal-stagger].is-visible > *:nth-child(3) { transition-delay: calc(var(--reveal-step, 40ms) * 2); }
747
- [data-reveal-stagger].is-visible > *:nth-child(4) { transition-delay: calc(var(--reveal-step, 40ms) * 3); }
748
- [data-reveal-stagger].is-visible > *:nth-child(5) { transition-delay: calc(var(--reveal-step, 40ms) * 4); }
749
- [data-reveal-stagger].is-visible > *:nth-child(6) { transition-delay: calc(var(--reveal-step, 40ms) * 5); }
750
- [data-reveal-stagger].is-visible > *:nth-child(7) { transition-delay: calc(var(--reveal-step, 40ms) * 6); }
751
- [data-reveal-stagger].is-visible > *:nth-child(8) { transition-delay: calc(var(--reveal-step, 40ms) * 7); }
752
- [data-reveal-stagger].is-visible > *:nth-child(9) { transition-delay: calc(var(--reveal-step, 40ms) * 8); }
753
- [data-reveal-stagger].is-visible > *:nth-child(10) { transition-delay: calc(var(--reveal-step, 40ms) * 9); }
754
- [data-reveal-stagger].is-visible > *:nth-child(11) { transition-delay: calc(var(--reveal-step, 40ms) * 10); }
755
- [data-reveal-stagger].is-visible > *:nth-child(12) { transition-delay: calc(var(--reveal-step, 40ms) * 11); }
756
-
757
-
758
451
  // ---- Keyframes -------------------------------------------------------
759
- @keyframes fadeIn {
760
- from { opacity: 0; transform: translateY(4px); }
761
- to { opacity: 1; transform: translateY(0); }
762
- }
763
- @keyframes pageFadeIn {
764
- from { opacity: 0; transform: translateY(6px); }
765
- to { opacity: 1; transform: translateY(0); }
766
- }
767
- @keyframes rise {
768
- from { opacity: 0; transform: translateY(8px); }
769
- to { opacity: 1; transform: translateY(0); }
770
- }
771
- @keyframes staggerRise {
772
- from { opacity: 0; transform: translateY(8px); }
773
- to { opacity: 1; transform: translateY(0); }
774
- }
452
+
775
453
  @keyframes shimmer {
776
- 0% { transform: translateX(-100%); }
454
+ 0% { transform: translateX(-100%); }
777
455
  100% { transform: translateX(100%); }
778
456
  }
779
- @keyframes marketing-marquee {
780
- from { transform: translateX(0); }
781
- to { transform: translateX(-50%); }
782
- }
783
-
784
457
 
785
- // ---- Refined native scrollbars --------------------------------------
786
- // Mirrors the locked design's always-visible scrollbar treatment:
458
+ // ---- Native scrollbars (document + opt-in) --------------------------
459
+ // Scoped to html/body and `.wui-scroll` — never `*`. Global
460
+ // `*` restyled every nested scroller (modals, tiny panels) and fought
461
+ // component-local rails. AppScrollbar uses `.wui-scroll` (see wui/_scrollbar).
787
462
  //
788
- // - 8px thin WebKit thumb (also Firefox via `scrollbar-width: thin`)
789
- // - Quiet grey-blue tint always visible at rest (~40% alpha)
790
- // - Brighter on hover (~60% alpha) and active (~80%)
791
- // - Pill thumb with a 2px transparent border (visual cushion)
792
- // - `scrollbar-gutter: stable` on <html>/<body> so the page never
793
- // shifts when scrollable content grows/shrinks. Internal containers
794
- // (avatars, badges, rounded buttons) intentionally skip the gutter
795
- // reservation — applying it everywhere would push centered glyphs
796
- // off-center inside rounded boxes.
797
- //
798
- // `wwwroot/js/scroll-state.js` toggles `html.is-scrolling` for ~800ms
799
- // after each scroll event; the thumb gets an extra opacity bump while
800
- // the class is on, giving subtle "I'm scrolling now" feedback without
801
- // sacrificing the always-visible affordance.
463
+ // `scrollbar-gutter: stable` only on the document so internal boxes do not
464
+ // push centered glyphs off-center. `html.is-scrolling` (scroll-state.js)
465
+ // brightens the document thumb while the page is scrolling.
802
466
  html,
803
467
  body {
804
468
  scrollbar-gutter: stable;
805
- }
806
- * {
807
469
  scrollbar-width: thin;
808
470
  scrollbar-color: var(--wui-scrollbar) transparent;
809
471
  }
810
- *::-webkit-scrollbar {
472
+ html::-webkit-scrollbar,
473
+ body::-webkit-scrollbar {
811
474
  width: 8px;
812
475
  height: 8px;
813
476
  }
814
- *::-webkit-scrollbar-track {
477
+ html::-webkit-scrollbar-track,
478
+ body::-webkit-scrollbar-track {
815
479
  background: transparent;
816
480
  }
817
- *::-webkit-scrollbar-thumb {
481
+ html::-webkit-scrollbar-thumb,
482
+ body::-webkit-scrollbar-thumb {
818
483
  background-color: var(--wui-scrollbar);
819
484
  border-radius: 9999px;
820
485
  border: 2px solid transparent;
@@ -823,29 +488,31 @@ body {
823
488
  min-width: 36px;
824
489
  transition: background-color 220ms ease;
825
490
  }
826
- *::-webkit-scrollbar-thumb:hover {
491
+ html::-webkit-scrollbar-thumb:hover,
492
+ body::-webkit-scrollbar-thumb:hover {
827
493
  background-color: var(--wui-scrollbar-hover);
828
494
  background-clip: padding-box;
829
495
  }
830
- *::-webkit-scrollbar-thumb:active {
496
+ html::-webkit-scrollbar-thumb:active,
497
+ body::-webkit-scrollbar-thumb:active {
831
498
  background-color: var(--wui-scrollbar-active);
832
499
  background-clip: padding-box;
833
500
  }
834
- *::-webkit-scrollbar-corner {
501
+ html::-webkit-scrollbar-corner,
502
+ body::-webkit-scrollbar-corner {
835
503
  background: transparent;
836
504
  }
837
505
 
838
- // While the page is actively scrolling, give the thumb a slight extra
839
- // pop so the user can track its position more easily.
840
- html.is-scrolling * {
506
+ html.is-scrolling,
507
+ html.is-scrolling body {
841
508
  scrollbar-color: var(--wui-scrollbar-hover) transparent;
842
509
  }
843
- html.is-scrolling *::-webkit-scrollbar-thumb {
510
+ html.is-scrolling::-webkit-scrollbar-thumb,
511
+ html.is-scrolling body::-webkit-scrollbar-thumb {
844
512
  background-color: var(--wui-scrollbar-hover);
845
513
  }
846
514
 
847
- // Utility: completely hide a scrollbar (e.g., for marquee/horizontal
848
- // carousels where the rail itself is visually intrusive).
515
+ // Hide a scrollbar (marquee / horizontal carousels).
849
516
  .scrollbar-hide {
850
517
  scrollbar-width: none;
851
518
  }
@@ -853,86 +520,30 @@ html.is-scrolling *::-webkit-scrollbar-thumb {
853
520
  display: none;
854
521
  }
855
522
 
856
-
857
- // ---- Render perf primitives ----------------------------------------
858
- //
859
- // `content-visibility: auto` lets the browser skip layout + paint for
860
- // offscreen sections — a 1500px page with 8 sections paints in ~⅓ the
861
- // time after this change. Sections opt in via [data-section] (already
862
- // used on most homepage sections) or .section-y / .section-y-lg /
863
- // .section-y-sm wrappers. `contain-intrinsic-size` reserves a sane
864
- // height so the scrollbar doesn't snap as sections paint in.
865
- //
866
- // Notes:
867
- // - Don't apply to the inner hero or any element that hosts
868
- // position: sticky descendants — `content-visibility` creates a
869
- // containing block which breaks sticky positioning in those areas.
870
- // - First section is excluded — it's above the fold on every page,
871
- // no benefit and risks delaying LCP.
872
- [data-section]:not(:first-of-type):not([data-section="inner-hero"]):not([data-section="home-hero"]):not([data-section-skip-cv]),
873
- .section-y:not(:first-of-type):not([data-section-skip-cv]),
874
- .section-y-sm:not(:first-of-type):not([data-section-skip-cv]),
875
- .section-y-lg:not(:first-of-type):not([data-section-skip-cv]) {
876
- content-visibility: auto;
877
- contain-intrinsic-size: auto 720px;
878
- }
879
-
880
- // GPU-promote frequently-animated elements at rest so the compositor
881
- // can flip them on hover without a paint hit. We deliberately do NOT
882
- // add will-change globally — that costs memory on every element. Only
883
- // elements with motion-on-interact get it.
523
+ // GPU-promote portal interactive surfaces (marketing selectors → marketing.scss).
884
524
  .card-lifted,
885
525
  .lift-on-hover,
886
- .press-feedback,
887
- .contact-submit,
888
- .btn-pill,
889
- .btn-pill-sm,
890
- [data-reveal],
891
- [data-reveal-stagger] > * {
892
- will-change: auto; // baseline — opt out of promotion
526
+ .press-feedback {
527
+ will-change: auto;
893
528
  }
894
529
  .card-lifted:hover,
895
530
  .lift-on-hover:hover,
896
- .press-feedback:active,
897
- [data-reveal]:not(.is-visible),
898
- [data-reveal-stagger]:not(.is-visible) > * {
531
+ .press-feedback:active {
899
532
  will-change: transform, opacity;
900
533
  }
901
534
 
902
- // Reduce paint on form-field focus rings — box-shadow repaints are
903
- // cheaper when the painted layer is isolated.
904
- .contact-field,
905
- .custom-select-toggle,
906
- .contact-submit {
907
- backface-visibility: hidden;
908
- transform: translateZ(0);
909
- }
910
-
911
-
912
535
  // ---- Reduced motion -------------------------------------------------
913
536
  @media (prefers-reduced-motion: reduce) {
914
537
  .card-lifted,
915
538
  .lift-on-hover,
916
- .link-wipe,
917
539
  .press-feedback {
918
540
  transition: none;
919
541
  }
920
- .page-fade-in,
921
- .fade-in,
922
- .stagger-children > *,
923
- .stagger-item,
924
- .animate-marquee,
925
- .shimmer::after,
926
- [data-reveal],
927
- [data-reveal-stagger] > * {
542
+ .shimmer::after {
928
543
  animation: none;
929
- opacity: 1;
930
- transform: none;
931
- transition: none;
932
544
  }
933
545
  }
934
546
 
935
-
936
547
  // ---- Theme-flipped typography utilities ------------------------------------
937
548
  // Bootstrap builds `.text-body` as `rgba(var(--bs-body-color-rgb), …)` — an RGB
938
549
  // TRIPLET baked in at compile time (37, 47, 74). Bridging `--bs-body-color`