maverick-wave 4.20.0 → 4.22.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 (48) hide show
  1. package/.claude/skills/maverick-wave/SKILL.md +9 -7
  2. package/.claude/skills/maverick-wave/references/components.md +85 -0
  3. package/.claude/skills/maverick-wave/references/layout.md +30 -7
  4. package/.claude/skills/maverick-wave/references/theming.md +1 -1
  5. package/CHANGELOG.md +17 -0
  6. package/index.html +40 -0
  7. package/package.json +2 -2
  8. package/src/assets/images/gallery-city.svg +49 -0
  9. package/src/assets/images/gallery-desert.svg +25 -0
  10. package/src/assets/images/gallery-forest.svg +46 -0
  11. package/src/assets/images/gallery-lake.svg +26 -0
  12. package/src/assets/images/gallery-mountain.svg +19 -0
  13. package/src/assets/images/gallery-ocean.svg +26 -0
  14. package/src/assets/images/gallery-oldtown.svg +41 -0
  15. package/src/assets/images/gallery-snow.svg +39 -0
  16. package/src/assets/images/slider-field.svg +124 -0
  17. package/src/assets/images/slider-forest.svg +33 -0
  18. package/src/assets/images/slider-lake.svg +35 -0
  19. package/src/assets/images/slider-mountains.svg +29 -0
  20. package/src/assets/images/slider-trees.svg +41 -0
  21. package/src/assets/images/tile-analytics.svg +22 -0
  22. package/src/assets/images/tile-cloud.svg +19 -0
  23. package/src/assets/images/tile-ecommerce.svg +14 -0
  24. package/src/assets/images/tile-enterprise.svg +22 -0
  25. package/src/assets/images/tile-marketing.svg +16 -0
  26. package/src/assets/media/demo-chime.wav +0 -0
  27. package/src/partials/announcement-container.html +67 -0
  28. package/src/partials/content-slider-container.html +5 -17
  29. package/src/partials/gallery-container.html +8 -8
  30. package/src/partials/home-container.html +11 -20
  31. package/src/partials/media-container.html +27 -0
  32. package/src/partials/prose-container.html +43 -0
  33. package/src/partials/testimonials-container.html +82 -0
  34. package/src/partials/tiles-container.html +5 -20
  35. package/src/scss/base/_base.scss +7 -1
  36. package/src/scss/components/_announcement.scss +84 -0
  37. package/src/scss/components/_index.scss +4 -0
  38. package/src/scss/components/_media.scss +26 -0
  39. package/src/scss/components/_prose.scss +42 -0
  40. package/src/scss/components/_testimonial.scss +58 -0
  41. package/src/scss/layout/_grid.scss +39 -0
  42. package/src/scss/layout/_main.scss +16 -1
  43. package/src/scss/layout/_section.scss +24 -0
  44. package/src/scss/utilities/_index.scss +1 -0
  45. package/src/scss/utilities/_reveal.scss +34 -0
  46. package/src/scss/utilities/_touch-targets.scss +10 -2
  47. package/maverick-wave.min.css +0 -12
  48. package/maverick-wave.min.js +0 -1
@@ -0,0 +1,27 @@
1
+ <h3 class="mw-section-subtitle">Media</h3>
2
+ <p class="mw-text-muted mw-text-measure mw-mb-6">
3
+ Self-hosted audio and video. The framework ships no player - the native
4
+ controls are the player, and <code>accent-color</code> already brands them.
5
+ The wrapper makes the element a full-width block; a <code>video</code> also
6
+ gets the surface silhouette and a resting shadow, same markup with a
7
+ <code>&lt;video&gt;</code> element inside.
8
+ </p>
9
+
10
+ <div class="mw-grid-2 mw-gap-6">
11
+ <div>
12
+ <h5 class="mw-text-medium mw-mb-2">Audio with caption</h5>
13
+ <div class="mw-card mw-card-simple">
14
+ <div class="mw-media">
15
+ <audio controls preload="metadata" src="media/demo-chime.wav"></audio>
16
+ <p class="mw-media-caption">A short chime, served from this site.</p>
17
+ </div>
18
+ </div>
19
+ </div>
20
+
21
+ <div>
22
+ <h5 class="mw-text-medium mw-mb-2">Bare, without a card</h5>
23
+ <div class="mw-media">
24
+ <audio controls preload="metadata" src="media/demo-chime.wav"></audio>
25
+ </div>
26
+ </div>
27
+ </div>
@@ -0,0 +1,43 @@
1
+ <h3 class="mw-section-subtitle">Prose</h3>
2
+ <p class="mw-text-muted mw-text-measure mw-mb-6">
3
+ Long-form running text - a legal page, terms, a plain article. One class on
4
+ the container, bare <code>h2</code>/<code>h3</code>/<code>p</code>/<code
5
+ >ul</code
6
+ >
7
+ inside: exactly what a CMS or a JSON-driven page produces. Headings stay
8
+ deliberately small - in a document they structure the reading, they do not
9
+ compete with the page title.
10
+ </p>
11
+
12
+ <div class="mw-card mw-card-simple mw-prose">
13
+ <h2>1. Scope</h2>
14
+ <p>
15
+ These terms apply to every order placed through this site. Deviating
16
+ conditions of the customer do not apply unless expressly confirmed in
17
+ writing.
18
+ </p>
19
+
20
+ <h2>2. Contract</h2>
21
+ <p>
22
+ The presentation of products is not a binding offer. A contract comes into
23
+ existence when the order confirmation arrives, at the latest with the
24
+ delivery of the goods.
25
+ </p>
26
+ <h3>2.1 Cancellation</h3>
27
+ <p>
28
+ Consumers may cancel within fourteen days without giving a reason. The
29
+ period starts on the day the goods arrive. For the cancellation to count,
30
+ one of the following is enough:
31
+ </p>
32
+ <ul>
33
+ <li>an informal e-mail naming the order number</li>
34
+ <li>the returned goods with a short note</li>
35
+ <li>the cancellation form from the appendix</li>
36
+ </ul>
37
+
38
+ <h2>3. Prices</h2>
39
+ <p>
40
+ All prices are final prices and include the statutory value-added tax.
41
+ Shipping costs are shown separately before the order is placed.
42
+ </p>
43
+ </div>
@@ -0,0 +1,82 @@
1
+ <h3 class="mw-section-subtitle">Testimonials</h3>
2
+ <p class="mw-text-muted mw-text-measure mw-mb-6">
3
+ A quote card with its source - a <code>figure</code>, because quote and
4
+ attribution belong together. One voice may stand above the grid as
5
+ <code>mw-testimonial-featured</code>; the rest flow in
6
+ <code>mw-columns-3</code>, the masonry layout for cards of unequal height. The
7
+ cards below also carry <code>mw-reveal</code> - scroll them into view to see
8
+ the entrance.
9
+ </p>
10
+
11
+ <figure class="mw-testimonial mw-testimonial-featured mw-mb-6">
12
+ <blockquote>
13
+ We rebuilt our whole customer portal on this in three weeks. The team never
14
+ argued about spacing or shadows again - the framework had already decided,
15
+ and it had decided well.
16
+ </blockquote>
17
+ <figcaption class="mw-testimonial-source">
18
+ Maren K.
19
+ <span class="mw-testimonial-detail">&middot; Head of Engineering</span>
20
+ </figcaption>
21
+ </figure>
22
+
23
+ <div class="mw-columns-3">
24
+ <figure class="mw-testimonial mw-reveal">
25
+ <blockquote>
26
+ The dark theme just worked. We flipped one class and every component
27
+ followed.
28
+ </blockquote>
29
+ <figcaption class="mw-testimonial-source">
30
+ Jonas R.
31
+ <span class="mw-testimonial-detail">&middot; Frontend Developer</span>
32
+ </figcaption>
33
+ </figure>
34
+
35
+ <figure class="mw-testimonial mw-reveal">
36
+ <blockquote>
37
+ I came for the grid and stayed for the corner accents. Our app finally
38
+ looks like one product instead of five teams.
39
+ </blockquote>
40
+ <figcaption class="mw-testimonial-source">
41
+ Priya S.
42
+ <span class="mw-testimonial-detail">&middot; Product Designer</span>
43
+ </figcaption>
44
+ </figure>
45
+
46
+ <figure class="mw-testimonial mw-reveal">
47
+ <blockquote>Small CSS, no build step, readable class names.</blockquote>
48
+ <figcaption class="mw-testimonial-source">
49
+ Tom W. <span class="mw-testimonial-detail">&middot; Freelancer</span>
50
+ </figcaption>
51
+ </figure>
52
+
53
+ <figure class="mw-testimonial mw-reveal">
54
+ <blockquote>
55
+ The form controls line up with the buttons out of the box. That alone
56
+ saved us a week of pixel pushing across three breakpoints - and the touch
57
+ targets grow on their own on mobile.
58
+ </blockquote>
59
+ <figcaption class="mw-testimonial-source">
60
+ Aylin D. <span class="mw-testimonial-detail">&middot; Agency Lead</span>
61
+ </figcaption>
62
+ </figure>
63
+
64
+ <figure class="mw-testimonial mw-reveal">
65
+ <blockquote>
66
+ Swapped one primary color, the whole palette followed.
67
+ </blockquote>
68
+ <figcaption class="mw-testimonial-source">
69
+ Ben L. <span class="mw-testimonial-detail">&middot; Indie Hacker</span>
70
+ </figcaption>
71
+ </figure>
72
+
73
+ <figure class="mw-testimonial mw-reveal">
74
+ <blockquote>
75
+ Our legal pages, our landing page and our admin tables all speak the same
76
+ language now.
77
+ </blockquote>
78
+ <figcaption class="mw-testimonial-source">
79
+ Carla M. <span class="mw-testimonial-detail">&middot; CTO</span>
80
+ </figcaption>
81
+ </figure>
82
+ </div>
@@ -69,10 +69,7 @@
69
69
  <div class="mw-tiles mw-tiles-2col">
70
70
  <div class="mw-tile">
71
71
  <div class="mw-tile-img">
72
- <img
73
- src="https://picsum.photos/80/80?random=1"
74
- alt="E-commerce platform"
75
- />
72
+ <img src="images/tile-ecommerce.svg" alt="E-commerce platform" />
76
73
  </div>
77
74
  <h3 class="mw-tile-header">E-commerce Platform</h3>
78
75
  <div class="mw-tile-body">
@@ -84,10 +81,7 @@
84
81
 
85
82
  <div class="mw-tile">
86
83
  <div class="mw-tile-img">
87
- <img
88
- src="https://picsum.photos/80/80?random=2"
89
- alt="Marketing automation"
90
- />
84
+ <img src="images/tile-marketing.svg" alt="Marketing automation" />
91
85
  </div>
92
86
  <h3 class="mw-tile-header">Marketing Automation</h3>
93
87
  <div class="mw-tile-body">
@@ -181,10 +175,7 @@
181
175
  <div class="mw-tiles">
182
176
  <div class="mw-tile mw-tile-lg">
183
177
  <div class="mw-tile-img">
184
- <img
185
- src="https://picsum.photos/80/80?random=3"
186
- alt="Cloud infrastructure"
187
- />
178
+ <img src="images/tile-cloud.svg" alt="Cloud infrastructure" />
188
179
  </div>
189
180
  <h2 class="mw-tile-header">Cloud Infrastructure</h2>
190
181
  <div class="mw-tile-body">
@@ -201,10 +192,7 @@
201
192
 
202
193
  <div class="mw-tile mw-tile-lg">
203
194
  <div class="mw-tile-img">
204
- <img
205
- src="https://picsum.photos/100/100?random=8"
206
- alt="Business intelligence"
207
- />
195
+ <img src="images/tile-analytics.svg" alt="Business intelligence" />
208
196
  </div>
209
197
  <h2 class="mw-tile-header">Business Intelligence Suite</h2>
210
198
  <div class="mw-tile-body">
@@ -222,10 +210,7 @@
222
210
 
223
211
  <div class="mw-tile mw-tile-lg">
224
212
  <div class="mw-tile-img">
225
- <img
226
- src="https://picsum.photos/100/100?random=9"
227
- alt="Enterprise solutions"
228
- />
213
+ <img src="images/tile-enterprise.svg" alt="Enterprise solutions" />
229
214
  </div>
230
215
  <h2 class="mw-tile-header">Enterprise Solutions</h2>
231
216
  <div class="mw-tile-body">
@@ -99,6 +99,12 @@ $_active-prefix: 'dark';
99
99
  // header height
100
100
  --mw-header-height: #{$header-height};
101
101
 
102
+ // Height the announcement ribbon reserves - the anchor scroll offset is
103
+ // calculated from it, so a project with a taller ribbon raises this token
104
+ // rather than fighting the offset. Below `sm` the component itself steps it
105
+ // up for the wrapped line.
106
+ --mw-announcement-height: 2.7rem;
107
+
102
108
  // Layout knobs. A good default is not enough here: what reads as generous on
103
109
  // a landing page costs visible content in an application with a sticky
104
110
  // header. Same framework, two different right answers.
@@ -123,7 +129,7 @@ $_active-prefix: 'dark';
123
129
  1200px,
124
130
  calc(100% - 2 * var(--mw-container-gutter))
125
131
  );
126
- --mw-section-padding-block: #{spacing('7')};
132
+ --mw-section-padding-block: #{spacing('10')};
127
133
  --mw-section-title-gap: #{spacing('7')};
128
134
  --mw-scroll-hint-offset: #{spacing('9')};
129
135
  --mw-scroll-hint-inline: var(--mw-container-gutter);
@@ -0,0 +1,84 @@
1
+ @use '../abstracts' as *;
2
+
3
+ // The ribbon under the fixed header - a discount, a launch note, a maintenance
4
+ // window. One per page, and always a single line of content with an optional
5
+ // highlight pill in front of it.
6
+ .mw-announcement {
7
+ position: fixed;
8
+ top: var(--mw-header-height);
9
+ right: 0;
10
+ left: 0;
11
+ z-index: z-index('header') - 10;
12
+ background: color-mix(in srgb, var(--mw-primary-color) 75%, transparent);
13
+ backdrop-filter: blur(7px);
14
+ color: var(--mw-primary-accent-text-color);
15
+ box-shadow: var(--mw-elevation-2);
16
+ }
17
+
18
+ // The one row inside - usually a link to the thing announced.
19
+ .mw-announcement-content {
20
+ display: flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ gap: spacing('4');
24
+ min-height: var(--mw-announcement-height);
25
+ padding: spacing('1') spacing('4');
26
+ font-size: font-size('sm');
27
+ text-align: center;
28
+ color: inherit;
29
+ text-decoration: none;
30
+
31
+ &:hover,
32
+ &:focus-visible {
33
+ background: color-mix(in srgb, #000 10%, transparent);
34
+ }
35
+
36
+ @include focus-ring-inset;
37
+ @include tappable;
38
+ }
39
+
40
+ // The highlight pill - the "-20 %", the "NEW". Darkened glass rather than a
41
+ // color of its own, so it works on every variant without a token per pair.
42
+ .mw-announcement-highlight {
43
+ flex-shrink: 0;
44
+ padding: spacing('1') spacing('3');
45
+ border-radius: 999px;
46
+ background: color-mix(in srgb, #000 18%, transparent);
47
+ font-weight: font-weight('bold');
48
+ }
49
+
50
+ // The ribbon in the document flow instead of glued under the header - for a
51
+ // notice inside a page, or a demo of the component itself. Static, it scrolls
52
+ // with the content and shifts no anchor offset below.
53
+ .mw-announcement-static {
54
+ position: static;
55
+ }
56
+
57
+ // Anchors scrolled to under the fixed chrome would land under the ribbon too -
58
+ // the same reason the reset reserves the header height. Scoped with :has, so a
59
+ // page without a ribbon keeps the plain offset - and a static ribbon does not
60
+ // count, it scrolls with the content.
61
+ html:has(.mw-announcement:not(.mw-announcement-static)) {
62
+ scroll-padding-top: calc(
63
+ var(--mw-header-height) + var(--mw-announcement-height)
64
+ );
65
+ }
66
+
67
+ // On a phone the line wraps and the ribbon grows - the height token follows,
68
+ // so the scroll offset above stays honest. Here and not in base, so cherry-
69
+ // picking the component brings the whole behaviour along.
70
+ @include media-down('sm') {
71
+ :root {
72
+ --mw-announcement-height: 3.4rem;
73
+ }
74
+ }
75
+
76
+ // Color variants
77
+ $_mw-announcement-colors: (secondary, success, warning, danger, info);
78
+
79
+ @each $name in $_mw-announcement-colors {
80
+ .mw-announcement-#{$name} {
81
+ background: color-mix(in srgb, var(--mw-#{$name}-color) 75%, transparent);
82
+ color: var(--mw-#{$name}-accent-text-color);
83
+ }
84
+ }
@@ -1,5 +1,6 @@
1
1
  @forward 'accordions';
2
2
  @forward 'alerts';
3
+ @forward 'announcement';
3
4
  @forward 'avatars';
4
5
  @forward 'badge';
5
6
  @forward 'blog-post';
@@ -21,12 +22,14 @@
21
22
  @forward 'links';
22
23
  @forward 'lists';
23
24
  @forward 'localhost-indicator';
25
+ @forward 'media';
24
26
  @forward 'meta-info';
25
27
  @forward 'modals';
26
28
  @forward 'pagination';
27
29
  @forward 'panels';
28
30
  @forward 'pricing';
29
31
  @forward 'progress';
32
+ @forward 'prose';
30
33
  @forward 'ratings';
31
34
  @forward 'segmented';
32
35
  @forward 'skeleton';
@@ -36,6 +39,7 @@
36
39
  @forward 'tabs';
37
40
  @forward 'tags';
38
41
  @forward 'techstack-bucket';
42
+ @forward 'testimonial';
39
43
  @forward 'theme-toggle';
40
44
  @forward 'tiles';
41
45
  @forward 'timelines';
@@ -0,0 +1,26 @@
1
+ @use '../abstracts' as *;
2
+
3
+ // Self-hosted audio and video. The framework ships no player - the native
4
+ // controls are the player, and `accent-color` on :root already brands them.
5
+ // This only makes the element sit right: full width, block, and a video gets
6
+ // the surface silhouette so it reads as part of the family.
7
+ .mw-media {
8
+ audio,
9
+ video {
10
+ display: block;
11
+ width: 100%;
12
+ }
13
+
14
+ video {
15
+ height: auto;
16
+ border-radius: radius('sm') radius('xl') radius('sm') radius('xl');
17
+ box-shadow: var(--mw-elevation-2);
18
+ }
19
+ }
20
+
21
+ .mw-media-caption {
22
+ margin-top: spacing('2');
23
+ font-size: font-size('sm');
24
+ color: var(--mw-text-muted-color);
25
+ text-align: center;
26
+ }
@@ -0,0 +1,42 @@
1
+ @use '../abstracts' as *;
2
+
3
+ // Long-form running text - a legal page, terms, a plain article. One class on
4
+ // the container instead of a utility chain on every heading: the markup inside
5
+ // stays bare h2/h3/p/ul, which is exactly what a CMS or a JSON-driven page
6
+ // produces anyway.
7
+ //
8
+ // Headings are deliberately small: in a document they structure the reading,
9
+ // they do not compete with the page title.
10
+ .mw-prose {
11
+ h2 {
12
+ font-size: font-size('lg');
13
+ color: var(--mw-primary-text-color);
14
+ margin: spacing('8') 0 spacing('3');
15
+ }
16
+
17
+ h3 {
18
+ font-size: font-size('md');
19
+ color: var(--mw-primary-text-color);
20
+ margin: spacing('6') 0 spacing('2');
21
+ }
22
+
23
+ ul,
24
+ ol {
25
+ margin: 0 0 spacing('3') spacing('7');
26
+ }
27
+
28
+ li {
29
+ margin-bottom: spacing('1');
30
+ }
31
+
32
+ // The first heading in a card already sits on the card's padding, the last
33
+ // paragraph on its bottom edge - without these the container carries double
34
+ // spacing at both ends.
35
+ > :first-child {
36
+ margin-top: 0;
37
+ }
38
+
39
+ > :last-child {
40
+ margin-bottom: 0;
41
+ }
42
+ }
@@ -0,0 +1,58 @@
1
+ @use '../abstracts' as *;
2
+
3
+ // A voice about the product - a quote card with its source. A figure, because
4
+ // quote and attribution belong together and that is the element that says so:
5
+ //
6
+ // <figure class="mw-testimonial">
7
+ // <blockquote>...</blockquote>
8
+ // <figcaption class="mw-testimonial-source">
9
+ // Name <span class="mw-testimonial-detail">&middot; role or occasion</span>
10
+ // </figcaption>
11
+ // </figure>
12
+ .mw-testimonial {
13
+ @include surface;
14
+ display: flex;
15
+ flex-direction: column;
16
+ padding: spacing('5');
17
+ background: var(--mw-card-background);
18
+ border: 1px solid var(--mw-border);
19
+ box-shadow: var(--mw-elevation-2);
20
+
21
+ // Several of these in a row, each quote on the base blockquote tint, add up
22
+ // to a solid colored wall nobody reads through. The card already carries the
23
+ // surface - inside it the quote keeps only the edge and the quotation marks
24
+ // as its sign.
25
+ blockquote {
26
+ background: color-mix(in srgb, var(--mw-secondary-color) 6%, transparent);
27
+ border-left-color: color-mix(
28
+ in srgb,
29
+ var(--mw-secondary-color) 45%,
30
+ transparent
31
+ );
32
+ padding-block: spacing('2');
33
+ font-size: font-size('base');
34
+ line-height: 1.6;
35
+ }
36
+ }
37
+
38
+ // margin-top: auto, so in a grid of equal-height cards every source line sits
39
+ // on its card's bottom edge instead of floating under quotes of random length.
40
+ .mw-testimonial-source {
41
+ margin-top: auto;
42
+ padding-top: spacing('5');
43
+ font-size: font-size('sm');
44
+ font-weight: font-weight('bold');
45
+ color: var(--mw-primary-text-color);
46
+ }
47
+
48
+ .mw-testimonial-detail {
49
+ font-weight: font-weight('normal');
50
+ color: var(--mw-text-muted-color);
51
+ }
52
+
53
+ // The one voice lifted above the grid - it may speak a size louder.
54
+ .mw-testimonial-featured {
55
+ blockquote {
56
+ font-size: font-size('md');
57
+ }
58
+ }
@@ -220,6 +220,45 @@
220
220
  }
221
221
  }
222
222
 
223
+ // Masonry columns - for cards of unequal height, where a row grid tears a gap
224
+ // under every short one. CSS multi-column instead: entries flow into columns
225
+ // and close up, the column count steps down with the viewport. break-inside
226
+ // keeps each child whole - without it a page break lands mid-card.
227
+ //
228
+ // Reading order caveat: columns read top to bottom, then across. Right for
229
+ // testimonials and galleries, wrong for anything ranked or chronological -
230
+ // that stays in a grid.
231
+ %columns-base {
232
+ column-gap: spacing('6');
233
+
234
+ > * {
235
+ break-inside: avoid;
236
+ margin-bottom: spacing('6');
237
+ }
238
+ }
239
+
240
+ .mw-columns-2 {
241
+ @extend %columns-base;
242
+ columns: 2;
243
+
244
+ @include media-down('sm') {
245
+ columns: 1;
246
+ }
247
+ }
248
+
249
+ .mw-columns-3 {
250
+ @extend %columns-base;
251
+ columns: 3;
252
+
253
+ @include media-down('lg') {
254
+ columns: 2;
255
+ }
256
+
257
+ @include media-down('sm') {
258
+ columns: 1;
259
+ }
260
+ }
261
+
223
262
  // Base stack - items keep their natural width
224
263
  .mw-grid-stack {
225
264
  @extend %grid-base;
@@ -97,8 +97,15 @@
97
97
  // width it has, so both variants get the strip back. Last of the three,
98
98
  // because a media query adds no specificity and would otherwise lose to
99
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.
100
105
  @include media-down('md') {
101
106
  &:has(> .mw-scroll-hint) {
107
+ --mw-scroll-hint-reserve: #{spacing('6')};
108
+
102
109
  padding-bottom: calc(
103
110
  var(--mw-scroll-hint-offset) + var(--mw-scroll-hint-reserve)
104
111
  );
@@ -124,12 +131,20 @@
124
131
  flex-direction: column;
125
132
  align-items: center;
126
133
  gap: spacing('2');
127
- color: color-mix(in srgb, var(--mw-hero-text-color) 45%, transparent);
134
+ color: color-mix(in srgb, var(--mw-hero-text-color) 72%, transparent);
128
135
  font-size: font-size('xs');
129
136
  letter-spacing: 0.08em;
130
137
  text-transform: uppercase;
131
138
  text-decoration: none;
132
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');
147
+ }
133
148
 
134
149
  @include hover {
135
150
  color: var(--mw-hero-text-color);
@@ -22,6 +22,20 @@
22
22
  }
23
23
  }
24
24
 
25
+ // The lead line under a section title. Every landing page writes this
26
+ // paragraph, and it always wants the same four things: centred, muted,
27
+ // narrower than the container so it reads as a line rather than a block, and
28
+ // pulled up toward the title it belongs to - the title's gap is sized for the
29
+ // content below, not for its own subtitle.
30
+ .mw-section-intro {
31
+ max-width: 46rem;
32
+ margin: spacing('-3') auto spacing('9');
33
+ font-size: font-size('md');
34
+ color: var(--mw-text-muted-color);
35
+ text-align: center;
36
+ text-wrap: balance;
37
+ }
38
+
25
39
  .mw-section-subtitle {
26
40
  font-size: font-size('2xl');
27
41
  margin: spacing('8') spacing('0') spacing('6') spacing('0');
@@ -110,6 +124,16 @@
110
124
  .mw-section {
111
125
  padding: var(--mw-section-padding-block) spacing('0');
112
126
  position: relative;
127
+
128
+ // ...except around a hero. The hero container is already 100dvh, so the
129
+ // padding pushes exactly that much of it past the bottom of the screen -
130
+ // and with it the scroll cue sitting on its lower edge. A hero is the full
131
+ // screen by definition and brings its own breathing room.
132
+ @supports (selector(:has(.mw-hero))) {
133
+ &:has(.mw-hero) {
134
+ padding-block: spacing('0');
135
+ }
136
+ }
113
137
  }
114
138
 
115
139
  // Pattern variant for sections
@@ -4,6 +4,7 @@
4
4
  @forward 'display';
5
5
  @forward 'elevation';
6
6
  @forward 'flex';
7
+ @forward 'reveal';
7
8
  @forward 'spacing';
8
9
  @forward 'text';
9
10
  @forward 'touch-targets';
@@ -0,0 +1,34 @@
1
+ @use '../abstracts' as *;
2
+
3
+ // Scroll entrance: a block rises briefly as it scrolls into view. Driven by
4
+ // the browser's scroll timeline - no JavaScript, no IntersectionObserver
5
+ // recalculating on every scroll step.
6
+ //
7
+ // Both guards matter. `prefers-reduced-motion` is the obvious one. The
8
+ // @supports guard is the safety net: a browser without scroll timelines would
9
+ // otherwise apply `both` fill on an animation that never runs and leave the
10
+ // block invisible forever - guarded, it simply renders in place.
11
+ //
12
+ // The range ends well before the block reaches the middle of the screen, so
13
+ // nothing is still moving while someone already reads it.
14
+ @supports (animation-timeline: view()) {
15
+ @media (prefers-reduced-motion: no-preference) {
16
+ .mw-reveal {
17
+ animation: mw-reveal linear both;
18
+ animation-timeline: view();
19
+ animation-range: entry 2% entry 62%;
20
+ }
21
+ }
22
+ }
23
+
24
+ @keyframes mw-reveal {
25
+ from {
26
+ opacity: 0;
27
+ transform: translateY(26px);
28
+ }
29
+
30
+ to {
31
+ opacity: 1;
32
+ transform: translateY(0);
33
+ }
34
+ }
@@ -140,10 +140,18 @@
140
140
  -webkit-overflow-scrolling: touch;
141
141
  }
142
142
 
143
- // Phone layout
143
+ // Phone layout. The section rhythm steps down twice: a tablet keeps a middle
144
+ // value, a phone drops further - every rem here is one the reader has to
145
+ // scroll past on a screen that is already all scrolling.
144
146
  @include media-down('md') {
145
147
  :root {
146
- --mw-section-padding-block: #{spacing('6')};
148
+ --mw-section-padding-block: #{spacing('9')};
149
+ }
150
+ }
151
+
152
+ @include media-down('sm') {
153
+ :root {
154
+ --mw-section-padding-block: #{spacing('7')};
147
155
  }
148
156
  }
149
157