maverick-wave 5.24.0 → 5.25.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 (33) hide show
  1. package/.claude/skills/mw-maverick-wave/SKILL.md +32 -5
  2. package/.claude/skills/mw-maverick-wave/references/components.md +4 -0
  3. package/.claude/skills/mw-maverick-wave/references/javascript.md +44 -8
  4. package/.claude/skills/mw-maverick-wave/references/layout.md +23 -1
  5. package/.claude/skills/mw-maverick-wave/references/theming.md +21 -1
  6. package/CHANGELOG.md +14 -0
  7. package/CLAUDE.md +4 -0
  8. package/README.md +4 -2
  9. package/index.html +13 -0
  10. package/maverick-wave.min.css +4 -4
  11. package/maverick-wave.min.js +1 -1
  12. package/package.json +2 -2
  13. package/src/js/main.js +149 -57
  14. package/src/partials/get-started-container.html +5 -10
  15. package/src/partials/input-group-container.html +1 -3
  16. package/src/partials/tags-container.html +0 -6
  17. package/src/scss/components/_blog-post.scss +15 -10
  18. package/src/scss/components/_button-bar.scss +10 -6
  19. package/src/scss/components/_code.scss +23 -34
  20. package/src/scss/components/_info.scss +8 -1
  21. package/src/scss/components/_kanban.scss +6 -6
  22. package/src/scss/components/_meta-info.scss +7 -6
  23. package/src/scss/components/_tables.scss +6 -4
  24. package/src/scss/components/_tiles.scss +20 -30
  25. package/src/scss/layout/_grid.scss +28 -0
  26. package/src/scss/layout/_header.scss +16 -9
  27. package/src/scss/layout/_home.scss +21 -2
  28. package/src/scss/layout/_main.scss +7 -7
  29. package/src/scss/layout/_page-header.scss +4 -4
  30. package/src/scss/layout/_section.scss +9 -9
  31. package/src/scss/main-lean.scss +16 -0
  32. package/src/scss/utilities/_index.scss +1 -0
  33. package/src/scss/utilities/_print.scss +107 -0
@@ -75,6 +75,22 @@ Or from the npm package via SCSS: `@use 'maverick-wave/src/scss/main';`
75
75
  (details and configuration in `references/theming.md`). The viewport meta above
76
76
  belongs in `src/index.html` there, with `viewport-fit=cover` just the same.
77
77
 
78
+ **`main-lean` for a page that uses a handful of components.** The full build is
79
+ ~41 kB gzipped and carries all 48 of them. `main-lean` is the same framework
80
+ without any component - tokens, reset, typography, layout, utilities - and the
81
+ components come in one by one next to it:
82
+
83
+ ```scss
84
+ @use 'maverick-wave/src/scss/main-lean' with (
85
+ $primary-color: #1b1b1d
86
+ );
87
+ @use 'maverick-wave/src/scss/components/buttons';
88
+ @use 'maverick-wave/src/scss/components/cards';
89
+ ```
90
+
91
+ That lands at ~16 kB gzipped plus what the components add (three of them ≈ 19 kB
92
+ in total). The configuration goes on `main-lean`, not on the component imports.
93
+
78
94
  > **Never load `maverick-wave.min.js` in a SPA.** Everything in it is wired up
79
95
  > once on `DOMContentLoaded` and writes straight into the DOM. In an Angular
80
96
  > app that fires once during bootstrap: components rendered later are never
@@ -166,6 +182,11 @@ between them moved.
166
182
  Column grids and most components are mobile-first. The mixins emit range syntax
167
183
  (`media-up` is `width >= bp`, `media-down` is `width < bp`), so the two never
168
184
  overlap at the breakpoint itself and neither has to stop a fraction short of it.
185
+ Write `media-up`: the base block is the phone and each breakpoint adds to it.
186
+ `media-down` is right in three cases - a rule that sets what the base never sets
187
+ (turned around it would hard-wire an inherited value), a narrow layout that costs
188
+ more properties than the wide one (the navbar sheet, the modal bottom sheet), and
189
+ a class that only ever means "below here", like `mw-hide-mobile`.
169
190
 
170
191
  **Overriding.** Everything the framework emits sits in
171
192
  `@layer mw.reset, mw.base, mw.forms, mw.components, mw.layout, mw.utilities`.
@@ -234,7 +255,8 @@ none of them survive into the compiled CSS. Full list in `references/theming.md`
234
255
  zoom. A headline passes `$to: 'xl'`.
235
256
  - `fluid-container($min, $max, $from, $to)` - the same ramp in `cqi` against the
236
257
  container. Use it instead of `fluid()` inside a `@container` block.
237
- - `media-up($bp)` / `media-down($bp)` - the breakpoint map as a query.
258
+ - `media-up($bp)` / `media-down($bp)` - the breakpoint map as a query. `media-up`
259
+ is the default direction, see **Breakpoints** above.
238
260
  - `touch-context($bp: 'md')` - coarse pointer _or_ narrow viewport, the condition
239
261
  every target-size rule in the framework hangs under.
240
262
  - `touch-floor($size: 2.75rem)` - that condition plus a `min-height`.
@@ -299,7 +321,7 @@ feature frame) ·
299
321
  `mw-content` ·
300
322
  `mw-section` · `mw-section-head` (+ `-title`, `-intro`, `-numbered`, colours) ·
301
323
  `mw-page-header` · `mw-grid-*`
302
- (+ `mw-grid-even`) ·
324
+ (+ `mw-grid-even`, `mw-grid-from-{sm,md,lg,xl}`) ·
303
325
  `mw-columns-2/3` · `mw-row-split` · `mw-hero` (+ `mw-scroll-hint`, `-end`) ·
304
326
  `mw-parallax` (+ `-media`, `-content`, `-dimmed`, `-sticky`, `-rise`,
305
327
  `-slow`, `-pattern`) · `mw-footer`
@@ -312,7 +334,7 @@ feature frame) ·
312
334
  `mw-glow-{primary,secondary,info,success,warning,danger}` ·
313
335
  `mw-corner-plain` ·
314
336
  `mw-aspect-square|video|wide|portrait|photo` · `mw-d-{sm,md,lg,xl}-*` /
315
- `mw-hide-mobile` / `mw-hide-desktop` · `mw-overflow-*` / `mw-snap-x` ·
337
+ `mw-hide-mobile` / `mw-hide-desktop` / `mw-hide-print` / `mw-print-only` · `mw-overflow-*` / `mw-snap-x` ·
316
338
  `mw-reveal` / `mw-reveal-stagger` · spacing, flex, display, text
317
339
 
318
340
  ## Pitfalls
@@ -477,13 +499,18 @@ feature frame) ·
477
499
  directly after the block, so the overlap reads as a surface and not a cut.
478
500
  It sits at `:where()` weight, so a plain `box-shadow` of your own on that
479
501
  section replaces it.
480
- 30. **`viewport-fit=cover` belongs in the viewport meta.** The container
502
+ 30. **A stored theme needs a script in the `<head>`.** `main.js` runs at the
503
+ end of the body, so a reader whose choice differs from the OS sees the
504
+ other theme for one frame. Five inline lines in the head apply it before
505
+ the first paint - the snippet is in `references/javascript.md`. The theme
506
+ classes live on `<html>` for exactly this reason.
507
+ 31. **`viewport-fit=cover` belongs in the viewport meta.** The container
481
508
  gutter, the mobile nav panel and the modal padding all budget for the
482
509
  cutout with `env(safe-area-inset-*)`, and iOS resolves every one of those
483
510
  to `0` without it - on a notched phone the content then sits under the
484
511
  rounded corner. Nothing errors, which is why the shipped JS writes a
485
512
  console warning on localhost when the meta is missing.
486
- 31. **Half the layout reads its own width, not the viewport.** `mw-card`,
513
+ 32. **Half the layout reads its own width, not the viewport.** `mw-card`,
487
514
  `mw-tile`, `mw-stepper`, both timelines and - since 5.13.0 -
488
515
  `mw-gallery-container` and `mw-image-slider` are `@container` queries, so
489
516
  any of them in a half-width column takes its narrow layout there while the
@@ -1319,6 +1319,10 @@ edge drops and the key sits 1px lower.
1319
1319
  - The cursor is `default` because a figure is usually not clickable. On `<a>`
1320
1320
  and `<button>` it turns into a pointer automatically; on anything else use
1321
1321
  `mw-info-clickable`.
1322
+ - `mw-info-block` is the same surface carrying a paragraph: 68 characters
1323
+ wide, left-aligned, and it does not lift on hover. It sits at the left edge
1324
+ like everything else with running text in it - `mw-info-block-center` puts it
1325
+ back in the middle of its container.
1322
1326
  - `mw-info-mini` is the 20px status dot: sizes `-sm`, `-lg`, `-xl`, colours
1323
1327
  `-primary`, `-secondary`, `-success`, `-warning`, `-danger`, `-info`.
1324
1328
 
@@ -2,15 +2,28 @@
2
2
 
3
3
  ## What `maverick-wave.min.js` is
4
4
 
5
- One vanilla IIFE, no dependencies, ~24 kB. It queries the DOM **once** on
6
- `DOMContentLoaded` and attaches listeners. There is no re-init API, no
7
- `MutationObserver`, no exported module - it is built for a server-rendered or
8
- static page.
5
+ One vanilla IIFE, no dependencies, ~24 kB. It queries the DOM on
6
+ `DOMContentLoaded` and attaches listeners. No `MutationObserver`, no exported
7
+ module - it is built for a server-rendered or static page.
8
+
9
+ Markup that arrives later gets wired through the one public entry point:
10
+
11
+ ```js
12
+ window.MaverickWave.init(subtree); // or init() for the whole document
13
+ ```
14
+
15
+ Call it after an htmx swap, after filling a modal from a fetch, or after a view
16
+ transition replaced the body. Every element that already carries listeners is
17
+ skipped, so calling it twice over the same markup does nothing - the components
18
+ are safe to re-run, the document- and window-level behaviour is wired once and
19
+ never again.
20
+
21
+ What that does **not** make it is a SPA library - see below.
9
22
 
10
23
  ## Why a SPA must not load it
11
24
 
12
- - It runs once during bootstrap. Anything rendered afterwards - i.e. everything
13
- in a routed application - never gets initialised.
25
+ - Anything rendered after bootstrap needs `MaverickWave.init()` by hand, for
26
+ every subtree, on every render - in a routed application that is all of it.
14
27
  - It writes straight into the DOM (class toggles, generated elements, inline
15
28
  styles). In Angular that happens outside change detection; in a zoneless app
16
29
  the framework never learns about it, and on the next re-render your bindings
@@ -23,6 +36,29 @@ in a Vite entry point. Load the **CSS only** and rebuild the handful of
23
36
  behaviours in components. Each one is a few lines - the framework's state
24
37
  classes are the entire contract.
25
38
 
39
+ ## The theme has to be applied before the first paint
40
+
41
+ `main.js` sits at the end of the body, so a stored choice that differs from the
42
+ operating system reaches the page one frame too late - the reader sees the other
43
+ theme flash. The fix is five lines in the `<head>`, inline and synchronous,
44
+ before the stylesheet has painted anything:
45
+
46
+ ```html
47
+ <script>
48
+ (function () {
49
+ var stored = localStorage.getItem('mw-theme');
50
+ if (stored === 'light' || stored === 'dark') {
51
+ document.documentElement.classList.add('mw-theme-' + stored);
52
+ }
53
+ })();
54
+ </script>
55
+ ```
56
+
57
+ That is also why the toggle writes the class on `<html>` and not on `<body>`:
58
+ in the head there is no body yet. An external file would not help - it paints
59
+ first. With nothing stored, nothing happens here and the page follows the OS on
60
+ its own through `light-dark()`.
61
+
26
62
  ## Behaviour inventory
27
63
 
28
64
  | Behaviour | What the shipped JS does | What to do instead |
@@ -33,11 +69,11 @@ classes are the entire contract.
33
69
  | Mobile nav | Toggles `open` on `mw-menu-btn` and `mw-navbar` plus `mw-nav-open` on `<body>`, writes `aria-expanded` when the button is a `<button>`, closes on anchor click and on Escape (focus returns to the button) | One signal, bound to all three - the body class carries the scrim, the scroll lock and the pinning of the bar; reset it on navigation end |
34
70
  | Scroll spy | Sets `mw-active` on `mw-navbar-link` from the scroll position | Router-based: `routerLinkActive="mw-active"` |
35
71
  | Anchor scrolling | Intercepts `a[href^="#"]` and runs its own eased scroll - duration scales with distance, capped at 1.4s, cancelled by wheel or touch. Lands on `scroll-padding-top`, moves focus to the target, writes the hash with `replaceState`, and measures a sticky target unpinned | The router; for in-page anchors `scrollIntoView({ behavior: 'smooth' })` or your own animation |
36
- | Theme toggle | `localStorage['mw-theme']`, toggles `mw-theme-light` / `mw-theme-dark` on `<body>` and `mw-active` on the toggle, wrapped in `mw-theme-switching` on `<html>` so the flip starts no transitions. With nothing stored it takes `prefers-color-scheme` and keeps following it | A theme service - see `examples/angular-services.md` |
72
+ | Theme toggle | `localStorage['mw-theme']`, toggles `mw-theme-light` / `mw-theme-dark` on `<html>` and `mw-active` on the toggle, wrapped in `mw-theme-switching` on `<html>` so the flip starts no transitions. With nothing stored it takes `prefers-color-scheme` and keeps following it | A theme service - see `examples/angular-services.md` |
37
73
  | Progress bar | `IntersectionObserver` sets `width` from `data-value` | Bind `[style.width.%]="value()"` on `mw-progress-fill` |
38
74
  | Slider | On `input`, sets `--value` (track fill) and `data-value` (badge text) | Bind `[style.--value.%]` and `[attr.data-value]` |
39
75
  | Alerts | Close button adds `mw-alert-closing` (fade out), then `mw-alert-closed` (`display: none`) after `--mw-duration-base` | Remove the alert from the list/signal |
40
- | Checkbox lists | Adds `mw-selected` to the `li`, emits a `checkboxToggle` event, exposes `window.toggleCheckbox` | `[class.mw-selected]="item.checked"` |
76
+ | Checkbox lists | Adds `mw-selected` to the `li`, emits a `checkboxToggle` event, exposes `window.mwToggleCheckbox` | `[class.mw-selected]="item.checked"` |
41
77
  | Gallery | Generates the dots, moves the track, swipe handling (single finger, dominant axis, passive listeners), writes `mw-gallery-desc` | Render dots in the template, bind the track transform and `mw-active` on the current dot |
42
78
  | Image slider | Toggles `mw-active` on the overlay image and the control button with the matching `data-index` | Bind `mw-active` from the selected index |
43
79
  | Kanban board | Counts the tickets per lane, moves a card between lanes (`data-kanban-move`) and marks the arrival with `mw-kanban-card-moved-forward` / `-back`, clones `mw-kanban-card-template` on save, derives the next key from `data-kanban-prefix`, toggles `mw-active` on the composer | Keep the tickets in a signal/store and render the lanes from it; `mw-active` on the composer, `mw-kanban-editing` on the ticket it replaces. Neither the `<template>` nor the `mw-kanban-composer-*` hook classes are needed |
@@ -335,7 +335,12 @@ width.
335
335
 
336
336
  A `mw-container` that contains a `mw-hero` switches to full-bleed, full-height
337
337
  mode with the background image from `--mw-hero-background`.
338
- `mw-home-content-fade` fades the content in.
338
+ `mw-home-content-fade` fades the content in. `mw-home-start` puts the whole
339
+ block against the left container edge instead of centring it - for a hero that
340
+ points somewhere rather than sitting in the middle. `mw-home` carries the
341
+ container width itself, because the hero container runs the full width of the
342
+ screen; without that the hero content would start at a different edge than
343
+ every section below it.
339
344
 
340
345
  The image is treated per theme through `--mw-hero-image-filter` - a filter, not
341
346
  a tinted overlay: `$hero-filter-dark` (`brightness(0.7)`) against
@@ -489,6 +494,13 @@ row becomes as tall as the tallest instead of as tall as its own contents. For a
489
494
  column of cards that should end on one line even when one of them carries an
490
495
  extra sentence.
491
496
 
497
+ `mw-grid-from-sm|md|lg|xl` is the second modifier: it holds the grid at one
498
+ column until that breakpoint, and the grid's own ramp takes over from there.
499
+ Every ramp turns two-column at `sm`, and 576px is generous for running text
500
+ next to a picture - `mw-grid-2 mw-grid-from-md` is the pair that fixes that.
501
+ It also resets the `mw-reveal-stagger` cycle below the breakpoint, so two cards
502
+ standing above each other do not rise on different beats.
503
+
492
504
  Every one of them has a `-lg` twin with a wider gap (2.5rem instead of 1.35rem):
493
505
  `mw-grid-2-lg`, `mw-grid-3-lg`, `mw-grid-4-lg`, `mw-grid-5-lg`, `mw-grid-1-lg`,
494
506
  `mw-grid-auto-lg`, `mw-grid-flex-lg`, `mw-grid-lg`.
@@ -543,6 +555,16 @@ applies below that. The pair `mw-d-none mw-d-md-flex` is hidden on a phone and a
543
555
  row from a tablet on. `mw-hide-mobile` (gone below md) and `mw-hide-desktop`
544
556
  (gone from md up) spell out the two everyone actually reaches for.
545
557
 
558
+ **Print** - the framework ships a print stylesheet, so a page that is dark on
559
+ screen comes out of the printer as black on white: the surface, ink, border and
560
+ elevation tokens are redeclared for paper, the fixed chrome (header, ribbon,
561
+ section nav, scroll cue, burger, theme toggle, toasts) is dropped, cards keep a
562
+ hairline instead of a tone, headings do not end a page and cards, rows and
563
+ figures are not torn across one. A link in `mw-prose` or `mw-disclaimer` that
564
+ points at an http(s) address prints its target after the text. `mw-hide-print`
565
+ takes an element off the paper, `mw-print-only` is the note that exists only
566
+ there. Nothing to switch on.
567
+
546
568
  **Text overflow** - `mw-text-truncate` is one line ending in an ellipsis and
547
569
  carries `min-width: 0` with it, which is the reason truncation "does not work"
548
570
  nine times out of ten inside a flex row. `mw-text-clamp-2` through `-5` is that
@@ -396,6 +396,10 @@ Functions read the maps: `spacing('4')`, `font-size('lg')`,
396
396
  container widths, not breakpoint names, because a container has none.
397
397
  - `media-up($bp)` / `media-down($bp)` - the `$breakpoints` map as range syntax,
398
398
  `width >= bp` and `width < bp`, so the two never overlap at the breakpoint.
399
+ `media-up` is the direction the framework is written in: the base block is the
400
+ phone and each breakpoint adds to it. `media-down` is for a rule that sets what
401
+ the base never sets - turned around it would hard-wire an inherited value - and
402
+ for a narrow layout that costs more properties than the wide one.
399
403
  - `touch-context($bp: 'md')` - coarse pointer _or_ narrow viewport. Every
400
404
  target-size rule in the framework hangs under it; put your own control under
401
405
  the same condition instead of guessing it.
@@ -440,7 +444,7 @@ Give it a layer of its own:
440
444
 
441
445
  ## Importing only what you need
442
446
 
443
- The full stylesheet is ~281 kB raw / ~40 kB gzipped. Marketing components
447
+ The full stylesheet is ~290 kB raw / ~41 kB gzipped. Marketing components
444
448
  (`blog-post`, `gallery`, `content-slider`, `techstack-bucket`, `tiles`,
445
449
  `coming-soon`, `ratings`, `home`, `hero`) are dead weight in an application, and
446
450
  Angular bundle budgets notice.
@@ -452,6 +456,22 @@ colourless. The cascade layer order comes along on its own: every module loads
452
456
  `abstracts`, which declares it, so a hand-picked subset orders itself the same
453
457
  way the full build does.
454
458
 
459
+ **The short way is `main-lean`**: the whole framework minus every component -
460
+ tokens, reset, typography, the complete layout and all utilities, ~98 kB raw /
461
+ ~16 kB gzipped. Configure it like `main` and add the components next to it:
462
+
463
+ ```scss
464
+ @use 'maverick-wave/src/scss/main-lean' with (
465
+ $primary-color: #0f766e
466
+ );
467
+ @use 'maverick-wave/src/scss/components/buttons';
468
+ @use 'maverick-wave/src/scss/components/cards';
469
+ @use 'maverick-wave/src/scss/components/alerts';
470
+ ```
471
+
472
+ Those three land at ~19 kB gzipped against ~41 kB for the full build. Pick the
473
+ files by hand, as below, when you also want to leave parts of the layout out.
474
+
455
475
  ```scss
456
476
  // styles.scss - configure first, then pick
457
477
  @use 'maverick-wave/src/scss/abstracts/variables' with (
package/CHANGELOG.md CHANGED
@@ -6,6 +6,20 @@ Patch releases are only for test purposes - here I only document major and minor
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [5.25.0] - 2026-09-20
10
+
11
+ ### Added
12
+
13
+ - print stylesheet
14
+ - `main-lean` entry point
15
+ - `mw-home-start` for a hero at the left container edge
16
+ - `mw-grid-from-*` keeps a grid single column up to that breakpoint
17
+
18
+ ### Changed
19
+
20
+ - more "mobile first" design
21
+ - `mw-home` carries the container width
22
+
9
23
  ## [5.24.0] - 2026-09-20
10
24
 
11
25
  ### Changed
package/CLAUDE.md CHANGED
@@ -36,6 +36,10 @@ There are no test or lint scripts.
36
36
  - `utilities/` - Spacing, flex, display utility classes
37
37
  - `main.scss` - Entry point using `@use`/`@forward` (not `@import`)
38
38
 
39
+ **Mobile-first** - The unprefixed block is the phone layout; `media-up($bp)` adds
40
+ to it. `media-down($bp)` only for rules that set what the base never sets, or
41
+ where the narrow layout costs more properties than the wide one.
42
+
39
43
  **JavaScript** (`src/js/main.js`) - Single file. All components auto-initialize on `DOMContentLoaded`. Includes: gallery and image sliders, theme toggle (persisted to localStorage, transitions suppressed during the flip), accordion, mobile nav, progress bar (IntersectionObserver), scroll spy, tabs (which also get their ARIA and arrow-key handling here), alerts/toasts, modals, range inputs, dropdowns, language switcher, checkbox lists, kanban board, calendar.
40
44
 
41
45
  **HTML showcase** (`index.html` + `src/partials/`) - The top-level `index.html` uses `@@include()` syntax to pull in 63 partials from `src/partials/`. These compile into `dist/index.html`.
package/README.md CHANGED
@@ -45,12 +45,12 @@ The result is a framework that balances utility with simplicity, offering develo
45
45
  <title>My MaverickWave Project</title>
46
46
  <link
47
47
  rel="stylesheet"
48
- href="https://cdn.jsdelivr.net/npm/maverick-wave@5.24.0/maverick-wave.min.css"
48
+ href="https://cdn.jsdelivr.net/npm/maverick-wave@5.25.0/maverick-wave.min.css"
49
49
  />
50
50
  </head>
51
51
  <body>
52
52
  <!-- Your content here -->
53
- <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.24.0/maverick-wave.min.js"></script>
53
+ <script src="https://cdn.jsdelivr.net/npm/maverick-wave@5.25.0/maverick-wave.min.js"></script>
54
54
  </body>
55
55
  </html>
56
56
  ```
@@ -437,6 +437,8 @@ They exist on the SCSS path only - the compiled CSS carries none of them.
437
437
  ignores the reader's font size. The default range stops at `md`, a headline
438
438
  passes `$to: 'xl'`.
439
439
  - `media-up($bp)` / `media-down($bp)` - the `$breakpoints` map as a query.
440
+ `media-up` is the direction the framework is written in: the base block is the
441
+ phone, each breakpoint adds to it.
440
442
  - `touch-context($bp: 'md')` - coarse pointer _or_ narrow viewport, the condition
441
443
  every target-size rule in the framework hangs under.
442
444
  - `touch-floor($size: 2.75rem)` - that condition plus a `min-height`. Not for a
package/index.html CHANGED
@@ -25,6 +25,19 @@
25
25
  cached: the filename never changes, and without it a stale stylesheet
26
26
  survives a normal reload -->
27
27
  <link rel="stylesheet" href="maverick-wave.min.css?v={{VERSION}}" />
28
+
29
+ <!-- Applies a stored theme choice before the first paint. Without it a
30
+ reader whose choice differs from the OS sees the other theme for a
31
+ frame, because main.js only runs at the end of the body. Inline and
32
+ synchronous on purpose - an external file would paint first. -->
33
+ <script>
34
+ (function () {
35
+ var stored = localStorage.getItem('mw-theme');
36
+ if (stored === 'light' || stored === 'dark') {
37
+ document.documentElement.classList.add('mw-theme-' + stored);
38
+ }
39
+ })();
40
+ </script>
28
41
  <!-- Favicons -->
29
42
  <link rel="icon" type="image/svg+xml" href="favicon/favicon.svg" />
30
43
  <link