@webority/theme 0.0.9
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/LICENSE +5 -0
- package/README.md +33 -0
- package/dist/webority-theme.css +5 -0
- package/package.json +36 -0
- package/scss/_components.scss +720 -0
- package/scss/_tokens.scss +159 -0
- package/scss/_variables.scss +199 -0
- package/scss/_wui-aliases.scss +128 -0
- package/scss/_wui-components.scss +3296 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
// =====================================================================
|
|
2
|
+
// Lumex — 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.
|
|
7
|
+
// =====================================================================
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// ---- Brand wordmark / logo ------------------------------------------
|
|
11
|
+
.lumex-logo {
|
|
12
|
+
height: 2rem;
|
|
13
|
+
width: auto;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
// ---- Section helpers ------------------------------------------------
|
|
18
|
+
// Marketing + portal both use vertically-padded sections with the
|
|
19
|
+
// brand's spacing rhythm. Bootstrap's py-* gets you part-way; these
|
|
20
|
+
// are the named variants the design system reaches for.
|
|
21
|
+
.section-y-sm { padding-block: 3rem; }
|
|
22
|
+
.section-y { padding-block: 5rem; }
|
|
23
|
+
.section-y-lg { padding-block: 7rem; }
|
|
24
|
+
@media (min-width: 768px) {
|
|
25
|
+
.section-y-sm { padding-block: 4rem; }
|
|
26
|
+
.section-y { padding-block: 6rem; }
|
|
27
|
+
.section-y-lg { padding-block: 9rem; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
// ---- Eyebrow (accent-dot + label) -----------------------------------
|
|
32
|
+
.eyebrow {
|
|
33
|
+
display: inline-flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 0.5rem;
|
|
36
|
+
font-size: 14px;
|
|
37
|
+
font-weight: 700;
|
|
38
|
+
line-height: 1;
|
|
39
|
+
color: var(--wui-primary);
|
|
40
|
+
}
|
|
41
|
+
.eyebrow::before {
|
|
42
|
+
content: "";
|
|
43
|
+
width: 0.5rem;
|
|
44
|
+
height: 0.5rem;
|
|
45
|
+
border-radius: 9999px;
|
|
46
|
+
background: var(--wui-primary);
|
|
47
|
+
}
|
|
48
|
+
.eyebrow--on-dark {
|
|
49
|
+
color: rgba(255, 255, 255, 0.85);
|
|
50
|
+
}
|
|
51
|
+
.eyebrow--on-dark::before {
|
|
52
|
+
background: #FFFFFF;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// ---- Display heading -------------------------------------------------
|
|
57
|
+
// Bootstrap's .display-1..6 ladder skews bigger than our locked design.
|
|
58
|
+
// .h-display drops in where the marketing hero needs the locked
|
|
59
|
+
// font-weight 500 + tight letter-spacing.
|
|
60
|
+
.h-display {
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
letter-spacing: -0.03em;
|
|
63
|
+
line-height: 1.05;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
// ---- Gradient backgrounds -------------------------------------------
|
|
68
|
+
.bg-hero-gradient { background: var(--wui-hero-gradient); }
|
|
69
|
+
.bg-cta-gradient { background: var(--wui-cta-gradient); }
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
// ---- Button polish (used by both Razor + React) ---------------------
|
|
73
|
+
// Flat buttons — no drop shadow. Active-press feedback (1px nudge) is
|
|
74
|
+
// retained because it's a tactile signal, not a depth cue.
|
|
75
|
+
.btn-primary {
|
|
76
|
+
// Pin hover/active to the reference's `--accent-dark` (#2440C4).
|
|
77
|
+
// Bootstrap's auto-shaded $primary would compute a slightly different
|
|
78
|
+
// shade, drifting off-spec by a few hex points.
|
|
79
|
+
--bs-btn-hover-bg: #2440C4;
|
|
80
|
+
--bs-btn-hover-border-color: #2440C4;
|
|
81
|
+
--bs-btn-active-bg: #2440C4;
|
|
82
|
+
--bs-btn-active-border-color: #2440C4;
|
|
83
|
+
&:active:not(:disabled) {
|
|
84
|
+
transform: translateY(1px);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
// ---- Marketing pill buttons (locked design parity) -------------------
|
|
90
|
+
// Pixel-locked to the standalone reference's `btn-squircle inline-flex
|
|
91
|
+
// h-12 items-center text-sm font-medium` pattern. Each marketing CTA
|
|
92
|
+
// layers .btn-pill on a Bootstrap variant — these classes only override
|
|
93
|
+
// Bootstrap's btn CSS vars (height / padding / radius / font), so
|
|
94
|
+
// every variant inherits the right size without redefining colors.
|
|
95
|
+
//
|
|
96
|
+
// Reference values:
|
|
97
|
+
// - height : 48px (h-12 = 3rem)
|
|
98
|
+
// - radius : 16px (matches `.btn-squircle` literal)
|
|
99
|
+
// - font : 14px / 500 (text-sm font-medium)
|
|
100
|
+
// - padding-x : 20px (px-5) default — Watch demo / outline-dark
|
|
101
|
+
// Primary CTA overrides to 24px (px-6) inline
|
|
102
|
+
// - corner-shape : squircle (iOS-style smooth curve)
|
|
103
|
+
//
|
|
104
|
+
// Example:
|
|
105
|
+
// <a class="btn btn-primary btn-pill" style="--bs-btn-padding-x:1.5rem;">…</a>
|
|
106
|
+
// <a class="btn btn-light btn-pill-sm">Learn more</a>
|
|
107
|
+
.btn-pill {
|
|
108
|
+
--bs-btn-padding-y: 0; // height driven by min-height + flex centering
|
|
109
|
+
--bs-btn-padding-x: 1.25rem; // 20px (px-5)
|
|
110
|
+
--bs-btn-font-size: 0.875rem; // 14px
|
|
111
|
+
--bs-btn-font-weight: 500;
|
|
112
|
+
--bs-btn-line-height: 1.25;
|
|
113
|
+
--bs-btn-border-radius: 16px; // matches .btn-squircle exactly
|
|
114
|
+
min-height: 3rem; // 48px (h-12)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: 0.375rem; // 6px — matches primary arrow's ml-1.5
|
|
120
|
+
}
|
|
121
|
+
.btn-pill-sm {
|
|
122
|
+
--bs-btn-padding-y: 0;
|
|
123
|
+
--bs-btn-padding-x: 0.875rem; // 14px (px-3.5)
|
|
124
|
+
--bs-btn-font-size: 0.875rem; // 14px (text-sm)
|
|
125
|
+
--bs-btn-font-weight: 500;
|
|
126
|
+
--bs-btn-line-height: 1.25;
|
|
127
|
+
--bs-btn-border-radius: 12px;
|
|
128
|
+
min-height: 2.5rem; // 40px (h-10) — reference "Learn more" CTA
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
display: inline-flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: 0.375rem;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
// ---- Ghost-dark button (frosted glass on light surface) -------------
|
|
138
|
+
// Pixel-locked to the reference's `WatchDemoButton` "outline-dark" variant
|
|
139
|
+
// (src/components/marketing/brand/watch-demo-button.tsx):
|
|
140
|
+
// - border : rgba(13, 24, 43, 0.15) (foreground/15 in marketing-root)
|
|
141
|
+
// - bg : rgba(255, 255, 255, 0.6) (surface/60)
|
|
142
|
+
// - text : #0d182b (text-foreground in marketing-root)
|
|
143
|
+
// - blur : 4px (backdrop-blur-sm)
|
|
144
|
+
// - hover bg: #ffffff (hover:bg-surface = full opacity)
|
|
145
|
+
.btn-ghost-dark {
|
|
146
|
+
--bs-btn-color: #0d182b;
|
|
147
|
+
--bs-btn-bg: rgba(255, 255, 255, 0.60);
|
|
148
|
+
--bs-btn-border-color: rgba(13, 24, 43, 0.15);
|
|
149
|
+
--bs-btn-hover-color: #0d182b;
|
|
150
|
+
--bs-btn-hover-bg: #ffffff;
|
|
151
|
+
--bs-btn-hover-border-color: rgba(13, 24, 43, 0.20);
|
|
152
|
+
--bs-btn-active-color: #0d182b;
|
|
153
|
+
--bs-btn-active-bg: #ffffff;
|
|
154
|
+
--bs-btn-active-border-color: rgba(13, 24, 43, 0.20);
|
|
155
|
+
backdrop-filter: blur(4px);
|
|
156
|
+
-webkit-backdrop-filter: blur(4px);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
// ---- Card "lifted" (shadowless) -------------------------------------
|
|
161
|
+
// Previously a soft drop-shadow card; now flat. The `.card-lifted`
|
|
162
|
+
// class is preserved so existing call sites don't break — it now
|
|
163
|
+
// expresses elevation only via the 1px hover translate (kept as a
|
|
164
|
+
// tactile signal, not a depth cue). No box-shadow, here or on hover.
|
|
165
|
+
.card-lifted {
|
|
166
|
+
transition: transform 0.2s var(--wui-ease-out-quart);
|
|
167
|
+
&:hover {
|
|
168
|
+
transform: translateY(-1px);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
// ---- Inputs (Lumex canonical) ---------------------------------------
|
|
174
|
+
// 5 locked states — default / hover / focus / disabled / error —
|
|
175
|
+
// matching the reference `<Input>` component. Bootstrap drives the base
|
|
176
|
+
// look via $input-* vars in _lumex-variables.scss; these blocks add the
|
|
177
|
+
// hover-border-darken cue and the leading/trailing-slot wrapper.
|
|
178
|
+
//
|
|
179
|
+
// The error state is driven by `aria-invalid="true"` (NOT by Bootstrap's
|
|
180
|
+
// .is-invalid class) so accessibility tooling and visual cue stay in
|
|
181
|
+
// sync without extra markup.
|
|
182
|
+
|
|
183
|
+
.form-control,
|
|
184
|
+
.form-select {
|
|
185
|
+
transition: border-color 0.15s ease;
|
|
186
|
+
}
|
|
187
|
+
.form-control:hover:not(:focus):not(:disabled):not([readonly]):not([aria-invalid="true"]),
|
|
188
|
+
.form-select:hover:not(:focus):not(:disabled):not([aria-invalid="true"]) {
|
|
189
|
+
border-color: var(--color-border-strong); // --wui-text-3 @ 60%
|
|
190
|
+
}
|
|
191
|
+
.form-control[aria-invalid="true"],
|
|
192
|
+
.form-select[aria-invalid="true"] {
|
|
193
|
+
border-color: var(--bs-danger);
|
|
194
|
+
}
|
|
195
|
+
.form-control[aria-invalid="true"]:focus,
|
|
196
|
+
.form-select[aria-invalid="true"]:focus {
|
|
197
|
+
border-color: var(--bs-danger);
|
|
198
|
+
box-shadow: none;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Optional helper class for forms that prefer the explicit Bootstrap
|
|
202
|
+
// validation pattern. Mirrors aria-invalid styling.
|
|
203
|
+
.form-control.is-invalid,
|
|
204
|
+
.form-select.is-invalid {
|
|
205
|
+
border-color: var(--bs-danger);
|
|
206
|
+
background-image: none; // kill Bootstrap's exclamation-mark SVG
|
|
207
|
+
padding-right: 0.875rem;
|
|
208
|
+
}
|
|
209
|
+
.form-control.is-invalid:focus,
|
|
210
|
+
.form-select.is-invalid:focus {
|
|
211
|
+
border-color: var(--bs-danger);
|
|
212
|
+
box-shadow: none;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
// ---- Input group (leading / trailing slots) ------------------------
|
|
217
|
+
// Single outlined box that contains leading icon + input + trailing
|
|
218
|
+
// icon (or clear button). Border lives on the wrapper, so hover / focus
|
|
219
|
+
// / error states are visible regardless of which child has focus.
|
|
220
|
+
//
|
|
221
|
+
// Usage:
|
|
222
|
+
// <div class="lumex-input-group">
|
|
223
|
+
// <span class="lumex-input-leading"><i class="bi bi-currency-dollar"></i></span>
|
|
224
|
+
// <input type="text" class="form-control" placeholder="0.00" />
|
|
225
|
+
// </div>
|
|
226
|
+
//
|
|
227
|
+
// <div class="lumex-input-group">
|
|
228
|
+
// <input type="password" class="form-control" placeholder="Password" />
|
|
229
|
+
// <button type="button" class="lumex-input-trailing"><i class="bi bi-eye"></i></button>
|
|
230
|
+
// </div>
|
|
231
|
+
.lumex-input-group {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: 0.5rem;
|
|
235
|
+
padding-inline: 0.875rem;
|
|
236
|
+
height: calc(2.5rem + 2px); // 40px box + 1px border each side
|
|
237
|
+
background: var(--wui-bg-light, #FFFFFF);
|
|
238
|
+
border: 1px solid var(--bs-border-color);
|
|
239
|
+
border-radius: 0.625rem; // 10px — matches the bare input radius
|
|
240
|
+
transition: border-color 0.15s ease;
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
}
|
|
244
|
+
.lumex-input-group:hover:not(:focus-within):not(.is-disabled) {
|
|
245
|
+
border-color: rgba(120, 130, 157, 0.6);
|
|
246
|
+
}
|
|
247
|
+
.lumex-input-group:focus-within {
|
|
248
|
+
border-color: var(--wui-primary, #2F50E4);
|
|
249
|
+
}
|
|
250
|
+
.lumex-input-group.is-disabled,
|
|
251
|
+
.lumex-input-group:has(input:disabled) {
|
|
252
|
+
background: var(--wui-bg-muted, #F5F6FA);
|
|
253
|
+
cursor: not-allowed;
|
|
254
|
+
border-color: var(--bs-border-color);
|
|
255
|
+
}
|
|
256
|
+
.lumex-input-group.has-error,
|
|
257
|
+
.lumex-input-group:has(input[aria-invalid="true"]) {
|
|
258
|
+
border-color: var(--bs-danger);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Strip the bare input's own chrome — the wrapper carries it all.
|
|
262
|
+
.lumex-input-group > .form-control {
|
|
263
|
+
flex: 1 1 auto;
|
|
264
|
+
min-width: 0;
|
|
265
|
+
height: 100%;
|
|
266
|
+
padding-block: 0;
|
|
267
|
+
padding-inline: 0;
|
|
268
|
+
border: 0;
|
|
269
|
+
background: transparent;
|
|
270
|
+
box-shadow: none !important;
|
|
271
|
+
border-radius: 0;
|
|
272
|
+
}
|
|
273
|
+
.lumex-input-group > .form-control:focus,
|
|
274
|
+
.lumex-input-group > .form-control:hover {
|
|
275
|
+
border: 0;
|
|
276
|
+
box-shadow: none !important;
|
|
277
|
+
background: transparent;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Leading + trailing slots — flex children that hold icons or buttons.
|
|
281
|
+
// Default muted color, inherits hover/focus state from the wrapper.
|
|
282
|
+
.lumex-input-leading,
|
|
283
|
+
.lumex-input-trailing {
|
|
284
|
+
display: inline-flex;
|
|
285
|
+
align-items: center;
|
|
286
|
+
justify-content: center;
|
|
287
|
+
color: var(--wui-text-3, #78829D);
|
|
288
|
+
flex-shrink: 0;
|
|
289
|
+
font-size: 1rem;
|
|
290
|
+
line-height: 1;
|
|
291
|
+
pointer-events: none; // icons are visual only by default
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// When the trailing slot is a button (clear, toggle, etc.), reset
|
|
295
|
+
// button chrome and re-enable pointer events.
|
|
296
|
+
button.lumex-input-trailing,
|
|
297
|
+
button.lumex-input-leading {
|
|
298
|
+
background: transparent;
|
|
299
|
+
border: 0;
|
|
300
|
+
padding: 0;
|
|
301
|
+
pointer-events: auto;
|
|
302
|
+
cursor: pointer;
|
|
303
|
+
transition: color 0.15s ease;
|
|
304
|
+
}
|
|
305
|
+
button.lumex-input-trailing:hover,
|
|
306
|
+
button.lumex-input-leading:hover {
|
|
307
|
+
color: var(--wui-text-1, #252F4A);
|
|
308
|
+
}
|
|
309
|
+
button.lumex-input-trailing:focus-visible,
|
|
310
|
+
button.lumex-input-leading:focus-visible {
|
|
311
|
+
outline: none;
|
|
312
|
+
color: var(--wui-primary);
|
|
313
|
+
border-radius: 9999px;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
// ---- Link with the marketing wipe underline (matches locked design) -
|
|
318
|
+
// Uses background-image (not a pseudo-element with inline-block) so the link
|
|
319
|
+
// stays inline. inline-block + overflow:hidden used to give the link its own
|
|
320
|
+
// baseline, which dropped it below surrounding prose whenever the parent had
|
|
321
|
+
// a non-default font-size (see Pricing footnote "Talk to sales" regression).
|
|
322
|
+
|
|
323
|
+
.link-wipe {
|
|
324
|
+
background-image: linear-gradient(currentColor, currentColor);
|
|
325
|
+
background-repeat: no-repeat;
|
|
326
|
+
background-position-x: 100%;
|
|
327
|
+
background-position-y: 100%;
|
|
328
|
+
background-size: 0 1px;
|
|
329
|
+
transition: background-size 500ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
330
|
+
}
|
|
331
|
+
@media (hover: hover) {
|
|
332
|
+
.link-wipe:hover {
|
|
333
|
+
background-position-x: 0%;
|
|
334
|
+
background-size: 100% 1px;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
.link-wipe--accent {
|
|
338
|
+
background-image: linear-gradient(var(--wui-primary), var(--wui-primary));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
// ---- Tabular-numerals helper for KPIs / counters --------------------
|
|
343
|
+
.tabular-nums { font-variant-numeric: tabular-nums; }
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
// ---- Container with the marketing 1512px canvas ---------------------
|
|
347
|
+
// Pair with --wui-container-pad (responsive). Distinct from Bootstrap's
|
|
348
|
+
// .container (max-width steps at 540/720/960/1140/1320). Use this when a
|
|
349
|
+
// section needs to breathe up to 1512px on wide monitors.
|
|
350
|
+
.container-cloves {
|
|
351
|
+
max-width: var(--wui-container-max, 1512px);
|
|
352
|
+
margin-inline: auto;
|
|
353
|
+
padding-inline: var(--wui-container-pad, 24px);
|
|
354
|
+
width: 100%;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
// ---- Squircle corners (iOS-style) -----------------------------------
|
|
359
|
+
// `corner-shape: squircle` renders a smoother continuous curve instead
|
|
360
|
+
// of a simple circular arc — the same effect iOS app icons use. Mirrors
|
|
361
|
+
// the locked design's global `* { corner-shape: squircle }` rule.
|
|
362
|
+
// Browser support: Chrome 138+ and Safari 16+ with -webkit- prefix.
|
|
363
|
+
// Older browsers ignore the property and fall back to standard
|
|
364
|
+
// border-radius — no breakage.
|
|
365
|
+
//
|
|
366
|
+
// Apply by default to EVERY element, then explicitly opt
|
|
367
|
+
// `.rounded-circle` and `.rounded-pill` back to `round` — squircle on
|
|
368
|
+
// a 50% / 9999px radius reads as a subtly rounded square rather than a
|
|
369
|
+
// true circle/pill.
|
|
370
|
+
* {
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
}
|
|
374
|
+
.rounded-circle,
|
|
375
|
+
.rounded-pill {
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Named opt-in utilities — preserved for places where authors want a
|
|
381
|
+
// specific squircle radius without relying on Bootstrap's $border-radius.
|
|
382
|
+
.btn-squircle {
|
|
383
|
+
border-radius: 16px;
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
}
|
|
387
|
+
.rounded-squircle {
|
|
388
|
+
border-radius: 32px;
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
// ---- Hero radial wash ------------------------------------------------
|
|
395
|
+
// Soft tinted glow over an elevated surface — sits behind hero copy.
|
|
396
|
+
.hero-wash {
|
|
397
|
+
background:
|
|
398
|
+
radial-gradient(80% 50% at 50% 0%,
|
|
399
|
+
rgba(47, 80, 228, 0.08), transparent 70%),
|
|
400
|
+
var(--wui-bg-elevated);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
// ---- Page entry fade -------------------------------------------------
|
|
405
|
+
// Apply to <main> in _Layout so every page entry has a polite fade-up.
|
|
406
|
+
.page-fade-in {
|
|
407
|
+
animation: pageFadeIn 0.25s var(--wui-ease-ui) backwards;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
// ---- Single-element fade-in ------------------------------------------
|
|
412
|
+
.fade-in {
|
|
413
|
+
animation: fadeIn 220ms var(--wui-ease-out-quart);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
// ---- Stagger groups --------------------------------------------------
|
|
418
|
+
// Wrap a container in `.stagger-children` and its direct children
|
|
419
|
+
// cascade in 40ms apart, capped at 12 children. For bigger groups,
|
|
420
|
+
// use the IntersectionObserver-driven `[data-reveal]` pattern below.
|
|
421
|
+
.stagger-children > * {
|
|
422
|
+
animation: staggerRise 360ms var(--wui-ease-out-quart) backwards;
|
|
423
|
+
}
|
|
424
|
+
.stagger-children > *:nth-child(1) { animation-delay: 0ms; }
|
|
425
|
+
.stagger-children > *:nth-child(2) { animation-delay: 40ms; }
|
|
426
|
+
.stagger-children > *:nth-child(3) { animation-delay: 80ms; }
|
|
427
|
+
.stagger-children > *:nth-child(4) { animation-delay: 120ms; }
|
|
428
|
+
.stagger-children > *:nth-child(5) { animation-delay: 160ms; }
|
|
429
|
+
.stagger-children > *:nth-child(6) { animation-delay: 200ms; }
|
|
430
|
+
.stagger-children > *:nth-child(7) { animation-delay: 240ms; }
|
|
431
|
+
.stagger-children > *:nth-child(8) { animation-delay: 280ms; }
|
|
432
|
+
.stagger-children > *:nth-child(9) { animation-delay: 320ms; }
|
|
433
|
+
.stagger-children > *:nth-child(10) { animation-delay: 360ms; }
|
|
434
|
+
.stagger-children > *:nth-child(11) { animation-delay: 400ms; }
|
|
435
|
+
.stagger-children > *:nth-child(12) { animation-delay: 440ms; }
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
// ---- Opt-in single-element stagger -----------------------------------
|
|
439
|
+
// Use when you need granular control: set `--stagger: 240ms` inline on
|
|
440
|
+
// individual items inside a non-stagger-children container.
|
|
441
|
+
.stagger-item {
|
|
442
|
+
opacity: 0;
|
|
443
|
+
animation: rise 320ms var(--wui-ease-out-quart) forwards;
|
|
444
|
+
animation-delay: var(--stagger, 0ms);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
// ---- Press feedback --------------------------------------------------
|
|
449
|
+
// Lightweight tactile alternative to a Material ripple. Add to any
|
|
450
|
+
// pressable element (button, card link, chip). 80ms ease-out.
|
|
451
|
+
.press-feedback {
|
|
452
|
+
transition: transform 80ms ease-out;
|
|
453
|
+
}
|
|
454
|
+
.press-feedback:active:not(:disabled) {
|
|
455
|
+
transform: scale(0.96);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
// ---- Subtle hover lift -----------------------------------------------
|
|
460
|
+
.lift-on-hover {
|
|
461
|
+
transition: transform 0.2s var(--wui-ease-out-quart),
|
|
462
|
+
box-shadow 0.2s var(--wui-ease-out-quart);
|
|
463
|
+
}
|
|
464
|
+
.lift-on-hover:hover {
|
|
465
|
+
transform: translateY(-1px);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
// ---- Loading shimmer skeleton ----------------------------------------
|
|
470
|
+
.shimmer {
|
|
471
|
+
position: relative;
|
|
472
|
+
overflow: hidden;
|
|
473
|
+
background: var(--wui-bg-muted);
|
|
474
|
+
}
|
|
475
|
+
.shimmer::after {
|
|
476
|
+
content: "";
|
|
477
|
+
position: absolute;
|
|
478
|
+
inset: 0;
|
|
479
|
+
background: linear-gradient(90deg,
|
|
480
|
+
transparent,
|
|
481
|
+
rgba(255, 255, 255, 0.5),
|
|
482
|
+
transparent);
|
|
483
|
+
transform: translateX(-100%);
|
|
484
|
+
animation: shimmer 1.4s infinite;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
// ---- Marquee (customer-logo carousel etc.) ---------------------------
|
|
489
|
+
// Pair with a flex wrapper that duplicates its children so the
|
|
490
|
+
// translateX(-50%) loops seamlessly. Example:
|
|
491
|
+
// <div class="overflow-hidden">
|
|
492
|
+
// <div class="d-flex animate-marquee" style="width: max-content;">
|
|
493
|
+
// <ul class="d-flex gap-5 mb-0 list-unstyled">...</ul>
|
|
494
|
+
// <ul class="d-flex gap-5 mb-0 list-unstyled" aria-hidden="true">...</ul>
|
|
495
|
+
// </div>
|
|
496
|
+
// </div>
|
|
497
|
+
.animate-marquee {
|
|
498
|
+
animation: marketing-marquee 32s linear infinite;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
// ---- Scroll-triggered reveals (paired with wwwroot/js/reveal.js) -----
|
|
503
|
+
// Author opts in by adding `data-reveal` to a section. The JS module
|
|
504
|
+
// adds `.is-visible` when the element scrolls into view; CSS transitions
|
|
505
|
+
// fade + lift on the transition. Stagger descendants via
|
|
506
|
+
// `[data-reveal-stagger] > *` with `--reveal-step` inline (default 40ms).
|
|
507
|
+
[data-reveal] {
|
|
508
|
+
opacity: 0;
|
|
509
|
+
transform: translateY(8px);
|
|
510
|
+
transition:
|
|
511
|
+
opacity 540ms var(--wui-ease-out-quart),
|
|
512
|
+
transform 540ms var(--wui-ease-out-quart);
|
|
513
|
+
will-change: opacity, transform;
|
|
514
|
+
}
|
|
515
|
+
[data-reveal].is-visible {
|
|
516
|
+
opacity: 1;
|
|
517
|
+
transform: none;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
[data-reveal-stagger] > * {
|
|
521
|
+
opacity: 0;
|
|
522
|
+
transform: translateY(8px);
|
|
523
|
+
transition:
|
|
524
|
+
opacity 540ms var(--wui-ease-out-quart),
|
|
525
|
+
transform 540ms var(--wui-ease-out-quart);
|
|
526
|
+
}
|
|
527
|
+
[data-reveal-stagger].is-visible > * {
|
|
528
|
+
opacity: 1;
|
|
529
|
+
transform: none;
|
|
530
|
+
}
|
|
531
|
+
[data-reveal-stagger].is-visible > *:nth-child(1) { transition-delay: calc(var(--reveal-step, 40ms) * 0); }
|
|
532
|
+
[data-reveal-stagger].is-visible > *:nth-child(2) { transition-delay: calc(var(--reveal-step, 40ms) * 1); }
|
|
533
|
+
[data-reveal-stagger].is-visible > *:nth-child(3) { transition-delay: calc(var(--reveal-step, 40ms) * 2); }
|
|
534
|
+
[data-reveal-stagger].is-visible > *:nth-child(4) { transition-delay: calc(var(--reveal-step, 40ms) * 3); }
|
|
535
|
+
[data-reveal-stagger].is-visible > *:nth-child(5) { transition-delay: calc(var(--reveal-step, 40ms) * 4); }
|
|
536
|
+
[data-reveal-stagger].is-visible > *:nth-child(6) { transition-delay: calc(var(--reveal-step, 40ms) * 5); }
|
|
537
|
+
[data-reveal-stagger].is-visible > *:nth-child(7) { transition-delay: calc(var(--reveal-step, 40ms) * 6); }
|
|
538
|
+
[data-reveal-stagger].is-visible > *:nth-child(8) { transition-delay: calc(var(--reveal-step, 40ms) * 7); }
|
|
539
|
+
[data-reveal-stagger].is-visible > *:nth-child(9) { transition-delay: calc(var(--reveal-step, 40ms) * 8); }
|
|
540
|
+
[data-reveal-stagger].is-visible > *:nth-child(10) { transition-delay: calc(var(--reveal-step, 40ms) * 9); }
|
|
541
|
+
[data-reveal-stagger].is-visible > *:nth-child(11) { transition-delay: calc(var(--reveal-step, 40ms) * 10); }
|
|
542
|
+
[data-reveal-stagger].is-visible > *:nth-child(12) { transition-delay: calc(var(--reveal-step, 40ms) * 11); }
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
// ---- Keyframes -------------------------------------------------------
|
|
546
|
+
@keyframes fadeIn {
|
|
547
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
548
|
+
to { opacity: 1; transform: translateY(0); }
|
|
549
|
+
}
|
|
550
|
+
@keyframes pageFadeIn {
|
|
551
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
552
|
+
to { opacity: 1; transform: translateY(0); }
|
|
553
|
+
}
|
|
554
|
+
@keyframes rise {
|
|
555
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
556
|
+
to { opacity: 1; transform: translateY(0); }
|
|
557
|
+
}
|
|
558
|
+
@keyframes staggerRise {
|
|
559
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
560
|
+
to { opacity: 1; transform: translateY(0); }
|
|
561
|
+
}
|
|
562
|
+
@keyframes shimmer {
|
|
563
|
+
0% { transform: translateX(-100%); }
|
|
564
|
+
100% { transform: translateX(100%); }
|
|
565
|
+
}
|
|
566
|
+
@keyframes marketing-marquee {
|
|
567
|
+
from { transform: translateX(0); }
|
|
568
|
+
to { transform: translateX(-50%); }
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
// ---- Refined native scrollbars --------------------------------------
|
|
573
|
+
// Mirrors the locked design's always-visible scrollbar treatment:
|
|
574
|
+
//
|
|
575
|
+
// - 8px thin WebKit thumb (also Firefox via `scrollbar-width: thin`)
|
|
576
|
+
// - Quiet grey-blue tint always visible at rest (~40% alpha)
|
|
577
|
+
// - Brighter on hover (~60% alpha) and active (~80%)
|
|
578
|
+
// - Pill thumb with a 2px transparent border (visual cushion)
|
|
579
|
+
// - `scrollbar-gutter: stable` on <html>/<body> so the page never
|
|
580
|
+
// shifts when scrollable content grows/shrinks. Internal containers
|
|
581
|
+
// (avatars, badges, rounded buttons) intentionally skip the gutter
|
|
582
|
+
// reservation — applying it everywhere would push centered glyphs
|
|
583
|
+
// off-center inside rounded boxes.
|
|
584
|
+
//
|
|
585
|
+
// `wwwroot/js/scroll-state.js` toggles `html.is-scrolling` for ~800ms
|
|
586
|
+
// after each scroll event; the thumb gets an extra opacity bump while
|
|
587
|
+
// the class is on, giving subtle "I'm scrolling now" feedback without
|
|
588
|
+
// sacrificing the always-visible affordance.
|
|
589
|
+
html,
|
|
590
|
+
body {
|
|
591
|
+
scrollbar-gutter: stable;
|
|
592
|
+
}
|
|
593
|
+
* {
|
|
594
|
+
scrollbar-width: thin;
|
|
595
|
+
scrollbar-color: rgba(145, 158, 171, 0.45) transparent;
|
|
596
|
+
}
|
|
597
|
+
*::-webkit-scrollbar {
|
|
598
|
+
width: 8px;
|
|
599
|
+
height: 8px;
|
|
600
|
+
}
|
|
601
|
+
*::-webkit-scrollbar-track {
|
|
602
|
+
background: transparent;
|
|
603
|
+
}
|
|
604
|
+
*::-webkit-scrollbar-thumb {
|
|
605
|
+
background-color: rgba(145, 158, 171, 0.45);
|
|
606
|
+
border-radius: 9999px;
|
|
607
|
+
border: 2px solid transparent;
|
|
608
|
+
background-clip: padding-box;
|
|
609
|
+
min-height: 36px;
|
|
610
|
+
min-width: 36px;
|
|
611
|
+
transition: background-color 220ms ease;
|
|
612
|
+
}
|
|
613
|
+
*::-webkit-scrollbar-thumb:hover {
|
|
614
|
+
background-color: rgba(145, 158, 171, 0.65);
|
|
615
|
+
background-clip: padding-box;
|
|
616
|
+
}
|
|
617
|
+
*::-webkit-scrollbar-thumb:active {
|
|
618
|
+
background-color: rgba(145, 158, 171, 0.85);
|
|
619
|
+
background-clip: padding-box;
|
|
620
|
+
}
|
|
621
|
+
*::-webkit-scrollbar-corner {
|
|
622
|
+
background: transparent;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// While the page is actively scrolling, give the thumb a slight extra
|
|
626
|
+
// pop so the user can track its position more easily.
|
|
627
|
+
html.is-scrolling * {
|
|
628
|
+
scrollbar-color: rgba(145, 158, 171, 0.65) transparent;
|
|
629
|
+
}
|
|
630
|
+
html.is-scrolling *::-webkit-scrollbar-thumb {
|
|
631
|
+
background-color: rgba(145, 158, 171, 0.65);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Utility: completely hide a scrollbar (e.g., for marquee/horizontal
|
|
635
|
+
// carousels where the rail itself is visually intrusive).
|
|
636
|
+
.scrollbar-hide {
|
|
637
|
+
scrollbar-width: none;
|
|
638
|
+
}
|
|
639
|
+
.scrollbar-hide::-webkit-scrollbar {
|
|
640
|
+
display: none;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
// ---- Render perf primitives ----------------------------------------
|
|
645
|
+
//
|
|
646
|
+
// `content-visibility: auto` lets the browser skip layout + paint for
|
|
647
|
+
// offscreen sections — a 1500px page with 8 sections paints in ~⅓ the
|
|
648
|
+
// time after this change. Sections opt in via [data-section] (already
|
|
649
|
+
// used on most homepage sections) or .section-y / .section-y-lg /
|
|
650
|
+
// .section-y-sm wrappers. `contain-intrinsic-size` reserves a sane
|
|
651
|
+
// height so the scrollbar doesn't snap as sections paint in.
|
|
652
|
+
//
|
|
653
|
+
// Notes:
|
|
654
|
+
// - Don't apply to the inner hero or any element that hosts
|
|
655
|
+
// position: sticky descendants — `content-visibility` creates a
|
|
656
|
+
// containing block which breaks sticky positioning in those areas.
|
|
657
|
+
// - First section is excluded — it's above the fold on every page,
|
|
658
|
+
// no benefit and risks delaying LCP.
|
|
659
|
+
[data-section]:not(:first-of-type):not([data-section="inner-hero"]):not([data-section="home-hero"]):not([data-section-skip-cv]),
|
|
660
|
+
.section-y:not(:first-of-type):not([data-section-skip-cv]),
|
|
661
|
+
.section-y-sm:not(:first-of-type):not([data-section-skip-cv]),
|
|
662
|
+
.section-y-lg:not(:first-of-type):not([data-section-skip-cv]) {
|
|
663
|
+
content-visibility: auto;
|
|
664
|
+
contain-intrinsic-size: auto 720px;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// GPU-promote frequently-animated elements at rest so the compositor
|
|
668
|
+
// can flip them on hover without a paint hit. We deliberately do NOT
|
|
669
|
+
// add will-change globally — that costs memory on every element. Only
|
|
670
|
+
// elements with motion-on-interact get it.
|
|
671
|
+
.card-lifted,
|
|
672
|
+
.lift-on-hover,
|
|
673
|
+
.press-feedback,
|
|
674
|
+
.contact-submit,
|
|
675
|
+
.btn-pill,
|
|
676
|
+
.btn-pill-sm,
|
|
677
|
+
[data-reveal],
|
|
678
|
+
[data-reveal-stagger] > * {
|
|
679
|
+
will-change: auto; // baseline — opt out of promotion
|
|
680
|
+
}
|
|
681
|
+
.card-lifted:hover,
|
|
682
|
+
.lift-on-hover:hover,
|
|
683
|
+
.press-feedback:active,
|
|
684
|
+
[data-reveal]:not(.is-visible),
|
|
685
|
+
[data-reveal-stagger]:not(.is-visible) > * {
|
|
686
|
+
will-change: transform, opacity;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Reduce paint on form-field focus rings — box-shadow repaints are
|
|
690
|
+
// cheaper when the painted layer is isolated.
|
|
691
|
+
.contact-field,
|
|
692
|
+
.custom-select-toggle,
|
|
693
|
+
.contact-submit {
|
|
694
|
+
backface-visibility: hidden;
|
|
695
|
+
transform: translateZ(0);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
// ---- Reduced motion -------------------------------------------------
|
|
700
|
+
@media (prefers-reduced-motion: reduce) {
|
|
701
|
+
.card-lifted,
|
|
702
|
+
.lift-on-hover,
|
|
703
|
+
.link-wipe,
|
|
704
|
+
.press-feedback {
|
|
705
|
+
transition: none;
|
|
706
|
+
}
|
|
707
|
+
.page-fade-in,
|
|
708
|
+
.fade-in,
|
|
709
|
+
.stagger-children > *,
|
|
710
|
+
.stagger-item,
|
|
711
|
+
.animate-marquee,
|
|
712
|
+
.shimmer::after,
|
|
713
|
+
[data-reveal],
|
|
714
|
+
[data-reveal-stagger] > * {
|
|
715
|
+
animation: none;
|
|
716
|
+
opacity: 1;
|
|
717
|
+
transform: none;
|
|
718
|
+
transition: none;
|
|
719
|
+
}
|
|
720
|
+
}
|