maverick-wave 4.28.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.claude/settings.local.json +29 -1
  2. package/.claude/skills/mw-maverick-wave/SKILL.md +39 -24
  3. package/.claude/skills/mw-maverick-wave/examples/angular-services.md +16 -4
  4. package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +13 -4
  5. package/.claude/skills/mw-maverick-wave/references/components.md +42 -5
  6. package/.claude/skills/mw-maverick-wave/references/forms.md +14 -2
  7. package/.claude/skills/mw-maverick-wave/references/javascript.md +17 -4
  8. package/.claude/skills/mw-maverick-wave/references/layout.md +53 -19
  9. package/.claude/skills/mw-maverick-wave/references/theming.md +38 -1
  10. package/.impeccable/config.local.json +5 -0
  11. package/.impeccable/hook.cache.json +1 -0
  12. package/CHANGELOG.md +11 -0
  13. package/CLAUDE.md +5 -5
  14. package/README.md +48 -4
  15. package/index.html +19 -38
  16. package/maverick-wave.min.css +14 -12
  17. package/maverick-wave.min.js +1 -1
  18. package/package.json +2 -2
  19. package/scripts/verify.js +27 -2
  20. package/src/js/main.js +223 -83
  21. package/src/partials/accordions-container.html +6 -10
  22. package/src/partials/alerts-container.html +6 -6
  23. package/src/partials/announcement-container.html +5 -5
  24. package/src/partials/avatars-container.html +85 -17
  25. package/src/partials/badges-container.html +6 -6
  26. package/src/partials/blog-posts-container.html +11 -12
  27. package/src/partials/buttons-container.html +3 -3
  28. package/src/partials/cards-container.html +26 -9
  29. package/src/partials/code-container.html +5 -5
  30. package/src/partials/content-slider-container.html +5 -4
  31. package/src/partials/divider-container.html +7 -7
  32. package/src/partials/dropdown-container.html +2 -2
  33. package/src/partials/empty-state-container.html +5 -5
  34. package/src/partials/footer-container.html +21 -18
  35. package/src/partials/form-elements-container.html +14 -14
  36. package/src/partials/gallery-container.html +16 -8
  37. package/src/partials/get-started-container.html +4 -8
  38. package/src/partials/header-container.html +18 -7
  39. package/src/partials/home-container.html +10 -20
  40. package/src/partials/input-group-container.html +11 -2
  41. package/src/partials/kbd-container.html +1 -1
  42. package/src/partials/login-container.html +5 -1
  43. package/src/partials/media-container.html +2 -2
  44. package/src/partials/modals-container.html +22 -32
  45. package/src/partials/pricing-container.html +9 -9
  46. package/src/partials/skeleton-container.html +6 -6
  47. package/src/partials/stepper-container.html +2 -2
  48. package/src/partials/tables-container.html +12 -12
  49. package/src/partials/tabs-container.html +17 -27
  50. package/src/partials/tags-container.html +23 -17
  51. package/src/partials/techstack-bucket-container.html +13 -0
  52. package/src/partials/tiles-container.html +26 -9
  53. package/src/partials/typography-container.html +7 -6
  54. package/src/partials/utilities-container.html +35 -15
  55. package/src/scss/_layers.scss +10 -0
  56. package/src/scss/abstracts/_index.scss +6 -0
  57. package/src/scss/abstracts/_mixins.scss +61 -153
  58. package/src/scss/abstracts/_variables.scss +123 -319
  59. package/src/scss/base/_base.scss +170 -190
  60. package/src/scss/base/_reset.scss +106 -104
  61. package/src/scss/base/_typography.scss +153 -151
  62. package/src/scss/components/_accordions.scss +97 -97
  63. package/src/scss/components/_alerts.scss +71 -70
  64. package/src/scss/components/_announcement.scss +71 -70
  65. package/src/scss/components/_avatars.scss +130 -142
  66. package/src/scss/components/_badge.scss +131 -132
  67. package/src/scss/components/_blog-post.scss +223 -220
  68. package/src/scss/components/_breadcrumbs.scss +93 -92
  69. package/src/scss/components/_button-bar.scss +102 -110
  70. package/src/scss/components/_buttons.scss +319 -324
  71. package/src/scss/components/_calendar.scss +251 -252
  72. package/src/scss/components/_cards.scss +359 -386
  73. package/src/scss/components/_code.scss +190 -194
  74. package/src/scss/components/_coming-soon.scss +77 -75
  75. package/src/scss/components/_content-slider.scss +90 -89
  76. package/src/scss/components/_divider.scss +55 -53
  77. package/src/scss/components/_dropdown.scss +179 -185
  78. package/src/scss/components/_empty-state.scss +57 -57
  79. package/src/scss/components/_flag.scss +294 -278
  80. package/src/scss/components/_gallery.scss +131 -133
  81. package/src/scss/components/_info.scss +151 -167
  82. package/src/scss/components/_kanban.scss +332 -341
  83. package/src/scss/components/_kbd.scss +40 -38
  84. package/src/scss/components/_lang-switch.scss +89 -97
  85. package/src/scss/components/_links.scss +21 -28
  86. package/src/scss/components/_lists.scss +510 -514
  87. package/src/scss/components/_localhost-indicator.scss +26 -24
  88. package/src/scss/components/_media.scss +22 -20
  89. package/src/scss/components/_meta-info.scss +35 -33
  90. package/src/scss/components/_modals.scss +277 -186
  91. package/src/scss/components/_pagination.scss +81 -87
  92. package/src/scss/components/_panels.scss +99 -97
  93. package/src/scss/components/_pricing.scss +183 -241
  94. package/src/scss/components/_progress.scss +112 -89
  95. package/src/scss/components/_prose.scss +35 -33
  96. package/src/scss/components/_ratings.scss +40 -38
  97. package/src/scss/components/_segmented.scss +82 -84
  98. package/src/scss/components/_skeleton.scss +66 -72
  99. package/src/scss/components/_spinners.scss +155 -159
  100. package/src/scss/components/_stepper.scss +116 -116
  101. package/src/scss/components/_tables.scss +155 -148
  102. package/src/scss/components/_tabs.scss +232 -141
  103. package/src/scss/components/_tags.scss +109 -107
  104. package/src/scss/components/_techstack-bucket.scss +123 -122
  105. package/src/scss/components/_testimonial.scss +72 -70
  106. package/src/scss/components/_theme-toggle.scss +53 -51
  107. package/src/scss/components/_tiles.scss +174 -174
  108. package/src/scss/components/_timelines.scss +282 -280
  109. package/src/scss/components/_toasts.scss +66 -72
  110. package/src/scss/form-elements/_checkbox.scss +117 -132
  111. package/src/scss/form-elements/_form.scss +149 -115
  112. package/src/scss/form-elements/_input-group.scss +99 -103
  113. package/src/scss/form-elements/_input.scss +74 -74
  114. package/src/scss/form-elements/_login.scss +48 -48
  115. package/src/scss/form-elements/_prefilled.scss +53 -51
  116. package/src/scss/form-elements/_radio.scss +95 -107
  117. package/src/scss/form-elements/_select.scss +42 -42
  118. package/src/scss/form-elements/_slider.scss +117 -132
  119. package/src/scss/form-elements/_textarea.scss +68 -54
  120. package/src/scss/form-elements/_toggle.scss +127 -140
  121. package/src/scss/layout/_footer.scss +172 -171
  122. package/src/scss/layout/_grid.scss +268 -302
  123. package/src/scss/layout/_header.scss +426 -427
  124. package/src/scss/layout/_home.scss +28 -27
  125. package/src/scss/layout/_main.scss +275 -238
  126. package/src/scss/layout/_page-header.scss +38 -36
  127. package/src/scss/layout/_section.scss +150 -124
  128. package/src/scss/main.scss +1 -1
  129. package/src/scss/utilities/_accessibility.scss +46 -45
  130. package/src/scss/utilities/_aspect.scss +22 -20
  131. package/src/scss/utilities/_corner.scss +23 -5
  132. package/src/scss/utilities/_display.scss +76 -70
  133. package/src/scss/utilities/_elevation.scss +13 -11
  134. package/src/scss/utilities/_flex.scss +83 -85
  135. package/src/scss/utilities/_reveal.scss +40 -63
  136. package/src/scss/utilities/_spacing.scss +63 -64
  137. package/src/scss/utilities/_text.scss +139 -141
  138. package/src/scss/utilities/_touch-targets.scss +130 -147
@@ -1,40 +1,41 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
- .mw-home {
4
- justify-items: center;
5
- align-items: center;
6
- text-align: center;
3
+ @layer mw.layout {
4
+ .mw-home {
5
+ justify-items: center;
6
+ align-items: center;
7
+ text-align: center;
7
8
 
8
- &-content {
9
- padding: spacing('6');
10
- @include media-down('sm') {
9
+ &-content {
11
10
  padding: spacing('9') spacing('4');
12
- }
11
+ @include media-up('sm') {
12
+ padding: spacing('6');
13
+ }
13
14
 
14
- // choose mw-home-content or mw-home-content-fade
15
- &-fade {
16
- @extend .mw-home-content;
17
- animation: fadeIn var(--mw-duration-slower) var(--mw-ease-out);
15
+ &-fade {
16
+ @extend .mw-home-content;
17
+ animation: fadeIn var(--mw-duration-slower) var(--mw-ease-out);
18
+ }
18
19
  }
19
- }
20
20
 
21
- &-text {
22
- width: min(75%, 700px);
23
- position: relative;
24
- margin: spacing('0') auto spacing('4');
25
- @include media-down('sm') {
21
+ &-text {
26
22
  width: 85%;
23
+ position: relative;
24
+ margin: spacing('0') auto spacing('4');
25
+ @include media-up('sm') {
26
+ width: min(75%, 700px);
27
+ }
27
28
  }
28
29
  }
29
- }
30
30
 
31
- @keyframes fadeIn {
32
- from {
33
- opacity: 0;
34
- transform: translateX(-8px) translateY(-23px);
35
- }
36
- to {
37
- opacity: 1;
38
- transform: translateY(0);
31
+ @keyframes fadeIn {
32
+ from {
33
+ opacity: 0;
34
+ transform: translateX(-8px) translateY(-23px);
35
+ }
36
+ to {
37
+ opacity: 1;
38
+ transform: translateY(0);
39
+ }
39
40
  }
40
41
  }
@@ -1,282 +1,319 @@
1
1
  @use '../abstracts' as *;
2
2
 
3
- .mw-main {
4
- margin: spacing('0');
5
- padding: spacing('0');
6
- min-height: 100dvh;
7
- display: flex;
8
- flex-direction: column;
9
- }
10
-
11
- .mw-container {
12
- width: var(--mw-container-width);
13
- margin: spacing('0') auto;
14
- padding: spacing('0');
15
-
16
- // add a hero part for a full size background picture
17
- @supports (selector(:has(.mw-hero))) {
18
- // if it has *any* descendant .mw-hero
19
- &:has(.mw-hero) {
20
- position: relative;
21
- width: 100%;
22
- min-height: 100dvh;
23
- display: flex;
24
- align-items: center;
25
- justify-content: center;
26
- background-size: cover;
27
- background-position: center;
28
- background-image: var(--mw-hero-background);
29
-
30
- // The image does not change with the theme, so neither does the ink on
31
- // it - see $mw-hero-text-color. Set on the container so a hero without
32
- // .mw-home still gets it.
33
- color: var(--mw-hero-text-color);
3
+ @layer mw.layout {
4
+ .mw-main {
5
+ margin: spacing('0');
6
+ padding: spacing('0');
7
+ min-height: 100dvh;
8
+ display: flex;
9
+ flex-direction: column;
10
+ }
34
11
 
35
- // The theme treatment of the photo - see $hero-filter-dark. backdrop-filter
36
- // and not a background colour, because it has to change how the image
37
- // *looks* rather than lay something over it: an overlay on a dark photo
38
- // reads the same in both themes until its alpha is high enough to become
39
- // the colour you see. Filtering here rather than on the container keeps
40
- // the content in front of it unfiltered.
41
- &::before {
42
- content: '';
43
- position: absolute;
44
- inset: 0;
45
- backdrop-filter: var(--mw-hero-image-filter);
46
- z-index: z-index('lift');
47
- pointer-events: none;
48
- transition: var(--mw-transition);
49
- }
12
+ .mw-container {
13
+ width: var(--mw-container-width);
14
+ margin: spacing('0') auto;
15
+ padding: spacing('0');
50
16
 
51
- // ...so the content has to sit above it
52
- > * {
17
+ @supports (selector(:has(.mw-hero))) {
18
+ &:has(.mw-hero) {
53
19
  position: relative;
54
- z-index: z-index('card');
55
- }
20
+ width: 100%;
21
+ min-height: 100dvh;
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ background-size: cover;
26
+ background-position: center;
27
+ background-image: var(--mw-hero-background);
56
28
 
57
- // ...except for anything that paints its own surface. A card inside the
58
- // hero is still a card on --mw-card-background and takes the theme's ink,
59
- // or its text would be light on a light card in the light theme.
60
- .mw-card,
61
- .mw-panel,
62
- .mw-modal,
63
- .mw-tile,
64
- .mw-calendar:not(.mw-calendar-plain) {
65
- color: var(--mw-text-color);
66
- }
29
+ // Set on the container so a hero without .mw-home gets it too
30
+ color: var(--mw-hero-text-color);
67
31
 
68
- // The header is fixed, so it floats over the hero and centering against
69
- // the full viewport hides everything that reaches above the first line -
70
- // a version badge, a kicker - underneath the bar. Center against what is
71
- // left below the header instead. Guarded, because a hero without a header
72
- // would otherwise sit off-center for no reason.
73
- body:has(.mw-header) & {
74
- padding-top: calc(var(--mw-header-height) + #{spacing('7')});
75
- }
32
+ // backdrop-filter and not an overlay - see $hero-filter-dark. On the pseudo
33
+ // element, so the content in front of the image stays unfiltered.
34
+ &::before {
35
+ content: '';
36
+ position: absolute;
37
+ inset: 0;
38
+ backdrop-filter: var(--mw-hero-image-filter);
39
+ z-index: z-index('lift');
40
+ pointer-events: none;
41
+ transition: var(--mw-transition);
42
+ }
76
43
 
77
- // The scroll cue is taken out of flow, so the content above knows
78
- // nothing about it and a hero with buttons under its text parks them
79
- // right on top of the cue. Keep its strip free instead - the hero
80
- // centres its content in what is left, which is what a reader expects
81
- // anyway. Only when there actually is a cue, or every other hero would
82
- // pay for a feature it does not use.
83
- &:has(> .mw-scroll-hint) {
84
- padding-bottom: calc(
85
- var(--mw-scroll-hint-offset) + var(--mw-scroll-hint-reserve)
86
- );
87
- }
44
+ // ...so the content has to sit above it
45
+ > * {
46
+ position: relative;
47
+ z-index: z-index('card');
48
+ }
88
49
 
89
- // ...except when the cue sits in the corner. That is the whole point of
90
- // the end variant: it stands beside the content, not under it, so the
91
- // hero keeps the full height it had before.
92
- &:has(> .mw-scroll-hint-end) {
93
- padding-bottom: spacing('0');
94
- }
50
+ // The same shadow the scroll cue already carries: the light theme lifts
51
+ // the photo to brightness(1.1) and pale ink alone stops holding its edge
52
+ h1,
53
+ h2,
54
+ h3,
55
+ p {
56
+ text-shadow: 0 1px 4px rgb(0 0 0 / 65%);
57
+ }
95
58
 
96
- // On a phone even the corner is tight - the hero text is taller for the
97
- // width it has, so both variants get the strip back. Last of the three,
98
- // because a media query adds no specificity and would otherwise lose to
99
- // the end rule above.
100
- //
101
- // A smaller strip than on the desktop, though: a phone screen is barely
102
- // taller than the hero text on it, so the full reserve is what tips the
103
- // container past 100dvh - and everything below that line, the cue first,
104
- // is off screen. min-height already leaves room here in most cases.
105
- @include media-down('md') {
106
- &:has(> .mw-scroll-hint) {
107
- --mw-scroll-hint-reserve: #{spacing('6')};
59
+ // ...except anything that paints its own surface: a card in the hero is
60
+ // still a card, or its text is light on a light card in the light theme
61
+ .mw-card,
62
+ .mw-panel,
63
+ .mw-modal,
64
+ .mw-tile,
65
+ .mw-calendar:not(.mw-calendar-plain) {
66
+ color: var(--mw-text-color);
67
+
68
+ h1,
69
+ h2,
70
+ h3,
71
+ p {
72
+ text-shadow: none;
73
+ }
74
+ }
75
+
76
+ // ...and except the outline button, for the opposite reason: its tone is
77
+ // picked to read on the page background and goes quiet over a photograph.
78
+ // The hover is restated because this selector is exactly as specific as
79
+ // `.mw-btn-outline:hover:not(:disabled)` and the layer order beats it.
80
+ .mw-btn-outline {
81
+ border-color: color-mix(
82
+ in srgb,
83
+ var(--mw-hero-text-color) 45%,
84
+ transparent
85
+ );
86
+ background: rgb(0 0 0 / 30%);
87
+ backdrop-filter: blur(6px);
88
+ color: var(--mw-hero-text-color);
108
89
 
90
+ &:hover:not(:disabled) {
91
+ background: var(--mw-primary-color);
92
+ border-color: var(--mw-primary-color);
93
+ color: var(--mw-primary-accent-text-color);
94
+ }
95
+ }
96
+
97
+ // The bar floats over the hero, so centring against the full viewport hides
98
+ // a badge or a kicker underneath it. Guarded - no header, no offset.
99
+ body:has(.mw-header) & {
100
+ padding-top: calc(var(--mw-header-height) + #{spacing('7')});
101
+ }
102
+
103
+ // The cue is out of flow, so buttons under the hero text would park on top
104
+ // of it. Only when there is one, or every hero pays for a feature it lacks.
105
+ &:has(> .mw-scroll-hint) {
109
106
  padding-bottom: calc(
110
107
  var(--mw-scroll-hint-offset) + var(--mw-scroll-hint-reserve)
111
108
  );
112
109
  }
110
+
111
+ // ...except in the corner: the end variant stands beside the content
112
+ &:has(> .mw-scroll-hint-end) {
113
+ padding-bottom: spacing('0');
114
+ }
115
+
116
+ // On a phone the corner is tight too, so both variants get the strip back.
117
+ // Last of the three: a media query adds no specificity and would lose to
118
+ // the end rule above. Smaller than on the desktop, or the full reserve
119
+ // tips the container past 100dvh and pushes the cue off screen.
120
+ @include media-down('md') {
121
+ &:has(> .mw-scroll-hint) {
122
+ --mw-scroll-hint-reserve: #{spacing('6')};
123
+
124
+ padding-bottom: calc(
125
+ var(--mw-scroll-hint-offset) + var(--mw-scroll-hint-reserve)
126
+ );
127
+ }
128
+ }
113
129
  }
114
130
  }
115
131
  }
116
- }
117
132
 
118
- // The "scroll down" cue at the bottom of a hero. It belongs to the hero
119
- // container and not to the flow below it: a hero fills the viewport, so the
120
- // cue has to sit on its lower edge no matter how tall the content above is.
121
- // Selector beats the `> *` rule above it, which would otherwise put the
122
- // element back into flow.
123
- .mw-container:has(.mw-hero) > .mw-scroll-hint {
124
- position: absolute;
125
- bottom: var(--mw-scroll-hint-offset);
126
- left: 0;
127
- right: 0;
128
- width: fit-content;
129
- margin-inline: auto;
130
- display: flex;
131
- flex-direction: column;
132
- align-items: center;
133
- gap: spacing('2');
134
- color: color-mix(in srgb, var(--mw-hero-text-color) 72%, transparent);
135
- font-size: font-size('xs');
136
- letter-spacing: 0.08em;
137
- text-transform: uppercase;
138
- text-decoration: none;
139
- animation: mw-scroll-hint-float 2.2s var(--mw-ease-in-out) infinite;
140
- text-shadow: 0 1px 4px rgb(0 0 0 / 65%);
141
-
142
- // The arrow carries the meaning, the word only confirms it - so it is the
143
- // arrow that gets the size, and the label stays quiet at 'xs'.
144
- > i,
145
- > svg {
146
- font-size: font-size('lg');
133
+ // Glass rather than a fill: on a hero image a solid badge is the loudest thing
134
+ // on the screen and plain text sinks into the picture.
135
+ .mw-hero-badge {
136
+ display: inline-flex;
137
+ align-items: center;
138
+ justify-content: center;
139
+ gap: spacing('3');
140
+ max-width: 100%;
141
+ margin-bottom: spacing('6');
142
+ padding: spacing('2') spacing('5');
143
+ border: 1px solid #{fade('hero-text-color', 22%)};
144
+ border-radius: 999px;
145
+ background: fade('hero-text-color', 8%);
146
+ backdrop-filter: blur(4px);
147
+ color: fade('hero-text-color', 88%);
148
+ font-size: font-size('sm');
149
+ font-weight: font-weight('medium');
150
+ letter-spacing: 0.02em;
147
151
  }
148
152
 
149
- @include hover {
150
- color: var(--mw-hero-text-color);
153
+ // Carries the secondary colour, because the badge is a state, not a label -
154
+ // override `background` for a different one
155
+ .mw-hero-badge-dot {
156
+ width: 8px;
157
+ height: 8px;
158
+ flex-shrink: 0;
159
+ border-radius: radius('full');
160
+ background: var(--mw-secondary-color);
151
161
  }
152
- }
153
-
154
- // Bottom right instead of centred, for a hero whose text is not centred
155
- // either - the two would otherwise sit on top of each other.
156
- .mw-container:has(.mw-hero) > .mw-scroll-hint-end {
157
- left: auto;
158
- right: var(--mw-scroll-hint-inline);
159
- margin-inline: 0;
160
- }
161
162
 
162
- // On a phone the middle under the hero is taken: the text is longer for the
163
- // width it has and any buttons reach close to the bottom edge. The corner next
164
- // to them is free, so both variants go there and the cue stays on screen.
165
- @include media-down('md') {
163
+ // Belongs to the hero container, not to the flow below it, so the cue sits on
164
+ // the lower edge whatever the content does. Beats the `> *` rule above it.
166
165
  .mw-container:has(.mw-hero) > .mw-scroll-hint {
166
+ position: absolute;
167
+ bottom: var(--mw-scroll-hint-offset);
167
168
  left: auto;
168
169
  right: var(--mw-scroll-hint-inline);
170
+ width: fit-content;
169
171
  margin-inline: 0;
170
- }
171
- }
172
+ display: flex;
173
+ flex-direction: column;
174
+ align-items: center;
175
+ gap: spacing('2');
176
+ color: color-mix(in srgb, var(--mw-hero-text-color) 72%, transparent);
177
+ font-size: font-size('xs');
178
+ letter-spacing: 0.08em;
179
+ text-transform: uppercase;
180
+ text-decoration: none;
181
+ animation: mw-scroll-hint-float 2.2s var(--mw-ease-in-out) infinite;
182
+ text-shadow: 0 1px 4px rgb(0 0 0 / 65%);
172
183
 
173
- @keyframes mw-scroll-hint-float {
174
- 0%,
175
- 100% {
176
- transform: translateY(0);
177
- }
178
- 50% {
179
- transform: translateY(7px);
184
+ // The arrow carries the meaning, so it gets the size and the label stays quiet
185
+ > i,
186
+ > svg {
187
+ font-size: font-size('lg');
188
+ }
189
+
190
+ @include hover {
191
+ color: var(--mw-hero-text-color);
192
+ }
180
193
  }
181
- }
182
194
 
183
- // Same selector as the rule above on purpose: a media query adds no
184
- // specificity, so a bare `.mw-scroll-hint` here loses to the `.mw-container`
185
- // chain and the cue keeps bobbing for everyone who asked it not to.
186
- @media (prefers-reduced-motion: reduce) {
187
- .mw-container:has(.mw-hero) > .mw-scroll-hint {
188
- animation: none;
195
+ // For a hero whose text is not centred either, or the two sit on each other
196
+ .mw-container:has(.mw-hero) > .mw-scroll-hint-end {
197
+ left: auto;
198
+ right: var(--mw-scroll-hint-inline);
199
+ margin-inline: 0;
189
200
  }
190
- }
191
201
 
192
- .mw-content {
193
- flex: 1;
194
- padding-top: spacing('7');
195
- }
202
+ // Both variants start in the corner, because on a phone the middle under the
203
+ // hero is taken. Only the plain one comes back - the end variant belongs there,
204
+ // and the rule above reserves no strip for it.
205
+ @include media-up('md') {
206
+ .mw-container:has(.mw-hero) > .mw-scroll-hint:not(.mw-scroll-hint-end) {
207
+ left: 0;
208
+ right: 0;
209
+ margin-inline: auto;
210
+ }
211
+ }
196
212
 
197
- .mw-content-centered {
198
- min-height: 100dvh;
199
- display: flex;
200
- flex-direction: column;
201
- justify-content: center;
202
- }
213
+ @keyframes mw-scroll-hint-float {
214
+ 0%,
215
+ 100% {
216
+ transform: translateY(0);
217
+ }
218
+ 50% {
219
+ transform: translateY(7px);
220
+ }
221
+ }
203
222
 
204
- // tooltip - use it everywhere you want: put data-tooltip="..." on any element.
205
- // Pure CSS, no script needed. The flip side of that: it is a pseudo element on
206
- // the trigger, so an ancestor with `overflow: hidden` (a card, a scroll
207
- // container) cuts it off.
208
- //
209
- // Hidden means `display: none`, not `visibility: hidden`. A box that is hidden
210
- // but still laid out counts towards the page's scrollable overflow: a 280px
211
- // bubble centred on a trigger near the right edge added 50px of sideways scroll
212
- // to a 320px screen, on a page where nothing visible was out of place. Coming
213
- // back from `display: none` is a discrete transition, hence `allow-discrete`
214
- // and the starting style - a browser that knows neither simply drops the
215
- // transition and shows the bubble at once.
216
- $_mw-tooltip-arrow: 6px;
217
- $_mw-tooltip-gap: 8px;
218
-
219
- [data-tooltip] {
220
- position: relative;
221
-
222
- &::before {
223
- content: attr(data-tooltip);
224
- position: absolute;
225
- bottom: 100%;
226
- left: 50%;
227
- transform: translateX(-50%);
228
- margin-bottom: $_mw-tooltip-gap + $_mw-tooltip-arrow;
229
- max-width: min(280px, 90vw);
230
- width: max-content;
231
- padding: spacing('1') spacing('2');
232
- background: var(--mw-gray-color);
233
- color: $fixed-surface-text;
234
- font-size: font-size('xs');
235
- border-radius: radius('xs');
236
- box-shadow: var(--mw-elevation-2);
237
- pointer-events: none;
238
- opacity: 0;
239
- display: none;
240
- transition:
241
- opacity var(--mw-duration-fast) var(--mw-ease-out),
242
- display var(--mw-duration-fast) allow-discrete;
243
- z-index: z-index('tooltip');
223
+ .mw-content {
224
+ flex: 1;
225
+ padding-top: spacing('7');
244
226
  }
245
227
 
246
- &::after {
247
- content: '';
248
- position: absolute;
249
- bottom: 100%;
250
- left: 50%;
251
- transform: translateX(-50%);
252
- margin-bottom: $_mw-tooltip-gap;
253
- border: $_mw-tooltip-arrow solid transparent;
254
- border-bottom-width: 0;
255
- border-top-color: var(--mw-gray-color);
256
- opacity: 0;
257
- display: none;
258
- transition:
259
- opacity var(--mw-duration-fast) var(--mw-ease-out),
260
- display var(--mw-duration-fast) allow-discrete;
261
- z-index: z-index('tooltip');
228
+ .mw-content-centered {
229
+ min-height: 100dvh;
230
+ display: flex;
231
+ flex-direction: column;
232
+ justify-content: center;
262
233
  }
263
234
 
264
- &:hover::before,
265
- &:hover::after,
266
- &:focus-visible::before,
267
- &:focus-visible::after {
268
- opacity: 1;
269
- display: block;
235
+ // A pseudo element on the trigger, so an ancestor with `overflow: hidden` cuts
236
+ // it off. Hidden means `display: none`: a laid-out box still counts towards the
237
+ // page's overflow, and a 280px bubble near the right edge added 50px of sideways
238
+ // scroll on a phone. Hence `allow-discrete` and the starting style.
239
+ $_mw-tooltip-arrow: 6px;
240
+ $_mw-tooltip-gap: 8px;
270
241
 
271
- @starting-style {
242
+ [data-tooltip] {
243
+ position: relative;
244
+
245
+ &::before {
246
+ content: attr(data-tooltip);
247
+ position: absolute;
248
+ bottom: 100%;
249
+ left: 50%;
250
+ transform: translateX(-50%);
251
+ margin-bottom: $_mw-tooltip-gap + $_mw-tooltip-arrow;
252
+ max-width: min(280px, 90vw);
253
+ width: max-content;
254
+ padding: spacing('1') spacing('2');
255
+ background: var(--mw-gray-color);
256
+ color: $fixed-surface-text;
257
+ font-size: font-size('xs');
258
+ border-radius: radius('xs');
259
+ box-shadow: var(--mw-elevation-2);
260
+ pointer-events: none;
272
261
  opacity: 0;
262
+ display: none;
263
+ transition:
264
+ opacity var(--mw-duration-fast) var(--mw-ease-out),
265
+ display var(--mw-duration-fast) allow-discrete;
266
+ z-index: z-index('tooltip');
273
267
  }
274
- }
275
- }
276
268
 
277
- @media (prefers-reduced-motion: reduce) {
278
- [data-tooltip]::before,
279
- [data-tooltip]::after {
280
- transition: none;
269
+ &::after {
270
+ content: '';
271
+ position: absolute;
272
+ bottom: 100%;
273
+ left: 50%;
274
+ transform: translateX(-50%);
275
+ margin-bottom: $_mw-tooltip-gap;
276
+ border: $_mw-tooltip-arrow solid transparent;
277
+ border-bottom-width: 0;
278
+ border-top-color: var(--mw-gray-color);
279
+ opacity: 0;
280
+ display: none;
281
+ transition:
282
+ opacity var(--mw-duration-fast) var(--mw-ease-out),
283
+ display var(--mw-duration-fast) allow-discrete;
284
+ z-index: z-index('tooltip');
285
+ }
286
+
287
+ &:hover::before,
288
+ &:hover::after,
289
+ &:focus-visible::before,
290
+ &:focus-visible::after {
291
+ opacity: 1;
292
+ display: block;
293
+
294
+ @starting-style {
295
+ opacity: 0;
296
+ }
297
+ }
298
+
299
+ // The clipping caveat above, lifted where the browser can do it: `fixed` takes
300
+ // the bubble out of the trigger's containing block, anchor positioning keeps
301
+ // it attached. No `anchor-name` needed - a pseudo element is implicitly
302
+ // anchored to the element it comes from.
303
+ //
304
+ // No `position-try-fallbacks: flip-block` on purpose: the arrow is a border
305
+ // triangle and would still point up, into a bubble that flipped below.
306
+ @supports (position-area: block-start) {
307
+ &::before,
308
+ &::after {
309
+ position: fixed;
310
+ position-area: block-start;
311
+ // The absolute placement these replace - left as-is they fight the
312
+ // position-area, which resolves the inset properties itself.
313
+ bottom: auto;
314
+ left: auto;
315
+ transform: none;
316
+ }
317
+ }
281
318
  }
282
319
  }