maverick-wave 6.7.0 → 6.9.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.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "maverick-wave",
3
- "version": "6.7.0",
3
+ "version": "6.9.0",
4
4
  "config": {
5
- "version_short": "6.7"
5
+ "version_short": "6.9"
6
6
  },
7
7
  "description": "A lightweight, modern CSS framework for building responsive websites with elegance and speed.",
8
8
  "main": "src/js/main.js",
@@ -114,12 +114,21 @@
114
114
  // rather than off the end.
115
115
  $_radius-floor: map.get(v.$radius, 'xs');
116
116
 
117
+ // The sharp half of the signature on a value of its own, so the scale can round
118
+ // controls up without softening the one corner meant to read as sharp
119
+ $_radius-sharp-base: 5px;
120
+
117
121
  // Emitted through --mw-radius-scale, so one number retunes every rounded box in
118
122
  // the framework at once - 0 squares them off, above 1 rounds them further, and
119
123
  // the corner arc follows. 'none' and 'full' pass through: an explicit 0 stays
120
- // 0, and a circle has to stay a circle.
124
+ // 0, and a circle has to stay a circle. A length instead of a key takes the
125
+ // same scale and floor, for the few values that sit off the scale.
121
126
  @function radius($key) {
122
- $value: map.get(v.$radius, $key);
127
+ $value: $key;
128
+
129
+ @if meta.type-of($key) != 'number' {
130
+ $value: map.get(v.$radius, $key);
131
+ }
123
132
 
124
133
  @if $key == 'none' or $key == 'full' {
125
134
  @return $value;
@@ -137,7 +146,21 @@ $_radius-floor: map.get(v.$radius, 'xs');
137
146
 
138
147
  @return max(
139
148
  #{$_radius-floor},
140
- calc(#{map.get(v.$radius, 'sm')} * #{$scale} * #{$scale})
149
+ calc(#{$_radius-sharp-base} * #{$scale} * #{$scale})
150
+ );
151
+ }
152
+
153
+ // Where a floating island sits: percentages on a fixed box are the viewport's,
154
+ // so this puts the header's content box on the page container,
155
+ // $header-island-reach wider on either side - and the bar at the foot on the
156
+ // same edges. Logo and links stay put while the header docks around them.
157
+ @function island-edge() {
158
+ @return max(
159
+ calc(var(--mw-header-inset) - #{v.$header-island-reach}),
160
+ calc(
161
+ (100% - var(--mw-container-width)) / 2 - var(--mw-header-pad) -
162
+ #{v.$header-island-reach}
163
+ )
141
164
  );
142
165
  }
143
166
 
@@ -276,6 +276,15 @@
276
276
  }
277
277
  }
278
278
 
279
+ // The floating chrome, shared by the header island and the action bar island
280
+ @mixin island-surface {
281
+ background: var(--mw-header-glass);
282
+ backdrop-filter: blur(20px) saturate(1.6);
283
+ border: 1px solid var(--mw-header-island-border);
284
+ border-radius: radius('xl');
285
+ box-shadow: var(--mw-elevation-4);
286
+ }
287
+
279
288
  // The MaverickWave silhouette: sharp corners on one diagonal, round on the other,
280
289
  // with an accent arc on the round pair - the asymmetry alone reads as an accident.
281
290
  //
@@ -434,9 +434,9 @@ $header-colors: (
434
434
  );
435
435
  // The room the header takes from the top of the viewport - island plus the gap
436
436
  // above it - so every offset that reads it clears the island without knowing it
437
- // floats. The island itself is 60px: a 44px touch control + 2 × spacing('2') +
437
+ // floats. The island itself is 63px: a 44px touch control + 2 × spacing('2') +
438
438
  // 2px border, with room to spare around the 42px logo.
439
- $header-height: 74px;
439
+ $header-height: 77px;
440
440
  // Between the island and the viewport edge
441
441
  $header-inset: 14px;
442
442
  // How far the island reaches past the page container on either side, and in
@@ -537,14 +537,20 @@ $breakpoints: (
537
537
  $radius: (
538
538
  'none': 0,
539
539
  'xs': 2px,
540
- 'sm': 5px,
541
- 'md': 10px,
540
+ 'sm': 7px,
541
+ 'md': 12px,
542
542
  'lg': 15px,
543
543
  'xl': 20px,
544
544
  '2xl': 30px,
545
545
  'full': 50%,
546
546
  );
547
547
 
548
+ // Off the scale on purpose: a chip next to a tag stays tighter than the buttons
549
+ // and fields it would otherwise be mistaken for. Outside the map, or every key
550
+ // would turn into a mw-radius-* utility.
551
+ $chip-radius: 10px;
552
+ $chip-radius-sm: 5px;
553
+
548
554
  // Z-index
549
555
  $z-index: (
550
556
  'lift': 3,
@@ -1,5 +1,11 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
+ // Clear of the home indicator where there is one, the header's gap where not
4
+ $_island-bottom: max(var(--mw-header-inset), env(safe-area-inset-bottom, 0px));
5
+ // Lighter than the flat bar, and as low as it goes: the items keep their
6
+ // 2.75rem touch floor, so the rest is only the padding around them
7
+ $_island-height: calc(var(--mw-action-bar-height) - 4px);
8
+
3
9
  @layer mw.components {
4
10
  :root {
5
11
  --mw-action-bar-height: 3.75rem;
@@ -18,6 +24,10 @@
18
24
  }
19
25
  }
20
26
 
27
+ :root:has(.mw-action-bar-island:not(.mw-action-bar-embedded)) {
28
+ --mw-action-bar-reserve: calc(#{$_island-height} + #{$_island-bottom});
29
+ }
30
+
21
31
  // Chrome like the header, so it takes the header's colours in both themes
22
32
  .mw-action-bar {
23
33
  --mw-focus-ring-color: var(--mw-header-navbar-list-active-color);
@@ -44,6 +54,19 @@
44
54
  position: absolute;
45
55
  }
46
56
 
57
+ &-island {
58
+ inset-inline: island-edge();
59
+ bottom: $_island-bottom;
60
+ min-height: $_island-height;
61
+ padding: spacing('1');
62
+ @include island-surface;
63
+
64
+ // Its own frame has no page container to line up with
65
+ &.mw-action-bar-embedded {
66
+ inset-inline: calc(var(--mw-header-inset) - #{$header-island-reach});
67
+ }
68
+ }
69
+
47
70
  &-item {
48
71
  display: flex;
49
72
  flex-direction: column;
@@ -129,6 +152,12 @@
129
152
  justify-content: center;
130
153
  grid-auto-columns: minmax(0, 10rem);
131
154
  }
155
+
156
+ // A dock: a container-wide island would float the targets in empty glass
157
+ .mw-action-bar-island {
158
+ width: fit-content;
159
+ margin-inline: auto;
160
+ }
132
161
  }
133
162
 
134
163
  @media print {
@@ -95,7 +95,7 @@
95
95
  &-language {
96
96
  font-size: font-size('2xs');
97
97
  padding: spacing('1');
98
- border-radius: radius('sm');
98
+ border-radius: radius($chip-radius-sm);
99
99
 
100
100
  @include media-up('sm') {
101
101
  font-size: font-size('xs');
@@ -13,7 +13,7 @@
13
13
  text-decoration: none;
14
14
  background: var(--mw-card-background);
15
15
  border: 1px solid var(--mw-border);
16
- border-radius: radius('md');
16
+ border-radius: radius($chip-radius);
17
17
  padding: spacing('2');
18
18
  transition: var(--mw-transition-fast);
19
19
  color: var(--mw-text-color);
@@ -55,7 +55,7 @@
55
55
  padding: spacing('1') spacing('2');
56
56
  min-width: 32px;
57
57
  min-height: 32px;
58
- border-radius: radius('sm');
58
+ border-radius: radius($chip-radius-sm);
59
59
 
60
60
  .mw-techstack-logo {
61
61
  width: 18px;
@@ -78,7 +78,7 @@
78
78
 
79
79
  &-logo {
80
80
  object-fit: contain;
81
- border-radius: radius('sm');
81
+ border-radius: radius($chip-radius-sm);
82
82
  transition: transform var(--mw-duration-fast) var(--mw-ease-out);
83
83
  flex-shrink: 0;
84
84
  }
@@ -9,7 +9,6 @@ $dock-range: 160px;
9
9
  $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
10
10
 
11
11
  @layer mw.layout {
12
- // None of it on the flat bar, which stands where it is from the first frame
13
12
  :root:not(.mw-header-bar) {
14
13
  // A header that stays put starts as the full bar and docks into the island over
15
14
  // the first stretch of scroll. A revealing one is off screen up there anyway and
@@ -51,172 +50,171 @@ $float-edge: (0 0 0 1px rgb(255 255 255 / 0.22), 0 4px 14px rgb(0 0 0 / 0.4));
51
50
  }
52
51
  }
53
52
  }
53
+ }
54
54
 
55
- // The bar starts above the screen and rides in over the first stretch of scroll.
56
- // Its own file, and not next to .mw-header: that one uses the `animation`
57
- // shorthand, which a consumer bundler folds together with a timeline into a
58
- // declaration no browser accepts.
59
- @supports (animation-timeline: scroll()) {
60
- .mw-header-reveal {
61
- @include motion-safe {
62
- animation-name: mw-header-reveal;
63
- animation-timing-function: var(--mw-ease-out);
64
- animation-fill-mode: both;
65
- animation-timeline: scroll(root);
66
- animation-range: 0 420px;
67
- }
55
+ // The bar starts above the screen and rides in over the first stretch of scroll.
56
+ // Its own file, and not next to .mw-header: that one uses the `animation`
57
+ // shorthand, which a consumer bundler folds together with a timeline into a
58
+ // declaration no browser accepts.
59
+ @supports (animation-timeline: scroll()) {
60
+ .mw-header-reveal {
61
+ @include motion-safe {
62
+ animation-name: mw-header-reveal;
63
+ animation-timing-function: var(--mw-ease-out);
64
+ animation-fill-mode: both;
65
+ animation-timeline: scroll(root);
66
+ animation-range: 0 420px;
68
67
  }
68
+ }
69
69
 
70
- // The bar travels, a keep element travels the same distance back and stays
71
- // put - for a burger that has to be reachable while the hero is untouched.
72
- .mw-header-reveal .mw-header-keep {
73
- @include motion-safe {
74
- animation-name: mw-header-keep;
75
- animation-timing-function: var(--mw-ease-out);
76
- animation-fill-mode: both;
77
- animation-timeline: scroll(root);
78
- animation-range: 0 420px;
79
- }
70
+ // The bar travels, a keep element travels the same distance back and stays
71
+ // put - for a burger that has to be reachable while the hero is untouched.
72
+ .mw-header-reveal .mw-header-keep {
73
+ @include motion-safe {
74
+ animation-name: mw-header-keep;
75
+ animation-timing-function: var(--mw-ease-out);
76
+ animation-fill-mode: both;
77
+ animation-timeline: scroll(root);
78
+ animation-range: 0 420px;
80
79
  }
80
+ }
81
81
 
82
- // A group's opacity cannot be taken back by a child, so the fade goes as
83
- // soon as something is meant to stay visible through it.
84
- .mw-header-reveal:has(.mw-header-keep) {
85
- @include motion-safe {
86
- animation-name: mw-header-slide;
87
- }
82
+ // A group's opacity cannot be taken back by a child, so the fade goes as
83
+ // soon as something is meant to stay visible through it.
84
+ .mw-header-reveal:has(.mw-header-keep) {
85
+ @include motion-safe {
86
+ animation-name: mw-header-slide;
88
87
  }
88
+ }
89
89
 
90
- // Glued under the bar, so it leaves and arrives with it. No class of its
91
- // own: a ribbon under a revealing header has no other sensible behaviour.
92
- body:has(.mw-header-reveal)
93
- .mw-announcement:not(.mw-announcement-static) {
94
- @include motion-safe {
95
- animation-name: mw-announcement-reveal;
96
- animation-timing-function: var(--mw-ease-out);
97
- animation-fill-mode: both;
98
- animation-timeline: scroll(root);
99
- animation-range: 0 420px;
100
- }
90
+ // Glued under the bar, so it leaves and arrives with it. No class of its
91
+ // own: a ribbon under a revealing header has no other sensible behaviour.
92
+ body:has(.mw-header-reveal) .mw-announcement:not(.mw-announcement-static) {
93
+ @include motion-safe {
94
+ animation-name: mw-announcement-reveal;
95
+ animation-timing-function: var(--mw-ease-out);
96
+ animation-fill-mode: both;
97
+ animation-timeline: scroll(root);
98
+ animation-range: 0 420px;
101
99
  }
102
100
  }
101
+ }
102
+
103
+ // Both cover the height of the pair, or the shorter traveller arrives first and
104
+ // the two come in as two things instead of one block.
105
+ .mw-header-reveal {
106
+ --mw-header-reveal-offset: var(--mw-header-height);
107
+ }
103
108
 
104
- // Both cover the height of the pair, or the shorter traveller arrives first and
105
- // the two come in as two things instead of one block.
109
+ body:has(.mw-announcement:not(.mw-announcement-static)) .mw-header-reveal {
110
+ --mw-header-reveal-offset: calc(
111
+ var(--mw-header-height) + var(--mw-announcement-height)
112
+ );
113
+ }
114
+
115
+ // A focus ring on something off screen is WCAG 2.4.11, so focus brings the bar
116
+ // back. `!important` is the one author declaration that outranks a running
117
+ // animation; `animation-name: none` would strand the element on its last value
118
+ // until the next scroll. Keeps are left out - they are on screen already, and a
119
+ // click on the theme toggle would otherwise haul in the whole bar. `:focus` on a
120
+ // descendant and not `:focus-within`: that matches every wrapper too.
121
+ body:has(
122
+ .mw-header-reveal :focus:not(.mw-header-keep, .mw-header-keep *),
123
+ .mw-announcement:focus-within
124
+ ) {
106
125
  .mw-header-reveal {
107
- --mw-header-reveal-offset: var(--mw-header-height);
126
+ top: var(--mw-header-inset) !important;
127
+ opacity: 1 !important;
108
128
  }
109
129
 
110
- body:has(.mw-announcement:not(.mw-announcement-static)) .mw-header-reveal {
111
- --mw-header-reveal-offset: calc(
112
- var(--mw-header-height) + var(--mw-announcement-height)
113
- );
130
+ .mw-header-keep {
131
+ translate: none !important;
132
+ box-shadow: none !important;
114
133
  }
115
134
 
116
- // A focus ring on something off screen is WCAG 2.4.11, so focus brings the bar
117
- // back. `!important` is the one author declaration that outranks a running
118
- // animation; `animation-name: none` would strand the element on its last value
119
- // until the next scroll. Keeps are left out - they are on screen already, and a
120
- // click on the theme toggle would otherwise haul in the whole bar. `:focus` on a
121
- // descendant and not `:focus-within`: that matches every wrapper too.
122
- body:has(
123
- .mw-header-reveal :focus:not(.mw-header-keep, .mw-header-keep *),
124
- .mw-announcement:focus-within
125
- ) {
126
- .mw-header-reveal {
127
- top: var(--mw-header-inset) !important;
128
- opacity: 1 !important;
129
- }
135
+ .mw-announcement:not(.mw-announcement-static) {
136
+ transform: none !important;
137
+ opacity: 1 !important;
138
+ }
139
+ }
130
140
 
131
- .mw-header-keep {
132
- translate: none !important;
133
- box-shadow: none !important;
134
- }
141
+ // The panel hangs off the bar, so the bar has to be in place - and the burger,
142
+ // which is the X while the panel is out, has to stay on its own position.
143
+ //
144
+ // A class the script sets, not `:has(.mw-navbar.mw-active)`: the burger is the
145
+ // control that closes the panel and cannot wait for a selector re-match, which
146
+ // is where WebKit's invalidation has been unreliable.
147
+ //
148
+ // Zeroing the distance rather than overriding each position that reads it: four
149
+ // mechanisms carry the pair - `top` and `translate`, each once as a class and
150
+ // once as a keyframe - and all four resolve this one property. Overriding the
151
+ // values instead pits `!important` against a running animation, which WebKit
152
+ // does not reliably hand to the author.
153
+ body.mw-nav-open {
154
+ .mw-header-reveal {
155
+ // The ribbon variant above raises the same property from a `:has()`, which
156
+ // counts its argument and outranks this rule - author against author, which
157
+ // every engine resolves the same way, not author against an animation.
158
+ --mw-header-reveal-offset: 0px !important;
159
+ opacity: 1 !important;
160
+ }
135
161
 
136
- .mw-announcement:not(.mw-announcement-static) {
137
- transform: none !important;
138
- opacity: 1 !important;
139
- }
162
+ // Not carried by the offset, and there is no picture behind an open panel.
163
+ .mw-header-keep {
164
+ box-shadow: none !important;
140
165
  }
166
+ }
141
167
 
142
- // The panel hangs off the bar, so the bar has to be in place - and the burger,
143
- // which is the X while the panel is out, has to stay on its own position.
144
- //
145
- // A class the script sets, not `:has(.mw-navbar.mw-active)`: the burger is the
146
- // control that closes the panel and cannot wait for a selector re-match, which
147
- // is where WebKit's invalidation has been unreliable.
148
- //
149
- // Zeroing the distance rather than overriding each position that reads it: four
150
- // mechanisms carry the pair - `top` and `translate`, each once as a class and
151
- // once as a keyframe - and all four resolve this one property. Overriding the
152
- // values instead pits `!important` against a running animation, which WebKit
153
- // does not reliably hand to the author.
154
- body.mw-nav-open {
155
- .mw-header-reveal {
156
- // The ribbon variant above raises the same property from a `:has()`, which
157
- // counts its argument and outranks this rule - author against author, which
158
- // every engine resolves the same way, not author against an animation.
159
- --mw-header-reveal-offset: 0px !important;
160
- opacity: 1 !important;
168
+ // No scroll timelines: the script toggles these classes instead (main.js).
169
+ // State and transition are separate because the script sets the state a frame
170
+ // earlier - together the bar would slide away in front of the reader.
171
+ .mw-header-reveal.mw-header-away {
172
+ @include motion-safe {
173
+ top: calc(var(--mw-header-inset) - var(--mw-header-reveal-offset));
174
+ opacity: 0;
175
+
176
+ // Visible through the slide, so its shadow would hang into the hero
177
+ &:has(.mw-header-keep) {
178
+ opacity: 1;
179
+ box-shadow: none;
161
180
  }
162
181
 
163
- // Not carried by the offset, and there is no picture behind an open panel.
164
182
  .mw-header-keep {
165
- box-shadow: none !important;
183
+ translate: 0 var(--mw-header-reveal-offset);
184
+ box-shadow: $float-edge;
166
185
  }
167
186
  }
187
+ }
168
188
 
169
- // No scroll timelines: the script toggles these classes instead (main.js).
170
- // State and transition are separate because the script sets the state a frame
171
- // earlier - together the bar would slide away in front of the reader.
172
- .mw-header-reveal.mw-header-away {
173
- @include motion-safe {
174
- top: calc(var(--mw-header-inset) - var(--mw-header-reveal-offset));
175
- opacity: 0;
176
-
177
- // Visible through the slide, so its shadow would hang into the hero
178
- &:has(.mw-header-keep) {
179
- opacity: 1;
180
- box-shadow: none;
181
- }
182
-
183
- .mw-header-keep {
184
- translate: 0 var(--mw-header-reveal-offset);
185
- box-shadow: $float-edge;
186
- }
187
- }
188
- }
189
+ .mw-header-driven {
190
+ @include motion-safe {
191
+ // The shared list plus `top`: writing only `top` drops the bar's own.
192
+ transition:
193
+ var(--mw-transition),
194
+ top var(--mw-duration-slow) var(--mw-ease-out);
189
195
 
190
- .mw-header-driven {
191
- @include motion-safe {
192
- // The shared list plus `top`: writing only `top` drops the bar's own.
196
+ .mw-header-keep {
193
197
  transition:
194
- var(--mw-transition),
195
- top var(--mw-duration-slow) var(--mw-ease-out);
196
-
197
- .mw-header-keep {
198
- transition:
199
- translate var(--mw-duration-slow) var(--mw-ease-out),
200
- box-shadow var(--mw-duration-slow) var(--mw-ease-out);
201
- }
198
+ translate var(--mw-duration-slow) var(--mw-ease-out),
199
+ box-shadow var(--mw-duration-slow) var(--mw-ease-out);
202
200
  }
203
201
  }
202
+ }
204
203
 
205
- .mw-announcement.mw-announcement-away {
206
- @include motion-safe {
207
- transform: translateY(
208
- calc(-1 * (var(--mw-header-height) + var(--mw-announcement-height)))
209
- );
210
- opacity: 0;
211
- }
204
+ .mw-announcement.mw-announcement-away {
205
+ @include motion-safe {
206
+ transform: translateY(
207
+ calc(-1 * (var(--mw-header-height) + var(--mw-announcement-height)))
208
+ );
209
+ opacity: 0;
212
210
  }
211
+ }
213
212
 
214
- .mw-announcement-driven {
215
- @include motion-safe {
216
- transition:
217
- transform var(--mw-duration-slow) var(--mw-ease-out),
218
- opacity var(--mw-duration-slow) var(--mw-ease-out);
219
- }
213
+ .mw-announcement-driven {
214
+ @include motion-safe {
215
+ transition:
216
+ transform var(--mw-duration-slow) var(--mw-ease-out),
217
+ opacity var(--mw-duration-slow) var(--mw-ease-out);
220
218
  }
221
219
  }
222
220
 
@@ -7,17 +7,6 @@ $_control-size: 40px;
7
7
  // frame on a box this short
8
8
  $_mark-round: radius('lg');
9
9
 
10
- // Where the island sits: percentages on a fixed box are the viewport's, so this
11
- // puts its content box on the page container, $header-island-reach wider on
12
- // either side. Logo and links stay put while the bar docks around them.
13
- $_island-edge: max(
14
- calc(var(--mw-header-inset) - #{$header-island-reach}),
15
- calc(
16
- (100% - var(--mw-container-width)) / 2 - var(--mw-header-pad) -
17
- #{$header-island-reach}
18
- )
19
- );
20
-
21
10
  @mixin navbar-mark-shape {
22
11
  border-radius: radius-sharp() $_mark-round radius-sharp() $_mark-round;
23
12
 
@@ -51,16 +40,12 @@ $_island-edge: max(
51
40
  .mw-header {
52
41
  position: fixed;
53
42
  top: var(--mw-header-inset);
54
- left: $_island-edge;
55
- right: $_island-edge;
43
+ left: island-edge();
44
+ right: island-edge();
56
45
  z-index: z-index('header');
57
46
  // Dark in both themes, fixed light included, where no light-dark() flips
58
47
  --mw-focus-ring-color: var(--mw-header-navbar-list-active-color);
59
- background: var(--mw-header-glass);
60
- backdrop-filter: blur(20px) saturate(1.6);
61
- border: 1px solid var(--mw-header-island-border);
62
- border-radius: radius('xl');
63
- box-shadow: var(--mw-elevation-4);
48
+ @include island-surface;
64
49
  // The height comes from min-height; the padding only has to let a control
65
50
  // grown to its 2.75rem touch floor fit inside it
66
51
  padding: spacing('2') var(--mw-header-pad);
@@ -79,7 +64,7 @@ $_island-edge: max(
79
64
  min-width: 0;
80
65
  }
81
66
 
82
- // The island already sits on the page container, see $_island-edge
67
+ // The island already sits on the page container, see island-edge()
83
68
  .mw-container {
84
69
  width: 100%;
85
70
  display: flex;
@@ -263,7 +248,7 @@ $_island-edge: max(
263
248
  }
264
249
  }
265
250
 
266
- // The panel hangs under the island as a card of its own, not beside it. A
251
+ // The panel hangs under the island as its lower half, not beside it. A
267
252
  // drawer pinned to the right edge runs into the logo: 250px of panel against a
268
253
  // 180px logo leaves 60px of overlap on a phone, and .mw-logo sits higher in the
269
254
  // header's stacking context, so the logo prints on top of the menu. Under the
@@ -276,7 +261,8 @@ $_island-edge: max(
276
261
  // Bound to it, the card simply travels with it.
277
262
  @mixin navbar-top-sheet {
278
263
  position: absolute;
279
- top: calc(100% + #{spacing('3')});
264
+ // On the island's bottom border, so the two share one seam line
265
+ top: 100%;
280
266
  // Out to the island's outer edge, past its border
281
267
  left: -1px;
282
268
  right: -1px;
@@ -286,7 +272,7 @@ $_island-edge: max(
286
272
  // header, so a translucent card would show the page through it unblurred
287
273
  background: var(--mw-header-background);
288
274
  border: 1px solid var(--mw-header-island-border);
289
- border-radius: radius('xl');
275
+ border-radius: 0 0 radius('xl') radius('xl');
290
276
  z-index: z-index('header-panel');
291
277
 
292
278
  // Rolled out from under the island rather than slid in. A slide needs a
@@ -309,10 +295,9 @@ $_island-edge: max(
309
295
  // width of its longest label and the rows end a third of the way across
310
296
  width: 100%;
311
297
  // Tall menus stay reachable without the card growing past the screen: the
312
- // room under the island, less the gap above the card and the inset below
298
+ // room under the island, less the inset below
313
299
  max-height: calc(
314
- 100dvh - var(--mw-header-height) -
315
- #{spacing('3')} - var(--mw-header-inset)
300
+ 100dvh - var(--mw-header-height) - var(--mw-header-inset)
316
301
  );
317
302
  overflow-y: auto;
318
303
  // Keeps a rubber-band scroll inside the panel from moving the page
@@ -430,6 +415,11 @@ $_island-edge: max(
430
415
  z-index: z-index('header-scrim');
431
416
  animation: mw-navbar-scrim-in var(--mw-duration-base) var(--mw-ease-out);
432
417
  }
418
+
419
+ .mw-header {
420
+ border-bottom-left-radius: 0;
421
+ border-bottom-right-radius: 0;
422
+ }
433
423
  }
434
424
 
435
425
  @keyframes mw-navbar-item-in {
@@ -600,8 +590,8 @@ $_island-edge: max(
600
590
  // rounding seam opens between them.
601
591
  .mw-announcement:not(.mw-announcement-static) {
602
592
  top: calc(var(--mw-header-height) - 1px);
603
- left: $_island-edge;
604
- right: $_island-edge;
593
+ left: island-edge();
594
+ right: island-edge();
605
595
  border: 1px solid var(--mw-header-island-border);
606
596
  border-top: none;
607
597
  border-radius: 0 0 radius('xl') radius('xl');
@@ -614,14 +604,6 @@ $_island-edge: max(
614
604
  border-bottom-left-radius: 0;
615
605
  border-bottom-right-radius: 0;
616
606
  box-shadow: none;
617
-
618
- // The open menu covers the ribbon instead of hanging below it - a phone
619
- // would otherwise lose the height of both before the first link
620
- .mw-navbar {
621
- top: 100%;
622
- border-top-left-radius: 0;
623
- border-top-right-radius: 0;
624
- }
625
607
  }
626
608
 
627
609
  // The open menu covers it, but through the scrim's blur its edge would still
@@ -630,14 +612,16 @@ $_island-edge: max(
630
612
  visibility: hidden;
631
613
  }
632
614
 
633
- // The flat bar, for a site that wants the header to just stand there: full
634
- // width, fixed at the top, no island and no reveal (_header-reveal.scss is off
635
- // under the same class). On <html> like the other site-wide variants.
615
+ // The flat bar: full width, fixed at the top, no island and no docking (the
616
+ // dock in _header-reveal.scss is off under the same class). mw-header-reveal
617
+ // still rides it in. On <html> like the other site-wide variants.
636
618
  :root.mw-header-bar {
637
619
  --mw-header-height: #{$header-bar-height};
620
+ // Not `top: 0` on the header: that would outrank the reveal's resting and
621
+ // away positions, which are both counted from the inset
622
+ --mw-header-inset: 0px;
638
623
 
639
624
  .mw-header {
640
- top: 0;
641
625
  left: 0;
642
626
  right: 0;
643
627
  min-height: var(--mw-header-height);