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
@@ -4,286 +4,147 @@ $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
- // Ink on the hero image. The photo behind it is the same picture in both themes,
8
- // so the text on top cannot follow the theme: --mw-text-color would go dark the
9
- // moment the light theme is on, over a photo that stayed exactly as dark as it
10
- // was. Fixed to the light end of the palette instead, because a hero image is
11
- // usually the dark one. Set it to var(--mw-light-text-color) for a bright photo,
12
- // or to any color of your own.
7
+ // Fixed to the light end rather than theme-bound: the photo does not change with
8
+ // the theme, so its ink must not either. Use --mw-light-text-color on a bright one.
13
9
  $mw-hero-text-color: var(--mw-dark-text-color) !default;
14
10
 
15
- // How the hero image is treated per theme. A filter on the image, not a tinted
16
- // overlay: a translucent layer can only ever darken *toward its own colour*, so
17
- // on an already dark photo the two themes end up looking the same until the
18
- // alpha is high enough that the tint colour itself takes over - dark blue in
19
- // both, just more of it. brightness() moves the picture instead of covering it,
20
- // which reads as a difference even on a dark image.
21
- //
22
- // Kept below 1 in the dark theme and just above it in the light one, so the
23
- // photo stays dark enough for the fixed light ink either way. Any filter list
24
- // works - add contrast(), sepia(), grayscale() to taste.
11
+ // A filter on the image, not a tinted overlay: an overlay can only darken toward
12
+ // its own colour, so both themes end up alike on an already dark photo. Kept
13
+ // below 1 dark and just above it light, so the fixed light ink stays readable.
25
14
  $hero-filter-dark: brightness(0.7) !default;
26
15
  $hero-filter-light: brightness(1.1) !default;
27
16
 
28
- // ---------------------------------------------------------------------------
29
- // Root colors
30
- //
31
- // These are the only values a project has to set. Every other tone is derived
32
- // from them at runtime via color-mix(), so overriding a root token in plain
33
- // CSS (:root { --mw-primary-color: ... }) cascades to all of its variants -
34
- // including the lightened variants the dark theme needs.
35
- // ---------------------------------------------------------------------------
36
-
37
- // Brand
17
+ // Root colors - the only values a project has to set. Every other tone is derived
18
+ // from them at runtime with color-mix(), so overriding one in plain CSS cascades
19
+ // to all of its variants, the dark theme's lightened ones included.
38
20
  $primary-color: #1031bb !default;
39
21
  $secondary-color: #bb5710 !default;
40
22
 
41
- // Neutrals - a touch of the brand hue, so grays sit next to the brand colors
42
- // instead of fighting them
23
+ // A touch of the brand hue, so grays sit next to the brand instead of fighting it
43
24
  $gray-color: #6e6e78 !default;
44
- // Deep enough to read as dark, light enough that the surfaces derived from it
45
- // still have somewhere to go - see the surface stack below
25
+ // Light enough that the surfaces derived from it still have somewhere to go
46
26
  $dark-background: #171f30 !default;
47
27
  $dark-text-color: #f0eef9 !default;
48
28
  $light-background: #e7e7f0 !default;
49
29
  $light-text-color: #131925 !default;
50
- // Text on solid colored surfaces, in both themes. The one token that cannot be
51
- // derived: a dark brand color needs a light label, a light one a dark label.
52
- // Set it to the opposite end of your palette from $primary-color.
30
+ // The one token that cannot be derived - set it opposite $primary-color
53
31
  $accent-text-color: #f2f6fc !default;
54
32
  // Form controls stay light in both themes, so they get their own root value
55
33
  $form-elements-background: #f4f7fb !default;
56
34
 
57
- // Status - kept clear of the brand colors, so "accent" never reads as
58
- // "attention"
35
+ // Kept clear of the brand colors, so "accent" never reads as "attention"
59
36
  $success-color: #157f1a !default;
60
37
  $warning-color: #d1bb21 !default;
61
38
  $danger-color: #d41708 !default;
62
39
  $info-color: #0780ca !default;
63
40
 
64
- // Ink on the handful of surfaces that stay a fixed grey in both themes and
65
- // therefore cannot take --mw-accent-text-color: the tooltip, `mw-btn-mini`,
66
- // `mw-info-mini` and the gallery arrows. Compile-time on purpose - these are
67
- // not meant to be retuned per project, they just should not be four copies of
68
- // the same literal in four unrelated files.
41
+ // Ink on the surfaces that stay a fixed grey in both themes and therefore cannot
42
+ // take --mw-accent-text-color: tooltip, mw-btn-mini, mw-info-mini, gallery arrows
69
43
  $fixed-surface-text: #efefef !default;
70
44
 
71
- // Uniform hover tone for every solid brand/status surface. Darkening, in both
72
- // themes: the label on a solid surface is $accent-text-color, and lightening
73
- // a solid surface is what breaks it.
45
+ // Darkening in both themes: the label on a solid surface is $accent-text-color,
46
+ // and lightening the surface is what breaks it
74
47
  $hover-shift: 15% !default;
75
48
 
76
- // How light a color has to be before it reads as ink on a dark surface, and how
77
- // dark before it reads as ink on a light one - OKLch lightness, 0 to 1. A bound,
78
- // not a target: a color already inside the range is used untouched, so a bright
79
- // brand color stays bright and only one that would drown gets moved.
80
- //
81
- // 0.52 is where the default blue clears 3:1 on a dark card - the floor a rule
82
- // or a large label needs. At 0.4 it sat at 1.9:1: a price nobody reads and an
83
- // outline button whose border nobody finds. 0.58 does the same job for the
84
- // yellow on a light card, which was at 2.5:1, and is as low as that end can go
85
- // without collecting company - the orange and the cyan sit at 0.57, so one
86
- // step further moves three colors instead of one.
87
- //
88
- // Which is the point of a bound: in both directions exactly one of the six root
89
- // colors is outside it and gets moved. The other five pass through byte for
90
- // byte, and a project that swaps the palette gets the same treatment for
91
- // whichever of its own colors needs it.
49
+ // OKLch lightness a color needs to read as ink on a surface. A bound, not a
50
+ // target: a color already inside it passes through untouched. 0.52 is where the
51
+ // default blue clears 3:1 on a dark card, 0.58 does the same for the yellow on a
52
+ // light one - and 0.58 is as low as that end goes without moving three colors.
92
53
  $ink-lightness-dark: 0.52 !default;
93
54
  $ink-lightness-light: 0.58 !default;
94
- // How much of the primary color the muted text gray picks up, so it belongs to
95
- // the palette instead of being a stock #888. 0% is a true gray.
55
+ // How much primary the muted gray picks up, so it belongs to the palette. 0% is true gray.
96
56
  $muted-tint: 23% !default;
97
57
 
98
- // The surface stack - how far card and footer sit from the page background.
99
- // Each one scales the page's OKLch lightness *and* chroma by its factor and
100
- // keeps the hue, so the whole stack stays in one color family no matter how
101
- // tinted the page is.
102
- //
103
- // Below 1 steps toward black, above 1 toward white. Read them as "the card is
104
- // at 85% of the page's lightness": lower the dark factor or raise the light one
105
- // for more separation between a card and the page, move both toward 1 for a
106
- // flatter look.
107
- //
108
- // A card always steps *away* from the text color - down in the dark theme, up
109
- // in the light one - so content sits on the cleaner of the two surfaces. That is
110
- // also the one thing to watch when retuning: a near-black $dark-background
111
- // leaves nothing underneath, and a card at 0.6 of almost nothing is still
112
- // almost nothing. Keep the dark page around OKLch lightness 0.24 if you want
113
- // room for a low card factor.
58
+ // The surface stack: each factor scales the page's OKLch lightness and chroma and
59
+ // keeps the hue, so card and footer stay in the page's color family. A card always
60
+ // steps *away* from the text color. Watch the floor when retuning - a card at 0.6
61
+ // of a near-black page is still near-black, so keep the dark page around L 0.24.
114
62
  $card-surface-dark: 0.85 !default;
115
63
  $card-surface-light: 1.05 !default;
116
- // The footer band. In the dark theme it steps further out than the card, in the
117
- // light theme it steps the other way, so the band under the page reads as chrome
118
- // and not as another card. The header is not part of this - see below.
64
+ // Steps further out than the card in the dark theme and the other way in the
65
+ // light one, so the band reads as chrome rather than as another card
119
66
  $footer-surface-dark: 0.75 !default;
120
67
  $footer-surface-light: 0.95 !default;
121
68
 
122
- // The header bar. It stays dark in both themes, so it cannot be part of the
123
- // surface stack above: a project that only ever shows the light theme would be
124
- // styling its header through a dark page color it never configured, and the bar
125
- // would keep the framework default no matter what the palette says.
126
- //
127
- // It is built from the primary color instead - the one token every palette has
128
- // to set - darkened straight toward black, which is the ramp every color tool
129
- // shows as a color's shades. Kept well past the bottom of that ramp on purpose:
130
- // this deep the brand hue is a tint on near-black rather than a color of its
131
- // own, which is what keeps the bar reading as chrome behind the page.
132
- //
133
- // Lower it for a darker bar. The ink on the bar is near-white, so going far up
134
- // means overriding --mw-header-text-color and
135
- // --mw-header-navbar-list-active-color with it.
69
+ // The bar stays dark in both themes, so it cannot come from the surface stack -
70
+ // a light-only project would be styling it through a page color it never set.
71
+ // Built from the primary instead, darkened so deep that the hue reads as a tint
72
+ // on near-black. Going far up means overriding the header ink tokens with it.
136
73
  $header-surface: 14% !default;
137
74
 
138
- // How far the active navbar link is lifted off the primary colour. Lower is a
139
- // stronger, more saturated blue and closer to the brand tone; higher is paler
140
- // and carries more contrast against the bar.
141
- //
142
- // 25% lands on #435fd5 at 3.67:1 over the bar - past the 3:1 floor a large or
143
- // bold label needs, under the 4.5:1 an AA body-size label wants. That is a
144
- // deliberate trade for a nav item that is already marked by an underline and a
145
- // weight change as well as by colour; raise it toward 35% for the safe end.
75
+ // 25% lands at 3.67:1 over the bar: past the 3:1 floor for a bold label, under
76
+ // AA body size. A deliberate trade - the item also carries an underline and a
77
+ // weight change. Raise it toward 35% for the safe end.
146
78
  $header-active-tint: 25% !default;
147
79
 
148
- // The shadow pair every elevation is built from - see elevation() below.
149
- //
150
- // $shadow-cast-dark is how far a shadow in the dark theme sits below the page
151
- // colour. A black shadow on a near-black page is invisible, which is why the
152
- // framework used to carry a single light glow instead; the glow survives as the
153
- // far layer, and this is the near one that puts weight back underneath it.
80
+ // How far a dark-theme shadow sits below the page colour - a black shadow on a
81
+ // near-black page is invisible, so this is the near layer that puts weight back
154
82
  $shadow-cast-dark: 0.3 !default;
155
- // Near (contact) and far (ambient) opacity per theme. The dark theme's far layer
156
- // is the light rim that reads as elevation on a dark page, so it stays low - a
157
- // bright glow carries much further than a dark shadow at the same alpha.
83
+ // Near (contact) and far (ambient) opacity. The dark theme's far layer is a light
84
+ // rim, so it stays low - a glow carries further than a shadow at the same alpha.
158
85
  $shadow-near-dark: 55% !default;
159
86
  $shadow-far-dark: 13% !default;
160
87
  $shadow-near-light: 14% !default;
161
88
  $shadow-far-light: 10% !default;
162
89
 
163
- // ---------------------------------------------------------------------------
164
- // Control sizes
165
- //
166
- // One height per step, shared by everything that can end up in a row next to
167
- // something else: input, select, textarea, button.
168
- //
169
- // Before this each control worked its height out from its own font size, its own
170
- // line-height and its own padding, and no two of them agreed - an input, a
171
- // select and a button side by side measured 32.2, 34.4 and 37.2 pixels. That is
172
- // the kind of flaw you see without being able to name it: the row just looks
173
- // slightly wrong.
174
- //
175
- // A minimum, not a fixed height, so a control whose content needs more room
176
- // still gets it. The step is what makes the trio line up; the font scale below
177
- // is what makes them look like one family.
178
- // ---------------------------------------------------------------------------
179
- // 32 / 36 / 40px - an even 4px step. The middle one used to be 2.35rem, which
180
- // made the ramp 32 / 37.6 / 41.6: a 5.6px step to the base size and a 4px one
181
- // past it, and a base field a shade taller than it needed to be.
182
- //
183
- // The padding is not what sets these. A field's height comes from the minimum
184
- // below, so trimming `padding-block` on its own changes where the text sits
185
- // inside the box and not how tall the box is.
90
+ // Control sizes - 32 / 36 / 40px, shared by input, select, textarea and button so
91
+ // the row lines up. A minimum, not a fixed height, and it is what sets a field's
92
+ // height: trimming `padding-block` moves the text inside the box, not the box.
186
93
  $control-height-sm: 2rem !default;
187
94
  $control-height: 2.25rem !default;
188
95
  $control-height-lg: 2.5rem !default;
189
96
 
190
- // The font each step is set in. All three fields share it - the select used to
191
- // sit a whole step below the input beside it, which is why the two never looked
192
- // like a pair. Buttons stay one step above: a button carries a label, a field
193
- // carries data the user typed, and the label may be the louder of the two.
97
+ // All three fields share it; buttons stay one step above, because a button
98
+ // carries a label and a field carries what the user typed
194
99
  $control-font-sm: 0.8rem !default;
195
100
  $control-font: 0.9rem !default;
196
101
  $control-font-lg: 1rem !default;
197
102
 
198
- // Stated rather than inherited. `line-height: normal` is whatever the font
199
- // vendor decided, so an input and a select with the same padding still came out
200
- // different heights - and it changes the moment a project swaps the font stack.
103
+ // Stated, not inherited: `normal` is whatever the font vendor decided, and it
104
+ // changes the moment a project swaps the font stack
201
105
  $control-line-height: 1.35 !default;
202
106
 
203
- // ---------------------------------------------------------------------------
204
- // Motion
205
- //
206
- // Four durations and three curves, and every transition in the framework is
207
- // built from them. The point is not the numbers - it is that a hover, a drawer
208
- // and a modal that all say "0.3s ease" feel like three different products,
209
- // because 300ms is twice too slow for a colour swap and half too fast for a
210
- // panel crossing the screen.
211
- //
212
- // The curves: 'out' decelerates and is what almost everything wants, because
213
- // something arriving should land rather than stop. 'in-out' is symmetric, for
214
- // anything that moves from A to B and back. 'spring' overshoots a few percent,
215
- // which is what makes a press or a pop read as physical instead of animated.
216
- // ---------------------------------------------------------------------------
107
+ // Motion - one scale for everything, because 300ms is twice too slow for a colour
108
+ // swap and half too fast for a panel crossing the screen.
217
109
  $duration-instant: 110ms !default;
218
110
  $duration-fast: 180ms !default;
219
111
  $duration-base: 300ms !default;
220
112
  $duration-slow: 520ms !default;
221
113
  $duration-slower: 900ms !default;
222
114
 
223
- // The curves.
224
- //
225
- // 'out' was cubic-bezier(0.22, 1, 0.36, 1) for one revision, and it was wrong:
226
- // that curve is so front-loaded it covers about seventy percent of the distance
227
- // in the first quarter of the time. On a colour swap that reads as snappy. On
228
- // anything that actually travels - an image scaling inside a card, a panel
229
- // sliding - it reads as a jump followed by a crawl, which is the opposite of
230
- // smooth. This one decelerates evenly and is the right default for both.
115
+ // Decelerates evenly. A front-loaded curve reads as snappy on a colour swap and
116
+ // as a jump followed by a crawl on anything that travels.
231
117
  $ease-out: cubic-bezier(0.33, 1, 0.68, 1) !default;
232
118
  $ease-in-out: cubic-bezier(0.65, 0, 0.35, 1) !default;
233
119
  // A few percent of overshoot, not a bounce - the earlier 1.56 read as a wobble
234
120
  $ease-spring: cubic-bezier(0.34, 1.3, 0.64, 1) !default;
235
121
 
236
- // Anything that moves a large area rather than recolouring one - an image
237
- // zooming inside a card, a tile growing. Distance and duration go together: the
238
- // same 300ms that feels right on a 40px button feels snatched on a 300px photo,
239
- // because the eye reads speed as distance over time, not as time.
122
+ // For anything that moves a large area: the 300ms that fits a 40px button feels
123
+ // snatched on a 300px photo, because the eye reads speed as distance over time
240
124
  $duration-zoom: 650ms !default;
241
125
 
242
- // What --mw-transition actually animates.
243
- //
244
- // It used to be `all`, which is a footgun in two directions: it animates
245
- // properties nobody meant to animate - a width that changes at a breakpoint
246
- // crawls to its new value - and it forces the browser to check every property
247
- // on every change. The list below covers every paint-only property a hover,
248
- // focus or active state touches, and nothing that moves layout. Anything that
249
- // really does want to animate a size says so itself.
126
+ // Every paint-only property a hover, focus or active state touches, and nothing
127
+ // that moves layout. `all` would crawl a width that changed at a breakpoint.
250
128
  $transition-properties:
251
129
  color, background-color, background-image, border-color, box-shadow, opacity,
252
130
  transform, outline-color, outline-offset, filter, backdrop-filter, fill,
253
131
  stroke, text-decoration-color !default;
254
132
 
255
- // ---------------------------------------------------------------------------
256
- // Focus ring
257
- //
258
- // One ring for the whole framework, on :focus-visible only - a mouse click on a
259
- // button should not leave it outlined. 2px is the floor at which a ring stays
260
- // visible against a busy surface, and the offset is what keeps it from reading
261
- // as a border on the control itself.
262
- // ---------------------------------------------------------------------------
133
+ // One ring for the whole framework, on :focus-visible only. 2px is the floor at
134
+ // which it stays visible on a busy surface; the offset keeps it off the border.
263
135
  $focus-ring-width: 2px !default;
264
136
  $focus-ring-offset: 2px !default;
265
- // The soft halo a form field gets instead of a hard ring - a field already has a
266
- // border to recolour, so a second hard line around it reads as a mistake.
137
+ // A field already has a border to recolour, so it gets a soft halo, not a ring
267
138
  $focus-halo-size: 3px !default;
268
139
  $focus-halo-opacity: 28% !default;
269
140
 
270
- // ---------------------------------------------------------------------------
271
- // Derivation helpers
272
- //
273
- // They emit color-mix() referring back to another --mw-* token instead of
274
- // baking a value at compile time.
275
- //
276
- // shade()/tint() mix `in hsl`, which behaves like color.adjust($lightness: ...)
277
- // and scales proportionally, so a derivation looks the same on every palette.
278
- // Mixing with black/white also lowers the saturation a little - that is why
279
- // hover tones read slightly softer than the base color. Alpha blending uses
280
- // `srgb` against `transparent`.
141
+ // Derivation helpers - they emit color-mix() against another --mw-* token rather
142
+ // than baking a value at compile time.
281
143
  //
282
- // A derived token may only reference a token whose value does not depend on the
283
- // active theme, i.e. a root color or a theme-prefixed one (dark-*/light-*).
284
- // Custom properties are substituted on the element they are declared on, so a
285
- // base token pointing at a theme alias would freeze the dark value on :root.
286
- // ---------------------------------------------------------------------------
144
+ // A derived token may only reference one that does not depend on the active theme,
145
+ // so a root color or a dark-*/light-* alias. Custom properties are substituted on
146
+ // the element they are declared on, so pointing at a theme alias would freeze the
147
+ // dark value on :root.
287
148
 
288
149
  // Reference another token as-is
289
150
  @function token($name) {
@@ -311,32 +172,26 @@ $focus-halo-opacity: 28% !default;
311
172
  );
312
173
  }
313
174
 
314
- // Translucent variant of an arbitrary color expression. fade() is the same thing
315
- // for a token name; this one takes whatever another helper handed back, which is
316
- // what the shadow tones below need.
175
+ // Same as fade(), but for whatever another helper handed back rather than a name
317
176
  @function alpha($color, $amount) {
318
177
  @return string.unquote(
319
178
  'color-mix(in srgb, #{$color} #{$amount}, transparent)'
320
179
  );
321
180
  }
322
181
 
323
- // Darkened straight toward black in sRGB - the ramp a color tool prints as a
324
- // color's shades, where $amount is how much of the color survives. shade()
325
- // cannot stand in for it: mixing in hsl scales saturation along with lightness
326
- // and hands back a washed-out tone, which is what a hover state wants and what
327
- // a surface meant to still read as the brand color does not.
182
+ // Darkened straight toward black, $amount being how much of the color survives.
183
+ // shade() washes out the saturation on the way, which a surface must not do.
328
184
  @function deepen($name, $amount) {
329
185
  @return string.unquote(
330
186
  'color-mix(in srgb, var(--mw-#{$name}) #{$amount}, black)'
331
187
  );
332
188
  }
333
189
 
334
- // The two ink variants. They clamp lightness in OKLch and keep hue and chroma,
335
- // which is what makes them palette-agnostic: the same expression lifts a very
336
- // dark color, deepens a very light one and leaves everything in between alone.
337
- @function ink-on-dark($name) {
190
+ // Clamp lightness in OKLch, keep hue and chroma: the same expression lifts a very
191
+ // dark color, deepens a very light one and leaves everything between alone
192
+ @function ink-on-dark($name, $lightness: $ink-lightness-dark) {
338
193
  @return string.unquote(
339
- 'oklch(from var(--mw-#{$name}) max(l, #{$ink-lightness-dark}) c h)'
194
+ 'oklch(from var(--mw-#{$name}) max(l, #{$lightness}) c h)'
340
195
  );
341
196
  }
342
197
 
@@ -346,40 +201,25 @@ $focus-halo-opacity: 28% !default;
346
201
  );
347
202
  }
348
203
 
349
- // A theme surface derived from the page background: OKLch lightness and chroma
350
- // scaled by the same factor, hue kept. Below 1 steps toward black, above 1
351
- // toward white. Chroma rides along because that is what a hand-picked stack
352
- // does - a darker surface of the same hue carries less color, not the same
353
- // color at a lower lightness, which would read oversaturated.
204
+ // Lightness and chroma scaled by the same factor, hue kept. Chroma rides along
205
+ // because a darker surface carries less color, not the same color darker.
354
206
  @function surface($name, $factor) {
355
207
  @return string.unquote(
356
208
  'oklch(from var(--mw-#{$name}) calc(l * #{$factor}) calc(c * #{$factor}) h)'
357
209
  );
358
210
  }
359
211
 
360
- // Muted text tone. Built from a true gray on purpose, not by stepping the
361
- // theme's text color back: darkening keeps the source saturation and fading
362
- // keeps its hue, so a tinted text color hands its tint straight through - a
363
- // mint white page ends up with mint green secondary text. Starting neutral and
364
- // adding back a defined share of the brand keeps that under control. Opaque,
365
- // so it reads the same on the page and on a card.
212
+ // Built from a true gray, not by stepping the text color back: that would hand a
213
+ // tinted text color's tint straight through - a mint page, mint secondary text
366
214
  @function muted($lightness) {
367
215
  @return string.unquote(
368
216
  'color-mix(in oklab, oklch(#{$lightness} 0 0), var(--mw-primary-color) #{$muted-tint})'
369
217
  );
370
218
  }
371
219
 
372
- // Two-layer elevation.
373
- //
374
- // The near layer is the contact shadow - short offset, tight blur - that gives
375
- // an element weight and pins it to the surface underneath. The far layer is the
376
- // ambient spread that says how high it floats. One layer on its own reads as a
377
- // blur behind the box; the pair reads as light, which is the whole difference
378
- // between a framework that looks drawn and one that looks rendered.
379
- //
380
- // Both tones are theme-prefixed tokens, so the ramp is declared once per theme
381
- // and the same five level names work in either one - see the note on derived
382
- // tokens above for why they cannot be one shared set on :root.
220
+ // Near is the contact shadow that pins the box down, far the ambient spread that
221
+ // says how high it floats. One layer alone reads as a blur, the pair as light.
222
+ // Theme-prefixed tokens, so the ramp is declared once per theme.
383
223
  @function elevation($theme, $y-near, $blur-near, $y-far, $blur-far) {
384
224
  @return string.unquote(
385
225
  '0 #{$y-near} #{$blur-near} var(--mw-#{$theme}-shadow-near),' +
@@ -387,61 +227,37 @@ $focus-halo-opacity: 28% !default;
387
227
  );
388
228
  }
389
229
 
390
- // Base colors that don't change between themes.
391
- // Emitted as --mw-<key>.
392
- //
393
- // A brand color has two jobs, and they need two tokens:
394
- // --mw-primary-color the exact brand tone, for anything it *fills* -
395
- // buttons, badges, bars, filled surfaces. The label
396
- // on top is --mw-primary-accent-text-color, which
397
- // falls back to the shared --mw-accent-text-color.
398
- // --mw-primary-text-color the same color as ink on a theme surface - text,
399
- // icons, focus rings, accent borders. Lives in the
400
- // theme maps below, because a color chosen to carry
401
- // a label is by definition too dark or too light to
402
- // be read *on* the page it sits on.
403
- // Same split for secondary and for every status color.
230
+ // Emitted as --mw-<key>. A brand color has two jobs and needs two tokens:
231
+ // --mw-primary-color is the tone it *fills* with, --mw-primary-text-color the
232
+ // same color as ink on a theme surface. The second lives in the theme maps below,
233
+ // because a color picked to carry a label cannot be read *on* the page as well.
234
+ // Same split for secondary and every status color.
404
235
  $base-colors: (
405
236
  'primary-color': $primary-color,
406
237
  'primary-color-hover': shade('primary-color', $hover-shift),
407
- // One rule for every tinted surface in the framework: 20% of the color, 45%
408
- // while hovered. Both stay close enough to the surface underneath that the
409
- // theme's own --mw-text-color keeps working on top - which is what makes a
410
- // soft alert, badge or tag readable in the light *and* the dark theme.
238
+ // Every tinted surface in the framework, and both stay close enough to the one
239
+ // underneath that the theme's own --mw-text-color keeps working on top
411
240
  'primary-background': fade('primary-color', 30%),
412
241
  'primary-background-hover': fade('primary-color', 50%),
413
- // Same surface under the name the status colors use, so a component can
414
- // interpolate one token name across all six colors
242
+ // The name the status colors use, so a component can interpolate one token name
415
243
  'primary-info-background': token('primary-background'),
416
244
  'secondary-color': $secondary-color,
417
245
  'secondary-color-hover': shade('secondary-color', $hover-shift),
418
246
  'secondary-background': fade('secondary-color', 30%),
419
247
  'secondary-background-hover': fade('secondary-color', 50%),
420
248
  'secondary-info-background': token('secondary-background'),
421
- // Text on any solid colored surface - buttons of every variant, table and
422
- // panel headers, badges, tooltips, mini close buttons, stepper dots.
423
- // Deliberately derived from neither a brand color nor a theme text color:
424
- // it has to stay readable on a dark surface no matter how the palette or the
425
- // active theme is retuned.
249
+ // Derived from neither a brand color nor a theme text color: it has to stay
250
+ // readable on a solid surface however the palette or the theme is retuned
426
251
  'accent-text-color': $accent-text-color,
427
- // Per-color override of that label. One accent text color only works while
428
- // the whole palette sits on the same side of the lightness scale - a neon
429
- // green primary next to a deep blue secondary needs a dark label on the one
430
- // and a light label on the other. Defaults to the shared token, so nothing
431
- // changes until a project sets one:
432
- // :root { --mw-primary-accent-text-color: #000; }
433
- // Every component that fills a surface with one of the six colors reads the
434
- // matching token, so a button, a table header, a panel header, a badge and
435
- // the burger button all follow the same override. The status colors carry
436
- // the same token, see $status-colors.
252
+ // Per-color override, for a palette that does not sit on one side of the
253
+ // lightness scale. Defaults to the shared token, so nothing changes until a
254
+ // project sets one - every filled surface reads the matching token.
437
255
  'primary-accent-text-color': token('accent-text-color'),
438
256
  'secondary-accent-text-color': token('accent-text-color'),
439
257
  'gray-color': $gray-color,
440
258
  // Subtle neutral surface: zebra rows, disabled fields, tracks, skeletons
441
259
  'gray-background': fade('gray-color', 20%),
442
- // Same surface under the name people reach for when they want a slightly
443
- // set-off area *inside* a card - hint block, framed paragraph, form summary.
444
- // An alias on purpose: one tone, two names, so nobody invents a third.
260
+ // Alias on purpose: one tone, two names, so nobody invents a third
445
261
  'surface-muted': token('gray-background'),
446
262
  // Heavy scrim behind modals and blocking spinners
447
263
  'overlay-background': fade('gray-color', 60%),
@@ -475,24 +291,29 @@ $status-colors: (
475
291
  'info-accent-text-color': token('accent-text-color'),
476
292
  );
477
293
 
294
+ // The six colors a component offers variants in, as one map rather than a copy in
295
+ // each component that loops over them.
296
+ // The fill tone, not the ink tone - every consumer of this map paints with it
297
+ $semantic-colors: (
298
+ primary: var(--mw-primary-color),
299
+ secondary: var(--mw-secondary-color),
300
+ info: var(--mw-info-color),
301
+ success: var(--mw-success-color),
302
+ warning: var(--mw-warning-color),
303
+ danger: var(--mw-danger-color),
304
+ );
305
+
478
306
  // Dark theme colors. Emitted as --mw-dark-<key>, hence the prefixed references.
479
307
  $dark-theme-colors: (
480
308
  'page-background': $dark-background,
481
309
  'hero-image-filter': $hero-filter-dark,
482
- // The surface stack - the card and footer factors and the reasoning behind
483
- // them live with the derivation knobs above, so a project can retune the
484
- // stack from one place instead of editing this map. The rule between the
485
- // surfaces keeps its own factor: it runs against the card - lighter than a
486
- // dark page, darker than a light one - or it disappears into what it
487
- // separates, and that is not a per-project decision.
310
+ // Factors live with the derivation knobs above so the stack retunes from one
311
+ // place. The rule keeps its own: it runs against the card or it disappears.
488
312
  'card-background': surface('dark-page-background', $card-surface-dark),
489
313
  'footer-background': surface('dark-page-background', $footer-surface-dark),
490
314
  'border': surface('dark-page-background', 1.35),
491
315
  'shadow': fade('dark-text-color', 30%),
492
- // The two tones every elevation level is mixed from, and the five-step ramp
493
- // itself. A component names a level - `var(--mw-elevation-2)` - instead of
494
- // inventing another `0 5px 9px`; before this the framework carried twelve
495
- // different shadow values for what were really four heights.
316
+ // A component names a level instead of inventing another `0 5px 9px`
496
317
  'shadow-near': alpha(
497
318
  surface('dark-page-background', $shadow-cast-dark),
498
319
  $shadow-near-dark
@@ -505,21 +326,19 @@ $dark-theme-colors: (
505
326
  'elevation-5': elevation('dark', 10px, 20px, 30px, 64px),
506
327
  'text-color': $dark-text-color,
507
328
  'text-muted-color': muted(0.7),
508
- // Every color as ink on this page. Derived, not hardcoded - a project that
509
- // overrides a root color gets a readable dark-theme variant without setting a
510
- // second value, and a palette that is already light passes through unchanged.
329
+ // Derived, so overriding a root color yields a readable dark variant for free
511
330
  'primary-text-color': ink-on-dark('primary-color'),
512
331
  'secondary-text-color': ink-on-dark('secondary-color'),
513
332
  'success-text-color': ink-on-dark('success-color'),
514
333
  'warning-text-color': ink-on-dark('warning-color'),
515
334
  'danger-text-color': ink-on-dark('danger-color'),
516
335
  'info-text-color': ink-on-dark('info-color'),
517
- // Translucent accent line used for dividers, rules and outlines. Built from
518
- // the ink tone, not the fill tone, or the line disappears into the page.
336
+ // From the ink tone, not the fill tone, or the line disappears into the page
519
337
  'border-accent': fade('dark-primary-text-color', 70%),
520
- // The accent arc on the two round corners of every mw-surface. Full strength
521
- // here: against a dark card the ink tone is the faintest it gets.
338
+ // Full strength: against a dark card the ink tone is the faintest it gets
522
339
  'corner-accent': token('dark-primary-text-color'),
340
+ // Not tint(): mixing toward white washes the arc out instead of brightening it
341
+ 'corner-accent-hover': ink-on-dark('primary-color', 0.72),
523
342
  );
524
343
 
525
344
  // Light theme colors. Emitted as --mw-light-<key>.
@@ -530,10 +349,6 @@ $light-theme-colors: (
530
349
  'footer-background': surface('light-page-background', $footer-surface-light),
531
350
  'border': surface('light-page-background', 0.8),
532
351
  'shadow': fade('light-text-color', 30%),
533
- // The two tones every elevation level is mixed from, and the five-step ramp
534
- // itself. A component names a level - `var(--mw-elevation-2)` - instead of
535
- // inventing another `0 5px 9px`; before this the framework carried twelve
536
- // different shadow values for what were really four heights.
537
352
  'shadow-near': fade('light-text-color', $shadow-near-light),
538
353
  'shadow-far': fade('light-text-color', $shadow-far-light),
539
354
  'elevation-1': elevation('light', 1px, 2px, 1px, 4px),
@@ -543,8 +358,7 @@ $light-theme-colors: (
543
358
  'elevation-5': elevation('light', 10px, 20px, 30px, 64px),
544
359
  'text-color': $light-text-color,
545
360
  'text-muted-color': muted(0.5),
546
- // Same idea, other direction: a color too light to be read on paper is
547
- // deepened, everything else passes through as it is
361
+ // Other direction: a color too light to read on paper is deepened
548
362
  'primary-text-color': ink-on-light('primary-color'),
549
363
  'secondary-text-color': ink-on-light('secondary-color'),
550
364
  'success-text-color': ink-on-light('success-color'),
@@ -552,38 +366,31 @@ $light-theme-colors: (
552
366
  'danger-text-color': ink-on-light('danger-color'),
553
367
  'info-text-color': ink-on-light('info-color'),
554
368
  'border-accent': fade('light-primary-text-color', 70%),
555
- // Held back a little against the dark theme: the same blue on a near-white
556
- // card carries far more contrast than it does on a dark one, and at full
557
- // strength the arc stops being a detail and starts being a frame.
369
+ // Held back: on a near-white card the same tone carries far more contrast, and
370
+ // at full strength the arc stops being a detail and starts being a frame
558
371
  'corner-accent': fade('light-primary-text-color', 80%),
372
+ // Full strength is the frame a resting card must not have and a hovered one wants
373
+ 'corner-accent-hover': token('light-primary-text-color'),
559
374
  );
560
375
 
561
- // Header/navbar colors. The bar is dark in both themes, so every tone here is
562
- // built from the primary color rather than from a theme token: the header is the
563
- // one piece of chrome a light-only project would otherwise be styling through
564
- // dark-theme values it never set. Emitted as --mw-header-<key>, and each one is
565
- // still overridable on its own.
376
+ // Emitted as --mw-header-<key>. Built from the primary rather than a theme token,
377
+ // because the bar is dark in both themes - see $header-surface.
566
378
  $header-colors: (
567
379
  'background': deepen('primary-color', $header-surface),
568
380
  'text-color': tint('primary-color', 92%),
569
381
  'navbar-list-color': fade('header-text-color', 70%),
570
382
  'navbar-list-active-color': tint('primary-color', $header-active-tint),
571
- // The burger sits on the primary button surface, and on the secondary one
572
- // while the drawer is open, so it takes the label ink of each - not the
573
- // header text color, which is picked for the dark bar behind it.
383
+ // The burger sits on a button surface, so it takes that label's ink
574
384
  'burgerbutton-color': token('primary-accent-text-color'),
575
385
  'burgerbutton-open-color': token('secondary-accent-text-color'),
576
386
  'border': fade('header-navbar-list-active-color', 40%),
577
387
  );
578
- // Drives the bar height, the anchor scroll offset and the mobile drawer padding.
579
- // 59px is the intrinsic height: 42px logo + 2 × spacing('3') + 1px border.
388
+ // Drives bar height, anchor scroll offset and drawer padding. Intrinsic height is
389
+ // 59px: 42px logo + 2 × spacing('3') + 1px border.
580
390
  $header-height: 62px;
581
391
 
582
- // Typography
583
- //
584
- // The stacks live here and not next to the typography rules, because this is
585
- // the file `@use 'main' with (...)` configures. No font is bundled - a project
586
- // that wants its own loads it and overrides the stack.
392
+ // Here and not next to the typography rules, because this is the file
393
+ // `@use 'main' with (...)` configures. No font is bundled.
587
394
  $font-family-base:
588
395
  -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu,
589
396
  Cantarell, 'Helvetica Neue', sans-serif !default;
@@ -650,9 +457,7 @@ $spacing: (
650
457
  );
651
458
 
652
459
  // Breakpoints
653
- // Card width at which a card footer stops being a row. Shared: the footer rule
654
- // lives in _cards.scss, the exception for a price in it in _pricing.scss, and
655
- // the two have to agree or a price gets the row back one pixel too late.
460
+ // Shared by _cards.scss and _pricing.scss, which have to agree on it
656
461
  $mw-card-narrow: 360px !default;
657
462
 
658
463
  $breakpoints: (
@@ -669,7 +474,6 @@ $breakpoints: (
669
474
  '2xl': 1400px,
670
475
  );
671
476
 
672
- // Border radius
673
477
  $radius: (
674
478
  'none': 0,
675
479
  'xs': 2px,