@webority/theme 0.7.4 → 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,65 +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
94
  // Soft neutral tinted button — a quieter alternative to .btn-light for a secondary
157
95
  // action that shouldn't compete with a primary/danger CTA in the same row.
158
96
  .btn-subtle {
159
- --bs-btn-bg: var(--color-bg-muted);
160
- --bs-btn-color: var(--color-text);
161
- --bs-btn-border-color: transparent;
162
- --bs-btn-hover-bg: var(--color-border);
163
- --bs-btn-hover-color: var(--color-text);
164
- --bs-btn-hover-border-color: transparent;
165
- --bs-btn-active-bg: var(--color-border-strong);
166
- --bs-btn-active-color: var(--color-text);
167
- --bs-btn-active-border-color: transparent;
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;
168
106
  }
169
107
 
170
108
  // Light outline for a dark banner/hero CTA, where .btn-outline-primary's brand-coloured
171
109
  // border/text would disappear against a dark background. White is used directly (not a
172
110
  // token) because it's the one colour that reads on any dark surface, brand-independent.
173
111
  .btn-outline-light {
174
- --bs-btn-color: #fff;
175
- --bs-btn-bg: transparent;
176
- --bs-btn-border-color: rgba(255, 255, 255, 0.6);
177
- --bs-btn-hover-color: #fff;
178
- --bs-btn-hover-bg: rgba(255, 255, 255, 0.15);
179
- --bs-btn-hover-border-color: #fff;
180
- --bs-btn-active-color: #fff;
181
- --bs-btn-active-bg: rgba(255, 255, 255, 0.25);
182
- --bs-btn-active-border-color: #fff;
183
- --bs-btn-disabled-color: rgba(255, 255, 255, 0.5);
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);
184
122
  --bs-btn-disabled-border-color: rgba(255, 255, 255, 0.3);
185
123
  }
186
124
 
187
- // Themed disabled state (fleet default). Bootstrap's own disabled look is just the
125
+ // Themed disabled state (default). Bootstrap's own disabled look is just the
188
126
  // variant's colour at --bs-btn-disabled-opacity (.65) — barely different from the active
189
127
  // button on a tinted brand palette. Give the ordinary case a real desaturated/muted look
190
128
  // instead, reusing the same tokens .input:disabled already uses so a disabled button and
@@ -202,7 +140,6 @@
202
140
  cursor: not-allowed;
203
141
  }
204
142
 
205
-
206
143
  // ---- Component radii from tokens ------------------------------------
207
144
  // .btn/.card/.input capture Bootstrap's compiled $*-border-radius (the library
208
145
  // ladder) before a product loads, so a product's --wui-radius* override never
@@ -224,7 +161,6 @@
224
161
  --bs-btn-border-radius: var(--wui-radius-lg);
225
162
  }
226
163
 
227
-
228
164
  // ---- Dropdown menu (AppDropdown / <app-dropdown>) -------------------
229
165
  // Bootstrap ships a bare .dropdown-item; the design system gives the menu real
230
166
  // padding, an icon–label gap, a rounded hover highlight tinted with the brand,
@@ -233,19 +169,19 @@
233
169
  // Elevated surface (like .card), not Bootstrap's default page --bs-body-bg: otherwise the
234
170
  // menu blends into a tinted page and, in dark mode, resolves to the page colour instead of
235
171
  // the elevated surface.
236
- --bs-dropdown-bg: var(--color-surface);
237
- --bs-dropdown-min-width: 12rem;
238
- --bs-dropdown-padding-x: 0.375rem;
239
- --bs-dropdown-padding-y: 0.375rem;
240
- --bs-dropdown-item-padding-y: 0.5rem;
241
- --bs-dropdown-item-padding-x: 0.625rem;
242
- --bs-dropdown-border-color: var(--wui-border-light);
243
- --bs-dropdown-border-radius: var(--wui-radius-lg);
244
- --bs-dropdown-link-color: var(--wui-text-1);
245
- --bs-dropdown-link-hover-color: var(--wui-primary-pressed);
246
- --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);
247
183
  --bs-dropdown-link-active-color: var(--wui-primary-pressed);
248
- --bs-dropdown-link-active-bg: var(--wui-primary-10);
184
+ --bs-dropdown-link-active-bg: var(--wui-primary-10);
249
185
  box-shadow: var(--wui-shadow-overlay);
250
186
  }
251
187
 
@@ -283,87 +219,15 @@
283
219
 
284
220
  // Destructive row action — red label, red glyph, red-tinted hover.
285
221
  &.danger {
286
- --bs-dropdown-link-color: var(--wui-danger-text);
222
+ --bs-dropdown-link-color: var(--wui-danger-text);
287
223
  --bs-dropdown-link-hover-color: var(--wui-danger-text);
288
- --bs-dropdown-link-hover-bg: var(--wui-danger-10);
224
+ --bs-dropdown-link-hover-bg: var(--wui-danger-10);
289
225
  .bi, i, svg { color: var(--wui-danger); }
290
226
  &:hover .bi, &:hover i, &:hover svg,
291
227
  &:focus .bi, &:focus i, &:focus svg { color: var(--wui-danger); }
292
228
  }
293
229
  }
294
230
 
295
-
296
- // ---- Marketing pill buttons (locked design parity) -------------------
297
- // Pixel-locked to the standalone reference's `btn-squircle inline-flex
298
- // h-12 items-center text-sm font-medium` pattern. Each marketing CTA
299
- // layers .btn-pill on a Bootstrap variant — these classes only override
300
- // Bootstrap's btn CSS vars (height / padding / radius / font), so
301
- // every variant inherits the right size without redefining colors.
302
- //
303
- // Reference values:
304
- // - height : 48px (h-12 = 3rem)
305
- // - radius : 16px (--wui-radius-xl)
306
- // - font : 14px / 500 (text-sm font-medium)
307
- // - padding-x : 20px (px-5) default — Watch demo / outline-dark
308
- // Primary CTA overrides to 24px (px-6) inline
309
- // - corner-shape : squircle (iOS-style smooth curve)
310
- //
311
- // Example:
312
- // <a class="btn btn-primary btn-pill" style="--bs-btn-padding-x:1.5rem;">…</a>
313
- // <a class="btn btn-light btn-pill-sm">Learn more</a>
314
- .btn-pill {
315
- --bs-btn-padding-y: 0; // height driven by min-height + flex centering
316
- --bs-btn-padding-x: 1.25rem; // 20px (px-5)
317
- --bs-btn-font-size: 0.875rem; // 14px
318
- --bs-btn-font-weight: 500;
319
- --bs-btn-line-height: 1.25;
320
- --bs-btn-border-radius: var(--wui-radius-xl); // 16px
321
- min-height: 3rem; // 48px (h-12)
322
-
323
-
324
- display: inline-flex;
325
- align-items: center;
326
- gap: 0.375rem; // 6px — matches primary arrow's ml-1.5
327
- }
328
- .btn-pill-sm {
329
- --bs-btn-padding-y: 0;
330
- --bs-btn-padding-x: 0.875rem; // 14px (px-3.5)
331
- --bs-btn-font-size: 0.875rem; // 14px (text-sm)
332
- --bs-btn-font-weight: 500;
333
- --bs-btn-line-height: 1.25;
334
- --bs-btn-border-radius: 12px;
335
- min-height: 2.5rem; // 40px (h-10) — reference "Learn more" CTA
336
-
337
-
338
- display: inline-flex;
339
- align-items: center;
340
- gap: 0.375rem;
341
- }
342
-
343
-
344
- // ---- Ghost-dark button (frosted glass on light surface) -------------
345
- // Pixel-locked to the reference's `WatchDemoButton` "outline-dark" variant
346
- // (src/components/marketing/brand/watch-demo-button.tsx):
347
- // - border : rgba(13, 24, 43, 0.15) (foreground/15 in marketing-root)
348
- // - bg : rgba(255, 255, 255, 0.6) (surface/60)
349
- // - text : --wui-text-heading (text-foreground in marketing-root)
350
- // - blur : 4px (backdrop-blur-sm)
351
- // - hover bg: #ffffff (hover:bg-surface = full opacity)
352
- .btn-ghost-dark {
353
- --bs-btn-color: var(--wui-text-heading);
354
- --bs-btn-bg: rgba(255, 255, 255, 0.60);
355
- --bs-btn-border-color: rgba(13, 24, 43, 0.15);
356
- --bs-btn-hover-color: var(--wui-text-heading);
357
- --bs-btn-hover-bg: #ffffff;
358
- --bs-btn-hover-border-color: rgba(13, 24, 43, 0.20);
359
- --bs-btn-active-color: var(--wui-text-heading);
360
- --bs-btn-active-bg: #ffffff;
361
- --bs-btn-active-border-color: rgba(13, 24, 43, 0.20);
362
- backdrop-filter: blur(4px);
363
- -webkit-backdrop-filter: blur(4px);
364
- }
365
-
366
-
367
231
  // ---- Ghost button (transparent, brand-tinted hover) ------------------
368
232
  // A real ghost button: transparent at rest (no fill, no border), current text colour;
369
233
  // only the background lights up — a soft brand tint, the same recipe as a
@@ -376,22 +240,21 @@
376
240
  // is an ordinary .btn variant, so it sizes the same way .btn-primary/.btn-danger do —
377
241
  // Bootstrap's own .btn-sm/.btn-lg.
378
242
  .btn-ghost {
379
- --bs-btn-color: var(--color-text);
380
- --bs-btn-bg: transparent;
381
- --bs-btn-border-color: transparent;
382
- --bs-btn-hover-color: var(--color-text);
383
- --bs-btn-hover-bg: var(--wui-primary-5);
384
- --bs-btn-hover-border-color: transparent;
385
- --bs-btn-active-color: var(--color-text);
386
- --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);
387
251
  --bs-btn-active-border-color: transparent;
388
252
  }
389
253
 
390
-
391
254
  // ---- Card hover-lift (name is historical) -----------------------------
392
255
  // `.card-lifted` does NOT cast a shadow — in-page cards stay flat. It only
393
256
  // nudges 1px on hover (tactile signal). API prop stays `lifted` / class
394
- // `card-lifted` so call sites don't break (TASK-06086).
257
+ // `card-lifted` so call sites don't break.
395
258
  .card-lifted {
396
259
  transition: transform 0.2s var(--wui-ease-out-quart);
397
260
  &:hover {
@@ -399,7 +262,6 @@
399
262
  }
400
263
  }
401
264
 
402
-
403
265
  // ---- Card surface + radius (token-driven) ---------------------------
404
266
  // Radius follows the token ladder (see component-radii note) instead of the
405
267
  // compiled $card-border-radius. Background is an ELEVATED surface, not the page
@@ -425,12 +287,10 @@
425
287
  margin-left: auto;
426
288
  }
427
289
 
428
-
429
290
  // ---- Inputs ---------------------------------------------------------
430
291
  // Field chrome (height, hover, focus, invalid) lives in wui/_controls.scss on
431
292
  // `.form-control` / `.form-select` with `.input` as an alias — one skin, not two
432
- // (TASK-06079). This partial only keeps the marketing-side input-group shell.
433
-
293
+ // This partial only keeps the marketing-side input-group shell.
434
294
 
435
295
  // ---- Input group (leading / trailing slots) ------------------------
436
296
  // Single outlined box that contains leading icon + input + trailing
@@ -438,15 +298,15 @@
438
298
  // / error states are visible regardless of which child has focus.
439
299
  //
440
300
  // Usage:
441
- // <div class="wui-input-group">
442
- // <span class="wui-input-leading"><i class="bi bi-currency-dollar"></i></span>
443
- // <input type="text" class="form-control" placeholder="0.00" />
444
- // </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>
445
305
  //
446
- // <div class="wui-input-group">
447
- // <input type="password" class="form-control" placeholder="Password" />
448
- // <button type="button" class="wui-input-trailing"><i class="bi bi-eye"></i></button>
449
- // </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>
450
310
  .wui-input-group {
451
311
  display: flex;
452
312
  align-items: center;
@@ -508,7 +368,7 @@
508
368
  flex-shrink: 0;
509
369
  font-size: 1rem;
510
370
  line-height: 1;
511
- pointer-events: none; // icons are visual only by default
371
+ pointer-events: none; // icons are visual only by default
512
372
  }
513
373
 
514
374
  // When the trailing slot is a button (clear, toggle, etc.), reset
@@ -533,120 +393,24 @@ button.wui-input-leading:focus-visible {
533
393
  border-radius: 9999px;
534
394
  }
535
395
 
536
-
537
- // ---- Link with the marketing wipe underline (matches locked design) -
538
- // Uses background-image (not a pseudo-element with inline-block) so the link
539
- // stays inline. inline-block + overflow:hidden used to give the link its own
540
- // baseline, which dropped it below surrounding prose whenever the parent had
541
- // a non-default font-size (see Pricing footnote "Talk to sales" regression).
542
-
543
- .link-wipe {
544
- background-image: linear-gradient(currentColor, currentColor);
545
- background-repeat: no-repeat;
546
- background-position-x: 100%;
547
- background-position-y: 100%;
548
- background-size: 0 1px;
549
- transition: background-size 500ms cubic-bezier(0.4, 0, 0.2, 1);
550
- }
551
- @media (hover: hover) {
552
- .link-wipe:hover {
553
- background-position-x: 0%;
554
- background-size: 100% 1px;
555
- }
556
- }
557
- .link-wipe--accent {
558
- background-image: linear-gradient(var(--wui-primary), var(--wui-primary));
559
- }
560
-
561
-
562
396
  // ---- Tabular-numerals helper for KPIs / counters --------------------
563
397
  .tabular-nums { font-variant-numeric: tabular-nums; }
564
398
 
565
-
566
- // ---- Container with the marketing 1512px canvas ---------------------
567
- // Pair with --wui-container-pad (responsive). Distinct from Bootstrap's
568
- // .container (max-width steps at 540/720/960/1140/1320). Use this when a
569
- // section needs to breathe up to 1512px on wide monitors.
570
- .container-cloves {
571
- max-width: var(--wui-container-max, 1512px);
572
- margin-inline: auto;
573
- padding-inline: var(--wui-container-pad, 24px);
574
- width: 100%;
575
- }
576
-
577
-
578
399
  // ---- Radius step utilities ------------------------------------------
579
400
  // Every .btn is already a squircle (see base .btn above), so a button that
580
401
  // wants a larger corner only needs a bigger radius — hence .btn-radius-xl,
581
402
  // named for what it does (an xl radius step) rather than the corner shape.
582
403
  .btn-radius-xl {
583
- --bs-btn-border-radius: var(--wui-radius-xl); // 16px
404
+ --bs-btn-border-radius: var(--wui-radius-xl); // 16px
584
405
  }
585
406
 
586
- // Generic (non-button) squircle rounding. Unlike .btn, an arbitrary element
587
- // gets no corner-shape from the base rules, so this utility sets it itself —
588
- // otherwise the name would lie and it'd render a plain 32px arc. Older
589
- // 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.
590
409
  .rounded-squircle {
591
410
  border-radius: 32px;
592
411
  corner-shape: squircle;
593
412
  }
594
413
 
595
-
596
- // ---- Hero radial wash ------------------------------------------------
597
- // Soft tinted glow over an elevated surface — sits behind hero copy.
598
- .hero-wash {
599
- background:
600
- radial-gradient(80% 50% at 50% 0%,
601
- color-mix(in srgb, var(--wui-primary) 8%, transparent), transparent 70%),
602
- var(--wui-bg-elevated);
603
- }
604
-
605
-
606
- // ---- Page entry fade -------------------------------------------------
607
- // Apply to <main> in _Layout so every page entry has a polite fade-up.
608
- .page-fade-in {
609
- animation: pageFadeIn 0.25s var(--wui-ease-ui) backwards;
610
- }
611
-
612
-
613
- // ---- Single-element fade-in ------------------------------------------
614
- .fade-in {
615
- animation: fadeIn 220ms var(--wui-ease-out-quart);
616
- }
617
-
618
-
619
- // ---- Stagger groups --------------------------------------------------
620
- // Wrap a container in `.stagger-children` and its direct children
621
- // cascade in 40ms apart, capped at 12 children. For bigger groups,
622
- // use the IntersectionObserver-driven `[data-reveal]` pattern below.
623
- .stagger-children > * {
624
- animation: staggerRise 360ms var(--wui-ease-out-quart) backwards;
625
- }
626
- .stagger-children > *:nth-child(1) { animation-delay: 0ms; }
627
- .stagger-children > *:nth-child(2) { animation-delay: 40ms; }
628
- .stagger-children > *:nth-child(3) { animation-delay: 80ms; }
629
- .stagger-children > *:nth-child(4) { animation-delay: 120ms; }
630
- .stagger-children > *:nth-child(5) { animation-delay: 160ms; }
631
- .stagger-children > *:nth-child(6) { animation-delay: 200ms; }
632
- .stagger-children > *:nth-child(7) { animation-delay: 240ms; }
633
- .stagger-children > *:nth-child(8) { animation-delay: 280ms; }
634
- .stagger-children > *:nth-child(9) { animation-delay: 320ms; }
635
- .stagger-children > *:nth-child(10) { animation-delay: 360ms; }
636
- .stagger-children > *:nth-child(11) { animation-delay: 400ms; }
637
- .stagger-children > *:nth-child(12) { animation-delay: 440ms; }
638
-
639
-
640
- // ---- Opt-in single-element stagger -----------------------------------
641
- // Use when you need granular control: set `--stagger: 240ms` inline on
642
- // individual items inside a non-stagger-children container.
643
- .stagger-item {
644
- opacity: 0;
645
- animation: rise 320ms var(--wui-ease-out-quart) forwards;
646
- animation-delay: var(--stagger, 0ms);
647
- }
648
-
649
-
650
414
  // ---- Press feedback --------------------------------------------------
651
415
  // Lightweight tactile alternative to a Material ripple. Add to any
652
416
  // pressable element (button, card link, chip). 80ms ease-out.
@@ -657,7 +421,6 @@ button.wui-input-leading:focus-visible {
657
421
  transform: scale(0.96);
658
422
  }
659
423
 
660
-
661
424
  // ---- Subtle hover lift -----------------------------------------------
662
425
  .lift-on-hover {
663
426
  transition: transform 0.2s var(--wui-ease-out-quart),
@@ -667,7 +430,6 @@ button.wui-input-leading:focus-visible {
667
430
  transform: translateY(-1px);
668
431
  }
669
432
 
670
-
671
433
  // ---- Loading shimmer skeleton ----------------------------------------
672
434
  .shimmer {
673
435
  position: relative;
@@ -686,93 +448,15 @@ button.wui-input-leading:focus-visible {
686
448
  animation: shimmer 1.4s infinite;
687
449
  }
688
450
 
689
-
690
- // ---- Marquee (customer-logo carousel etc.) ---------------------------
691
- // Pair with a flex wrapper that duplicates its children so the
692
- // translateX(-50%) loops seamlessly. Example:
693
- // <div class="overflow-hidden">
694
- // <div class="d-flex animate-marquee" style="width: max-content;">
695
- // <ul class="d-flex gap-5 mb-0 list-unstyled">...</ul>
696
- // <ul class="d-flex gap-5 mb-0 list-unstyled" aria-hidden="true">...</ul>
697
- // </div>
698
- // </div>
699
- .animate-marquee {
700
- animation: marketing-marquee 32s linear infinite;
701
- }
702
-
703
-
704
- // ---- Scroll-triggered reveals (paired with wwwroot/js/reveal.js) -----
705
- // Author opts in by adding `data-reveal` to a section. The JS module
706
- // adds `.is-visible` when the element scrolls into view; CSS transitions
707
- // fade + lift on the transition. Stagger descendants via
708
- // `[data-reveal-stagger] > *` with `--reveal-step` inline (default 40ms).
709
- [data-reveal] {
710
- opacity: 0;
711
- transform: translateY(8px);
712
- transition:
713
- opacity 540ms var(--wui-ease-out-quart),
714
- transform 540ms var(--wui-ease-out-quart);
715
- will-change: opacity, transform;
716
- }
717
- [data-reveal].is-visible {
718
- opacity: 1;
719
- transform: none;
720
- }
721
-
722
- [data-reveal-stagger] > * {
723
- opacity: 0;
724
- transform: translateY(8px);
725
- transition:
726
- opacity 540ms var(--wui-ease-out-quart),
727
- transform 540ms var(--wui-ease-out-quart);
728
- }
729
- [data-reveal-stagger].is-visible > * {
730
- opacity: 1;
731
- transform: none;
732
- }
733
- [data-reveal-stagger].is-visible > *:nth-child(1) { transition-delay: calc(var(--reveal-step, 40ms) * 0); }
734
- [data-reveal-stagger].is-visible > *:nth-child(2) { transition-delay: calc(var(--reveal-step, 40ms) * 1); }
735
- [data-reveal-stagger].is-visible > *:nth-child(3) { transition-delay: calc(var(--reveal-step, 40ms) * 2); }
736
- [data-reveal-stagger].is-visible > *:nth-child(4) { transition-delay: calc(var(--reveal-step, 40ms) * 3); }
737
- [data-reveal-stagger].is-visible > *:nth-child(5) { transition-delay: calc(var(--reveal-step, 40ms) * 4); }
738
- [data-reveal-stagger].is-visible > *:nth-child(6) { transition-delay: calc(var(--reveal-step, 40ms) * 5); }
739
- [data-reveal-stagger].is-visible > *:nth-child(7) { transition-delay: calc(var(--reveal-step, 40ms) * 6); }
740
- [data-reveal-stagger].is-visible > *:nth-child(8) { transition-delay: calc(var(--reveal-step, 40ms) * 7); }
741
- [data-reveal-stagger].is-visible > *:nth-child(9) { transition-delay: calc(var(--reveal-step, 40ms) * 8); }
742
- [data-reveal-stagger].is-visible > *:nth-child(10) { transition-delay: calc(var(--reveal-step, 40ms) * 9); }
743
- [data-reveal-stagger].is-visible > *:nth-child(11) { transition-delay: calc(var(--reveal-step, 40ms) * 10); }
744
- [data-reveal-stagger].is-visible > *:nth-child(12) { transition-delay: calc(var(--reveal-step, 40ms) * 11); }
745
-
746
-
747
451
  // ---- Keyframes -------------------------------------------------------
748
- @keyframes fadeIn {
749
- from { opacity: 0; transform: translateY(4px); }
750
- to { opacity: 1; transform: translateY(0); }
751
- }
752
- @keyframes pageFadeIn {
753
- from { opacity: 0; transform: translateY(6px); }
754
- to { opacity: 1; transform: translateY(0); }
755
- }
756
- @keyframes rise {
757
- from { opacity: 0; transform: translateY(8px); }
758
- to { opacity: 1; transform: translateY(0); }
759
- }
760
- @keyframes staggerRise {
761
- from { opacity: 0; transform: translateY(8px); }
762
- to { opacity: 1; transform: translateY(0); }
763
- }
452
+
764
453
  @keyframes shimmer {
765
- 0% { transform: translateX(-100%); }
454
+ 0% { transform: translateX(-100%); }
766
455
  100% { transform: translateX(100%); }
767
456
  }
768
- @keyframes marketing-marquee {
769
- from { transform: translateX(0); }
770
- to { transform: translateX(-50%); }
771
- }
772
-
773
457
 
774
458
  // ---- Native scrollbars (document + opt-in) --------------------------
775
- // Scoped to html/body and `.wui-scroll` — never `*` (TASK-06085). Global
459
+ // Scoped to html/body and `.wui-scroll` — never `*`. Global
776
460
  // `*` restyled every nested scroller (modals, tiny panels) and fought
777
461
  // component-local rails. AppScrollbar uses `.wui-scroll` (see wui/_scrollbar).
778
462
  //
@@ -836,86 +520,30 @@ html.is-scrolling body::-webkit-scrollbar-thumb {
836
520
  display: none;
837
521
  }
838
522
 
839
-
840
- // ---- Render perf primitives ----------------------------------------
841
- //
842
- // `content-visibility: auto` lets the browser skip layout + paint for
843
- // offscreen sections — a 1500px page with 8 sections paints in ~⅓ the
844
- // time after this change. Sections opt in via [data-section] (already
845
- // used on most homepage sections) or .section-y / .section-y-lg /
846
- // .section-y-sm wrappers. `contain-intrinsic-size` reserves a sane
847
- // height so the scrollbar doesn't snap as sections paint in.
848
- //
849
- // Notes:
850
- // - Don't apply to the inner hero or any element that hosts
851
- // position: sticky descendants — `content-visibility` creates a
852
- // containing block which breaks sticky positioning in those areas.
853
- // - First section is excluded — it's above the fold on every page,
854
- // no benefit and risks delaying LCP.
855
- [data-section]:not(:first-of-type):not([data-section="inner-hero"]):not([data-section="home-hero"]):not([data-section-skip-cv]),
856
- .section-y:not(:first-of-type):not([data-section-skip-cv]),
857
- .section-y-sm:not(:first-of-type):not([data-section-skip-cv]),
858
- .section-y-lg:not(:first-of-type):not([data-section-skip-cv]) {
859
- content-visibility: auto;
860
- contain-intrinsic-size: auto 720px;
861
- }
862
-
863
- // GPU-promote frequently-animated elements at rest so the compositor
864
- // can flip them on hover without a paint hit. We deliberately do NOT
865
- // add will-change globally — that costs memory on every element. Only
866
- // elements with motion-on-interact get it.
523
+ // GPU-promote portal interactive surfaces (marketing selectors → marketing.scss).
867
524
  .card-lifted,
868
525
  .lift-on-hover,
869
- .press-feedback,
870
- .contact-submit,
871
- .btn-pill,
872
- .btn-pill-sm,
873
- [data-reveal],
874
- [data-reveal-stagger] > * {
875
- will-change: auto; // baseline — opt out of promotion
526
+ .press-feedback {
527
+ will-change: auto;
876
528
  }
877
529
  .card-lifted:hover,
878
530
  .lift-on-hover:hover,
879
- .press-feedback:active,
880
- [data-reveal]:not(.is-visible),
881
- [data-reveal-stagger]:not(.is-visible) > * {
531
+ .press-feedback:active {
882
532
  will-change: transform, opacity;
883
533
  }
884
534
 
885
- // Reduce paint on form-field focus rings — box-shadow repaints are
886
- // cheaper when the painted layer is isolated.
887
- .contact-field,
888
- .custom-select-toggle,
889
- .contact-submit {
890
- backface-visibility: hidden;
891
- transform: translateZ(0);
892
- }
893
-
894
-
895
535
  // ---- Reduced motion -------------------------------------------------
896
536
  @media (prefers-reduced-motion: reduce) {
897
537
  .card-lifted,
898
538
  .lift-on-hover,
899
- .link-wipe,
900
539
  .press-feedback {
901
540
  transition: none;
902
541
  }
903
- .page-fade-in,
904
- .fade-in,
905
- .stagger-children > *,
906
- .stagger-item,
907
- .animate-marquee,
908
- .shimmer::after,
909
- [data-reveal],
910
- [data-reveal-stagger] > * {
542
+ .shimmer::after {
911
543
  animation: none;
912
- opacity: 1;
913
- transform: none;
914
- transition: none;
915
544
  }
916
545
  }
917
546
 
918
-
919
547
  // ---- Theme-flipped typography utilities ------------------------------------
920
548
  // Bootstrap builds `.text-body` as `rgba(var(--bs-body-color-rgb), …)` — an RGB
921
549
  // TRIPLET baked in at compile time (37, 47, 74). Bridging `--bs-body-color`