maverick-wave 5.38.0 → 6.0.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.
Files changed (69) hide show
  1. package/README.md +63 -47
  2. package/maverick-wave.min.css +94 -68
  3. package/maverick-wave.min.js +1 -1
  4. package/package.json +7 -7
  5. package/src/js/main.js +382 -57
  6. package/src/scss/abstracts/_mixins.scss +35 -24
  7. package/src/scss/abstracts/_variables.scss +15 -0
  8. package/src/scss/base/_base.scss +107 -64
  9. package/src/scss/base/_index.scss +1 -0
  10. package/src/scss/base/_page-transitions.scss +29 -0
  11. package/src/scss/base/_reset.scss +0 -8
  12. package/src/scss/components/_accordions.scss +23 -18
  13. package/src/scss/components/_action-bar.scss +143 -0
  14. package/src/scss/components/_alerts.scss +13 -1
  15. package/src/scss/components/_avatars.scss +17 -0
  16. package/src/scss/components/_badge.scss +12 -0
  17. package/src/scss/components/_breadcrumbs.scss +10 -2
  18. package/src/scss/components/_chat.scss +250 -0
  19. package/src/scss/components/_code.scss +3 -1
  20. package/src/scss/components/_compare.scss +135 -0
  21. package/src/scss/components/_deck.scss +8 -14
  22. package/src/scss/components/_devices.scss +323 -0
  23. package/src/scss/components/_dropdown.scss +43 -2
  24. package/src/scss/components/_empty-state.scss +43 -0
  25. package/src/scss/components/_flag.scss +16 -0
  26. package/src/scss/components/_index.scss +6 -0
  27. package/src/scss/components/_lightbox.scss +1 -1
  28. package/src/scss/components/_marquee.scss +132 -0
  29. package/src/scss/components/_modals.scss +112 -83
  30. package/src/scss/components/_occasions.scss +62 -11
  31. package/src/scss/components/_pagination.scss +91 -0
  32. package/src/scss/components/_portrait-gallery.scss +1 -1
  33. package/src/scss/components/_progress.scss +122 -2
  34. package/src/scss/components/_segmented.scss +65 -2
  35. package/src/scss/components/_skeleton.scss +8 -0
  36. package/src/scss/components/_spinners.scss +38 -0
  37. package/src/scss/components/_stat.scss +232 -0
  38. package/src/scss/components/_stepper.scss +42 -2
  39. package/src/scss/components/_stories.scss +36 -22
  40. package/src/scss/components/_tables.scss +160 -17
  41. package/src/scss/components/_tabs.scss +42 -4
  42. package/src/scss/components/_tags.scss +39 -4
  43. package/src/scss/components/_tiles.scss +1 -1
  44. package/src/scss/components/_timelines.scss +138 -1
  45. package/src/scss/components/_toasts.scss +5 -1
  46. package/src/scss/components/_tooltip.scss +13 -0
  47. package/src/scss/form-elements/_checkbox.scss +40 -7
  48. package/src/scss/form-elements/_form.scss +2 -4
  49. package/src/scss/form-elements/_input.scss +9 -6
  50. package/src/scss/form-elements/_radio.scss +32 -3
  51. package/src/scss/form-elements/_select.scss +44 -1
  52. package/src/scss/form-elements/_slider.scss +18 -0
  53. package/src/scss/form-elements/_textarea.scss +9 -6
  54. package/src/scss/form-elements/_toggle.scss +37 -4
  55. package/src/scss/layout/_app-shell.scss +661 -0
  56. package/src/scss/layout/_bento.scss +43 -0
  57. package/src/scss/layout/_footer.scss +114 -2
  58. package/src/scss/layout/_grid.scss +7 -2
  59. package/src/scss/layout/_header-reveal.scss +30 -17
  60. package/src/scss/layout/_header.scss +21 -4
  61. package/src/scss/layout/_index.scss +2 -0
  62. package/src/scss/layout/_parallax.scss +13 -11
  63. package/src/scss/utilities/_accessibility.scss +4 -19
  64. package/src/scss/utilities/_index.scss +1 -0
  65. package/src/scss/utilities/_print.scss +7 -3
  66. package/src/scss/utilities/_reveal.scss +25 -17
  67. package/src/scss/utilities/_shape.scss +24 -0
  68. package/src/scss/utilities/_touch-targets.scss +23 -0
  69. package/src/scss/utilities/_variants.scss +25 -6
@@ -5,7 +5,8 @@
5
5
  @layer mw.layout {
6
6
  .mw-footer {
7
7
  background: var(--mw-footer-background);
8
- padding: spacing('7') spacing('0');
8
+ padding: spacing('7') spacing('0')
9
+ calc(#{spacing('7')} + env(safe-area-inset-bottom, 0px));
9
10
  border-top: 1px solid var(--mw-border-accent);
10
11
 
11
12
  // The same ramp .mw-grid-4 rides, read from the one place it is written
@@ -15,10 +16,21 @@
15
16
  margin-bottom: spacing('6');
16
17
 
17
18
  @include grid.column-grid(map.get(grid.$column-steps, 4));
19
+
20
+ // Two or three columns fill the row instead of leaving a slot empty
21
+ @include media-up('lg') {
22
+ &:has(> :nth-child(2):last-child) {
23
+ grid-template-columns: repeat(2, minmax(0, 1fr));
24
+ }
25
+
26
+ &:has(> :nth-child(3):last-child) {
27
+ grid-template-columns: repeat(3, minmax(0, 1fr));
28
+ }
29
+ }
18
30
  }
19
31
 
20
32
  &-column {
21
- h3 {
33
+ :is(h2, h3, h4, .mw-footer-title) {
22
34
  font-size: font-size('md');
23
35
  margin-bottom: spacing('3');
24
36
  color: var(--mw-primary-text-color);
@@ -51,6 +63,7 @@
51
63
  color: var(--mw-text-muted-color);
52
64
  text-decoration: none;
53
65
  padding-bottom: 2px;
66
+ @include focus-ring;
54
67
 
55
68
  // Always drawn, unlike the navbar's: a footer link sits in a list of
56
69
  // them and needs to read as a link without being pointed at first
@@ -108,6 +121,7 @@
108
121
  border: 1px solid var(--mw-border-accent);
109
122
  box-shadow: var(--mw-elevation-3);
110
123
  text-decoration: none;
124
+ @include focus-ring;
111
125
 
112
126
  &:hover {
113
127
  background: var(--mw-primary-color);
@@ -168,6 +182,78 @@
168
182
  }
169
183
  }
170
184
 
185
+ // Imprint, privacy, cookie settings - the links a German site must carry
186
+ .mw-footer-links {
187
+ display: flex;
188
+ flex-wrap: wrap;
189
+ justify-content: center;
190
+ gap: spacing('1') spacing('5');
191
+ margin: 0;
192
+ padding: 0;
193
+ list-style: none;
194
+ font-size: font-size('xs');
195
+
196
+ @include media-up('sm') {
197
+ font-size: font-size('sm');
198
+ }
199
+
200
+ a {
201
+ color: var(--mw-text-muted-color);
202
+ text-decoration: underline;
203
+ text-decoration-color: color-mix(
204
+ in srgb,
205
+ currentColor 40%,
206
+ transparent
207
+ );
208
+ text-underline-offset: 0.25em;
209
+ @include focus-ring;
210
+
211
+ &:hover {
212
+ color: var(--mw-primary-text-color);
213
+ text-decoration-color: currentColor;
214
+ }
215
+ }
216
+ }
217
+
218
+ // Copyright at one end, the links at the other; stacked and centred on a phone
219
+ .mw-footer-bottom {
220
+ display: flex;
221
+ flex-direction: column;
222
+ align-items: center;
223
+ gap: spacing('3');
224
+ margin-top: spacing('5');
225
+ padding-top: spacing('4');
226
+ border-top: 1px solid var(--mw-border-accent);
227
+ text-align: center;
228
+
229
+ // Also inside a wrapper that stacks the copyright over the date
230
+ :is(.mw-copyright, .mw-last-updated) {
231
+ width: auto;
232
+ margin: 0;
233
+ padding: 0;
234
+ border: 0;
235
+ text-align: inherit;
236
+ }
237
+
238
+ .mw-copyright + .mw-last-updated {
239
+ margin-top: spacing('1');
240
+ }
241
+
242
+ // Nothing above it to set off
243
+ &:first-child {
244
+ margin-top: 0;
245
+ padding-top: 0;
246
+ border-top: 0;
247
+ }
248
+
249
+ @include media-up('md') {
250
+ flex-direction: row;
251
+ flex-wrap: wrap;
252
+ justify-content: space-between;
253
+ text-align: start;
254
+ }
255
+ }
256
+
171
257
  .mw-last-updated {
172
258
  text-align: center;
173
259
  color: var(--mw-text-muted-color);
@@ -180,4 +266,30 @@
180
266
  }
181
267
  }
182
268
  }
269
+
270
+ // An application's footer: the bottom bar alone, under the content
271
+ .mw-footer-compact {
272
+ padding-block: spacing('4')
273
+ calc(#{spacing('4')} + env(safe-area-inset-bottom, 0px));
274
+
275
+ .mw-footer-bottom {
276
+ margin-top: 0;
277
+ padding-top: 0;
278
+ border-top: 0;
279
+ }
280
+ }
281
+
282
+ // The header bar's colours, so it bookends the page in either theme
283
+ .mw-footer-dark {
284
+ --mw-footer-background: var(--mw-header-background);
285
+ --mw-text-color: var(--mw-header-text-color);
286
+ --mw-text-muted-color: var(--mw-header-navbar-list-color);
287
+ --mw-primary-text-color: var(--mw-header-navbar-list-active-color);
288
+ --mw-card-background: var(--mw-dark-card-background);
289
+ --mw-border-accent: var(--mw-header-border);
290
+ --mw-focus-ring-color: var(--mw-header-navbar-list-active-color);
291
+
292
+ color: var(--mw-text-color);
293
+ color-scheme: dark;
294
+ }
183
295
  }
@@ -350,9 +350,12 @@ $_grid-from: ('sm', 'md', 'lg', 'xl');
350
350
  @layer mw.utilities {
351
351
  // The `:nth-child(n)` reset carries the same specificity as the cycle rules,
352
352
  // so a narrower breakpoint can override the wider one's cycle for every child.
353
+ // Behind motion-safe too, or the cycle's :root guard outweighs these
353
354
  @mixin _reveal-stagger($cols) {
354
355
  &.mw-reveal-stagger > :nth-child(n) {
355
- @include reveal-range(1);
356
+ @include motion-safe {
357
+ @include reveal-range(1);
358
+ }
356
359
  }
357
360
 
358
361
  // Guarded: Sass counts `from 2 through 1` downwards and would emit a rule
@@ -360,7 +363,9 @@ $_grid-from: ('sm', 'md', 'lg', 'xl');
360
363
  @if $cols > 1 {
361
364
  @for $i from 2 through $cols {
362
365
  &.mw-reveal-stagger > :nth-child(#{$cols}n + #{$i}) {
363
- @include reveal-range($i);
366
+ @include motion-safe {
367
+ @include reveal-range($i);
368
+ }
364
369
  }
365
370
  }
366
371
  }
@@ -1,3 +1,5 @@
1
+ @use '../abstracts' as *;
2
+
1
3
  // Carried by the float and not by the class, or the ring would still be there in
2
4
  // the arrived bar. Fixed values and not the elevation ramp: this lands on a
3
5
  // picture, not on a theme surface, and the light theme cuts its shadows for paper.
@@ -9,35 +11,40 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
9
11
  // shorthand, which a consumer bundler folds together with a timeline into a
10
12
  // declaration no browser accepts.
11
13
  @supports (animation-timeline: scroll()) {
12
- @media (prefers-reduced-motion: no-preference) {
13
- .mw-header-reveal {
14
+ .mw-header-reveal {
15
+ @include motion-safe {
14
16
  animation-name: mw-header-reveal;
15
17
  animation-timing-function: var(--mw-ease-out);
16
18
  animation-fill-mode: both;
17
19
  animation-timeline: scroll(root);
18
20
  animation-range: 0 420px;
19
21
  }
22
+ }
20
23
 
21
- // The bar travels, a keep element travels the same distance back and stays
22
- // put - for a burger that has to be reachable while the hero is untouched.
23
- .mw-header-reveal .mw-header-keep {
24
+ // The bar travels, a keep element travels the same distance back and stays
25
+ // put - for a burger that has to be reachable while the hero is untouched.
26
+ .mw-header-reveal .mw-header-keep {
27
+ @include motion-safe {
24
28
  animation-name: mw-header-keep;
25
29
  animation-timing-function: var(--mw-ease-out);
26
30
  animation-fill-mode: both;
27
31
  animation-timeline: scroll(root);
28
32
  animation-range: 0 420px;
29
33
  }
34
+ }
30
35
 
31
- // A group's opacity cannot be taken back by a child, so the fade goes as
32
- // soon as something is meant to stay visible through it.
33
- .mw-header-reveal:has(.mw-header-keep) {
36
+ // A group's opacity cannot be taken back by a child, so the fade goes as
37
+ // soon as something is meant to stay visible through it.
38
+ .mw-header-reveal:has(.mw-header-keep) {
39
+ @include motion-safe {
34
40
  animation-name: mw-header-slide;
35
41
  }
42
+ }
36
43
 
37
- // Glued under the bar, so it leaves and arrives with it. No class of its
38
- // own: a ribbon under a revealing header has no other sensible behaviour.
39
- body:has(.mw-header-reveal)
40
- .mw-announcement:not(.mw-announcement-static) {
44
+ // Glued under the bar, so it leaves and arrives with it. No class of its
45
+ // own: a ribbon under a revealing header has no other sensible behaviour.
46
+ body:has(.mw-header-reveal) .mw-announcement:not(.mw-announcement-static) {
47
+ @include motion-safe {
41
48
  animation-name: mw-announcement-reveal;
42
49
  animation-timing-function: var(--mw-ease-out);
43
50
  animation-fill-mode: both;
@@ -115,8 +122,8 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
115
122
  // No scroll timelines: the script toggles these classes instead (main.js).
116
123
  // State and transition are separate because the script sets the state a frame
117
124
  // earlier - together the bar would slide away in front of the reader.
118
- @media (prefers-reduced-motion: no-preference) {
119
- .mw-header-reveal.mw-header-away {
125
+ .mw-header-reveal.mw-header-away {
126
+ @include motion-safe {
120
127
  top: calc(-1 * var(--mw-header-reveal-offset));
121
128
  opacity: 0;
122
129
 
@@ -129,8 +136,10 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
129
136
  box-shadow: $float-edge;
130
137
  }
131
138
  }
139
+ }
132
140
 
133
- .mw-header-driven {
141
+ .mw-header-driven {
142
+ @include motion-safe {
134
143
  // The shared list plus `top`: writing only `top` drops the bar's own.
135
144
  transition:
136
145
  var(--mw-transition),
@@ -142,15 +151,19 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
142
151
  box-shadow var(--mw-duration-slow) var(--mw-ease-out);
143
152
  }
144
153
  }
154
+ }
145
155
 
146
- .mw-announcement.mw-announcement-away {
156
+ .mw-announcement.mw-announcement-away {
157
+ @include motion-safe {
147
158
  transform: translateY(
148
159
  calc(-1 * (var(--mw-header-height) + var(--mw-announcement-height)))
149
160
  );
150
161
  opacity: 0;
151
162
  }
163
+ }
152
164
 
153
- .mw-announcement-driven {
165
+ .mw-announcement-driven {
166
+ @include motion-safe {
154
167
  transition:
155
168
  transform var(--mw-duration-slow) var(--mw-ease-out),
156
169
  opacity var(--mw-duration-slow) var(--mw-ease-out);
@@ -10,6 +10,8 @@ $_control-size: 40px;
10
10
  left: spacing('0');
11
11
  width: 100%;
12
12
  z-index: z-index('header');
13
+ // Dark in both themes, fixed light included, where no light-dark() flips
14
+ --mw-focus-ring-color: var(--mw-header-navbar-list-active-color);
13
15
  background: var(--mw-header-background);
14
16
  backdrop-filter: blur(7px);
15
17
  border-bottom: 1px solid var(--mw-header-border);
@@ -338,7 +340,7 @@ $_control-size: 40px;
338
340
  }
339
341
 
340
342
  // Last, so the current page keeps its look while being hovered
341
- @include active() {
343
+ &:is(.mw-active, .active, [aria-current='page']) {
342
344
  color: var(--mw-header-navbar-list-active-color);
343
345
  background: var(--mw-primary-background);
344
346
  // Marks the left edge of a stacked list. In a row the links are pills
@@ -369,7 +371,7 @@ $_control-size: 40px;
369
371
  }
370
372
  }
371
373
 
372
- @media (prefers-reduced-motion: reduce) {
374
+ @include motion-reduced {
373
375
  transition-duration: 0.01ms;
374
376
 
375
377
  .mw-navbar-item {
@@ -470,13 +472,15 @@ $_control-size: 40px;
470
472
 
471
473
  &:hover,
472
474
  &.mw-active,
473
- &.active {
475
+ &.active,
476
+ &[aria-current='page'] {
474
477
  color: var(--mw-header-navbar-list-active-color);
475
478
  }
476
479
 
477
480
  &:hover::after,
478
481
  &.mw-active::after,
479
- &.active::after {
482
+ &.active::after,
483
+ &[aria-current='page']::after {
480
484
  transform: scaleX(1);
481
485
  opacity: 1;
482
486
  }
@@ -573,4 +577,17 @@ $_control-size: 40px;
573
577
  }
574
578
  }
575
579
  }
580
+
581
+ // Menus in the bar lie on the page's card, so their ring is the page's
582
+ .mw-header .mw-dropdown-menu {
583
+ --mw-focus-ring-color: var(--mw-primary-text-color);
584
+ }
585
+
586
+ // Forced colours paint the drawn line away; the current page gets a real one
587
+ @media (forced-colors: active) {
588
+ .mw-navbar-link:is(.mw-active, .active, [aria-current='page']) {
589
+ text-decoration: underline 2px;
590
+ text-underline-offset: 0.35em;
591
+ }
592
+ }
576
593
  }
@@ -1,3 +1,5 @@
1
+ @forward 'app-shell';
2
+ @forward 'bento';
1
3
  @forward 'footer';
2
4
  @forward 'grid';
3
5
  @forward 'header';
@@ -104,8 +104,8 @@
104
104
  }
105
105
 
106
106
  @supports (animation-timeline: view()) {
107
- @media (prefers-reduced-motion: no-preference) {
108
- .mw-parallax-slow {
107
+ .mw-parallax-slow {
108
+ @include motion-safe {
109
109
  animation-name: mw-parallax-band;
110
110
  animation-timing-function: linear;
111
111
  animation-fill-mode: both;
@@ -188,19 +188,21 @@
188
188
  // `animation:` that cannot carry a timeline. The default range is the whole
189
189
  // cover pass, so the band keeps moving as long as any part of it is on screen.
190
190
  @supports (animation-timeline: view()) {
191
- @media (prefers-reduced-motion: no-preference) {
192
- .mw-parallax-media {
191
+ .mw-parallax-media {
192
+ @include motion-safe {
193
193
  animation-name: mw-parallax;
194
194
  animation-timing-function: linear;
195
195
  animation-fill-mode: both;
196
196
  animation-timeline: view();
197
197
  }
198
+ }
198
199
 
199
- // A pinned block does not travel through the viewport, so its view()
200
- // timeline stands still with it. The document's own scroll is what the
201
- // picture has left to play against - over the first screen, because after
202
- // that the page has covered it anyway.
203
- .mw-parallax-sticky .mw-parallax-media {
200
+ // A pinned block does not travel through the viewport, so its view()
201
+ // timeline stands still with it. The document's own scroll is what the
202
+ // picture has left to play against - over the first screen, because after
203
+ // that the page has covered it anyway.
204
+ .mw-parallax-sticky .mw-parallax-media {
205
+ @include motion-safe {
204
206
  animation-timeline: scroll(root);
205
207
  animation-range: 0 100dvh;
206
208
  }
@@ -213,8 +215,8 @@
213
215
  // inline `--mw-parallax-depth` - go on working untouched.
214
216
  //
215
217
  // 0 puts the layer at +depth and 1 at -depth, the two ends of the keyframes.
216
- @media (prefers-reduced-motion: no-preference) {
217
- .mw-parallax-driven {
218
+ .mw-parallax-driven {
219
+ @include motion-safe {
218
220
  transform: translate3d(
219
221
  0,
220
222
  calc(var(--mw-parallax-depth) * (1 - 2 * var(--mw-parallax-progress))),
@@ -2,15 +2,7 @@
2
2
 
3
3
  @layer mw.utilities {
4
4
  .mw-sr-only {
5
- position: absolute;
6
- width: 1px;
7
- height: 1px;
8
- padding: 0;
9
- margin: -1px;
10
- overflow: hidden;
11
- clip-path: inset(50%);
12
- white-space: nowrap;
13
- border: 0;
5
+ @include visually-hidden;
14
6
  }
15
7
 
16
8
  // The first thing a keyboard user meets on the page: a link that jumps past the
@@ -28,11 +20,12 @@
28
20
  background: var(--mw-primary-color);
29
21
  color: var(--mw-primary-accent-text-color);
30
22
  font-weight: font-weight('medium');
31
- box-shadow: var(--mw-elevation-3);
32
23
  transform: translateY(calc(-100% - #{spacing('5')}));
33
24
  transition: transform var(--mw-duration-fast) var(--mw-ease-out);
34
25
 
26
+ // Only on focus: parked above the edge, the shadow still reached into view
35
27
  &:focus-visible {
28
+ box-shadow: var(--mw-elevation-3);
36
29
  transform: translateY(0);
37
30
  }
38
31
  }
@@ -40,14 +33,6 @@
40
33
  // Content that is there for a screen reader but comes back on focus - the same
41
34
  // idea as sr-only, for something that has to be reachable by tab.
42
35
  .mw-sr-only-focusable:not(:focus):not(:focus-within) {
43
- position: absolute;
44
- width: 1px;
45
- height: 1px;
46
- padding: 0;
47
- margin: -1px;
48
- overflow: hidden;
49
- clip-path: inset(50%);
50
- white-space: nowrap;
51
- border: 0;
36
+ @include visually-hidden;
52
37
  }
53
38
  }
@@ -7,6 +7,7 @@
7
7
  @forward 'flex';
8
8
  @forward 'print';
9
9
  @forward 'reveal';
10
+ @forward 'shape';
10
11
  @forward 'spacing';
11
12
  @forward 'text';
12
13
  @forward 'touch-targets';
@@ -12,10 +12,9 @@
12
12
 
13
13
  @media print {
14
14
  // Paper is white and ink is black, whatever the screen was told to be. The
15
- // tokens are redeclared on .mw-theme-light too - that copy sits further
16
- // down the tree and would otherwise shadow the one on :root.
15
+ // dark footer brings its own copy of these, which would shadow the root's.
17
16
  :root,
18
- .mw-theme-light {
17
+ .mw-footer-dark {
19
18
  --mw-page-background: #fff;
20
19
  --mw-card-background: #fff;
21
20
  --mw-footer-background: #fff;
@@ -38,6 +37,11 @@
38
37
  color: #000;
39
38
  }
40
39
 
40
+ .mw-footer-dark {
41
+ --mw-primary-text-color: inherit;
42
+ color-scheme: light;
43
+ }
44
+
41
45
  // Chrome that is fixed on screen would print on the first page and cover
42
46
  // what is underneath, and navigation is useless on paper either way.
43
47
  .mw-header,
@@ -10,26 +10,30 @@
10
10
  // context. Longhands and never the shorthand, which esbuild collapses into one
11
11
  // `animation:` that cannot carry a timeline.
12
12
  @supports (animation-timeline: view()) {
13
- @media (prefers-reduced-motion: no-preference) {
14
- :root:not(.mw-scroll-static) :is(.mw-reveal, .mw-reveal-stagger > *) {
13
+ :root:not(.mw-scroll-static) :is(.mw-reveal, .mw-reveal-stagger > *) {
14
+ @include motion-safe {
15
15
  animation-name: mw-reveal;
16
16
  animation-timing-function: var(--mw-ease-out);
17
17
  animation-fill-mode: backwards;
18
18
  animation-timeline: view();
19
19
  @include reveal-range(1);
20
20
  }
21
+ }
21
22
 
22
- // A row crosses the viewport edge on one scroll step, so mw-reveal on each
23
- // card rises as one slab. This shifts every second and third child instead -
24
- // a wave. On the grid and not the cards, because the grid knows the columns.
25
- //
26
- // Cycles of three are the fallback for anything that is not one of the fixed
27
- // grids; mw-grid-2 to -5 restate the cycle to their own count in _grid.scss.
28
- .mw-reveal-stagger > :nth-child(3n + 2) {
23
+ // A row crosses the viewport edge on one scroll step, so mw-reveal on each
24
+ // card rises as one slab. This shifts every second and third child instead -
25
+ // a wave. On the grid and not the cards, because the grid knows the columns.
26
+ //
27
+ // Cycles of three are the fallback for anything that is not one of the fixed
28
+ // grids; mw-grid-2 to -5 restate the cycle to their own count in _grid.scss.
29
+ .mw-reveal-stagger > :nth-child(3n + 2) {
30
+ @include motion-safe {
29
31
  @include reveal-range(2);
30
32
  }
33
+ }
31
34
 
32
- .mw-reveal-stagger > :nth-child(3n + 3) {
35
+ .mw-reveal-stagger > :nth-child(3n + 3) {
36
+ @include motion-safe {
33
37
  @include reveal-range(3);
34
38
  }
35
39
  }
@@ -40,21 +44,25 @@
40
44
  // waits below the fold, `mw-reveal-run` once it enters. Both classes are
41
45
  // added by the script and never by the markup - a page without JS keeps
42
46
  // everything visible, which is what the guard above is for.
43
- @media (prefers-reduced-motion: no-preference) {
44
- .mw-reveal-hidden {
47
+ .mw-reveal-hidden {
48
+ @include motion-safe {
45
49
  opacity: 0;
46
50
  transform: translateY(40px);
47
51
  }
52
+ }
48
53
 
49
- // The script has already hidden whatever was below the fold when the class
50
- // arrives, and it only ever un-hides on intersection - without this the
51
- // switch would leave those elements invisible instead of static.
52
- :root.mw-scroll-static .mw-reveal-hidden {
54
+ // The script has already hidden whatever was below the fold when the class
55
+ // arrives, and it only ever un-hides on intersection - without this the
56
+ // switch would leave those elements invisible instead of static.
57
+ :root.mw-scroll-static .mw-reveal-hidden {
58
+ @include motion-safe {
53
59
  opacity: 1;
54
60
  transform: none;
55
61
  }
62
+ }
56
63
 
57
- .mw-reveal-run {
64
+ .mw-reveal-run {
65
+ @include motion-safe {
58
66
  // `backwards` for the reason above - a forwards fill would pin the
59
67
  // transform and cost the card its hover lift. The delay comes inline,
60
68
  // from the element's column.
@@ -0,0 +1,24 @@
1
+ @use '../abstracts' as *;
2
+
3
+ @layer mw.utilities {
4
+ // `clip`, not `hidden`: a wrapper clips without becoming a scroll container
5
+ .mw-shape-arch,
6
+ .mw-shape-signature,
7
+ .mw-shape-leaf {
8
+ overflow: clip;
9
+ }
10
+
11
+ // Oversized radii are scaled down together, which lands on a half circle
12
+ .mw-shape-arch {
13
+ border-radius: 999px 999px 0 0;
14
+ }
15
+
16
+ .mw-shape-signature {
17
+ border-radius: radius-sharp() radius('xl') radius-sharp() radius('xl');
18
+ }
19
+
20
+ // The silhouette taken to its end: the round pair opened up to the full side
21
+ .mw-shape-leaf {
22
+ border-radius: radius-sharp() 50% radius-sharp() 50%;
23
+ }
24
+ }
@@ -62,6 +62,29 @@
62
62
  align-items: center;
63
63
  }
64
64
 
65
+ // Footer links grow to a target; the text's own underline replaces the drawn one
66
+ .mw-footer-column li a,
67
+ .mw-footer-links a {
68
+ display: inline-flex;
69
+ align-items: center;
70
+ min-height: 2.5rem;
71
+ }
72
+
73
+ .mw-footer-column li a {
74
+ text-decoration: underline;
75
+ text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
76
+ text-underline-offset: 0.25em;
77
+
78
+ &::after {
79
+ content: none;
80
+ }
81
+ }
82
+
83
+ .mw-footer .mw-social-links a {
84
+ min-width: 2.75rem;
85
+ min-height: 2.75rem;
86
+ }
87
+
65
88
  // A checkbox or radio is tapped by its label as much as by the box, so the
66
89
  // whole row has to be the target - which it only is if the row is tall enough
67
90
  .mw-checkbox,
@@ -44,12 +44,7 @@
44
44
  // page, told apart by their line alone. That line then has to carry them:
45
45
  // --mw-border is a neighbouring shade of the surface and reads as nothing once
46
46
  // the surface *is* the page, so it is redrawn from the ink instead.
47
- //
48
- // Theme-bound tokens, and those *are* re-declared on <body> when the toggle puts
49
- // .mw-theme-light there - a value written on :root alone would be shadowed for
50
- // the whole body subtree and the switch would do nothing in light mode.
51
- :root.mw-surfaces-flush,
52
- :root.mw-surfaces-flush .mw-theme-light {
47
+ :root.mw-surfaces-flush {
53
48
  --mw-card-background: var(--mw-page-background);
54
49
  --mw-footer-background: var(--mw-page-background);
55
50
  --mw-border: color-mix(in srgb, var(--mw-text-color) 20%, transparent);
@@ -227,3 +222,27 @@
227
222
  }
228
223
  }
229
224
  }
225
+
226
+ @layer mw.utilities {
227
+ // Near-black from a root source: ink would be a light slab on a dark page
228
+ :root.mw-shadows-hard {
229
+ --mw-shadow-hard: oklch(from var(--mw-light-text-color) min(l, 0.12) c h);
230
+
231
+ @each $level, $offset in (1: 2px, 2: 4px, 3: 6px, 4: 8px, 5: 10px) {
232
+ --mw-elevation-#{$level}: #{$offset} #{$offset} 0 var(--mw-shadow-hard);
233
+ }
234
+ }
235
+ }
236
+
237
+ @layer mw.utilities {
238
+ // Black or white per fill by contrast-color(); without it the set tokens stay
239
+ @supports (color: contrast-color(red)) {
240
+ :root.mw-accent-text-auto {
241
+ @each $name in (primary, secondary, success, warning, danger, info) {
242
+ --mw-#{$name}-accent-text-color: contrast-color(
243
+ var(--mw-#{$name}-color)
244
+ );
245
+ }
246
+ }
247
+ }
248
+ }