jekyll-theme-zer0 1.28.0 → 1.30.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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1143 -13
  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 +507 -2
  7. data/_data/consumers.yml +157 -9
  8. data/_data/features.yml +303 -20
  9. data/_data/feedback_types.yml +17 -12
  10. data/_data/i18n/fr.yml +12 -7
  11. data/_data/i18n/manifest.yml +39 -12
  12. data/_data/ingredient_densities.yml +122 -0
  13. data/_data/landing.yml +5 -2
  14. data/_data/navigation/main.yml +16 -0
  15. data/_data/navigation/quickstart.yml +4 -0
  16. data/_data/recipe_courses.yml +64 -0
  17. data/_data/site_builder.yml +874 -0
  18. data/_data/theme-manifest.yml +160 -124
  19. data/_data/ui-text.yml +26 -0
  20. data/_includes/README.md +26 -2
  21. data/_includes/analytics/posthog.html +2 -2
  22. data/_includes/components/admin-links.html +2 -2
  23. data/_includes/components/admin-tabs.html +2 -2
  24. data/_includes/components/ai-chat.html +14 -11
  25. data/_includes/components/analytics-dashboard.html +8 -8
  26. data/_includes/components/author-bio.html +1 -1
  27. data/_includes/components/author-card.html +10 -2
  28. data/_includes/components/author-eeat.html +4 -4
  29. data/_includes/components/background-customizer.html +10 -10
  30. data/_includes/components/background-image.html +114 -0
  31. data/_includes/components/background-settings.html +28 -15
  32. data/_includes/components/collection-manager.html +5 -5
  33. data/_includes/components/component-showcase.html +13 -13
  34. data/_includes/components/config-editor.html +12 -12
  35. data/_includes/components/config-viewer.html +8 -8
  36. data/_includes/components/cookie-consent.html +15 -15
  37. data/_includes/components/cta-button.html +7 -2
  38. data/_includes/components/dev-shortcuts.html +7 -7
  39. data/_includes/components/env-dashboard.html +8 -8
  40. data/_includes/components/env-switcher.html +9 -9
  41. data/_includes/components/feature-card.html +2 -2
  42. data/_includes/components/halfmoon.html +2 -2
  43. data/_includes/components/info-section.html +42 -37
  44. data/_includes/components/js-cdn.html +15 -15
  45. data/_includes/components/language-toggle.html +168 -21
  46. data/_includes/components/mermaid.html +72 -435
  47. data/_includes/components/nanobar.html +5 -5
  48. data/_includes/components/nav-editor.html +2 -2
  49. data/_includes/components/nav-export.html +2 -2
  50. data/_includes/components/nav-overview.html +2 -2
  51. data/_includes/components/page-feedback.html +45 -30
  52. data/_includes/components/page-views-init.html +55 -0
  53. data/_includes/components/page-views.html +33 -0
  54. data/_includes/components/post-card.html +22 -22
  55. data/_includes/components/post-type-badge.html +2 -2
  56. data/_includes/components/powered-by.html +2 -2
  57. data/_includes/components/preview-image.html +6 -0
  58. data/_includes/components/quick-index.html +2 -2
  59. data/_includes/components/recipe-card.html +67 -0
  60. data/_includes/components/recipe-duration.html +50 -0
  61. data/_includes/components/recipe-grams.html +58 -0
  62. data/_includes/components/recipe-index.html +96 -0
  63. data/_includes/components/recipe-ingredients.html +90 -0
  64. data/_includes/components/recipe-meta.html +96 -0
  65. data/_includes/components/recipe-nutrition.html +57 -0
  66. data/_includes/components/recipe-qty.html +73 -0
  67. data/_includes/components/recipe-ratio.html +151 -0
  68. data/_includes/components/recipe-scaler.html +73 -0
  69. data/_includes/components/recipe-steps.html +86 -0
  70. data/_includes/components/recipe-temp.html +45 -0
  71. data/_includes/components/search-modal.html +29 -4
  72. data/_includes/components/searchbar.html +2 -2
  73. data/_includes/components/shortcuts-modal.html +3 -0
  74. data/_includes/components/svg-background.html +2 -2
  75. data/_includes/components/theme-customizer.html +2 -2
  76. data/_includes/components/theme-info.html +14 -7
  77. data/_includes/components/theme-preview-gallery.html +22 -22
  78. data/_includes/content/giscus.html +2 -2
  79. data/_includes/content/intro.html +8 -8
  80. data/_includes/content/jsonld-faq.html +2 -2
  81. data/_includes/content/jsonld-software.html +24 -5
  82. data/_includes/content/seo.html +4 -4
  83. data/_includes/content/sitemap.html +27 -27
  84. data/_includes/content/toc.html +183 -183
  85. data/_includes/core/branding.html +6 -6
  86. data/_includes/core/console-capture.html +32 -74
  87. data/_includes/core/favicon.html +49 -7
  88. data/_includes/core/footer-fabs.html +17 -3
  89. data/_includes/core/footer.html +49 -34
  90. data/_includes/core/head.html +110 -86
  91. data/_includes/core/header.html +76 -54
  92. data/_includes/docs/bootstrap-docs.html +8 -8
  93. data/_includes/landing/landing-install-cards.html +2 -2
  94. data/_includes/landing/landing-quick-links.html +1 -1
  95. data/_includes/navigation/admin-nav.html +2 -2
  96. data/_includes/navigation/nav-tree.html +8 -8
  97. data/_includes/navigation/navbar.html +12 -12
  98. data/_includes/navigation/section-sidebar.html +109 -27
  99. data/_includes/navigation/sidebar-config.html +36 -2
  100. data/_includes/navigation/sidebar-left.html +17 -16
  101. data/_includes/navigation/sidebar-right.html +8 -7
  102. data/_includes/obsidian/full-graph.html +2 -2
  103. data/_includes/setup/claude-session.html +72 -0
  104. data/_includes/setup/prereq-checklist.html +90 -0
  105. data/_includes/setup/wizard.html +924 -222
  106. data/_includes/stats/stats-categories.html +8 -8
  107. data/_includes/stats/stats-header.html +14 -14
  108. data/_includes/stats/stats-metrics.html +14 -14
  109. data/_includes/stats/stats-no-data.html +12 -12
  110. data/_includes/stats/stats-overview.html +6 -6
  111. data/_includes/stats/stats-tags.html +8 -8
  112. data/_layouts/404.html +38 -24
  113. data/_layouts/README.md +2 -0
  114. data/_layouts/admin.html +24 -24
  115. data/_layouts/article.html +43 -33
  116. data/_layouts/author.html +20 -20
  117. data/_layouts/authors.html +2 -2
  118. data/_layouts/book-abc.html +12 -12
  119. data/_layouts/book-story.html +15 -15
  120. data/_layouts/book.html +12 -12
  121. data/_layouts/collection.html +33 -33
  122. data/_layouts/cookbook.html +88 -0
  123. data/_layouts/default.html +31 -28
  124. data/_layouts/home.html +23 -23
  125. data/_layouts/index.html +10 -10
  126. data/_layouts/landing.html +17 -17
  127. data/_layouts/news.html +44 -44
  128. data/_layouts/note.html +38 -38
  129. data/_layouts/notebook.html +34 -34
  130. data/_layouts/recipe.html +274 -0
  131. data/_layouts/root.html +92 -55
  132. data/_layouts/section.html +62 -33
  133. data/_layouts/setup.html +3 -3
  134. data/_layouts/sitemap-collection.html +49 -49
  135. data/_layouts/stats.html +40 -40
  136. data/_layouts/tag.html +12 -12
  137. data/_layouts/welcome.html +21 -21
  138. data/_sass/components/_callout.scss +1 -1
  139. data/_sass/components/_footer.scss +37 -1
  140. data/_sass/components/_mermaid.scss +375 -0
  141. data/_sass/components/_page-views.scss +36 -0
  142. data/_sass/components/_recipe.scss +506 -0
  143. data/_sass/components/_setup-wizard.scss +764 -0
  144. data/_sass/components/_ui-enhancements.scss +6 -6
  145. data/_sass/core/_navbar.scss +261 -46
  146. data/_sass/layouts/_landing.scss +2 -2
  147. data/_sass/layouts/_navbar-extras.scss +14 -4
  148. data/_sass/tokens/_color.scss +6 -0
  149. data/_sass/tokens/_index.scss +2 -0
  150. data/_sass/tokens/_radius.scss +21 -0
  151. data/_sass/tokens/_typography.scss +4 -0
  152. data/_sass/utilities/_focus.scss +14 -0
  153. data/assets/css/main.scss +4 -0
  154. data/assets/js/ai-chat.js +47 -5
  155. data/assets/js/fleet-feedback-capture.js +124 -0
  156. data/assets/js/fleet-feedback.js +853 -0
  157. data/assets/js/mermaid-diagrams.js +1267 -0
  158. data/assets/js/modules/navigation/config.js +9 -6
  159. data/assets/js/modules/navigation/navbar.js +55 -0
  160. data/assets/js/modules/navigation/scroll-spy.js +315 -80
  161. data/assets/js/modules/theme/appearance.js +8 -2
  162. data/assets/js/obsidian-wiki-links.js +8 -3
  163. data/assets/js/page-feedback.js +125 -192
  164. data/assets/js/page-views.js +372 -0
  165. data/assets/js/recipe-scaler.js +501 -0
  166. data/assets/js/search-modal.js +36 -0
  167. data/assets/js/setup-wizard.js +2279 -226
  168. data/assets/js/site-builder.js +1834 -0
  169. data/assets/js/ui-enhancements.js +11 -3
  170. data/scripts/README.md +44 -0
  171. data/scripts/ai/README.md +38 -0
  172. data/scripts/ai/api_call.rb +124 -0
  173. data/scripts/ai/usage.rb +314 -0
  174. data/scripts/ai/usage_report.rb +225 -0
  175. data/scripts/bin/audit-consumer +39 -7
  176. data/scripts/bin/giscus-discussions +213 -14
  177. data/scripts/bin/manifest +35 -12
  178. data/scripts/ci/agent_review_result.py +164 -0
  179. data/scripts/ci/test_agent_review_result.py +172 -0
  180. data/scripts/ci/test_visual_evidence_autogen.py +341 -0
  181. data/scripts/ci/visual_evidence_autogen.py +1060 -0
  182. data/scripts/content-review.rb +20 -1
  183. data/scripts/design-system-check.rb +170 -0
  184. data/scripts/lib/audit.sh +42 -2
  185. data/scripts/lint-liquid-raw.rb +137 -0
  186. data/scripts/test/integration/mermaid +22 -8
  187. data/scripts/test/lib/run_tests.sh +3 -1
  188. data/scripts/test/lib/test_agent_review_result.sh +27 -0
  189. data/scripts/test/lib/test_visual_evidence_autogen.sh +24 -0
  190. data/scripts/translate.rb +94 -16
  191. metadata +48 -2
data/CHANGELOG.md CHANGED
@@ -12,6 +12,1111 @@ 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.30.0](https://github.com/bamr87/zer0-mistakes/compare/v1.29.0...v1.30.0) (2026-09-12)
16
+
17
+
18
+ ### Features
19
+
20
+ * **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))
21
+ * **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))
22
+ * **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))
23
+ * **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))
24
+ * **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))
25
+ * **includes:** themed Mermaid figures + toolbar ([#455](https://github.com/bamr87/zer0-mistakes/issues/455)) ([96776fa](https://github.com/bamr87/zer0-mistakes/commit/96776fa48bbe7b63058d1d246962019c745d7b93))
26
+ * **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))
27
+
28
+
29
+ ### Bug Fixes
30
+
31
+ * **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))
32
+ * **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))
33
+ * **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))
34
+ * **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))
35
+ * **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))
36
+ * **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))
37
+ * **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)
38
+ * **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)
39
+ * **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))
40
+ * **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))
41
+ * **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)
42
+ * **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))
43
+
44
+
45
+ ### Performance Improvements
46
+
47
+ * **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))
48
+ * **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))
49
+
50
+ ## [Unreleased]
51
+
52
+ ### Changed
53
+
54
+ - **`claude-run` is now the fleet's shared `ai-runner` kit.** The composite
55
+ action and its new `scripts/ai/run.sh` are byte-identical copies of
56
+ lifehacker.dev's (the kit source of truth) instead of a hand-rolled variant.
57
+ What changes for the two callers (`issue-autopilot.yml`,
58
+ `visual-evidence-autogen.yml`): an AI call that was attempted and rejected —
59
+ revoked credential, exhausted quota, `is_error` payload, CLI install failure
60
+ — now fails the step with the reason as a `::error::` annotation, where the
61
+ old action exited 0 and a dead run read green; the OAuth-first rule is
62
+ enforced with `env -u ANTHROPIC_API_KEY`; the model override is the canonical
63
+ `AI_MODEL` (or the new `model` input) rather than `ZER0_AI_MODEL`, and
64
+ `max-turns` is a new input. Metering (`scripts/ai/usage.rb`,
65
+ `usage_report.rb`, prices in `_data/ai_pricing.yml`) and the Claude API
66
+ fallback (`scripts/ai/api_call.rb`) ride along as optional companions. The
67
+ exit-code contract is pinned by `scripts/ci/test_ai_runner.sh`, wired into
68
+ `./scripts/bin/test`. The action's six existing inputs are unchanged.
69
+ - **`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)).
70
+ - **Consumer registry corrections.** `ai-world-view/ai-world-view.github.io`
71
+ is `remote_theme_floating` (both its `_config.yml` and `hub.yml` pins are
72
+ untagged), not `remote_theme_pinned`; `amr-bash/bash-365.com` is registered
73
+ as a consumer (floating `remote_theme`, unconstrained gem on Azure, path gem
74
+ for dev).
75
+
76
+ ### Fixed
77
+
78
+ - **The weekly UI/UX audit was blind, and reported it as clean.** `sweep.mjs`
79
+ built its pages with `browser.newPage()`, which `@axe-core/playwright`
80
+ refuses; the throw was caught by a single per-route `try` that also discarded
81
+ the overflow, console-error and link-collection data already gathered for
82
+ that route. Screenshots still succeeded, so the harness looked alive while
83
+ every accessibility, console, overflow and broken-link result on every route
84
+ was silently dropped — and the report rendered the absence as "0 axe
85
+ violations / 0 broken links". Pages are now built from `browser.newContext()`,
86
+ each measurement fails independently, a scan that errored is reported as
87
+ UNKNOWN rather than clean, and a measurement that fails on every route turns
88
+ the sweep red the way a total capture failure already did. On the demo site
89
+ the sweep goes from 18/21 captured with 21 blackout entries to **21/21
90
+ captured, 0 harness errors, 191 links crawled and 8 genuinely broken links
91
+ found** ([#468](https://github.com/bamr87/zer0-mistakes/issues/468)).
92
+ - **Theme-skin buttons now announce which skin is applied.** The Theme Skin
93
+ group in Settings → Appearance conveyed selection only through the Bootstrap
94
+ `.active` class, which carries no accessibility semantics, so screen readers
95
+ announced nine identical unlabelled buttons — WCAG 2.1 SC 4.1.2 (Name, Role,
96
+ Value). Each button now ships `aria-pressed`, and the click and "Reset
97
+ background" handlers keep it in sync with `.active` through one shared
98
+ helper, matching the sibling color-mode group
99
+ ([#467](https://github.com/bamr87/zer0-mistakes/issues/467)).
100
+
101
+ ## [1.29.0](https://github.com/bamr87/zer0-mistakes/compare/v1.28.0...v1.29.0) (2026-09-01)
102
+
103
+
104
+ ### Features
105
+
106
+ * **analytics:** track and display per-page view counts ([#413](https://github.com/bamr87/zer0-mistakes/issues/413)) ([e488c4c](https://github.com/bamr87/zer0-mistakes/commit/e488c4c3101da78e98eecbf8299ce7c8705ed281))
107
+ * **design:** fill Fab, PostCard, Skeleton + motion/layers gaps in the design-system mirror ([#391](https://github.com/bamr87/zer0-mistakes/issues/391)) ([3b49741](https://github.com/bamr87/zer0-mistakes/commit/3b497417564b4c5e3d71eaa113cb136d2cdaf15c))
108
+ * **includes:** icon-only language trigger and a compact, positive menu ([#431](https://github.com/bamr87/zer0-mistakes/issues/431)) ([353bfa5](https://github.com/bamr87/zer0-mistakes/commit/353bfa5f3a8dd949ca6e513213a155d648218642)), closes [#406](https://github.com/bamr87/zer0-mistakes/issues/406)
109
+ * **includes:** live preview, vertical stepper and draft persistence in the setup wizard ([#432](https://github.com/bamr87/zer0-mistakes/issues/432)) ([73bdc55](https://github.com/bamr87/zer0-mistakes/commit/73bdc5547350c448e1617ec6cf5b9f49b10d65fb)), closes [#408](https://github.com/bamr87/zer0-mistakes/issues/408)
110
+ * **layouts:** add cookbook collection with recipe scaling and unit conversion ([#414](https://github.com/bamr87/zer0-mistakes/issues/414)) ([bb8eefa](https://github.com/bamr87/zer0-mistakes/commit/bb8eefa2beea0104fec28d173a1e57f369acada8))
111
+ * **pages:** publish the design system and page canvas on the live site ([#392](https://github.com/bamr87/zer0-mistakes/issues/392)) ([6cf73cd](https://github.com/bamr87/zer0-mistakes/commit/6cf73cdf2cca9ec02394b6ae1ba7e39c19969134))
112
+ * **sass:** align design tokens with the Claude Design system ([#389](https://github.com/bamr87/zer0-mistakes/issues/389)) ([7fc2fe0](https://github.com/bamr87/zer0-mistakes/commit/7fc2fe02683d652c0326536158bd48b3690df51f))
113
+
114
+
115
+ ### Bug Fixes
116
+
117
+ * **a11y:** dialog titles use a bare <h5>, skipping heading levels ([#387](https://github.com/bamr87/zer0-mistakes/issues/387)) ([98fd6f9](https://github.com/bamr87/zer0-mistakes/commit/98fd6f9e63dfb75c58efc3b15a642fb3e4c4095b))
118
+ * **ci:** fail the content review when it did not actually run ([#424](https://github.com/bamr87/zer0-mistakes/issues/424)) ([f048cb2](https://github.com/bamr87/zer0-mistakes/commit/f048cb22e565b3122d6682c026e13521b6fe52d9)), closes [#418](https://github.com/bamr87/zer0-mistakes/issues/418)
119
+ * **ci:** fail the Pages build on Liquid errors, unmangle two live pages ([#439](https://github.com/bamr87/zer0-mistakes/issues/439)) ([afd2ec8](https://github.com/bamr87/zer0-mistakes/commit/afd2ec83879a793d77dfac21b41d74d5204126c4))
120
+ * **ci:** make the pixel gate see the data files that move pixels ([#425](https://github.com/bamr87/zer0-mistakes/issues/425)) ([02ea639](https://github.com/bamr87/zer0-mistakes/commit/02ea639b3152900682ca298bd172925133d10dea)), closes [#417](https://github.com/bamr87/zer0-mistakes/issues/417)
121
+ * **ci:** stop discarding good translations when one page fails ([#388](https://github.com/bamr87/zer0-mistakes/issues/388)) ([19cea46](https://github.com/bamr87/zer0-mistakes/commit/19cea462a7822d6db536fc9c23218abd4a13d830))
122
+ * **ci:** wait past the cold bundle install for Jekyll in the snapshot gate ([#426](https://github.com/bamr87/zer0-mistakes/issues/426)) ([de1d1ca](https://github.com/bamr87/zer0-mistakes/commit/de1d1ca426ce7a25d5a8548c4a696c0002877200))
123
+ * **cookie-consent:** text-dark on bg-body made privacy rights invisible in dark mode ([#386](https://github.com/bamr87/zer0-mistakes/issues/386)) ([9bde7df](https://github.com/bamr87/zer0-mistakes/commit/9bde7dfa13f426908e00e53f8e4013a049fd928d))
124
+ * **giscus:** comments were broken sitewide — add an end-to-end health check and correct the docs ([#378](https://github.com/bamr87/zer0-mistakes/issues/378)) ([45e222c](https://github.com/bamr87/zer0-mistakes/commit/45e222c3c2c5b7c507af4df2f5f0b48bc783801e))
125
+ * **includes:** footer Info/Cookie Preferences are buttons, not anchors ([#427](https://github.com/bamr87/zer0-mistakes/issues/427)) ([f9041b9](https://github.com/bamr87/zer0-mistakes/commit/f9041b98b8a267be59f560724821fd941090f52d)), closes [#320](https://github.com/bamr87/zer0-mistakes/issues/320)
126
+ * **layouts:** match section topic filters on whole tags, not substrings ([#420](https://github.com/bamr87/zer0-mistakes/issues/420)) ([338ad1d](https://github.com/bamr87/zer0-mistakes/commit/338ad1ddfb71c8d1dd9c48b36fb4634daf93f2de))
127
+ * **layouts:** move keyboard focus to the skip-link target (tabindex="-1") ([#395](https://github.com/bamr87/zer0-mistakes/issues/395)) ([f5db650](https://github.com/bamr87/zer0-mistakes/commit/f5db65048e48ca71eedebc04d02d051e0b3152a6)), closes [#278](https://github.com/bamr87/zer0-mistakes/issues/278)
128
+ * **layouts:** stop emitting dead sub-topic anchors in section sidebars ([#402](https://github.com/bamr87/zer0-mistakes/issues/402)) ([f50e790](https://github.com/bamr87/zer0-mistakes/commit/f50e790c4bfd56fefa4a74d7876de90c9776c689))
129
+ * **layouts:** upgrade the no-js class to "js" so the hook works ([#428](https://github.com/bamr87/zer0-mistakes/issues/428)) ([a2d31f5](https://github.com/bamr87/zer0-mistakes/commit/a2d31f5533a4fb4793042e32326c1a78c9954268)), closes [#319](https://github.com/bamr87/zer0-mistakes/issues/319)
130
+ * **navigation:** fit all nav items, full-bleed bar, language into Settings ([#423](https://github.com/bamr87/zer0-mistakes/issues/423)) ([689006e](https://github.com/bamr87/zer0-mistakes/commit/689006e7654e900a4509c30539fcf12bd7f397c6))
131
+ * **scripts:** correct consumer auditor, plugin contract, and JSON-LD scope ([#400](https://github.com/bamr87/zer0-mistakes/issues/400)) ([b466cad](https://github.com/bamr87/zer0-mistakes/commit/b466cad526501e7e7a0eef5f24a12c5bbd181dc7))
132
+ * **scripts:** recover from a rejected Anthropic credential, and link the cookbook ([#419](https://github.com/bamr87/zer0-mistakes/issues/419)) ([fac6bee](https://github.com/bamr87/zer0-mistakes/commit/fac6beeb7af7dcae96f727bf0cf581b080e95f15))
133
+ * **ui-audit:** serve without --detach so the sweep can capture, and fail loudly when it cannot ([#434](https://github.com/bamr87/zer0-mistakes/issues/434)) ([fac6415](https://github.com/bamr87/zer0-mistakes/commit/fac641574c951b2887d94f8b39561fb8b6f8fe8c)), closes [#321](https://github.com/bamr87/zer0-mistakes/issues/321)
134
+
135
+ ## [Unreleased]
136
+
137
+ ### Added
138
+
139
+ - **Bring your own AI provider — Claude or Grok — and an open, coding-session-style
140
+ Site Builder (ZER0-087)** — the chat proxy gains a provider layer
141
+ (`templates/deploy/chat-proxy/providers.js`): xAI's Grok is a first-class
142
+ provider (`XAI_API_KEY`, `CHAT_PROVIDER`, `XAI_CHAT_MODEL`) for the chat
143
+ widget, the feedback triage and the Site Builder, translated server-side to
144
+ and from the Anthropic Messages dialect so no client changed. The Site
145
+ Builder's Connect step shows which providers the local dev proxy already has
146
+ keys for (masked), takes a token for the session (sent once to localhost,
147
+ kept in the proxy's memory, optionally saved to `.env` with mode 600), offers
148
+ the model catalog, an image renderer (Grok Imagine / OpenAI Images) and two
149
+ session modes: the guided nine steps, or an open session that can open an
150
+ existing site under the target root, read/edit/write/delete its files, run
151
+ `git status`/`diff`/`log`, generate images into `assets/`, run `jekyll build`
152
+ inside the container and stop a long tool run. Generated sites are
153
+ pre-configured for the chosen provider (`ai_chat.provider`, `preview_images`,
154
+ `.env.example`). The dev proxy now starts without a credential. Also fixes a
155
+ pre-existing 162px horizontal overflow of the whole `/setup/` page at phone
156
+ widths (the Connect step's command boxes could not shrink). (evidence:
157
+ [`test/visual/evidence/site-builder-providers/`](test/visual/evidence/site-builder-providers/README.md)
158
+ — Connect step: 2 provider cards, 2 session modes, token field is a password
159
+ input inert offline; mocked proxy: "needs a token" → "Grok connected"; Build:
160
+ existing sites listed, Open sets the target; mobile overflow 162px → 0px)
161
+ - **CI now produces a UI pull request's visual artifacts instead of only
162
+ checking for them (ZER0-085)** — `visual-evidence-autogen.yml` renders every
163
+ same-repo PR in the same jammy Playwright image the snapshot gate uses, runs
164
+ the PR's `test/visual/*-evidence.mjs` generators (or the new generic
165
+ base-vs-head generator `test/visual/pr-evidence.mjs`, which renders the base
166
+ branch and the head side by side), verifies the 9-skin baselines, and pushes
167
+ the montages + `metrics.json` to the branch. Stale baselines are refreshed
168
+ **only** when the new `visual-evidence-reviewer` agent, having viewed the
169
+ expected | actual | diff montage, judges the diff to be the change the PR
170
+ describes — a code step disposes, the model only proposes ([#417](https://github.com/bamr87/zer0-mistakes/issues/417)
171
+ is why). The evidence gate now requires generated proof (a README alone no
172
+ longer passes), `ci-self-repair` leaves a red `Visual Snapshots` job to this
173
+ lane, and `test/update-snapshots.sh` gained `PRE_TEST_SCRIPT` /
174
+ `POST_TEST_SCRIPT` / `SKIP_PLAYWRIGHT` hooks. Closes the gap that kept
175
+ [#454](https://github.com/bamr87/zer0-mistakes/pull/454) red: its authoring
176
+ agents ran where Docker was gated, and nothing in CI could render what they
177
+ could not. Kill switch: repo variable `VISUAL_EVIDENCE_AUTOGEN_ENABLED=false`.
178
+ The lane restores its own tooling from the base branch before it runs, so it
179
+ works on branches cut before it shipped: without that its first real run on
180
+ #454 died in 20 seconds (that branch has no copy of the orchestrator), and a
181
+ branch carrying an older `update-snapshots.sh` would have silently generated
182
+ nothing at all.
183
+ - **Site Builder — a Claude-guided setup wizard that ends with a running site
184
+ (ZER0-086, extends ZER0-067)** — `/setup/` (and the `welcome` layout) grew
185
+ from a five-step `_config.yml` form into a nine-step builder: Connect →
186
+ Prerequisites → Identity → URLs → Structure → Appearance → Voice →
187
+ Integrations → Build. An embedded Claude session rides alongside every step
188
+ through the local dev proxy (Claude Code OAuth, `claude setup-token`; the
189
+ token never reaches the page). It sees the whole wizard state on every turn
190
+ and acts on it with tools: fill in fields, override a generated file, run
191
+ live prerequisite checks (Docker, Git, gh, VS Code, Node, Claude CLI — a
192
+ fixed command table), read the theme's real source and search the docs,
193
+ resolve a project folder, write the generated project, run `docker compose
194
+ up|ps|logs|down`, and check the new site answers — each mutation behind a
195
+ confirmation card. The form half still works with no proxy at all (GitHub
196
+ Pages included): it generates `_config.yml`, `_config_dev.yml`, `Gemfile`,
197
+ `docker-compose.yml`, `index.md`, navigation, about page, welcome post, one
198
+ index page per collection, `.gitignore`, `zer0.install.yml`, `.env.example`
199
+ and `README.md`, with per-file download and a self-extracting bash bundle.
200
+ Forms gained a site brief, a URL **Suggest** button, a site-type quick-pick,
201
+ a navigation row editor, skin cards with a live **Preview on this page**
202
+ toggle, tone/audience pickers, integration switches with conditional
203
+ sub-fields, a copy button on every command, and a toast for feedback. New:
204
+ `_data/site_builder.yml` (steps, prerequisites mirroring
205
+ `machine-setup.md`, catalogs, framework brief),
206
+ `templates/deploy/chat-proxy/wizard-store.mjs` (the dev-only sandbox behind
207
+ `/api/wizard/*`), `assets/js/site-builder.js`, `_includes/setup/{claude-
208
+ session,prereq-checklist}.html`, the `site_builder:` config block, a
209
+ quickstart page and a feature reference. (evidence:
210
+ [`test/visual/evidence/site-builder/`](test/visual/evidence/site-builder/README.md))
211
+ A first recorded end-to-end run then shaped the build half: the generated
212
+ `docker-compose.yml` publishes LiveReload on the site's port + 1 (a fixed
213
+ 35729 collided with the theme's own dev container and the new site never
214
+ started) and shares one gem cache across generated sites; every enabled
215
+ collection now ships a valid starter document (a doc, a quickstart step, a
216
+ note with a wiki-link, a structured recipe) and the cookbook index uses the
217
+ theme's `cookbook` layout over the real `recipes` collection; `_config.yml`
218
+ sets `collections_dir: pages` (without it every collection but posts was
219
+ invisible to Jekyll); each site gets its own `assets/images/logo.svg`
220
+ monogram and gem-based sites turn the SVG background layers off, because
221
+ the published gem ships no theme images; a revoked Claude credential now
222
+ says so and names the fix. Validation is a scenario
223
+ runner, `test/visual/site-builder-walkthrough.mjs`, that samples random
224
+ briefs per site type (`test/visual/site-builder-scenarios.mjs`), drives the
225
+ wizard on video, builds the site with Docker and asserts its routes, title
226
+ and skin — replayable by seed.
227
+ The builder then gained a **schema-driven site plan**: `plan_schema` in
228
+ `_data/site_builder.yml` (a JSON-Schema subset validated in the browser)
229
+ describes what the agent may produce — a landing page (five templates,
230
+ nine section types, hero + CTAs), navigation shape (flat or grouped
231
+ dropdowns, sidebar none/auto/docs tree), theme overrides (nine palettes or
232
+ custom colours, seven font pairings, three corner radii) and up to twelve
233
+ example pages with Markdown bodies — and the generators turn it into
234
+ `index.md` (a Liquid landing engine) + `_data/landing.yml`,
235
+ `_data/navigation/main.yml` and `docs.yml`, `assets/css/user-overrides.css`
236
+ + `_includes/custom/head.html`, and one file per page. Claude submits plans
237
+ with `set_site_plan` behind a confirmation card; the Structure and
238
+ Appearance steps expose the same choices as controls, with a page planner
239
+ and a **Preview on this page** toggle that applies the generated overrides
240
+ to the wizard itself. The scenario runner randomises the plan too and
241
+ asserts the built site's landing template, palette colour, web fonts and
242
+ planned routes.
243
+ - **Mermaid diagrams are now accessible figures with a toolbar (ZER0-013)** —
244
+ every ```` ```mermaid ```` fence (and legacy `<div class="mermaid">`) renders
245
+ as a `<figure>` with a rendered SVG and a small toolbar: zoom out / in / reset
246
+ (50–400 %, real layout — the SVG is resized, so the frame scrolls and
247
+ drag-to-pan, `Ctrl`+wheel and the `+` / `-` / `0` keys all work), **View
248
+ fullscreen** (a native `<dialog>` — the fix for a wide diagram shrunk to
249
+ illegibility on a phone; `Esc` closes it and focus returns to the opener),
250
+ **Copy diagram source**, and **Download as SVG** (page background baked in so
251
+ a dark-mode export stays readable). On pointer devices the toolbar floats over
252
+ the top-right corner on hover/focus; on touch devices it is a static row above
253
+ the diagram so it never covers it. `accTitle` becomes the visible
254
+ `<figcaption>` and the diagram's accessible name, `accDescr` its `<desc>`,
255
+ and the scrollable frame is a focusable, labelled region. A syntax error no
256
+ longer blanks the block: the figure shows the parse message, a hint, and the
257
+ source in a `<details>`, with *copy* still enabled. Colours are **derived from
258
+ the live design tokens** (`--bs-primary`, `--bs-body-bg`, `--zer0-color-*`)
259
+ through Mermaid's `base` theme, so diagrams follow the colour mode, the
260
+ selected skin and any `theme_color` override, and re-render in place when
261
+ those change; dark mode is decided by background luminance, so wizard mode and
262
+ dark skins get legible ink, and a contrast guard keeps nodes visible when the
263
+ brand equals the page colour. Pie slices, git branches and mind-map branches
264
+ get a 12-colour series fanned out from the brand hue. `securityLevel` now
265
+ defaults to `strict` (`mermaid.security_level: loose` restores `click`
266
+ callbacks / HTML labels); `mermaid.toolbar`, `fullscreen` and `download` can
267
+ switch the controls off. Implemented as `_includes/components/mermaid.html`
268
+ (loader: config JSON + two `defer` scripts — the 3.3 MB bundle used to load
269
+ synchronously in `<head>`) + `assets/js/mermaid-diagrams.js` +
270
+ `_sass/components/_mermaid.scss`, replacing ~400 lines of inline
271
+ `<script>`/`<style>` in the include; the `!important` SVG overrides that
272
+ flattened `classDef`/`style` colours and pie slices are gone. Toolbar strings
273
+ are `diagram_*` keys in `_data/ui-text.yml`. The docs page
274
+ (`/docs/features/mermaid-diagrams/`) now renders a live example of every
275
+ diagram type, the caption directive, per-node styling and the error state.
276
+ Guarded by `test/visual/features/mermaid.spec.js` (12 tests, smoke tier)
277
+ (evidence: [`test/visual/evidence/mermaid/`](test/visual/evidence/mermaid/README.md)
278
+ — dark-mode toggle keeps the SVG (before: SVG lost, stylesheet text on
279
+ screen); page overflow 0px at 320/390px; 11/12 docs diagrams rendered, 1
280
+ shown as an error card with its source kept).
281
+
282
+ ### Fixed
283
+
284
+ - **Generated posts and notes were invisible on GitHub Pages** — every dated
285
+ document the wizard wrote was stamped `T09:00:00.000Z` on the day it was
286
+ generated. Jekyll refuses to publish future-dated documents unless
287
+ `future: true`, and GitHub Pages builds with the default, so a site generated
288
+ before 09:00 UTC went live with its posts and notes missing: linked from the
289
+ collection index and listed in the sitemap, but 404 when clicked. Generated
290
+ content is now stamped with the actual moment it was written, and a
291
+ regression test fails if any generated document carries a future date.
292
+ Found by publishing two example sites and clicking the links.
293
+ - **Every page of every generated site 404'd on `user-overrides.js`** — the
294
+ Site Builder writes `user_overrides: true` so the theme loads the palette and
295
+ font overrides it generates, but that same flag also makes the theme load
296
+ `assets/js/user-overrides.js`, which the wizard never created. Generated
297
+ sites now ship a commented stub, so the request resolves and site owners have
298
+ an obvious place to put their own JavaScript. Found while publishing example
299
+ sites built by the wizard.
300
+ - **Every generated landing page shipped an invisible button** —
301
+ `components/cta-button.html` mapped `variant: outline` to `btn-outline-light`,
302
+ which is white text on a white border, and the Site Builder's own default
303
+ call to action uses `outline` on a light hero. `outline` is now an outlined
304
+ *primary* button; `outline-light` is the explicit variant for a dark surface,
305
+ and the generated landing engine picks between them from `hero.variant`.
306
+ The theme's own homepage hero is `bg-primary text-white`, so its two
307
+ outlined buttons moved to `outline-light` and render exactly as before —
308
+ a consumer who copied `_data/landing.yml` into a site with a dark hero
309
+ should make the same one-word change.
310
+ - **A planned `landing.hero.image` was accepted and then thrown away** — the
311
+ site plan's schema has offered `landing.hero.image` since the Site Builder
312
+ shipped, but the landing engine it generates never rendered it, so an agent
313
+ (or a human) could set a hero image, see it validated, watch it land in
314
+ `_data/landing.yml`, and get a landing page with no picture. `index.md` now
315
+ renders it inside the hero, decorative (empty `alt`) so a screen reader does
316
+ not hear the headline twice. Found by building a site end to end through the
317
+ new Grok path, where the "Hero image with …" shortcut pointed straight at the
318
+ dead field.
319
+ - **A partial hero patch silently wiped the rest of the hero** —
320
+ `set_site_plan` replaced `landing.hero` wholesale, so the natural agent move
321
+ of "write the copy now, add the artwork two turns later"
322
+ (`{landing: {hero: {image}}}`) discarded the headline, subheadline and every
323
+ call to action, and the landing page quietly reverted to the site title and
324
+ the default buttons. The hero now merges field by field; `sections`,
325
+ `navigation.items` and `pages` still replace, because a shorter list has to
326
+ mean a shorter list. The tool description says so, and the regression test
327
+ pins both halves.
328
+
329
+ - **A new post appeared at its own URL but on no index** — the generated
330
+ `_config_dev.yml` turned on Jekyll's experimental incremental regeneration,
331
+ which rebuilds only documents whose own source changed. Adding a post left
332
+ the collection index, the home page's latest-posts list and the feed showing
333
+ the old set, with the post reachable only by typing its URL. New sites are
334
+ now generated with `incremental: false`; a full rebuild of a fresh site takes
335
+ about a second and is always right.
336
+
337
+ - **The 15-second status poll handed the composer back mid-turn** — the Site
338
+ Builder's proxy poll rebuilt the panel's enabled state from the connection
339
+ alone, so during a long action (a Docker build, an image render) the input
340
+ and Send button re-enabled themselves and the Stop button vanished, while
341
+ tools were still running. A second message could then interleave with the
342
+ first. The poll now respects the busy state.
343
+ - **A newly added post never appeared on the running site** — Jekyll's
344
+ `--watch` only tracks collection documents that existed when `serve` started,
345
+ so a page or post the assistant added to a running project stayed invisible
346
+ with no explanation. `write_project_file` now says so in its result and
347
+ offers a restart, and `run_compose` gained a `restart` action.
348
+
349
+ - **Switching colour mode destroyed every Mermaid diagram** — the old include's
350
+ `MutationObserver` re-render emptied each `.mermaid` div and refilled it with
351
+ the div's *current* text, which after the first render is the SVG's own
352
+ stylesheet, and never cleared Mermaid's `data-processed` flag, so
353
+ `mermaid.run()` skipped the div. Toggling dark mode on `/quickstart/`,
354
+ `/about/` or `/docs/ruby-101/` replaced the diagram with a wall of
355
+ `#mermaid-… {font-family: …}` text and a permanent "Loading diagram…" label
356
+ (reproduced headlessly on `main`: `svgs: 1 → 0` after the toggle). Sources are
357
+ now kept per figure and every render goes through `mermaid.render()`, so a
358
+ mode or skin change re-renders from the original definition — asserted by the
359
+ regression test in `test/visual/features/mermaid.spec.js`.
360
+ - **The Obsidian wiki-link resolver rewrote the inside of inline SVGs** —
361
+ `assets/js/obsidian-wiki-links.js` skips text under `CODE`/`PRE`/`A`/
362
+ `SCRIPT`/`STYLE` by comparing `nodeName`, but elements in the SVG namespace
363
+ report it in lower case (`style`, `text`), so a Mermaid diagram's own
364
+ `<style>` was walked and every `#id` selector in it was turned into a
365
+ `<a class="obsidian-tag">` link. The stylesheet lost its scoping: one
366
+ diagram's `#arrowhead path { fill }` became a bare `path { fill }` for the
367
+ whole page, and pie slices rendered dark grey while their legend showed the
368
+ right colours. The old include escaped it only because the resolver
369
+ special-cases the `.mermaid` class. The name check is now case-insensitive
370
+ and any `<svg>` subtree (graphics, never prose) is skipped, with
371
+ `.zer0-diagram` added to the class exemptions. `test/test_resolver.js` gains
372
+ a fixture with a lower-case `style`/`text` inside `<svg>`, and the Mermaid
373
+ spec asserts a pie slice's computed fill equals its own attribute and that no
374
+ unscoped `path` rule exists on the page.
375
+
376
+ ### Changed
377
+
378
+ - **The page-feedback widget now files the fleet's issue contract, not its own
379
+ (UPS-FB).** The theme built its own issue body, its own escaping, and its own
380
+ URL-length trimming — and so did the 404 page and the AI chat, three builders
381
+ in one repo with three different behaviours. All three now go through
382
+ `FleetFeedbackCore` in `assets/js/fleet-feedback.js`, vendored byte-identically
383
+ from the fleet kit (`bamr87/bamr87` `templates/feedback/`, held to the hub's
384
+ copy by the drift gate). It is the same builder the fleet's `<fleet-feedback>`
385
+ web component uses, so an issue filed from a page here and one filed from a
386
+ React app elsewhere are byte-identical to the pipeline that triages them.
387
+
388
+ What that adds to every filed issue: sections in a fixed order, a
389
+ `<!-- fleet-feedback v1 type=... -->` marker the issue pipeline reads to
390
+ recognise an already-structured report, and a real over-budget path. The AI
391
+ chat in particular used to `.slice(0, 6000)` its body — silently dropping
392
+ whatever Claude had written past that point — and now trims by section and
393
+ hands the full text to the clipboard.
394
+
395
+ The theme keeps its Bootstrap modal and its AI triage step; only the body
396
+ assembly moved. Two UIs, one contract.
397
+
398
+ - **Request-type labels map onto the fleet issue-pipeline taxonomy.**
399
+ `enhancement`/`documentation` become `feature`/`docs`; `area:docs`/`area:feat`
400
+ drop out. Every label the widget applies exists in this repo (GitHub silently
401
+ drops the ones that do not), and a widget-filed issue is now pipeline-eligible
402
+ on the next scan. The zero-`_data` fallback taxonomy carries them too, so
403
+ `remote_theme` consumers benefit the moment those labels exist in their repo.
404
+
405
+ - **Captured console lines are redacted before they enter the buffer.** Bearer
406
+ tokens, API keys, JWTs, GitHub tokens and email addresses are masked on the
407
+ way in, so a secret cannot be previewed, copied, or filed even by accident.
408
+ Credentials reach the console more often than anyone expects — an
409
+ `Authorization` header logged by a fetch wrapper, a signed URL in a 403.
410
+
411
+ - **The capture buffer hooks `console.warn` and `console.error` only.**
412
+ Previously `log`, `info` and `debug` were captured too. At a 40-entry ring,
413
+ debug chatter evicts the one line that explains the failure — which is the
414
+ line a report exists to carry. `_includes/core/console-capture.html` is now a
415
+ two-line loader for the vendored buffer rather than 60 lines of inline script,
416
+ which also *shrinks* `<head>` and buys back room for the charset meta that has
417
+ to land in the first 1024 bytes (#372).
418
+
419
+ ### Fixed
420
+
421
+ - **The 404 page files a report a maintainer can act on.** "Submit an Issue on
422
+ GitHub" opened a hand-built form containing two sentences of boilerplate — no
423
+ page context, no environment, no captured errors. It now opens the feedback
424
+ widget pre-typed as `fix-page` with the URL that failed (and the page that
425
+ linked to it) already in the description.
426
+
427
+ - **The feedback FAB survives its script failing to load.** It was a `<button>`
428
+ whose only behaviour came from JavaScript; with the widget now depending on a
429
+ second script, a 404 on either one would have left a decorative circle in the
430
+ corner. It is an anchor to the issue form, upgraded in place when the script
431
+ runs — the same progressive enhancement the inline "Improve" link already had.
432
+
433
+ - **The page-feedback Playwright spec dismisses the cookie-consent banner.** The
434
+ banner is a full-width bar on a layer above the FAB stack, so it swallowed
435
+ every click aimed at the FAB. Every other spec that touches lower-screen chrome
436
+ already seeded the consent choice through the shared fixture; this one never
437
+ did.
438
+
439
+ ### Changed
440
+
441
+ - **TOC scroll spy now bolds the section you are actually reading** — the
442
+ highlighted entry in the right-hand Table of Contents jumped around and sat
443
+ one to three sections ahead of the viewport. Measured on `/docs/features/toc/`
444
+ at 1280×820, the wrong entry was bolded at **19–23 of 25 scroll positions**
445
+ — the count moves between runs because which implementation won depended on
446
+ callback timing, which is the bug in one sentence.
447
+
448
+ Three implementations were toggling `.active` on the same
449
+ `#TableOfContents a` links, so whichever fired last won. Bootstrap's native
450
+ ScrollSpy was wired up twice — `data-bs-spy="scroll"
451
+ data-bs-target="#TableOfContents"` on `<body>` in `_layouts/root.html` *and*
452
+ on `.bd-main` in `_layouts/default.html` (whose `data-bs-offset="100"` had
453
+ been dead since Bootstrap 5.2 replaced that option with
454
+ `data-bs-root-margin`). `assets/js/ui-enhancements.js` ran a third observer
455
+ over *every* `a[href^="#"]` on the page, clearing `.active` from all of them
456
+ each time a `section[id]` intersected. Both Bootstrap hooks are removed —
457
+ the `<body>` one was also stripping server-rendered `.active` classes off
458
+ admin sidebar links, which `fixtures.gotoBeforeScrollSpy` exists to work
459
+ around — and the `ui-enhancements.js` observer is now scoped away from TOC
460
+ and sidebar links.
461
+
462
+ The theme's own spy (`assets/js/modules/navigation/scroll-spy.js`) was wrong
463
+ on its own terms too: it asked IntersectionObserver for the "most visible"
464
+ heading, but headings are a few pixels tall, so every heading inside the
465
+ observer band reports the same `intersectionRatio` — the winner was whichever
466
+ entry happened to be in that callback's batch, and a heading scrolling *out*
467
+ of the band triggered no re-evaluation at all. It now applies a positional
468
+ rule: the active heading is the last one whose top has crossed the reading
469
+ line (the document's `scroll-padding-top`, the same offset anchor navigation
470
+ uses), with the last heading winning once the page is scrolled to the bottom
471
+ so trailing sections shorter than the viewport are still reachable. The
472
+ answer is recomputed from scratch on each rAF-throttled scroll frame from
473
+ cached heading offsets, re-measured on resize and content reflow
474
+ (`ResizeObserver`), so it cannot drift out of sync.
475
+
476
+ Two smaller fixes ride along. Clicking a TOC entry now holds that entry
477
+ active while the smooth scroll animates, instead of flashing every heading
478
+ passed on the way. And keeping the active entry visible inside the TOC no
479
+ longer calls `scrollIntoView()`, which bubbles up and scrolls the *page* —
480
+ feeding straight back into the spy; it adjusts the TOC container's own
481
+ `scrollTop`, resolving that container at call time (`.bd-toc` on desktop,
482
+ `.offcanvas-body` on mobile) rather than assuming one. The active link also
483
+ carries `aria-current="true"`.
484
+
485
+ `config.scrollSpy.rootMargin`/`threshold` are replaced by
486
+ `config.scrollSpy.offset` (`null` = derive from `scroll-padding-top`) and
487
+ `tolerance`. Guarded by `test/visual/features/scroll-spy.spec.js` (smoke
488
+ tier), which is 5 failed / 1 passed against the pre-fix theme and 6 passed
489
+ with the fix. (evidence:
490
+ [`test/visual/evidence/scroll-spy/`](test/visual/evidence/scroll-spy/README.md)
491
+ — wrong TOC highlight at 19/25 scroll positions → 0)
492
+ - **Navbar controls no longer flicker under a stationary pointer (#404)** —
493
+ `.nav-link:hover`, the Search/Settings utility buttons and the two sidebar/TOC
494
+ FABs applied `transform: translateY(-1px)`. A 1px lift moves the element out
495
+ from under a pointer parked on the row it vacates, so hover exits, the element
496
+ drops back under the pointer, and hover re-enters — an infinite flicker with
497
+ no pointer movement at all. Nav items are thin enough for a 1px lift to
498
+ reliably do it.
499
+
500
+ Deleting those declarations was **not sufficient**, and the reason is the
501
+ substantive finding here: `_sass/components/_ui-enhancements.scss` applies
502
+ `transform: translateY(-2px)` to `.btn:hover` — *every* button in the theme.
503
+ The utility buttons and both FABs are `.btn`s, so with the navbar's own 1px
504
+ lift removed the buttons kept flickering on the inherited 2px one — measured
505
+ on the built site, the Settings button lost and regained hover 14 times in
506
+ 800ms with the pointer completely still. The fix therefore cancels it
507
+ explicitly with `transform: none`, the corrective pattern this repo already
508
+ uses in `_post-navigation.scss` and its `prefers-reduced-motion` blocks. The
509
+ FABs get the same treatment by the same reasoning — they are the same kind of
510
+ `.btn` with the same lift — but they were not rendered at any viewport reached
511
+ during verification, so that half is reasoned, not observed.
512
+
513
+ Background, colour and shadow transitions are untouched, and the controls keep
514
+ a pressed cue as `:active { transform: translateY(1px) }` — safe, because
515
+ `:active` only holds while the button is held down, so the pointer cannot
516
+ leave and re-enter in a loop.
517
+
518
+ Three corrections to the issue as filed. The vacated row is the **bottom**
519
+ edge, not the top — a probe parked on the top edge passes without exercising
520
+ the bug at all. Only a transform on the **hovered element's own box** can
521
+ flicker: `&:hover i { transform: scale(1.1) }` moves a child and never shrinks
522
+ the parent's hit box, so those went for consistency, not as causes. And
523
+ `.nav-link:hover`, named as the primary culprit, was **dead code** — its lift
524
+ sat inside `@media (min-width: 992px) { @container navbar-main (max-width:
525
+ 75rem) }`, and measured on the built site at 1040/1140/1200/1280px its
526
+ computed hover transform is `none` at every width, on `main` too. Every
527
+ flicker actually observed was on the utility buttons.
528
+
529
+ Guarded by `test/visual/core/hover-stability.spec.js`: with the pointer on the
530
+ control's centre the border box must be byte-identical to its resting box and
531
+ the computed transform must be `none`; with the pointer held half a pixel
532
+ inside the bottom edge for 800ms, `mouseleave` must stay at zero. The verdict
533
+ is the *computed* value rather than "no matching rule exists", because
534
+ `.btn:hover` does still match these controls and always will — it is
535
+ overridden, not deleted. `mouseleave`, not `mouseout`: the latter also fires
536
+ when the pointer crosses onto a descendant, which is not the element losing
537
+ hover, and it produces a false positive on every `.nav-hover-dropdown` parent.
538
+ Against `main` the spec is 4 failed / 1 passed; with the fix, 5 passed.
539
+ Evidence in `test/visual/evidence/hover-flicker/` — the Settings button lost
540
+ hover 14 times in 800ms before, 0 after.
541
+
542
+ Scoped to the navbar. The issue's original acceptance criterion — *no
543
+ transform in any `:hover` rule under `_sass/`* — is 42 rules across ~15 files
544
+ (card lifts, book-cover tilts, recipe and author hovers) and a visual redesign
545
+ of a published theme rather than a defect fix; it also flagged
546
+ `transform: none`, which is the corrective pattern this repo already uses in
547
+ `_post-navigation.scss` and its `prefers-reduced-motion` blocks. A theme-wide
548
+ motion policy belongs in its own task.
549
+ - **Mobile browser chrome now tracks the page surface (#281)** — `theme-color`
550
+ drives the iOS Safari address bar and the Chrome/Android task-switcher card.
551
+ It fell back to `theme_color.main`, which is the brand **accent** (`#007bff`
552
+ here), so a dark-first theme advertised a bright blue address bar; and being
553
+ config-gated it emitted **nothing at all** on `remote_theme` consumers, which
554
+ do not inherit this repo's `_config.yml` — the case the issue was originally
555
+ filed about. The include now emits scheme-aware tags sourced from
556
+ `--bs-body-bg` (light `#ffffff`, dark `#212529`, Bootstrap 5.3.3's own
557
+ surfaces) **with no configuration required**, and honors new optional
558
+ `favicon.theme_color_light` / `theme_color_dark` keys. A site that pins its
559
+ mode (`color_mode_default: dark`/`light`, or `color_mode_lock: true`) gets a
560
+ single unconditional tag instead, because `media="(prefers-color-scheme: …)"`
561
+ keys off the OS and a pair would hand light chrome to an OS-light visitor
562
+ reading a page the site renders dark. `favicon.theme_color` still pins one
563
+ color for both schemes, so existing consumer config is unchanged. Guarded by
564
+ four `@critical` tests in `test/visual/core/head-contract.spec.js` that
565
+ compare each value against the page's computed `--bs-body-bg` rather than a
566
+ constant, plus `test_theme_color_fallback_without_config` covering the
567
+ no-config consumer case.
568
+
569
+
570
+ - **Consumers no longer inherit the theme's own navigation (#332)** — `install.sh`
571
+ copied the theme's entire `_data/` into every consumer, so each site's *own*
572
+ `_data/navigation/*.yml` was a verbatim copy of the theme's and carried the
573
+ theme's page taxonomy. Measured on a real install: **106 of 129 seeded
574
+ navigation URLs pointed at pages the installer does not create** — ~47 per
575
+ doc page from `docs.yml` alone, which htmlproofer on a consumer counted as
576
+ ~1,598 broken internal links across ~34 docs, none of them removable from
577
+ consumer content. `_data/navigation/` is now held back from the theme copy
578
+ and seeded from `templates/data/navigation-*.yml.template`, whose links
579
+ resolve against the pages the installer actually creates: **0 of 26
580
+ unresolvable**. All seven files are covered, not just `docs.yml`. Because
581
+ `create_from_template` skips a file that already exists, a re-run or upgrade
582
+ preserves navigation a consumer has edited. The sidebar renderer is
583
+ unchanged — `nav: tree` → `auto` → `page.collection` was always correct, it
584
+ was being handed the wrong data. Existing consumers are not fixed
585
+ retroactively (`_data` is not part of Jekyll's theme payload); see
586
+ [Migrating already-seeded navigation data](docs/systems/theme-propagation.md).
587
+ Guarded by `test/test_install_navigation_seed.sh`, which fails on the old
588
+ installer naming all 51 seeded dead links.
589
+
590
+ - **Translation placeholders leaked into published French pages** — before a
591
+ line is sent to the model, `scripts/translate.rb` masks every non-translatable
592
+ span as a `⟦N⟧` token and restores it afterwards. The patterns run in order,
593
+ coarse first, so a span can be masked **twice**: a Liquid expression inside a
594
+ link destination becomes `](⟦3⟧)` and is then masked again as a whole, and the
595
+ same happens to Liquid inside an inline code span. `unmask` used a single
596
+ `gsub`, and `gsub` never rescans its own replacement text — so the outer span
597
+ came back and the inner token was stranded as a literal `⟦3⟧` on the page.
598
+ That is 107 tokens across 23 generated pages on `main` today, and where the
599
+ masked span was a URL it is a dead link: `[couleurs](⟦3⟧)` in
600
+ `fr/about/design.md`, `href="⟦24⟧"` in the same file, the whole Liquid
601
+ variable table in `fr/docs/customization/layouts.md`. `unmask` now substitutes
602
+ until the text stops changing, and refuses to return anything still holding a
603
+ placeholder — a corrupt page fails its own job (counted, logged, run exits
604
+ non-zero) rather than shipping. The existing "no placeholder tokens leak"
605
+ assertions in `test/test_i18n.sh` were correct but toothless: no fixture line
606
+ nested a mask. Both shapes are now in the fixture, so the guard fails without
607
+ the fix.
608
+
609
+ The already-generated pages are not repaired by this change. Staleness is
610
+ decided by source SHA alone, so a masker fix does not make an unchanged page
611
+ stale; repairing them needs a `--full` re-translation.
612
+ - **`components/background-image.html` — cover art painted as a CSS background,
613
+ announced correctly (#401)** — the theme had a solid convention for art
614
+ rendered as {% raw %}`<img>`{% endraw %} (`preview-image.html` always emits an escaped `alt`)
615
+ and none for art painted as a **background**. A background image is invisible
616
+ to assistive technology, so such a box announces nothing at all. The correct
617
+ pattern already existed by hand in three places (`abc-letter.html`, the
618
+ article rating stars, `theme-customizer.html`) without being a named
619
+ convention; this generalizes it. Two branches: a real image gets
620
+ `role="img"` with an escaped `aria-label`, a decorative one gets
621
+ `aria-hidden="true"` and **neither** `role` nor `aria-label` — combining them
622
+ announces a graphic and then hides it, which is worse than either alone.
623
+ Decorative is the **default**, and the real-image branch must earn it with
624
+ both a `src` and a non-empty `alt`. Path handling follows
625
+ `preview-image.html`. Purely additive: no in-theme call site, no CSS, no
626
+ layout change, and `content/intro.html` is deliberately untouched — it paints
627
+ a background *and* wraps the page `<h1>`, so `role="img"` there would delete
628
+ the heading from the accessibility tree. Guarded by
629
+ `test_background_image_include_contract`, which renders the include and
630
+ checks both branches, the escaping and all three path cases.
631
+ - **Heading outlines no longer skip levels in theme chrome (#436)** — several
632
+ components picked a heading element for its default **font-size** rather than
633
+ its position in the document, so an article read
634
+ `h1 → h2 → h6 → h2 → h5 → h3 → h6` to anyone navigating by heading. Measured
635
+ across a 415-page build: **391 of 415 pages carried at least one skip, 1,060
636
+ in total**; now 66 and 173. Size and level are decoupled — `<h3 class="h6">`
637
+ keeps the small type and states the real depth — in the cookie dialog, the
638
+ settings offcanvas, the language panel, the author card and the related-posts
639
+ region. The language panel was not in the report and was doing the same thing
640
+ on every page. `author-card.html` renders under twelve different call sites,
641
+ so its level is now `heading_level` (default `5`, unchanged for callers that
642
+ do not pass it) rather than a guess. Fails **WCAG 1.3.1**. Nothing moves
643
+ visually. Guarded by `test/visual/core/heading-outline.spec.js`. One offender
644
+ is injected at **runtime** by `assets/js/modules/theme/appearance.js`, so no
645
+ grep over `_includes`/`_layouts` could find it — only a rendered assertion
646
+ does. Still open, deliberately: 66 pages with inline card markup in the news
647
+ and section index layouts, plus the `palette-generator` and `skin-editor`
648
+ admin tools — see the evidence bundle.
649
+
650
+ - **Liquid written as documentation was being executed, not displayed** — Liquid
651
+ runs before Markdown, so backticks and code fences never escaped it; they only
652
+ changed how its *output* was displayed. Two pages leaked as a result. The
653
+ CHANGELOG page carried a **second full copy of `<head>` inside its body**
654
+ (Bootstrap CSS, Bootstrap Icons and `X-UA-Compatible` all emitted twice, and
655
+ the Google Tag Manager snippet a second time on a production build) because an
656
+ {% raw %}`{% include core/head.html %}`{% endraw %} in prose was run rather than shown. The
657
+ Layout Variables table in `/docs/customization/layouts/` documented nothing:
658
+ {% raw %}`{{ content }}`{% endraw %} injected the whole rendered page into a table cell and
659
+ {% raw %}`{{ page.title }}`{% endraw %} rendered as the literal word "Layouts", leaving
660
+ 196 KB of leaked layout output where a five-line code sample belonged. Both are
661
+ now wrapped in Liquid raw blocks, the idiom the same files already used
662
+ elsewhere. A `test_content_liquid_is_raw_protected` guard in
663
+ `test/test_core.sh` fails if injecting Liquid appears unprotected inside a code
664
+ fence or code span again. The layouts doc also had its layout **names** and
665
+ **hierarchy** corrected against `_layouts/`: `journals` has not existed since
666
+ the v1.0 rename to `article`, and three of the four children in the
667
+ inheritance diagram were wrong — `home` and `landing` inherit `root`, not
668
+ `default`, which decides whether a layout renders the sidebar and TOC at all.
669
+
670
+ - **Developer doc banners no longer ship to visitors (#375)** — the 1,065
671
+ multi-line {% raw %}`<!-- ... -->`{% endraw %} banners documenting `_includes/**` and
672
+ `_layouts/**` (file paths, dependency lists, design rationale) are now Liquid
673
+ {% raw %}`{% comment %}`{% endraw %} blocks, which Jekyll strips at build time. The
674
+ in-source documentation is unchanged; only the delimiters moved. Across a
675
+ 415-page build the delivered HTML drops from 91.5 MB to 71.6 MB — **21.7%** —
676
+ because an include's banner is re-emitted at every call site, not once:
677
+ `/authors/default/` alone carried 130,840 bytes of it from `post-card.html`
678
+ rendering 1,100 times. Per page: 47,898 comment bytes in 344 blocks → 111
679
+ bytes in 3. The four boundary markers inside Google's own copy-paste
680
+ analytics snippets are kept verbatim, and comments inside
681
+ `<script>`, `<style>`, `<pre>` or a Liquid raw block are untouched. A new
682
+ `test_developer_doc_banners_are_liquid` guard in `test/test_core.sh` fails if
683
+ any reappear. Two side effects worth knowing: reading-time estimates were
684
+ counting the banners and are now correct (`/docs/` claimed 63 minutes, really
685
+ 24), and three pages that leak unescaped Liquid emit less as a result — those
686
+ leaks are pre-existing and filed separately.
687
+
688
+ - **Setup wizard: live preview, vertical stepper, draft persistence (T-040, #408)** —
689
+ the `_config.yml` preview is now a **persistent sticky panel** shown at every
690
+ step rather than an element inside step 5, regenerated on every keystroke,
691
+ with Copy and Download always enabled. The nav-pills tab row is replaced by a
692
+ left **vertical stepper** with done/active/upcoming state: a step is locked
693
+ until every earlier step validates, and Back is always allowed. The form is
694
+ mirrored to `localStorage` under `zer0-setup-draft` (debounced 300ms),
695
+ restored on load with a "Draft saved" chip, and cleared once the file is
696
+ downloaded. Email and URL fields are validated **on blur** with `is-invalid`
697
+ and an explanatory message, cleared as soon as you retype; the Review step
698
+ lists any unfilled recommended fields as warnings. Every step pane shares one
699
+ CSS grid cell, so the container is always as tall as the tallest step, and the
700
+ nav row is pinned to the bottom — **Back/Next no longer move vertically**
701
+ between steps. New
702
+ `_sass/components/_setup-wizard.scss`, and the wizard gains its first
703
+ automated coverage in `test/visual/features/setup-wizard.spec.js`.
704
+
705
+ Holding the container's height turned out to be only half of "Back/Next no
706
+ longer move": the nav row's own height still varied. Step 1's row was a block
707
+ box, so its button sat on a baseline and rode ~2px high, and in the real
708
+ ~270px middle column `Review & Download` wrapped to a second line, pushing
709
+ that button's top up a further 16px. The row is now always a flex box and its
710
+ buttons never wrap. Draft persistence gained a matching fix: the 300ms
711
+ debounce meant the last edit before a reload was still only a queued timer and
712
+ was lost, so a pending write is now flushed on `pagehide` and on a hidden
713
+ `visibilitychange`. Before/after measurements are in
714
+ `test/visual/evidence/setup-wizard-nav-alignment/`.
715
+
716
+ - **Language switcher: icon-only trigger and a compact, positive menu (T-038, #406)** —
717
+ the navbar trigger drops its `EN` text span and the Bootstrap caret for a
718
+ 38px square icon button, with `title` and `aria-label` carrying the language
719
+ name. In the menu (both the navbar dropdown and the Settings panel variant),
720
+ the current language is now marked with a tint and a check icon instead of
721
+ Bootstrap's `.active` primary fill, which read as a selected nav item rather
722
+ than "you are here". **Untranslated languages are no longer disabled rows** —
723
+ they link to the source page and still record the `zer0-lang` preference, so
724
+ the menu has no dead ends. Machine-generated translations carry a small
725
+ `auto` chip, and the per-row "(Not yet translated)" text — the main driver of
726
+ menu width — collapses into one footnote under a divider, referenced from
727
+ each marked row via `aria-describedby`. The dropdown menu is capped at 220px.
728
+ New `ui` keys `lang_machine_translated`, `lang_machine_translated_title` and
729
+ `lang_untranslated_note` in `_data/ui-text.yml`; each use carries a literal
730
+ fallback, because `core/i18n.html` replaces the `ui` map wholesale on a
731
+ translated page and `_data/i18n/<lang>.yml` will not have them until
732
+ `scripts/translate.rb` next runs.
733
+
734
+ ### Added
735
+
736
+ - **Cookbook & recipes collection (ZER0-084)** — a `recipes` collection that
737
+ renders structured front matter as a working recipe page: `layout: recipe`
738
+ builds a fact bar (prep/cook/rest/total, yield, difficulty, cuisine, oven),
739
+ a grouped ingredient checklist, a numbered method with per-step timings and
740
+ temperatures, a baker's-percentage ratio table computed at build time, a
741
+ per-serving nutrition panel, and one `schema.org/Recipe` JSON-LD block.
742
+ `assets/js/recipe-scaler.js` adds serving scaling and live US↔metric
743
+ conversion — including volume↔weight via `_data/ingredient_densities.yml`
744
+ — as progressive enhancement, so every amount is still rendered exactly as
745
+ authored with JavaScript disabled. `layout: cookbook` indexes recipes into
746
+ course sections from `_data/recipe_courses.yml`. Twelve new
747
+ `components/recipe-*.html` includes, `_sass/components/_recipe.scss` (with
748
+ print styles), a demo cookbook at `/recipes/`, and docs at
749
+ [Cookbook collection](pages/_docs/features/cookbook-collection.md). Regression
750
+ coverage: `test/visual/features/recipes.spec.js` (smoke tier) and "Cookbook
751
+ Recipe Collection" in `test/test_core.sh` (evidence:
752
+ [`test/visual/evidence/recipes-collection/`](test/visual/evidence/recipes-collection/README.md)
753
+ — 0px page overflow across 320–1440px; 1 cup butter → 227 g, 500 g flour →
754
+ 4 cups; ratio percentages identical at 1× and 3×).
755
+
756
+ - **Page-view counter (ZER0-083)** — the theme now tracks a view for the page
757
+ being read and displays the count in the article meta row, next to the
758
+ reading time (`👁 12 views`). Two providers, configured under `page_views:`
759
+ in `_config.yml`: `local` (the default — counts live in the visitor's
760
+ `localStorage`, so it works on GitHub Pages with no server, no network, and
761
+ no third party) and `remote` (counts come from an HTTP counter endpoint you
762
+ control, with `{path}` substitution, a configurable method, and a dotted
763
+ `count_key` for the response shape). Privacy gates mirror the `posthog:`
764
+ block — Do Not Track, Global Privacy Control, and an optional
765
+ `require_consent` tie-in to the cookie-consent "analytics" category — and
766
+ they suppress *recording* only, never the display of a count that is already
767
+ known; a view blocked pending consent is recorded when consent arrives.
768
+ `dedupe: session` counts one view per page per browser session so reloads do
769
+ not inflate it. The badge include (`components/page-views.html`) ships hidden
770
+ and is revealed only once a count exists, carrying its own leading separator
771
+ so a page with no data leaves no dangling bullet. New:
772
+ `_includes/components/page-views.html`,
773
+ `_includes/components/page-views-init.html`, `assets/js/page-views.js`,
774
+ `_sass/components/_page-views.scss`. Regression spec:
775
+ `test/visual/features/page-views.spec.js`. Docs:
776
+ [`docs/features/page-views.md`](docs/features/page-views.md)
777
+ (evidence: [`test/visual/evidence/page-views/`](test/visual/evidence/page-views/README.md)).
778
+
779
+ - **Search shortcut affordances (ZER0-032)** — the navbar search button now
780
+ advertises its keyboard shortcuts, matching the design-system navbar
781
+ pattern: a quiet `/` kbd chip (xl-up), a hover title ("Search — press / or
782
+ Ctrl+K", shown as `⌘K` on macOS), and `aria-keyshortcuts`; the sidebar (`b`)
783
+ and table-of-contents (`t`) hide toggles gained shortcut hover hints, and
784
+ the keyboard-shortcuts modal now lists `⌘/Ctrl+K`
785
+ (evidence: [`test/visual/evidence/search-shortcut-hint/`](test/visual/evidence/search-shortcut-hint/README.md)).
786
+
787
+ - **Live design-system pages (ZER0-082)** — the design system is now
788
+ part of the site itself: `_design-system/` is force-included in the Jekyll
789
+ build so `styles.css`, every token file, specimen cards, and component
790
+ sources get real URLs under `/_design-system/`; a new `/design/` hub page
791
+ renders the foundation specimens in live iframes (colors, skins, type,
792
+ spacing, radii, motion, layers, iconography), catalogs the component twins,
793
+ and links the Claude Design page canvas (all 24 layouts as a browsable
794
+ presentation). Navbar: About → Design System. Regression spec:
795
+ `test/visual/features/design-hub.spec.js`.
796
+
797
+ - **Claude Design system round-trip (ZER0-081)** — the design-token layer is now
798
+ aligned with the zer0-mistakes Claude Design project
799
+ (`claude.ai/design/p/e75121c0-9210-42d1-ade3-2c8af9111cbe`): new
800
+ `--zer0-radius-*`, `--zer0-logo-*`, and `--zer0-text-display-*` tokens in
801
+ `_sass/tokens/`; the design project vendored as a git mirror under
802
+ `_design-system/` (plain-CSS tokens, React component specs, guideline cards,
803
+ website UI kit — sync contract in `_design-system/SYNC.md`); and
804
+ `scripts/design-system-check.rb` wired into the core test suite so the theme
805
+ and the mirror cannot drift silently. Callouts, landing feature cards, mobile
806
+ nav, tables, and focus rings now consume the radius tokens (value-preserving —
807
+ rendered output unchanged, pinned by a new runtime token assertion in
808
+ `test/visual/core/styling.spec.js`).
809
+ - **Design-system component twins (PR #391)** — `Fab`/`FabStack`, `PostCard`,
810
+ and `Skeleton` join Button/Badge/Card/FeatureCard/Callout/Input in
811
+ `_design-system/components/`, plus new Motion and Layers foundation cards;
812
+ all pushed live to the Claude Design project. The feature registry is
813
+ re-synced (`features/features.yml` ↔ `_data/features.yml` — #389's ZER0-081
814
+ entry had landed in the `_data` mirror only, which the registry validator
815
+ rejects) and `docs/ui/components.md` gains a theme-source →
816
+ design-system-twin map.
817
+
818
+ ### Changed
819
+
820
+ - **Language selector moved from the navbar into Settings → Appearance** — the
821
+ header utility cluster is now Search + Settings only, returning ~70px to the
822
+ menubar. `components/language-toggle.html` grew a `variant` parameter: the new
823
+ `panel` variant (shipped) renders a self-contained **Language** section — an
824
+ always-visible list of languages with the active one checked and
825
+ untranslated targets disabled — as the first block of the Settings
826
+ Appearance tab, next to Color Mode and Theme Skin. The original header
827
+ dropdown remains available as `variant="navbar"` for consumers that prefer
828
+ it. Same data, same `localStorage("zer0-lang")` preference, same
829
+ never-a-dead-link contract; `test/visual/features/language-toggle.spec.js`
830
+ was updated to exercise it through the Settings panel.
831
+
832
+ ### Fixed
833
+
834
+ - **Finished work kept reopening its own issues** — `scripts/sync-backlog.rb`
835
+ treats `_data/backlog.yml` as the source of truth and **reopens** any issue whose
836
+ task is not `done`. T-038, T-040 and T-045 shipped in v1.29.0 (#431, #432, #425)
837
+ and their `Closes #NNN` commits duly closed #406, #408 and #417 — then the next
838
+ sync reopened all three, because the backlog still said `open`. Marked done with
839
+ their PR links, which is the only thing that actually closes them.
840
+ - **The nightly sticky issue could be raised but never cleared (#326)** —
841
+ `nightly-extended.yml` files and updates a sticky issue on failure and does
842
+ nothing on recovery, so a fixed failure stayed open indefinitely. #326 sat open
843
+ five days past its own fix, carrying a P1 into every triage pass. A
844
+ `report-recovery` job now comments and closes it when the tier goes green. It is
845
+ gated on `preflight.outputs.go == 'true'` as well as both jobs succeeding: the
846
+ preflight short-circuits on an unchanged `main` and reports `skipped`, so a bare
847
+ `success()` would close the issue on a night that tested nothing.
848
+ - **37 pages shipped dead controls with dangling ARIA references (#373)** —
849
+ `#bdSidebar` and `#tocContents` are emitted by `_layouts/default.html` alone,
850
+ but their toggles live in `core/header.html` and `core/footer-fabs.html`, which
851
+ `_layouts/root.html` includes on **every** layout. On any layout inheriting
852
+ `root` directly (admin, stats, 404, section, home, book-*) the toggle rendered
853
+ pointing at an offcanvas that was never in the document: clicking did nothing,
854
+ and assistive tech followed `aria-controls` to a missing element. Measured
855
+ across a 415-page build: **19 pages with a dangling `#bdSidebar`** (the French
856
+ `/about/**` tree) and **18 with a dangling `#tocContents`** (404, CHANGELOG,
857
+ `_design-system/**`) — **0 after**. `navigation/sidebar-config.html` now
858
+ publishes `sidebar_offcanvas_rendered`, and both controls are gated on it.
859
+ `footer-fabs.html`'s previous **denylist** of layouts to skip is replaced by
860
+ that positive gate: a denylist fails open, and this one had already fallen
861
+ behind (`404` and the `book-*` layouts were missing). The layout list is pinned
862
+ against the real inheritance graph by
863
+ `test/test_core.sh :: test_sidebar_offcanvas_layout_gate`, which walks every
864
+ `_layouts/*.html` and fails if the two disagree in either direction.
865
+
866
+ - **Seven footer links had no accessible name on mobile (#435)** — the "Follow
867
+ Us" social links and the RSS link render an `<i>` that is `aria-hidden="true"`
868
+ beside a `<span class="d-none d-md-inline">` label. Below the `md` breakpoint
869
+ the span is `display:none`, so it leaves the accessibility tree too and the link
870
+ was announced as a bare "link" — measured: **7 nameless links at 375px**. Each
871
+ now carries an `aria-label` matching its visible text exactly, so the name is
872
+ correct on mobile and still contains the visible label at `md`+ (WCAG 2.5.3
873
+ Label in Name). Covered by `test/visual/core/accessible-names.spec.js`, which
874
+ asserts at **mobile** width — the same assertions pass at desktop on the
875
+ unfixed code, which is why the bug survived the existing suite.
876
+ - **Search results were never announced to screen readers (#279)** — the modal
877
+ rewrites its results container without moving focus, so assistive tech had no
878
+ signal that anything had happened. A visually-hidden `role="status"`
879
+ `aria-live="polite"` region now announces a concise count ("7 results for
880
+ jekyll", or "12 of 40 results…" when the list is truncated), and "No results
881
+ found." The **results list itself is deliberately not live**: announcing eight
882
+ titles and their snippets on every keystroke would be unusable. `aria-atomic`
883
+ keeps the summary a single message, and a repeated string is cleared first so an
884
+ unchanged announcement is not swallowed as a no-op.
885
+
886
+ - **`<meta charset>` shipped ~26 KB into the document, past the spec's 1024-byte
887
+ window (#372)** — the HTML spec only scans the first 1024 bytes for the
888
+ character-encoding declaration. The tag sat in `_includes/core/head.html` below
889
+ Google Tag Manager, console-capture, page-views, mermaid and nanobar — and below
890
+ two multi-line HTML doc banners that shipped verbatim to production — so a
891
+ served page declared its encoding at byte **26058**. Past the limit a browser
892
+ stops looking and decodes with its locale default, mangling every non-ASCII byte
893
+ until it re-parses; this theme ships UTF-8 throughout (`fr/**` translations,
894
+ em-dashes, curly quotes), so that is a live corruption risk rather than a lint
895
+ nit. The tag now sits at the very top of `_layouts/root.html` — measured at byte
896
+ **318**, verified across all 400 built pages — and `root.html`'s own banner is a
897
+ Liquid comment (stripped at build) rather than an HTML one, which is what freed
898
+ the budget. Regression coverage in `test/visual/core/head-contract.spec.js`
899
+ measures the **served bytes**, since a source-order assertion would have passed
900
+ both before and after.
901
+ - **The Atom feed was undiscoverable (#371)** — `jekyll-feed` generates
902
+ `/feed.xml` but emits no autodiscovery `<link>` unless `feed_meta` is called,
903
+ which the theme never did, so readers and crawlers could not find the feed even
904
+ though the footer linked it. `<head>` now carries
905
+ `<link rel="alternate" type="application/atom+xml">`, honouring `site.feed.path`.
906
+ Written out directly rather than via `feed_meta` so a consumer site that has not
907
+ enabled the plugin does not hard-fail with `Unknown tag`. A test asserts the
908
+ advertised URL actually returns a feed — a discoverable link to a 404 would be
909
+ worse than none.
910
+
911
+ - **Footer "Info" and "Cookie Preferences" are buttons, not links (#320)** —
912
+ both controls open in-page UI (the `#info-section` offcanvas and the
913
+ `#cookieSettingsModal` modal) but shipped as `<a href="#" data-bs-toggle>`, so
914
+ assistive tech announced them as links — WCAG 4.1.2 Name, Role, Value — and
915
+ whenever Bootstrap's toggle did not fire the browser followed `href="#"` and
916
+ jumped the page to the top. All three occurrences are now
917
+ `<button type="button">`: the Info trigger is emitted **twice** by
918
+ `_includes/core/footer.html`, once in each branch of the powered-by loop, and
919
+ a site with no `powered_by` credits configured renders the branch a
920
+ single-site fix would have missed. `.powered-by-link` is now element-agnostic
921
+ (it already styled `<a>`, `<span>` and now `<button>`) and carries the
922
+ `<button>` reset Bootstrap Reboot does not supply — `appearance`,
923
+ `background`, `border`, `colour` — with the new `.footer-inline-button` doing
924
+ the same for a button in running text, so the swap is visually inert. The
925
+ back-to-top control keeps its `<a href="#">`: scrolling to the top is its
926
+ actual behaviour, not a side effect. Regression coverage: "Accessibility —
927
+ footer in-page toggles are buttons" in
928
+ `test/visual/core/accessibility.spec.js`, which asserts the element identity
929
+ directly — axe-core has no rule that fires on an anchor used as a button, so
930
+ an axe-based assertion passes on the broken markup too. "powered-by credits
931
+ are real links" in `test/visual/core/layout-chrome.spec.js` already banned
932
+ `href="#"` but sampled only the first five links, and `site.powered_by` pushed
933
+ the offending Info anchor past index 4; it now checks every credit link.
934
+ (evidence:
935
+ [`test/visual/evidence/footer-button-semantics/`](test/visual/evidence/footer-button-semantics/README.md)
936
+ — shipped vs. reset-stripped at 1280px, footer overflow 0px at 375/768/1280)
937
+
938
+ - **`<html class="no-js">` is upgraded to `js` again (#319)** — `_layouts/root.html`
939
+ shipped the standard progressive-enhancement hook but nothing ever swapped
940
+ it: the string `no-js` occurred in exactly one file in the repository, and in
941
+ no SCSS, JS, test or doc. Every page on every downstream site permanently
942
+ carried `no-js` with JavaScript fully enabled, so a consumer writing
943
+ `html.no-js .foo {…}` / `html.js .foo {…}` got the no-JS branch for everyone
944
+ — the class advertised a capability the theme did not deliver. An inline
945
+ script is now the **first child of `<head>`**, above
946
+ {% raw %}`{% include core/head.html %}`{% endraw %} (which opens with Google Tag
947
+ Manager, so
948
+ "first in that include" is not "first in `<head>`") and deliberately neither
949
+ `defer` nor `async`, since it has to run before the first stylesheet is
950
+ fetched or `html.js` rules would flash. The served HTML still carries
951
+ `class="no-js"`, so the JS-disabled branch stays correct and the change is
952
+ purely additive for consumers already styling against the served markup.
953
+ Regression coverage: `test/visual/core/progressive-enhancement.spec.js`
954
+ (smoke tier) pins both directions of the contract — `js` with scripts on,
955
+ `no-js` with them off — plus the delivery mechanism, because a fix that
956
+ simply deleted `class="no-js"` would satisfy a naive one-sided assertion
957
+ while removing the hook entirely.
958
+
959
+ - **Pages build: Liquid errors no longer ship mangled pages silently.** The
960
+ `github-pages` gem renders every non-excluded Markdown file through Liquid, so
961
+ prose that merely *looks* like a template is executed as one. Jekyll 3 reports
962
+ that as a `Liquid Warning`, drops the expression, and still exits 0 — so the
963
+ Pages workflow stayed green while publishing damaged pages. Two were live:
964
+ `/CHANGELOG/` rendered "Added `,,` and to the template renderer" because
965
+ `{% raw %}{{DEFAULT_BRANCH}}{% endraw %}` and its siblings were evaluated to
966
+ nothing, and `/_design-system/components/feedback/Skeleton.prompt/` rendered
967
+ `<div style=>` because the JSX `style={% raw %}{{ … }}{% endraw %}` was a
968
+ Liquid syntax error. A third defect was invisible: a literal
969
+ `{% raw %}{% raw %}{% endraw %}` written as prose in a changelog entry opened a
970
+ real block that swallowed the next 945 lines of the published page. All the
971
+ affected spans are now escaped, `pages.yml` fails the build on
972
+ `Liquid Warning`/`Liquid Exception` for both the theme and the example site,
973
+ and `scripts/lint-liquid-raw.rb` (wired into the `quality` suite) catches the
974
+ silent half — an unbalanced raw block never raises a warning to gate on.
975
+
976
+ - **Navbar: every top-level item now fits, at every desktop width** — the bar
977
+ was pinned inside a centred `.container-xl` (max 1140–1320px), so the menubar
978
+ track never got more than ~740px against the ~815px the theme's seven items
979
+ need. Every label rendered truncated — `Quick …`, `N…`, `Note…`, `A…` — while
980
+ the page had 100+px of unused margin on either side, and below ~1050px the
981
+ whole menu collapsed to bare icons. Four changes give the labels their room:
982
+ the bar is now `.container-fluid` and spans the full screen width (opt back
983
+ into a cap with `--zer0-navbar-max-width`); the brand cluster is capped at
984
+ `30cqi` of the bar so a long title cannot starve the menubar; label density
985
+ degrades in three measured tiers instead of two (icon + label ≥ 51rem →
986
+ label-only 41–50.99rem → icon-only < 41rem), each boundary set to the width
987
+ the tier below it actually needs; and the home-link icons wait for 86rem,
988
+ where they no longer cost the menubar more than it can spare. Measured on the
989
+ demo site: **0 truncated labels from 992px to 2560px** (was: all 7 truncated
990
+ at 1440px, all 7 icon-only at 992px). Regression coverage: "no nav label is
991
+ CSS-truncated at any desktop width" / "the bar spans the full viewport width"
992
+ / "the brand title stays legible next to the menubar" in
993
+ `test/visual/features/navbar.spec.js` — the pre-existing label test only
994
+ checked for a literal `...` in `textContent`, which CSS `text-overflow` never
995
+ writes, so it passed throughout (evidence:
996
+ [`test/visual/evidence/navbar-fit/`](test/visual/evidence/navbar-fit/README.md)
997
+ — up to 7 truncated labels and 300px of dead bar margin → 0 across 8 widths).
998
+
999
+ - **Settings no longer opens behind the mobile nav menu** — on phones, tapping
1000
+ **Settings** inside the open navigation menu appeared to do nothing: the
1001
+ Settings offcanvas and the nav menu are both `offcanvas-end` panels at
1002
+ Bootstrap's `$zindex-offcanvas`, and `_navbar.scss` lifts the fixed header
1003
+ (which contains the nav menu) to 1046 while that menu is open — so Settings
1004
+ painted underneath it, fully hidden. `navbar.js` now intercepts the offcanvas
1005
+ `show` event and closes the host panel first, opening Settings once it has
1006
+ finished sliding out; `_navbar.scss` releases the header lift, and raises
1007
+ `#info-section` above it, for browsers without `:has()`. Covered by "Settings
1008
+ opened from the nav menu paints ABOVE it" in
1009
+ `test/visual/features/mobile-overlay-stacking.spec.js` (evidence:
1010
+ [`test/visual/evidence/navbar-fit/`](test/visual/evidence/navbar-fit/README.md)
1011
+ — `03-settings-stacking.png`: top element at the panel centre `a.nav-link`
1012
+ → the panel's own content; header `z-index` 1046 → 1030).
1013
+
1014
+ - **Section topic filters match whole tags, not substrings** — filtering a
1015
+ section by a topic silently revealed posts that did not carry it. `data-tags`
1016
+ was built as `tags | join: ' ' | slugify`, which slugifies *after* joining and
1017
+ so turns the separators into hyphens — making a tag boundary indistinguishable
1018
+ from a hyphen inside a slug — and the click handler then matched with
1019
+ `String.includes`. Filtering `/news/technology/` by **ai** returned 6 cards
1020
+ while the sidebar badge for that topic said 5, because a post tagged `edge-ai`
1021
+ matched too. `_layouts/section.html` now slugifies each tag separately into a
1022
+ space-separated list and matches whole tokens, so the rendered count always
1023
+ agrees with the badge (which Liquid computes with exact `contains` membership).
1024
+ Regression coverage in `test/visual/features/section-topic-controls.spec.js`
1025
+ asserts token membership and pins the filtered count to the badge (evidence:
1026
+ [`test/visual/evidence/section-topic-filter/`](test/visual/evidence/section-topic-filter/README.md)
1027
+ — filtering /news/technology/ by "ai" showed 6 cards against a badge of 5, now
1028
+ 5; topics whose badge disagrees with what is shown: 2 → 0).
1029
+
1030
+ - **Nightly smoke tier is green again** — three `smoke` tests had been failing
1031
+ every night since 2026-08-15 (the tier the PR gate does not run, so `main`
1032
+ stayed green). Two were the topic-filter bug above; the third asserted the
1033
+ sidebar's "All Articles" control only as an `<a href="#all-posts">`, but the
1034
+ sidebar renders a `<button data-filter="all">` for the grid/list styles —
1035
+ currently every section — so it could never pass. The topic-button selector
1036
+ also used `.filter({ hasNot })`, which excludes elements *containing* a match
1037
+ rather than matching elements themselves, so it selected the "All Articles"
1038
+ button and asserted against an unfiltered list; it now uses `:not()`.
1039
+
1040
+ - **A rejected Anthropic credential now falls back to the next one** — the
1041
+ translation pipeline picked the first credential that was *set* and never
1042
+ reconsidered, so a revoked `CLAUDE_CODE_OAUTH_TOKEN` shadowed a working
1043
+ `ANTHROPIC_API_KEY` and took the whole run down with a `401 OAuth access
1044
+ token has been revoked`. `scripts/translate.rb` now treats 401/403 as a
1045
+ credential failure rather than a request failure and retries with the next
1046
+ configured credential, rebuilding the payload so the OAuth-only Claude Code
1047
+ identity block is not sent with an API key. The switch is sticky, so a dead
1048
+ credential costs one rejection per run rather than one per chunk.
1049
+
1050
+ - **A failed translation run no longer opens a PR containing nothing** —
1051
+ `_data/i18n/manifest.yml` stamped `updated_at` on every save, so a run in
1052
+ which *every* page failed still produced a one-line diff. That was enough
1053
+ for `translate.yml` to open (and keep updating) a PR that read like a routine
1054
+ translation refresh and contained no translations. The manifest is now
1055
+ written only when the mapping itself changed; a partially successful run
1056
+ still pushes the pages that succeeded, and the run still exits non-zero.
1057
+
1058
+ - **The cookbook is reachable from the navbar** — the `recipes` collection
1059
+ (ZER0-084) shipped with a landing page at `/recipes/` that nothing linked to,
1060
+ so the deployed cookbook could only be found by typing the URL.
1061
+ `_data/navigation/main.yml` now carries a Recipes entry with the demo
1062
+ recipes and the feature docs.
1063
+
1064
+ - **Section sidebars no longer emit dead sub-topic anchors** — the sidebar
1065
+ produced `<a href="#tag">` for every tag, but the matching
1066
+ `<section id="tag">` target exists only in the `magazine` branch of
1067
+ `_layouts/section.html`, and only for the first 5 sub-categories that keep a
1068
+ post once the featured hero is excluded. `grid` and `list` sections (and
1069
+ `grid` is the default) therefore emitted one dead anchor per tag, and
1070
+ `magazine` emitted them for tags 6–15 — 64 of them on this repo's own demo
1071
+ site. Dead anchors are invalid markup, a dead click for keyboard/AT users,
1072
+ and they left the sidebar scrollspy unable to activate. Anchors are now
1073
+ emitted only where a target provably exists; every other style renders
1074
+ `<button data-filter>`, which the layout's existing handler already wires to
1075
+ the `[data-tags]` cards. The `list` branch gained the `data-tags` attribute
1076
+ it was missing (without it those buttons would have been inert), and the
1077
+ filter handler now applies pill colours only to actual pills, so the pills
1078
+ and the sidebar stay in sync instead of fighting over each other's classes.
1079
+ Regression cover: `test/visual/features/section-topic-controls.spec.js`
1080
+ - **Consumer homepages no longer claim to be this theme** — the
1081
+ `SoftwareApplication` JSON-LD in `_includes/content/jsonld-software.html` is
1082
+ now opt-in via `jsonld_software_application` (set on this site only). The
1083
+ block hardcodes this theme's RubyGems URL, GitHub repository, MIT licence,
1084
+ feature list and author, but was wired unconditionally into every consumer's
1085
+ homepage — so a consulting firm's site was telling search engines it was a
1086
+ free open-source Ruby gem. Consumers need no action: absent config is falsey.
1087
+ Evidence: [`test/visual/evidence/jsonld-software-optin/`](test/visual/evidence/jsonld-software-optin/README.md).
1088
+ - **`audit-consumer` misread most consumers' theme mode** — `detect_consumer_mode()`
1089
+ matched `^remote_theme:` with no tolerance for whitespace before the colon, so
1090
+ the column-aligned YAML style that 6 of 9 fleet consumers use never matched and
1091
+ every one of them was silently reported as `gem` mode. That skipped the plugin
1092
+ checks entirely, with no warning that they had been skipped.
1093
+ - **`audit-consumer --format json` emitted unparseable JSON** — every entry
1094
+ carried a trailing comma, so the array always closed on `},\n]}`. The
1095
+ human-readable log lines also went to stdout, mixing into the payload. Entries
1096
+ are now comma-separated correctly and logging moves to stderr under
1097
+ `--format json`, so the output pipes straight into `jq`.
1098
+ - **`audit-consumer` ignored Jekyll's `source:`** — a consumer that relocates its
1099
+ site tree (e.g. `source: pages`) was audited at the repo root, reporting a
1100
+ falsely clean "no overrides, no unique files". It now follows `source:` for the
1101
+ file scan while still reading config and `.theme-overrides.yml` from the root.
1102
+ - **The Obsidian plugin was demanded of consumers that cannot run it** — the
1103
+ manifest listed `_plugins/obsidian_links.rb` as required for every
1104
+ `remote_theme` consumer, contradicting `obsidian.instructions.md`, which
1105
+ documents it as opt-in and "skipped under the `github-pages` gem". GitHub Pages
1106
+ builds in safe mode and loads no local plugins, so nothing the theme ships is
1107
+ mandatory; `required_plugin_paths` is now empty and the plugin is optional.
1108
+
1109
+ ### Changed
1110
+
1111
+ - **`_data/consumers.yml` now covers the whole fleet** — added `zer0-pages`,
1112
+ `irony-works`, `wargames` and `zer0-pages-remote`, which were unregistered and
1113
+ therefore invisible to `propagate.rb` in both directions (no release dispatch,
1114
+ never in the drift report). The `it-journey` risk note was corrected: its
1115
+ "~55 shadowed files / ~75 lines of !important CSS working around #338" claim
1116
+ is not supported by `audit-consumer` — only 2 files shadow a theme file, the
1117
+ CSS is 31 lines in this theme's own documented `user_overrides` hook, and no
1118
+ fix numbered #338 could be substantiated in that repo.
1119
+
15
1120
  ## [1.28.0](https://github.com/bamr87/zer0-mistakes/compare/v1.27.0...v1.28.0) (2026-08-09)
16
1121
 
17
1122
 
@@ -42,8 +1147,33 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
42
1147
 
43
1148
  ## [Unreleased]
44
1149
 
1150
+ ### Fixed
1151
+
1152
+ - **The skip link never moved keyboard focus (#278)** — `_layouts/root.html`
1153
+ rendered `<main id="main-content">` with no `tabindex`, and `<main>` is not natively focusable, so activating "Skip to main content" scrolled the page while focus stayed on `<body>`: the next `Tab` returned the user to the header nav they had just skipped. That defeats WCAG 2.4.1 (Bypass Blocks) on every consuming site, and the theme's own docs already prescribed the fix (`skip-to-content.md` → *Focus Not Moving* → "Add `tabindex=\"-1\"` to target") while the shipped layout omitted it. Now `<main id="main-content" tabindex="-1">`, paired with `#main-content:focus { outline: none; }` in `_sass/utilities/_focus.scss` so the container does not paint a ring — descendants keep theirs, and the rule is safe only because `-1` keeps the container out of the sequential `Tab` order. Measured across all three engines: focus lands on `#main-content` in Chromium, Firefox **and WebKit** (the engine the report named), where before it moved in none of them. The stale `<div id="main-content">` example in the English and French docs now matches the shipped markup.
1154
+ - **Seven theme dialogs labelled themselves with a bare `<h5>`** — the search
1155
+ modal, cookie settings modal, statistics help modal and the settings / background / section / admin offcanvases each opened with `<h5 class="modal-title">` or `<h5 class="offcanvas-title">`, so anyone navigating by heading met a level-5 heading as the dialog's title, skipping h3 and h4. Six other dialogs already used the correct `<h2 class="…-title h5">` form, so the theme contradicted itself half the time. All seven now match. `.h5` reproduces the exact sizing, so the change is visually inert. `_includes/docs/bootstrap-docs.html` and the docs prose examples are deliberately untouched — they reproduce upstream Bootstrap's own markup and should keep doing so. (evidence: [`test/visual/evidence/dialog-heading-levels/`](test/visual/evidence/dialog-heading-levels/README.md) — heading walk h2 → h5 becomes h2 → h2 on all six live dialogs, renders byte-identical)
1156
+ - **Cookie modal's "Your Privacy Rights" list was invisible in dark mode** — the
1157
+ list used Bootstrap's fixed `text-dark` utility inside a `bg-body` panel. `.text-dark` resolves to `rgba(var(--bs-dark-rgb), 1)`, and `--bs-dark-rgb` is **not** remapped by `[data-bs-theme=dark]` — it stays `33,37,41`, which is byte-for-byte the dark `--bs-body-bg` (`#212529`). Measured in a browser: **1.00:1**, against WCAG 1.4.3 (AA)'s 4.5:1 minimum. Not "hard to read" — the same colour as its background, on the one dialog a visitor is most likely forced to read, in the theme's default colour mode, shipped to every consumer. Now `text-body` (**11.85:1** dark, 15.43:1 light, unchanged). `.cookie-category`'s hardcoded `#dee2e6` border became `var(--bs-border-color)`, which is the identical colour in light mode and a visible one in dark. A regression test opens the dialog and asserts the measured contrast ratio — `axe` cannot catch this class of bug on this theme, because the skin system's `body::after` background image makes axe report every contrast check in the dialog as `incomplete` rather than a violation. (evidence: [`test/visual/evidence/cookie-consent-contrast/`](test/visual/evidence/cookie-consent-contrast/README.md) — dark-mode contrast 1.00:1 → 11.85:1, light mode unchanged)
1158
+ - **`--page` lookups never matched a real Giscus thread** — `normalize_page()`
1159
+ prepended a leading slash to build the discussion title, but Giscus's `pathname` mapping searches for `location.pathname` with the leading slash **removed** (verified live: `/posts/2025/01/21/remote-work-revolution/` requests `term=posts/2025/01/21/remote-work-revolution/`). So `seed` created discussions Giscus would never find, and `thread`/`draft`/`post --page` missed every thread Giscus created itself — the entire "build conversations from page comments" workflow was inoperable against real data. Lookups now match all four title spellings (with/without leading and trailing slash) so both new and legacy/hand-made threads resolve.
1160
+ - **`discussion_number_for_page` silently ignored pagination** — the query
1161
+ declared `$after` and selected `pageInfo`, but never used either, so on a site with more than 100 discussions no thread past the first page could ever be found. Now uses `gh api graphql --paginate`.
1162
+ - **`set -e` aborted the script on the success path** — `[[ -z "$NUMBER" ]] &&
1163
+ error ...` as the last statement of a branch returns 1 when the lookup *succeeds*, which `set -e` turned into a silent exit with no output at all. Latent and unreachable while lookups always failed; the fix above exposed it immediately. Rewritten as `if` blocks in `resolve_number_arg` and `cmd_post`.
1164
+ - **Giscus comments were broken sitewide** — every page rendering the comment
1165
+ widget showed `An error occurred: giscus is not installed on this repository` instead of a comment box, on `zer0-mistakes.com` and locally, because the [giscus GitHub App](https://github.com/apps/giscus) was never installed on the repository. The `_config.yml` values were all correct (`data-repo-id` and `data-category-id` both verified against the live GitHub API, repo public, Discussions enabled, `Announcements` category), which is exactly why it went unnoticed: **no check in the repo could see it**. The `Giscus Comments Configuration` core test only inspects config keys and Liquid, and `test/visual/features/comments.spec.js` stubs `giscus.app/client.js` out entirely — so all 16 core tests passed at 100% against a completely non-functional feature. **Resolved**: the app is now installed and `doctor` reports the chain healthy; comments render and accept sign-in on production. Verified with a seeded demo thread ([#379](https://github.com/bamr87/zer0-mistakes/discussions/379)) that the widget correctly resolves and displays.
1166
+ - **Docs claimed the widget "won't load on localhost"** — it does. Giscus keys
1167
+ off the `data-repo` attribute, not the page origin, so the iframe renders and reports real errors locally. The false claim told developers to expect a non-working widget in dev, which is precisely what hid this bug. `pages/_docs/features/giscus-comments.md` now documents localhost as a genuine end-to-end check, with a table mapping each rendered state to its cause.
1168
+
45
1169
  ### Added
46
1170
 
1171
+ - **`giscus-discussions doctor`** — a new subcommand that verifies the entire
1172
+ comment chain end-to-end: repository public, Discussions enabled, `giscus.enabled: true`, `data-repo-id` actually belonging to this repo (the fork trap), `data-category-id` resolving to a real category, and — the part nothing else could check — whether the giscus GitHub App is installed, by asking the Giscus API itself. Exits non-zero with a specific remediation line per failure.
1173
+ - **Giscus health check in CI** — `.github/workflows/giscus-digest.yml` (already
1174
+ scheduled weekly) now runs `doctor` and fails the job with a summary when comments are down, so a sitewide comment outage surfaces on its own instead of waiting for a reader to report it.
1175
+ - **Troubleshooting: "The giscus app is not installed"** — a new section in the
1176
+ feature docs with captured screenshots of both the broken widget and the giscus.app configurator rejecting the repo, an explanation of why this step is uniquely easy to skip, a fork-specific warning, and a copy-pasteable `curl` health check.
47
1177
  - **Obsidian graph frontend revamp** — the graph views now mirror the Obsidian
48
1178
  desktop experience. The full graph page gains a floating **Graph settings**
49
1179
  card (Filters / Display / Forces): search with `tag:`/`path:` operators,
@@ -369,13 +1499,13 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
369
1499
  `index.md`, and navigation. Empty now serialises to `[]`; `deploy` is
370
1500
  deploy-only and also skips agent-file rewrites.
371
1501
  - **github-pages / remote profile emitted a broken `remote_theme`.** The remote
372
- `_config.yml` template resolved `{{GITHUB_REPO}}` to the *site's* repo; it now
373
- uses a dedicated `{{THEME_REMOTE}}` variable (default `bamr87/zer0-mistakes`,
1502
+ `_config.yml` template resolved `{% raw %}{{GITHUB_REPO}}{% endraw %}` to the *site's* repo; it now
1503
+ uses a dedicated `{% raw %}{{THEME_REMOTE}}{% endraw %}` variable (default `bamr87/zer0-mistakes`,
374
1504
  overridable via `THEME_REMOTE`).
375
1505
  - **Deploy workflow templates had unsubstituted variables.** Added
376
- `{{DEFAULT_BRANCH}}`, `{{RUBY_VERSION}}`, and `{{SITE_NAME}}` to the template
1506
+ `{% raw %}{{DEFAULT_BRANCH}}{% endraw %}`, `{% raw %}{{RUBY_VERSION}}{% endraw %}`, and `{% raw %}{{SITE_NAME}}{% endraw %}` to the template
377
1507
  renderer, so the generated `jekyll-gh-pages.yml` (and docker-prod/azure-swa
378
- artifacts) no longer contain literal `{{…}}` tokens. Regression tests assert
1508
+ artifacts) no longer contain literal `{% raw %}{{…}}{% endraw %}` tokens. Regression tests assert
379
1509
  no unresolved tokens survive in any deploy artifact.
380
1510
  - **Agent files were written twice** when `agents` appeared in both the task
381
1511
  list and `SPEC_AGENTS`; `apply.sh` now runs the agents task at most once.
@@ -522,7 +1652,7 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
522
1652
  - **Author avatars never render as protocol-relative `//assets/…` URLs** —
523
1653
  `author-avatar-url.html` now builds the relative-path branch in a capture,
524
1654
  collapses doubled slashes, and applies `relative_url` exactly once, instead
525
- of the manual `{{ site.baseurl }}/{{ site.public_folder }}{{ avatar }}`
1655
+ of the manual `{% raw %}{{ site.baseurl }}/{{ site.public_folder }}{{ avatar }}{% endraw %}`
526
1656
  concatenation that produced `src="//assets/…"` (a URL browsers resolve
527
1657
  against a host named `assets`) on consumer sites where `public_folder` is
528
1658
  unset or carries a leading slash
@@ -1068,7 +2198,7 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
1068
2198
  to it from the Bootstrap, Layouts, and Styles docs.
1069
2199
  - **Bylines now use the shared author component.** The `article`, `note`,
1070
2200
  `notebook`, `news`, and `section` layouts plus `components/post-card.html`
1071
- previously printed `{{ page.author }}` as bare text; they now render
2201
+ previously printed `{% raw %}{{ page.author }}{% endraw %}` as bare text; they now render
1072
2202
  `components/author-card.html` (`inline`), so a known author key resolves to a
1073
2203
  display name, avatar, and a link to their profile page. The inline
1074
2204
  "About the Author" block that was hard-coded in `_layouts/article.html` was
@@ -1215,7 +2345,7 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
1215
2345
  - Version bump: patch release
1216
2346
 
1217
2347
  ### Commits in this release
1218
- - ae76a61f fix(content-review): correct code-fence detection (closing fences + {% raw %}) (#155)
2348
+ - ae76a61f fix(content-review): correct code-fence detection (closing fences + {% raw %}{% raw %}{% endraw %}) (#155)
1219
2349
  - f00fb654 docs(seo): strengthen SEO docs index metadata + fix agent-tier workflow (#154)
1220
2350
 
1221
2351
  ### Fixed
@@ -1225,10 +2355,10 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
1225
2355
  a file's score (e.g. `pages/_about/features/jekyll.md` scored 0/100 almost
1226
2356
  entirely from this false positive). The check now tracks fence open/close state
1227
2357
  and only validates opening fences.
1228
- - **content-review: ignore Liquid `{% raw %}` blocks** in the quality and style
1229
- checks. Code fences, headings, images, and terminology inside `{% raw %}…
1230
- {% endraw %}` are literal display examples, not page structure, and were being
1231
- counted as real findings.
2358
+ - **content-review: ignore Liquid `{% raw %}{% raw %}{% endraw %}` blocks** in the quality and style
2359
+ checks. Code fences, headings, images, and terminology inside a Liquid
2360
+ `{% raw %}{% raw %}{% endraw %}` block are literal display examples, not page
2361
+ structure, and were being counted as real findings.
1232
2362
 
1233
2363
  ## [1.18.0] - 2026-06-13
1234
2364
 
@@ -1656,7 +2786,7 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
1656
2786
  - **Sidebar collapse — VS Code style**: Left sidebar (`#bdSidebar`) and right TOC (`#tocContents`) now collapse to a slim 36 px rail (`--zer0-sidebar-rail-width`) instead of being fully hidden on desktop. The visibility toggle icon (`bi-layout-sidebar-inset` / `bi-layout-sidebar-inset-reverse`) stays mounted on the rail so users can re-expand the panel with a single click — the floating action buttons (`.bd-sidebar-fab`, `.bd-toc-fab`) are now hidden at `≥992 px` since the rail toggle replaces them. `_sass/core/_docs-layout.scss`, `_sass/layouts/_navbar-extras.scss`.
1657
2787
  - **Smooth transitions**: `.bd-layout` and `.bd-main` now animate `grid-template-columns` and `gap` over `--zer0-motion-duration-base` (0.3 s) with `--zer0-motion-ease-standard`; sidebar contents cross-fade via `opacity` + delayed `visibility`. Honors `@media (prefers-reduced-motion: reduce)` by disabling all related transitions.
1658
2788
  - **Toggle behavior**: `sidebar-visibility.js` and `toc-visibility.js` no longer set `button.hidden = true` on the rail toggle when collapsed, keeping it interactive in the collapsed state. Aria labels (`Hide…` / `Show…`) update on each toggle.
1659
- - **Cache-bust**: Added `?v={{ site.time | date: '%s' }}` to the navigation ES-module `<script type="module">` tag in `_includes/components/js-cdn.html` to force re-fetch on rebuild (browsers cache ES modules indefinitely by URL).
2789
+ - **Cache-bust**: Added `{% raw %}?v={{ site.time | date: '%s' }}{% endraw %}` to the navigation ES-module `<script type="module">` tag in `_includes/components/js-cdn.html` to force re-fetch on rebuild (browsers cache ES modules indefinitely by URL).
1660
2790
  - **Navbar dropdown**: Dropdown toggle button set to `align-self: stretch` so it spans the full navbar height, making it easier to invoke on touch/small screens; chevron icon `font-size` increased to `1em` for better legibility. `_sass/core/_navbar.scss`.
1661
2791
  - **Syntax highlighting**: Dual-palette system — `_sass/core/_syntax.scss` now uses a GitHub Light palette for `.highlight` (light mode) and scopes the Material Dark base16 palette to `[data-bs-theme="dark"] .highlight`, fixing near-invisible token colors on light backgrounds.
1662
2792
  - **Theme preview gallery**: Expanded to 20 sections with 6 new components: Callouts (5 types), Accordion, Progress & Spinners, Breadcrumb & Pagination, Tooltips & Popovers, and Icons showcase. TOC updated accordingly; Bootstrap tooltip/popover JS initializer added. `_includes/components/theme-preview-gallery.html`, `pages/_about/settings/theme-preview.md`.
@@ -2701,7 +3831,7 @@ See [`docs/installation/migration-from-0.x.md`](docs/installation/migration-from
2701
3831
  - **Troubleshooting Guide**: Added sections for Docker issues, Jekyll build errors, front matter problems, performance optimization
2702
3832
  - **Jekyll Guide**: Added directory structure, configuration files, content collections, essential commands, topic index
2703
3833
  - **Bootstrap Guide**: Added CDN loading patterns, key components, responsive breakpoints, custom styles, icons
2704
- - **Liquid Guide**: Added syntax examples with `{% raw %}` tags, filters, control flow, includes
3834
+ - **Liquid Guide**: Added syntax examples with `{% raw %}{% raw %}{% endraw %}` tags, filters, control flow, includes
2705
3835
  - **Ruby Guide**: Added version commands, common commands, key files, Docker usage, troubleshooting
2706
3836
  - **Front Matter Guide**: Added required/optional fields, layout options, collection-specific fields, complete examples
2707
3837
  - All documentation pages now include `sidebar: nav: docs` for consistent navigation