maverick-wave 5.26.0 → 5.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.claude/skills/mw-maverick-wave/SKILL.md +9 -1
  2. package/.claude/skills/mw-maverick-wave/references/components.md +13 -6
  3. package/.claude/skills/mw-maverick-wave/references/layout.md +2 -2
  4. package/.claude/skills/mw-maverick-wave/references/theming.md +2 -1
  5. package/CHANGELOG.md +13 -0
  6. package/README.md +2 -2
  7. package/maverick-wave.min.css +7 -7
  8. package/maverick-wave.min.js +1 -1
  9. package/package.json +2 -2
  10. package/scripts/verify.js +10 -1
  11. package/src/js/main.js +8 -8
  12. package/src/partials/tiles-container.html +38 -38
  13. package/src/scss/abstracts/_variables.scss +6 -0
  14. package/src/scss/base/_base.scss +17 -7
  15. package/src/scss/base/_reset.scss +7 -1
  16. package/src/scss/components/_announcement.scss +1 -3
  17. package/src/scss/components/_blog-post.scss +5 -4
  18. package/src/scss/components/_cards.scss +2 -2
  19. package/src/scss/components/_contact.scss +5 -3
  20. package/src/scss/components/_hero-badge.scss +48 -0
  21. package/src/scss/components/_index.scss +2 -0
  22. package/src/scss/components/_leader-row.scss +24 -18
  23. package/src/scss/components/_localhost-indicator.scss +2 -2
  24. package/src/scss/components/_panels.scss +18 -52
  25. package/src/scss/components/_tabs.scss +2 -2
  26. package/src/scss/components/_tiles.scss +43 -49
  27. package/src/scss/components/_tooltip.scss +193 -0
  28. package/src/scss/layout/_footer.scss +46 -84
  29. package/src/scss/layout/_grid.scss +67 -42
  30. package/src/scss/layout/_header-reveal.scss +1 -1
  31. package/src/scss/layout/_header.scss +49 -77
  32. package/src/scss/layout/_home.scss +7 -4
  33. package/src/scss/layout/_main.scss +100 -337
  34. package/src/scss/layout/_parallax.scss +1 -1
  35. package/src/scss/layout/_section.scss +37 -21
@@ -0,0 +1,48 @@
1
+ @use '../abstracts' as *;
2
+
3
+ @layer mw.components {
4
+ // Glass rather than a fill: on a hero image a solid badge is the loudest thing
5
+ // on the screen and plain text sinks into the picture.
6
+ .mw-hero-badge {
7
+ display: inline-flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ gap: spacing('3');
11
+ max-width: 100%;
12
+ margin-bottom: spacing('6');
13
+ padding: spacing('2') spacing('5');
14
+ border: 1px solid #{fade('hero-text-color', 22%)};
15
+ border-radius: 999px;
16
+ // A vertical ramp and not a flat tint: glass is lit from above, and the
17
+ // gradient is what gives the pill a thickness instead of a fogged panel
18
+ background: linear-gradient(
19
+ to bottom,
20
+ #{fade('hero-text-color', 16%)},
21
+ #{fade('hero-text-color', 5%)}
22
+ );
23
+ // saturate() is what separates glass from frosted plastic: the blur alone
24
+ // greys out what shows through, and pushing the chroma back up is how a
25
+ // real lens reads. The two inset lines are the specular highlight - light
26
+ // catching the top edge, a fainter bounce off the bottom - and the outer
27
+ // shadow is what lifts the pill off the photo instead of pasting it on.
28
+ backdrop-filter: blur(14px) saturate(180%);
29
+ box-shadow:
30
+ inset 0 1px 0 #{fade('hero-text-color', 40%)},
31
+ inset 0 -1px 0 #{fade('hero-text-color', 12%)},
32
+ 0 2px 12px rgb(0 0 0 / 18%);
33
+ color: fade('hero-text-color', 88%);
34
+ font-size: font-size('sm');
35
+ font-weight: font-weight('medium');
36
+ letter-spacing: 0.02em;
37
+ }
38
+
39
+ // Carries the secondary colour, because the badge is a state, not a label -
40
+ // override `background` for a different one
41
+ .mw-hero-badge-dot {
42
+ width: 8px;
43
+ height: 8px;
44
+ flex-shrink: 0;
45
+ border-radius: radius('full');
46
+ background: var(--mw-secondary-color);
47
+ }
48
+ }
@@ -19,6 +19,7 @@
19
19
  @forward 'empty-state';
20
20
  @forward 'flag';
21
21
  @forward 'gallery';
22
+ @forward 'hero-badge';
22
23
  @forward 'info';
23
24
  @forward 'kanban';
24
25
  @forward 'kbd';
@@ -49,3 +50,4 @@
49
50
  @forward 'tiles';
50
51
  @forward 'timelines';
51
52
  @forward 'toasts';
53
+ @forward 'tooltip';
@@ -10,7 +10,7 @@
10
10
  // and invoice draws one.
11
11
  //
12
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
13
+ // A generated box on a grid container is a grid item, so "order" can place it
14
14
  // between the two spans - and generated content stays out of the
15
15
  // accessibility tree, where a filler element would need aria-hidden.
16
16
  .mw-leader-row {
@@ -18,20 +18,27 @@
18
18
  --mw-leader-row-width: 2px;
19
19
  --mw-leader-row-color: var(--mw-border);
20
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;
21
+ // Grid, not a wrapping flex row. Flex fills a line greedily and moves the
22
+ // next item down before it shrinks anything, so a label that still fit on
23
+ // its own line kept it and pushed the value underneath - on a phone the
24
+ // row then read as text, dashes, and a number stranded on the line below.
25
+ // A grid column gives instead: the label breaks between its words and the
26
+ // value stays where it belongs.
27
+ //
28
+ // The leader column holds a stub of line when label and value nearly fill
29
+ // the row. Anything past the two declared columns - a second value - takes
30
+ // a column of its own.
31
+ display: grid;
32
+ grid-template-columns: minmax(0, auto) minmax(spacing('6'), 1fr);
33
+ grid-auto-flow: column;
34
+ grid-auto-columns: max-content;
35
+ align-items: end;
26
36
  gap: spacing('3');
27
37
  margin: 0;
28
38
 
29
39
  &::before {
30
40
  content: '';
31
41
  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
42
  border-bottom: var(--mw-leader-row-width) var(--mw-leader-row-style)
36
43
  var(--mw-leader-row-color);
37
44
  margin-bottom: $_mw-leader-baseline-lift;
@@ -39,17 +46,16 @@
39
46
 
40
47
  &-label {
41
48
  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
- //
49
+ // Words first, and the column floor is zero, so a single word wider than
50
+ // what is left of the row breaks inside itself rather than running past
51
+ // the edge - a German compound noun reaches that point on a phone.
52
+ overflow-wrap: break-word;
46
53
  // The row also works as a dl group, and a dd carries 40px of browser margin
47
54
  margin: 0;
48
55
  }
49
56
 
50
57
  &-value {
51
58
  order: 2;
52
- flex-shrink: 0;
53
59
  margin: 0;
54
60
  // A price or a page number must not break, and a column of them only
55
61
  // lines up with tabular figures.
@@ -59,8 +65,8 @@
59
65
 
60
66
  // Two values in one row read as a single long range with only a gap between
61
67
  // 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.
68
+ // It hangs off the first of the pair, not the second, where a trailing mark
69
+ // would read as "and there is more" at the end of the row.
64
70
  &-value:has(+ &-value)::after {
65
71
  content: '|';
66
72
  margin-left: spacing('3');
@@ -77,10 +83,10 @@
77
83
  }
78
84
 
79
85
  &-top {
80
- align-items: flex-start;
86
+ align-items: start;
81
87
 
82
88
  &::before {
83
- // flex-start pins the leader to the top edge, so it drops to the first
89
+ // start pins the leader to the top edge, so it drops to the first
84
90
  // line's baseline itself instead of being lifted off the last one
85
91
  margin: calc(1lh - #{$_mw-leader-baseline-lift}) 0 0;
86
92
  }
@@ -10,11 +10,11 @@
10
10
  height: 4px;
11
11
  background: var(--mw-warning-color);
12
12
  z-index: z-index('modal')-10;
13
- animation: pulse-indicator motion(4s) infinite;
13
+ animation: mw-pulse-indicator motion(4s) infinite;
14
14
  }
15
15
 
16
16
  // Keyframes for the pulsing indicator
17
- @keyframes pulse-indicator {
17
+ @keyframes mw-pulse-indicator {
18
18
  0% {
19
19
  opacity: 0.5;
20
20
  box-shadow: 0 0 3px var(--mw-warning-color);
@@ -1,6 +1,14 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
3
  @layer mw.components {
4
+ // -scrollable is the older spelling of -max-height-md
5
+ $_mw-panel-heights: (
6
+ 'max-height-sm': 300px,
7
+ 'max-height-md': 500px,
8
+ 'max-height-lg': 700px,
9
+ 'scrollable': 500px,
10
+ );
11
+
4
12
  .mw-panel {
5
13
  background: var(--mw-card-background);
6
14
  @include surface(2px);
@@ -8,13 +16,15 @@
8
16
  overflow: hidden;
9
17
  display: flex;
10
18
  flex-direction: column;
11
- min-height: 100%;
12
- margin: spacing('1');
13
19
 
14
20
  &-header {
15
21
  padding: spacing('4') spacing('5');
16
22
  border-bottom: 1px solid var(--mw-border-accent);
17
23
  display: flex;
24
+ // A title with an action beside it runs out of room on a phone long
25
+ // before the title runs out of words
26
+ flex-wrap: wrap;
27
+ gap: spacing('3');
18
28
  justify-content: space-between;
19
29
  align-items: center;
20
30
  margin: spacing('0');
@@ -48,7 +58,6 @@
48
58
  gap: spacing('3');
49
59
  }
50
60
 
51
- // Panel variants
52
61
  &-primary {
53
62
  .mw-panel-header {
54
63
  background: var(--mw-primary-color);
@@ -72,56 +81,13 @@
72
81
  @include corner-accent(2px, 'bottom');
73
82
  }
74
83
 
75
- // Scrollable variant for fixed height
76
- &-scrollable {
77
- max-height: 500px;
78
-
79
- .mw-panel-body {
80
- overflow-y: auto;
81
- max-height: calc(500px - #{2 * spacing('4')} - 2px);
82
- }
83
- }
84
-
85
- // Scrollable variants for max height
86
- &-max-height {
87
- &-sm {
88
- max-height: 300px;
89
- min-height: 0;
90
- align-self: start;
91
- .mw-panel-body {
92
- max-height: calc(300px - #{2 * spacing('4')} - 2px);
93
- overflow-y: auto;
94
- }
95
- }
96
-
97
- &-md {
98
- max-height: 500px;
99
- min-height: 0;
100
- align-self: start;
101
- .mw-panel-body {
102
- max-height: calc(500px - #{2 * spacing('4')} - 2px);
103
- overflow-y: auto;
104
- }
105
- }
106
-
107
- &-lg {
108
- max-height: 700px;
109
- min-height: 0;
84
+ // Only the cap - the body already scrolls and already takes what the header
85
+ // and footer leave it. `align-self` because a grid row stretches its items
86
+ // first, and a stretched panel reaches the cap from above whatever it says.
87
+ @each $name, $height in $_mw-panel-heights {
88
+ &-#{$name} {
89
+ max-height: $height;
110
90
  align-self: start;
111
- .mw-panel-body {
112
- max-height: calc(700px - #{2 * spacing('4')} - 2px);
113
- overflow-y: auto;
114
- }
115
- }
116
- }
117
-
118
- @include media-down('sm') {
119
- font-size: font-size('sm');
120
-
121
- &-header {
122
- h3 {
123
- font-size: font-size('sm');
124
- }
125
91
  }
126
92
  }
127
93
  }
@@ -132,7 +132,7 @@
132
132
  display: block;
133
133
  // 1.1s to reveal a tab panel means a second of blank space after every
134
134
  // click. A panel switch is a state change, not an entrance.
135
- animation: fadeIn var(--mw-duration-base) var(--mw-ease-out);
135
+ animation: mw-tabs-fade var(--mw-duration-base) var(--mw-ease-out);
136
136
  }
137
137
  }
138
138
 
@@ -281,7 +281,7 @@
281
281
  }
282
282
  }
283
283
 
284
- @keyframes fadeIn {
284
+ @keyframes mw-tabs-fade {
285
285
  from {
286
286
  opacity: 0;
287
287
  }
@@ -1,51 +1,8 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
3
  @layer mw.components {
4
- .mw-tiles {
5
- width: 100%;
6
- margin: spacing('0') auto;
7
- padding: spacing('0');
8
- display: grid;
9
- gap: spacing('4');
10
- grid-template-columns: 1fr;
11
-
12
- @include media-up('sm') {
13
- grid-template-columns: repeat(2, 1fr);
14
- width: 80%;
15
- padding: spacing('3');
16
- gap: spacing('6');
17
- }
18
-
19
- @include media-up('lg') {
20
- grid-template-columns: repeat(3, 1fr);
21
- }
22
-
23
- // The page gutter is the container's job, a second inset only narrows the tile
24
- &-4col {
25
- @include media-up('sm') {
26
- width: 85%;
27
- grid-template-columns: repeat(2, 1fr);
28
- }
29
-
30
- @include media-up('md') {
31
- grid-template-columns: repeat(3, 1fr);
32
- }
33
-
34
- @include media-up('lg') {
35
- grid-template-columns: repeat(4, 1fr);
36
- }
37
- }
38
-
39
- &-2col {
40
- @include media-up('sm') {
41
- width: 75%;
42
- grid-template-columns: repeat(2, 1fr);
43
- }
44
- }
45
- }
46
-
47
4
  // The tile sizes its type off its own width, not the window's. In
48
- // `.mw-tiles-4col` on a wide desktop a tile is a quarter of 85% of the page -
5
+ // `.mw-tiles-4col` on a wide desktop a tile is a quarter of the container -
49
6
  // narrower than the same tile alone on a phone, where it used to be the one
50
7
  // getting the small type.
51
8
  //
@@ -69,7 +26,6 @@
69
26
  overflow: hidden;
70
27
  display: flex;
71
28
  flex-direction: column;
72
- cursor: pointer;
73
29
 
74
30
  // Ahead of the size modifiers below, which take over from 'sm'
75
31
  @include media-up('sm') {
@@ -90,13 +46,22 @@
90
46
 
91
47
  @include hover-move {
92
48
  transform: translateY(-2px);
49
+
50
+ // The whole tile is the target, unlike a card where the image frame
51
+ // owns its own hover - a tile is one face, not a stack of regions.
52
+ .mw-tile-img img {
53
+ transform: scale(1.05);
54
+ }
93
55
  }
94
56
 
95
57
  // A tile is tapped, so it needs a press of its own - the lift above never
96
- // runs on a finger.
97
- &:active {
98
- transform: scale(0.985);
99
- transition-duration: var(--mw-duration-instant);
58
+ // runs on a finger. Guarded like every other travel in the framework; what
59
+ // is left without it is the latched hover, which is feedback enough.
60
+ @media (prefers-reduced-motion: no-preference) {
61
+ &:active {
62
+ transform: scale(0.985);
63
+ transition-duration: var(--mw-duration-instant);
64
+ }
100
65
  }
101
66
 
102
67
  &-img {
@@ -145,6 +110,13 @@
145
110
  font-size: font-size('sm');
146
111
  line-height: 1.3;
147
112
 
113
+ // The ramp the header takes: a tile filling a phone is a wide
114
+ // container and reads at full size, one squeezed into a desktop
115
+ // column stays small.
116
+ @container mw-tile (width >= #{$_mw-tile-narrow}) {
117
+ font-size: font-size('base');
118
+ }
119
+
148
120
  &:last-child {
149
121
  margin-bottom: spacing('0');
150
122
  }
@@ -186,6 +158,10 @@
186
158
 
187
159
  p {
188
160
  font-size: font-size('xs');
161
+
162
+ @container mw-tile (width >= #{$_mw-tile-narrow}) {
163
+ font-size: font-size('sm');
164
+ }
189
165
  }
190
166
  }
191
167
  }
@@ -225,6 +201,24 @@
225
201
  }
226
202
  }
227
203
 
204
+ // A tile is only a control when the markup makes it one. The look above is a
205
+ // surface and stays on a div; everything a finger or a keyboard needs sits
206
+ // here, where the element itself answers for it - a hand cursor on a div
207
+ // promises a click nothing handles, and a ring on it would never be reached.
208
+ a.mw-tile,
209
+ button.mw-tile {
210
+ cursor: pointer;
211
+ // A button brings its own font, alignment and colour, and the tile's own
212
+ // children set none of the three
213
+ font: inherit;
214
+ color: inherit;
215
+ text-align: inherit;
216
+ text-decoration: none;
217
+
218
+ @include focus-ring;
219
+ @include tappable;
220
+ }
221
+
228
222
  // A cramped tile reads better ranged left whatever the window is doing - a
229
223
  // centred line inside 200px is two words and a gap. Every selector here is a
230
224
  // child: a container cannot answer a query about itself, and the tile *is* the
@@ -0,0 +1,193 @@
1
+ @use 'sass:math';
2
+ @use '../abstracts' as *;
3
+
4
+ @layer mw.components {
5
+ // A pseudo element on the trigger, so an ancestor with `overflow: hidden` cuts
6
+ // it off. Hidden means `display: none`: a laid-out box still counts towards the
7
+ // page's overflow, and a 280px bubble near the right edge added 50px of sideways
8
+ // scroll on a phone. Hence `allow-discrete` and the starting style.
9
+ $_mw-tooltip-arrow: 6px;
10
+ $_mw-tooltip-gap: 8px;
11
+ // A square turned 45deg rather than a border triangle: it reads the same from
12
+ // above and from below, so the second placement costs no second arrow and a
13
+ // flip never has to repaint anything. The diagonal carries the visible width,
14
+ // so the side is the visible height doubled over root two.
15
+ $_mw-tooltip-arrow-side: math.div($_mw-tooltip-arrow * 2, math.sqrt(2));
16
+ // Half a side short of where the triangle's edge sat, so the turned square is
17
+ // centred on the bubble's edge and only its outer half shows. The inner half
18
+ // lands on the bubble in the same colour, which is what makes it read as one
19
+ // shape rather than a diamond stuck to a box.
20
+ $_mw-tooltip-arrow-inset: $_mw-tooltip-gap +
21
+ $_mw-tooltip-arrow - math.div($_mw-tooltip-arrow-side, 2);
22
+
23
+ [data-tooltip] {
24
+ position: relative;
25
+
26
+ &::before {
27
+ content: attr(data-tooltip);
28
+ position: absolute;
29
+ bottom: 100%;
30
+ left: 50%;
31
+ transform: translateX(-50%);
32
+ margin-bottom: $_mw-tooltip-gap + $_mw-tooltip-arrow;
33
+ max-width: min(280px, 90vw);
34
+ width: max-content;
35
+ padding: spacing('1') spacing('2');
36
+ background: var(--mw-gray-color);
37
+ color: $fixed-surface-text;
38
+ font-size: font-size('xs');
39
+ border-radius: radius('xs');
40
+ box-shadow: var(--mw-elevation-2);
41
+ pointer-events: none;
42
+ opacity: 0;
43
+ display: none;
44
+ transition:
45
+ opacity var(--mw-duration-fast) var(--mw-ease-out),
46
+ display var(--mw-duration-fast) allow-discrete;
47
+ z-index: z-index('tooltip');
48
+ }
49
+
50
+ &::after {
51
+ content: '';
52
+ position: absolute;
53
+ bottom: 100%;
54
+ left: 50%;
55
+ transform: translateX(-50%) rotate(45deg);
56
+ margin-bottom: $_mw-tooltip-arrow-inset;
57
+ width: $_mw-tooltip-arrow-side;
58
+ height: $_mw-tooltip-arrow-side;
59
+ background: var(--mw-gray-color);
60
+ opacity: 0;
61
+ display: none;
62
+ transition:
63
+ opacity var(--mw-duration-fast) var(--mw-ease-out),
64
+ display var(--mw-duration-fast) allow-discrete;
65
+ z-index: z-index('tooltip');
66
+ }
67
+
68
+ // The placement API: the text is the attribute, the side is a class. The
69
+ // three steer different properties - block axis, inline axis - so `below`
70
+ // and `end` compose without a rule for the pairing.
71
+ //
72
+ // They place the bubble against the trigger rather than joining the anchored
73
+ // path below, and that is the point, not a shortcut. The reason to want them
74
+ // is a control in a fixed header, and that header carries a backdrop-filter,
75
+ // which makes it the containing block for anything `fixed` inside it.
76
+ // `block-end` is then the few pixels between the trigger and the header's own
77
+ // bottom edge, the bubble does not fit, and it gets pushed back over the
78
+ // trigger it belongs to. What this gives up is the clipping immunity the
79
+ // anchored path buys - a modifier inside a scroll container is cut off.
80
+ //
81
+ // Opting out has to restore the placement, not just the positioning scheme:
82
+ // the anchored path below is the later rule and clears `bottom`, `top`,
83
+ // `left` and the centring transform so `position-area` can resolve them
84
+ // itself. Leave that cleared and the box falls to its static position, which
85
+ // for a pseudo element is inside the trigger it is supposed to sit above.
86
+ // The three rules under this one then move the one edge each of them owns.
87
+ &:is(.mw-tooltip-below, .mw-tooltip-end, .mw-tooltip-start) {
88
+ &::before,
89
+ &::after {
90
+ position: absolute;
91
+ position-area: none;
92
+ bottom: 100%;
93
+ top: auto;
94
+ left: 50%;
95
+ }
96
+
97
+ &::before {
98
+ transform: translateX(-50%);
99
+ }
100
+
101
+ &::after {
102
+ transform: translateX(-50%) rotate(45deg);
103
+ }
104
+ }
105
+
106
+ &.mw-tooltip-below {
107
+ &::before {
108
+ top: 100%;
109
+ bottom: auto;
110
+ margin-top: $_mw-tooltip-gap + $_mw-tooltip-arrow;
111
+ margin-bottom: 0;
112
+ }
113
+
114
+ &::after {
115
+ top: 100%;
116
+ bottom: auto;
117
+ margin-top: $_mw-tooltip-arrow-inset;
118
+ margin-bottom: 0;
119
+ }
120
+ }
121
+
122
+ // Only the bubble moves - the arrow stays centred on the trigger it points at.
123
+ &.mw-tooltip-end::before {
124
+ left: auto;
125
+ right: 0;
126
+ transform: none;
127
+ }
128
+
129
+ &.mw-tooltip-start::before {
130
+ left: 0;
131
+ right: auto;
132
+ transform: none;
133
+ }
134
+
135
+ &:hover::before,
136
+ &:hover::after,
137
+ &:focus-visible::before,
138
+ &:focus-visible::after {
139
+ opacity: 1;
140
+ display: block;
141
+
142
+ @starting-style {
143
+ opacity: 0;
144
+ }
145
+ }
146
+
147
+ // A press usually scales the trigger, and a transform makes it a containing
148
+ // block for the fixed-position bubble above - which would drop the label
149
+ // into the element being pressed. So it steps aside for the click instead;
150
+ // by then it has been read anyway. After the hover rule, same weight.
151
+ &:active::before,
152
+ &:active::after {
153
+ opacity: 0;
154
+ display: none;
155
+ // Not transitioned: `allow-discrete` holds `display: block` until the fade
156
+ // ends, and the bubble would spend those 180ms visibly sitting in the
157
+ // element. Going back out on release keeps the fade - that one is wanted.
158
+ transition: none;
159
+ }
160
+
161
+ // The clipping caveat above, lifted where the browser can do it: `fixed` takes
162
+ // the bubble out of the trigger's containing block, anchor positioning keeps
163
+ // it attached. No `anchor-name` needed - a pseudo element is implicitly
164
+ // anchored to the element it comes from.
165
+ //
166
+ // Still no `position-try-fallbacks: flip-block`, and no longer because of the
167
+ // arrow - the turned square survives a flip. Bubble and arrow are two
168
+ // separately anchored boxes, so each would decide on its own whether it fits,
169
+ // and the tiny one nearly always does: the label flips, the arrow stays.
170
+ @supports (position-area: block-start) {
171
+ &::before,
172
+ &::after {
173
+ position: fixed;
174
+ // The absolute placement these replace - left as-is they fight the
175
+ // position-area, which resolves the inset properties itself.
176
+ bottom: auto;
177
+ left: auto;
178
+ top: auto;
179
+ }
180
+
181
+ &::before {
182
+ position-area: block-start;
183
+ transform: none;
184
+ }
185
+
186
+ // The turn stays - it is the arrow's shape, not its placement.
187
+ &::after {
188
+ position-area: block-start;
189
+ transform: rotate(45deg);
190
+ }
191
+ }
192
+ }
193
+ }