maverick-wave 5.8.0 → 5.10.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 (43) hide show
  1. package/.claude/skills/mw-maverick-wave/SKILL.md +22 -12
  2. package/.claude/skills/mw-maverick-wave/examples/angular-services.md +10 -1
  3. package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +4 -3
  4. package/.claude/skills/mw-maverick-wave/references/components.md +23 -9
  5. package/.claude/skills/mw-maverick-wave/references/javascript.md +1 -1
  6. package/.claude/skills/mw-maverick-wave/references/layout.md +8 -3
  7. package/.claude/skills/mw-maverick-wave/references/theming.md +48 -25
  8. package/CHANGELOG.md +13 -0
  9. package/README.md +41 -24
  10. package/index.html +325 -20
  11. package/maverick-wave.min.css +61 -5
  12. package/maverick-wave.min.js +1 -1
  13. package/package.json +2 -2
  14. package/scripts/verify.js +7 -1
  15. package/src/js/main.js +24 -5
  16. package/src/partials/palette-container.html +105 -65
  17. package/src/partials/preview-container.html +136 -0
  18. package/src/partials/utilities-container.html +38 -0
  19. package/src/scss/abstracts/_functions.scss +31 -3
  20. package/src/scss/abstracts/_mixins.scss +62 -12
  21. package/src/scss/abstracts/_variables.scss +47 -21
  22. package/src/scss/base/_base.scss +97 -8
  23. package/src/scss/components/_badge.scss +1 -1
  24. package/src/scss/components/_buttons.scss +1 -1
  25. package/src/scss/components/_cards.scss +23 -14
  26. package/src/scss/components/_dropdown.scss +58 -1
  27. package/src/scss/components/_kanban.scss +10 -9
  28. package/src/scss/components/_lang-switch.scss +1 -1
  29. package/src/scss/components/_localhost-indicator.scss +1 -1
  30. package/src/scss/components/_modals.scss +13 -1
  31. package/src/scss/components/_pricing.scss +1 -1
  32. package/src/scss/components/_progress.scss +9 -5
  33. package/src/scss/components/_skeleton.scss +1 -1
  34. package/src/scss/components/_spinners.scss +5 -5
  35. package/src/scss/components/_stepper.scss +4 -1
  36. package/src/scss/components/_tabs.scss +28 -8
  37. package/src/scss/components/_tiles.scss +41 -16
  38. package/src/scss/components/_timelines.scss +12 -5
  39. package/src/scss/layout/_main.scss +1 -1
  40. package/src/scss/layout/_parallax.scss +10 -5
  41. package/src/scss/utilities/_elevation.scss +23 -0
  42. package/src/scss/utilities/_touch-targets.scss +1 -1
  43. package/src/scss/utilities/_variants.scss +74 -7
@@ -7,6 +7,8 @@
7
7
  // Room the ribbon needs in the top right corner of a ticket
8
8
  $_mw-kanban-ribbon-clearance: 66px;
9
9
  $_mw-kanban-ribbon-clearance-compact: 58px;
10
+ // Narrowest a lane may get before the board wraps it to the next row
11
+ $_mw-kanban-lane-min: 260px;
10
12
 
11
13
  @keyframes mw-kanban-card-in {
12
14
  from {
@@ -58,21 +60,20 @@
58
60
  --mw-kanban-column-min-height: 120px;
59
61
 
60
62
  display: grid;
61
- grid-auto-flow: column;
62
- grid-auto-columns: minmax(0, 1fr);
63
+ // As many lanes as fit at a readable width, the rest wrap. The board asks
64
+ // its own width, so one inside a panel wraps on a desktop too. `min()`
65
+ // against 100% keeps a lane out of a container narrower than the minimum.
66
+ grid-template-columns: repeat(
67
+ auto-fit,
68
+ minmax(min(#{$_mw-kanban-lane-min}, 100%), 1fr)
69
+ );
63
70
  gap: spacing('4');
64
71
  padding: spacing('4');
65
72
  background: var(--mw-kanban-background);
66
73
  border-radius: radius('md');
67
74
 
68
- // Two lanes side by side stay readable, four do not
69
- @include media-down('lg') {
70
- grid-auto-flow: row;
71
- grid-template-columns: repeat(2, minmax(0, 1fr));
72
- }
73
-
75
+ // Frame only - how tight the board sits on a phone is a viewport call
74
76
  @include media-down('sm') {
75
- grid-template-columns: minmax(0, 1fr);
76
77
  gap: spacing('3');
77
78
  padding: spacing('3');
78
79
  }
@@ -52,7 +52,7 @@
52
52
  .mw-lang-switch .mw-dropdown-menu {
53
53
  min-width: 170px;
54
54
 
55
- @media (pointer: coarse), (max-width: #{breakpoint('sm')}) {
55
+ @media (pointer: coarse), (width <= #{breakpoint('sm')}) {
56
56
  min-width: max(100%, 240px);
57
57
  }
58
58
  }
@@ -10,7 +10,7 @@
10
10
  height: 4px;
11
11
  background: var(--mw-warning-color);
12
12
  z-index: z-index('modal')-10;
13
- animation: pulse-indicator 4s infinite;
13
+ animation: pulse-indicator motion(4s) infinite;
14
14
  }
15
15
 
16
16
  // Keyframes for the pulsing indicator
@@ -16,12 +16,24 @@
16
16
  justify-content: center;
17
17
  padding: spacing('7');
18
18
  opacity: 0;
19
- transition: opacity var(--mw-duration-base) var(--mw-ease-out);
19
+ // `display` in the list and `allow-discrete` behind it, or the overlay is
20
+ // taken off the page the frame the class comes off and the fade out never
21
+ // renders - the modal inside it would slide down over nothing. The dialog
22
+ // version below has had this; the div version was landing hard.
23
+ transition:
24
+ opacity var(--mw-duration-base) var(--mw-ease-out),
25
+ display var(--mw-duration-base) allow-discrete;
20
26
 
21
27
  // Show modal when class is added
22
28
  &.mw-modal-open {
23
29
  display: flex;
24
30
  opacity: 1;
31
+
32
+ // A box that goes from `display: none` starts at its end state otherwise,
33
+ // so there is nothing to fade from
34
+ @starting-style {
35
+ opacity: 0;
36
+ }
25
37
  }
26
38
  }
27
39
 
@@ -200,7 +200,7 @@
200
200
  // row survives the narrow-card stack and the action takes half of it. Here and
201
201
  // not in _cards.scss because the price is the guest - the footer's own rule is
202
202
  // right for everything that is not one.
203
- @container mw-card (max-width: #{$mw-card-narrow}) {
203
+ @container mw-card (width <= #{$mw-card-narrow}) {
204
204
  .mw-card-footer:has(> .mw-price) {
205
205
  flex-direction: row;
206
206
  align-items: center;
@@ -1,6 +1,15 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
3
  @layer mw.components {
4
+ // How far the bar is filled - written by the script, read by the keyframe at
5
+ // the bottom. Registered and not declared on the element: a `data-value` the
6
+ // script cannot parse lands on 0% instead of dropping `width` altogether.
7
+ @property --mw-progress-value {
8
+ syntax: '<length-percentage>';
9
+ inherits: true;
10
+ initial-value: 0%;
11
+ }
12
+
4
13
  .mw-progress {
5
14
  &-container {
6
15
  margin: spacing('1');
@@ -61,11 +70,6 @@
61
70
  // the variants at the bottom of the file
62
71
  --mw-progress-ink: var(--mw-secondary-accent-text-color);
63
72
 
64
- // How far the bar is filled. Written by the script from data-value or from
65
- // an inline width, and read by the keyframe below. Declared here so it has
66
- // a definition rather than only a fallback - an unset bar is an empty one.
67
- --mw-progress-value: 0;
68
-
69
73
  height: 100%;
70
74
  background: var(--mw-secondary-color);
71
75
  border-radius: radius('sm');
@@ -18,7 +18,7 @@
18
18
  var(--mw-card-background) 75%
19
19
  );
20
20
  background-size: 200% 100%;
21
- animation: mw-skeleton-shimmer 1.5s infinite linear;
21
+ animation: mw-skeleton-shimmer motion(1.5s) infinite linear;
22
22
  border-radius: radius('sm');
23
23
  display: block;
24
24
  }
@@ -16,20 +16,20 @@
16
16
  border: 0.5rem solid var(--mw-border);
17
17
  border-radius: radius('full');
18
18
  border-top-color: var(--mw-primary-text-color);
19
- animation: mw-spin 1.2s linear infinite;
19
+ animation: mw-spin motion(1.2s) linear infinite;
20
20
 
21
21
  &.mw-spinner-sm {
22
22
  width: $_size-sm;
23
23
  height: $_size-sm;
24
24
  border-width: 0.3rem;
25
- animation: mw-spin 1.4s linear infinite;
25
+ animation: mw-spin motion(1.4s) linear infinite;
26
26
  }
27
27
 
28
28
  &.mw-spinner-lg {
29
29
  width: $_size-lg;
30
30
  height: $_size-lg;
31
31
  border-width: 0.7rem;
32
- animation: mw-spin 1s linear infinite;
32
+ animation: mw-spin motion(1s) linear infinite;
33
33
  }
34
34
 
35
35
  &.mw-spinner-primary {
@@ -54,7 +54,7 @@
54
54
  border-radius: radius('full');
55
55
  display: inline-block;
56
56
  margin: spacing('0') spacing('2');
57
- animation: mw-bounce 1.4s infinite ease-in-out both;
57
+ animation: mw-bounce motion(1.4s) infinite ease-in-out both;
58
58
 
59
59
  &:nth-child(1) {
60
60
  animation-delay: -0.32s;
@@ -110,7 +110,7 @@
110
110
  border: 0.5rem solid var(--mw-primary-text-color);
111
111
  border-color: var(--mw-primary-text-color) transparent
112
112
  var(--mw-primary-text-color) transparent;
113
- animation: mw-spin 1.5s linear infinite;
113
+ animation: mw-spin motion(1.5s) linear infinite;
114
114
  }
115
115
 
116
116
  &.mw-spinner-sm {
@@ -6,6 +6,7 @@
6
6
  display: flex;
7
7
  align-items: flex-start;
8
8
  width: 100%;
9
+ container: mw-stepper / inline-size;
9
10
 
10
11
  &-step {
11
12
  display: flex;
@@ -79,7 +80,9 @@
79
80
  }
80
81
  }
81
82
 
82
- @include media-up('sm') {
83
+ // The step labels follow the strip's own width, not the window's - a
84
+ // stepper in a narrow card keeps the small label on a wide desktop.
85
+ @container mw-stepper (width >= #{breakpoint('sm')}) {
83
86
  &-label {
84
87
  font-size: font-size('xs');
85
88
  }
@@ -1,6 +1,34 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
3
  @layer mw.components {
4
+ // Registered, so a half-written value from the script cannot invalidate the
5
+ // whole `transform` and leave the marker somewhere other than at rest.
6
+ // `inherits: true` because the bar is the nav's ::after, and a pseudo element
7
+ // only ever sees these through inheritance.
8
+ @property --mw-tabs-marker-x {
9
+ syntax: '<length>';
10
+ inherits: true;
11
+ initial-value: 0px;
12
+ }
13
+
14
+ @property --mw-tabs-marker-y {
15
+ syntax: '<length>';
16
+ inherits: true;
17
+ initial-value: 0px;
18
+ }
19
+
20
+ @property --mw-tabs-marker-w {
21
+ syntax: '<number>';
22
+ inherits: true;
23
+ initial-value: 0;
24
+ }
25
+
26
+ @property --mw-tabs-marker-h {
27
+ syntax: '<number>';
28
+ inherits: true;
29
+ initial-value: 0;
30
+ }
31
+
4
32
  .mw-tabs {
5
33
  display: flex;
6
34
  flex-direction: column;
@@ -71,14 +99,6 @@
71
99
  // A 1px bar scaled to width, not a bar whose width is set: scaleX runs on the
72
100
  // compositor, `width` relayouts the strip on every frame of the slide.
73
101
  &-nav[data-mw-tabs-marker] {
74
- // Declared with a zero length so the four are real tokens with a resting
75
- // value rather than four names the stylesheet only ever reads. The script
76
- // sets them on the element, which outranks this either way.
77
- --mw-tabs-marker-x: 0;
78
- --mw-tabs-marker-w: 0;
79
- --mw-tabs-marker-y: 0;
80
- --mw-tabs-marker-h: 0;
81
-
82
102
  .mw-tabs-nav-item.mw-active,
83
103
  .mw-tabs-nav-item.active {
84
104
  border-bottom-color: transparent;
@@ -57,7 +57,22 @@
57
57
  }
58
58
  }
59
59
 
60
+ // The tile sizes its type off its own width, not the window's. In
61
+ // `.mw-tiles-4col` on a wide desktop a tile is a quarter of 85% of the page -
62
+ // narrower than the same tile alone on a phone, where it used to be the one
63
+ // getting the small type.
64
+ //
65
+ // The cost: `container-type` makes the tile a containing block for its own
66
+ // fixed-position descendants, and unlike a card the tile clips - so a
67
+ // [data-tooltip] inside one is now cut off at its edge. Taken knowingly, there
68
+ // being nothing in a tile's anatomy that wants a tooltip. Drop the two lines
69
+ // below and the queries fall back to the window.
70
+ $_mw-tile-wide: 300px;
71
+ $_mw-tile-narrow: 220px;
72
+
60
73
  .mw-tile {
74
+ width: 100%;
75
+ container: mw-tile / inline-size;
61
76
  background: var(--mw-card-background);
62
77
  @include surface;
63
78
  padding: spacing('5');
@@ -116,11 +131,11 @@
116
131
  line-height: 1.2;
117
132
  text-align: center;
118
133
 
119
- @include media-up('sm') {
134
+ @container mw-tile (width >= #{$_mw-tile-narrow}) {
120
135
  font-size: font-size('xl');
121
136
  }
122
137
 
123
- @include media-up('md') {
138
+ @container mw-tile (width >= #{$_mw-tile-wide}) {
124
139
  font-size: font-size('2xl');
125
140
  }
126
141
  }
@@ -166,7 +181,7 @@
166
181
  font-size: font-size('md');
167
182
  margin-bottom: spacing('2');
168
183
 
169
- @include media-up('md') {
184
+ @container mw-tile (width >= #{$_mw-tile-wide}) {
170
185
  font-size: font-size('lg');
171
186
  }
172
187
  }
@@ -194,11 +209,11 @@
194
209
  font-size: font-size('xl');
195
210
  margin-bottom: spacing('4');
196
211
 
197
- @include media-up('sm') {
212
+ @container mw-tile (width >= #{$_mw-tile-narrow}) {
198
213
  font-size: font-size('2xl');
199
214
  }
200
215
 
201
- @include media-up('md') {
216
+ @container mw-tile (width >= #{$_mw-tile-wide}) {
202
217
  font-size: font-size('3xl');
203
218
  }
204
219
  }
@@ -214,20 +229,30 @@
214
229
  }
215
230
  }
216
231
 
217
- @include media-down('sm') {
218
- .mw-tile {
219
- padding: spacing('4');
232
+ // A cramped tile reads better ranged left whatever the window is doing - a
233
+ // centred line inside 200px is two words and a gap. Every selector here is a
234
+ // child: a container cannot answer a query about itself, and the tile *is* the
235
+ // container - which is also why the padding below stays on the window.
236
+ @container mw-tile (width < #{$_mw-tile-narrow}) {
237
+ .mw-tile-header,
238
+ .mw-tile-body {
220
239
  text-align: left;
240
+ }
221
241
 
222
- &-img {
223
- width: 60px;
224
- height: 60px;
225
- margin: spacing('0') auto spacing('2');
226
- }
242
+ .mw-tile-body {
243
+ margin-bottom: spacing('2');
244
+ }
227
245
 
228
- &-body {
229
- margin-bottom: spacing('2');
230
- }
246
+ .mw-tile-img {
247
+ width: 60px;
248
+ height: 60px;
249
+ margin: spacing('0') auto spacing('2');
250
+ }
251
+ }
252
+
253
+ @include media-down('sm') {
254
+ .mw-tile {
255
+ padding: spacing('4');
231
256
  }
232
257
  }
233
258
  }
@@ -57,6 +57,7 @@
57
57
  margin: spacing('1') auto;
58
58
  padding: spacing('5') 0;
59
59
  width: min(100%, 900px);
60
+ container: mw-timeline / inline-size;
60
61
 
61
62
  // Center vertical line
62
63
  &::before {
@@ -185,7 +186,7 @@
185
186
  background: var(--mw-primary-color);
186
187
  border-color: var(--mw-primary-text-color);
187
188
  transform: scale(1.1);
188
- animation: mw-pulse-ring-big 2s infinite;
189
+ animation: mw-pulse-ring-big motion(2s) infinite;
189
190
 
190
191
  .mw-timeline-big-date-main,
191
192
  .mw-timeline-big-date-sub {
@@ -193,9 +194,10 @@
193
194
  }
194
195
  }
195
196
 
196
- @include media-down('sm') {
197
- width: 100%;
198
-
197
+ // The two-sided layout collapses when the timeline itself runs out of room,
198
+ // not when the window does - the same timeline in a half-width column had
199
+ // its content overlapping the centre line on a wide desktop.
200
+ @container mw-timeline (width < #{breakpoint('sm')}) {
199
201
  // Line moves to the left
200
202
  &::before {
201
203
  left: 0;
@@ -258,6 +260,7 @@
258
260
  margin: spacing('1') auto;
259
261
  padding: spacing('3') 0;
260
262
  width: min(96%, 930px);
263
+ container: mw-timeline / inline-size;
261
264
 
262
265
  // Vertical line centered over the date column
263
266
  &::before {
@@ -319,12 +322,16 @@
319
322
  background: var(--mw-primary-color);
320
323
  color: var(--mw-primary-accent-text-color);
321
324
  transform: scale(1.1);
322
- animation: mw-pulse-ring-simple 2s infinite;
325
+ animation: mw-pulse-ring-simple motion(2s) infinite;
323
326
  }
324
327
 
328
+ // The inset is a page-level call: on a phone the timeline takes the full
329
+ // width, however wide its column happens to be.
325
330
  @include media-down('sm') {
326
331
  width: 100%;
332
+ }
327
333
 
334
+ @container mw-timeline (width < #{breakpoint('sm')}) {
328
335
  &::before {
329
336
  left: 0;
330
337
  transform: none;
@@ -208,7 +208,7 @@
208
208
  letter-spacing: 0.08em;
209
209
  text-transform: uppercase;
210
210
  text-decoration: none;
211
- animation: mw-scroll-hint-float 2.2s var(--mw-ease-in-out) infinite;
211
+ animation: mw-scroll-hint-float motion(2.2s) var(--mw-ease-in-out) infinite;
212
212
  text-shadow: 0 1px 4px rgb(0 0 0 / 65%);
213
213
 
214
214
  // The arrow carries the meaning, so it gets the size and the label stays quiet
@@ -1,6 +1,15 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
3
  @layer mw.layout {
4
+ // Registered so a bad value cannot invalidate the calc() and drop the layer
5
+ // somewhere else. 0.5 and not 0 is the resting point - see the transform
6
+ // at the bottom of this file.
7
+ @property --mw-parallax-progress {
8
+ syntax: '<number>';
9
+ inherits: true;
10
+ initial-value: 0.5;
11
+ }
12
+
4
13
  // The image gets its own layer instead of `background-attachment: fixed`: that
5
14
  // one is ignored on iOS and sizes against the viewport rather than the block.
6
15
  // Here a transform runs on the browser's own scroll timeline - no listener, no
@@ -151,13 +160,9 @@
151
160
  // is what lets every depth variant above - and an inline
152
161
  // `--mw-parallax-depth` - go on working untouched.
153
162
  //
154
- // 0 puts the layer at +depth and 1 at -depth, the two ends of the keyframes
155
- // below. Declared and not only used, so the frame before the script has
156
- // written anything sits at rest instead of nowhere.
163
+ // 0 puts the layer at +depth and 1 at -depth, the two ends of the keyframes.
157
164
  @media (prefers-reduced-motion: no-preference) {
158
165
  .mw-parallax-driven {
159
- --mw-parallax-progress: 0.5;
160
-
161
166
  transform: translate3d(
162
167
  0,
163
168
  calc(var(--mw-parallax-depth) * (1 - 2 * var(--mw-parallax-progress))),
@@ -15,4 +15,27 @@
15
15
  box-shadow: var(--mw-elevation-#{$level});
16
16
  }
17
17
  }
18
+
19
+ // Light instead of shadow. Elevation says how far a surface floats above the
20
+ // page; this says it is the thing giving off the light - for the one element on
21
+ // a screen that has to be looked at first, and for nothing else.
22
+ //
23
+ // Two layers like elevation, and for the same reason: the tight one reads as
24
+ // the edge of the source, the wide one as the air around it. Both are
25
+ // offsetless, which is what separates a glow from a shadow - light does not
26
+ // fall downwards. Bare `.mw-glow` rides along on primary, because the brand
27
+ // colour is what a glow means unless it says otherwise.
28
+ @each $name, $col in $semantic-colors {
29
+ $selector: '.mw-glow-#{$name}';
30
+
31
+ @if $name == 'primary' {
32
+ $selector: '.mw-glow, #{$selector}';
33
+ }
34
+
35
+ #{$selector} {
36
+ box-shadow:
37
+ 0 0 6px color-mix(in srgb, #{$col} 45%, transparent),
38
+ 0 0 26px color-mix(in srgb, #{$col} 28%, transparent);
39
+ }
40
+ }
18
41
  }
@@ -5,7 +5,7 @@
5
5
  // sizes are right under a pointer and too small under a thumb. Coarse *or*
6
6
  // narrow, because a tablet in landscape is wide and still finger-operated.
7
7
  // Loaded last, so these win without extra specificity.
8
- @media (pointer: coarse), (max-width: #{breakpoint('md')}) {
8
+ @media (pointer: coarse), (width <= #{breakpoint('md')}) {
9
9
  // Padding and line-height add up to ~35px, under the 44px a thumb needs. Only
10
10
  // the height grows: the padding sets the label's relationship to the edges.
11
11
  .mw-btn {
@@ -31,12 +31,10 @@
31
31
  // the modal, which genuinely float above content and keep their shadow - in a
32
32
  // flat page they are the only two things that should cast one.
33
33
  //
34
- // The second selector is not optional: elevation is a theme alias, and the
35
- // light theme re-declares it on <body>. A value written on :root alone is
36
- // shadowed for the whole body subtree, so the switch would do nothing at all
37
- // in light mode. Any variant that retunes a theme-bound token needs both.
38
- :root.mw-shadows-flat,
39
- :root.mw-shadows-flat .mw-theme-light {
34
+ // One selector, unlike mw-surfaces-flush below: the ramp is no longer declared
35
+ // per theme, so there is no copy further down the tree to shadow this one. The
36
+ // theme reaches it through --mw-shadow-near/-far instead - see elevation().
37
+ :root.mw-shadows-flat {
40
38
  --mw-elevation-1: none;
41
39
  --mw-elevation-2: none;
42
40
  --mw-elevation-3: none;
@@ -47,7 +45,9 @@
47
45
  // --mw-border is a neighbouring shade of the surface and reads as nothing once
48
46
  // the surface *is* the page, so it is redrawn from the ink instead.
49
47
  //
50
- // Theme-bound tokens, so both selectors - see the note above.
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
51
  :root.mw-surfaces-flush,
52
52
  :root.mw-surfaces-flush .mw-theme-light {
53
53
  --mw-card-background: var(--mw-page-background);
@@ -159,4 +159,71 @@
159
159
  letter-spacing: 0.045em;
160
160
  }
161
161
  }
162
+
163
+ // The other axis to --mw-root-font-size, which zooms the whole page: there
164
+ // the layout is the same layout, larger. This moves the padding inside a
165
+ // surface and leaves the type alone, so a dense screen reads as well as a
166
+ // roomy one and only fits more on.
167
+ //
168
+ // --mw-section-padding-block is deliberately untouched: section spacing is
169
+ // its own switch, and both writing it would have the panel print a setup that
170
+ // contradicts itself. Steps along the spacing scale rather than free values,
171
+ // because that scale is what every other measure is built from - two of them
172
+ // down for compact, where the point is to fit more on screen, one up for
173
+ // roomy, where a second step would only look loose.
174
+ :root.mw-density-compact {
175
+ --mw-control-height-sm: 1.5rem;
176
+ --mw-control-height: 1.75rem;
177
+ --mw-control-height-lg: 2rem;
178
+
179
+ .mw-card-body {
180
+ padding: spacing('4') spacing('2') spacing('3');
181
+
182
+ @container mw-card (width >= #{$mw-card-wide}) {
183
+ padding: spacing('5') spacing('3') spacing('4');
184
+ }
185
+ }
186
+
187
+ .mw-panel-header,
188
+ .mw-panel-body {
189
+ padding: spacing('2') spacing('3');
190
+ }
191
+ }
192
+
193
+ :root.mw-density-roomy {
194
+ --mw-control-height-sm: 2.25rem;
195
+ --mw-control-height: 2.5rem;
196
+ --mw-control-height-lg: 2.75rem;
197
+
198
+ .mw-card-body {
199
+ padding: spacing('7') spacing('5') spacing('6');
200
+
201
+ @container mw-card (width >= #{$mw-card-wide}) {
202
+ padding: spacing('8') spacing('6') spacing('7');
203
+ }
204
+ }
205
+
206
+ .mw-panel-header,
207
+ .mw-panel-body {
208
+ padding: spacing('5') spacing('6');
209
+ }
210
+ }
211
+
212
+ :root.mw-links-underline {
213
+ .mw-link,
214
+ .mw-link-muted,
215
+ .mw-btn-link {
216
+ // The longhand, because the `text-decoration` shorthand resets
217
+ // text-decoration-thickness along with it and would drop the 1.5px
218
+ // link-look() draws the line at.
219
+ text-decoration-line: underline;
220
+
221
+ // The underline was the hover signal and is now the resting state, so the
222
+ // weight takes over: .mw-link carries no colour change of its own and
223
+ // would otherwise go completely dead under the pointer.
224
+ &:hover:not(:disabled) {
225
+ text-decoration-thickness: 3px;
226
+ }
227
+ }
228
+ }
162
229
  }