maverick-wave 5.27.0 → 5.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,3 @@
1
- @use 'sass:math';
2
1
  @use '../abstracts' as *;
3
2
 
4
3
  @layer mw.layout {
@@ -34,182 +33,135 @@
34
33
  margin: spacing('0') auto;
35
34
  padding: spacing('0');
36
35
 
37
- @supports (selector(:has(.mw-hero))) {
38
- &:has(.mw-hero) {
36
+ &:has(.mw-hero) {
37
+ position: relative;
38
+ width: 100%;
39
+ min-height: 100dvh;
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ background-size: cover;
44
+ background-position: center;
45
+ background-image: var(--mw-hero-background);
46
+
47
+ // Set on the container so a hero without .mw-home gets it too
48
+ color: var(--mw-hero-text-color);
49
+
50
+ // backdrop-filter and not an overlay - see $hero-filter-dark. On the pseudo
51
+ // element, so the content in front of the image stays unfiltered.
52
+ &::before {
53
+ content: '';
54
+ position: absolute;
55
+ inset: 0;
56
+ backdrop-filter: var(--mw-hero-image-filter);
57
+ z-index: z-index('lift');
58
+ pointer-events: none;
59
+ transition: var(--mw-transition);
60
+ }
61
+
62
+ // ...so the content has to sit above it - except the parallax layer,
63
+ // which is the one thing that belongs underneath
64
+ > *:not(.mw-parallax-media) {
39
65
  position: relative;
40
- width: 100%;
41
- min-height: 100dvh;
42
- display: flex;
43
- align-items: center;
44
- justify-content: center;
45
- background-size: cover;
46
- background-position: center;
47
- background-image: var(--mw-hero-background);
66
+ z-index: z-index('card');
67
+ }
48
68
 
49
- // Set on the container so a hero without .mw-home gets it too
50
- color: var(--mw-hero-text-color);
69
+ // Add `mw-parallax` here and a `.mw-parallax-media` child and the picture
70
+ // becomes its own layer, free to drift or to stay pinned. It reads the
71
+ // same token, so the image is still configured in one place - and a
72
+ // second media element carrying `mw-parallax-pattern` takes the other.
73
+ &:has(> .mw-parallax-media) {
74
+ background-image: none;
51
75
 
52
- // backdrop-filter and not an overlay - see $hero-filter-dark. On the pseudo
53
- // element, so the content in front of the image stays unfiltered.
54
- &::before {
55
- content: '';
56
- position: absolute;
57
- inset: 0;
58
- backdrop-filter: var(--mw-hero-image-filter);
59
- z-index: z-index('lift');
60
- pointer-events: none;
61
- transition: var(--mw-transition);
62
- }
76
+ --mw-parallax-pattern-image: var(--mw-hero-pattern);
63
77
 
64
- // ...so the content has to sit above it - except the parallax layer,
65
- // which is the one thing that belongs underneath
66
- > *:not(.mw-parallax-media) {
67
- position: relative;
68
- z-index: z-index('card');
78
+ > .mw-parallax-media:not(.mw-parallax-pattern) {
79
+ background-image: var(--mw-hero-background);
69
80
  }
81
+ }
70
82
 
71
- // Add `mw-parallax` here and a `.mw-parallax-media` child and the picture
72
- // becomes its own layer, free to drift or to stay pinned. It reads the
73
- // same token, so the image is still configured in one place - and a
74
- // second media element carrying `mw-parallax-pattern` takes the other.
75
- &:has(> .mw-parallax-media) {
76
- background-image: none;
77
-
78
- --mw-parallax-pattern-image: var(--mw-hero-pattern);
83
+ // The same shadow the scroll cue already carries: the light theme lifts
84
+ // the photo to brightness(1.1) and pale ink alone stops holding its edge
85
+ h1,
86
+ h2,
87
+ h3,
88
+ p {
89
+ text-shadow: 0 1px 4px rgb(0 0 0 / 65%);
90
+ }
79
91
 
80
- > .mw-parallax-media:not(.mw-parallax-pattern) {
81
- background-image: var(--mw-hero-background);
82
- }
83
- }
92
+ // ...except anything that paints its own surface: a card in the hero is
93
+ // still a card, or its text is light on a light card in the light theme
94
+ .mw-card,
95
+ .mw-panel,
96
+ .mw-modal,
97
+ .mw-tile,
98
+ .mw-calendar:not(.mw-calendar-plain) {
99
+ color: var(--mw-text-color);
84
100
 
85
- // The same shadow the scroll cue already carries: the light theme lifts
86
- // the photo to brightness(1.1) and pale ink alone stops holding its edge
87
101
  h1,
88
102
  h2,
89
103
  h3,
90
104
  p {
91
- text-shadow: 0 1px 4px rgb(0 0 0 / 65%);
105
+ text-shadow: none;
92
106
  }
107
+ }
93
108
 
94
- // ...except anything that paints its own surface: a card in the hero is
95
- // still a card, or its text is light on a light card in the light theme
96
- .mw-card,
97
- .mw-panel,
98
- .mw-modal,
99
- .mw-tile,
100
- .mw-calendar:not(.mw-calendar-plain) {
101
- color: var(--mw-text-color);
109
+ // ...and except the outline button, for the opposite reason: its tone is
110
+ // picked to read on the page background and goes quiet over a photograph.
111
+ // The hover is restated because this selector is exactly as specific as
112
+ // `.mw-btn-outline:hover:not(:disabled)` and the layer order beats it.
113
+ .mw-btn-outline {
114
+ border-color: color-mix(
115
+ in srgb,
116
+ var(--mw-hero-text-color) 45%,
117
+ transparent
118
+ );
119
+ background: rgb(0 0 0 / 30%);
120
+ backdrop-filter: blur(6px);
121
+ color: var(--mw-hero-text-color);
102
122
 
103
- h1,
104
- h2,
105
- h3,
106
- p {
107
- text-shadow: none;
108
- }
123
+ &:hover:not(:disabled) {
124
+ background: var(--mw-primary-color);
125
+ border-color: var(--mw-primary-color);
126
+ color: var(--mw-primary-accent-text-color);
109
127
  }
128
+ }
110
129
 
111
- // ...and except the outline button, for the opposite reason: its tone is
112
- // picked to read on the page background and goes quiet over a photograph.
113
- // The hover is restated because this selector is exactly as specific as
114
- // `.mw-btn-outline:hover:not(:disabled)` and the layer order beats it.
115
- .mw-btn-outline {
116
- border-color: color-mix(
117
- in srgb,
118
- var(--mw-hero-text-color) 45%,
119
- transparent
120
- );
121
- background: rgb(0 0 0 / 30%);
122
- backdrop-filter: blur(6px);
123
- color: var(--mw-hero-text-color);
130
+ // The bar floats over the hero, so centring against the full viewport hides
131
+ // a badge or a kicker underneath it. Guarded - no header, no offset.
132
+ body:has(.mw-header) & {
133
+ padding-top: calc(var(--mw-header-height) + #{spacing('7')});
134
+ }
124
135
 
125
- &:hover:not(:disabled) {
126
- background: var(--mw-primary-color);
127
- border-color: var(--mw-primary-color);
128
- color: var(--mw-primary-accent-text-color);
129
- }
130
- }
136
+ // The cue is out of flow, so buttons under the hero text would park on top
137
+ // of it. Only when there is one, or every hero pays for a feature it lacks.
138
+ &:has(> .mw-scroll-hint) {
139
+ padding-bottom: calc(
140
+ var(--mw-scroll-hint-offset) + var(--mw-scroll-hint-reserve)
141
+ );
142
+ }
131
143
 
132
- // The bar floats over the hero, so centring against the full viewport hides
133
- // a badge or a kicker underneath it. Guarded - no header, no offset.
134
- body:has(.mw-header) & {
135
- padding-top: calc(var(--mw-header-height) + #{spacing('7')});
136
- }
144
+ // ...except in the corner: the end variant stands beside the content
145
+ &:has(> .mw-scroll-hint-end) {
146
+ padding-bottom: spacing('0');
147
+ }
137
148
 
138
- // The cue is out of flow, so buttons under the hero text would park on top
139
- // of it. Only when there is one, or every hero pays for a feature it lacks.
149
+ // On a phone the corner is tight too, so both variants get the strip back.
150
+ // Last of the three: a media query adds no specificity and would lose to
151
+ // the end rule above. Smaller than on the desktop, or the full reserve
152
+ // tips the container past 100dvh and pushes the cue off screen.
153
+ @include media-down('md') {
140
154
  &:has(> .mw-scroll-hint) {
155
+ --mw-scroll-hint-reserve: #{spacing('6')};
156
+
141
157
  padding-bottom: calc(
142
158
  var(--mw-scroll-hint-offset) + var(--mw-scroll-hint-reserve)
143
159
  );
144
160
  }
145
-
146
- // ...except in the corner: the end variant stands beside the content
147
- &:has(> .mw-scroll-hint-end) {
148
- padding-bottom: spacing('0');
149
- }
150
-
151
- // On a phone the corner is tight too, so both variants get the strip back.
152
- // Last of the three: a media query adds no specificity and would lose to
153
- // the end rule above. Smaller than on the desktop, or the full reserve
154
- // tips the container past 100dvh and pushes the cue off screen.
155
- @include media-down('md') {
156
- &:has(> .mw-scroll-hint) {
157
- --mw-scroll-hint-reserve: #{spacing('6')};
158
-
159
- padding-bottom: calc(
160
- var(--mw-scroll-hint-offset) + var(--mw-scroll-hint-reserve)
161
- );
162
- }
163
- }
164
161
  }
165
162
  }
166
163
  }
167
164
 
168
- // Glass rather than a fill: on a hero image a solid badge is the loudest thing
169
- // on the screen and plain text sinks into the picture.
170
- .mw-hero-badge {
171
- display: inline-flex;
172
- align-items: center;
173
- justify-content: center;
174
- gap: spacing('3');
175
- max-width: 100%;
176
- margin-bottom: spacing('6');
177
- padding: spacing('2') spacing('5');
178
- border: 1px solid #{fade('hero-text-color', 22%)};
179
- border-radius: 999px;
180
- // A vertical ramp and not a flat tint: glass is lit from above, and the
181
- // gradient is what gives the pill a thickness instead of a fogged panel
182
- background: linear-gradient(
183
- to bottom,
184
- #{fade('hero-text-color', 16%)},
185
- #{fade('hero-text-color', 5%)}
186
- );
187
- // saturate() is what separates glass from frosted plastic: the blur alone
188
- // greys out what shows through, and pushing the chroma back up is how a
189
- // real lens reads. The two inset lines are the specular highlight - light
190
- // catching the top edge, a fainter bounce off the bottom - and the outer
191
- // shadow is what lifts the pill off the photo instead of pasting it on.
192
- backdrop-filter: blur(14px) saturate(180%);
193
- box-shadow:
194
- inset 0 1px 0 #{fade('hero-text-color', 40%)},
195
- inset 0 -1px 0 #{fade('hero-text-color', 12%)},
196
- 0 2px 12px rgb(0 0 0 / 18%);
197
- color: fade('hero-text-color', 88%);
198
- font-size: font-size('sm');
199
- font-weight: font-weight('medium');
200
- letter-spacing: 0.02em;
201
- }
202
-
203
- // Carries the secondary colour, because the badge is a state, not a label -
204
- // override `background` for a different one
205
- .mw-hero-badge-dot {
206
- width: 8px;
207
- height: 8px;
208
- flex-shrink: 0;
209
- border-radius: radius('full');
210
- background: var(--mw-secondary-color);
211
- }
212
-
213
165
  // Belongs to the hero container, not to the flow below it, so the cue sits on
214
166
  // the lower edge whatever the content does. Beats the `> *` rule above it.
215
167
  .mw-container:has(.mw-hero) > .mw-scroll-hint {
@@ -281,193 +233,4 @@
281
233
  flex-direction: column;
282
234
  justify-content: center;
283
235
  }
284
-
285
- // A pseudo element on the trigger, so an ancestor with `overflow: hidden` cuts
286
- // it off. Hidden means `display: none`: a laid-out box still counts towards the
287
- // page's overflow, and a 280px bubble near the right edge added 50px of sideways
288
- // scroll on a phone. Hence `allow-discrete` and the starting style.
289
- $_mw-tooltip-arrow: 6px;
290
- $_mw-tooltip-gap: 8px;
291
- // A square turned 45deg rather than a border triangle: it reads the same from
292
- // above and from below, so the second placement costs no second arrow and a
293
- // flip never has to repaint anything. The diagonal carries the visible width,
294
- // so the side is the visible height doubled over root two.
295
- $_mw-tooltip-arrow-side: math.div($_mw-tooltip-arrow * 2, math.sqrt(2));
296
- // Half a side short of where the triangle's edge sat, so the turned square is
297
- // centred on the bubble's edge and only its outer half shows. The inner half
298
- // lands on the bubble in the same colour, which is what makes it read as one
299
- // shape rather than a diamond stuck to a box.
300
- $_mw-tooltip-arrow-inset: $_mw-tooltip-gap +
301
- $_mw-tooltip-arrow - math.div($_mw-tooltip-arrow-side, 2);
302
-
303
- [data-tooltip] {
304
- position: relative;
305
-
306
- &::before {
307
- content: attr(data-tooltip);
308
- position: absolute;
309
- bottom: 100%;
310
- left: 50%;
311
- transform: translateX(-50%);
312
- margin-bottom: $_mw-tooltip-gap + $_mw-tooltip-arrow;
313
- max-width: min(280px, 90vw);
314
- width: max-content;
315
- padding: spacing('1') spacing('2');
316
- background: var(--mw-gray-color);
317
- color: $fixed-surface-text;
318
- font-size: font-size('xs');
319
- border-radius: radius('xs');
320
- box-shadow: var(--mw-elevation-2);
321
- pointer-events: none;
322
- opacity: 0;
323
- display: none;
324
- transition:
325
- opacity var(--mw-duration-fast) var(--mw-ease-out),
326
- display var(--mw-duration-fast) allow-discrete;
327
- z-index: z-index('tooltip');
328
- }
329
-
330
- &::after {
331
- content: '';
332
- position: absolute;
333
- bottom: 100%;
334
- left: 50%;
335
- transform: translateX(-50%) rotate(45deg);
336
- margin-bottom: $_mw-tooltip-arrow-inset;
337
- width: $_mw-tooltip-arrow-side;
338
- height: $_mw-tooltip-arrow-side;
339
- background: var(--mw-gray-color);
340
- opacity: 0;
341
- display: none;
342
- transition:
343
- opacity var(--mw-duration-fast) var(--mw-ease-out),
344
- display var(--mw-duration-fast) allow-discrete;
345
- z-index: z-index('tooltip');
346
- }
347
-
348
- // The placement API: the text is the attribute, the side is a class. The
349
- // three steer different properties - block axis, inline axis - so `below`
350
- // and `end` compose without a rule for the pairing.
351
- //
352
- // They place the bubble against the trigger rather than joining the anchored
353
- // path below, and that is the point, not a shortcut. The reason to want them
354
- // is a control in a fixed header, and that header carries a backdrop-filter,
355
- // which makes it the containing block for anything `fixed` inside it.
356
- // `block-end` is then the few pixels between the trigger and the header's own
357
- // bottom edge, the bubble does not fit, and it gets pushed back over the
358
- // trigger it belongs to. What this gives up is the clipping immunity the
359
- // anchored path buys - a modifier inside a scroll container is cut off.
360
- //
361
- // Opting out has to restore the placement, not just the positioning scheme:
362
- // the anchored path below is the later rule and clears `bottom`, `top`,
363
- // `left` and the centring transform so `position-area` can resolve them
364
- // itself. Leave that cleared and the box falls to its static position, which
365
- // for a pseudo element is inside the trigger it is supposed to sit above.
366
- // The three rules under this one then move the one edge each of them owns.
367
- &:is(.mw-tooltip-below, .mw-tooltip-end, .mw-tooltip-start) {
368
- &::before,
369
- &::after {
370
- position: absolute;
371
- position-area: none;
372
- bottom: 100%;
373
- top: auto;
374
- left: 50%;
375
- }
376
-
377
- &::before {
378
- transform: translateX(-50%);
379
- }
380
-
381
- &::after {
382
- transform: translateX(-50%) rotate(45deg);
383
- }
384
- }
385
-
386
- &.mw-tooltip-below {
387
- &::before {
388
- top: 100%;
389
- bottom: auto;
390
- margin-top: $_mw-tooltip-gap + $_mw-tooltip-arrow;
391
- margin-bottom: 0;
392
- }
393
-
394
- &::after {
395
- top: 100%;
396
- bottom: auto;
397
- margin-top: $_mw-tooltip-arrow-inset;
398
- margin-bottom: 0;
399
- }
400
- }
401
-
402
- // Only the bubble moves - the arrow stays centred on the trigger it points at.
403
- &.mw-tooltip-end::before {
404
- left: auto;
405
- right: 0;
406
- transform: none;
407
- }
408
-
409
- &.mw-tooltip-start::before {
410
- left: 0;
411
- right: auto;
412
- transform: none;
413
- }
414
-
415
- &:hover::before,
416
- &:hover::after,
417
- &:focus-visible::before,
418
- &:focus-visible::after {
419
- opacity: 1;
420
- display: block;
421
-
422
- @starting-style {
423
- opacity: 0;
424
- }
425
- }
426
-
427
- // A press usually scales the trigger, and a transform makes it a containing
428
- // block for the fixed-position bubble above - which would drop the label
429
- // into the element being pressed. So it steps aside for the click instead;
430
- // by then it has been read anyway. After the hover rule, same weight.
431
- &:active::before,
432
- &:active::after {
433
- opacity: 0;
434
- display: none;
435
- // Not transitioned: `allow-discrete` holds `display: block` until the fade
436
- // ends, and the bubble would spend those 180ms visibly sitting in the
437
- // element. Going back out on release keeps the fade - that one is wanted.
438
- transition: none;
439
- }
440
-
441
- // The clipping caveat above, lifted where the browser can do it: `fixed` takes
442
- // the bubble out of the trigger's containing block, anchor positioning keeps
443
- // it attached. No `anchor-name` needed - a pseudo element is implicitly
444
- // anchored to the element it comes from.
445
- //
446
- // Still no `position-try-fallbacks: flip-block`, and no longer because of the
447
- // arrow - the turned square survives a flip. Bubble and arrow are two
448
- // separately anchored boxes, so each would decide on its own whether it fits,
449
- // and the tiny one nearly always does: the label flips, the arrow stays.
450
- @supports (position-area: block-start) {
451
- &::before,
452
- &::after {
453
- position: fixed;
454
- // The absolute placement these replace - left as-is they fight the
455
- // position-area, which resolves the inset properties itself.
456
- bottom: auto;
457
- left: auto;
458
- top: auto;
459
- }
460
-
461
- &::before {
462
- position-area: block-start;
463
- transform: none;
464
- }
465
-
466
- // The turn stays - it is the arrow's shape, not its placement.
467
- &::after {
468
- position-area: block-start;
469
- transform: rotate(45deg);
470
- }
471
- }
472
- }
473
236
  }
@@ -24,7 +24,7 @@
24
24
  position: relative;
25
25
  display: flex;
26
26
  align-items: center;
27
- min-height: 60vh;
27
+ min-height: 60dvh;
28
28
  // `clip` and never `hidden`: hidden makes this a scroll container, and the
29
29
  // view() timeline below would then resolve against this box instead of the
30
30
  // viewport - the layer keeps its overhang and never moves
@@ -1,6 +1,9 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
3
  @layer mw.layout {
4
+ // Every multiplier below is a count of `--mw-section-head-step` (_base.scss),
5
+ // which is where the rhythm is set and where changing it belongs.
6
+ //
4
7
  // The head of a section: a rule, a title, and the line that says what the
5
8
  // section is. All three left-aligned, and that is the whole point of the
6
9
  // component. Centred running text moves its own left edge with every line
@@ -49,12 +52,12 @@
49
52
  text-wrap: balance;
50
53
  }
51
54
 
52
- // Two limits, and the narrower one wins: 65 characters is where a line stops
55
+ // Two limits, and the narrower one wins: 70 characters is where a line stops
53
56
  // being comfortable, four fifths is what keeps the paragraph from filling the
54
57
  // container edge to edge on a wide screen. Below md only the character count
55
58
  // is left - four fifths of a phone is a column nothing fits in.
56
59
  .mw-section-head-intro {
57
- max-width: 75ch;
60
+ max-width: 70ch;
58
61
  margin: calc(var(--mw-section-head-step) * 0.56) spacing('0') spacing('0');
59
62
  font-size: font-size('md');
60
63
  line-height: 1.6;
@@ -86,13 +89,13 @@
86
89
  content: counter(mw-section-head, decimal-leading-zero);
87
90
  position: absolute;
88
91
  top: var(--mw-section-head-pad);
89
- right: 7px;
92
+ // The tracking sits after the last digit too, so it comes back off the
93
+ // right - in em, or it stops matching the moment the size changes
94
+ right: -0.2em;
90
95
  color: var(--mw-text-muted-color);
91
96
  font-size: font-size('sm');
92
97
  font-weight: font-weight('medium');
93
98
  line-height: 1;
94
- // The tracking sits after the last digit too, so it has to come back off
95
- // the right or the number stands a space short of the edge it aligns to.
96
99
  letter-spacing: 0.2em;
97
100
  }
98
101
 
@@ -237,7 +240,7 @@
237
240
  // between two demos - the heading leans toward what it names, not toward
238
241
  // what came before it.
239
242
  .mw-section-subtitle {
240
- margin-top: calc(var(--mw-section-head-step) * 1.49);
243
+ margin-top: calc(var(--mw-section-head-step) * 1.5);
241
244
  }
242
245
  }
243
246
 
@@ -249,22 +252,26 @@
249
252
  // padding pushes exactly that much of it past the bottom of the screen -
250
253
  // and with it the scroll cue sitting on its lower edge. A hero is the full
251
254
  // screen by definition and brings its own breathing room.
252
- @supports (selector(:has(.mw-hero))) {
253
- &:has(.mw-hero) {
254
- padding-block: spacing('0');
255
- }
255
+ &:has(.mw-hero) {
256
+ padding-block: spacing('0');
256
257
  }
257
258
  }
258
259
 
259
260
  .mw-section-alternate {
260
261
  position: relative;
261
- --s: 5px;
262
- --c1: var(--mw-page-background);
263
- --c2: var(--mw-section-alternate-pattern);
264
- --g: var(--c2) -5% 5%, var(--c1) 0 45%;
265
262
  background:
266
- linear-gradient(0deg, #0000 10%, var(--c1) 0 40%, #0000 0),
267
- repeating-linear-gradient(-45deg, var(--g)) var(--c1);
268
- background-size: var(--s) var(--s);
263
+ linear-gradient(
264
+ 0deg,
265
+ transparent 10%,
266
+ var(--mw-page-background) 0 40%,
267
+ transparent 0
268
+ ),
269
+ repeating-linear-gradient(
270
+ -45deg,
271
+ var(--mw-section-alternate-pattern) -5% 5%,
272
+ var(--mw-page-background) 0 45%
273
+ )
274
+ var(--mw-page-background);
275
+ background-size: 5px 5px;
269
276
  }
270
277
  }