maverick-wave 5.3.0 → 5.5.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 (36) hide show
  1. package/.claude/skills/mw-maverick-wave/SKILL.md +13 -4
  2. package/.claude/skills/mw-maverick-wave/examples/angular-services.md +91 -0
  3. package/.claude/skills/mw-maverick-wave/examples/static-landing-page.md +5 -4
  4. package/.claude/skills/mw-maverick-wave/references/components.md +7 -0
  5. package/.claude/skills/mw-maverick-wave/references/javascript.md +20 -2
  6. package/.claude/skills/mw-maverick-wave/references/layout.md +81 -10
  7. package/.claude/skills/mw-maverick-wave/references/theming.md +10 -0
  8. package/CHANGELOG.md +18 -0
  9. package/CLAUDE.md +1 -1
  10. package/README.md +3 -4
  11. package/index.html +291 -3
  12. package/maverick-wave.min.css +6 -4
  13. package/maverick-wave.min.js +1 -1
  14. package/package.json +2 -2
  15. package/scripts/verify.js +4 -7
  16. package/src/js/main.js +237 -11
  17. package/src/partials/documentation-container.html +4 -0
  18. package/src/partials/footer-container.html +3 -0
  19. package/src/partials/history-container.html +67 -0
  20. package/src/partials/home-container.html +2 -0
  21. package/src/partials/palette-container.html +142 -0
  22. package/src/partials/parallax-container.html +114 -0
  23. package/src/scss/abstracts/_mixins.scss +6 -5
  24. package/src/scss/abstracts/_variables.scss +4 -0
  25. package/src/scss/base/_base.scss +1 -0
  26. package/src/scss/base/_reset.scss +4 -1
  27. package/src/scss/components/_accordions.scss +31 -7
  28. package/src/scss/components/_toasts.scss +1 -1
  29. package/src/scss/form-elements/_slider.scss +5 -23
  30. package/src/scss/layout/_header-reveal.scss +133 -0
  31. package/src/scss/layout/_index.scss +4 -0
  32. package/src/scss/layout/_main.scss +17 -2
  33. package/src/scss/layout/_parallax.scss +178 -0
  34. package/src/scss/utilities/_reveal.scss +22 -0
  35. package/.impeccable/config.local.json +0 -5
  36. package/.impeccable/hook.cache.json +0 -75
@@ -0,0 +1,142 @@
1
+ <button
2
+ type="button"
3
+ class="palette-fab"
4
+ id="palette-fab"
5
+ aria-expanded="false"
6
+ aria-controls="palette-panel"
7
+ aria-label="Try another palette"
8
+ >
9
+ <i class="fas fa-palette"></i>
10
+ </button>
11
+
12
+ <div class="palette-panel" id="palette-panel" hidden>
13
+ <h3 class="palette-heading">Palette</h3>
14
+ <p class="palette-hint">
15
+ Two colors. Everything else - bar, ink, borders, tints - is derived from
16
+ them at runtime.
17
+ </p>
18
+
19
+ <div class="palette-grid">
20
+ <button
21
+ type="button"
22
+ class="palette-item mw-active"
23
+ data-primary="#1031bb"
24
+ data-secondary="#bb5710"
25
+ data-dark="#171f30"
26
+ data-light="#e7e7f0"
27
+ >
28
+ Maverick
29
+ </button>
30
+ <button
31
+ type="button"
32
+ class="palette-item"
33
+ data-primary="#0f6f8c"
34
+ data-secondary="#e8862a"
35
+ data-dark="#0f1f26"
36
+ data-light="#e2edf1"
37
+ >
38
+ Deep Sea
39
+ </button>
40
+ <button
41
+ type="button"
42
+ class="palette-item"
43
+ data-primary="#c2321f"
44
+ data-secondary="#256f83"
45
+ data-dark="#13161a"
46
+ data-light="#f4e9e6"
47
+ >
48
+ Ember
49
+ </button>
50
+ <button
51
+ type="button"
52
+ class="palette-item"
53
+ data-primary="#2f7a3f"
54
+ data-secondary="#7f4dbd"
55
+ data-dark="#141f16"
56
+ data-light="#e6eee7"
57
+ >
58
+ Moss
59
+ </button>
60
+ <button
61
+ type="button"
62
+ class="palette-item"
63
+ data-primary="#8e3bd4"
64
+ data-secondary="#2fa38a"
65
+ data-dark="#1d1530"
66
+ data-light="#ece8f5"
67
+ >
68
+ Orchid
69
+ </button>
70
+ <button
71
+ type="button"
72
+ class="palette-item"
73
+ data-primary="#46617f"
74
+ data-secondary="#c08a2e"
75
+ data-dark="#1a1f26"
76
+ data-light="#e8ebef"
77
+ >
78
+ Slate
79
+ </button>
80
+ <button
81
+ type="button"
82
+ class="palette-item"
83
+ data-primary="#00b3ff"
84
+ data-secondary="#ff2d95"
85
+ data-dark="#0d1117"
86
+ data-light="#e6f3fb"
87
+ >
88
+ Neon
89
+ </button>
90
+ <button
91
+ type="button"
92
+ class="palette-item"
93
+ data-primary="#9a5f23"
94
+ data-secondary="#3b6ea5"
95
+ data-dark="#241c15"
96
+ data-light="#f2eadd"
97
+ >
98
+ Sand
99
+ </button>
100
+ <button
101
+ type="button"
102
+ class="palette-item"
103
+ data-primary="#12a37a"
104
+ data-secondary="#e2699b"
105
+ data-dark="#101f1b"
106
+ data-light="#e2f1ec"
107
+ >
108
+ Mint
109
+ </button>
110
+ <button
111
+ type="button"
112
+ class="palette-item"
113
+ data-primary="#4b2fc4"
114
+ data-secondary="#d9a72e"
115
+ data-dark="#171331"
116
+ data-light="#eae8f7"
117
+ >
118
+ Royal
119
+ </button>
120
+ <button
121
+ type="button"
122
+ class="palette-item"
123
+ data-primary="#9b1b4a"
124
+ data-secondary="#23a08d"
125
+ data-dark="#1e1220"
126
+ data-light="#f3e7ec"
127
+ >
128
+ Wine
129
+ </button>
130
+ </div>
131
+
132
+ <div class="palette-custom">
133
+ <label class="palette-field">
134
+ <span>Primary</span>
135
+ <input type="color" id="palette-primary" value="#1031bb" />
136
+ </label>
137
+ <label class="palette-field">
138
+ <span>Secondary</span>
139
+ <input type="color" id="palette-secondary" value="#bb5710" />
140
+ </label>
141
+ </div>
142
+ </div>
@@ -0,0 +1,114 @@
1
+ <h3 class="mw-section-subtitle">Parallax</h3>
2
+ <p class="mw-text-muted mw-text-measure mw-mb-6">
3
+ Two ways to play a picture against the scroll, both on the browser's scroll
4
+ timeline - no listener, and no
5
+ <code>background-attachment: fixed</code>, which iOS ignores. The image lives
6
+ in its own <code>mw-parallax-media</code> layer, as an
7
+ <code>&lt;img&gt;</code> or as a div with a background image. Where scroll
8
+ timelines are missing, or with <code>prefers-reduced-motion</code>, the
9
+ picture stands still and nothing else changes.
10
+ </p>
11
+
12
+ <h4 class="mw-text-medium mw-mb-2">
13
+ Drift - <code>mw-parallax</code>, the picture moves slower than the page
14
+ </h4>
15
+ <p class="mw-text-muted mw-text-measure mw-mb-6">
16
+ How far it travels is <code>--mw-parallax-depth</code> (<code>10vh</code> by
17
+ default); the layer overhangs the block by exactly that much, so no edge is
18
+ ever uncovered. <code>mw-parallax-dimmed</code> turns the picture down and the
19
+ text light.
20
+ </p>
21
+
22
+ <div class="mw-grid-1 mw-gap-6 mw-mb-6">
23
+ <div class="mw-parallax mw-parallax-dimmed">
24
+ <img
25
+ class="mw-parallax-media"
26
+ src="images/slider-field.svg"
27
+ alt=""
28
+ loading="lazy"
29
+ />
30
+ <div class="mw-parallax-content mw-text-center">
31
+ <h3>Scroll past me</h3>
32
+ <p class="mw-mb-0">The field moves slower than the page does.</p>
33
+ </div>
34
+ </div>
35
+
36
+ <div class="mw-parallax" style="--mw-parallax-depth: 20vh">
37
+ <img
38
+ class="mw-parallax-media"
39
+ src="images/gallery-mountain.svg"
40
+ alt="Mountain range"
41
+ loading="lazy"
42
+ />
43
+ </div>
44
+ </div>
45
+
46
+ <h4 class="mw-text-medium mw-mb-2">
47
+ Reveal - <code>mw-parallax-sticky</code>, the picture stays and the page rides
48
+ over it
49
+ </h4>
50
+ <p class="mw-text-muted mw-text-measure mw-mb-6">
51
+ The home section of this page does it: the block pins itself to the top of the
52
+ screen and everything after it slides up over the picture. It goes on whatever
53
+ element the page content is a <strong>sibling</strong> of - the section around
54
+ a hero, not the hero itself - because that is what sticky and the sibling
55
+ selector need. Everything after it gets the page background and a layer above,
56
+ or the pinned picture shows through. Pair it with
57
+ <code>mw-header-reveal</code> on the header and the page opens on the picture
58
+ alone: the bar stays away for the first 130px of scroll and has ridden in by
59
+ 420px.
60
+ </p>
61
+ <p class="mw-text-muted mw-text-measure mw-mb-6">
62
+ Add <code>mw-parallax-rise</code> next to it and the picture lifts by
63
+ <code>12vh</code> while the text stays where it is - a pinned block does not
64
+ travel through the viewport, so that layer runs on the document's own scroll
65
+ instead. <code>--mw-parallax-depth</code> is the dial, here as everywhere.
66
+ </p>
67
+
68
+ <h4 class="mw-text-medium mw-mb-2">
69
+ Pattern - <code>mw-parallax-pattern</code>, a second layer that makes the
70
+ movement visible
71
+ </h4>
72
+ <p class="mw-text-muted mw-text-measure mw-mb-6">
73
+ The travel is vertical, so only an edge across it - a rule, a grid, a hatch -
74
+ ever shows it. A gradient has none and slides without looking like it moves.
75
+ Add a second <code>mw-parallax-media</code> carrying
76
+ <code>mw-parallax-pattern</code>: it travels a third of the distance, and it
77
+ is the difference between the two layers that reads as depth. In a hero the
78
+ pattern comes from <code>$mw-hero-pattern</code>, the way the picture comes
79
+ from <code>$mw-hero-image</code>; anywhere else set
80
+ <code>--mw-parallax-pattern-image</code> on it.
81
+ </p>
82
+
83
+ <div class="mw-parallax mw-parallax-dimmed mw-mb-6">
84
+ <div class="mw-parallax-media parallax-demo-ground"></div>
85
+ <div class="mw-parallax-media mw-parallax-pattern parallax-demo-lines"></div>
86
+ <div class="mw-parallax-content mw-text-center">
87
+ <h3>The grid drifts, the gradient holds</h3>
88
+ <p class="mw-mb-0">
89
+ Both layers move. Only the one with edges tells you so.
90
+ </p>
91
+ </div>
92
+ </div>
93
+
94
+ <div class="mw-code-block">
95
+ <div class="mw-code-block-header">
96
+ <span class="mw-code-block-filename">index.html</span>
97
+ <span class="mw-code-block-language">HTML</span>
98
+ </div>
99
+ <div class="mw-code-container">
100
+ <pre><code>&lt;header class="mw-header mw-header-reveal"&gt;...&lt;/header&gt;
101
+
102
+ &lt;main class="mw-main"&gt;
103
+ &lt;section class="mw-section mw-parallax-sticky mw-parallax-rise"&gt;
104
+ &lt;div class="mw-container mw-parallax"&gt;
105
+ &lt;div class="mw-parallax-media"&gt;&lt;/div&gt;
106
+ &lt;div class="mw-parallax-media mw-parallax-pattern"&gt;&lt;/div&gt;
107
+ &lt;div class="mw-hero"&gt;...&lt;/div&gt;
108
+ &lt;/div&gt;
109
+ &lt;/section&gt;
110
+
111
+ &lt;section class="mw-section"&gt;...&lt;/section&gt;
112
+ &lt;/main&gt;</code></pre>
113
+ </div>
114
+ </div>
@@ -253,11 +253,12 @@
253
253
  }
254
254
  }
255
255
 
256
- // The reveal zone as the range of the n-th column. Viewport lengths and not
257
- // percentages of the block, so a short band and a tall card rise through the same
258
- // region of the screen. Each further column starts 4dvh after the one before.
256
+ // The reveal zone as the range of the n-th column, measured along the block's own
257
+ // entry and not in viewport lengths: a zone in dvh is the same length whatever the
258
+ // block measures, so a card shorter than the zone stands fully in place while it is
259
+ // still transparent. Each further column starts a tenth of that entry later.
259
260
  @mixin reveal-range($column: 1) {
260
- $shift: 4dvh * ($column - 1);
261
+ $shift: 10% * ($column - 1);
261
262
 
262
- animation-range: entry #{8dvh + $shift} entry #{34dvh + $shift};
263
+ animation-range: entry #{$shift} entry #{60% + $shift};
263
264
  }
@@ -4,6 +4,10 @@ $mw-theme-mode: 'switchable' !default; // 'dark', 'light', or 'switchable'
4
4
 
5
5
  $mw-hero-image: url('https://images.unsplash.com/photo-1591638462868-19cfad9c49d1?q=80&w=1300&auto=format&fit=crop') !default;
6
6
 
7
+ // A second, flatter layer over the image - see mw-parallax-pattern. Fine lines
8
+ // or a grid, because a gradient has no edge across the travel to show it moving.
9
+ $mw-hero-pattern: none !default;
10
+
7
11
  // Fixed to the light end rather than theme-bound: the photo does not change with
8
12
  // the theme, so its ink must not either. Use --mw-light-text-color on a bright one.
9
13
  $mw-hero-text-color: var(--mw-dark-text-color) !default;
@@ -85,6 +85,7 @@
85
85
 
86
86
  // hero background image and the ink that sits on it
87
87
  --mw-hero-background: #{$mw-hero-image};
88
+ --mw-hero-pattern: #{$mw-hero-pattern};
88
89
  --mw-hero-text-color: #{$mw-hero-text-color};
89
90
 
90
91
  // header height
@@ -24,9 +24,12 @@
24
24
  -webkit-font-smoothing: antialiased;
25
25
  -moz-osx-font-smoothing: grayscale;
26
26
 
27
+ // `dvh` and not `vh`: on iOS `100vh` is the tall viewport behind the
28
+ // address bar, so the locked body stays taller than the screen and keeps a
29
+ // strip of rubber-band scroll under the modal.
27
30
  &:has(.mw-modal-open) {
28
31
  overflow: hidden;
29
- height: 100vh;
32
+ height: 100dvh;
30
33
  }
31
34
  }
32
35
 
@@ -83,27 +83,51 @@
83
83
  }
84
84
  }
85
85
 
86
+ // A grid row from `0fr` to `1fr`, and not `height: 0` to `auto`: `auto` is
87
+ // not an animatable value, and `interpolate-size`, which would make it one,
88
+ // is Chrome's alone - Firefox and Safari jumped the panel open instead of
89
+ // running the 900ms. A fraction interpolates in all three.
86
90
  &-content {
87
- height: spacing('0');
91
+ display: grid;
92
+ grid-template-rows: 0fr;
88
93
  overflow: hidden;
89
- transition: height var(--mw-duration-slower) var(--mw-ease-out);
94
+ transition: grid-template-rows var(--mw-duration-slower)
95
+ var(--mw-ease-out);
90
96
  background: var(--mw-card-background);
91
- interpolate-size: allow-keywords;
92
97
 
93
98
  &-inner {
94
- padding: spacing('4');
95
- border-top: 1px solid var(--mw-border-accent);
99
+ // The row is never shorter than what the item contributes, and padding
100
+ // and a border do not collapse with it - so both travel along, or the
101
+ // shut panel keeps a 25px strip of itself on screen. No `overflow` here
102
+ // on purpose: the panel above already clips, and clipping twice would
103
+ // hide the overflow that makes a long panel scrollable.
104
+ min-height: 0;
105
+ padding: spacing('0') spacing('4');
106
+ border-top: 0 solid var(--mw-border-accent);
107
+ transition:
108
+ padding-block var(--mw-duration-slower) var(--mw-ease-out),
109
+ border-top-width var(--mw-duration-slower) var(--mw-ease-out);
110
+
96
111
  @include media-down('sm') {
97
112
  font-size: font-size('sm');
98
113
  }
99
114
  }
100
115
 
101
116
  @include active() {
102
- height: auto;
117
+ grid-template-rows: 1fr;
103
118
  max-height: 500px;
104
119
  overflow-y: auto;
105
120
 
106
- // Scrollbar styling
121
+ > .mw-accordion-content-inner {
122
+ padding-block: spacing('4');
123
+ border-top-width: 1px;
124
+ }
125
+
126
+ // Both spellings: the standard pair is what Firefox reads, the webkit
127
+ // pseudos are for Safari below 18.2.
128
+ scrollbar-width: thin;
129
+ scrollbar-color: var(--mw-primary-color-hover) var(--mw-card-background);
130
+
107
131
  &::-webkit-scrollbar {
108
132
  width: 6px;
109
133
  }
@@ -9,7 +9,7 @@
9
9
  flex-direction: column;
10
10
  gap: spacing('3');
11
11
  width: min(380px, calc(100vw - #{2 * spacing('5')}));
12
- max-height: calc(100vh - #{2 * spacing('5')});
12
+ max-height: calc(100dvh - #{2 * spacing('5')});
13
13
  overflow-y: auto;
14
14
  pointer-events: none;
15
15
 
@@ -38,10 +38,14 @@
38
38
  transform: scale(1.1);
39
39
  }
40
40
 
41
+ // Same fill and ring as the webkit thumb above - Firefox paints its own
42
+ // grey thumb unless `background` is set, so leaving it out is not the same
43
+ // declaration set, it is a different slider.
41
44
  &::-moz-range-thumb {
42
45
  width: spacing('5');
43
46
  height: spacing('5');
44
- border: 2px solid var(--fill);
47
+ background: var(--fill);
48
+ border: 2px solid var(--mw-border);
45
49
  border-radius: radius('full');
46
50
  transition:
47
51
  transform var(--mw-duration-fast) var(--mw-ease-out),
@@ -55,20 +59,6 @@
55
59
  background: transparent;
56
60
  }
57
61
 
58
- // IE
59
- &::-ms-fill-lower {
60
- background: var(--fill);
61
- }
62
- &::-ms-fill-upper {
63
- background: var(--track);
64
- }
65
- &::-ms-thumb {
66
- width: spacing('5');
67
- height: spacing('5');
68
- border: 2px solid var(--fill);
69
- border-radius: radius('full');
70
- }
71
-
72
62
  &-sm {
73
63
  height: 6px;
74
64
  &::-webkit-slider-thumb {
@@ -79,10 +69,6 @@
79
69
  width: spacing('4');
80
70
  height: spacing('4');
81
71
  }
82
- &::-ms-thumb {
83
- width: spacing('4');
84
- height: spacing('4');
85
- }
86
72
  }
87
73
  &-lg {
88
74
  height: 16px;
@@ -95,10 +81,6 @@
95
81
  width: spacing('6');
96
82
  height: spacing('6');
97
83
  }
98
- &::-ms-thumb {
99
- width: spacing('6');
100
- height: spacing('6');
101
- }
102
84
  }
103
85
  &:disabled {
104
86
  opacity: 0.3;
@@ -0,0 +1,133 @@
1
+ @layer mw.layout {
2
+ // For a page that opens on a full-bleed picture and wants nothing on top of
3
+ // it: the bar starts above the screen and rides in over the first stretch of
4
+ // scroll. Inside the @supports, so a browser without scroll timelines gets a
5
+ // normal header rather than none at all.
6
+ //
7
+ // Its own file, and not next to .mw-header: that one uses the `animation`
8
+ // shorthand, which a consumer bundler folds together with a timeline into a
9
+ // declaration no browser accepts.
10
+ @supports (animation-timeline: scroll()) {
11
+ @media (prefers-reduced-motion: no-preference) {
12
+ .mw-header-reveal {
13
+ animation-name: mw-header-reveal;
14
+ animation-timing-function: var(--mw-ease-out);
15
+ animation-fill-mode: both;
16
+ animation-timeline: scroll(root);
17
+ animation-range: 0 420px;
18
+ }
19
+
20
+ // The ribbon is glued under the bar, so it has to leave and arrive with
21
+ // it - left behind it would hang alone at the top of an otherwise clean
22
+ // hero. No class of its own: a ribbon on a page with a revealing header
23
+ // has no other sensible behaviour.
24
+ body:has(.mw-header-reveal)
25
+ .mw-announcement:not(.mw-announcement-static) {
26
+ animation-name: mw-announcement-reveal;
27
+ animation-timing-function: var(--mw-ease-out);
28
+ animation-fill-mode: both;
29
+ animation-timeline: scroll(root);
30
+ animation-range: 0 420px;
31
+ }
32
+ }
33
+ }
34
+
35
+ // Both cover the height of the pair, or the shorter traveller arrives first
36
+ // and the two come in as two things instead of one block. Without a ribbon
37
+ // that height is just the bar's.
38
+ .mw-header-reveal {
39
+ --mw-header-reveal-offset: var(--mw-header-height);
40
+ }
41
+
42
+ body:has(.mw-announcement:not(.mw-announcement-static)) .mw-header-reveal {
43
+ --mw-header-reveal-offset: calc(
44
+ var(--mw-header-height) + var(--mw-announcement-height)
45
+ );
46
+ }
47
+
48
+ // A keyboard reaches the chrome long before the first scroll does, and a focus
49
+ // ring on something that is off screen is WCAG 2.4.11 - so focus brings it
50
+ // back, either half of the pair pulling in the other. `!important` is the one
51
+ // author declaration that outranks a running animation; dropping
52
+ // `animation-name` instead leaves the element stuck on its last value, because
53
+ // a restarted scroll timeline only catches up on the next scroll.
54
+ body:has(.mw-header-reveal:focus-within, .mw-announcement:focus-within) {
55
+ .mw-header-reveal {
56
+ top: 0 !important;
57
+ opacity: 1 !important;
58
+ }
59
+
60
+ .mw-announcement:not(.mw-announcement-static) {
61
+ transform: none !important;
62
+ opacity: 1 !important;
63
+ }
64
+ }
65
+
66
+ // Firefox ships no scroll timelines, so there the script toggles these
67
+ // classes instead (main.js) - the same two positions, triggered instead of
68
+ // scrubbed. State and transition are separate because the script sets the
69
+ // state a frame earlier: together they would send the bar sliding away in
70
+ // front of the reader instead of having it gone already.
71
+ @media (prefers-reduced-motion: no-preference) {
72
+ .mw-header-reveal.mw-header-away {
73
+ top: calc(-1 * var(--mw-header-reveal-offset));
74
+ opacity: 0;
75
+ }
76
+
77
+ .mw-header-driven {
78
+ // The shared list plus `top`, which is not in it - writing only `top`
79
+ // here would drop the header's own colour and shadow transitions.
80
+ transition:
81
+ var(--mw-transition),
82
+ top var(--mw-duration-slow) var(--mw-ease-out);
83
+ }
84
+
85
+ .mw-announcement.mw-announcement-away {
86
+ transform: translateY(
87
+ calc(-1 * (var(--mw-header-height) + var(--mw-announcement-height)))
88
+ );
89
+ opacity: 0;
90
+ }
91
+
92
+ .mw-announcement-driven {
93
+ transition:
94
+ transform var(--mw-duration-slow) var(--mw-ease-out),
95
+ opacity var(--mw-duration-slow) var(--mw-ease-out);
96
+ }
97
+ }
98
+
99
+ // `top` and not a transform: the off-canvas drawer inside the bar is
100
+ // position: fixed, and a transform on the header would make it the drawer's
101
+ // containing block - at every value, the resting one included.
102
+ // The wait sits in the keyframes and not in the range, because a range that
103
+ // starts past 0 leaves the bar in its resting state below that point - so the
104
+ // header would be visible for exactly the stretch it should stay away for.
105
+ @keyframes mw-header-reveal {
106
+ 0%,
107
+ 30% {
108
+ top: calc(-1 * var(--mw-header-reveal-offset));
109
+ opacity: 0;
110
+ }
111
+
112
+ 100% {
113
+ top: 0;
114
+ opacity: 1;
115
+ }
116
+ }
117
+
118
+ // The ribbon holds nothing fixed, so it can use the cheaper transform
119
+ @keyframes mw-announcement-reveal {
120
+ 0%,
121
+ 30% {
122
+ transform: translateY(
123
+ calc(-1 * (var(--mw-header-height) + var(--mw-announcement-height)))
124
+ );
125
+ opacity: 0;
126
+ }
127
+
128
+ 100% {
129
+ transform: translateY(0);
130
+ opacity: 1;
131
+ }
132
+ }
133
+ }
@@ -1,7 +1,11 @@
1
1
  @forward 'footer';
2
2
  @forward 'grid';
3
3
  @forward 'header';
4
+ @forward 'header-reveal';
4
5
  @forward 'home';
5
6
  @forward 'main';
6
7
  @forward 'page-header';
7
8
  @forward 'section';
9
+
10
+ // Last on purpose: mw-parallax-sticky has to beat the `position: relative`
11
+ @forward 'parallax';
@@ -41,12 +41,27 @@
41
41
  transition: var(--mw-transition);
42
42
  }
43
43
 
44
- // ...so the content has to sit above it
45
- > * {
44
+ // ...so the content has to sit above it - except the parallax layer,
45
+ // which is the one thing that belongs underneath
46
+ > *:not(.mw-parallax-media) {
46
47
  position: relative;
47
48
  z-index: z-index('card');
48
49
  }
49
50
 
51
+ // Add `mw-parallax` here and a `.mw-parallax-media` child and the picture
52
+ // becomes its own layer, free to drift or to stay pinned. It reads the
53
+ // same token, so the image is still configured in one place - and a
54
+ // second media element carrying `mw-parallax-pattern` takes the other.
55
+ &:has(> .mw-parallax-media) {
56
+ background-image: none;
57
+
58
+ --mw-parallax-pattern-image: var(--mw-hero-pattern);
59
+
60
+ > .mw-parallax-media:not(.mw-parallax-pattern) {
61
+ background-image: var(--mw-hero-background);
62
+ }
63
+ }
64
+
50
65
  // The same shadow the scroll cue already carries: the light theme lifts
51
66
  // the photo to brightness(1.1) and pale ink alone stops holding its edge
52
67
  h1,