maverick-wave 5.21.0 → 5.23.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.
@@ -23,17 +23,48 @@
23
23
  // Only the slope is in vw: a size in vw alone ignores the reader's font size and
24
24
  // stops responding to zoom, the rem term is what keeps that alive.
25
25
  @function fluid($min, $max, $from: 'xs', $to: 'md') {
26
- $min-rem: _to-rem($min, 'min');
27
- $max-rem: _to-rem($max, 'max');
28
- $vw-from: math.div(map.get(v.$breakpoints, $from), 16px);
29
- $vw-to: math.div(map.get(v.$breakpoints, $to), 16px);
26
+ @return _ramp(
27
+ _to-rem($min, 'min'),
28
+ _to-rem($max, 'max'),
29
+ math.div(map.get(v.$breakpoints, $from), 16px),
30
+ math.div(map.get(v.$breakpoints, $to), 16px),
31
+ vw
32
+ );
33
+ }
34
+
35
+ // The same ramp against the container instead of the viewport. For anything
36
+ // inside a `@container` block: sizing there off vw means a 440px card on a
37
+ // 1400px screen takes the width a 900px card was measured for, which is the
38
+ // mismatch container queries exist to remove. The endpoints are container
39
+ // widths rather than breakpoint names, because a container has no breakpoints.
40
+ @function fluid-container($min, $max, $from, $to) {
41
+ $from-rem: _to-rem($from, 'from');
42
+ $to-rem: _to-rem($to, 'to');
43
+
44
+ @if $from-rem == $to-rem {
45
+ @error "fluid-container(): $from and $to must differ, both are #{$from}.";
46
+ }
47
+
48
+ @return _ramp(
49
+ _to-rem($min, 'min'),
50
+ _to-rem($max, 'max'),
51
+ $from-rem,
52
+ $to-rem,
53
+ cqi
54
+ );
55
+ }
30
56
 
31
- $slope: math.div($max-rem - $min-rem, $vw-to - $vw-from);
32
- $intercept: $min-rem - $slope * $vw-from;
57
+ // Both of the above draw the same line through (from, min) and (to, max) and
58
+ // disagree only on what measures the axis. Shared so a change to the rem term -
59
+ // the part that keeps the size responding to zoom - cannot reach one and miss
60
+ // the other.
61
+ @function _ramp($min-rem, $max-rem, $from-rem, $to-rem, $unit) {
62
+ $slope: math.div($max-rem - $min-rem, $to-rem - $from-rem);
63
+ $intercept: $min-rem - $slope * $from-rem;
33
64
 
34
65
  @return clamp(
35
66
  #{$min-rem}rem,
36
- #{_round4($intercept)}rem + #{_round4($slope * 100)}vw,
67
+ #{_round4($intercept)}rem + #{_round4($slope * 100)}#{$unit},
37
68
  #{$max-rem}rem
38
69
  );
39
70
  }
@@ -0,0 +1,29 @@
1
+ @use '../abstracts' as *;
2
+
3
+ @layer mw.components {
4
+ // The block that says how to reach someone - postal address, phone, mail.
5
+ // Belongs on an <address>, which is what the one font-style is for: the
6
+ // element is italic by default, and nobody sets an address in italics.
7
+ .mw-contact {
8
+ font-style: normal;
9
+ display: grid;
10
+ gap: spacing('3');
11
+
12
+ > * {
13
+ margin: 0;
14
+ }
15
+
16
+ &-row {
17
+ display: flex;
18
+ flex-wrap: wrap;
19
+ gap: spacing('1') spacing('4');
20
+ }
21
+
22
+ // Sized rather than auto, so the values line up under each other however
23
+ // long the words in front of them are
24
+ &-label {
25
+ flex: 0 0 5rem;
26
+ color: var(--mw-text-muted-color);
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,141 @@
1
+ @use '../abstracts' as *;
2
+
3
+ @layer mw.components {
4
+ // Every fluid value here runs the same ramp: from the width the two-column
5
+ // layout starts at to the width a day card stops growing at. Against the
6
+ // container, not the viewport - the layout switch below is a container query,
7
+ // and sizing off vw would hand a narrow card in a wide window the
8
+ // measurements of a wide one.
9
+ @function _stamp-ramp($min, $max) {
10
+ @return fluid-container($min, $max, $mw-card-wide, 900px);
11
+ }
12
+
13
+ // Named because the today fill cancels the paddings to reach the card border,
14
+ // and the price variant repeats the track list - two copies would drift apart.
15
+ $_mw-date-stamp-pad-top: _stamp-ramp(spacing('4'), spacing('6'));
16
+ $_mw-date-stamp-pad-bottom: _stamp-ramp(spacing('3'), spacing('5'));
17
+ $_mw-date-stamp-day-col: _stamp-ramp(4.25rem, 7rem);
18
+ $_mw-date-stamp-gap: _stamp-ramp(spacing('4'), spacing('7'));
19
+ $_mw-date-stamp-day-size: _stamp-ramp('xl', '3xl');
20
+
21
+ // The date on a dated card - an event, a changelog entry, a day on a menu.
22
+ // Narrow, it is a line above the title. Wide, it turns into a column beside
23
+ // the content, which is the only thing that fills the right half of a card
24
+ // that has a whole row to itself.
25
+ .mw-date-stamp {
26
+ display: flex;
27
+ align-items: baseline;
28
+ gap: spacing('3');
29
+ margin-bottom: spacing('4');
30
+ color: var(--mw-text-muted-color);
31
+
32
+ &-day {
33
+ font-size: font-size('sm');
34
+ font-weight: font-weight('bold');
35
+ text-transform: uppercase;
36
+ letter-spacing: 0.08em;
37
+ }
38
+
39
+ &-date {
40
+ font-size: font-size('sm');
41
+ font-variant-numeric: tabular-nums;
42
+ }
43
+
44
+ // A fill, not a text colour: the price on the same card is already primary,
45
+ // and two blues compete where one would point. The padding is pulled back
46
+ // out again so the fill bleeds around the line instead of indenting it.
47
+ &-today {
48
+ padding: spacing('2') spacing('3');
49
+ margin-inline: spacing('-3');
50
+ margin-block-start: spacing('-2');
51
+ border-radius: radius('sm') radius('lg') radius('sm') radius('lg');
52
+ background: var(--mw-primary-color);
53
+ color: var(--mw-primary-accent-text-color);
54
+ }
55
+ }
56
+
57
+ // No box of its own until there are two columns to be one of, so the card body
58
+ // keeps seeing the title, the text and the price as its own children - which
59
+ // is what mw-offer and mw-card-body's last-child rule both select on
60
+ .mw-date-stamp-body {
61
+ display: contents;
62
+ }
63
+
64
+ @container mw-card (width >= #{$mw-card-wide}) {
65
+ // The rule sits here and not in _cards.scss because the stamp is the guest -
66
+ // the card body's own single column is right for everything that is not one
67
+ .mw-card-body:has(> .mw-date-stamp) {
68
+ display: grid;
69
+ grid-template-columns: $_mw-date-stamp-day-col minmax(0, 1fr);
70
+ // On the same curve as the column, so the dish keeps the width the day
71
+ // and the price give up rather than losing it again to the gutters
72
+ column-gap: $_mw-date-stamp-gap;
73
+ padding-block: $_mw-date-stamp-pad-top $_mw-date-stamp-pad-bottom;
74
+ }
75
+
76
+ // A third track, for the one price that covers the whole day - it is what the
77
+ // right half of a wide card is otherwise spent on. Only when there is a
78
+ // price: with a dish per row it travels with its dish, and an empty track
79
+ // would still charge the card a gap.
80
+ .mw-card-body:has(> .mw-date-stamp):has(> .mw-price) {
81
+ grid-template-columns: $_mw-date-stamp-day-col minmax(0, 1fr) auto;
82
+ }
83
+
84
+ // The row is left to stretch to the body, so a today fill can span the whole
85
+ // card - which means each column has to centre its own content instead
86
+ .mw-date-stamp-body {
87
+ display: flex;
88
+ flex-direction: column;
89
+ justify-content: center;
90
+ }
91
+
92
+ // The card body zeroes its own last child, but the wrapper hides ours from
93
+ // that rule - without this a trailing tag row adds a paragraph margin
94
+ .mw-date-stamp-body > *:last-child {
95
+ margin-bottom: spacing('0');
96
+ }
97
+
98
+ // Beside the dish it prices, not under the description that follows it. The
99
+ // second selector only exists to outrank mw-offer, which pushes a price to
100
+ // the bottom of the card and ties with the first on specificity alone.
101
+ .mw-card-body:has(> .mw-date-stamp) > .mw-price,
102
+ .mw-offer > .mw-card-body:has(> .mw-date-stamp) > .mw-price {
103
+ align-self: center;
104
+ justify-self: end;
105
+ margin-top: spacing('0');
106
+ padding-top: spacing('0');
107
+ text-align: right;
108
+ }
109
+
110
+ .mw-date-stamp {
111
+ flex-direction: column;
112
+ align-items: flex-start;
113
+ // Stretched to the row, so the day and the date centre against the title
114
+ // and description beside them rather than hanging off the first line
115
+ justify-content: center;
116
+ gap: spacing('2');
117
+ margin-bottom: spacing('0');
118
+ }
119
+
120
+ // Top edge of the card to its bottom one: the body's padding is handed back
121
+ // so the fill reaches the border. Only the two left corners are rounded, and
122
+ // to the card's own arc less the 1px it sits inside - the right edge is an
123
+ // inside edge and a cut is what reads as one.
124
+ .mw-date-stamp-today {
125
+ margin-block: calc(-1 * #{$_mw-date-stamp-pad-top})
126
+ calc(-1 * #{$_mw-date-stamp-pad-bottom});
127
+ margin-inline: calc(-1 * #{spacing('5')}) 0;
128
+ padding-block: $_mw-date-stamp-pad-top $_mw-date-stamp-pad-bottom;
129
+ padding-inline: spacing('5');
130
+ border-radius: calc(#{radius-sharp()} - 1px) 0 0
131
+ calc(#{radius('xl')} - 1px);
132
+ }
133
+
134
+ .mw-date-stamp-day {
135
+ // Tracks the column it sits in, or it would outgrow it on the way down
136
+ font-size: $_mw-date-stamp-day-size;
137
+ line-height: 1;
138
+ letter-spacing: 0.02em;
139
+ }
140
+ }
141
+ }
@@ -11,7 +11,9 @@
11
11
  @forward 'cards';
12
12
  @forward 'code';
13
13
  @forward 'coming-soon';
14
+ @forward 'contact';
14
15
  @forward 'content-slider';
16
+ @forward 'date-stamp';
15
17
  @forward 'divider';
16
18
  @forward 'dropdown';
17
19
  @forward 'empty-state';
@@ -21,6 +23,7 @@
21
23
  @forward 'kanban';
22
24
  @forward 'kbd';
23
25
  @forward 'lang-switch';
26
+ @forward 'leader-row';
24
27
  @forward 'links';
25
28
  @forward 'lists';
26
29
  @forward 'localhost-indicator';
@@ -0,0 +1,89 @@
1
+ @use '../abstracts' as *;
2
+
3
+ @layer mw.components {
4
+ // Dots on the baseline, not under the descenders; em so they follow font-size
5
+ $_mw-leader-baseline-lift: 0.34em;
6
+
7
+ // A label on the left, its value on the right, a dotted line bridging the
8
+ // gap. The line is the whole point: over a wide row the eye loses track of
9
+ // which value belongs to which label, which is why every printed menu, index
10
+ // and invoice draws one.
11
+ //
12
+ // The leader is the row's own ::before rather than an element in the markup.
13
+ // A generated box on a flex container is a flex item, so "order" can place it
14
+ // between the two spans - and generated content stays out of the
15
+ // accessibility tree, where a filler element would need aria-hidden.
16
+ .mw-leader-row {
17
+ --mw-leader-row-style: dotted;
18
+ --mw-leader-row-width: 2px;
19
+ --mw-leader-row-color: var(--mw-border);
20
+
21
+ display: flex;
22
+ // The values never shrink, so without a wrap the label is the only thing
23
+ // left to give - and it gives until it is one letter per line
24
+ flex-wrap: wrap;
25
+ align-items: flex-end;
26
+ gap: spacing('3');
27
+ margin: 0;
28
+
29
+ &::before {
30
+ content: '';
31
+ order: 1;
32
+ flex: 1 1 auto;
33
+ // Keeps a stub of line visible when label and value nearly fill the row
34
+ min-width: spacing('6');
35
+ border-bottom: var(--mw-leader-row-width) var(--mw-leader-row-style)
36
+ var(--mw-leader-row-color);
37
+ margin-bottom: $_mw-leader-baseline-lift;
38
+ }
39
+
40
+ &-label {
41
+ order: 0;
42
+ // No min-width override: the default floor is the longest word, so the
43
+ // label wraps between words and stops there instead of going to nothing.
44
+ // Past that floor the row wraps rather than the label shrinking further.
45
+ //
46
+ // The row also works as a dl group, and a dd carries 40px of browser margin
47
+ margin: 0;
48
+ }
49
+
50
+ &-value {
51
+ order: 2;
52
+ flex-shrink: 0;
53
+ margin: 0;
54
+ // A price or a page number must not break, and a column of them only
55
+ // lines up with tabular figures.
56
+ white-space: nowrap;
57
+ font-variant-numeric: tabular-nums;
58
+ }
59
+
60
+ // Two values in one row read as a single long range with only a gap between
61
+ // them. The mark takes the leader's colour, so retuning a row moves both.
62
+ // It hangs off the first of the pair, not the second: once the row wraps, a
63
+ // trailing mark reads as "continues below" and a leading one as a slip.
64
+ &-value:has(+ &-value)::after {
65
+ content: '|';
66
+ margin-left: spacing('3');
67
+ color: var(--mw-leader-row-color);
68
+ }
69
+
70
+ &-dashed {
71
+ --mw-leader-row-style: dashed;
72
+ }
73
+
74
+ &-solid {
75
+ --mw-leader-row-style: solid;
76
+ --mw-leader-row-width: 1px;
77
+ }
78
+
79
+ &-top {
80
+ align-items: flex-start;
81
+
82
+ &::before {
83
+ // flex-start pins the leader to the top edge, so it drops to the first
84
+ // line's baseline itself instead of being lifted off the last one
85
+ margin: calc(1lh - #{$_mw-leader-baseline-lift}) 0 0;
86
+ }
87
+ }
88
+ }
89
+ }
@@ -91,6 +91,10 @@
91
91
  }
92
92
 
93
93
  // Size. Only the token moves - see the `em` above.
94
+ .mw-price-xs {
95
+ --mw-price-size: #{font-size('lg')};
96
+ }
97
+
94
98
  .mw-price-sm {
95
99
  --mw-price-size: #{font-size('xl')};
96
100
  }
@@ -159,6 +159,12 @@ $_column-steps-lg: (
159
159
  grid-template-columns: repeat(auto-fill, minmax(min(350px, 100%), 1fr));
160
160
  }
161
161
 
162
+ // Every row as tall as the tallest rather than as tall as its own contents.
163
+ // A modifier, not a grid of its own - it needs one of the above to size.
164
+ .mw-grid-even {
165
+ grid-auto-rows: 1fr;
166
+ }
167
+
162
168
  .mw-grid-lg {
163
169
  @extend %grid-base-lg;
164
170
  }
@@ -1,18 +1,10 @@
1
- // Floating free over a picture, a control has to carry its own edge - the burger
2
- // and the login button bring a primary surface, the theme toggle is a dark pill
3
- // that a dark photo swallows. It travels with the float and not with the class,
4
- // or the ring would still be there in the arrived bar, on one control and not
5
- // its neighbour. Fixed values and not the elevation ramp, the same call the
6
- // parallax shadow makes: this lands on the picture, not on a theme surface, and
7
- // the light theme cuts its shadows for paper.
1
+ // Carried by the float and not by the class, or the ring would still be there in
2
+ // the arrived bar. Fixed values and not the elevation ramp: this lands on a
3
+ // picture, not on a theme surface, and the light theme cuts its shadows for paper.
8
4
  $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
9
5
 
10
6
  @layer mw.layout {
11
- // For a page that opens on a full-bleed picture and wants nothing on top of
12
- // it: the bar starts above the screen and rides in over the first stretch of
13
- // scroll. Inside the @supports, so a browser without scroll timelines gets a
14
- // normal header rather than none at all.
15
- //
7
+ // The bar starts above the screen and rides in over the first stretch of scroll.
16
8
  // Its own file, and not next to .mw-header: that one uses the `animation`
17
9
  // shorthand, which a consumer bundler folds together with a timeline into a
18
10
  // declaration no browser accepts.
@@ -26,12 +18,8 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
26
18
  animation-range: 0 420px;
27
19
  }
28
20
 
29
- // Anything carrying mw-header-keep sits the ride out: the bar travels,
30
- // the element travels the same distance back and stays where it is. The
31
- // distance is the bar's own, so it waits at the position it will hold in
32
- // the arrived bar - nothing jumps at the end, the bar just closes in
33
- // behind it. For a login or burger button that has to be reachable while
34
- // the hero is still untouched.
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.
35
23
  .mw-header-reveal .mw-header-keep {
36
24
  animation-name: mw-header-keep;
37
25
  animation-timing-function: var(--mw-ease-out);
@@ -40,18 +28,14 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
40
28
  animation-range: 0 420px;
41
29
  }
42
30
 
43
- // A group's opacity cannot be taken back by a child, so the fade has to
44
- // go as soon as something is meant to stay visible through it. No loss:
45
- // the bar is off screen for that stretch anyway, the fade only softened
46
- // the last few pixels of the arrival.
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.
47
33
  .mw-header-reveal:has(.mw-header-keep) {
48
34
  animation-name: mw-header-slide;
49
35
  }
50
36
 
51
- // The ribbon is glued under the bar, so it has to leave and arrive with
52
- // it - left behind it would hang alone at the top of an otherwise clean
53
- // hero. No class of its own: a ribbon on a page with a revealing header
54
- // has no other sensible behaviour.
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.
55
39
  body:has(.mw-header-reveal)
56
40
  .mw-announcement:not(.mw-announcement-static) {
57
41
  animation-name: mw-announcement-reveal;
@@ -63,9 +47,8 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
63
47
  }
64
48
  }
65
49
 
66
- // Both cover the height of the pair, or the shorter traveller arrives first
67
- // and the two come in as two things instead of one block. Without a ribbon
68
- // that height is just the bar's.
50
+ // Both cover the height of the pair, or the shorter traveller arrives first and
51
+ // the two come in as two things instead of one block.
69
52
  .mw-header-reveal {
70
53
  --mw-header-reveal-offset: var(--mw-header-height);
71
54
  }
@@ -76,18 +59,12 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
76
59
  );
77
60
  }
78
61
 
79
- // A keyboard reaches the chrome long before the first scroll does, and a focus
80
- // ring on something that is off screen is WCAG 2.4.11 - so focus brings it
81
- // back, either half of the pair pulling in the other. `!important` is the one
82
- // author declaration that outranks a running animation; dropping
83
- // `animation-name` instead leaves the element stuck on its last value, because
84
- // a restarted scroll timeline only catches up on the next scroll.
85
- //
86
- // A keep element is on screen already and needs no rescue, so it is left out -
87
- // otherwise a click on the theme toggle, which focuses it, would haul in the
88
- // whole bar. It takes `:focus` on a descendant and not `:focus-within` on the
89
- // header: within also matches every wrapper around the focused control, and no
90
- // `:not()` on those can tell which one holds the keep.
62
+ // A focus ring on something off screen is WCAG 2.4.11, so focus brings the bar
63
+ // back. `!important` is the one author declaration that outranks a running
64
+ // animation; `animation-name: none` would strand the element on its last value
65
+ // until the next scroll. Keeps are left out - they are on screen already, and a
66
+ // click on the theme toggle would otherwise haul in the whole bar. `:focus` on a
67
+ // descendant and not `:focus-within`: that matches every wrapper too.
91
68
  body:has(
92
69
  .mw-header-reveal :focus:not(.mw-header-keep, .mw-header-keep *),
93
70
  .mw-announcement:focus-within
@@ -108,43 +85,36 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
108
85
  }
109
86
  }
110
87
 
111
- // Same treatment while the panel is open. The panel hangs off the bar, so the
112
- // bar has to be in place for it to open anywhere sensible - and the burger,
113
- // which is the X from the moment the panel is out, has to stay on the burger's
114
- // own position rather than a header height below it.
88
+ // The panel hangs off the bar, so the bar has to be in place - and the burger,
89
+ // which is the X while the panel is out, has to stay on its own position.
115
90
  //
116
- // A class the script sets, and not `:has(.mw-navbar.open)`: this is the one
117
- // rule in the file that has to hold on every engine, and `:has()` against a
118
- // class toggled deep in the subtree is exactly where WebKit's style
119
- // invalidation has been unreliable. The burger is the control that closes the
120
- // panel - it cannot be the thing that waits for a repaint.
91
+ // A class the script sets, not `:has(.mw-navbar.open)`: the burger is the
92
+ // control that closes the panel and cannot wait for a selector re-match, which
93
+ // is where WebKit's invalidation has been unreliable.
121
94
  //
122
- // The distance is set to zero rather than each position that reads it being
123
- // overridden. Four mechanisms carry the pair - the bar's `top` and the keep
124
- // element's `translate`, each once as a class and once as a keyframe - and
125
- // every one of them resolves this one property, so zeroing it parks all four
126
- // at the arrived position at once. Overriding the values instead means
127
- // `!important` against a running animation, and WebKit does not reliably hand
128
- // that win to the author: the bar came in, the burger kept its offset, and the
129
- // X sat a header height below the panel it belongs to.
95
+ // Zeroing the distance rather than overriding each position that reads it: four
96
+ // mechanisms carry the pair - `top` and `translate`, each once as a class and
97
+ // once as a keyframe - and all four resolve this one property. Overriding the
98
+ // values instead pits `!important` against a running animation, which WebKit
99
+ // does not reliably hand to the author.
130
100
  body.mw-nav-open {
131
101
  .mw-header-reveal {
132
- --mw-header-reveal-offset: 0px;
102
+ // The ribbon variant above raises the same property from a `:has()`, which
103
+ // counts its argument and outranks this rule - author against author, which
104
+ // every engine resolves the same way, not author against an animation.
105
+ --mw-header-reveal-offset: 0px !important;
133
106
  opacity: 1 !important;
134
107
  }
135
108
 
136
- // Not carried by the offset: this one is pure decoration for a control
137
- // floating over a picture, and there is no picture behind an open panel.
109
+ // Not carried by the offset, and there is no picture behind an open panel.
138
110
  .mw-header-keep {
139
111
  box-shadow: none !important;
140
112
  }
141
113
  }
142
114
 
143
- // Older Safari and Firefox ship no scroll timelines, so there the script
144
- // toggles these classes instead (main.js) - the same two positions,
145
- // triggered instead of scrubbed. State and transition are separate because
146
- // the script sets the state a frame earlier: together they would send the
147
- // bar sliding away in front of the reader instead of having it gone already.
115
+ // No scroll timelines: the script toggles these classes instead (main.js).
116
+ // State and transition are separate because the script sets the state a frame
117
+ // earlier - together the bar would slide away in front of the reader.
148
118
  @media (prefers-reduced-motion: no-preference) {
149
119
  .mw-header-reveal.mw-header-away {
150
120
  top: calc(-1 * var(--mw-header-reveal-offset));
@@ -161,8 +131,7 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
161
131
  }
162
132
 
163
133
  .mw-header-driven {
164
- // The shared list plus `top`, which is not in it - writing only `top`
165
- // here would drop the header's own colour and shadow transitions.
134
+ // The shared list plus `top`: writing only `top` drops the bar's own.
166
135
  transition:
167
136
  var(--mw-transition),
168
137
  top var(--mw-duration-slow) var(--mw-ease-out);
@@ -188,12 +157,10 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
188
157
  }
189
158
  }
190
159
 
191
- // `top` and not a transform: a transform on the header would make it the
192
- // containing block for anything fixed inside it - at every value, the resting
193
- // one included - and the bar is chrome that other things hang off.
194
- // The wait sits in the keyframes and not in the range, because a range that
195
- // starts past 0 leaves the bar in its resting state below that point - so the
196
- // header would be visible for exactly the stretch it should stay away for.
160
+ // `top` and not a transform: that would make the header the containing block
161
+ // for anything fixed inside it, at every value. The wait sits in the keyframes
162
+ // and not in the range - a range starting past 0 leaves the bar resting, and
163
+ // visible, for exactly the stretch it should stay away for.
197
164
  @keyframes mw-header-reveal {
198
165
  0%,
199
166
  30% {
@@ -219,10 +186,8 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
219
186
  }
220
187
  }
221
188
 
222
- // `translate` and not `transform`: the burger puts its hover lift and its
223
- // press on transform, and a running animation outranks an author declaration
224
- // - it would hold the button flat for the rest of the page. The standalone
225
- // property stacks with the shorthand instead of replacing it.
189
+ // `translate` and not `transform`: the burger puts its hover lift on transform,
190
+ // and a running animation would hold it flat for the rest of the page.
226
191
  @keyframes mw-header-keep {
227
192
  0%,
228
193
  30% {
@@ -10,6 +10,22 @@
10
10
  flex-direction: column;
11
11
  }
12
12
 
13
+ // Set in from the container it sits in, and giving that back a step at a time
14
+ // as the screen shrinks. For a column of full-width cards, which at 1200px
15
+ // leaves more empty card than content.
16
+ .mw-container-narrow {
17
+ width: 80%;
18
+ margin-inline: auto;
19
+
20
+ @include media-down('lg') {
21
+ width: 90%;
22
+ }
23
+
24
+ @include media-down('md') {
25
+ width: 100%;
26
+ }
27
+ }
28
+
13
29
  .mw-container {
14
30
  width: var(--mw-container-width);
15
31
  margin: spacing('0') auto;