jekyll-theme-zer0 1.29.0 → 1.31.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 (163) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +886 -2
  3. data/_data/README.md +2 -0
  4. data/_data/ai.yml +5 -3
  5. data/_data/ai_pricing.yml +36 -0
  6. data/_data/backlog.yml +196 -22
  7. data/_data/consumers.yml +147 -5
  8. data/_data/features.yml +147 -21
  9. data/_data/feedback_types.yml +17 -12
  10. data/_data/landing.yml +5 -2
  11. data/_data/navigation/quickstart.yml +4 -0
  12. data/_data/site_builder.yml +874 -0
  13. data/_data/theme-manifest.yml +132 -122
  14. data/_data/ui-text.yml +19 -0
  15. data/_includes/README.md +11 -1
  16. data/_includes/analytics/posthog.html +30 -11
  17. data/_includes/components/README.md +1 -1
  18. data/_includes/components/admin-links.html +2 -2
  19. data/_includes/components/admin-tabs.html +2 -2
  20. data/_includes/components/ai-chat.html +14 -11
  21. data/_includes/components/analytics-dashboard.html +8 -8
  22. data/_includes/components/author-bio.html +1 -1
  23. data/_includes/components/author-card.html +10 -2
  24. data/_includes/components/author-eeat.html +4 -4
  25. data/_includes/components/background-customizer.html +8 -8
  26. data/_includes/components/background-image.html +120 -0
  27. data/_includes/components/background-settings.html +28 -15
  28. data/_includes/components/collection-manager.html +5 -5
  29. data/_includes/components/component-showcase.html +13 -13
  30. data/_includes/components/config-editor.html +12 -12
  31. data/_includes/components/config-viewer.html +8 -8
  32. data/_includes/components/cookie-consent.html +41 -41
  33. data/_includes/components/cta-button.html +7 -2
  34. data/_includes/components/dev-shortcuts.html +7 -7
  35. data/_includes/components/env-dashboard.html +8 -8
  36. data/_includes/components/env-switcher.html +9 -9
  37. data/_includes/components/feature-card.html +2 -2
  38. data/_includes/components/halfmoon.html +2 -2
  39. data/_includes/components/info-section.html +36 -36
  40. data/_includes/components/js-cdn.html +15 -15
  41. data/_includes/components/language-toggle.html +4 -4
  42. data/_includes/components/mermaid.html +79 -435
  43. data/_includes/components/nanobar.html +5 -5
  44. data/_includes/components/nav-editor.html +2 -2
  45. data/_includes/components/nav-export.html +2 -2
  46. data/_includes/components/nav-overview.html +2 -2
  47. data/_includes/components/page-feedback.html +45 -30
  48. data/_includes/components/page-views-init.html +1 -1
  49. data/_includes/components/post-card.html +22 -22
  50. data/_includes/components/post-type-badge.html +2 -2
  51. data/_includes/components/powered-by.html +2 -2
  52. data/_includes/components/preview-image.html +28 -3
  53. data/_includes/components/quick-index.html +2 -2
  54. data/_includes/components/search-modal.html +27 -2
  55. data/_includes/components/searchbar.html +2 -2
  56. data/_includes/components/svg-background.html +2 -2
  57. data/_includes/components/theme-customizer.html +2 -2
  58. data/_includes/components/theme-info.html +6 -6
  59. data/_includes/components/theme-preview-gallery.html +23 -23
  60. data/_includes/content/giscus.html +2 -2
  61. data/_includes/content/intro.html +15 -8
  62. data/_includes/content/jsonld-faq.html +2 -2
  63. data/_includes/content/jsonld-software.html +2 -2
  64. data/_includes/content/seo.html +12 -4
  65. data/_includes/content/sitemap.html +247 -290
  66. data/_includes/content/toc.html +183 -183
  67. data/_includes/core/branding.html +13 -20
  68. data/_includes/core/console-capture.html +32 -74
  69. data/_includes/core/favicon.html +49 -7
  70. data/_includes/core/footer-fabs.html +17 -3
  71. data/_includes/core/footer.html +31 -18
  72. data/_includes/core/head.html +128 -92
  73. data/_includes/core/header.html +85 -54
  74. data/_includes/core/tokens-inline.html +16 -1
  75. data/_includes/docs/bootstrap-docs.html +8 -8
  76. data/_includes/landing/landing-install-cards.html +2 -2
  77. data/_includes/landing/landing-quick-links.html +1 -1
  78. data/_includes/navigation/admin-nav.html +2 -2
  79. data/_includes/navigation/nav-tree.html +8 -8
  80. data/_includes/navigation/navbar.html +21 -18
  81. data/_includes/navigation/section-sidebar.html +16 -16
  82. data/_includes/navigation/sidebar-config.html +36 -2
  83. data/_includes/navigation/sidebar-left.html +15 -15
  84. data/_includes/navigation/sidebar-right.html +6 -6
  85. data/_includes/obsidian/full-graph.html +2 -2
  86. data/_includes/setup/claude-session.html +72 -0
  87. data/_includes/setup/prereq-checklist.html +90 -0
  88. data/_includes/setup/wizard.html +910 -291
  89. data/_includes/stats/stats-categories.html +8 -8
  90. data/_includes/stats/stats-header.html +14 -14
  91. data/_includes/stats/stats-metrics.html +12 -12
  92. data/_includes/stats/stats-no-data.html +12 -12
  93. data/_includes/stats/stats-overview.html +6 -6
  94. data/_includes/stats/stats-tags.html +8 -8
  95. data/_layouts/404.html +38 -24
  96. data/_layouts/admin.html +22 -22
  97. data/_layouts/article.html +54 -37
  98. data/_layouts/author.html +20 -20
  99. data/_layouts/authors.html +2 -2
  100. data/_layouts/book-abc.html +12 -12
  101. data/_layouts/book-story.html +15 -15
  102. data/_layouts/book.html +12 -12
  103. data/_layouts/collection.html +33 -33
  104. data/_layouts/cookbook.html +12 -12
  105. data/_layouts/default.html +27 -24
  106. data/_layouts/home.html +36 -24
  107. data/_layouts/index.html +10 -10
  108. data/_layouts/landing.html +17 -17
  109. data/_layouts/news.html +44 -44
  110. data/_layouts/note.html +40 -39
  111. data/_layouts/notebook.html +36 -35
  112. data/_layouts/recipe.html +24 -24
  113. data/_layouts/root.html +95 -55
  114. data/_layouts/section.html +23 -23
  115. data/_layouts/setup.html +3 -3
  116. data/_layouts/sitemap-collection.html +49 -49
  117. data/_layouts/stats.html +40 -40
  118. data/_layouts/tag.html +12 -12
  119. data/_layouts/welcome.html +21 -21
  120. data/_sass/components/_cookie-banner.scss +48 -0
  121. data/_sass/components/_mermaid.scss +375 -0
  122. data/_sass/components/_setup-wizard.scss +588 -41
  123. data/_sass/components/_ui-enhancements.scss +11 -0
  124. data/_sass/core/_navbar.scss +147 -100
  125. data/_sass/core/_sidebar-extras.scss +2 -14
  126. data/_sass/core/code-copy.scss +27 -8
  127. data/_sass/layouts/_navbar-extras.scss +14 -4
  128. data/assets/css/extension-points-demo.css +27 -0
  129. data/assets/css/main.scss +1 -0
  130. data/assets/data/wiki-index.json +19 -0
  131. data/assets/js/ai-chat.js +47 -5
  132. data/assets/js/auto-hide-nav.js +53 -13
  133. data/assets/js/code-copy.js +72 -1
  134. data/assets/js/extension-points-demo.js +48 -0
  135. data/assets/js/fleet-feedback-capture.js +124 -0
  136. data/assets/js/fleet-feedback.js +853 -0
  137. data/assets/js/mermaid-diagrams.js +1373 -0
  138. data/assets/js/modules/navigation/config.js +9 -6
  139. data/assets/js/modules/navigation/scroll-spy.js +315 -80
  140. data/assets/js/modules/theme/appearance.js +8 -2
  141. data/assets/js/obsidian-local-graph.js +27 -5
  142. data/assets/js/obsidian-wiki-links.js +8 -3
  143. data/assets/js/page-feedback.js +125 -192
  144. data/assets/js/search-modal.js +26 -0
  145. data/assets/js/setup-wizard.js +2119 -363
  146. data/assets/js/site-builder.js +1834 -0
  147. data/assets/js/ui-enhancements.js +11 -3
  148. data/scripts/README.md +15 -0
  149. data/scripts/ai/README.md +38 -0
  150. data/scripts/ai/api_call.rb +124 -0
  151. data/scripts/ai/usage.rb +314 -0
  152. data/scripts/ai/usage_report.rb +225 -0
  153. data/scripts/ci/test_visual_evidence_autogen.py +403 -0
  154. data/scripts/ci/visual_evidence_autogen.py +1070 -0
  155. data/scripts/content-review.rb +20 -1
  156. data/scripts/features/install-preview-generator +1 -1
  157. data/scripts/lib/README.md +12 -0
  158. data/scripts/lib/preview_generator.py +2 -2
  159. data/scripts/test/integration/mermaid +22 -8
  160. data/scripts/test/lib/run_tests.sh +1 -0
  161. data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
  162. data/scripts/translate.rb +23 -1
  163. metadata +21 -2
data/CHANGELOG.md CHANGED
@@ -12,6 +12,263 @@ hand-written prose covering the same releases in more depth; they sit below
12
12
  their version because release-please inserts each new release at the top of the
13
13
  file. Only `## [Unreleased]` describes work that has not shipped yet.
14
14
 
15
+ ## [1.31.0](https://github.com/bamr87/zer0-mistakes/compare/v1.30.0...v1.31.0) (2026-10-07)
16
+
17
+
18
+ ### Features
19
+
20
+ * **navigation:** two clean navbar label tiers, merged chevron, centred grid ([#495](https://github.com/bamr87/zer0-mistakes/issues/495)) ([9ebb29a](https://github.com/bamr87/zer0-mistakes/commit/9ebb29a4662fbc4c62d3fdd8df9c282bdd181204))
21
+ * **theme:** four consumer extension points for notes/knowledge-base builds ([#492](https://github.com/bamr87/zer0-mistakes/issues/492)) ([1e2b228](https://github.com/bamr87/zer0-mistakes/commit/1e2b2283303012b28b9e81209fb66d0344aa0597))
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * **article:** eager-load the hero LCP image and reserve its box ([#498](https://github.com/bamr87/zer0-mistakes/issues/498)) ([529b84b](https://github.com/bamr87/zer0-mistakes/commit/529b84bd0f5ea8e79b7711187b52b2bb80c5b134)), closes [#485](https://github.com/bamr87/zer0-mistakes/issues/485)
27
+ * **ci:** keep a LiveReload push from crashing the generic evidence generator ([#500](https://github.com/bamr87/zer0-mistakes/issues/500)) ([eb1b5ae](https://github.com/bamr87/zer0-mistakes/commit/eb1b5ae201ed0829e0c3fac05e629e55a6ea1040))
28
+ * **ci:** Playwright 1.58 attachment names, docs front matter, test race ([#497](https://github.com/bamr87/zer0-mistakes/issues/497)) ([f1bdc7e](https://github.com/bamr87/zer0-mistakes/commit/f1bdc7ef22091f0506617b11a95e357c439cfa94))
29
+ * **ci:** stop auto-merge guard failing on Dependabot Actions bumps ([#503](https://github.com/bamr87/zer0-mistakes/issues/503)) ([a998b2d](https://github.com/bamr87/zer0-mistakes/commit/a998b2dd9cf96bb1fd09ac3a95644908713afc37))
30
+ * **features:** escape `<en-url>` in the translation card so the Features page renders ([#505](https://github.com/bamr87/zer0-mistakes/issues/505)) ([8e4345f](https://github.com/bamr87/zer0-mistakes/commit/8e4345f28dfc675b5efe95ba66ec72af613cbcf5))
31
+ * **includes:** join preview paths without a leading slash under assets_prefix ([#509](https://github.com/bamr87/zer0-mistakes/issues/509)) ([406250c](https://github.com/bamr87/zer0-mistakes/commit/406250c2b5b94469cae9a0b6f93e8782cb8509f9))
32
+ * **includes:** stop the draft chip shifting the Site Builder on first save ([#489](https://github.com/bamr87/zer0-mistakes/issues/489)) ([d472aa2](https://github.com/bamr87/zer0-mistakes/commit/d472aa267e164212cf8b844e701cfcdad08e4d8d))
33
+ * **layouts:** one main landmark per page on post, notebook and note ([#487](https://github.com/bamr87/zer0-mistakes/issues/487)) ([78b0910](https://github.com/bamr87/zer0-mistakes/commit/78b0910fb2258591eab9ba13efb5ac037eee80c5))
34
+ * **navigation:** stop Liquid whitespace-trim gluing navbar attributes ([#477](https://github.com/bamr87/zer0-mistakes/issues/477)) ([4349ea1](https://github.com/bamr87/zer0-mistakes/commit/4349ea17d690e5e6b8585f1d3a33b3ea4d3bf4fe))
35
+ * **scripts:** default xAI preview renderer to grok-imagine-image-2.0 ([#499](https://github.com/bamr87/zer0-mistakes/issues/499)) ([95cebdb](https://github.com/bamr87/zer0-mistakes/commit/95cebdb535799544f05ed5929e528a074448ef63)), closes [#474](https://github.com/bamr87/zer0-mistakes/issues/474)
36
+ * **tests:** make the Liquid nested-tag and gem-content checks able to fail ([#478](https://github.com/bamr87/zer0-mistakes/issues/478)) ([8510d08](https://github.com/bamr87/zer0-mistakes/commit/8510d0824f79823f66f765f12db9be9b67fff3ae))
37
+ * **tokens:** let the active skin win over site theme_color for skin-owned tokens ([#502](https://github.com/bamr87/zer0-mistakes/issues/502)) ([af468e0](https://github.com/bamr87/zer0-mistakes/commit/af468e072543a448e7be80230f3d3a246b5e758e))
38
+ * **ui:** make the cookie consent banner a compact bottom bar ([#510](https://github.com/bamr87/zer0-mistakes/issues/510)) ([2d7d0db](https://github.com/bamr87/zer0-mistakes/commit/2d7d0db7f9f6b1433b63d8be76103cce2fd7fb05))
39
+
40
+
41
+ ### Performance Improvements
42
+
43
+ * **js:** lazy-load Mermaid and cytoscape, defer PostHog, stop nav reflows ([#508](https://github.com/bamr87/zer0-mistakes/issues/508)) ([702789c](https://github.com/bamr87/zer0-mistakes/commit/702789cecf93d8e9b9f7013168ca47f0294064eb))
44
+ * **sitemap:** keep one page of the index in the DOM and build cards lazily ([#511](https://github.com/bamr87/zer0-mistakes/issues/511)) ([9e42fcb](https://github.com/bamr87/zer0-mistakes/commit/9e42fcbd9421a994fd22a347bbef95573ce340a9))
45
+
46
+ ## [Unreleased]
47
+
48
+ ### Added
49
+
50
+ - **Four consumer extension points** ([#412](https://github.com/bamr87/zer0-mistakes/issues/412)), documented at [`/docs/customization/extension-points/`](pages/_docs/customization/extension-points.md) and [`docs/development/extension-points.md`](docs/development/extension-points.md):
51
+ - `zer0:code-block-ready` — a bubbling `CustomEvent` dispatched once per code block carrying `{ wrapper, header, pre, code, lang }`, plus `window.zer0OnCodeBlock(fn)`, which **replays** already-decorated blocks before subscribing, and `window.__zer0CodeBlocks`. This replaces the `MutationObserver` and wrapper-shape guessing a consumer previously needed — the guess is what silently rendered two buttons on every block downstream.
52
+ - `styles:` / `scripts:` frontmatter — per-page CSS and JS resolved through `relative_url`, with `scripts:` deferred after the theme bundle. A page that declares neither emits nothing at all.
53
+ - `lastmod` and `description` on every `/assets/data/wiki-index.json` entry — ISO-8601 and the authored subtitle, always defined (`null` when the document has neither), no new traversal.
54
+
55
+ ### Changed
56
+
57
+ - **Navbar label tiers reduced from three to two** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)).
58
+ Top-level nav items now render as icon + full label above 51rem of centre
59
+ track and icon-only below it; `text-overflow: ellipsis` is gone, so a label
60
+ is never cut to "Quicksta…". Icons now survive in **both** tiers — the old
61
+ 41–50.99rem tier dropped them to buy width for bare labels. **Behaviour
62
+ change:** centre-track widths between 41rem and 51rem (roughly 992–1200px
63
+ viewports) previously showed bare labels and now show icons, with the
64
+ existing `.nav-tooltip` carrying the name on hover.
65
+ - **Dropdown chevrons merged into their parent row** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)).
66
+ The split toggle is now absolutely positioned inside padding the parent link
67
+ reserves, and hover applies to the whole row, so there is no dead zone
68
+ between the label and its chevron. It remains a real `<button>` with
69
+ `aria-expanded`, `aria-haspopup` and its visually-hidden label unchanged.
70
+ (evidence: [`test/visual/evidence/navbar-tiers-405/`](test/visual/evidence/navbar-tiers-405/README.md) — chevron dead zone up to 4px → overlapping; 0 truncated labels across 6 widths)
71
+ - **The navbar carries logo + title only** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)).
72
+ `site.subtitle` moved out of `_includes/core/branding.html` and onto the home
73
+ hero (`_layouts/home.html`), returning its width to the menubar. It renders
74
+ under the page heading on pages that use the `home` layout, and is hidden
75
+ with it by `hide_title` or `hide_intro`. A homepage on another layout, such
76
+ as `landing`, no longer shows the subtitle at all; add `site.subtitle` to
77
+ that layout's hero to keep it.
78
+ - **The below-`lg` menu toggle is labelled "Menu"** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)),
79
+ so it is distinguishable from the sidebar hamburger beside it. Override the
80
+ string with `ui.nav_menu_toggle_label`.
81
+ - The desktop navbar grid gives its two side tracks a shared minimum
82
+ (`--zer0-navbar-side-min`, default `9rem`) so the menubar is optically
83
+ centred in the bar rather than only within its own track.
84
+
85
+ ### Fixed
86
+
87
+ - **The cookie banner no longer covers the first screen.** Below 768px `components/cookie-consent.html` stacked a heading, a long sentence and three full-width buttons (the shim in `_sass/core/_sidebar-extras.scss`): 324px of an 850px-tall 400px viewport on it-journey.dev and lifehacker.dev, hiding most of the hero or article; on desktop it was 94px and sat over the lower hero copy. It is now a compact bar: a two-line summary over one row of three equal buttons on phones, and a single row from 768px up. The layout lives in `_sass/components/_cookie-banner.scss` and the stacking shim is gone. The banner is a labelled `region` ("Cookie consent"), and the button ids, the modal and the consent logic are unchanged. Height at 400px drops from 324px to 107px, and at 1366px from 94px to 48px. Pinned by `test/visual/features/cookie-banner-compact.spec.js`.
88
+ - **The active skin now moves the whole `--zer0-color-*` layer, not just Bootstrap's** ([#459](https://github.com/bamr87/zer0-mistakes/issues/459)). `_includes/core/tokens-inline.html` re-set `--zer0-color-primary`, `-link` and `-accent` from `theme_color` on `:root` after `main.css`. `:root` and `[data-theme-skin]` have the same specificity, so the config value won on source order. On any site that sets `theme_color`, components reading the theme layer therefore kept the config color in every skin, and `--zer0-color-primary` disagreed with its own `-rgb` companion. The skin now wins: those three keys are emitted only when no palette skin is active, as with `dark` and `contrast`. `theme_color` still sets `secondary`, `danger`, `warning`, `success` and `info`, and the Appearance-panel override still beats both. The rule is documented in [`docs/ui/design-tokens.md`](docs/ui/design-tokens.md#precedence-skin-vs-theme_color) and checked by `test/visual/features/appearance.spec.js` (evidence: [`test/visual/evidence/agent-issue-459/`](test/visual/evidence/agent-issue-459/README.md) — page overflow 0px at six widths; the nine skin baselines are unchanged).
89
+ - **A preview path without a leading slash no longer 404s.** `preview: images/previews/x.webp` was appended straight onto `assets_prefix`, so `components/preview-image.html`, `components/background-image.html`, `content/intro.html` and `content/seo.html` emitted `/assetsimages/previews/x.webp` for the card image, the hero background and `og:image` (165 it-journey content files use that form). All four now prepend `/` to a site-relative path before joining; URLs and `data:` URIs are left alone, and already-slashed or `/assets/`-prefixed paths render byte-identically. `preview-image.html` also gained optional `width`/`height` parameters (emitted only when passed); the article layout's related-post cards pass 1536×1024 so their box is reserved before the lazy image arrives (0px → 217px at 1366px). Pinned by the `Preview Path Join` unit test in `test/test_core.sh` and `test/visual/features/preview-path-join.spec.js`.
90
+ - **The sitemap index no longer puts every page in the DOM twice.** `content/sitemap.html` (the `/sitemap/` page, the search modal's "View all results" target, and it-journey.dev's `/search/`) kept all rows attached. Its script also eagerly built a second copy of every row as a card for the hidden card view: about 19,900 elements on it-journey's `/search/` (431 entries), with Lighthouse mobile TBT at 9.7 s. Rows are still rendered server-side, so the page works without JS and with `?q=`. The script now keeps only one page of 50 matches in the document and appends the next page with **Show more**. Search, filters and sorting work across every row, and cards are built on the first switch to card view, one page at a time. Card text is set through `textContent` rather than interpolated HTML. On the theme's own `/sitemap/` (386 entries), elements after load drop from 23,063 to 8,086, and local Lighthouse mobile TBT drops from 1,662 ms to 181 ms. Badge contrast is fixed too: the collection pill is `text-bg-info` (was white on `#0dcaf0`, 1.96:1), tag chips use `--bs-primary-text-emphasis`, and `+N` overflow chips (white on transparent) get body text and a border. Pinned by `test/visual/features/sitemap-pagination.spec.js`.
91
+ - **The xAI preview-image renderer defaults to `grok-imagine-image-2.0`** ([#474](https://github.com/bamr87/zer0-mistakes/issues/474)). `XAIProvider.default_model()` in `scripts/lib/preview_generator.py` still returned the retired `grok-2-image`, so a site with `preview_images.provider: xai` and no explicit `model:` sent a retired model upstream. An explicitly configured `grok-2-image` is still accepted as an xAI-family model.
92
+ - **A code-copy partial no longer restyles every button on the site** ([#412](https://github.com/bamr87/zer0-mistakes/issues/412)). `_sass/core/code-copy.scss` shipped a bare `.button, button:not(.copy)` rule — an element selector at specificity (0,1,1) that out-ranked `.btn` and any consumer's own class, applying `padding: 0 20px`, `font-size: 11px` and a hardcoded `#bbb` border site-wide. It is scoped to `.code-block-header` / `pre.highlight` now and uses `--zer0-*` tokens, so `#bbb` is gone from the compiled stylesheet. Every button on the site — the theme's own chrome included — gets its intended metrics back (evidence: [`test/visual/evidence/agent-issue-412/`](test/visual/evidence/agent-issue-412/README.md) — page overflow 0px at six widths; the nine skin baselines moved, and every red region in their diffs is a `<button>`).
93
+ - **The article hero no longer lazy-loads its LCP image or shoves the article down when it arrives** ([#485](https://github.com/bamr87/zer0-mistakes/issues/485)). `figure.featured-hero` inherited `components/preview-image.html`'s `loading="lazy"` default and had no reserved box. It now passes `loading="eager" fetchpriority="high" decoding="async"`, and its box is pinned by `.featured-hero img { aspect-ratio: 3 / 2 }`. The box is set in CSS rather than with `width`/`height` attributes, because preview assets vary in shape and `object-fit: cover` crops a portrait instead of stretching it. `preview-image.html` gained optional `fetchpriority`/`decoding` parameters that are emitted only when passed, so every other consumer renders byte-identically (evidence: [`test/visual/evidence/hero-lcp/`](test/visual/evidence/hero-lcp/README.md) — hero loading lazy → eager + fetchpriority=high; content shift when the hero lands 476px → 0px across 2 widths × landscape/portrait).
94
+ - **Mermaid, cytoscape and PostHog no longer load up front, and the auto-hide navbar stops forcing reflows.** `assets/js/mermaid-diagrams.js` now converts fences into figures straight away but injects the ~1 MB Mermaid bundle only when a diagram exists and the first one comes within 400px of the viewport (IntersectionObserver); `components/mermaid.html` passes the bundle URL as `window.zer0MermaidSrc` (a script global, not DOM text, and only http(s) URLs are injected) instead of a `<script defer>`. The local-graph panel loads cytoscape on first open instead of on every indexed page. `analytics/posthog.html` keeps the queueing stub but fetches `array.js` after `load` + idle, and `core/head.html` no longer includes it a second time (`root.html` already did, so every production page called `posthog.init()` twice). `auto-hide-nav.js` measures the navbar with a ResizeObserver (rAF read/next-frame write fallback) and caches the height instead of reading `offsetHeight` at DOMContentLoaded and on resize. Lighthouse mobile, local static build: home TBT 1,705 → 651 ms and weight 5,581 → 2,327 KiB; `/docs/features/mermaid-diagrams/` TBT 2,017 → 243 ms; `auto-hide-nav.js` gone from forced-reflow. New lazy-load test in `test/visual/features/mermaid.spec.js`.
95
+ ### Fixed
96
+
97
+ - **The Site Builder no longer jumps the first time it saves a draft.**
98
+ `showDraftChip()` cleared the "Draft saved" chip's `hidden` attribute, and the
99
+ 2s timer that follows only removes `is-visible` — an opacity class — so the
100
+ first debounced save moved the chip from `display: none` into layout *for
101
+ good*. Its box is 26px against the 19px "Start over" button beside it, so
102
+ `.wizard-header` grew and took `#wizardTabContent` and every Back/Next row
103
+ below it down with it: 7px where the header's right-hand block had already
104
+ wrapped, a whole wrapped line where those 7px were what tipped it over. The
105
+ chip now holds its box from first paint and is hidden with `visibility`,
106
+ which is what `_setup-wizard.scss` said it did all along ("Kept in the layout
107
+ (no display toggling) so its appearance never shifts the header") and still
108
+ keeps it out of the accessibility tree when it has nothing to say. This is
109
+ what failed the `@critical` stable-height spec on every push to `main`
110
+ (`2029, 2036, 2036, …`), and with it the retry that spec's failure forced
111
+ ([bamr87/bamr87#265](https://github.com/bamr87/bamr87/issues/265)) (evidence:
112
+ [`test/visual/evidence/agent-issue-265/`](test/visual/evidence/agent-issue-265/README.md)
113
+ — first draft save moved the wizard 42px on the base branch, 0px here).
114
+
115
+ ### Changed
116
+
117
+ - The stable-height spec now walks Connect **first and last** and records
118
+ `#wizardTabContent`'s document-y **top** beside each Back/Next offset. The
119
+ contract itself (`spread ≤ 1`) is unchanged; what is new is that a shift
120
+ *above* the panes now fails as a shift above the panes instead of looking
121
+ like one step with bad CSS. The companion `heights` assertion keeps a comment
122
+ saying it is step-invariant by construction, so it is never again read as
123
+ evidence about anything above the container.
124
+ - **Exactly one `main` landmark again on post, notebook and note pages.**
125
+ `_layouts/{article,notebook,note}.html` each rendered
126
+ `<article id="main" role="main">` *inside* root.html's
127
+ `<main id="main-content">`, so every such page exposed two `main` landmarks —
128
+ ambiguous "skip to main content" and landmark-rotor navigation in
129
+ NVDA/JAWS/VoiceOver (WCAG 2.1 SC 1.3.1) — and the `role` additionally
130
+ overrode the element's native `article` landmark, so the post was no longer
131
+ announced as an article. This is the same class of defect as #299, arriving on
132
+ a `role` attribute instead of a nested `<main>` element. The generic
133
+ `id="main"` is gone with it: nothing in the theme referenced it, and the skip
134
+ link targets `#main-content`. Microdata (`h-entry`, `itemscope`,
135
+ `BlogPosting`/`TechArticle`/`Article`) is unchanged, and the rendered pixels
136
+ are identical. Pinned by `test/visual/core/landmarks.spec.js`
137
+ ([#484](https://github.com/bamr87/zer0-mistakes/issues/484)) (evidence:
138
+ [`test/visual/evidence/agent-issue-484/`](test/visual/evidence/agent-issue-484/README.md)
139
+ — base vs head on a post and a note page, page overflow 0px → 0px across 6
140
+ widths, 9 pixel baselines unchanged).
141
+
142
+ ## [1.30.0](https://github.com/bamr87/zer0-mistakes/compare/v1.29.0...v1.30.0) (2026-09-12)
143
+
144
+
145
+ ### Features
146
+
147
+ * **ci:** produce visual evidence and baselines on PRs, not just verify them ([#462](https://github.com/bamr87/zer0-mistakes/issues/462)) ([a377fae](https://github.com/bamr87/zer0-mistakes/commit/a377fae6ea31a0bb015479dea6d1a7c41e141c3f))
148
+ * **includes:** add background-image.html for CSS-painted cover art ([#451](https://github.com/bamr87/zer0-mistakes/issues/451)) ([9b15a67](https://github.com/bamr87/zer0-mistakes/commit/9b15a673f57b8f7f1f9dae9262191742d99d8080))
149
+ * **includes:** Claude or Grok sessions in the Site Builder ([#473](https://github.com/bamr87/zer0-mistakes/issues/473)) ([e1c5753](https://github.com/bamr87/zer0-mistakes/commit/e1c57531fc23be8d071757cd1941e1ae15285da7))
150
+ * **includes:** file the fleet issue contract from the feedback widget ([#472](https://github.com/bamr87/zer0-mistakes/issues/472)) ([24d4e2c](https://github.com/bamr87/zer0-mistakes/commit/24d4e2c6380541a894e9e671f0f0b0b859a29b05))
151
+ * **includes:** make theme-color track the page surface, not the accent ([#447](https://github.com/bamr87/zer0-mistakes/issues/447)) ([f1fda85](https://github.com/bamr87/zer0-mistakes/commit/f1fda857bb0cf5bd64bebfc1215ba0a906f05e5c))
152
+ * **includes:** themed Mermaid figures + toolbar ([#455](https://github.com/bamr87/zer0-mistakes/issues/455)) ([96776fa](https://github.com/bamr87/zer0-mistakes/commit/96776fa48bbe7b63058d1d246962019c745d7b93))
153
+ * **scripts:** adopt the shared ai-runner kit for claude-run; consumers registry fixes; hub baseline ([#475](https://github.com/bamr87/zer0-mistakes/issues/475)) ([feb4133](https://github.com/bamr87/zer0-mistakes/commit/feb41332eb15d2c13fe0c0f6d15b61007e14daa3))
154
+
155
+
156
+ ### Bug Fixes
157
+
158
+ * **a11y:** decouple heading size from heading level in theme chrome ([#450](https://github.com/bamr87/zer0-mistakes/issues/450)) ([bb92eb5](https://github.com/bamr87/zer0-mistakes/commit/bb92eb583c955e76339a9c7ab98532b35e226b36))
159
+ * **a11y:** give footer icon links an accessible name, announce search results ([#441](https://github.com/bamr87/zer0-mistakes/issues/441)) ([a1811e8](https://github.com/bamr87/zer0-mistakes/commit/a1811e81ac0c17b0836994be7c48eba51252bae7))
160
+ * **a11y:** stop emitting sidebar/TOC controls on layouts that render no offcanvas ([#443](https://github.com/bamr87/zer0-mistakes/issues/443)) ([619322d](https://github.com/bamr87/zer0-mistakes/commit/619322d8fd7c6e2a700ba154e065bf65caf67461))
161
+ * **ci:** run the autogen lane's tooling from the base branch, not the PR ([#464](https://github.com/bamr87/zer0-mistakes/issues/464)) ([8473050](https://github.com/bamr87/zer0-mistakes/commit/8473050be2b4ddfd994c7bea8a8c35e37a66b7f8))
162
+ * **ci:** stop the UI audit reporting a measurement blackout as a clean run ([#471](https://github.com/bamr87/zer0-mistakes/issues/471)) ([ab92619](https://github.com/bamr87/zer0-mistakes/commit/ab9261946e088417dabe4291590da1c7a964d4fa))
163
+ * **content:** raw-protect Liquid that is written as documentation ([#446](https://github.com/bamr87/zer0-mistakes/issues/446)) ([6123c1f](https://github.com/bamr87/zer0-mistakes/commit/6123c1f1e6ffd167376d4ef421b0a9f78af3985e))
164
+ * **includes:** declare charset inside the 1024-byte window, make the feed discoverable ([#440](https://github.com/bamr87/zer0-mistakes/issues/440)) ([4025205](https://github.com/bamr87/zer0-mistakes/commit/40252050ec21c70aa1d84820fdcde6d564e03478)), closes [#372](https://github.com/bamr87/zer0-mistakes/issues/372) [#371](https://github.com/bamr87/zer0-mistakes/issues/371)
165
+ * **includes:** expose skin-button selected state via aria-pressed ([#469](https://github.com/bamr87/zer0-mistakes/issues/469)) ([6579b16](https://github.com/bamr87/zer0-mistakes/commit/6579b1621a5eff147e224977633728aadc960505)), closes [#467](https://github.com/bamr87/zer0-mistakes/issues/467)
166
+ * **install:** stop seeding the theme's own navigation into consumers ([#449](https://github.com/bamr87/zer0-mistakes/issues/449)) ([232857c](https://github.com/bamr87/zer0-mistakes/commit/232857c66d7c3abccb166b5151a7f80a74dd17ca))
167
+ * **navigation:** scroll spy bolds the section actually being read ([#456](https://github.com/bamr87/zer0-mistakes/issues/456)) ([5ff38bf](https://github.com/bamr87/zer0-mistakes/commit/5ff38bf98faf96cdc3bf63ea3f0e7b10e21f3aab))
168
+ * **sass:** stop navbar controls flickering under a stationary pointer ([#453](https://github.com/bamr87/zer0-mistakes/issues/453)) ([fc84b9a](https://github.com/bamr87/zer0-mistakes/commit/fc84b9a4c714f8e04b8a0f940bed9e3795c144bc)), closes [#404](https://github.com/bamr87/zer0-mistakes/issues/404)
169
+ * **scripts:** restore nested translation placeholders instead of leaking them ([#452](https://github.com/bamr87/zer0-mistakes/issues/452)) ([00cab73](https://github.com/bamr87/zer0-mistakes/commit/00cab73d915f4e116040ef6ca4c0381669ae4eb2))
170
+
171
+
172
+ ### Performance Improvements
173
+
174
+ * **ci:** run the critical PR gate on 3 workers instead of 1 ([#481](https://github.com/bamr87/zer0-mistakes/issues/481)) ([b3fcbc7](https://github.com/bamr87/zer0-mistakes/commit/b3fcbc76a1af2a3a5e43eb28fbff7b3057751de6))
175
+ * **includes:** strip developer doc banners from delivered HTML ([#445](https://github.com/bamr87/zer0-mistakes/issues/445)) ([89b10f1](https://github.com/bamr87/zer0-mistakes/commit/89b10f1ff361c919fd631d0e7e664ed6d7c28a14))
176
+
177
+ ## [Unreleased]
178
+
179
+ ### Changed
180
+
181
+ - **`claude-run` is now the fleet's shared `ai-runner` kit.** The composite
182
+ action and its new `scripts/ai/run.sh` are byte-identical copies of
183
+ lifehacker.dev's (the kit source of truth) instead of a hand-rolled variant.
184
+ What changes for the two callers (`issue-autopilot.yml`,
185
+ `visual-evidence-autogen.yml`): an AI call that was attempted and rejected —
186
+ revoked credential, exhausted quota, `is_error` payload, CLI install failure
187
+ — now fails the step with the reason as a `::error::` annotation, where the
188
+ old action exited 0 and a dead run read green; the OAuth-first rule is
189
+ enforced with `env -u ANTHROPIC_API_KEY`; the model override is the canonical
190
+ `AI_MODEL` (or the new `model` input) rather than `ZER0_AI_MODEL`, and
191
+ `max-turns` is a new input. Metering (`scripts/ai/usage.rb`,
192
+ `usage_report.rb`, prices in `_data/ai_pricing.yml`) and the Claude API
193
+ fallback (`scripts/ai/api_call.rb`) ride along as optional companions. The
194
+ exit-code contract is pinned by `scripts/ci/test_ai_runner.sh`, wired into
195
+ `./scripts/bin/test`. The action's six existing inputs are unchanged.
196
+ - **`claude-run` is consumed by reference from the hub.** `issue-autopilot.yml` and `visual-evidence-autogen.yml` now call `bamr87/bamr87/.github/actions/claude-run@main` (identical inputs) instead of a vendored `./.github/actions/claude-run`; the local action, `scripts/ai/run.sh`, and the vendored contract test (`scripts/ci/test_ai_runner.sh` + its `scripts/test/lib/` bridge) are deleted, while the consumer companions the hub runner probes for — `scripts/ai/usage.rb`, `usage_report.rb`, `api_call.rb`, `_data/ai.yml`, `_data/ai_pricing.yml`, `tools/unwrap-prose.py` — stay ([bamr87/bamr87#254](https://github.com/bamr87/bamr87/pull/254)).
197
+ - **Consumer registry corrections.** `ai-world-view/ai-world-view.github.io`
198
+ is `remote_theme_floating` (both its `_config.yml` and `hub.yml` pins are
199
+ untagged), not `remote_theme_pinned`; `amr-bash/bash-365.com` is registered
200
+ as a consumer (floating `remote_theme`, unconstrained gem on Azure, path gem
201
+ for dev).
202
+
203
+ ### Fixed
204
+
205
+ - **Two `test_core.sh` checks could never fail, and one shouted on every clean
206
+ run (#460).** The Liquid nested-tag check grepped for
207
+ {% raw %}`{{.*{{`{% endraw %}, which matches
208
+ any two *sibling* output tags on one line — it flagged 64 of the theme's
209
+ includes, starting with `navigation/sidebar-pagetree.html`'s valid
210
+ {% raw %}`{{ _base }}{{ _section }}`{% endraw %} — and its `return 1` sat inside a `find | while`
211
+ pipeline, i.e. a subshell, so the function printed `[ERROR]` and then reported
212
+ the check as passed. The gem-content check ran `tar -tzf` on the built `.gem`,
213
+ but a gem is an *uncompressed* tar wrapping `metadata.gz`, `data.tar.gz` and
214
+ `checksums.yaml.gz`, so gzip printed `stdin: not in gzip format` four times
215
+ and both branches fell through to "Gem may not contain…" without ever
216
+ inspecting the payload — a gem shipped with no `_layouts/` would have passed.
217
+ The nested-tag pattern is now {% raw %}`{{[^}]*{{`{% endraw %} (cannot cross the first `}`, so
218
+ siblings do not match), both loops read from a process substitution so a
219
+ failure propagates, and the gem check extracts `data.tar.gz` and asserts
220
+ `_layouts/` and `assets/` are really in it. The layout balance check, now
221
+ live, blanks Liquid comment bodies before counting, so prose in a comment that
222
+ quotes a closing delimiter (as `_layouts/root.html`'s per-page-scripts note
223
+ does) is no longer read as an unbalanced tag. New `test/test_core_checks.sh`
224
+ drives both checks against known-good and known-bad fixtures, so a check that
225
+ stops being able to fail is itself a test failure. `./test/test_core.sh` now
226
+ runs clean: 26/26, no `[ERROR]` line, no gzip/tar noise.
227
+ - **Every navbar link emitted invalid HTML — attributes glued together with no
228
+ separating whitespace (#465)** — the Liquid whitespace-trim markers around the
229
+ conditional `aria-current` in `_includes/navigation/navbar.html` ate the
230
+ newlines that separated the surrounding attributes, so the primary nav
231
+ rendered `aria-label="News"aria-current="page"title="News"` (the WHATWG
232
+ `missing-whitespace-between-attributes` parse error). The leading {% raw %}`{%-`{% endraw %}
233
+ stripped the newline after `aria-label` and the trailing {% raw %}`-%}`{% endraw %} the whitespace
234
+ before `title`, which means **two of the four sites were broken on every page,
235
+ not just the current one**: where the conditional sits between two
236
+ unconditional attributes both markers fire even when the `if` emits nothing.
237
+ Parsers recover, so nothing looked broken — but the attributes affected are
238
+ exactly `aria-label`, `aria-current` and `title`, and a stricter parser is
239
+ entitled to drop the "you are here" announcement for screen-reader users. All
240
+ four sites now use the non-trimming {% raw %}`{% if %}`{% endraw %} form already present at line 26
241
+ of the same file, with the separator outside the tag. Two guards in
242
+ `test/test_core.sh`: `test_navbar_attribute_whitespace` renders the real
243
+ include through Liquid across both `aria-current` branches and both nav modes
244
+ (6 of 8 rendered variants were invalid before the fix), and
245
+ `test_attribute_whitespace_in_markup` models the trim rules over every include
246
+ and layout with no gem dependency — it found a second live instance, a literal
247
+ missing space in `components/theme-preview-gallery.html`, fixed here too.
248
+ `test_jekyll_build` re-checks the delivered `_site` bytes. No visual change.
249
+ - **The weekly UI/UX audit was blind, and reported it as clean.** `sweep.mjs`
250
+ built its pages with `browser.newPage()`, which `@axe-core/playwright`
251
+ refuses; the throw was caught by a single per-route `try` that also discarded
252
+ the overflow, console-error and link-collection data already gathered for
253
+ that route. Screenshots still succeeded, so the harness looked alive while
254
+ every accessibility, console, overflow and broken-link result on every route
255
+ was silently dropped — and the report rendered the absence as "0 axe
256
+ violations / 0 broken links". Pages are now built from `browser.newContext()`,
257
+ each measurement fails independently, a scan that errored is reported as
258
+ UNKNOWN rather than clean, and a measurement that fails on every route turns
259
+ the sweep red the way a total capture failure already did. On the demo site
260
+ the sweep goes from 18/21 captured with 21 blackout entries to **21/21
261
+ captured, 0 harness errors, 191 links crawled and 8 genuinely broken links
262
+ found** ([#468](https://github.com/bamr87/zer0-mistakes/issues/468)).
263
+ - **Theme-skin buttons now announce which skin is applied.** The Theme Skin
264
+ group in Settings → Appearance conveyed selection only through the Bootstrap
265
+ `.active` class, which carries no accessibility semantics, so screen readers
266
+ announced nine identical unlabelled buttons — WCAG 2.1 SC 4.1.2 (Name, Role,
267
+ Value). Each button now ships `aria-pressed`, and the click and "Reset
268
+ background" handlers keep it in sync with `.active` through one shared
269
+ helper, matching the sibling color-mode group
270
+ ([#467](https://github.com/bamr87/zer0-mistakes/issues/467)).
271
+
15
272
  ## [1.29.0](https://github.com/bamr87/zer0-mistakes/compare/v1.28.0...v1.29.0) (2026-09-01)
16
273
 
17
274
 
@@ -48,7 +305,556 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
48
305
 
49
306
  ## [Unreleased]
50
307
 
51
- ### Changed
308
+ ### Added
309
+
310
+ - **Bring your own AI provider — Claude or Grok — and an open, coding-session-style
311
+ Site Builder (ZER0-087)** — the chat proxy gains a provider layer
312
+ (`templates/deploy/chat-proxy/providers.js`): xAI's Grok is a first-class
313
+ provider (`XAI_API_KEY`, `CHAT_PROVIDER`, `XAI_CHAT_MODEL`) for the chat
314
+ widget, the feedback triage and the Site Builder, translated server-side to
315
+ and from the Anthropic Messages dialect so no client changed. The Site
316
+ Builder's Connect step shows which providers the local dev proxy already has
317
+ keys for (masked), takes a token for the session (sent once to localhost,
318
+ kept in the proxy's memory, optionally saved to `.env` with mode 600), offers
319
+ the model catalog, an image renderer (Grok Imagine / OpenAI Images) and two
320
+ session modes: the guided nine steps, or an open session that can open an
321
+ existing site under the target root, read/edit/write/delete its files, run
322
+ `git status`/`diff`/`log`, generate images into `assets/`, run `jekyll build`
323
+ inside the container and stop a long tool run. Generated sites are
324
+ pre-configured for the chosen provider (`ai_chat.provider`, `preview_images`,
325
+ `.env.example`). The dev proxy now starts without a credential. Also fixes a
326
+ pre-existing 162px horizontal overflow of the whole `/setup/` page at phone
327
+ widths (the Connect step's command boxes could not shrink). (evidence:
328
+ [`test/visual/evidence/site-builder-providers/`](test/visual/evidence/site-builder-providers/README.md)
329
+ — Connect step: 2 provider cards, 2 session modes, token field is a password
330
+ input inert offline; mocked proxy: "needs a token" → "Grok connected"; Build:
331
+ existing sites listed, Open sets the target; mobile overflow 162px → 0px)
332
+ - **CI now produces a UI pull request's visual artifacts instead of only
333
+ checking for them (ZER0-085)** — `visual-evidence-autogen.yml` renders every
334
+ same-repo PR in the same jammy Playwright image the snapshot gate uses, runs
335
+ the PR's `test/visual/*-evidence.mjs` generators (or the new generic
336
+ base-vs-head generator `test/visual/pr-evidence.mjs`, which renders the base
337
+ branch and the head side by side), verifies the 9-skin baselines, and pushes
338
+ the montages + `metrics.json` to the branch. Stale baselines are refreshed
339
+ **only** when the new `visual-evidence-reviewer` agent, having viewed the
340
+ expected | actual | diff montage, judges the diff to be the change the PR
341
+ describes — a code step disposes, the model only proposes ([#417](https://github.com/bamr87/zer0-mistakes/issues/417)
342
+ is why). The evidence gate now requires generated proof (a README alone no
343
+ longer passes), `ci-self-repair` leaves a red `Visual Snapshots` job to this
344
+ lane, and `test/update-snapshots.sh` gained `PRE_TEST_SCRIPT` /
345
+ `POST_TEST_SCRIPT` / `SKIP_PLAYWRIGHT` hooks. Closes the gap that kept
346
+ [#454](https://github.com/bamr87/zer0-mistakes/pull/454) red: its authoring
347
+ agents ran where Docker was gated, and nothing in CI could render what they
348
+ could not. Kill switch: repo variable `VISUAL_EVIDENCE_AUTOGEN_ENABLED=false`.
349
+ The lane restores its own tooling from the base branch before it runs, so it
350
+ works on branches cut before it shipped: without that its first real run on
351
+ #454 died in 20 seconds (that branch has no copy of the orchestrator), and a
352
+ branch carrying an older `update-snapshots.sh` would have silently generated
353
+ nothing at all.
354
+ - **Site Builder — a Claude-guided setup wizard that ends with a running site
355
+ (ZER0-086, extends ZER0-067)** — `/setup/` (and the `welcome` layout) grew
356
+ from a five-step `_config.yml` form into a nine-step builder: Connect →
357
+ Prerequisites → Identity → URLs → Structure → Appearance → Voice →
358
+ Integrations → Build. An embedded Claude session rides alongside every step
359
+ through the local dev proxy (Claude Code OAuth, `claude setup-token`; the
360
+ token never reaches the page). It sees the whole wizard state on every turn
361
+ and acts on it with tools: fill in fields, override a generated file, run
362
+ live prerequisite checks (Docker, Git, gh, VS Code, Node, Claude CLI — a
363
+ fixed command table), read the theme's real source and search the docs,
364
+ resolve a project folder, write the generated project, run `docker compose
365
+ up|ps|logs|down`, and check the new site answers — each mutation behind a
366
+ confirmation card. The form half still works with no proxy at all (GitHub
367
+ Pages included): it generates `_config.yml`, `_config_dev.yml`, `Gemfile`,
368
+ `docker-compose.yml`, `index.md`, navigation, about page, welcome post, one
369
+ index page per collection, `.gitignore`, `zer0.install.yml`, `.env.example`
370
+ and `README.md`, with per-file download and a self-extracting bash bundle.
371
+ Forms gained a site brief, a URL **Suggest** button, a site-type quick-pick,
372
+ a navigation row editor, skin cards with a live **Preview on this page**
373
+ toggle, tone/audience pickers, integration switches with conditional
374
+ sub-fields, a copy button on every command, and a toast for feedback. New:
375
+ `_data/site_builder.yml` (steps, prerequisites mirroring
376
+ `machine-setup.md`, catalogs, framework brief),
377
+ `templates/deploy/chat-proxy/wizard-store.mjs` (the dev-only sandbox behind
378
+ `/api/wizard/*`), `assets/js/site-builder.js`, `_includes/setup/{claude-
379
+ session,prereq-checklist}.html`, the `site_builder:` config block, a
380
+ quickstart page and a feature reference. (evidence:
381
+ [`test/visual/evidence/site-builder/`](test/visual/evidence/site-builder/README.md))
382
+ A first recorded end-to-end run then shaped the build half: the generated
383
+ `docker-compose.yml` publishes LiveReload on the site's port + 1 (a fixed
384
+ 35729 collided with the theme's own dev container and the new site never
385
+ started) and shares one gem cache across generated sites; every enabled
386
+ collection now ships a valid starter document (a doc, a quickstart step, a
387
+ note with a wiki-link, a structured recipe) and the cookbook index uses the
388
+ theme's `cookbook` layout over the real `recipes` collection; `_config.yml`
389
+ sets `collections_dir: pages` (without it every collection but posts was
390
+ invisible to Jekyll); each site gets its own `assets/images/logo.svg`
391
+ monogram and gem-based sites turn the SVG background layers off, because
392
+ the published gem ships no theme images; a revoked Claude credential now
393
+ says so and names the fix. Validation is a scenario
394
+ runner, `test/visual/site-builder-walkthrough.mjs`, that samples random
395
+ briefs per site type (`test/visual/site-builder-scenarios.mjs`), drives the
396
+ wizard on video, builds the site with Docker and asserts its routes, title
397
+ and skin — replayable by seed.
398
+ The builder then gained a **schema-driven site plan**: `plan_schema` in
399
+ `_data/site_builder.yml` (a JSON-Schema subset validated in the browser)
400
+ describes what the agent may produce — a landing page (five templates,
401
+ nine section types, hero + CTAs), navigation shape (flat or grouped
402
+ dropdowns, sidebar none/auto/docs tree), theme overrides (nine palettes or
403
+ custom colours, seven font pairings, three corner radii) and up to twelve
404
+ example pages with Markdown bodies — and the generators turn it into
405
+ `index.md` (a Liquid landing engine) + `_data/landing.yml`,
406
+ `_data/navigation/main.yml` and `docs.yml`, `assets/css/user-overrides.css`
407
+ + `_includes/custom/head.html`, and one file per page. Claude submits plans
408
+ with `set_site_plan` behind a confirmation card; the Structure and
409
+ Appearance steps expose the same choices as controls, with a page planner
410
+ and a **Preview on this page** toggle that applies the generated overrides
411
+ to the wizard itself. The scenario runner randomises the plan too and
412
+ asserts the built site's landing template, palette colour, web fonts and
413
+ planned routes.
414
+ - **Mermaid diagrams are now accessible figures with a toolbar (ZER0-013)** —
415
+ every ```` ```mermaid ```` fence (and legacy `<div class="mermaid">`) renders
416
+ as a `<figure>` with a rendered SVG and a small toolbar: zoom out / in / reset
417
+ (50–400 %, real layout — the SVG is resized, so the frame scrolls and
418
+ drag-to-pan, `Ctrl`+wheel and the `+` / `-` / `0` keys all work), **View
419
+ fullscreen** (a native `<dialog>` — the fix for a wide diagram shrunk to
420
+ illegibility on a phone; `Esc` closes it and focus returns to the opener),
421
+ **Copy diagram source**, and **Download as SVG** (page background baked in so
422
+ a dark-mode export stays readable). On pointer devices the toolbar floats over
423
+ the top-right corner on hover/focus; on touch devices it is a static row above
424
+ the diagram so it never covers it. `accTitle` becomes the visible
425
+ `<figcaption>` and the diagram's accessible name, `accDescr` its `<desc>`,
426
+ and the scrollable frame is a focusable, labelled region. A syntax error no
427
+ longer blanks the block: the figure shows the parse message, a hint, and the
428
+ source in a `<details>`, with *copy* still enabled. Colours are **derived from
429
+ the live design tokens** (`--bs-primary`, `--bs-body-bg`, `--zer0-color-*`)
430
+ through Mermaid's `base` theme, so diagrams follow the colour mode, the
431
+ selected skin and any `theme_color` override, and re-render in place when
432
+ those change; dark mode is decided by background luminance, so wizard mode and
433
+ dark skins get legible ink, and a contrast guard keeps nodes visible when the
434
+ brand equals the page colour. Pie slices, git branches and mind-map branches
435
+ get a 12-colour series fanned out from the brand hue. `securityLevel` now
436
+ defaults to `strict` (`mermaid.security_level: loose` restores `click`
437
+ callbacks / HTML labels); `mermaid.toolbar`, `fullscreen` and `download` can
438
+ switch the controls off. Implemented as `_includes/components/mermaid.html`
439
+ (loader: config JSON + two `defer` scripts — the 3.3 MB bundle used to load
440
+ synchronously in `<head>`) + `assets/js/mermaid-diagrams.js` +
441
+ `_sass/components/_mermaid.scss`, replacing ~400 lines of inline
442
+ `<script>`/`<style>` in the include; the `!important` SVG overrides that
443
+ flattened `classDef`/`style` colours and pie slices are gone. Toolbar strings
444
+ are `diagram_*` keys in `_data/ui-text.yml`. The docs page
445
+ (`/docs/features/mermaid-diagrams/`) now renders a live example of every
446
+ diagram type, the caption directive, per-node styling and the error state.
447
+ Guarded by `test/visual/features/mermaid.spec.js` (12 tests, smoke tier)
448
+ (evidence: [`test/visual/evidence/mermaid/`](test/visual/evidence/mermaid/README.md)
449
+ — dark-mode toggle keeps the SVG (before: SVG lost, stylesheet text on
450
+ screen); page overflow 0px at 320/390px; 11/12 docs diagrams rendered, 1
451
+ shown as an error card with its source kept).
452
+
453
+ ### Fixed
454
+
455
+ - **Generated posts and notes were invisible on GitHub Pages** — every dated
456
+ document the wizard wrote was stamped `T09:00:00.000Z` on the day it was
457
+ generated. Jekyll refuses to publish future-dated documents unless
458
+ `future: true`, and GitHub Pages builds with the default, so a site generated
459
+ before 09:00 UTC went live with its posts and notes missing: linked from the
460
+ collection index and listed in the sitemap, but 404 when clicked. Generated
461
+ content is now stamped with the actual moment it was written, and a
462
+ regression test fails if any generated document carries a future date.
463
+ Found by publishing two example sites and clicking the links.
464
+ - **Every page of every generated site 404'd on `user-overrides.js`** — the
465
+ Site Builder writes `user_overrides: true` so the theme loads the palette and
466
+ font overrides it generates, but that same flag also makes the theme load
467
+ `assets/js/user-overrides.js`, which the wizard never created. Generated
468
+ sites now ship a commented stub, so the request resolves and site owners have
469
+ an obvious place to put their own JavaScript. Found while publishing example
470
+ sites built by the wizard.
471
+ - **Every generated landing page shipped an invisible button** —
472
+ `components/cta-button.html` mapped `variant: outline` to `btn-outline-light`,
473
+ which is white text on a white border, and the Site Builder's own default
474
+ call to action uses `outline` on a light hero. `outline` is now an outlined
475
+ *primary* button; `outline-light` is the explicit variant for a dark surface,
476
+ and the generated landing engine picks between them from `hero.variant`.
477
+ The theme's own homepage hero is `bg-primary text-white`, so its two
478
+ outlined buttons moved to `outline-light` and render exactly as before —
479
+ a consumer who copied `_data/landing.yml` into a site with a dark hero
480
+ should make the same one-word change.
481
+ - **A planned `landing.hero.image` was accepted and then thrown away** — the
482
+ site plan's schema has offered `landing.hero.image` since the Site Builder
483
+ shipped, but the landing engine it generates never rendered it, so an agent
484
+ (or a human) could set a hero image, see it validated, watch it land in
485
+ `_data/landing.yml`, and get a landing page with no picture. `index.md` now
486
+ renders it inside the hero, decorative (empty `alt`) so a screen reader does
487
+ not hear the headline twice. Found by building a site end to end through the
488
+ new Grok path, where the "Hero image with …" shortcut pointed straight at the
489
+ dead field.
490
+ - **A partial hero patch silently wiped the rest of the hero** —
491
+ `set_site_plan` replaced `landing.hero` wholesale, so the natural agent move
492
+ of "write the copy now, add the artwork two turns later"
493
+ (`{landing: {hero: {image}}}`) discarded the headline, subheadline and every
494
+ call to action, and the landing page quietly reverted to the site title and
495
+ the default buttons. The hero now merges field by field; `sections`,
496
+ `navigation.items` and `pages` still replace, because a shorter list has to
497
+ mean a shorter list. The tool description says so, and the regression test
498
+ pins both halves.
499
+
500
+ - **A new post appeared at its own URL but on no index** — the generated
501
+ `_config_dev.yml` turned on Jekyll's experimental incremental regeneration,
502
+ which rebuilds only documents whose own source changed. Adding a post left
503
+ the collection index, the home page's latest-posts list and the feed showing
504
+ the old set, with the post reachable only by typing its URL. New sites are
505
+ now generated with `incremental: false`; a full rebuild of a fresh site takes
506
+ about a second and is always right.
507
+
508
+ - **The 15-second status poll handed the composer back mid-turn** — the Site
509
+ Builder's proxy poll rebuilt the panel's enabled state from the connection
510
+ alone, so during a long action (a Docker build, an image render) the input
511
+ and Send button re-enabled themselves and the Stop button vanished, while
512
+ tools were still running. A second message could then interleave with the
513
+ first. The poll now respects the busy state.
514
+ - **A newly added post never appeared on the running site** — Jekyll's
515
+ `--watch` only tracks collection documents that existed when `serve` started,
516
+ so a page or post the assistant added to a running project stayed invisible
517
+ with no explanation. `write_project_file` now says so in its result and
518
+ offers a restart, and `run_compose` gained a `restart` action.
519
+
520
+ - **Switching colour mode destroyed every Mermaid diagram** — the old include's
521
+ `MutationObserver` re-render emptied each `.mermaid` div and refilled it with
522
+ the div's *current* text, which after the first render is the SVG's own
523
+ stylesheet, and never cleared Mermaid's `data-processed` flag, so
524
+ `mermaid.run()` skipped the div. Toggling dark mode on `/quickstart/`,
525
+ `/about/` or `/docs/ruby-101/` replaced the diagram with a wall of
526
+ `#mermaid-… {font-family: …}` text and a permanent "Loading diagram…" label
527
+ (reproduced headlessly on `main`: `svgs: 1 → 0` after the toggle). Sources are
528
+ now kept per figure and every render goes through `mermaid.render()`, so a
529
+ mode or skin change re-renders from the original definition — asserted by the
530
+ regression test in `test/visual/features/mermaid.spec.js`.
531
+ - **The Obsidian wiki-link resolver rewrote the inside of inline SVGs** —
532
+ `assets/js/obsidian-wiki-links.js` skips text under `CODE`/`PRE`/`A`/
533
+ `SCRIPT`/`STYLE` by comparing `nodeName`, but elements in the SVG namespace
534
+ report it in lower case (`style`, `text`), so a Mermaid diagram's own
535
+ `<style>` was walked and every `#id` selector in it was turned into a
536
+ `<a class="obsidian-tag">` link. The stylesheet lost its scoping: one
537
+ diagram's `#arrowhead path { fill }` became a bare `path { fill }` for the
538
+ whole page, and pie slices rendered dark grey while their legend showed the
539
+ right colours. The old include escaped it only because the resolver
540
+ special-cases the `.mermaid` class. The name check is now case-insensitive
541
+ and any `<svg>` subtree (graphics, never prose) is skipped, with
542
+ `.zer0-diagram` added to the class exemptions. `test/test_resolver.js` gains
543
+ a fixture with a lower-case `style`/`text` inside `<svg>`, and the Mermaid
544
+ spec asserts a pie slice's computed fill equals its own attribute and that no
545
+ unscoped `path` rule exists on the page.
546
+
547
+ ### Changed
548
+
549
+ - **The page-feedback widget now files the fleet's issue contract, not its own
550
+ (UPS-FB).** The theme built its own issue body, its own escaping, and its own
551
+ URL-length trimming — and so did the 404 page and the AI chat, three builders
552
+ in one repo with three different behaviours. All three now go through
553
+ `FleetFeedbackCore` in `assets/js/fleet-feedback.js`, vendored byte-identically
554
+ from the fleet kit (`bamr87/bamr87` `templates/feedback/`, held to the hub's
555
+ copy by the drift gate). It is the same builder the fleet's `<fleet-feedback>`
556
+ web component uses, so an issue filed from a page here and one filed from a
557
+ React app elsewhere are byte-identical to the pipeline that triages them.
558
+
559
+ What that adds to every filed issue: sections in a fixed order, a
560
+ `<!-- fleet-feedback v1 type=... -->` marker the issue pipeline reads to
561
+ recognise an already-structured report, and a real over-budget path. The AI
562
+ chat in particular used to `.slice(0, 6000)` its body — silently dropping
563
+ whatever Claude had written past that point — and now trims by section and
564
+ hands the full text to the clipboard.
565
+
566
+ The theme keeps its Bootstrap modal and its AI triage step; only the body
567
+ assembly moved. Two UIs, one contract.
568
+
569
+ - **Request-type labels map onto the fleet issue-pipeline taxonomy.**
570
+ `enhancement`/`documentation` become `feature`/`docs`; `area:docs`/`area:feat`
571
+ drop out. Every label the widget applies exists in this repo (GitHub silently
572
+ drops the ones that do not), and a widget-filed issue is now pipeline-eligible
573
+ on the next scan. The zero-`_data` fallback taxonomy carries them too, so
574
+ `remote_theme` consumers benefit the moment those labels exist in their repo.
575
+
576
+ - **Captured console lines are redacted before they enter the buffer.** Bearer
577
+ tokens, API keys, JWTs, GitHub tokens and email addresses are masked on the
578
+ way in, so a secret cannot be previewed, copied, or filed even by accident.
579
+ Credentials reach the console more often than anyone expects — an
580
+ `Authorization` header logged by a fetch wrapper, a signed URL in a 403.
581
+
582
+ - **The capture buffer hooks `console.warn` and `console.error` only.**
583
+ Previously `log`, `info` and `debug` were captured too. At a 40-entry ring,
584
+ debug chatter evicts the one line that explains the failure — which is the
585
+ line a report exists to carry. `_includes/core/console-capture.html` is now a
586
+ two-line loader for the vendored buffer rather than 60 lines of inline script,
587
+ which also *shrinks* `<head>` and buys back room for the charset meta that has
588
+ to land in the first 1024 bytes (#372).
589
+
590
+ ### Fixed
591
+
592
+ - **The 404 page files a report a maintainer can act on.** "Submit an Issue on
593
+ GitHub" opened a hand-built form containing two sentences of boilerplate — no
594
+ page context, no environment, no captured errors. It now opens the feedback
595
+ widget pre-typed as `fix-page` with the URL that failed (and the page that
596
+ linked to it) already in the description.
597
+
598
+ - **The feedback FAB survives its script failing to load.** It was a `<button>`
599
+ whose only behaviour came from JavaScript; with the widget now depending on a
600
+ second script, a 404 on either one would have left a decorative circle in the
601
+ corner. It is an anchor to the issue form, upgraded in place when the script
602
+ runs — the same progressive enhancement the inline "Improve" link already had.
603
+
604
+ - **The page-feedback Playwright spec dismisses the cookie-consent banner.** The
605
+ banner is a full-width bar on a layer above the FAB stack, so it swallowed
606
+ every click aimed at the FAB. Every other spec that touches lower-screen chrome
607
+ already seeded the consent choice through the shared fixture; this one never
608
+ did.
609
+
610
+ ### Changed
611
+
612
+ - **TOC scroll spy now bolds the section you are actually reading** — the
613
+ highlighted entry in the right-hand Table of Contents jumped around and sat
614
+ one to three sections ahead of the viewport. Measured on `/docs/features/toc/`
615
+ at 1280×820, the wrong entry was bolded at **19–23 of 25 scroll positions**
616
+ — the count moves between runs because which implementation won depended on
617
+ callback timing, which is the bug in one sentence.
618
+
619
+ Three implementations were toggling `.active` on the same
620
+ `#TableOfContents a` links, so whichever fired last won. Bootstrap's native
621
+ ScrollSpy was wired up twice — `data-bs-spy="scroll"
622
+ data-bs-target="#TableOfContents"` on `<body>` in `_layouts/root.html` *and*
623
+ on `.bd-main` in `_layouts/default.html` (whose `data-bs-offset="100"` had
624
+ been dead since Bootstrap 5.2 replaced that option with
625
+ `data-bs-root-margin`). `assets/js/ui-enhancements.js` ran a third observer
626
+ over *every* `a[href^="#"]` on the page, clearing `.active` from all of them
627
+ each time a `section[id]` intersected. Both Bootstrap hooks are removed —
628
+ the `<body>` one was also stripping server-rendered `.active` classes off
629
+ admin sidebar links, which `fixtures.gotoBeforeScrollSpy` exists to work
630
+ around — and the `ui-enhancements.js` observer is now scoped away from TOC
631
+ and sidebar links.
632
+
633
+ The theme's own spy (`assets/js/modules/navigation/scroll-spy.js`) was wrong
634
+ on its own terms too: it asked IntersectionObserver for the "most visible"
635
+ heading, but headings are a few pixels tall, so every heading inside the
636
+ observer band reports the same `intersectionRatio` — the winner was whichever
637
+ entry happened to be in that callback's batch, and a heading scrolling *out*
638
+ of the band triggered no re-evaluation at all. It now applies a positional
639
+ rule: the active heading is the last one whose top has crossed the reading
640
+ line (the document's `scroll-padding-top`, the same offset anchor navigation
641
+ uses), with the last heading winning once the page is scrolled to the bottom
642
+ so trailing sections shorter than the viewport are still reachable. The
643
+ answer is recomputed from scratch on each rAF-throttled scroll frame from
644
+ cached heading offsets, re-measured on resize and content reflow
645
+ (`ResizeObserver`), so it cannot drift out of sync.
646
+
647
+ Two smaller fixes ride along. Clicking a TOC entry now holds that entry
648
+ active while the smooth scroll animates, instead of flashing every heading
649
+ passed on the way. And keeping the active entry visible inside the TOC no
650
+ longer calls `scrollIntoView()`, which bubbles up and scrolls the *page* —
651
+ feeding straight back into the spy; it adjusts the TOC container's own
652
+ `scrollTop`, resolving that container at call time (`.bd-toc` on desktop,
653
+ `.offcanvas-body` on mobile) rather than assuming one. The active link also
654
+ carries `aria-current="true"`.
655
+
656
+ `config.scrollSpy.rootMargin`/`threshold` are replaced by
657
+ `config.scrollSpy.offset` (`null` = derive from `scroll-padding-top`) and
658
+ `tolerance`. Guarded by `test/visual/features/scroll-spy.spec.js` (smoke
659
+ tier), which is 5 failed / 1 passed against the pre-fix theme and 6 passed
660
+ with the fix. (evidence:
661
+ [`test/visual/evidence/scroll-spy/`](test/visual/evidence/scroll-spy/README.md)
662
+ — wrong TOC highlight at 19/25 scroll positions → 0)
663
+ - **Navbar controls no longer flicker under a stationary pointer (#404)** —
664
+ `.nav-link:hover`, the Search/Settings utility buttons and the two sidebar/TOC
665
+ FABs applied `transform: translateY(-1px)`. A 1px lift moves the element out
666
+ from under a pointer parked on the row it vacates, so hover exits, the element
667
+ drops back under the pointer, and hover re-enters — an infinite flicker with
668
+ no pointer movement at all. Nav items are thin enough for a 1px lift to
669
+ reliably do it.
670
+
671
+ Deleting those declarations was **not sufficient**, and the reason is the
672
+ substantive finding here: `_sass/components/_ui-enhancements.scss` applies
673
+ `transform: translateY(-2px)` to `.btn:hover` — *every* button in the theme.
674
+ The utility buttons and both FABs are `.btn`s, so with the navbar's own 1px
675
+ lift removed the buttons kept flickering on the inherited 2px one — measured
676
+ on the built site, the Settings button lost and regained hover 14 times in
677
+ 800ms with the pointer completely still. The fix therefore cancels it
678
+ explicitly with `transform: none`, the corrective pattern this repo already
679
+ uses in `_post-navigation.scss` and its `prefers-reduced-motion` blocks. The
680
+ FABs get the same treatment by the same reasoning — they are the same kind of
681
+ `.btn` with the same lift — but they were not rendered at any viewport reached
682
+ during verification, so that half is reasoned, not observed.
683
+
684
+ Background, colour and shadow transitions are untouched, and the controls keep
685
+ a pressed cue as `:active { transform: translateY(1px) }` — safe, because
686
+ `:active` only holds while the button is held down, so the pointer cannot
687
+ leave and re-enter in a loop.
688
+
689
+ Three corrections to the issue as filed. The vacated row is the **bottom**
690
+ edge, not the top — a probe parked on the top edge passes without exercising
691
+ the bug at all. Only a transform on the **hovered element's own box** can
692
+ flicker: `&:hover i { transform: scale(1.1) }` moves a child and never shrinks
693
+ the parent's hit box, so those went for consistency, not as causes. And
694
+ `.nav-link:hover`, named as the primary culprit, was **dead code** — its lift
695
+ sat inside `@media (min-width: 992px) { @container navbar-main (max-width:
696
+ 75rem) }`, and measured on the built site at 1040/1140/1200/1280px its
697
+ computed hover transform is `none` at every width, on `main` too. Every
698
+ flicker actually observed was on the utility buttons.
699
+
700
+ Guarded by `test/visual/core/hover-stability.spec.js`: with the pointer on the
701
+ control's centre the border box must be byte-identical to its resting box and
702
+ the computed transform must be `none`; with the pointer held half a pixel
703
+ inside the bottom edge for 800ms, `mouseleave` must stay at zero. The verdict
704
+ is the *computed* value rather than "no matching rule exists", because
705
+ `.btn:hover` does still match these controls and always will — it is
706
+ overridden, not deleted. `mouseleave`, not `mouseout`: the latter also fires
707
+ when the pointer crosses onto a descendant, which is not the element losing
708
+ hover, and it produces a false positive on every `.nav-hover-dropdown` parent.
709
+ Against `main` the spec is 4 failed / 1 passed; with the fix, 5 passed.
710
+ Evidence in `test/visual/evidence/hover-flicker/` — the Settings button lost
711
+ hover 14 times in 800ms before, 0 after.
712
+
713
+ Scoped to the navbar. The issue's original acceptance criterion — *no
714
+ transform in any `:hover` rule under `_sass/`* — is 42 rules across ~15 files
715
+ (card lifts, book-cover tilts, recipe and author hovers) and a visual redesign
716
+ of a published theme rather than a defect fix; it also flagged
717
+ `transform: none`, which is the corrective pattern this repo already uses in
718
+ `_post-navigation.scss` and its `prefers-reduced-motion` blocks. A theme-wide
719
+ motion policy belongs in its own task.
720
+ - **Mobile browser chrome now tracks the page surface (#281)** — `theme-color`
721
+ drives the iOS Safari address bar and the Chrome/Android task-switcher card.
722
+ It fell back to `theme_color.main`, which is the brand **accent** (`#007bff`
723
+ here), so a dark-first theme advertised a bright blue address bar; and being
724
+ config-gated it emitted **nothing at all** on `remote_theme` consumers, which
725
+ do not inherit this repo's `_config.yml` — the case the issue was originally
726
+ filed about. The include now emits scheme-aware tags sourced from
727
+ `--bs-body-bg` (light `#ffffff`, dark `#212529`, Bootstrap 5.3.3's own
728
+ surfaces) **with no configuration required**, and honors new optional
729
+ `favicon.theme_color_light` / `theme_color_dark` keys. A site that pins its
730
+ mode (`color_mode_default: dark`/`light`, or `color_mode_lock: true`) gets a
731
+ single unconditional tag instead, because `media="(prefers-color-scheme: …)"`
732
+ keys off the OS and a pair would hand light chrome to an OS-light visitor
733
+ reading a page the site renders dark. `favicon.theme_color` still pins one
734
+ color for both schemes, so existing consumer config is unchanged. Guarded by
735
+ four `@critical` tests in `test/visual/core/head-contract.spec.js` that
736
+ compare each value against the page's computed `--bs-body-bg` rather than a
737
+ constant, plus `test_theme_color_fallback_without_config` covering the
738
+ no-config consumer case.
739
+
740
+
741
+ - **Consumers no longer inherit the theme's own navigation (#332)** — `install.sh`
742
+ copied the theme's entire `_data/` into every consumer, so each site's *own*
743
+ `_data/navigation/*.yml` was a verbatim copy of the theme's and carried the
744
+ theme's page taxonomy. Measured on a real install: **106 of 129 seeded
745
+ navigation URLs pointed at pages the installer does not create** — ~47 per
746
+ doc page from `docs.yml` alone, which htmlproofer on a consumer counted as
747
+ ~1,598 broken internal links across ~34 docs, none of them removable from
748
+ consumer content. `_data/navigation/` is now held back from the theme copy
749
+ and seeded from `templates/data/navigation-*.yml.template`, whose links
750
+ resolve against the pages the installer actually creates: **0 of 26
751
+ unresolvable**. All seven files are covered, not just `docs.yml`. Because
752
+ `create_from_template` skips a file that already exists, a re-run or upgrade
753
+ preserves navigation a consumer has edited. The sidebar renderer is
754
+ unchanged — `nav: tree` → `auto` → `page.collection` was always correct, it
755
+ was being handed the wrong data. Existing consumers are not fixed
756
+ retroactively (`_data` is not part of Jekyll's theme payload); see
757
+ [Migrating already-seeded navigation data](docs/systems/theme-propagation.md).
758
+ Guarded by `test/test_install_navigation_seed.sh`, which fails on the old
759
+ installer naming all 51 seeded dead links.
760
+
761
+ - **Translation placeholders leaked into published French pages** — before a
762
+ line is sent to the model, `scripts/translate.rb` masks every non-translatable
763
+ span as a `⟦N⟧` token and restores it afterwards. The patterns run in order,
764
+ coarse first, so a span can be masked **twice**: a Liquid expression inside a
765
+ link destination becomes `](⟦3⟧)` and is then masked again as a whole, and the
766
+ same happens to Liquid inside an inline code span. `unmask` used a single
767
+ `gsub`, and `gsub` never rescans its own replacement text — so the outer span
768
+ came back and the inner token was stranded as a literal `⟦3⟧` on the page.
769
+ That is 107 tokens across 23 generated pages on `main` today, and where the
770
+ masked span was a URL it is a dead link: `[couleurs](⟦3⟧)` in
771
+ `fr/about/design.md`, `href="⟦24⟧"` in the same file, the whole Liquid
772
+ variable table in `fr/docs/customization/layouts.md`. `unmask` now substitutes
773
+ until the text stops changing, and refuses to return anything still holding a
774
+ placeholder — a corrupt page fails its own job (counted, logged, run exits
775
+ non-zero) rather than shipping. The existing "no placeholder tokens leak"
776
+ assertions in `test/test_i18n.sh` were correct but toothless: no fixture line
777
+ nested a mask. Both shapes are now in the fixture, so the guard fails without
778
+ the fix.
779
+
780
+ The already-generated pages are not repaired by this change. Staleness is
781
+ decided by source SHA alone, so a masker fix does not make an unchanged page
782
+ stale; repairing them needs a `--full` re-translation.
783
+ - **`components/background-image.html` — cover art painted as a CSS background,
784
+ announced correctly (#401)** — the theme had a solid convention for art
785
+ rendered as {% raw %}`<img>`{% endraw %} (`preview-image.html` always emits an escaped `alt`)
786
+ and none for art painted as a **background**. A background image is invisible
787
+ to assistive technology, so such a box announces nothing at all. The correct
788
+ pattern already existed by hand in three places (`abc-letter.html`, the
789
+ article rating stars, `theme-customizer.html`) without being a named
790
+ convention; this generalizes it. Two branches: a real image gets
791
+ `role="img"` with an escaped `aria-label`, a decorative one gets
792
+ `aria-hidden="true"` and **neither** `role` nor `aria-label` — combining them
793
+ announces a graphic and then hides it, which is worse than either alone.
794
+ Decorative is the **default**, and the real-image branch must earn it with
795
+ both a `src` and a non-empty `alt`. Path handling follows
796
+ `preview-image.html`. Purely additive: no in-theme call site, no CSS, no
797
+ layout change, and `content/intro.html` is deliberately untouched — it paints
798
+ a background *and* wraps the page `<h1>`, so `role="img"` there would delete
799
+ the heading from the accessibility tree. Guarded by
800
+ `test_background_image_include_contract`, which renders the include and
801
+ checks both branches, the escaping and all three path cases.
802
+ - **Heading outlines no longer skip levels in theme chrome (#436)** — several
803
+ components picked a heading element for its default **font-size** rather than
804
+ its position in the document, so an article read
805
+ `h1 → h2 → h6 → h2 → h5 → h3 → h6` to anyone navigating by heading. Measured
806
+ across a 415-page build: **391 of 415 pages carried at least one skip, 1,060
807
+ in total**; now 66 and 173. Size and level are decoupled — `<h3 class="h6">`
808
+ keeps the small type and states the real depth — in the cookie dialog, the
809
+ settings offcanvas, the language panel, the author card and the related-posts
810
+ region. The language panel was not in the report and was doing the same thing
811
+ on every page. `author-card.html` renders under twelve different call sites,
812
+ so its level is now `heading_level` (default `5`, unchanged for callers that
813
+ do not pass it) rather than a guess. Fails **WCAG 1.3.1**. Nothing moves
814
+ visually. Guarded by `test/visual/core/heading-outline.spec.js`. One offender
815
+ is injected at **runtime** by `assets/js/modules/theme/appearance.js`, so no
816
+ grep over `_includes`/`_layouts` could find it — only a rendered assertion
817
+ does. Still open, deliberately: 66 pages with inline card markup in the news
818
+ and section index layouts, plus the `palette-generator` and `skin-editor`
819
+ admin tools — see the evidence bundle.
820
+
821
+ - **Liquid written as documentation was being executed, not displayed** — Liquid
822
+ runs before Markdown, so backticks and code fences never escaped it; they only
823
+ changed how its *output* was displayed. Two pages leaked as a result. The
824
+ CHANGELOG page carried a **second full copy of `<head>` inside its body**
825
+ (Bootstrap CSS, Bootstrap Icons and `X-UA-Compatible` all emitted twice, and
826
+ the Google Tag Manager snippet a second time on a production build) because an
827
+ {% raw %}`{% include core/head.html %}`{% endraw %} in prose was run rather than shown. The
828
+ Layout Variables table in `/docs/customization/layouts/` documented nothing:
829
+ {% raw %}`{{ content }}`{% endraw %} injected the whole rendered page into a table cell and
830
+ {% raw %}`{{ page.title }}`{% endraw %} rendered as the literal word "Layouts", leaving
831
+ 196 KB of leaked layout output where a five-line code sample belonged. Both are
832
+ now wrapped in Liquid raw blocks, the idiom the same files already used
833
+ elsewhere. A `test_content_liquid_is_raw_protected` guard in
834
+ `test/test_core.sh` fails if injecting Liquid appears unprotected inside a code
835
+ fence or code span again. The layouts doc also had its layout **names** and
836
+ **hierarchy** corrected against `_layouts/`: `journals` has not existed since
837
+ the v1.0 rename to `article`, and three of the four children in the
838
+ inheritance diagram were wrong — `home` and `landing` inherit `root`, not
839
+ `default`, which decides whether a layout renders the sidebar and TOC at all.
840
+
841
+ - **Developer doc banners no longer ship to visitors (#375)** — the 1,065
842
+ multi-line {% raw %}`<!-- ... -->`{% endraw %} banners documenting `_includes/**` and
843
+ `_layouts/**` (file paths, dependency lists, design rationale) are now Liquid
844
+ {% raw %}`{% comment %}`{% endraw %} blocks, which Jekyll strips at build time. The
845
+ in-source documentation is unchanged; only the delimiters moved. Across a
846
+ 415-page build the delivered HTML drops from 91.5 MB to 71.6 MB — **21.7%** —
847
+ because an include's banner is re-emitted at every call site, not once:
848
+ `/authors/default/` alone carried 130,840 bytes of it from `post-card.html`
849
+ rendering 1,100 times. Per page: 47,898 comment bytes in 344 blocks → 111
850
+ bytes in 3. The four boundary markers inside Google's own copy-paste
851
+ analytics snippets are kept verbatim, and comments inside
852
+ `<script>`, `<style>`, `<pre>` or a Liquid raw block are untouched. A new
853
+ `test_developer_doc_banners_are_liquid` guard in `test/test_core.sh` fails if
854
+ any reappear. Two side effects worth knowing: reading-time estimates were
855
+ counting the banners and are now correct (`/docs/` claimed 63 minutes, really
856
+ 24), and three pages that leak unescaped Liquid emit less as a result — those
857
+ leaks are pre-existing and filed separately.
52
858
 
53
859
  - **Setup wizard: live preview, vertical stepper, draft persistence (T-040, #408)** —
54
860
  the `_config.yml` preview is now a **persistent sticky panel** shown at every
@@ -196,6 +1002,83 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
196
1002
 
197
1003
  ### Fixed
198
1004
 
1005
+ - **Finished work kept reopening its own issues** — `scripts/sync-backlog.rb`
1006
+ treats `_data/backlog.yml` as the source of truth and **reopens** any issue whose
1007
+ task is not `done`. T-038, T-040 and T-045 shipped in v1.29.0 (#431, #432, #425)
1008
+ and their `Closes #NNN` commits duly closed #406, #408 and #417 — then the next
1009
+ sync reopened all three, because the backlog still said `open`. Marked done with
1010
+ their PR links, which is the only thing that actually closes them.
1011
+ - **The nightly sticky issue could be raised but never cleared (#326)** —
1012
+ `nightly-extended.yml` files and updates a sticky issue on failure and does
1013
+ nothing on recovery, so a fixed failure stayed open indefinitely. #326 sat open
1014
+ five days past its own fix, carrying a P1 into every triage pass. A
1015
+ `report-recovery` job now comments and closes it when the tier goes green. It is
1016
+ gated on `preflight.outputs.go == 'true'` as well as both jobs succeeding: the
1017
+ preflight short-circuits on an unchanged `main` and reports `skipped`, so a bare
1018
+ `success()` would close the issue on a night that tested nothing.
1019
+ - **37 pages shipped dead controls with dangling ARIA references (#373)** —
1020
+ `#bdSidebar` and `#tocContents` are emitted by `_layouts/default.html` alone,
1021
+ but their toggles live in `core/header.html` and `core/footer-fabs.html`, which
1022
+ `_layouts/root.html` includes on **every** layout. On any layout inheriting
1023
+ `root` directly (admin, stats, 404, section, home, book-*) the toggle rendered
1024
+ pointing at an offcanvas that was never in the document: clicking did nothing,
1025
+ and assistive tech followed `aria-controls` to a missing element. Measured
1026
+ across a 415-page build: **19 pages with a dangling `#bdSidebar`** (the French
1027
+ `/about/**` tree) and **18 with a dangling `#tocContents`** (404, CHANGELOG,
1028
+ `_design-system/**`) — **0 after**. `navigation/sidebar-config.html` now
1029
+ publishes `sidebar_offcanvas_rendered`, and both controls are gated on it.
1030
+ `footer-fabs.html`'s previous **denylist** of layouts to skip is replaced by
1031
+ that positive gate: a denylist fails open, and this one had already fallen
1032
+ behind (`404` and the `book-*` layouts were missing). The layout list is pinned
1033
+ against the real inheritance graph by
1034
+ `test/test_core.sh :: test_sidebar_offcanvas_layout_gate`, which walks every
1035
+ `_layouts/*.html` and fails if the two disagree in either direction.
1036
+
1037
+ - **Seven footer links had no accessible name on mobile (#435)** — the "Follow
1038
+ Us" social links and the RSS link render an `<i>` that is `aria-hidden="true"`
1039
+ beside a `<span class="d-none d-md-inline">` label. Below the `md` breakpoint
1040
+ the span is `display:none`, so it leaves the accessibility tree too and the link
1041
+ was announced as a bare "link" — measured: **7 nameless links at 375px**. Each
1042
+ now carries an `aria-label` matching its visible text exactly, so the name is
1043
+ correct on mobile and still contains the visible label at `md`+ (WCAG 2.5.3
1044
+ Label in Name). Covered by `test/visual/core/accessible-names.spec.js`, which
1045
+ asserts at **mobile** width — the same assertions pass at desktop on the
1046
+ unfixed code, which is why the bug survived the existing suite.
1047
+ - **Search results were never announced to screen readers (#279)** — the modal
1048
+ rewrites its results container without moving focus, so assistive tech had no
1049
+ signal that anything had happened. A visually-hidden `role="status"`
1050
+ `aria-live="polite"` region now announces a concise count ("7 results for
1051
+ jekyll", or "12 of 40 results…" when the list is truncated), and "No results
1052
+ found." The **results list itself is deliberately not live**: announcing eight
1053
+ titles and their snippets on every keystroke would be unusable. `aria-atomic`
1054
+ keeps the summary a single message, and a repeated string is cleared first so an
1055
+ unchanged announcement is not swallowed as a no-op.
1056
+
1057
+ - **`<meta charset>` shipped ~26 KB into the document, past the spec's 1024-byte
1058
+ window (#372)** — the HTML spec only scans the first 1024 bytes for the
1059
+ character-encoding declaration. The tag sat in `_includes/core/head.html` below
1060
+ Google Tag Manager, console-capture, page-views, mermaid and nanobar — and below
1061
+ two multi-line HTML doc banners that shipped verbatim to production — so a
1062
+ served page declared its encoding at byte **26058**. Past the limit a browser
1063
+ stops looking and decodes with its locale default, mangling every non-ASCII byte
1064
+ until it re-parses; this theme ships UTF-8 throughout (`fr/**` translations,
1065
+ em-dashes, curly quotes), so that is a live corruption risk rather than a lint
1066
+ nit. The tag now sits at the very top of `_layouts/root.html` — measured at byte
1067
+ **318**, verified across all 400 built pages — and `root.html`'s own banner is a
1068
+ Liquid comment (stripped at build) rather than an HTML one, which is what freed
1069
+ the budget. Regression coverage in `test/visual/core/head-contract.spec.js`
1070
+ measures the **served bytes**, since a source-order assertion would have passed
1071
+ both before and after.
1072
+ - **The Atom feed was undiscoverable (#371)** — `jekyll-feed` generates
1073
+ `/feed.xml` but emits no autodiscovery `<link>` unless `feed_meta` is called,
1074
+ which the theme never did, so readers and crawlers could not find the feed even
1075
+ though the footer linked it. `<head>` now carries
1076
+ `<link rel="alternate" type="application/atom+xml">`, honouring `site.feed.path`.
1077
+ Written out directly rather than via `feed_meta` so a consumer site that has not
1078
+ enabled the plugin does not hard-fail with `Unknown tag`. A test asserts the
1079
+ advertised URL actually returns a feed — a discoverable link to a 404 would be
1080
+ worse than none.
1081
+
199
1082
  - **Footer "Info" and "Cookie Preferences" are buttons, not links (#320)** —
200
1083
  both controls open in-page UI (the `#info-section` offcanvas and the
201
1084
  `#cookieSettingsModal` modal) but shipped as `<a href="#" data-bs-toggle>`, so
@@ -231,7 +1114,8 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
231
1114
  `html.no-js .foo {…}` / `html.js .foo {…}` got the no-JS branch for everyone
232
1115
  — the class advertised a capability the theme did not deliver. An inline
233
1116
  script is now the **first child of `<head>`**, above
234
- `{% include core/head.html %}` (which opens with Google Tag Manager, so
1117
+ {% raw %}`{% include core/head.html %}`{% endraw %} (which opens with Google Tag
1118
+ Manager, so
235
1119
  "first in that include" is not "first in `<head>`") and deliberately neither
236
1120
  `defer` nor `async`, since it has to run before the first stylesheet is
237
1121
  fetched or `html.js` rules would flash. The served HTML still carries