jekyll-theme-zer0 1.31.0 → 1.31.1

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.
data/CHANGELOG.md CHANGED
@@ -12,36 +12,17 @@ hand-written prose covering the same releases in more depth; they sit below
12
12
  their version because release-please inserts each new release at the top of the
13
13
  file. Only `## [Unreleased]` describes work that has not shipped yet.
14
14
 
15
- ## [1.31.0](https://github.com/bamr87/zer0-mistakes/compare/v1.30.0...v1.31.0) (2026-10-07)
16
-
17
-
18
- ### Features
19
-
20
- * **navigation:** two clean navbar label tiers, merged chevron, centred grid ([#495](https://github.com/bamr87/zer0-mistakes/issues/495)) ([9ebb29a](https://github.com/bamr87/zer0-mistakes/commit/9ebb29a4662fbc4c62d3fdd8df9c282bdd181204))
21
- * **theme:** four consumer extension points for notes/knowledge-base builds ([#492](https://github.com/bamr87/zer0-mistakes/issues/492)) ([1e2b228](https://github.com/bamr87/zer0-mistakes/commit/1e2b2283303012b28b9e81209fb66d0344aa0597))
15
+ ## [1.31.1](https://github.com/bamr87/zer0-mistakes/compare/v1.31.0...v1.31.1) (2026-10-17)
22
16
 
23
17
 
24
18
  ### Bug Fixes
25
19
 
26
- * **article:** eager-load the hero LCP image and reserve its box ([#498](https://github.com/bamr87/zer0-mistakes/issues/498)) ([529b84b](https://github.com/bamr87/zer0-mistakes/commit/529b84bd0f5ea8e79b7711187b52b2bb80c5b134)), closes [#485](https://github.com/bamr87/zer0-mistakes/issues/485)
27
- * **ci:** keep a LiveReload push from crashing the generic evidence generator ([#500](https://github.com/bamr87/zer0-mistakes/issues/500)) ([eb1b5ae](https://github.com/bamr87/zer0-mistakes/commit/eb1b5ae201ed0829e0c3fac05e629e55a6ea1040))
28
- * **ci:** Playwright 1.58 attachment names, docs front matter, test race ([#497](https://github.com/bamr87/zer0-mistakes/issues/497)) ([f1bdc7e](https://github.com/bamr87/zer0-mistakes/commit/f1bdc7ef22091f0506617b11a95e357c439cfa94))
29
- * **ci:** stop auto-merge guard failing on Dependabot Actions bumps ([#503](https://github.com/bamr87/zer0-mistakes/issues/503)) ([a998b2d](https://github.com/bamr87/zer0-mistakes/commit/a998b2dd9cf96bb1fd09ac3a95644908713afc37))
30
- * **features:** escape `<en-url>` in the translation card so the Features page renders ([#505](https://github.com/bamr87/zer0-mistakes/issues/505)) ([8e4345f](https://github.com/bamr87/zer0-mistakes/commit/8e4345f28dfc675b5efe95ba66ec72af613cbcf5))
31
- * **includes:** join preview paths without a leading slash under assets_prefix ([#509](https://github.com/bamr87/zer0-mistakes/issues/509)) ([406250c](https://github.com/bamr87/zer0-mistakes/commit/406250c2b5b94469cae9a0b6f93e8782cb8509f9))
32
- * **includes:** stop the draft chip shifting the Site Builder on first save ([#489](https://github.com/bamr87/zer0-mistakes/issues/489)) ([d472aa2](https://github.com/bamr87/zer0-mistakes/commit/d472aa267e164212cf8b844e701cfcdad08e4d8d))
33
- * **layouts:** one main landmark per page on post, notebook and note ([#487](https://github.com/bamr87/zer0-mistakes/issues/487)) ([78b0910](https://github.com/bamr87/zer0-mistakes/commit/78b0910fb2258591eab9ba13efb5ac037eee80c5))
34
- * **navigation:** stop Liquid whitespace-trim gluing navbar attributes ([#477](https://github.com/bamr87/zer0-mistakes/issues/477)) ([4349ea1](https://github.com/bamr87/zer0-mistakes/commit/4349ea17d690e5e6b8585f1d3a33b3ea4d3bf4fe))
35
- * **scripts:** default xAI preview renderer to grok-imagine-image-2.0 ([#499](https://github.com/bamr87/zer0-mistakes/issues/499)) ([95cebdb](https://github.com/bamr87/zer0-mistakes/commit/95cebdb535799544f05ed5929e528a074448ef63)), closes [#474](https://github.com/bamr87/zer0-mistakes/issues/474)
36
- * **tests:** make the Liquid nested-tag and gem-content checks able to fail ([#478](https://github.com/bamr87/zer0-mistakes/issues/478)) ([8510d08](https://github.com/bamr87/zer0-mistakes/commit/8510d0824f79823f66f765f12db9be9b67fff3ae))
37
- * **tokens:** let the active skin win over site theme_color for skin-owned tokens ([#502](https://github.com/bamr87/zer0-mistakes/issues/502)) ([af468e0](https://github.com/bamr87/zer0-mistakes/commit/af468e072543a448e7be80230f3d3a246b5e758e))
38
- * **ui:** make the cookie consent banner a compact bottom bar ([#510](https://github.com/bamr87/zer0-mistakes/issues/510)) ([2d7d0db](https://github.com/bamr87/zer0-mistakes/commit/2d7d0db7f9f6b1433b63d8be76103cce2fd7fb05))
39
-
40
-
41
- ### Performance Improvements
42
-
43
- * **js:** lazy-load Mermaid and cytoscape, defer PostHog, stop nav reflows ([#508](https://github.com/bamr87/zer0-mistakes/issues/508)) ([702789c](https://github.com/bamr87/zer0-mistakes/commit/702789cecf93d8e9b9f7013168ca47f0294064eb))
44
- * **sitemap:** keep one page of the index in the DOM and build cards lazily ([#511](https://github.com/bamr87/zer0-mistakes/issues/511)) ([9e42fcb](https://github.com/bamr87/zer0-mistakes/commit/9e42fcbd9421a994fd22a347bbef95573ce340a9))
20
+ * **a11y:** name mobile intro actions, 24px nav chevrons, badge links ([#507](https://github.com/bamr87/zer0-mistakes/issues/507)) ([59963a9](https://github.com/bamr87/zer0-mistakes/commit/59963a958fb08cff9d49c00eac9e4c430d70a5c5))
21
+ * **color-mode:** mark the selected mode pressed on first paint ([#521](https://github.com/bamr87/zer0-mistakes/issues/521)) ([#526](https://github.com/bamr87/zer0-mistakes/issues/526)) ([2a60e73](https://github.com/bamr87/zer0-mistakes/commit/2a60e7328709f9902721fb83f7d2923d0c3f204a))
22
+ * **footer:** keep newsletter hint in the a11y tree below md ([#522](https://github.com/bamr87/zer0-mistakes/issues/522)) ([#525](https://github.com/bamr87/zer0-mistakes/issues/525)) ([90489d3](https://github.com/bamr87/zer0-mistakes/commit/90489d36a73bdb7f7182219efed6a399a145b388))
23
+ * **includes:** drop orphaned itemprop metas from &lt;head&gt; ([#517](https://github.com/bamr87/zer0-mistakes/issues/517)) ([47f4ff8](https://github.com/bamr87/zer0-mistakes/commit/47f4ff8f8f44933d65a7d38e6b0552d2288f8180)), closes [#513](https://github.com/bamr87/zer0-mistakes/issues/513)
24
+ * **navigation:** keep unlinked crumbs out of the BreadcrumbList ([#518](https://github.com/bamr87/zer0-mistakes/issues/518)) ([4fda7ab](https://github.com/bamr87/zer0-mistakes/commit/4fda7ab63a31c3614df30a13d38453568a7d5ff5))
25
+ * **tests:** serve the Visual Snapshots stack without LiveReload ([#530](https://github.com/bamr87/zer0-mistakes/issues/530)) ([6f61421](https://github.com/bamr87/zer0-mistakes/commit/6f6142182654b09a15c6460bf1643d2b6719a5b2))
45
26
 
46
27
  ## [Unreleased]
47
28
 
@@ -52,68 +33,422 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
52
33
  - `styles:` / `scripts:` frontmatter — per-page CSS and JS resolved through `relative_url`, with `scripts:` deferred after the theme bundle. A page that declares neither emits nothing at all.
53
34
  - `lastmod` and `description` on every `/assets/data/wiki-index.json` entry — ISO-8601 and the authored subtitle, always defined (`null` when the document has neither), no new traversal.
54
35
 
55
- ### Changed
36
+ - **Bring your own AI provider — Claude or Grok — and an open, coding-session-style
37
+ Site Builder (ZER0-087)** — the chat proxy gains a provider layer
38
+ (`templates/deploy/chat-proxy/providers.js`): xAI's Grok is a first-class
39
+ provider (`XAI_API_KEY`, `CHAT_PROVIDER`, `XAI_CHAT_MODEL`) for the chat
40
+ widget, the feedback triage and the Site Builder, translated server-side to
41
+ and from the Anthropic Messages dialect so no client changed. The Site
42
+ Builder's Connect step shows which providers the local dev proxy already has
43
+ keys for (masked), takes a token for the session (sent once to localhost,
44
+ kept in the proxy's memory, optionally saved to `.env` with mode 600), offers
45
+ the model catalog, an image renderer (Grok Imagine / OpenAI Images) and two
46
+ session modes: the guided nine steps, or an open session that can open an
47
+ existing site under the target root, read/edit/write/delete its files, run
48
+ `git status`/`diff`/`log`, generate images into `assets/`, run `jekyll build`
49
+ inside the container and stop a long tool run. Generated sites are
50
+ pre-configured for the chosen provider (`ai_chat.provider`, `preview_images`,
51
+ `.env.example`). The dev proxy now starts without a credential. Also fixes a
52
+ pre-existing 162px horizontal overflow of the whole `/setup/` page at phone
53
+ widths (the Connect step's command boxes could not shrink). (evidence:
54
+ [`test/visual/evidence/site-builder-providers/`](test/visual/evidence/site-builder-providers/README.md)
55
+ — Connect step: 2 provider cards, 2 session modes, token field is a password
56
+ input inert offline; mocked proxy: "needs a token" → "Grok connected"; Build:
57
+ existing sites listed, Open sets the target; mobile overflow 162px → 0px)
58
+ - **CI now produces a UI pull request's visual artifacts instead of only
59
+ checking for them (ZER0-085)** — `visual-evidence-autogen.yml` renders every
60
+ same-repo PR in the same jammy Playwright image the snapshot gate uses, runs
61
+ the PR's `test/visual/*-evidence.mjs` generators (or the new generic
62
+ base-vs-head generator `test/visual/pr-evidence.mjs`, which renders the base
63
+ branch and the head side by side), verifies the 9-skin baselines, and pushes
64
+ the montages + `metrics.json` to the branch. Stale baselines are refreshed
65
+ **only** when the new `visual-evidence-reviewer` agent, having viewed the
66
+ expected | actual | diff montage, judges the diff to be the change the PR
67
+ describes — a code step disposes, the model only proposes ([#417](https://github.com/bamr87/zer0-mistakes/issues/417)
68
+ is why). The evidence gate now requires generated proof (a README alone no
69
+ longer passes), `ci-self-repair` leaves a red `Visual Snapshots` job to this
70
+ lane, and `test/update-snapshots.sh` gained `PRE_TEST_SCRIPT` /
71
+ `POST_TEST_SCRIPT` / `SKIP_PLAYWRIGHT` hooks. Closes the gap that kept
72
+ [#454](https://github.com/bamr87/zer0-mistakes/pull/454) red: its authoring
73
+ agents ran where Docker was gated, and nothing in CI could render what they
74
+ could not. Kill switch: repo variable `VISUAL_EVIDENCE_AUTOGEN_ENABLED=false`.
75
+ The lane restores its own tooling from the base branch before it runs, so it
76
+ works on branches cut before it shipped: without that its first real run on
77
+ #454 died in 20 seconds (that branch has no copy of the orchestrator), and a
78
+ branch carrying an older `update-snapshots.sh` would have silently generated
79
+ nothing at all.
80
+ - **Site Builder — a Claude-guided setup wizard that ends with a running site
81
+ (ZER0-086, extends ZER0-067)** — `/setup/` (and the `welcome` layout) grew
82
+ from a five-step `_config.yml` form into a nine-step builder: Connect →
83
+ Prerequisites → Identity → URLs → Structure → Appearance → Voice →
84
+ Integrations → Build. An embedded Claude session rides alongside every step
85
+ through the local dev proxy (Claude Code OAuth, `claude setup-token`; the
86
+ token never reaches the page). It sees the whole wizard state on every turn
87
+ and acts on it with tools: fill in fields, override a generated file, run
88
+ live prerequisite checks (Docker, Git, gh, VS Code, Node, Claude CLI — a
89
+ fixed command table), read the theme's real source and search the docs,
90
+ resolve a project folder, write the generated project, run `docker compose
91
+ up|ps|logs|down`, and check the new site answers — each mutation behind a
92
+ confirmation card. The form half still works with no proxy at all (GitHub
93
+ Pages included): it generates `_config.yml`, `_config_dev.yml`, `Gemfile`,
94
+ `docker-compose.yml`, `index.md`, navigation, about page, welcome post, one
95
+ index page per collection, `.gitignore`, `zer0.install.yml`, `.env.example`
96
+ and `README.md`, with per-file download and a self-extracting bash bundle.
97
+ Forms gained a site brief, a URL **Suggest** button, a site-type quick-pick,
98
+ a navigation row editor, skin cards with a live **Preview on this page**
99
+ toggle, tone/audience pickers, integration switches with conditional
100
+ sub-fields, a copy button on every command, and a toast for feedback. New:
101
+ `_data/site_builder.yml` (steps, prerequisites mirroring
102
+ `machine-setup.md`, catalogs, framework brief),
103
+ `templates/deploy/chat-proxy/wizard-store.mjs` (the dev-only sandbox behind
104
+ `/api/wizard/*`), `assets/js/site-builder.js`, `_includes/setup/{claude-
105
+ session,prereq-checklist}.html`, the `site_builder:` config block, a
106
+ quickstart page and a feature reference. (evidence:
107
+ [`test/visual/evidence/site-builder/`](test/visual/evidence/site-builder/README.md))
108
+ A first recorded end-to-end run then shaped the build half: the generated
109
+ `docker-compose.yml` publishes LiveReload on the site's port + 1 (a fixed
110
+ 35729 collided with the theme's own dev container and the new site never
111
+ started) and shares one gem cache across generated sites; every enabled
112
+ collection now ships a valid starter document (a doc, a quickstart step, a
113
+ note with a wiki-link, a structured recipe) and the cookbook index uses the
114
+ theme's `cookbook` layout over the real `recipes` collection; `_config.yml`
115
+ sets `collections_dir: pages` (without it every collection but posts was
116
+ invisible to Jekyll); each site gets its own `assets/images/logo.svg`
117
+ monogram and gem-based sites turn the SVG background layers off, because
118
+ the published gem ships no theme images; a revoked Claude credential now
119
+ says so and names the fix. Validation is a scenario
120
+ runner, `test/visual/site-builder-walkthrough.mjs`, that samples random
121
+ briefs per site type (`test/visual/site-builder-scenarios.mjs`), drives the
122
+ wizard on video, builds the site with Docker and asserts its routes, title
123
+ and skin — replayable by seed.
124
+ The builder then gained a **schema-driven site plan**: `plan_schema` in
125
+ `_data/site_builder.yml` (a JSON-Schema subset validated in the browser)
126
+ describes what the agent may produce — a landing page (five templates,
127
+ nine section types, hero + CTAs), navigation shape (flat or grouped
128
+ dropdowns, sidebar none/auto/docs tree), theme overrides (nine palettes or
129
+ custom colours, seven font pairings, three corner radii) and up to twelve
130
+ example pages with Markdown bodies — and the generators turn it into
131
+ `index.md` (a Liquid landing engine) + `_data/landing.yml`,
132
+ `_data/navigation/main.yml` and `docs.yml`, `assets/css/user-overrides.css`
133
+ + `_includes/custom/head.html`, and one file per page. Claude submits plans
134
+ with `set_site_plan` behind a confirmation card; the Structure and
135
+ Appearance steps expose the same choices as controls, with a page planner
136
+ and a **Preview on this page** toggle that applies the generated overrides
137
+ to the wizard itself. The scenario runner randomises the plan too and
138
+ asserts the built site's landing template, palette colour, web fonts and
139
+ planned routes.
140
+ - **Mermaid diagrams are now accessible figures with a toolbar (ZER0-013)** —
141
+ every ```` ```mermaid ```` fence (and legacy `<div class="mermaid">`) renders
142
+ as a `<figure>` with a rendered SVG and a small toolbar: zoom out / in / reset
143
+ (50–400 %, real layout — the SVG is resized, so the frame scrolls and
144
+ drag-to-pan, `Ctrl`+wheel and the `+` / `-` / `0` keys all work), **View
145
+ fullscreen** (a native `<dialog>` — the fix for a wide diagram shrunk to
146
+ illegibility on a phone; `Esc` closes it and focus returns to the opener),
147
+ **Copy diagram source**, and **Download as SVG** (page background baked in so
148
+ a dark-mode export stays readable). On pointer devices the toolbar floats over
149
+ the top-right corner on hover/focus; on touch devices it is a static row above
150
+ the diagram so it never covers it. `accTitle` becomes the visible
151
+ `<figcaption>` and the diagram's accessible name, `accDescr` its `<desc>`,
152
+ and the scrollable frame is a focusable, labelled region. A syntax error no
153
+ longer blanks the block: the figure shows the parse message, a hint, and the
154
+ source in a `<details>`, with *copy* still enabled. Colours are **derived from
155
+ the live design tokens** (`--bs-primary`, `--bs-body-bg`, `--zer0-color-*`)
156
+ through Mermaid's `base` theme, so diagrams follow the colour mode, the
157
+ selected skin and any `theme_color` override, and re-render in place when
158
+ those change; dark mode is decided by background luminance, so wizard mode and
159
+ dark skins get legible ink, and a contrast guard keeps nodes visible when the
160
+ brand equals the page colour. Pie slices, git branches and mind-map branches
161
+ get a 12-colour series fanned out from the brand hue. `securityLevel` now
162
+ defaults to `strict` (`mermaid.security_level: loose` restores `click`
163
+ callbacks / HTML labels); `mermaid.toolbar`, `fullscreen` and `download` can
164
+ switch the controls off. Implemented as `_includes/components/mermaid.html`
165
+ (loader: config JSON + two `defer` scripts — the 3.3 MB bundle used to load
166
+ synchronously in `<head>`) + `assets/js/mermaid-diagrams.js` +
167
+ `_sass/components/_mermaid.scss`, replacing ~400 lines of inline
168
+ `<script>`/`<style>` in the include; the `!important` SVG overrides that
169
+ flattened `classDef`/`style` colours and pie slices are gone. Toolbar strings
170
+ are `diagram_*` keys in `_data/ui-text.yml`. The docs page
171
+ (`/docs/features/mermaid-diagrams/`) now renders a live example of every
172
+ diagram type, the caption directive, per-node styling and the error state.
173
+ Guarded by `test/visual/features/mermaid.spec.js` (12 tests, smoke tier)
174
+ (evidence: [`test/visual/evidence/mermaid/`](test/visual/evidence/mermaid/README.md)
175
+ — dark-mode toggle keeps the SVG (before: SVG lost, stylesheet text on
176
+ screen); page overflow 0px at 320/390px; 11/12 docs diagrams rendered, 1
177
+ shown as an error card with its source kept).
56
178
 
57
- - **Navbar label tiers reduced from three to two** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)).
58
- Top-level nav items now render as icon + full label above 51rem of centre
59
- track and icon-only below it; `text-overflow: ellipsis` is gone, so a label
60
- is never cut to "Quicksta…". Icons now survive in **both** tiers — the old
61
- 41–50.99rem tier dropped them to buy width for bare labels. **Behaviour
62
- change:** centre-track widths between 41rem and 51rem (roughly 992–1200px
63
- viewports) previously showed bare labels and now show icons, with the
64
- existing `.nav-tooltip` carrying the name on hover.
65
- - **Dropdown chevrons merged into their parent row** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)).
66
- The split toggle is now absolutely positioned inside padding the parent link
67
- reserves, and hover applies to the whole row, so there is no dead zone
68
- between the label and its chevron. It remains a real `<button>` with
69
- `aria-expanded`, `aria-haspopup` and its visually-hidden label unchanged.
70
- (evidence: [`test/visual/evidence/navbar-tiers-405/`](test/visual/evidence/navbar-tiers-405/README.md) — chevron dead zone up to 4px → overlapping; 0 truncated labels across 6 widths)
71
- - **The navbar carries logo + title only** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)).
72
- `site.subtitle` moved out of `_includes/core/branding.html` and onto the home
73
- hero (`_layouts/home.html`), returning its width to the menubar. It renders
74
- under the page heading on pages that use the `home` layout, and is hidden
75
- with it by `hide_title` or `hide_intro`. A homepage on another layout, such
76
- as `landing`, no longer shows the subtitle at all; add `site.subtitle` to
77
- that layout's hero to keep it.
78
- - **The below-`lg` menu toggle is labelled "Menu"** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)),
79
- so it is distinguishable from the sidebar hamburger beside it. Override the
80
- string with `ui.nav_menu_toggle_label`.
81
- - The desktop navbar grid gives its two side tracks a shared minimum
82
- (`--zer0-navbar-side-min`, default `9rem`) so the menubar is optically
83
- centred in the bar rather than only within its own track.
179
+ - **Cookbook & recipes collection (ZER0-084)** — a `recipes` collection that
180
+ renders structured front matter as a working recipe page: `layout: recipe`
181
+ builds a fact bar (prep/cook/rest/total, yield, difficulty, cuisine, oven),
182
+ a grouped ingredient checklist, a numbered method with per-step timings and
183
+ temperatures, a baker's-percentage ratio table computed at build time, a
184
+ per-serving nutrition panel, and one `schema.org/Recipe` JSON-LD block.
185
+ `assets/js/recipe-scaler.js` adds serving scaling and live US↔metric
186
+ conversion — including volume↔weight via `_data/ingredient_densities.yml`
187
+ — as progressive enhancement, so every amount is still rendered exactly as
188
+ authored with JavaScript disabled. `layout: cookbook` indexes recipes into
189
+ course sections from `_data/recipe_courses.yml`. Twelve new
190
+ `components/recipe-*.html` includes, `_sass/components/_recipe.scss` (with
191
+ print styles), a demo cookbook at `/recipes/`, and docs at
192
+ [Cookbook collection](pages/_docs/features/cookbook-collection.md). Regression
193
+ coverage: `test/visual/features/recipes.spec.js` (smoke tier) and "Cookbook
194
+ Recipe Collection" in `test/test_core.sh` (evidence:
195
+ [`test/visual/evidence/recipes-collection/`](test/visual/evidence/recipes-collection/README.md)
196
+ — 0px page overflow across 320–1440px; 1 cup butter → 227 g, 500 g flour →
197
+ 4 cups; ratio percentages identical at 1× and 3×).
84
198
 
85
- ### Fixed
199
+ - **Page-view counter (ZER0-083)** — the theme now tracks a view for the page
200
+ being read and displays the count in the article meta row, next to the
201
+ reading time (`👁 12 views`). Two providers, configured under `page_views:`
202
+ in `_config.yml`: `local` (the default — counts live in the visitor's
203
+ `localStorage`, so it works on GitHub Pages with no server, no network, and
204
+ no third party) and `remote` (counts come from an HTTP counter endpoint you
205
+ control, with `{path}` substitution, a configurable method, and a dotted
206
+ `count_key` for the response shape). Privacy gates mirror the `posthog:`
207
+ block — Do Not Track, Global Privacy Control, and an optional
208
+ `require_consent` tie-in to the cookie-consent "analytics" category — and
209
+ they suppress *recording* only, never the display of a count that is already
210
+ known; a view blocked pending consent is recorded when consent arrives.
211
+ `dedupe: session` counts one view per page per browser session so reloads do
212
+ not inflate it. The badge include (`components/page-views.html`) ships hidden
213
+ and is revealed only once a count exists, carrying its own leading separator
214
+ so a page with no data leaves no dangling bullet. New:
215
+ `_includes/components/page-views.html`,
216
+ `_includes/components/page-views-init.html`, `assets/js/page-views.js`,
217
+ `_sass/components/_page-views.scss`. Regression spec:
218
+ `test/visual/features/page-views.spec.js`. Docs:
219
+ [`docs/features/page-views.md`](docs/features/page-views.md)
220
+ (evidence: [`test/visual/evidence/page-views/`](test/visual/evidence/page-views/README.md)).
86
221
 
87
- - **The cookie banner no longer covers the first screen.** Below 768px `components/cookie-consent.html` stacked a heading, a long sentence and three full-width buttons (the shim in `_sass/core/_sidebar-extras.scss`): 324px of an 850px-tall 400px viewport on it-journey.dev and lifehacker.dev, hiding most of the hero or article; on desktop it was 94px and sat over the lower hero copy. It is now a compact bar: a two-line summary over one row of three equal buttons on phones, and a single row from 768px up. The layout lives in `_sass/components/_cookie-banner.scss` and the stacking shim is gone. The banner is a labelled `region` ("Cookie consent"), and the button ids, the modal and the consent logic are unchanged. Height at 400px drops from 324px to 107px, and at 1366px from 94px to 48px. Pinned by `test/visual/features/cookie-banner-compact.spec.js`.
88
- - **The active skin now moves the whole `--zer0-color-*` layer, not just Bootstrap's** ([#459](https://github.com/bamr87/zer0-mistakes/issues/459)). `_includes/core/tokens-inline.html` re-set `--zer0-color-primary`, `-link` and `-accent` from `theme_color` on `:root` after `main.css`. `:root` and `[data-theme-skin]` have the same specificity, so the config value won on source order. On any site that sets `theme_color`, components reading the theme layer therefore kept the config color in every skin, and `--zer0-color-primary` disagreed with its own `-rgb` companion. The skin now wins: those three keys are emitted only when no palette skin is active, as with `dark` and `contrast`. `theme_color` still sets `secondary`, `danger`, `warning`, `success` and `info`, and the Appearance-panel override still beats both. The rule is documented in [`docs/ui/design-tokens.md`](docs/ui/design-tokens.md#precedence-skin-vs-theme_color) and checked by `test/visual/features/appearance.spec.js` (evidence: [`test/visual/evidence/agent-issue-459/`](test/visual/evidence/agent-issue-459/README.md) — page overflow 0px at six widths; the nine skin baselines are unchanged).
89
- - **A preview path without a leading slash no longer 404s.** `preview: images/previews/x.webp` was appended straight onto `assets_prefix`, so `components/preview-image.html`, `components/background-image.html`, `content/intro.html` and `content/seo.html` emitted `/assetsimages/previews/x.webp` for the card image, the hero background and `og:image` (165 it-journey content files use that form). All four now prepend `/` to a site-relative path before joining; URLs and `data:` URIs are left alone, and already-slashed or `/assets/`-prefixed paths render byte-identically. `preview-image.html` also gained optional `width`/`height` parameters (emitted only when passed); the article layout's related-post cards pass 1536×1024 so their box is reserved before the lazy image arrives (0px → 217px at 1366px). Pinned by the `Preview Path Join` unit test in `test/test_core.sh` and `test/visual/features/preview-path-join.spec.js`.
90
- - **The sitemap index no longer puts every page in the DOM twice.** `content/sitemap.html` (the `/sitemap/` page, the search modal's "View all results" target, and it-journey.dev's `/search/`) kept all rows attached. Its script also eagerly built a second copy of every row as a card for the hidden card view: about 19,900 elements on it-journey's `/search/` (431 entries), with Lighthouse mobile TBT at 9.7 s. Rows are still rendered server-side, so the page works without JS and with `?q=`. The script now keeps only one page of 50 matches in the document and appends the next page with **Show more**. Search, filters and sorting work across every row, and cards are built on the first switch to card view, one page at a time. Card text is set through `textContent` rather than interpolated HTML. On the theme's own `/sitemap/` (386 entries), elements after load drop from 23,063 to 8,086, and local Lighthouse mobile TBT drops from 1,662 ms to 181 ms. Badge contrast is fixed too: the collection pill is `text-bg-info` (was white on `#0dcaf0`, 1.96:1), tag chips use `--bs-primary-text-emphasis`, and `+N` overflow chips (white on transparent) get body text and a border. Pinned by `test/visual/features/sitemap-pagination.spec.js`.
91
- - **The xAI preview-image renderer defaults to `grok-imagine-image-2.0`** ([#474](https://github.com/bamr87/zer0-mistakes/issues/474)). `XAIProvider.default_model()` in `scripts/lib/preview_generator.py` still returned the retired `grok-2-image`, so a site with `preview_images.provider: xai` and no explicit `model:` sent a retired model upstream. An explicitly configured `grok-2-image` is still accepted as an xAI-family model.
92
- - **A code-copy partial no longer restyles every button on the site** ([#412](https://github.com/bamr87/zer0-mistakes/issues/412)). `_sass/core/code-copy.scss` shipped a bare `.button, button:not(.copy)` rule — an element selector at specificity (0,1,1) that out-ranked `.btn` and any consumer's own class, applying `padding: 0 20px`, `font-size: 11px` and a hardcoded `#bbb` border site-wide. It is scoped to `.code-block-header` / `pre.highlight` now and uses `--zer0-*` tokens, so `#bbb` is gone from the compiled stylesheet. Every button on the site — the theme's own chrome included — gets its intended metrics back (evidence: [`test/visual/evidence/agent-issue-412/`](test/visual/evidence/agent-issue-412/README.md) — page overflow 0px at six widths; the nine skin baselines moved, and every red region in their diffs is a `<button>`).
93
- - **The article hero no longer lazy-loads its LCP image or shoves the article down when it arrives** ([#485](https://github.com/bamr87/zer0-mistakes/issues/485)). `figure.featured-hero` inherited `components/preview-image.html`'s `loading="lazy"` default and had no reserved box. It now passes `loading="eager" fetchpriority="high" decoding="async"`, and its box is pinned by `.featured-hero img { aspect-ratio: 3 / 2 }`. The box is set in CSS rather than with `width`/`height` attributes, because preview assets vary in shape and `object-fit: cover` crops a portrait instead of stretching it. `preview-image.html` gained optional `fetchpriority`/`decoding` parameters that are emitted only when passed, so every other consumer renders byte-identically (evidence: [`test/visual/evidence/hero-lcp/`](test/visual/evidence/hero-lcp/README.md) — hero loading lazy → eager + fetchpriority=high; content shift when the hero lands 476px → 0px across 2 widths × landscape/portrait).
94
- - **Mermaid, cytoscape and PostHog no longer load up front, and the auto-hide navbar stops forcing reflows.** `assets/js/mermaid-diagrams.js` now converts fences into figures straight away but injects the ~1 MB Mermaid bundle only when a diagram exists and the first one comes within 400px of the viewport (IntersectionObserver); `components/mermaid.html` passes the bundle URL as `window.zer0MermaidSrc` (a script global, not DOM text, and only http(s) URLs are injected) instead of a `<script defer>`. The local-graph panel loads cytoscape on first open instead of on every indexed page. `analytics/posthog.html` keeps the queueing stub but fetches `array.js` after `load` + idle, and `core/head.html` no longer includes it a second time (`root.html` already did, so every production page called `posthog.init()` twice). `auto-hide-nav.js` measures the navbar with a ResizeObserver (rAF read/next-frame write fallback) and caches the height instead of reading `offsetHeight` at DOMContentLoaded and on resize. Lighthouse mobile, local static build: home TBT 1,705 → 651 ms and weight 5,581 → 2,327 KiB; `/docs/features/mermaid-diagrams/` TBT 2,017 → 243 ms; `auto-hide-nav.js` gone from forced-reflow. New lazy-load test in `test/visual/features/mermaid.spec.js`.
95
- ### Fixed
222
+ - **Search shortcut affordances (ZER0-032)** — the navbar search button now
223
+ advertises its keyboard shortcuts, matching the design-system navbar
224
+ pattern: a quiet `/` kbd chip (xl-up), a hover title ("Search — press / or
225
+ Ctrl+K", shown as `⌘K` on macOS), and `aria-keyshortcuts`; the sidebar (`b`)
226
+ and table-of-contents (`t`) hide toggles gained shortcut hover hints, and
227
+ the keyboard-shortcuts modal now lists `⌘/Ctrl+K`
228
+ (evidence: [`test/visual/evidence/search-shortcut-hint/`](test/visual/evidence/search-shortcut-hint/README.md)).
96
229
 
97
- - **The Site Builder no longer jumps the first time it saves a draft.**
98
- `showDraftChip()` cleared the "Draft saved" chip's `hidden` attribute, and the
99
- 2s timer that follows only removes `is-visible` — an opacity class — so the
100
- first debounced save moved the chip from `display: none` into layout *for
101
- good*. Its box is 26px against the 19px "Start over" button beside it, so
102
- `.wizard-header` grew and took `#wizardTabContent` and every Back/Next row
103
- below it down with it: 7px where the header's right-hand block had already
104
- wrapped, a whole wrapped line where those 7px were what tipped it over. The
105
- chip now holds its box from first paint and is hidden with `visibility`,
106
- which is what `_setup-wizard.scss` said it did all along ("Kept in the layout
107
- (no display toggling) so its appearance never shifts the header") and still
108
- keeps it out of the accessibility tree when it has nothing to say. This is
109
- what failed the `@critical` stable-height spec on every push to `main`
110
- (`2029, 2036, 2036, …`), and with it the retry that spec's failure forced
111
- ([bamr87/bamr87#265](https://github.com/bamr87/bamr87/issues/265)) (evidence:
112
- [`test/visual/evidence/agent-issue-265/`](test/visual/evidence/agent-issue-265/README.md)
113
- — first draft save moved the wizard 42px on the base branch, 0px here).
230
+ - **Live design-system pages (ZER0-082)** — the design system is now
231
+ part of the site itself: `_design-system/` is force-included in the Jekyll
232
+ build so `styles.css`, every token file, specimen cards, and component
233
+ sources get real URLs under `/_design-system/`; a new `/design/` hub page
234
+ renders the foundation specimens in live iframes (colors, skins, type,
235
+ spacing, radii, motion, layers, iconography), catalogs the component twins,
236
+ and links the Claude Design page canvas (all 24 layouts as a browsable
237
+ presentation). Navbar: About → Design System. Regression spec:
238
+ `test/visual/features/design-hub.spec.js`.
239
+
240
+ - **Claude Design system round-trip (ZER0-081)** — the design-token layer is now
241
+ aligned with the zer0-mistakes Claude Design project
242
+ (`claude.ai/design/p/e75121c0-9210-42d1-ade3-2c8af9111cbe`): new
243
+ `--zer0-radius-*`, `--zer0-logo-*`, and `--zer0-text-display-*` tokens in
244
+ `_sass/tokens/`; the design project vendored as a git mirror under
245
+ `_design-system/` (plain-CSS tokens, React component specs, guideline cards,
246
+ website UI kit — sync contract in `_design-system/SYNC.md`); and
247
+ `scripts/design-system-check.rb` wired into the core test suite so the theme
248
+ and the mirror cannot drift silently. Callouts, landing feature cards, mobile
249
+ nav, tables, and focus rings now consume the radius tokens (value-preserving —
250
+ rendered output unchanged, pinned by a new runtime token assertion in
251
+ `test/visual/core/styling.spec.js`).
252
+ - **Design-system component twins (PR #391)** — `Fab`/`FabStack`, `PostCard`,
253
+ and `Skeleton` join Button/Badge/Card/FeatureCard/Callout/Input in
254
+ `_design-system/components/`, plus new Motion and Layers foundation cards;
255
+ all pushed live to the Claude Design project. The feature registry is
256
+ re-synced (`features/features.yml` ↔ `_data/features.yml` — #389's ZER0-081
257
+ entry had landed in the `_data` mirror only, which the registry validator
258
+ rejects) and `docs/ui/components.md` gains a theme-source →
259
+ design-system-twin map.
260
+
261
+ - **`giscus-discussions doctor`** — a new subcommand that verifies the entire
262
+ 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.
263
+ - **Giscus health check in CI** — `.github/workflows/giscus-digest.yml` (already
264
+ 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.
265
+ - **Troubleshooting: "The giscus app is not installed"** — a new section in the
266
+ 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.
267
+ - **Obsidian graph frontend revamp** — the graph views now mirror the Obsidian
268
+ desktop experience. The full graph page gains a floating **Graph settings**
269
+ card (Filters / Display / Forces): search with `tag:`/`path:` operators,
270
+ per-collection toggles generated from the wiki-index (count badge + color
271
+ swatch, doubling as the legend), show-orphans and show-unresolved switches,
272
+ node-size / link-thickness / label-fade sliders, repel-force and
273
+ link-distance sliders with animated re-layout, plus a zoom / fit / fullscreen
274
+ button stack, an Obsidian-style hover preview card, live dark-mode restyling,
275
+ and localStorage persistence. The local graph panel gains a depth selector
276
+ (1–3) and outgoing/incoming direction switches that re-render the subgraph in
277
+ place. Same vendored cytoscape.js, unchanged `wiki-index.json` contract
278
+ (evidence:
279
+ [`test/visual/evidence/obsidian-graph-revamp/`](test/visual/evidence/obsidian-graph-revamp/README.md);
280
+ regression: `test/visual/features/obsidian-graph.spec.js`).
281
+
282
+ - **Theme-canary reporting contract** (`docs/systems/theme-canary-contract.md`) —
283
+ the canonical upstream bug-intake protocol for downstream theme-scout agents
284
+ (it-journey, lifehacker.dev): required `theme-canary` + `from-<consumer>`
285
+ labels, a deterministic 12-hex dedupe key + hidden body marker, the mandated
286
+ open-AND-closed pre-filing search, a 5-issues-per-consumer-per-run cap, and a
287
+ required evidence format that records the theme version/commit observed (so
288
+ fixed-in-release triage is mechanical). Ships with the matching
289
+ `theme-canary.yml` issue form.
290
+
291
+ - **`examples/` — self-contained demo sites built on the theme.** New top-level
292
+ directory (excluded from the theme's own site build and from the gem) for
293
+ complete Jekyll sites that show the theme configured for a particular kind of
294
+ site. Each has its own `_config.yml`, `Gemfile`, and content, and builds
295
+ against the local theme sources via `_config_dev.yml`.
296
+ - `examples/swerve-of-shore/` — **Swerve of Shore**, a serialized literary
297
+ reading blog rebuilt from
298
+ [swerveofshore.com](https://www.swerveofshore.com/) with the site owner's
299
+ permission. 52 pages, 0 broken internal links. Exercises a `series` output
300
+ collection with nested per-section paths, a data-driven taxonomy
301
+ (`_data/episodes.yml`) that generates the sidebar rail, the episode
302
+ schedule with live entry counts, and a section page for every node — so a
303
+ nav target resolves even where nothing is written yet. Also covers article
304
+ hero images (`show_hero`), entries ordered by number rather than date, the
305
+ shared `post-card` component in a single-column feed, and a full editorial
306
+ light-mode restyle done entirely through `assets/css/user-overrides.css` —
307
+ no theme files touched. The site's identity, taxonomy, entry titles/dates,
308
+ and featured images are reproduced, along with a short quoted excerpt per
309
+ entry (the opening the original publishes in its own RSS feed), attributed
310
+ and linked back. The essays themselves are not reproduced.
311
+ - `examples/README.md` — how to run one and the conventions for adding
312
+ another.
313
+ - **Theme propagation system — releases now reach the sites built on them.**
314
+ Releasing ended at RubyGems; nothing told the five downstream consumers a new
315
+ version existed, so all three pinned sites were still on v1.26.0 when v1.27.0
316
+ shipped and the two unpinned ones had been silently tracking `main`.
317
+ - `_data/consumers.yml` — registry of every consumer, its consumption mode
318
+ (`remote_theme_pinned` / `remote_theme_floating` / `gem` / `path`), and every
319
+ file that holds a version pin.
320
+ - `scripts/propagate.rb` — reports pin drift across the registry
321
+ (`--format text|json|github`, `--strict`) or dispatches a `theme-release`
322
+ event to each consumer (`--dispatch`).
323
+ - `.github/workflows/propagate-theme.yml` — dispatches on release publish;
324
+ weekly drift report. Never pushes to a consumer: each one opens its own PR so
325
+ its own CI gates the bump.
326
+ - `templates/consumer/` — the consumer's half of the contract:
327
+ `bump-theme-pins.sh` (rewrites `remote_theme`, `theme_repo`, and Gemfile gem
328
+ constraints in one pass; `--check`, `--dry-run`, `--latest`, `--pin`),
329
+ `theme-bump.yml` (dispatch + weekly schedule + manual), and a
330
+ `.theme-overrides.yml` starter. The schedule matters most for gem-mode
331
+ consumers, which have no `remote_theme` and so pick up nothing automatically.
332
+ - `docs/systems/theme-propagation.md` — how the whole flow fits together.
333
+ - **`./scripts/bin/manifest --check`** — verifies the committed theme manifest
334
+ matches the working tree, ignoring `generated_at`. `--dry-run` now emits only
335
+ YAML on stdout (progress logs moved to stderr) so its output can be diffed
336
+ directly.
337
+ - **Consumer extension hooks (`_includes/custom/`).** Four empty stubs the
338
+ chrome now includes at fixed points — `custom/head.html` (end of `<head>`),
339
+ `custom/body-start.html` (right after `<body>`, e.g. GTM noscript),
340
+ `custom/footer.html` (after the theme footer), `custom/body-end.html` (last
341
+ thing before `</body>`). A consumer site shadows just the stub to inject
342
+ JSON-LD, fonts, verification tags, or scripts — no more forking
343
+ `core/head.html`/`root.html` and drifting from the theme. Documented in
344
+ `_includes/README.md`.
345
+ - **`components/card-grid.html` + `components/data-card.html`.** A generic,
346
+ data-driven card grid (title/url/icon/badge/meta-pills/buttons schema) for
347
+ org hub dashboards, fleet registries, service grids, and link boards —
348
+ the markup consumer sites have been hand-rolling per page. Includes an
349
+ optional empty-state message.
350
+ - **`_layouts/404.html` — the 404 page as a theme layout.** The full 404
351
+ experience (including the unconfigured-site setup guide) now lives in the
352
+ theme; a consumer's `/404.html` shrinks from a 224-line vendored copy to a
353
+ 4-line front-matter stub (`layout: "404"`). Setup mode is branded with
354
+ `site.title` instead of a hardcoded theme welcome, and the stub page's body
355
+ renders below the standard content for site-specific additions.
356
+ - **`color_mode_lock` config.** Pin the site to one color mode: visitor
357
+ preference and OS scheme are ignored, the light/dark/auto switcher is
358
+ removed, and `core/color-mode-init.html` + `halfmoon.js` enforce the lock
359
+ pre-paint. Replaces consumer JS workarounds that re-asserted a mode on every
360
+ `matchMedia` change.
361
+ - **PostHog privacy options** (`posthog.privacy.*`): `respect_gpc` (honor the
362
+ Global Privacy Control signal alongside DNT, default on), `require_consent`
363
+ (start opted out with memory-only persistence until the visitor accepts the
364
+ cookie-consent "analytics" category), and `geoip_disable` (no IP→location
365
+ resolution). Upstreams the deltas consumer sites were maintaining in a
366
+ full fork of `analytics/posthog.html`.
367
+ - **`news:` config surface for the news/section layouts.** `category_base`
368
+ (e.g. `/categories/` instead of the hardcoded `/news/`), `index_url`,
369
+ `hero: false`, `archives_url`, and a `newsletter` block (endpoint-or-mailto,
370
+ or hidden entirely — the old markup shipped a form that submitted nowhere).
371
+
372
+ - **ABC board books (`book-abc` layout) — the "ABC & Language" series.** A new
373
+ immersive, single-scroll toddler alphabet layout (`_layouts/book-abc.html`)
374
+ driven entirely by an `alphabet:` front-matter list — one big letter, one
375
+ word, one picture per card, with A–Z quick-jump anchors and a cover hero.
376
+ The big letter is HTML typography (never baked into the art), and a card
377
+ whose plate is still `planned` shows a tinted "illustration coming soon"
378
+ placeholder so a book reads end-to-end before any art renders. New
379
+ `_includes/components/abc-letter.html` renders each card; `_sass/components/_book.scss`
380
+ gains `.abc-board` / `.abc-letter-*` styles plus a `.abc-style--<id>` skin per
381
+ shared art-style id. `components/bookshelf.html` now lists both `book` and
382
+ `book-abc` books, grouped into series shelves from `_data/series.yml`;
383
+ `book-card.html` shows a letter count for ABC books. Ships a demo book
384
+ (`pages/_books/abc-demo/`) + default series metadata (`_data/series.yml`).
385
+ Regression test [`test/visual/features/book-abc.spec.js`](test/visual/features/book-abc.spec.js)
386
+ and before/after evidence in
387
+ [`test/visual/evidence/book-abc/`](test/visual/evidence/book-abc/). Docs:
388
+ [`docs/features/abc-books.md`](docs/features/abc-books.md).
389
+ - **`nav: pages` sidebar mode — auto-build the left tree from page URLs.** A new
390
+ sidebar mode (`_includes/navigation/sidebar-pagetree.html`) derives a
391
+ collapsible section tree purely from page permalinks under a `sidebar.base`
392
+ prefix — no curated `_data/navigation/*.yml` file to write or keep in sync.
393
+ Enable per page / collection / site with `sidebar: {nav: pages, base: /docs/,
394
+ order_by: nav_order, title: …}`. It covers plain pages (`site.html_pages`) and
395
+ collection documents (`site.documents`) alike, groups them by first path
396
+ segment, humanizes section labels from the URL (never leaking a generic
397
+ "Index" title), sorts each section by a numeric `order_by` (natural order),
398
+ supports per-page `sidebar_label` / `sidebar_exclude`, and marks only the
399
+ current page active with its section expanded server-side — pure Liquid, so
400
+ it is GitHub Pages / `remote_theme` safe. Documented at
401
+ `/docs/features/sidebar-page-tree/`.
402
+ - **Claude Code OAuth in the AI installer** — the spec-driven installer
403
+ (`scripts/bin/install`) is now multi-provider. `scripts/install/ai/client.sh`
404
+ resolves a provider via `ZER0_AI_PROVIDER` (default `auto`), preferring the
405
+ logged-in `claude` CLI (**Claude Code OAuth** — zero key handling), then the
406
+ Anthropic Messages API (`CLAUDE_CODE_OAUTH_TOKEN` OAuth bearer or
407
+ `ANTHROPIC_API_KEY`), then OpenAI. `install doctor` reports the active
408
+ provider; `--ai-provider` / `--ai-model` / `ZER0_AI_MODEL` tune it; `--no-ai`
409
+ / `ZER0_NO_AI=1` disable it. User context is sanitized before every call.
410
+ - **Config-file layer for the installer** — new `scripts/install/config.sh`
411
+ discovers and merges `~/.config/zer0/install.yml`, `<target>/zer0.install.yml`,
412
+ `<target>/.zer0/config.yml`, and `--config FILE` (precedence:
413
+ defaults < profile < config < env < flags). Recognises site / github / theme /
414
+ deploy / agents / tasks / ai keys; API keys stay environment-only.
415
+ - **`install suggest`** subcommand — recommend a profile + deploy target
416
+ (AI-assisted with rule-based fallback) — plus `--yes` as an alias for
417
+ `--auto-accept`.
418
+ - Installer regression matrix (`test/test_installer.sh`) now covers AI
419
+ provider resolution, text extraction across provider shapes, the config-file
420
+ layer + precedence, and the `doctor` AI check (all offline).
114
421
 
115
422
  ### Changed
116
423
 
424
+ - **Navbar label tiers reduced from three to two** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)).
425
+ Top-level nav items now render as icon + full label above 51rem of centre
426
+ track and icon-only below it; `text-overflow: ellipsis` is gone, so a label
427
+ is never cut to "Quicksta…". Icons now survive in **both** tiers — the old
428
+ 41–50.99rem tier dropped them to buy width for bare labels. **Behaviour
429
+ change:** centre-track widths between 41rem and 51rem (roughly 992–1200px
430
+ viewports) previously showed bare labels and now show icons, with the
431
+ existing `.nav-tooltip` carrying the name on hover.
432
+ - **Dropdown chevrons merged into their parent row** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)).
433
+ The split toggle is now absolutely positioned inside padding the parent link
434
+ reserves, and hover applies to the whole row, so there is no dead zone
435
+ between the label and its chevron. It remains a real `<button>` with
436
+ `aria-expanded`, `aria-haspopup` and its visually-hidden label unchanged.
437
+ (evidence: [`test/visual/evidence/navbar-tiers-405/`](test/visual/evidence/navbar-tiers-405/README.md) — chevron dead zone up to 4px → overlapping; 0 truncated labels across 6 widths)
438
+ - **The navbar carries logo + title only** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)).
439
+ `site.subtitle` moved out of `_includes/core/branding.html` and onto the home
440
+ hero (`_layouts/home.html`), returning its width to the menubar. It renders
441
+ under the page heading on pages that use the `home` layout, and is hidden
442
+ with it by `hide_title` or `hide_intro`. A homepage on another layout, such
443
+ as `landing`, no longer shows the subtitle at all; add `site.subtitle` to
444
+ that layout's hero to keep it.
445
+ - **The below-`lg` menu toggle is labelled "Menu"** ([#405](https://github.com/bamr87/zer0-mistakes/issues/405)),
446
+ so it is distinguishable from the sidebar hamburger beside it. Override the
447
+ string with `ui.nav_menu_toggle_label`.
448
+ - The desktop navbar grid gives its two side tracks a shared minimum
449
+ (`--zer0-navbar-side-min`, default `9rem`) so the menubar is optically
450
+ centred in the bar rather than only within its own track.
451
+
117
452
  - The stable-height spec now walks Connect **first and last** and records
118
453
  `#wizardTabContent`'s document-y **top** beside each Back/Next offset. The
119
454
  contract itself (`spread ≤ 1`) is unchanged; what is new is that a shift
@@ -139,576 +474,169 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
139
474
  — base vs head on a post and a note page, page overflow 0px → 0px across 6
140
475
  widths, 9 pixel baselines unchanged).
141
476
 
142
- ## [1.30.0](https://github.com/bamr87/zer0-mistakes/compare/v1.29.0...v1.30.0) (2026-09-12)
477
+ - **`claude-run` is now the fleet's shared `ai-runner` kit.** The composite
478
+ action and its new `scripts/ai/run.sh` are byte-identical copies of
479
+ lifehacker.dev's (the kit source of truth) instead of a hand-rolled variant.
480
+ What changes for the two callers (`issue-autopilot.yml`,
481
+ `visual-evidence-autogen.yml`): an AI call that was attempted and rejected —
482
+ revoked credential, exhausted quota, `is_error` payload, CLI install failure
483
+ — now fails the step with the reason as a `::error::` annotation, where the
484
+ old action exited 0 and a dead run read green; the OAuth-first rule is
485
+ enforced with `env -u ANTHROPIC_API_KEY`; the model override is the canonical
486
+ `AI_MODEL` (or the new `model` input) rather than `ZER0_AI_MODEL`, and
487
+ `max-turns` is a new input. Metering (`scripts/ai/usage.rb`,
488
+ `usage_report.rb`, prices in `_data/ai_pricing.yml`) and the Claude API
489
+ fallback (`scripts/ai/api_call.rb`) ride along as optional companions. The
490
+ exit-code contract is pinned by `scripts/ci/test_ai_runner.sh`, wired into
491
+ `./scripts/bin/test`. The action's six existing inputs are unchanged.
492
+ - **`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)).
493
+ - **Consumer registry corrections.** `ai-world-view/ai-world-view.github.io`
494
+ is `remote_theme_floating` (both its `_config.yml` and `hub.yml` pins are
495
+ untagged), not `remote_theme_pinned`; `amr-bash/bash-365.com` is registered
496
+ as a consumer (floating `remote_theme`, unconstrained gem on Azure, path gem
497
+ for dev).
143
498
 
499
+ - **The page-feedback widget now files the fleet's issue contract, not its own
500
+ (UPS-FB).** The theme built its own issue body, its own escaping, and its own
501
+ URL-length trimming — and so did the 404 page and the AI chat, three builders
502
+ in one repo with three different behaviours. All three now go through
503
+ `FleetFeedbackCore` in `assets/js/fleet-feedback.js`, vendored byte-identically
504
+ from the fleet kit (`bamr87/bamr87` `templates/feedback/`, held to the hub's
505
+ copy by the drift gate). It is the same builder the fleet's `<fleet-feedback>`
506
+ web component uses, so an issue filed from a page here and one filed from a
507
+ React app elsewhere are byte-identical to the pipeline that triages them.
144
508
 
145
- ### Features
146
-
147
- * **ci:** produce visual evidence and baselines on PRs, not just verify them ([#462](https://github.com/bamr87/zer0-mistakes/issues/462)) ([a377fae](https://github.com/bamr87/zer0-mistakes/commit/a377fae6ea31a0bb015479dea6d1a7c41e141c3f))
148
- * **includes:** add background-image.html for CSS-painted cover art ([#451](https://github.com/bamr87/zer0-mistakes/issues/451)) ([9b15a67](https://github.com/bamr87/zer0-mistakes/commit/9b15a673f57b8f7f1f9dae9262191742d99d8080))
149
- * **includes:** Claude or Grok sessions in the Site Builder ([#473](https://github.com/bamr87/zer0-mistakes/issues/473)) ([e1c5753](https://github.com/bamr87/zer0-mistakes/commit/e1c57531fc23be8d071757cd1941e1ae15285da7))
150
- * **includes:** file the fleet issue contract from the feedback widget ([#472](https://github.com/bamr87/zer0-mistakes/issues/472)) ([24d4e2c](https://github.com/bamr87/zer0-mistakes/commit/24d4e2c6380541a894e9e671f0f0b0b859a29b05))
151
- * **includes:** make theme-color track the page surface, not the accent ([#447](https://github.com/bamr87/zer0-mistakes/issues/447)) ([f1fda85](https://github.com/bamr87/zer0-mistakes/commit/f1fda857bb0cf5bd64bebfc1215ba0a906f05e5c))
152
- * **includes:** themed Mermaid figures + toolbar ([#455](https://github.com/bamr87/zer0-mistakes/issues/455)) ([96776fa](https://github.com/bamr87/zer0-mistakes/commit/96776fa48bbe7b63058d1d246962019c745d7b93))
153
- * **scripts:** adopt the shared ai-runner kit for claude-run; consumers registry fixes; hub baseline ([#475](https://github.com/bamr87/zer0-mistakes/issues/475)) ([feb4133](https://github.com/bamr87/zer0-mistakes/commit/feb41332eb15d2c13fe0c0f6d15b61007e14daa3))
154
-
155
-
156
- ### Bug Fixes
157
-
158
- * **a11y:** decouple heading size from heading level in theme chrome ([#450](https://github.com/bamr87/zer0-mistakes/issues/450)) ([bb92eb5](https://github.com/bamr87/zer0-mistakes/commit/bb92eb583c955e76339a9c7ab98532b35e226b36))
159
- * **a11y:** give footer icon links an accessible name, announce search results ([#441](https://github.com/bamr87/zer0-mistakes/issues/441)) ([a1811e8](https://github.com/bamr87/zer0-mistakes/commit/a1811e81ac0c17b0836994be7c48eba51252bae7))
160
- * **a11y:** stop emitting sidebar/TOC controls on layouts that render no offcanvas ([#443](https://github.com/bamr87/zer0-mistakes/issues/443)) ([619322d](https://github.com/bamr87/zer0-mistakes/commit/619322d8fd7c6e2a700ba154e065bf65caf67461))
161
- * **ci:** run the autogen lane's tooling from the base branch, not the PR ([#464](https://github.com/bamr87/zer0-mistakes/issues/464)) ([8473050](https://github.com/bamr87/zer0-mistakes/commit/8473050be2b4ddfd994c7bea8a8c35e37a66b7f8))
162
- * **ci:** stop the UI audit reporting a measurement blackout as a clean run ([#471](https://github.com/bamr87/zer0-mistakes/issues/471)) ([ab92619](https://github.com/bamr87/zer0-mistakes/commit/ab9261946e088417dabe4291590da1c7a964d4fa))
163
- * **content:** raw-protect Liquid that is written as documentation ([#446](https://github.com/bamr87/zer0-mistakes/issues/446)) ([6123c1f](https://github.com/bamr87/zer0-mistakes/commit/6123c1f1e6ffd167376d4ef421b0a9f78af3985e))
164
- * **includes:** declare charset inside the 1024-byte window, make the feed discoverable ([#440](https://github.com/bamr87/zer0-mistakes/issues/440)) ([4025205](https://github.com/bamr87/zer0-mistakes/commit/40252050ec21c70aa1d84820fdcde6d564e03478)), closes [#372](https://github.com/bamr87/zer0-mistakes/issues/372) [#371](https://github.com/bamr87/zer0-mistakes/issues/371)
165
- * **includes:** expose skin-button selected state via aria-pressed ([#469](https://github.com/bamr87/zer0-mistakes/issues/469)) ([6579b16](https://github.com/bamr87/zer0-mistakes/commit/6579b1621a5eff147e224977633728aadc960505)), closes [#467](https://github.com/bamr87/zer0-mistakes/issues/467)
166
- * **install:** stop seeding the theme's own navigation into consumers ([#449](https://github.com/bamr87/zer0-mistakes/issues/449)) ([232857c](https://github.com/bamr87/zer0-mistakes/commit/232857c66d7c3abccb166b5151a7f80a74dd17ca))
167
- * **navigation:** scroll spy bolds the section actually being read ([#456](https://github.com/bamr87/zer0-mistakes/issues/456)) ([5ff38bf](https://github.com/bamr87/zer0-mistakes/commit/5ff38bf98faf96cdc3bf63ea3f0e7b10e21f3aab))
168
- * **sass:** stop navbar controls flickering under a stationary pointer ([#453](https://github.com/bamr87/zer0-mistakes/issues/453)) ([fc84b9a](https://github.com/bamr87/zer0-mistakes/commit/fc84b9a4c714f8e04b8a0f940bed9e3795c144bc)), closes [#404](https://github.com/bamr87/zer0-mistakes/issues/404)
169
- * **scripts:** restore nested translation placeholders instead of leaking them ([#452](https://github.com/bamr87/zer0-mistakes/issues/452)) ([00cab73](https://github.com/bamr87/zer0-mistakes/commit/00cab73d915f4e116040ef6ca4c0381669ae4eb2))
170
-
509
+ What that adds to every filed issue: sections in a fixed order, a
510
+ `<!-- fleet-feedback v1 type=... -->` marker the issue pipeline reads to
511
+ recognise an already-structured report, and a real over-budget path. The AI
512
+ chat in particular used to `.slice(0, 6000)` its body — silently dropping
513
+ whatever Claude had written past that point — and now trims by section and
514
+ hands the full text to the clipboard.
171
515
 
172
- ### Performance Improvements
516
+ The theme keeps its Bootstrap modal and its AI triage step; only the body
517
+ assembly moved. Two UIs, one contract.
173
518
 
174
- * **ci:** run the critical PR gate on 3 workers instead of 1 ([#481](https://github.com/bamr87/zer0-mistakes/issues/481)) ([b3fcbc7](https://github.com/bamr87/zer0-mistakes/commit/b3fcbc76a1af2a3a5e43eb28fbff7b3057751de6))
175
- * **includes:** strip developer doc banners from delivered HTML ([#445](https://github.com/bamr87/zer0-mistakes/issues/445)) ([89b10f1](https://github.com/bamr87/zer0-mistakes/commit/89b10f1ff361c919fd631d0e7e664ed6d7c28a14))
519
+ - **Request-type labels map onto the fleet issue-pipeline taxonomy.**
520
+ `enhancement`/`documentation` become `feature`/`docs`; `area:docs`/`area:feat`
521
+ drop out. Every label the widget applies exists in this repo (GitHub silently
522
+ drops the ones that do not), and a widget-filed issue is now pipeline-eligible
523
+ on the next scan. The zero-`_data` fallback taxonomy carries them too, so
524
+ `remote_theme` consumers benefit the moment those labels exist in their repo.
176
525
 
177
- ## [Unreleased]
526
+ - **Captured console lines are redacted before they enter the buffer.** Bearer
527
+ tokens, API keys, JWTs, GitHub tokens and email addresses are masked on the
528
+ way in, so a secret cannot be previewed, copied, or filed even by accident.
529
+ Credentials reach the console more often than anyone expects — an
530
+ `Authorization` header logged by a fetch wrapper, a signed URL in a 403.
178
531
 
179
- ### Changed
532
+ - **The capture buffer hooks `console.warn` and `console.error` only.**
533
+ Previously `log`, `info` and `debug` were captured too. At a 40-entry ring,
534
+ debug chatter evicts the one line that explains the failure — which is the
535
+ line a report exists to carry. `_includes/core/console-capture.html` is now a
536
+ two-line loader for the vendored buffer rather than 60 lines of inline script,
537
+ which also *shrinks* `<head>` and buys back room for the charset meta that has
538
+ to land in the first 1024 bytes (#372).
180
539
 
181
- - **`claude-run` is now the fleet's shared `ai-runner` kit.** The composite
182
- action and its new `scripts/ai/run.sh` are byte-identical copies of
183
- lifehacker.dev's (the kit source of truth) instead of a hand-rolled variant.
184
- What changes for the two callers (`issue-autopilot.yml`,
185
- `visual-evidence-autogen.yml`): an AI call that was attempted and rejected —
186
- revoked credential, exhausted quota, `is_error` payload, CLI install failure
187
- — now fails the step with the reason as a `::error::` annotation, where the
188
- old action exited 0 and a dead run read green; the OAuth-first rule is
189
- enforced with `env -u ANTHROPIC_API_KEY`; the model override is the canonical
190
- `AI_MODEL` (or the new `model` input) rather than `ZER0_AI_MODEL`, and
191
- `max-turns` is a new input. Metering (`scripts/ai/usage.rb`,
192
- `usage_report.rb`, prices in `_data/ai_pricing.yml`) and the Claude API
193
- fallback (`scripts/ai/api_call.rb`) ride along as optional companions. The
194
- exit-code contract is pinned by `scripts/ci/test_ai_runner.sh`, wired into
195
- `./scripts/bin/test`. The action's six existing inputs are unchanged.
196
- - **`claude-run` is consumed by reference from the hub.** `issue-autopilot.yml` and `visual-evidence-autogen.yml` now call `bamr87/bamr87/.github/actions/claude-run@main` (identical inputs) instead of a vendored `./.github/actions/claude-run`; the local action, `scripts/ai/run.sh`, and the vendored contract test (`scripts/ci/test_ai_runner.sh` + its `scripts/test/lib/` bridge) are deleted, while the consumer companions the hub runner probes for — `scripts/ai/usage.rb`, `usage_report.rb`, `api_call.rb`, `_data/ai.yml`, `_data/ai_pricing.yml`, `tools/unwrap-prose.py` — stay ([bamr87/bamr87#254](https://github.com/bamr87/bamr87/pull/254)).
197
- - **Consumer registry corrections.** `ai-world-view/ai-world-view.github.io`
198
- is `remote_theme_floating` (both its `_config.yml` and `hub.yml` pins are
199
- untagged), not `remote_theme_pinned`; `amr-bash/bash-365.com` is registered
200
- as a consumer (floating `remote_theme`, unconstrained gem on Azure, path gem
201
- for dev).
540
+ - **TOC scroll spy now bolds the section you are actually reading** — the
541
+ highlighted entry in the right-hand Table of Contents jumped around and sat
542
+ one to three sections ahead of the viewport. Measured on `/docs/features/toc/`
543
+ at 1280×820, the wrong entry was bolded at **19–23 of 25 scroll positions**
544
+ — the count moves between runs because which implementation won depended on
545
+ callback timing, which is the bug in one sentence.
202
546
 
203
- ### Fixed
547
+ Three implementations were toggling `.active` on the same
548
+ `#TableOfContents a` links, so whichever fired last won. Bootstrap's native
549
+ ScrollSpy was wired up twice — `data-bs-spy="scroll"
550
+ data-bs-target="#TableOfContents"` on `<body>` in `_layouts/root.html` *and*
551
+ on `.bd-main` in `_layouts/default.html` (whose `data-bs-offset="100"` had
552
+ been dead since Bootstrap 5.2 replaced that option with
553
+ `data-bs-root-margin`). `assets/js/ui-enhancements.js` ran a third observer
554
+ over *every* `a[href^="#"]` on the page, clearing `.active` from all of them
555
+ each time a `section[id]` intersected. Both Bootstrap hooks are removed —
556
+ the `<body>` one was also stripping server-rendered `.active` classes off
557
+ admin sidebar links, which `fixtures.gotoBeforeScrollSpy` exists to work
558
+ around — and the `ui-enhancements.js` observer is now scoped away from TOC
559
+ and sidebar links.
204
560
 
205
- - **Two `test_core.sh` checks could never fail, and one shouted on every clean
206
- run (#460).** The Liquid nested-tag check grepped for
207
- {% raw %}`{{.*{{`{% endraw %}, which matches
208
- any two *sibling* output tags on one line — it flagged 64 of the theme's
209
- includes, starting with `navigation/sidebar-pagetree.html`'s valid
210
- {% raw %}`{{ _base }}{{ _section }}`{% endraw %} — and its `return 1` sat inside a `find | while`
211
- pipeline, i.e. a subshell, so the function printed `[ERROR]` and then reported
212
- the check as passed. The gem-content check ran `tar -tzf` on the built `.gem`,
213
- but a gem is an *uncompressed* tar wrapping `metadata.gz`, `data.tar.gz` and
214
- `checksums.yaml.gz`, so gzip printed `stdin: not in gzip format` four times
215
- and both branches fell through to "Gem may not contain…" without ever
216
- inspecting the payload — a gem shipped with no `_layouts/` would have passed.
217
- The nested-tag pattern is now {% raw %}`{{[^}]*{{`{% endraw %} (cannot cross the first `}`, so
218
- siblings do not match), both loops read from a process substitution so a
219
- failure propagates, and the gem check extracts `data.tar.gz` and asserts
220
- `_layouts/` and `assets/` are really in it. The layout balance check, now
221
- live, blanks Liquid comment bodies before counting, so prose in a comment that
222
- quotes a closing delimiter (as `_layouts/root.html`'s per-page-scripts note
223
- does) is no longer read as an unbalanced tag. New `test/test_core_checks.sh`
224
- drives both checks against known-good and known-bad fixtures, so a check that
225
- stops being able to fail is itself a test failure. `./test/test_core.sh` now
226
- runs clean: 26/26, no `[ERROR]` line, no gzip/tar noise.
227
- - **Every navbar link emitted invalid HTML — attributes glued together with no
228
- separating whitespace (#465)** — the Liquid whitespace-trim markers around the
229
- conditional `aria-current` in `_includes/navigation/navbar.html` ate the
230
- newlines that separated the surrounding attributes, so the primary nav
231
- rendered `aria-label="News"aria-current="page"title="News"` (the WHATWG
232
- `missing-whitespace-between-attributes` parse error). The leading {% raw %}`{%-`{% endraw %}
233
- stripped the newline after `aria-label` and the trailing {% raw %}`-%}`{% endraw %} the whitespace
234
- before `title`, which means **two of the four sites were broken on every page,
235
- not just the current one**: where the conditional sits between two
236
- unconditional attributes both markers fire even when the `if` emits nothing.
237
- Parsers recover, so nothing looked broken — but the attributes affected are
238
- exactly `aria-label`, `aria-current` and `title`, and a stricter parser is
239
- entitled to drop the "you are here" announcement for screen-reader users. All
240
- four sites now use the non-trimming {% raw %}`{% if %}`{% endraw %} form already present at line 26
241
- of the same file, with the separator outside the tag. Two guards in
242
- `test/test_core.sh`: `test_navbar_attribute_whitespace` renders the real
243
- include through Liquid across both `aria-current` branches and both nav modes
244
- (6 of 8 rendered variants were invalid before the fix), and
245
- `test_attribute_whitespace_in_markup` models the trim rules over every include
246
- and layout with no gem dependency — it found a second live instance, a literal
247
- missing space in `components/theme-preview-gallery.html`, fixed here too.
248
- `test_jekyll_build` re-checks the delivered `_site` bytes. No visual change.
249
- - **The weekly UI/UX audit was blind, and reported it as clean.** `sweep.mjs`
250
- built its pages with `browser.newPage()`, which `@axe-core/playwright`
251
- refuses; the throw was caught by a single per-route `try` that also discarded
252
- the overflow, console-error and link-collection data already gathered for
253
- that route. Screenshots still succeeded, so the harness looked alive while
254
- every accessibility, console, overflow and broken-link result on every route
255
- was silently dropped — and the report rendered the absence as "0 axe
256
- violations / 0 broken links". Pages are now built from `browser.newContext()`,
257
- each measurement fails independently, a scan that errored is reported as
258
- UNKNOWN rather than clean, and a measurement that fails on every route turns
259
- the sweep red the way a total capture failure already did. On the demo site
260
- the sweep goes from 18/21 captured with 21 blackout entries to **21/21
261
- captured, 0 harness errors, 191 links crawled and 8 genuinely broken links
262
- found** ([#468](https://github.com/bamr87/zer0-mistakes/issues/468)).
263
- - **Theme-skin buttons now announce which skin is applied.** The Theme Skin
264
- group in Settings → Appearance conveyed selection only through the Bootstrap
265
- `.active` class, which carries no accessibility semantics, so screen readers
266
- announced nine identical unlabelled buttons — WCAG 2.1 SC 4.1.2 (Name, Role,
267
- Value). Each button now ships `aria-pressed`, and the click and "Reset
268
- background" handlers keep it in sync with `.active` through one shared
269
- helper, matching the sibling color-mode group
270
- ([#467](https://github.com/bamr87/zer0-mistakes/issues/467)).
561
+ The theme's own spy (`assets/js/modules/navigation/scroll-spy.js`) was wrong
562
+ on its own terms too: it asked IntersectionObserver for the "most visible"
563
+ heading, but headings are a few pixels tall, so every heading inside the
564
+ observer band reports the same `intersectionRatio` — the winner was whichever
565
+ entry happened to be in that callback's batch, and a heading scrolling *out*
566
+ of the band triggered no re-evaluation at all. It now applies a positional
567
+ rule: the active heading is the last one whose top has crossed the reading
568
+ line (the document's `scroll-padding-top`, the same offset anchor navigation
569
+ uses), with the last heading winning once the page is scrolled to the bottom
570
+ so trailing sections shorter than the viewport are still reachable. The
571
+ answer is recomputed from scratch on each rAF-throttled scroll frame from
572
+ cached heading offsets, re-measured on resize and content reflow
573
+ (`ResizeObserver`), so it cannot drift out of sync.
271
574
 
272
- ## [1.29.0](https://github.com/bamr87/zer0-mistakes/compare/v1.28.0...v1.29.0) (2026-09-01)
575
+ Two smaller fixes ride along. Clicking a TOC entry now holds that entry
576
+ active while the smooth scroll animates, instead of flashing every heading
577
+ passed on the way. And keeping the active entry visible inside the TOC no
578
+ longer calls `scrollIntoView()`, which bubbles up and scrolls the *page* —
579
+ feeding straight back into the spy; it adjusts the TOC container's own
580
+ `scrollTop`, resolving that container at call time (`.bd-toc` on desktop,
581
+ `.offcanvas-body` on mobile) rather than assuming one. The active link also
582
+ carries `aria-current="true"`.
273
583
 
584
+ `config.scrollSpy.rootMargin`/`threshold` are replaced by
585
+ `config.scrollSpy.offset` (`null` = derive from `scroll-padding-top`) and
586
+ `tolerance`. Guarded by `test/visual/features/scroll-spy.spec.js` (smoke
587
+ tier), which is 5 failed / 1 passed against the pre-fix theme and 6 passed
588
+ with the fix. (evidence:
589
+ [`test/visual/evidence/scroll-spy/`](test/visual/evidence/scroll-spy/README.md)
590
+ — wrong TOC highlight at 19/25 scroll positions → 0)
591
+ - **Navbar controls no longer flicker under a stationary pointer (#404)** —
592
+ `.nav-link:hover`, the Search/Settings utility buttons and the two sidebar/TOC
593
+ FABs applied `transform: translateY(-1px)`. A 1px lift moves the element out
594
+ from under a pointer parked on the row it vacates, so hover exits, the element
595
+ drops back under the pointer, and hover re-enters — an infinite flicker with
596
+ no pointer movement at all. Nav items are thin enough for a 1px lift to
597
+ reliably do it.
274
598
 
275
- ### Features
599
+ Deleting those declarations was **not sufficient**, and the reason is the
600
+ substantive finding here: `_sass/components/_ui-enhancements.scss` applies
601
+ `transform: translateY(-2px)` to `.btn:hover` — *every* button in the theme.
602
+ The utility buttons and both FABs are `.btn`s, so with the navbar's own 1px
603
+ lift removed the buttons kept flickering on the inherited 2px one — measured
604
+ on the built site, the Settings button lost and regained hover 14 times in
605
+ 800ms with the pointer completely still. The fix therefore cancels it
606
+ explicitly with `transform: none`, the corrective pattern this repo already
607
+ uses in `_post-navigation.scss` and its `prefers-reduced-motion` blocks. The
608
+ FABs get the same treatment by the same reasoning — they are the same kind of
609
+ `.btn` with the same lift — but they were not rendered at any viewport reached
610
+ during verification, so that half is reasoned, not observed.
276
611
 
277
- * **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))
278
- * **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))
279
- * **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)
280
- * **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)
281
- * **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))
282
- * **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))
283
- * **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))
612
+ Background, colour and shadow transitions are untouched, and the controls keep
613
+ a pressed cue as `:active { transform: translateY(1px) }` — safe, because
614
+ `:active` only holds while the button is held down, so the pointer cannot
615
+ leave and re-enter in a loop.
284
616
 
617
+ Three corrections to the issue as filed. The vacated row is the **bottom**
618
+ edge, not the top — a probe parked on the top edge passes without exercising
619
+ the bug at all. Only a transform on the **hovered element's own box** can
620
+ flicker: `&:hover i { transform: scale(1.1) }` moves a child and never shrinks
621
+ the parent's hit box, so those went for consistency, not as causes. And
622
+ `.nav-link:hover`, named as the primary culprit, was **dead code** — its lift
623
+ sat inside `@media (min-width: 992px) { @container navbar-main (max-width:
624
+ 75rem) }`, and measured on the built site at 1040/1140/1200/1280px its
625
+ computed hover transform is `none` at every width, on `main` too. Every
626
+ flicker actually observed was on the utility buttons.
285
627
 
286
- ### Bug Fixes
287
-
288
- * **a11y:** dialog titles use a bare &lt;h5&gt;, skipping heading levels ([#387](https://github.com/bamr87/zer0-mistakes/issues/387)) ([98fd6f9](https://github.com/bamr87/zer0-mistakes/commit/98fd6f9e63dfb75c58efc3b15a642fb3e4c4095b))
289
- * **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)
290
- * **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))
291
- * **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)
292
- * **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))
293
- * **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))
294
- * **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))
295
- * **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))
296
- * **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)
297
- * **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))
298
- * **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)
299
- * **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))
300
- * **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)
301
- * **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))
302
- * **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))
303
- * **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))
304
- * **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)
305
-
306
- ## [Unreleased]
307
-
308
- ### Added
309
-
310
- - **Bring your own AI provider — Claude or Grok — and an open, coding-session-style
311
- Site Builder (ZER0-087)** — the chat proxy gains a provider layer
312
- (`templates/deploy/chat-proxy/providers.js`): xAI's Grok is a first-class
313
- provider (`XAI_API_KEY`, `CHAT_PROVIDER`, `XAI_CHAT_MODEL`) for the chat
314
- widget, the feedback triage and the Site Builder, translated server-side to
315
- and from the Anthropic Messages dialect so no client changed. The Site
316
- Builder's Connect step shows which providers the local dev proxy already has
317
- keys for (masked), takes a token for the session (sent once to localhost,
318
- kept in the proxy's memory, optionally saved to `.env` with mode 600), offers
319
- the model catalog, an image renderer (Grok Imagine / OpenAI Images) and two
320
- session modes: the guided nine steps, or an open session that can open an
321
- existing site under the target root, read/edit/write/delete its files, run
322
- `git status`/`diff`/`log`, generate images into `assets/`, run `jekyll build`
323
- inside the container and stop a long tool run. Generated sites are
324
- pre-configured for the chosen provider (`ai_chat.provider`, `preview_images`,
325
- `.env.example`). The dev proxy now starts without a credential. Also fixes a
326
- pre-existing 162px horizontal overflow of the whole `/setup/` page at phone
327
- widths (the Connect step's command boxes could not shrink). (evidence:
328
- [`test/visual/evidence/site-builder-providers/`](test/visual/evidence/site-builder-providers/README.md)
329
- — Connect step: 2 provider cards, 2 session modes, token field is a password
330
- input inert offline; mocked proxy: "needs a token" → "Grok connected"; Build:
331
- existing sites listed, Open sets the target; mobile overflow 162px → 0px)
332
- - **CI now produces a UI pull request's visual artifacts instead of only
333
- checking for them (ZER0-085)** — `visual-evidence-autogen.yml` renders every
334
- same-repo PR in the same jammy Playwright image the snapshot gate uses, runs
335
- the PR's `test/visual/*-evidence.mjs` generators (or the new generic
336
- base-vs-head generator `test/visual/pr-evidence.mjs`, which renders the base
337
- branch and the head side by side), verifies the 9-skin baselines, and pushes
338
- the montages + `metrics.json` to the branch. Stale baselines are refreshed
339
- **only** when the new `visual-evidence-reviewer` agent, having viewed the
340
- expected | actual | diff montage, judges the diff to be the change the PR
341
- describes — a code step disposes, the model only proposes ([#417](https://github.com/bamr87/zer0-mistakes/issues/417)
342
- is why). The evidence gate now requires generated proof (a README alone no
343
- longer passes), `ci-self-repair` leaves a red `Visual Snapshots` job to this
344
- lane, and `test/update-snapshots.sh` gained `PRE_TEST_SCRIPT` /
345
- `POST_TEST_SCRIPT` / `SKIP_PLAYWRIGHT` hooks. Closes the gap that kept
346
- [#454](https://github.com/bamr87/zer0-mistakes/pull/454) red: its authoring
347
- agents ran where Docker was gated, and nothing in CI could render what they
348
- could not. Kill switch: repo variable `VISUAL_EVIDENCE_AUTOGEN_ENABLED=false`.
349
- The lane restores its own tooling from the base branch before it runs, so it
350
- works on branches cut before it shipped: without that its first real run on
351
- #454 died in 20 seconds (that branch has no copy of the orchestrator), and a
352
- branch carrying an older `update-snapshots.sh` would have silently generated
353
- nothing at all.
354
- - **Site Builder — a Claude-guided setup wizard that ends with a running site
355
- (ZER0-086, extends ZER0-067)** — `/setup/` (and the `welcome` layout) grew
356
- from a five-step `_config.yml` form into a nine-step builder: Connect →
357
- Prerequisites → Identity → URLs → Structure → Appearance → Voice →
358
- Integrations → Build. An embedded Claude session rides alongside every step
359
- through the local dev proxy (Claude Code OAuth, `claude setup-token`; the
360
- token never reaches the page). It sees the whole wizard state on every turn
361
- and acts on it with tools: fill in fields, override a generated file, run
362
- live prerequisite checks (Docker, Git, gh, VS Code, Node, Claude CLI — a
363
- fixed command table), read the theme's real source and search the docs,
364
- resolve a project folder, write the generated project, run `docker compose
365
- up|ps|logs|down`, and check the new site answers — each mutation behind a
366
- confirmation card. The form half still works with no proxy at all (GitHub
367
- Pages included): it generates `_config.yml`, `_config_dev.yml`, `Gemfile`,
368
- `docker-compose.yml`, `index.md`, navigation, about page, welcome post, one
369
- index page per collection, `.gitignore`, `zer0.install.yml`, `.env.example`
370
- and `README.md`, with per-file download and a self-extracting bash bundle.
371
- Forms gained a site brief, a URL **Suggest** button, a site-type quick-pick,
372
- a navigation row editor, skin cards with a live **Preview on this page**
373
- toggle, tone/audience pickers, integration switches with conditional
374
- sub-fields, a copy button on every command, and a toast for feedback. New:
375
- `_data/site_builder.yml` (steps, prerequisites mirroring
376
- `machine-setup.md`, catalogs, framework brief),
377
- `templates/deploy/chat-proxy/wizard-store.mjs` (the dev-only sandbox behind
378
- `/api/wizard/*`), `assets/js/site-builder.js`, `_includes/setup/{claude-
379
- session,prereq-checklist}.html`, the `site_builder:` config block, a
380
- quickstart page and a feature reference. (evidence:
381
- [`test/visual/evidence/site-builder/`](test/visual/evidence/site-builder/README.md))
382
- A first recorded end-to-end run then shaped the build half: the generated
383
- `docker-compose.yml` publishes LiveReload on the site's port + 1 (a fixed
384
- 35729 collided with the theme's own dev container and the new site never
385
- started) and shares one gem cache across generated sites; every enabled
386
- collection now ships a valid starter document (a doc, a quickstart step, a
387
- note with a wiki-link, a structured recipe) and the cookbook index uses the
388
- theme's `cookbook` layout over the real `recipes` collection; `_config.yml`
389
- sets `collections_dir: pages` (without it every collection but posts was
390
- invisible to Jekyll); each site gets its own `assets/images/logo.svg`
391
- monogram and gem-based sites turn the SVG background layers off, because
392
- the published gem ships no theme images; a revoked Claude credential now
393
- says so and names the fix. Validation is a scenario
394
- runner, `test/visual/site-builder-walkthrough.mjs`, that samples random
395
- briefs per site type (`test/visual/site-builder-scenarios.mjs`), drives the
396
- wizard on video, builds the site with Docker and asserts its routes, title
397
- and skin — replayable by seed.
398
- The builder then gained a **schema-driven site plan**: `plan_schema` in
399
- `_data/site_builder.yml` (a JSON-Schema subset validated in the browser)
400
- describes what the agent may produce — a landing page (five templates,
401
- nine section types, hero + CTAs), navigation shape (flat or grouped
402
- dropdowns, sidebar none/auto/docs tree), theme overrides (nine palettes or
403
- custom colours, seven font pairings, three corner radii) and up to twelve
404
- example pages with Markdown bodies — and the generators turn it into
405
- `index.md` (a Liquid landing engine) + `_data/landing.yml`,
406
- `_data/navigation/main.yml` and `docs.yml`, `assets/css/user-overrides.css`
407
- + `_includes/custom/head.html`, and one file per page. Claude submits plans
408
- with `set_site_plan` behind a confirmation card; the Structure and
409
- Appearance steps expose the same choices as controls, with a page planner
410
- and a **Preview on this page** toggle that applies the generated overrides
411
- to the wizard itself. The scenario runner randomises the plan too and
412
- asserts the built site's landing template, palette colour, web fonts and
413
- planned routes.
414
- - **Mermaid diagrams are now accessible figures with a toolbar (ZER0-013)** —
415
- every ```` ```mermaid ```` fence (and legacy `<div class="mermaid">`) renders
416
- as a `<figure>` with a rendered SVG and a small toolbar: zoom out / in / reset
417
- (50–400 %, real layout — the SVG is resized, so the frame scrolls and
418
- drag-to-pan, `Ctrl`+wheel and the `+` / `-` / `0` keys all work), **View
419
- fullscreen** (a native `<dialog>` — the fix for a wide diagram shrunk to
420
- illegibility on a phone; `Esc` closes it and focus returns to the opener),
421
- **Copy diagram source**, and **Download as SVG** (page background baked in so
422
- a dark-mode export stays readable). On pointer devices the toolbar floats over
423
- the top-right corner on hover/focus; on touch devices it is a static row above
424
- the diagram so it never covers it. `accTitle` becomes the visible
425
- `<figcaption>` and the diagram's accessible name, `accDescr` its `<desc>`,
426
- and the scrollable frame is a focusable, labelled region. A syntax error no
427
- longer blanks the block: the figure shows the parse message, a hint, and the
428
- source in a `<details>`, with *copy* still enabled. Colours are **derived from
429
- the live design tokens** (`--bs-primary`, `--bs-body-bg`, `--zer0-color-*`)
430
- through Mermaid's `base` theme, so diagrams follow the colour mode, the
431
- selected skin and any `theme_color` override, and re-render in place when
432
- those change; dark mode is decided by background luminance, so wizard mode and
433
- dark skins get legible ink, and a contrast guard keeps nodes visible when the
434
- brand equals the page colour. Pie slices, git branches and mind-map branches
435
- get a 12-colour series fanned out from the brand hue. `securityLevel` now
436
- defaults to `strict` (`mermaid.security_level: loose` restores `click`
437
- callbacks / HTML labels); `mermaid.toolbar`, `fullscreen` and `download` can
438
- switch the controls off. Implemented as `_includes/components/mermaid.html`
439
- (loader: config JSON + two `defer` scripts — the 3.3 MB bundle used to load
440
- synchronously in `<head>`) + `assets/js/mermaid-diagrams.js` +
441
- `_sass/components/_mermaid.scss`, replacing ~400 lines of inline
442
- `<script>`/`<style>` in the include; the `!important` SVG overrides that
443
- flattened `classDef`/`style` colours and pie slices are gone. Toolbar strings
444
- are `diagram_*` keys in `_data/ui-text.yml`. The docs page
445
- (`/docs/features/mermaid-diagrams/`) now renders a live example of every
446
- diagram type, the caption directive, per-node styling and the error state.
447
- Guarded by `test/visual/features/mermaid.spec.js` (12 tests, smoke tier)
448
- (evidence: [`test/visual/evidence/mermaid/`](test/visual/evidence/mermaid/README.md)
449
- — dark-mode toggle keeps the SVG (before: SVG lost, stylesheet text on
450
- screen); page overflow 0px at 320/390px; 11/12 docs diagrams rendered, 1
451
- shown as an error card with its source kept).
452
-
453
- ### Fixed
454
-
455
- - **Generated posts and notes were invisible on GitHub Pages** — every dated
456
- document the wizard wrote was stamped `T09:00:00.000Z` on the day it was
457
- generated. Jekyll refuses to publish future-dated documents unless
458
- `future: true`, and GitHub Pages builds with the default, so a site generated
459
- before 09:00 UTC went live with its posts and notes missing: linked from the
460
- collection index and listed in the sitemap, but 404 when clicked. Generated
461
- content is now stamped with the actual moment it was written, and a
462
- regression test fails if any generated document carries a future date.
463
- Found by publishing two example sites and clicking the links.
464
- - **Every page of every generated site 404'd on `user-overrides.js`** — the
465
- Site Builder writes `user_overrides: true` so the theme loads the palette and
466
- font overrides it generates, but that same flag also makes the theme load
467
- `assets/js/user-overrides.js`, which the wizard never created. Generated
468
- sites now ship a commented stub, so the request resolves and site owners have
469
- an obvious place to put their own JavaScript. Found while publishing example
470
- sites built by the wizard.
471
- - **Every generated landing page shipped an invisible button** —
472
- `components/cta-button.html` mapped `variant: outline` to `btn-outline-light`,
473
- which is white text on a white border, and the Site Builder's own default
474
- call to action uses `outline` on a light hero. `outline` is now an outlined
475
- *primary* button; `outline-light` is the explicit variant for a dark surface,
476
- and the generated landing engine picks between them from `hero.variant`.
477
- The theme's own homepage hero is `bg-primary text-white`, so its two
478
- outlined buttons moved to `outline-light` and render exactly as before —
479
- a consumer who copied `_data/landing.yml` into a site with a dark hero
480
- should make the same one-word change.
481
- - **A planned `landing.hero.image` was accepted and then thrown away** — the
482
- site plan's schema has offered `landing.hero.image` since the Site Builder
483
- shipped, but the landing engine it generates never rendered it, so an agent
484
- (or a human) could set a hero image, see it validated, watch it land in
485
- `_data/landing.yml`, and get a landing page with no picture. `index.md` now
486
- renders it inside the hero, decorative (empty `alt`) so a screen reader does
487
- not hear the headline twice. Found by building a site end to end through the
488
- new Grok path, where the "Hero image with …" shortcut pointed straight at the
489
- dead field.
490
- - **A partial hero patch silently wiped the rest of the hero** —
491
- `set_site_plan` replaced `landing.hero` wholesale, so the natural agent move
492
- of "write the copy now, add the artwork two turns later"
493
- (`{landing: {hero: {image}}}`) discarded the headline, subheadline and every
494
- call to action, and the landing page quietly reverted to the site title and
495
- the default buttons. The hero now merges field by field; `sections`,
496
- `navigation.items` and `pages` still replace, because a shorter list has to
497
- mean a shorter list. The tool description says so, and the regression test
498
- pins both halves.
499
-
500
- - **A new post appeared at its own URL but on no index** — the generated
501
- `_config_dev.yml` turned on Jekyll's experimental incremental regeneration,
502
- which rebuilds only documents whose own source changed. Adding a post left
503
- the collection index, the home page's latest-posts list and the feed showing
504
- the old set, with the post reachable only by typing its URL. New sites are
505
- now generated with `incremental: false`; a full rebuild of a fresh site takes
506
- about a second and is always right.
507
-
508
- - **The 15-second status poll handed the composer back mid-turn** — the Site
509
- Builder's proxy poll rebuilt the panel's enabled state from the connection
510
- alone, so during a long action (a Docker build, an image render) the input
511
- and Send button re-enabled themselves and the Stop button vanished, while
512
- tools were still running. A second message could then interleave with the
513
- first. The poll now respects the busy state.
514
- - **A newly added post never appeared on the running site** — Jekyll's
515
- `--watch` only tracks collection documents that existed when `serve` started,
516
- so a page or post the assistant added to a running project stayed invisible
517
- with no explanation. `write_project_file` now says so in its result and
518
- offers a restart, and `run_compose` gained a `restart` action.
519
-
520
- - **Switching colour mode destroyed every Mermaid diagram** — the old include's
521
- `MutationObserver` re-render emptied each `.mermaid` div and refilled it with
522
- the div's *current* text, which after the first render is the SVG's own
523
- stylesheet, and never cleared Mermaid's `data-processed` flag, so
524
- `mermaid.run()` skipped the div. Toggling dark mode on `/quickstart/`,
525
- `/about/` or `/docs/ruby-101/` replaced the diagram with a wall of
526
- `#mermaid-… {font-family: …}` text and a permanent "Loading diagram…" label
527
- (reproduced headlessly on `main`: `svgs: 1 → 0` after the toggle). Sources are
528
- now kept per figure and every render goes through `mermaid.render()`, so a
529
- mode or skin change re-renders from the original definition — asserted by the
530
- regression test in `test/visual/features/mermaid.spec.js`.
531
- - **The Obsidian wiki-link resolver rewrote the inside of inline SVGs** —
532
- `assets/js/obsidian-wiki-links.js` skips text under `CODE`/`PRE`/`A`/
533
- `SCRIPT`/`STYLE` by comparing `nodeName`, but elements in the SVG namespace
534
- report it in lower case (`style`, `text`), so a Mermaid diagram's own
535
- `<style>` was walked and every `#id` selector in it was turned into a
536
- `<a class="obsidian-tag">` link. The stylesheet lost its scoping: one
537
- diagram's `#arrowhead path { fill }` became a bare `path { fill }` for the
538
- whole page, and pie slices rendered dark grey while their legend showed the
539
- right colours. The old include escaped it only because the resolver
540
- special-cases the `.mermaid` class. The name check is now case-insensitive
541
- and any `<svg>` subtree (graphics, never prose) is skipped, with
542
- `.zer0-diagram` added to the class exemptions. `test/test_resolver.js` gains
543
- a fixture with a lower-case `style`/`text` inside `<svg>`, and the Mermaid
544
- spec asserts a pie slice's computed fill equals its own attribute and that no
545
- unscoped `path` rule exists on the page.
546
-
547
- ### Changed
548
-
549
- - **The page-feedback widget now files the fleet's issue contract, not its own
550
- (UPS-FB).** The theme built its own issue body, its own escaping, and its own
551
- URL-length trimming — and so did the 404 page and the AI chat, three builders
552
- in one repo with three different behaviours. All three now go through
553
- `FleetFeedbackCore` in `assets/js/fleet-feedback.js`, vendored byte-identically
554
- from the fleet kit (`bamr87/bamr87` `templates/feedback/`, held to the hub's
555
- copy by the drift gate). It is the same builder the fleet's `<fleet-feedback>`
556
- web component uses, so an issue filed from a page here and one filed from a
557
- React app elsewhere are byte-identical to the pipeline that triages them.
558
-
559
- What that adds to every filed issue: sections in a fixed order, a
560
- `<!-- fleet-feedback v1 type=... -->` marker the issue pipeline reads to
561
- recognise an already-structured report, and a real over-budget path. The AI
562
- chat in particular used to `.slice(0, 6000)` its body — silently dropping
563
- whatever Claude had written past that point — and now trims by section and
564
- hands the full text to the clipboard.
565
-
566
- The theme keeps its Bootstrap modal and its AI triage step; only the body
567
- assembly moved. Two UIs, one contract.
568
-
569
- - **Request-type labels map onto the fleet issue-pipeline taxonomy.**
570
- `enhancement`/`documentation` become `feature`/`docs`; `area:docs`/`area:feat`
571
- drop out. Every label the widget applies exists in this repo (GitHub silently
572
- drops the ones that do not), and a widget-filed issue is now pipeline-eligible
573
- on the next scan. The zero-`_data` fallback taxonomy carries them too, so
574
- `remote_theme` consumers benefit the moment those labels exist in their repo.
575
-
576
- - **Captured console lines are redacted before they enter the buffer.** Bearer
577
- tokens, API keys, JWTs, GitHub tokens and email addresses are masked on the
578
- way in, so a secret cannot be previewed, copied, or filed even by accident.
579
- Credentials reach the console more often than anyone expects — an
580
- `Authorization` header logged by a fetch wrapper, a signed URL in a 403.
581
-
582
- - **The capture buffer hooks `console.warn` and `console.error` only.**
583
- Previously `log`, `info` and `debug` were captured too. At a 40-entry ring,
584
- debug chatter evicts the one line that explains the failure — which is the
585
- line a report exists to carry. `_includes/core/console-capture.html` is now a
586
- two-line loader for the vendored buffer rather than 60 lines of inline script,
587
- which also *shrinks* `<head>` and buys back room for the charset meta that has
588
- to land in the first 1024 bytes (#372).
589
-
590
- ### Fixed
591
-
592
- - **The 404 page files a report a maintainer can act on.** "Submit an Issue on
593
- GitHub" opened a hand-built form containing two sentences of boilerplate — no
594
- page context, no environment, no captured errors. It now opens the feedback
595
- widget pre-typed as `fix-page` with the URL that failed (and the page that
596
- linked to it) already in the description.
597
-
598
- - **The feedback FAB survives its script failing to load.** It was a `<button>`
599
- whose only behaviour came from JavaScript; with the widget now depending on a
600
- second script, a 404 on either one would have left a decorative circle in the
601
- corner. It is an anchor to the issue form, upgraded in place when the script
602
- runs — the same progressive enhancement the inline "Improve" link already had.
603
-
604
- - **The page-feedback Playwright spec dismisses the cookie-consent banner.** The
605
- banner is a full-width bar on a layer above the FAB stack, so it swallowed
606
- every click aimed at the FAB. Every other spec that touches lower-screen chrome
607
- already seeded the consent choice through the shared fixture; this one never
608
- did.
609
-
610
- ### Changed
611
-
612
- - **TOC scroll spy now bolds the section you are actually reading** — the
613
- highlighted entry in the right-hand Table of Contents jumped around and sat
614
- one to three sections ahead of the viewport. Measured on `/docs/features/toc/`
615
- at 1280×820, the wrong entry was bolded at **19–23 of 25 scroll positions**
616
- — the count moves between runs because which implementation won depended on
617
- callback timing, which is the bug in one sentence.
618
-
619
- Three implementations were toggling `.active` on the same
620
- `#TableOfContents a` links, so whichever fired last won. Bootstrap's native
621
- ScrollSpy was wired up twice — `data-bs-spy="scroll"
622
- data-bs-target="#TableOfContents"` on `<body>` in `_layouts/root.html` *and*
623
- on `.bd-main` in `_layouts/default.html` (whose `data-bs-offset="100"` had
624
- been dead since Bootstrap 5.2 replaced that option with
625
- `data-bs-root-margin`). `assets/js/ui-enhancements.js` ran a third observer
626
- over *every* `a[href^="#"]` on the page, clearing `.active` from all of them
627
- each time a `section[id]` intersected. Both Bootstrap hooks are removed —
628
- the `<body>` one was also stripping server-rendered `.active` classes off
629
- admin sidebar links, which `fixtures.gotoBeforeScrollSpy` exists to work
630
- around — and the `ui-enhancements.js` observer is now scoped away from TOC
631
- and sidebar links.
632
-
633
- The theme's own spy (`assets/js/modules/navigation/scroll-spy.js`) was wrong
634
- on its own terms too: it asked IntersectionObserver for the "most visible"
635
- heading, but headings are a few pixels tall, so every heading inside the
636
- observer band reports the same `intersectionRatio` — the winner was whichever
637
- entry happened to be in that callback's batch, and a heading scrolling *out*
638
- of the band triggered no re-evaluation at all. It now applies a positional
639
- rule: the active heading is the last one whose top has crossed the reading
640
- line (the document's `scroll-padding-top`, the same offset anchor navigation
641
- uses), with the last heading winning once the page is scrolled to the bottom
642
- so trailing sections shorter than the viewport are still reachable. The
643
- answer is recomputed from scratch on each rAF-throttled scroll frame from
644
- cached heading offsets, re-measured on resize and content reflow
645
- (`ResizeObserver`), so it cannot drift out of sync.
646
-
647
- Two smaller fixes ride along. Clicking a TOC entry now holds that entry
648
- active while the smooth scroll animates, instead of flashing every heading
649
- passed on the way. And keeping the active entry visible inside the TOC no
650
- longer calls `scrollIntoView()`, which bubbles up and scrolls the *page* —
651
- feeding straight back into the spy; it adjusts the TOC container's own
652
- `scrollTop`, resolving that container at call time (`.bd-toc` on desktop,
653
- `.offcanvas-body` on mobile) rather than assuming one. The active link also
654
- carries `aria-current="true"`.
655
-
656
- `config.scrollSpy.rootMargin`/`threshold` are replaced by
657
- `config.scrollSpy.offset` (`null` = derive from `scroll-padding-top`) and
658
- `tolerance`. Guarded by `test/visual/features/scroll-spy.spec.js` (smoke
659
- tier), which is 5 failed / 1 passed against the pre-fix theme and 6 passed
660
- with the fix. (evidence:
661
- [`test/visual/evidence/scroll-spy/`](test/visual/evidence/scroll-spy/README.md)
662
- — wrong TOC highlight at 19/25 scroll positions → 0)
663
- - **Navbar controls no longer flicker under a stationary pointer (#404)** —
664
- `.nav-link:hover`, the Search/Settings utility buttons and the two sidebar/TOC
665
- FABs applied `transform: translateY(-1px)`. A 1px lift moves the element out
666
- from under a pointer parked on the row it vacates, so hover exits, the element
667
- drops back under the pointer, and hover re-enters — an infinite flicker with
668
- no pointer movement at all. Nav items are thin enough for a 1px lift to
669
- reliably do it.
670
-
671
- Deleting those declarations was **not sufficient**, and the reason is the
672
- substantive finding here: `_sass/components/_ui-enhancements.scss` applies
673
- `transform: translateY(-2px)` to `.btn:hover` — *every* button in the theme.
674
- The utility buttons and both FABs are `.btn`s, so with the navbar's own 1px
675
- lift removed the buttons kept flickering on the inherited 2px one — measured
676
- on the built site, the Settings button lost and regained hover 14 times in
677
- 800ms with the pointer completely still. The fix therefore cancels it
678
- explicitly with `transform: none`, the corrective pattern this repo already
679
- uses in `_post-navigation.scss` and its `prefers-reduced-motion` blocks. The
680
- FABs get the same treatment by the same reasoning — they are the same kind of
681
- `.btn` with the same lift — but they were not rendered at any viewport reached
682
- during verification, so that half is reasoned, not observed.
683
-
684
- Background, colour and shadow transitions are untouched, and the controls keep
685
- a pressed cue as `:active { transform: translateY(1px) }` — safe, because
686
- `:active` only holds while the button is held down, so the pointer cannot
687
- leave and re-enter in a loop.
688
-
689
- Three corrections to the issue as filed. The vacated row is the **bottom**
690
- edge, not the top — a probe parked on the top edge passes without exercising
691
- the bug at all. Only a transform on the **hovered element's own box** can
692
- flicker: `&:hover i { transform: scale(1.1) }` moves a child and never shrinks
693
- the parent's hit box, so those went for consistency, not as causes. And
694
- `.nav-link:hover`, named as the primary culprit, was **dead code** — its lift
695
- sat inside `@media (min-width: 992px) { @container navbar-main (max-width:
696
- 75rem) }`, and measured on the built site at 1040/1140/1200/1280px its
697
- computed hover transform is `none` at every width, on `main` too. Every
698
- flicker actually observed was on the utility buttons.
699
-
700
- Guarded by `test/visual/core/hover-stability.spec.js`: with the pointer on the
701
- control's centre the border box must be byte-identical to its resting box and
702
- the computed transform must be `none`; with the pointer held half a pixel
703
- inside the bottom edge for 800ms, `mouseleave` must stay at zero. The verdict
704
- is the *computed* value rather than "no matching rule exists", because
705
- `.btn:hover` does still match these controls and always will — it is
706
- overridden, not deleted. `mouseleave`, not `mouseout`: the latter also fires
707
- when the pointer crosses onto a descendant, which is not the element losing
708
- hover, and it produces a false positive on every `.nav-hover-dropdown` parent.
709
- Against `main` the spec is 4 failed / 1 passed; with the fix, 5 passed.
710
- Evidence in `test/visual/evidence/hover-flicker/` — the Settings button lost
711
- hover 14 times in 800ms before, 0 after.
628
+ Guarded by `test/visual/core/hover-stability.spec.js`: with the pointer on the
629
+ control's centre the border box must be byte-identical to its resting box and
630
+ the computed transform must be `none`; with the pointer held half a pixel
631
+ inside the bottom edge for 800ms, `mouseleave` must stay at zero. The verdict
632
+ is the *computed* value rather than "no matching rule exists", because
633
+ `.btn:hover` does still match these controls and always will — it is
634
+ overridden, not deleted. `mouseleave`, not `mouseout`: the latter also fires
635
+ when the pointer crosses onto a descendant, which is not the element losing
636
+ hover, and it produces a false positive on every `.nav-hover-dropdown` parent.
637
+ Against `main` the spec is 4 failed / 1 passed; with the fix, 5 passed.
638
+ Evidence in `test/visual/evidence/hover-flicker/` — the Settings button lost
639
+ hover 14 times in 800ms before, 0 after.
712
640
 
713
641
  Scoped to the navbar. The issue's original acceptance criterion — *no
714
642
  transform in any `:hover` rule under `_sass/`* — is 42 rules across ~15 files
@@ -737,7 +665,6 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
737
665
  constant, plus `test_theme_color_fallback_without_config` covering the
738
666
  no-config consumer case.
739
667
 
740
-
741
668
  - **Consumers no longer inherit the theme's own navigation (#332)** — `install.sh`
742
669
  copied the theme's entire `_data/` into every consumer, so each site's *own*
743
670
  `_data/navigation/*.yml` was a verbatim copy of the theme's and carried the
@@ -884,123 +811,327 @@ file. Only `## [Unreleased]` describes work that has not shipped yet.
884
811
  `visibilitychange`. Before/after measurements are in
885
812
  `test/visual/evidence/setup-wizard-nav-alignment/`.
886
813
 
887
- - **Language switcher: icon-only trigger and a compact, positive menu (T-038, #406)** —
888
- the navbar trigger drops its `EN` text span and the Bootstrap caret for a
889
- 38px square icon button, with `title` and `aria-label` carrying the language
890
- name. In the menu (both the navbar dropdown and the Settings panel variant),
891
- the current language is now marked with a tint and a check icon instead of
892
- Bootstrap's `.active` primary fill, which read as a selected nav item rather
893
- than "you are here". **Untranslated languages are no longer disabled rows** —
894
- they link to the source page and still record the `zer0-lang` preference, so
895
- the menu has no dead ends. Machine-generated translations carry a small
896
- `auto` chip, and the per-row "(Not yet translated)" text — the main driver of
897
- menu width — collapses into one footnote under a divider, referenced from
898
- each marked row via `aria-describedby`. The dropdown menu is capped at 220px.
899
- New `ui` keys `lang_machine_translated`, `lang_machine_translated_title` and
900
- `lang_untranslated_note` in `_data/ui-text.yml`; each use carries a literal
901
- fallback, because `core/i18n.html` replaces the `ui` map wholesale on a
902
- translated page and `_data/i18n/<lang>.yml` will not have them until
903
- `scripts/translate.rb` next runs.
814
+ - **Language switcher: icon-only trigger and a compact, positive menu (T-038, #406)** —
815
+ the navbar trigger drops its `EN` text span and the Bootstrap caret for a
816
+ 38px square icon button, with `title` and `aria-label` carrying the language
817
+ name. In the menu (both the navbar dropdown and the Settings panel variant),
818
+ the current language is now marked with a tint and a check icon instead of
819
+ Bootstrap's `.active` primary fill, which read as a selected nav item rather
820
+ than "you are here". **Untranslated languages are no longer disabled rows** —
821
+ they link to the source page and still record the `zer0-lang` preference, so
822
+ the menu has no dead ends. Machine-generated translations carry a small
823
+ `auto` chip, and the per-row "(Not yet translated)" text — the main driver of
824
+ menu width — collapses into one footnote under a divider, referenced from
825
+ each marked row via `aria-describedby`. The dropdown menu is capped at 220px.
826
+ New `ui` keys `lang_machine_translated`, `lang_machine_translated_title` and
827
+ `lang_untranslated_note` in `_data/ui-text.yml`; each use carries a literal
828
+ fallback, because `core/i18n.html` replaces the `ui` map wholesale on a
829
+ translated page and `_data/i18n/<lang>.yml` will not have them until
830
+ `scripts/translate.rb` next runs.
831
+
832
+ - **Language selector moved from the navbar into Settings → Appearance** — the
833
+ header utility cluster is now Search + Settings only, returning ~70px to the
834
+ menubar. `components/language-toggle.html` grew a `variant` parameter: the new
835
+ `panel` variant (shipped) renders a self-contained **Language** section — an
836
+ always-visible list of languages with the active one checked and
837
+ untranslated targets disabled — as the first block of the Settings
838
+ Appearance tab, next to Color Mode and Theme Skin. The original header
839
+ dropdown remains available as `variant="navbar"` for consumers that prefer
840
+ it. Same data, same `localStorage("zer0-lang")` preference, same
841
+ never-a-dead-link contract; `test/visual/features/language-toggle.spec.js`
842
+ was updated to exercise it through the Settings panel.
843
+
844
+ - **`_data/consumers.yml` now covers the whole fleet** — added `zer0-pages`,
845
+ `irony-works`, `wargames` and `zer0-pages-remote`, which were unregistered and
846
+ therefore invisible to `propagate.rb` in both directions (no release dispatch,
847
+ never in the drift report). The `it-journey` risk note was corrected: its
848
+ "~55 shadowed files / ~75 lines of !important CSS working around #338" claim
849
+ is not supported by `audit-consumer` — only 2 files shadow a theme file, the
850
+ CSS is 31 lines in this theme's own documented `user_overrides` hook, and no
851
+ fix numbered #338 could be substantiated in that repo.
852
+
853
+ - **The news/section layouts work with zero `_data`.** Section navigation
854
+ falls back from `_data/navigation/posts.yml` to `site.categories`; the hero
855
+ falls back to the newest post when nothing is marked `breaking`/`featured`;
856
+ tags links render only when a tags page exists (honoring `site.tags_page`);
857
+ archive month links render only when an archives page exists. A data-less
858
+ consumer now gets a fully working magazine homepage instead of empty chrome
859
+ and 404 links.
860
+ - **Zero-`_data` resilience across components.** The skin quick-select and
861
+ theme customizer fall back to the compiled skin registry when
862
+ `_data/theme_skins.yml` is absent; the page-feedback widget falls back to a
863
+ generic four-type taxonomy when `_data/feedback_types.yml` is absent; the
864
+ landing get-started band (theme install cards) renders only when the site
865
+ ships a `landing.get_started` block — a data-less consumer no longer
866
+ advertises the theme's own install methods.
867
+ - **`home` layout accepts `hide_intro` as an alias of `hide_title`**, matching
868
+ the flag other layouts use for "suppress the generated page header".
869
+ - **`halfmoon.js` honors `color_mode_default`.** The switcher no longer
870
+ overrides the configured default with the OS preference on first visit
871
+ (previously the page could flip modes right after load).
872
+ - The default skin fallback is now `air` everywhere (`root.html`,
873
+ controls bar, customizer) — `dark` was never a compiled skin, so the old
874
+ fallback selected a skin that doesn't exist.
875
+
876
+ - Installer spec default `ai.provider` is now `auto` (was `openai`); the AI
877
+ wizard records the provider that actually served the run.
878
+ - **CI workflows now honor `CLAUDE_CODE_OAUTH_TOKEN`** — `ui-audit.yml`,
879
+ `ai-content-review.yml`, and `ci-self-repair.yml` previously gated their
880
+ Claude Code agent tier on `ANTHROPIC_API_KEY` alone, silently ignoring a
881
+ configured OAuth token. They now follow the same house convention as
882
+ `claude.yml` / `issue-autopilot.yml`: `CLAUDE_CODE_OAUTH_TOKEN` preferred,
883
+ `ANTHROPIC_API_KEY` used only when it's absent.
884
+
885
+ ### Fixed
904
886
 
905
- ### Added
887
+ - **Color-mode toggle exposes the selected mode** ([#521](https://github.com/bamr87/zer0-mistakes/issues/521)).
888
+ `_includes/components/halfmoon.html` server-rendered Light/Dark/Auto all
889
+ `aria-pressed="false"`, and on load `assets/js/halfmoon.js` marked the
890
+ *resolved* OS scheme pressed, so a first visit on an `auto` site announced
891
+ "Dark, pressed" (WCAG 4.1.2). The configured mode is now server-rendered
892
+ `.active` + `aria-pressed="true"`, and the script marks the selected *mode*
893
+ (stored preference, else the site default) while still applying the
894
+ resolved scheme to `data-bs-theme`. Pinned by `test/visual/features/settings-panel.spec.js`.
895
+ - **Footer newsletter hint reaches screen readers on phones** ([#522](https://github.com/bamr87/zer0-mistakes/issues/522)).
896
+ The disabled `#footer-email` input's `aria-describedby` pointed at a
897
+ `d-none d-md-block` paragraph, so below `md` its only explanation was
898
+ `display:none`. The describedby target is now an always-present
899
+ `visually-hidden` span, and the visible paragraph is an `aria-hidden` duplicate.
900
+ The sighted layout is unchanged at every width. Pinned by `test/visual/core/accessibility.spec.js`.
901
+ - **Visual Snapshots no longer reload mid-test** ([#461](https://github.com/bamr87/zer0-mistakes/issues/461)).
902
+ `test/update-snapshots.sh` ran `npm ci` into the bind mount Jekyll was already
903
+ watching, and LiveReload reloaded the page under test ("Execution context was
904
+ destroyed"), hidden by `retries: 1`. The snapshot stack now starts with
905
+ `JEKYLL_LIVERELOAD=0` (no `--livereload`, plus `_config_snapshots.yml`, since
906
+ `_config_dev.yml` turns LiveReload on), and `test/lib/assert_no_livereload.sh`
907
+ fails the run if anything can still reload the page. Plain `docker compose up`
908
+ keeps LiveReload.
909
+ - **Mobile offcanvas TOC scroll spy is now under test** ([#457](https://github.com/bamr87/zer0-mistakes/issues/457)).
910
+ `test/visual/features/scroll-spy.spec.js` gains a 375px block that opens the
911
+ `#tocContents` offcanvas and asserts the active entry tracks the page and stays
912
+ visible inside `.offcanvas-body`. The no-op `gotoBeforeScrollSpy` fixture is
913
+ removed (the `data-bs-spy` it stripped is no longer rendered); its admin call
914
+ site uses `waitForJekyll`.
915
+ - **No orphaned microdata in `<head>`** ([#513](https://github.com/bamr87/zer0-mistakes/issues/513)).
916
+ `_includes/core/head.html` emitted `<meta itemprop="headline|description|datePublished|dateModified">`
917
+ with no `itemscope` ancestor, so every parser discarded them. The block is
918
+ removed; jekyll-seo-tag's JSON-LD and the article layouts' `BlogPosting` item
919
+ already publish the same facts. Pinned by `test/visual/core/head-contract.spec.js`.
920
+ - **Valid breadcrumb `BreadcrumbList` when a section has no index page** ([#512](https://github.com/bamr87/zer0-mistakes/issues/512)).
921
+ Since #204, an unlinked crumb ("Posts" on a site without `/posts/`, or a deep
922
+ middle segment) rendered as a `ListItem` with no `item`. That invalidated the
923
+ whole list, and Google dropped the breadcrumb rich result. Such crumbs now stay
924
+ visible as plain text outside the structured list, and `position` is renumbered
925
+ `1..n`. Every remaining `item` resolves. Pinned by `test/visual/features/breadcrumbs.spec.js`
926
+ (evidence: [`test/visual/evidence/breadcrumbs/`](test/visual/evidence/breadcrumbs/README.md):
927
+ the visible trail is unchanged, with 0px overflow before and after).
928
+
929
+ - **Intro action buttons keep their names on phones, nav chevrons meet the 24px target, and badge links are legible on every skin.** Below 576px the Share button and "Edit on GitHub" link in `_includes/content/intro.html` showed only an icon, so axe reported `button-name`/`link-name`; both now carry an `aria-label` matching their visible text. The edit and "View source" URLs no longer contain a double slash (`blob/main//pages/tags.md`) for pages outside a collection. The merged desktop split-toggle chevron (`_sass/core/_navbar.scss`) grows from 20px to 24px wide (WCAG 2.2 SC 2.5.8 `target-size`). Skin link colours (`_sass/theme/_skins.scss`) no longer apply to `a.badge`, which had turned tag/category chips purple-on-purple on neon (1.45:1, now 5.61:1). Regression test: `test/visual/features/intro-actions-a11y.spec.js`.
930
+ - **Closing the search modal returns focus to where it came from.** The modal is opened from code ("/", Ctrl/Cmd+K, the toggle's click handler), so Bootstrap's data-api focus restore never ran and Escape left focus on `<body>` (WCAG 2.4.3). `assets/js/search-modal.js` now remembers the opener (the clicked Search button, or the focused element) and falls back to the visible `[data-search-toggle]` when a shortcut opened it from the document. Two new tests in `test/visual/features/search.spec.js`.
931
+ - **The navbar brand title no longer truncates on phones.** Next to the sidebar toggle, logo, search and the labelled Menu toggle, a 14-character title at the 20px `.navbar-brand` size lost its tail ("Lifehacker.dev" → "Lifehacker…" on lifehacker.dev post pages at 400px: 121px for 133px of text). Below 576px `#navbar .site-title-text` steps down to 1rem (0.9375rem below 375px), and the logo drops Bootstrap's 1rem `margin-end`, which stacked on the brand group's 0.5rem gap. That title now fits from 375px up on pages with the sidebar toggle and from 320px up without it. Two new tests in `test/visual/features/intro-actions-a11y.spec.js`.
932
+ - **The cookie banner no longer covers the first screen.** Below 768px `components/cookie-consent.html` stacked a heading, a long sentence and three full-width buttons (the shim in `_sass/core/_sidebar-extras.scss`): 324px of an 850px-tall 400px viewport on it-journey.dev and lifehacker.dev, hiding most of the hero or article; on desktop it was 94px and sat over the lower hero copy. It is now a compact bar: a two-line summary over one row of three equal buttons on phones, and a single row from 768px up. The layout lives in `_sass/components/_cookie-banner.scss` and the stacking shim is gone. The banner is a labelled `region` ("Cookie consent"), and the button ids, the modal and the consent logic are unchanged. Height at 400px drops from 324px to 107px, and at 1366px from 94px to 48px. Pinned by `test/visual/features/cookie-banner-compact.spec.js`.
933
+ - **The active skin now moves the whole `--zer0-color-*` layer, not just Bootstrap's** ([#459](https://github.com/bamr87/zer0-mistakes/issues/459)). `_includes/core/tokens-inline.html` re-set `--zer0-color-primary`, `-link` and `-accent` from `theme_color` on `:root` after `main.css`. `:root` and `[data-theme-skin]` have the same specificity, so the config value won on source order. On any site that sets `theme_color`, components reading the theme layer therefore kept the config color in every skin, and `--zer0-color-primary` disagreed with its own `-rgb` companion. The skin now wins: those three keys are emitted only when no palette skin is active, as with `dark` and `contrast`. `theme_color` still sets `secondary`, `danger`, `warning`, `success` and `info`, and the Appearance-panel override still beats both. The rule is documented in [`docs/ui/design-tokens.md`](docs/ui/design-tokens.md#precedence-skin-vs-theme_color) and checked by `test/visual/features/appearance.spec.js` (evidence: [`test/visual/evidence/agent-issue-459/`](test/visual/evidence/agent-issue-459/README.md) — page overflow 0px at six widths; the nine skin baselines are unchanged).
934
+ - **A preview path without a leading slash no longer 404s.** `preview: images/previews/x.webp` was appended straight onto `assets_prefix`, so `components/preview-image.html`, `components/background-image.html`, `content/intro.html` and `content/seo.html` emitted `/assetsimages/previews/x.webp` for the card image, the hero background and `og:image` (165 it-journey content files use that form). All four now prepend `/` to a site-relative path before joining; URLs and `data:` URIs are left alone, and already-slashed or `/assets/`-prefixed paths render byte-identically. `preview-image.html` also gained optional `width`/`height` parameters (emitted only when passed); the article layout's related-post cards pass 1536×1024 so their box is reserved before the lazy image arrives (0px → 217px at 1366px). Pinned by the `Preview Path Join` unit test in `test/test_core.sh` and `test/visual/features/preview-path-join.spec.js`.
935
+ - **The sitemap index no longer puts every page in the DOM twice.** `content/sitemap.html` (the `/sitemap/` page, the search modal's "View all results" target, and it-journey.dev's `/search/`) kept all rows attached. Its script also eagerly built a second copy of every row as a card for the hidden card view: about 19,900 elements on it-journey's `/search/` (431 entries), with Lighthouse mobile TBT at 9.7 s. Rows are still rendered server-side, so the page works without JS and with `?q=`. The script now keeps only one page of 50 matches in the document and appends the next page with **Show more**. Search, filters and sorting work across every row, and cards are built on the first switch to card view, one page at a time. Card text is set through `textContent` rather than interpolated HTML. On the theme's own `/sitemap/` (386 entries), elements after load drop from 23,063 to 8,086, and local Lighthouse mobile TBT drops from 1,662 ms to 181 ms. Badge contrast is fixed too: the collection pill is `text-bg-info` (was white on `#0dcaf0`, 1.96:1), tag chips use `--bs-primary-text-emphasis`, and `+N` overflow chips (white on transparent) get body text and a border. Pinned by `test/visual/features/sitemap-pagination.spec.js`.
936
+ - **The xAI preview-image renderer defaults to `grok-imagine-image-2.0`** ([#474](https://github.com/bamr87/zer0-mistakes/issues/474)). `XAIProvider.default_model()` in `scripts/lib/preview_generator.py` still returned the retired `grok-2-image`, so a site with `preview_images.provider: xai` and no explicit `model:` sent a retired model upstream. An explicitly configured `grok-2-image` is still accepted as an xAI-family model.
937
+ - **A code-copy partial no longer restyles every button on the site** ([#412](https://github.com/bamr87/zer0-mistakes/issues/412)). `_sass/core/code-copy.scss` shipped a bare `.button, button:not(.copy)` rule — an element selector at specificity (0,1,1) that out-ranked `.btn` and any consumer's own class, applying `padding: 0 20px`, `font-size: 11px` and a hardcoded `#bbb` border site-wide. It is scoped to `.code-block-header` / `pre.highlight` now and uses `--zer0-*` tokens, so `#bbb` is gone from the compiled stylesheet. Every button on the site — the theme's own chrome included — gets its intended metrics back (evidence: [`test/visual/evidence/agent-issue-412/`](test/visual/evidence/agent-issue-412/README.md) — page overflow 0px at six widths; the nine skin baselines moved, and every red region in their diffs is a `<button>`).
938
+ - **The article hero no longer lazy-loads its LCP image or shoves the article down when it arrives** ([#485](https://github.com/bamr87/zer0-mistakes/issues/485)). `figure.featured-hero` inherited `components/preview-image.html`'s `loading="lazy"` default and had no reserved box. It now passes `loading="eager" fetchpriority="high" decoding="async"`, and its box is pinned by `.featured-hero img { aspect-ratio: 3 / 2 }`. The box is set in CSS rather than with `width`/`height` attributes, because preview assets vary in shape and `object-fit: cover` crops a portrait instead of stretching it. `preview-image.html` gained optional `fetchpriority`/`decoding` parameters that are emitted only when passed, so every other consumer renders byte-identically (evidence: [`test/visual/evidence/hero-lcp/`](test/visual/evidence/hero-lcp/README.md) — hero loading lazy → eager + fetchpriority=high; content shift when the hero lands 476px → 0px across 2 widths × landscape/portrait).
939
+ - **Mermaid, cytoscape and PostHog no longer load up front, and the auto-hide navbar stops forcing reflows.** `assets/js/mermaid-diagrams.js` now converts fences into figures straight away but injects the ~1 MB Mermaid bundle only when a diagram exists and the first one comes within 400px of the viewport (IntersectionObserver); `components/mermaid.html` passes the bundle URL as `window.zer0MermaidSrc` (a script global, not DOM text, and only http(s) URLs are injected) instead of a `<script defer>`. The local-graph panel loads cytoscape on first open instead of on every indexed page. `analytics/posthog.html` keeps the queueing stub but fetches `array.js` after `load` + idle, and `core/head.html` no longer includes it a second time (`root.html` already did, so every production page called `posthog.init()` twice). `auto-hide-nav.js` measures the navbar with a ResizeObserver (rAF read/next-frame write fallback) and caches the height instead of reading `offsetHeight` at DOMContentLoaded and on resize. Lighthouse mobile, local static build: home TBT 1,705 → 651 ms and weight 5,581 → 2,327 KiB; `/docs/features/mermaid-diagrams/` TBT 2,017 → 243 ms; `auto-hide-nav.js` gone from forced-reflow. New lazy-load test in `test/visual/features/mermaid.spec.js`.
906
940
 
907
- - **Cookbook & recipes collection (ZER0-084)** — a `recipes` collection that
908
- renders structured front matter as a working recipe page: `layout: recipe`
909
- builds a fact bar (prep/cook/rest/total, yield, difficulty, cuisine, oven),
910
- a grouped ingredient checklist, a numbered method with per-step timings and
911
- temperatures, a baker's-percentage ratio table computed at build time, a
912
- per-serving nutrition panel, and one `schema.org/Recipe` JSON-LD block.
913
- `assets/js/recipe-scaler.js` adds serving scaling and live US↔metric
914
- conversion — including volume↔weight via `_data/ingredient_densities.yml`
915
- — as progressive enhancement, so every amount is still rendered exactly as
916
- authored with JavaScript disabled. `layout: cookbook` indexes recipes into
917
- course sections from `_data/recipe_courses.yml`. Twelve new
918
- `components/recipe-*.html` includes, `_sass/components/_recipe.scss` (with
919
- print styles), a demo cookbook at `/recipes/`, and docs at
920
- [Cookbook collection](pages/_docs/features/cookbook-collection.md). Regression
921
- coverage: `test/visual/features/recipes.spec.js` (smoke tier) and "Cookbook
922
- Recipe Collection" in `test/test_core.sh` (evidence:
923
- [`test/visual/evidence/recipes-collection/`](test/visual/evidence/recipes-collection/README.md)
924
- — 0px page overflow across 320–1440px; 1 cup butter → 227 g, 500 g flour →
925
- 4 cups; ratio percentages identical at 1× and 3×).
941
+ - **The Site Builder no longer jumps the first time it saves a draft.**
942
+ `showDraftChip()` cleared the "Draft saved" chip's `hidden` attribute, and the
943
+ 2s timer that follows only removes `is-visible` — an opacity class — so the
944
+ first debounced save moved the chip from `display: none` into layout *for
945
+ good*. Its box is 26px against the 19px "Start over" button beside it, so
946
+ `.wizard-header` grew and took `#wizardTabContent` and every Back/Next row
947
+ below it down with it: 7px where the header's right-hand block had already
948
+ wrapped, a whole wrapped line where those 7px were what tipped it over. The
949
+ chip now holds its box from first paint and is hidden with `visibility`,
950
+ which is what `_setup-wizard.scss` said it did all along ("Kept in the layout
951
+ (no display toggling) so its appearance never shifts the header") and still
952
+ keeps it out of the accessibility tree when it has nothing to say. This is
953
+ what failed the `@critical` stable-height spec on every push to `main`
954
+ (`2029, 2036, 2036, …`), and with it the retry that spec's failure forced
955
+ ([bamr87/bamr87#265](https://github.com/bamr87/bamr87/issues/265)) (evidence:
956
+ [`test/visual/evidence/agent-issue-265/`](test/visual/evidence/agent-issue-265/README.md)
957
+ — first draft save moved the wizard 42px on the base branch, 0px here).
926
958
 
927
- - **Page-view counter (ZER0-083)** — the theme now tracks a view for the page
928
- being read and displays the count in the article meta row, next to the
929
- reading time (`👁 12 views`). Two providers, configured under `page_views:`
930
- in `_config.yml`: `local` (the default — counts live in the visitor's
931
- `localStorage`, so it works on GitHub Pages with no server, no network, and
932
- no third party) and `remote` (counts come from an HTTP counter endpoint you
933
- control, with `{path}` substitution, a configurable method, and a dotted
934
- `count_key` for the response shape). Privacy gates mirror the `posthog:`
935
- block — Do Not Track, Global Privacy Control, and an optional
936
- `require_consent` tie-in to the cookie-consent "analytics" category — and
937
- they suppress *recording* only, never the display of a count that is already
938
- known; a view blocked pending consent is recorded when consent arrives.
939
- `dedupe: session` counts one view per page per browser session so reloads do
940
- not inflate it. The badge include (`components/page-views.html`) ships hidden
941
- and is revealed only once a count exists, carrying its own leading separator
942
- so a page with no data leaves no dangling bullet. New:
943
- `_includes/components/page-views.html`,
944
- `_includes/components/page-views-init.html`, `assets/js/page-views.js`,
945
- `_sass/components/_page-views.scss`. Regression spec:
946
- `test/visual/features/page-views.spec.js`. Docs:
947
- [`docs/features/page-views.md`](docs/features/page-views.md)
948
- (evidence: [`test/visual/evidence/page-views/`](test/visual/evidence/page-views/README.md)).
959
+ - **Two `test_core.sh` checks could never fail, and one shouted on every clean
960
+ run (#460).** The Liquid nested-tag check grepped for
961
+ {% raw %}`{{.*{{`{% endraw %}, which matches
962
+ any two *sibling* output tags on one line — it flagged 64 of the theme's
963
+ includes, starting with `navigation/sidebar-pagetree.html`'s valid
964
+ {% raw %}`{{ _base }}{{ _section }}`{% endraw %} — and its `return 1` sat inside a `find | while`
965
+ pipeline, i.e. a subshell, so the function printed `[ERROR]` and then reported
966
+ the check as passed. The gem-content check ran `tar -tzf` on the built `.gem`,
967
+ but a gem is an *uncompressed* tar wrapping `metadata.gz`, `data.tar.gz` and
968
+ `checksums.yaml.gz`, so gzip printed `stdin: not in gzip format` four times
969
+ and both branches fell through to "Gem may not contain…" without ever
970
+ inspecting the payload — a gem shipped with no `_layouts/` would have passed.
971
+ The nested-tag pattern is now {% raw %}`{{[^}]*{{`{% endraw %} (cannot cross the first `}`, so
972
+ siblings do not match), both loops read from a process substitution so a
973
+ failure propagates, and the gem check extracts `data.tar.gz` and asserts
974
+ `_layouts/` and `assets/` are really in it. The layout balance check, now
975
+ live, blanks Liquid comment bodies before counting, so prose in a comment that
976
+ quotes a closing delimiter (as `_layouts/root.html`'s per-page-scripts note
977
+ does) is no longer read as an unbalanced tag. New `test/test_core_checks.sh`
978
+ drives both checks against known-good and known-bad fixtures, so a check that
979
+ stops being able to fail is itself a test failure. `./test/test_core.sh` now
980
+ runs clean: 26/26, no `[ERROR]` line, no gzip/tar noise.
981
+ - **Every navbar link emitted invalid HTML — attributes glued together with no
982
+ separating whitespace (#465)** — the Liquid whitespace-trim markers around the
983
+ conditional `aria-current` in `_includes/navigation/navbar.html` ate the
984
+ newlines that separated the surrounding attributes, so the primary nav
985
+ rendered `aria-label="News"aria-current="page"title="News"` (the WHATWG
986
+ `missing-whitespace-between-attributes` parse error). The leading {% raw %}`{%-`{% endraw %}
987
+ stripped the newline after `aria-label` and the trailing {% raw %}`-%}`{% endraw %} the whitespace
988
+ before `title`, which means **two of the four sites were broken on every page,
989
+ not just the current one**: where the conditional sits between two
990
+ unconditional attributes both markers fire even when the `if` emits nothing.
991
+ Parsers recover, so nothing looked broken — but the attributes affected are
992
+ exactly `aria-label`, `aria-current` and `title`, and a stricter parser is
993
+ entitled to drop the "you are here" announcement for screen-reader users. All
994
+ four sites now use the non-trimming {% raw %}`{% if %}`{% endraw %} form already present at line 26
995
+ of the same file, with the separator outside the tag. Two guards in
996
+ `test/test_core.sh`: `test_navbar_attribute_whitespace` renders the real
997
+ include through Liquid across both `aria-current` branches and both nav modes
998
+ (6 of 8 rendered variants were invalid before the fix), and
999
+ `test_attribute_whitespace_in_markup` models the trim rules over every include
1000
+ and layout with no gem dependency — it found a second live instance, a literal
1001
+ missing space in `components/theme-preview-gallery.html`, fixed here too.
1002
+ `test_jekyll_build` re-checks the delivered `_site` bytes. No visual change.
1003
+ - **The weekly UI/UX audit was blind, and reported it as clean.** `sweep.mjs`
1004
+ built its pages with `browser.newPage()`, which `@axe-core/playwright`
1005
+ refuses; the throw was caught by a single per-route `try` that also discarded
1006
+ the overflow, console-error and link-collection data already gathered for
1007
+ that route. Screenshots still succeeded, so the harness looked alive while
1008
+ every accessibility, console, overflow and broken-link result on every route
1009
+ was silently dropped — and the report rendered the absence as "0 axe
1010
+ violations / 0 broken links". Pages are now built from `browser.newContext()`,
1011
+ each measurement fails independently, a scan that errored is reported as
1012
+ UNKNOWN rather than clean, and a measurement that fails on every route turns
1013
+ the sweep red the way a total capture failure already did. On the demo site
1014
+ the sweep goes from 18/21 captured with 21 blackout entries to **21/21
1015
+ captured, 0 harness errors, 191 links crawled and 8 genuinely broken links
1016
+ found** ([#468](https://github.com/bamr87/zer0-mistakes/issues/468)).
1017
+ - **Theme-skin buttons now announce which skin is applied.** The Theme Skin
1018
+ group in Settings → Appearance conveyed selection only through the Bootstrap
1019
+ `.active` class, which carries no accessibility semantics, so screen readers
1020
+ announced nine identical unlabelled buttons — WCAG 2.1 SC 4.1.2 (Name, Role,
1021
+ Value). Each button now ships `aria-pressed`, and the click and "Reset
1022
+ background" handlers keep it in sync with `.active` through one shared
1023
+ helper, matching the sibling color-mode group
1024
+ ([#467](https://github.com/bamr87/zer0-mistakes/issues/467)).
949
1025
 
950
- - **Search shortcut affordances (ZER0-032)** — the navbar search button now
951
- advertises its keyboard shortcuts, matching the design-system navbar
952
- pattern: a quiet `/` kbd chip (xl-up), a hover title ("Search — press / or
953
- Ctrl+K", shown as `⌘K` on macOS), and `aria-keyshortcuts`; the sidebar (`b`)
954
- and table-of-contents (`t`) hide toggles gained shortcut hover hints, and
955
- the keyboard-shortcuts modal now lists `⌘/Ctrl+K`
956
- (evidence: [`test/visual/evidence/search-shortcut-hint/`](test/visual/evidence/search-shortcut-hint/README.md)).
1026
+ - **Generated posts and notes were invisible on GitHub Pages** — every dated
1027
+ document the wizard wrote was stamped `T09:00:00.000Z` on the day it was
1028
+ generated. Jekyll refuses to publish future-dated documents unless
1029
+ `future: true`, and GitHub Pages builds with the default, so a site generated
1030
+ before 09:00 UTC went live with its posts and notes missing: linked from the
1031
+ collection index and listed in the sitemap, but 404 when clicked. Generated
1032
+ content is now stamped with the actual moment it was written, and a
1033
+ regression test fails if any generated document carries a future date.
1034
+ Found by publishing two example sites and clicking the links.
1035
+ - **Every page of every generated site 404'd on `user-overrides.js`** — the
1036
+ Site Builder writes `user_overrides: true` so the theme loads the palette and
1037
+ font overrides it generates, but that same flag also makes the theme load
1038
+ `assets/js/user-overrides.js`, which the wizard never created. Generated
1039
+ sites now ship a commented stub, so the request resolves and site owners have
1040
+ an obvious place to put their own JavaScript. Found while publishing example
1041
+ sites built by the wizard.
1042
+ - **Every generated landing page shipped an invisible button** —
1043
+ `components/cta-button.html` mapped `variant: outline` to `btn-outline-light`,
1044
+ which is white text on a white border, and the Site Builder's own default
1045
+ call to action uses `outline` on a light hero. `outline` is now an outlined
1046
+ *primary* button; `outline-light` is the explicit variant for a dark surface,
1047
+ and the generated landing engine picks between them from `hero.variant`.
1048
+ The theme's own homepage hero is `bg-primary text-white`, so its two
1049
+ outlined buttons moved to `outline-light` and render exactly as before —
1050
+ a consumer who copied `_data/landing.yml` into a site with a dark hero
1051
+ should make the same one-word change.
1052
+ - **A planned `landing.hero.image` was accepted and then thrown away** — the
1053
+ site plan's schema has offered `landing.hero.image` since the Site Builder
1054
+ shipped, but the landing engine it generates never rendered it, so an agent
1055
+ (or a human) could set a hero image, see it validated, watch it land in
1056
+ `_data/landing.yml`, and get a landing page with no picture. `index.md` now
1057
+ renders it inside the hero, decorative (empty `alt`) so a screen reader does
1058
+ not hear the headline twice. Found by building a site end to end through the
1059
+ new Grok path, where the "Hero image with …" shortcut pointed straight at the
1060
+ dead field.
1061
+ - **A partial hero patch silently wiped the rest of the hero** —
1062
+ `set_site_plan` replaced `landing.hero` wholesale, so the natural agent move
1063
+ of "write the copy now, add the artwork two turns later"
1064
+ (`{landing: {hero: {image}}}`) discarded the headline, subheadline and every
1065
+ call to action, and the landing page quietly reverted to the site title and
1066
+ the default buttons. The hero now merges field by field; `sections`,
1067
+ `navigation.items` and `pages` still replace, because a shorter list has to
1068
+ mean a shorter list. The tool description says so, and the regression test
1069
+ pins both halves.
957
1070
 
958
- - **Live design-system pages (ZER0-082)** — the design system is now
959
- part of the site itself: `_design-system/` is force-included in the Jekyll
960
- build so `styles.css`, every token file, specimen cards, and component
961
- sources get real URLs under `/_design-system/`; a new `/design/` hub page
962
- renders the foundation specimens in live iframes (colors, skins, type,
963
- spacing, radii, motion, layers, iconography), catalogs the component twins,
964
- and links the Claude Design page canvas (all 24 layouts as a browsable
965
- presentation). Navbar: About → Design System. Regression spec:
966
- `test/visual/features/design-hub.spec.js`.
1071
+ - **A new post appeared at its own URL but on no index** — the generated
1072
+ `_config_dev.yml` turned on Jekyll's experimental incremental regeneration,
1073
+ which rebuilds only documents whose own source changed. Adding a post left
1074
+ the collection index, the home page's latest-posts list and the feed showing
1075
+ the old set, with the post reachable only by typing its URL. New sites are
1076
+ now generated with `incremental: false`; a full rebuild of a fresh site takes
1077
+ about a second and is always right.
967
1078
 
968
- - **Claude Design system round-trip (ZER0-081)** — the design-token layer is now
969
- aligned with the zer0-mistakes Claude Design project
970
- (`claude.ai/design/p/e75121c0-9210-42d1-ade3-2c8af9111cbe`): new
971
- `--zer0-radius-*`, `--zer0-logo-*`, and `--zer0-text-display-*` tokens in
972
- `_sass/tokens/`; the design project vendored as a git mirror under
973
- `_design-system/` (plain-CSS tokens, React component specs, guideline cards,
974
- website UI kit — sync contract in `_design-system/SYNC.md`); and
975
- `scripts/design-system-check.rb` wired into the core test suite so the theme
976
- and the mirror cannot drift silently. Callouts, landing feature cards, mobile
977
- nav, tables, and focus rings now consume the radius tokens (value-preserving —
978
- rendered output unchanged, pinned by a new runtime token assertion in
979
- `test/visual/core/styling.spec.js`).
980
- - **Design-system component twins (PR #391)** — `Fab`/`FabStack`, `PostCard`,
981
- and `Skeleton` join Button/Badge/Card/FeatureCard/Callout/Input in
982
- `_design-system/components/`, plus new Motion and Layers foundation cards;
983
- all pushed live to the Claude Design project. The feature registry is
984
- re-synced (`features/features.yml` ↔ `_data/features.yml` — #389's ZER0-081
985
- entry had landed in the `_data` mirror only, which the registry validator
986
- rejects) and `docs/ui/components.md` gains a theme-source →
987
- design-system-twin map.
1079
+ - **The 15-second status poll handed the composer back mid-turn** — the Site
1080
+ Builder's proxy poll rebuilt the panel's enabled state from the connection
1081
+ alone, so during a long action (a Docker build, an image render) the input
1082
+ and Send button re-enabled themselves and the Stop button vanished, while
1083
+ tools were still running. A second message could then interleave with the
1084
+ first. The poll now respects the busy state.
1085
+ - **A newly added post never appeared on the running site** — Jekyll's
1086
+ `--watch` only tracks collection documents that existed when `serve` started,
1087
+ so a page or post the assistant added to a running project stayed invisible
1088
+ with no explanation. `write_project_file` now says so in its result and
1089
+ offers a restart, and `run_compose` gained a `restart` action.
1090
+
1091
+ - **Switching colour mode destroyed every Mermaid diagram** — the old include's
1092
+ `MutationObserver` re-render emptied each `.mermaid` div and refilled it with
1093
+ the div's *current* text, which after the first render is the SVG's own
1094
+ stylesheet, and never cleared Mermaid's `data-processed` flag, so
1095
+ `mermaid.run()` skipped the div. Toggling dark mode on `/quickstart/`,
1096
+ `/about/` or `/docs/ruby-101/` replaced the diagram with a wall of
1097
+ `#mermaid-… {font-family: …}` text and a permanent "Loading diagram…" label
1098
+ (reproduced headlessly on `main`: `svgs: 1 → 0` after the toggle). Sources are
1099
+ now kept per figure and every render goes through `mermaid.render()`, so a
1100
+ mode or skin change re-renders from the original definition — asserted by the
1101
+ regression test in `test/visual/features/mermaid.spec.js`.
1102
+ - **The Obsidian wiki-link resolver rewrote the inside of inline SVGs** —
1103
+ `assets/js/obsidian-wiki-links.js` skips text under `CODE`/`PRE`/`A`/
1104
+ `SCRIPT`/`STYLE` by comparing `nodeName`, but elements in the SVG namespace
1105
+ report it in lower case (`style`, `text`), so a Mermaid diagram's own
1106
+ `<style>` was walked and every `#id` selector in it was turned into a
1107
+ `<a class="obsidian-tag">` link. The stylesheet lost its scoping: one
1108
+ diagram's `#arrowhead path { fill }` became a bare `path { fill }` for the
1109
+ whole page, and pie slices rendered dark grey while their legend showed the
1110
+ right colours. The old include escaped it only because the resolver
1111
+ special-cases the `.mermaid` class. The name check is now case-insensitive
1112
+ and any `<svg>` subtree (graphics, never prose) is skipped, with
1113
+ `.zer0-diagram` added to the class exemptions. `test/test_resolver.js` gains
1114
+ a fixture with a lower-case `style`/`text` inside `<svg>`, and the Mermaid
1115
+ spec asserts a pie slice's computed fill equals its own attribute and that no
1116
+ unscoped `path` rule exists on the page.
988
1117
 
989
- ### Changed
1118
+ - **The 404 page files a report a maintainer can act on.** "Submit an Issue on
1119
+ GitHub" opened a hand-built form containing two sentences of boilerplate — no
1120
+ page context, no environment, no captured errors. It now opens the feedback
1121
+ widget pre-typed as `fix-page` with the URL that failed (and the page that
1122
+ linked to it) already in the description.
990
1123
 
991
- - **Language selector moved from the navbar into Settings → Appearance** — the
992
- header utility cluster is now Search + Settings only, returning ~70px to the
993
- menubar. `components/language-toggle.html` grew a `variant` parameter: the new
994
- `panel` variant (shipped) renders a self-contained **Language** section — an
995
- always-visible list of languages with the active one checked and
996
- untranslated targets disabled — as the first block of the Settings
997
- Appearance tab, next to Color Mode and Theme Skin. The original header
998
- dropdown remains available as `variant="navbar"` for consumers that prefer
999
- it. Same data, same `localStorage("zer0-lang")` preference, same
1000
- never-a-dead-link contract; `test/visual/features/language-toggle.spec.js`
1001
- was updated to exercise it through the Settings panel.
1124
+ - **The feedback FAB survives its script failing to load.** It was a `<button>`
1125
+ whose only behaviour came from JavaScript; with the widget now depending on a
1126
+ second script, a 404 on either one would have left a decorative circle in the
1127
+ corner. It is an anchor to the issue form, upgraded in place when the script
1128
+ runs — the same progressive enhancement the inline "Improve" link already had.
1002
1129
 
1003
- ### Fixed
1130
+ - **The page-feedback Playwright spec dismisses the cookie-consent banner.** The
1131
+ banner is a full-width bar on a layer above the FAB stack, so it swallowed
1132
+ every click aimed at the FAB. Every other spec that touches lower-screen chrome
1133
+ already seeded the consent choice through the shared fixture; this one never
1134
+ did.
1004
1135
 
1005
1136
  - **Finished work kept reopening its own issues** — `scripts/sync-backlog.rb`
1006
1137
  treats `_data/backlog.yml` as the source of truth and **reopens** any issue whose
@@ -1178,7 +1309,6 @@ worse than none.
1178
1309
  `#info-section` above it, for browsers without `:has()`. Covered by "Settings
1179
1310
  opened from the nav menu paints ABOVE it" in
1180
1311
  `test/visual/features/mobile-overlay-stacking.spec.js` (evidence:
1181
- [`test/visual/evidence/navbar-fit/`](test/visual/evidence/navbar-fit/README.md)
1182
1312
  — `03-settings-stacking.png`: top element at the panel centre `a.nav-link`
1183
1313
  → the panel's own content; header `z-index` 1046 → 1030).
1184
1314
 
@@ -1277,49 +1407,6 @@ worse than none.
1277
1407
  builds in safe mode and loads no local plugins, so nothing the theme ships is
1278
1408
  mandatory; `required_plugin_paths` is now empty and the plugin is optional.
1279
1409
 
1280
- ### Changed
1281
-
1282
- - **`_data/consumers.yml` now covers the whole fleet** — added `zer0-pages`,
1283
- `irony-works`, `wargames` and `zer0-pages-remote`, which were unregistered and
1284
- therefore invisible to `propagate.rb` in both directions (no release dispatch,
1285
- never in the drift report). The `it-journey` risk note was corrected: its
1286
- "~55 shadowed files / ~75 lines of !important CSS working around #338" claim
1287
- is not supported by `audit-consumer` — only 2 files shadow a theme file, the
1288
- CSS is 31 lines in this theme's own documented `user_overrides` hook, and no
1289
- fix numbered #338 could be substantiated in that repo.
1290
-
1291
- ## [1.28.0](https://github.com/bamr87/zer0-mistakes/compare/v1.27.0...v1.28.0) (2026-08-09)
1292
-
1293
-
1294
- ### Features
1295
-
1296
- * **books:** children's picture-book collection with immersive layouts and components ([#328](https://github.com/bamr87/zer0-mistakes/issues/328)) ([e059d1a](https://github.com/bamr87/zer0-mistakes/commit/e059d1afdc95073c5ce5bf9d1516b362979ad402))
1297
- * **ci:** deploy the site via Actions so examples reach the live domain ([#353](https://github.com/bamr87/zer0-mistakes/issues/353)) ([959243c](https://github.com/bamr87/zer0-mistakes/commit/959243c0e8e504fe5d8dba44b89016b413bc88da))
1298
- * **docs:** theme-canary reporting contract for downstream theme-scouts (FF-0019) ([#358](https://github.com/bamr87/zer0-mistakes/issues/358)) ([c1c1b30](https://github.com/bamr87/zer0-mistakes/commit/c1c1b309c5504b014602982fc96ec80c3f8f70de))
1299
- * **examples:** add serialized reading-blog example site ([#339](https://github.com/bamr87/zer0-mistakes/issues/339)) ([aca0b53](https://github.com/bamr87/zer0-mistakes/commit/aca0b53b88067bda80fb299f9d5148ccb185d618))
1300
- * **includes:** config-driven favicon, GTM gating, og:image fallback fix, sidebar nav aliases ([#327](https://github.com/bamr87/zer0-mistakes/issues/327)) ([fe8aacc](https://github.com/bamr87/zer0-mistakes/commit/fe8aacc234003f9f170fcb3b8b29c60ac18ed94b))
1301
- * **install:** Claude Code OAuth + multi-provider AI + config-file layer ([#330](https://github.com/bamr87/zer0-mistakes/issues/330)) ([fde414c](https://github.com/bamr87/zer0-mistakes/commit/fde414cce5286f90de1ad02ca6ca40d634be5611))
1302
- * **layouts:** book-abc board-book layout + ABC art-style skins ([#336](https://github.com/bamr87/zer0-mistakes/issues/336)) ([d88038e](https://github.com/bamr87/zer0-mistakes/commit/d88038e806e0d1a492e5d12b4e10e55fd240d692))
1303
- * **navigation:** nav: pages sidebar mode — auto-build the tree from page URLs ([#333](https://github.com/bamr87/zer0-mistakes/issues/333)) ([c0c627d](https://github.com/bamr87/zer0-mistakes/commit/c0c627d5f50051e8f5eb4f0d6681244acaaa8d85))
1304
- * **obsidian:** revamp graph frontend with Obsidian-style controls ([#366](https://github.com/bamr87/zer0-mistakes/issues/366)) ([7143d9c](https://github.com/bamr87/zer0-mistakes/commit/7143d9cd137408ad82981c9f9835204bab3a3db5))
1305
- * **theme:** consumer-adoption hardening — mobile overlay fixes, zero-data resilience, extension hooks ([#338](https://github.com/bamr87/zer0-mistakes/issues/338)) ([84bcce1](https://github.com/bamr87/zer0-mistakes/commit/84bcce15e66b0bc312f513cef4a9dd11f972a049))
1306
-
1307
-
1308
- ### Bug Fixes
1309
-
1310
- * **books:** pin abc-demo permalink so its cover lands at /books/abc-demo/ ([#345](https://github.com/bamr87/zer0-mistakes/issues/345)) ([943f463](https://github.com/bamr87/zer0-mistakes/commit/943f463c73081e7cc3d914c28a2ec2eea3d1b5ae))
1311
- * **ci:** give the Dependabot label job an explicit GH_REPO ([#370](https://github.com/bamr87/zer0-mistakes/issues/370)) ([808b20e](https://github.com/bamr87/zer0-mistakes/commit/808b20ebbdcd5b0bc139b90947297cec6d226787))
1312
- * **ci:** prefer CLAUDE_CODE_OAUTH_TOKEN over ANTHROPIC_API_KEY in all AI workflows ([#334](https://github.com/bamr87/zer0-mistakes/issues/334)) ([043848e](https://github.com/bamr87/zer0-mistakes/commit/043848eb84222240efc121e8112ddc12ab959a64))
1313
- * **ci:** unbreak the production Pages build, frozen since 2026-08-05 ([#356](https://github.com/bamr87/zer0-mistakes/issues/356)) ([05465cf](https://github.com/bamr87/zer0-mistakes/commit/05465cfd5480a6bdfb0d164d56a892f2825ec1cf))
1314
- * **i18n:** exclude admin config page from translation to unbreak Pages deploy ([#349](https://github.com/bamr87/zer0-mistakes/issues/349)) ([3f4890c](https://github.com/bamr87/zer0-mistakes/commit/3f4890cee3dd59b7f1cbc6ec235a863017b075fb))
1315
- * **i18n:** normalise generated prose so translation runs keep CI green ([#342](https://github.com/bamr87/zer0-mistakes/issues/342)) ([70a7234](https://github.com/bamr87/zer0-mistakes/commit/70a723425b8cc6a77e1b845b212a8a7d127a7779))
1316
- * **navigation:** mark only the current page active in the sidebar nav-tree ([#331](https://github.com/bamr87/zer0-mistakes/issues/331)) ([3f1b723](https://github.com/bamr87/zer0-mistakes/commit/3f1b723169258993d4530dd5817740e62734ccd4))
1317
- * **sass:** restore mobile navigation in the example, collapsed to a 60px sliver ([#363](https://github.com/bamr87/zer0-mistakes/issues/363)) ([8c7d87a](https://github.com/bamr87/zer0-mistakes/commit/8c7d87af4fd476f06c16ce58ca4d4cd0e2c4169d))
1318
-
1319
- ## [Unreleased]
1320
-
1321
- ### Fixed
1322
-
1323
1410
  - **The skip link never moved keyboard focus (#278)** — `_layouts/root.html`
1324
1411
  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.
1325
1412
  - **Seven theme dialogs labelled themselves with a bare `<h5>`** — the search
@@ -1337,40 +1424,6 @@ widget showed `An error occurred: giscus is not installed on this repository` in
1337
1424
  - **Docs claimed the widget "won't load on localhost"** — it does. Giscus keys
1338
1425
  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.
1339
1426
 
1340
- ### Added
1341
-
1342
- - **`giscus-discussions doctor`** — a new subcommand that verifies the entire
1343
- 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.
1344
- - **Giscus health check in CI** — `.github/workflows/giscus-digest.yml` (already
1345
- 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.
1346
- - **Troubleshooting: "The giscus app is not installed"** — a new section in the
1347
- 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.
1348
- - **Obsidian graph frontend revamp** — the graph views now mirror the Obsidian
1349
- desktop experience. The full graph page gains a floating **Graph settings**
1350
- card (Filters / Display / Forces): search with `tag:`/`path:` operators,
1351
- per-collection toggles generated from the wiki-index (count badge + color
1352
- swatch, doubling as the legend), show-orphans and show-unresolved switches,
1353
- node-size / link-thickness / label-fade sliders, repel-force and
1354
- link-distance sliders with animated re-layout, plus a zoom / fit / fullscreen
1355
- button stack, an Obsidian-style hover preview card, live dark-mode restyling,
1356
- and localStorage persistence. The local graph panel gains a depth selector
1357
- (1–3) and outgoing/incoming direction switches that re-render the subgraph in
1358
- place. Same vendored cytoscape.js, unchanged `wiki-index.json` contract
1359
- (evidence:
1360
- [`test/visual/evidence/obsidian-graph-revamp/`](test/visual/evidence/obsidian-graph-revamp/README.md);
1361
- regression: `test/visual/features/obsidian-graph.spec.js`).
1362
-
1363
- - **Theme-canary reporting contract** (`docs/systems/theme-canary-contract.md`) —
1364
- the canonical upstream bug-intake protocol for downstream theme-scout agents
1365
- (it-journey, lifehacker.dev): required `theme-canary` + `from-<consumer>`
1366
- labels, a deterministic 12-hex dedupe key + hidden body marker, the mandated
1367
- open-AND-closed pre-filing search, a 5-issues-per-consumer-per-run cap, and a
1368
- required evidence format that records the theme version/commit observed (so
1369
- fixed-in-release triage is mechanical). Ships with the matching
1370
- `theme-canary.yml` issue form.
1371
-
1372
- ### Fixed
1373
-
1374
1427
  - **Mobile navigation opened as an unusable 60px sliver in the Swerve of Shore
1375
1428
  example.** Its skin set `backdrop-filter` on `.navbar` for a frosted sticky
1376
1429
  header, which — per the CSS spec, alongside `transform`/`filter`/`perspective`
@@ -1407,158 +1460,48 @@ feature docs with captured screenshots of both the broken widget and the giscus.
1407
1460
  the page that took the site down. Collection-only layouts are now dropped
1408
1461
  from generated translations so the `path: fr` default (`layout: default`)
1409
1462
  applies, and the affected page is regenerated.
1410
- 3. `CHANGELOG.md` is a published page (`/CHANGELOG/`), so its prose is parsed
1411
- as Liquid. Two entries quoted Liquid tags without a `raw` guard; one of
1412
- them genuinely executed `include_relative` on `_config.yml`, which in turn
1413
- tripped over a literal `mermaid` tag written inside a config comment.
1414
- Both entries are now guarded and the config comment no longer spells the
1415
- tag out.
1416
-
1417
- - **Production builds are now verified before merge.** `.github/workflows/pages.yml`
1418
- builds (never deploys) on pull requests. No other job ran `jekyll build`
1419
- against the production config, which is why all three faults above reached
1420
- `main` with CI green.
1421
-
1422
- - **GitHub Pages production deploy no longer fails on the translated config
1423
- page.** The admin config utility `pages/_about/settings/config.md` embeds a
1424
- raw `_config.yml` dump via {% raw %}`{% include_relative _config.yml %}`{% endraw %}, but the
1425
- translation pipeline copied the page into `fr/about/settings/` without its
1426
- sibling data file, so the production `github-pages` build died with
1427
- `Could not locate the included file '_config.yml'`. The page is now excluded
1428
- from translation (`translation.exclude` in `_config.yml`) — it has no
1429
- prose to translate anyway — and the stale `fr/about/settings/config.md` plus
1430
- its `_data/i18n/manifest.yml` entry are removed.
1431
- - **Translation runs no longer break the `oneline` check.** `scripts/translate.rb`
1432
- writes whatever the provider returns, and a provider is free to soft-wrap a
1433
- translated paragraph across several lines — no prompt instruction reliably
1434
- prevents it. Wrapped prose then landed in `fr/**`, turned
1435
- `markdown-oneline.yml` red on the translation PR, and stayed red on `main`
1436
- after it merged. Two of the currently committed translations were in exactly
1437
- that state.
1438
- - `translate.rb` now normalises every page it writes through
1439
- `tools/unwrap-prose.py` — the same tool CI runs, so the rule has one source
1440
- of truth instead of a reimplementation that could drift. Best-effort: a
1441
- missing `python3` warns rather than failing an otherwise good run.
1442
- - `.github/workflows/translate.yml` runs the same tool as a guaranteed
1443
- backstop immediately before committing, then `--check`s its own work so a
1444
- silent failure cannot just move the breakage to the PR's CI run.
1445
- - The tool is resolved relative to `translate.rb` rather than `--root`;
1446
- `--root` points at the content tree being translated, so the previous
1447
- lookup missed the tool whenever the two differed.
1448
- - Unwrapped the two affected files, so `oneline` passes on `main` again.
1449
- - Regression test in `test/test_i18n.sh` backed by a new `stub-wrap`
1450
- provider that soft-wraps at 40 columns. Asserting "output is unwrapped"
1451
- against the plain stub would have passed whether or not normalisation ran.
1452
-
1453
- ### Added
1454
-
1455
- - **`examples/` — self-contained demo sites built on the theme.** New top-level
1456
- directory (excluded from the theme's own site build and from the gem) for
1457
- complete Jekyll sites that show the theme configured for a particular kind of
1458
- site. Each has its own `_config.yml`, `Gemfile`, and content, and builds
1459
- against the local theme sources via `_config_dev.yml`.
1460
- - `examples/swerve-of-shore/` — **Swerve of Shore**, a serialized literary
1461
- reading blog rebuilt from
1462
- [swerveofshore.com](https://www.swerveofshore.com/) with the site owner's
1463
- permission. 52 pages, 0 broken internal links. Exercises a `series` output
1464
- collection with nested per-section paths, a data-driven taxonomy
1465
- (`_data/episodes.yml`) that generates the sidebar rail, the episode
1466
- schedule with live entry counts, and a section page for every node — so a
1467
- nav target resolves even where nothing is written yet. Also covers article
1468
- hero images (`show_hero`), entries ordered by number rather than date, the
1469
- shared `post-card` component in a single-column feed, and a full editorial
1470
- light-mode restyle done entirely through `assets/css/user-overrides.css` —
1471
- no theme files touched. The site's identity, taxonomy, entry titles/dates,
1472
- and featured images are reproduced, along with a short quoted excerpt per
1473
- entry (the opening the original publishes in its own RSS feed), attributed
1474
- and linked back. The essays themselves are not reproduced.
1475
- - `examples/README.md` — how to run one and the conventions for adding
1476
- another.
1477
- - **Theme propagation system — releases now reach the sites built on them.**
1478
- Releasing ended at RubyGems; nothing told the five downstream consumers a new
1479
- version existed, so all three pinned sites were still on v1.26.0 when v1.27.0
1480
- shipped and the two unpinned ones had been silently tracking `main`.
1481
- - `_data/consumers.yml` — registry of every consumer, its consumption mode
1482
- (`remote_theme_pinned` / `remote_theme_floating` / `gem` / `path`), and every
1483
- file that holds a version pin.
1484
- - `scripts/propagate.rb` — reports pin drift across the registry
1485
- (`--format text|json|github`, `--strict`) or dispatches a `theme-release`
1486
- event to each consumer (`--dispatch`).
1487
- - `.github/workflows/propagate-theme.yml` — dispatches on release publish;
1488
- weekly drift report. Never pushes to a consumer: each one opens its own PR so
1489
- its own CI gates the bump.
1490
- - `templates/consumer/` — the consumer's half of the contract:
1491
- `bump-theme-pins.sh` (rewrites `remote_theme`, `theme_repo`, and Gemfile gem
1492
- constraints in one pass; `--check`, `--dry-run`, `--latest`, `--pin`),
1493
- `theme-bump.yml` (dispatch + weekly schedule + manual), and a
1494
- `.theme-overrides.yml` starter. The schedule matters most for gem-mode
1495
- consumers, which have no `remote_theme` and so pick up nothing automatically.
1496
- - `docs/systems/theme-propagation.md` — how the whole flow fits together.
1497
- - **`./scripts/bin/manifest --check`** — verifies the committed theme manifest
1498
- matches the working tree, ignoring `generated_at`. `--dry-run` now emits only
1499
- YAML on stdout (progress logs moved to stderr) so its output can be diffed
1500
- directly.
1501
- - **Consumer extension hooks (`_includes/custom/`).** Four empty stubs the
1502
- chrome now includes at fixed points — `custom/head.html` (end of `<head>`),
1503
- `custom/body-start.html` (right after `<body>`, e.g. GTM noscript),
1504
- `custom/footer.html` (after the theme footer), `custom/body-end.html` (last
1505
- thing before `</body>`). A consumer site shadows just the stub to inject
1506
- JSON-LD, fonts, verification tags, or scripts — no more forking
1507
- `core/head.html`/`root.html` and drifting from the theme. Documented in
1508
- `_includes/README.md`.
1509
- - **`components/card-grid.html` + `components/data-card.html`.** A generic,
1510
- data-driven card grid (title/url/icon/badge/meta-pills/buttons schema) for
1511
- org hub dashboards, fleet registries, service grids, and link boards —
1512
- the markup consumer sites have been hand-rolling per page. Includes an
1513
- optional empty-state message.
1514
- - **`_layouts/404.html` — the 404 page as a theme layout.** The full 404
1515
- experience (including the unconfigured-site setup guide) now lives in the
1516
- theme; a consumer's `/404.html` shrinks from a 224-line vendored copy to a
1517
- 4-line front-matter stub (`layout: "404"`). Setup mode is branded with
1518
- `site.title` instead of a hardcoded theme welcome, and the stub page's body
1519
- renders below the standard content for site-specific additions.
1520
- - **`color_mode_lock` config.** Pin the site to one color mode: visitor
1521
- preference and OS scheme are ignored, the light/dark/auto switcher is
1522
- removed, and `core/color-mode-init.html` + `halfmoon.js` enforce the lock
1523
- pre-paint. Replaces consumer JS workarounds that re-asserted a mode on every
1524
- `matchMedia` change.
1525
- - **PostHog privacy options** (`posthog.privacy.*`): `respect_gpc` (honor the
1526
- Global Privacy Control signal alongside DNT, default on), `require_consent`
1527
- (start opted out with memory-only persistence until the visitor accepts the
1528
- cookie-consent "analytics" category), and `geoip_disable` (no IP→location
1529
- resolution). Upstreams the deltas consumer sites were maintaining in a
1530
- full fork of `analytics/posthog.html`.
1531
- - **`news:` config surface for the news/section layouts.** `category_base`
1532
- (e.g. `/categories/` instead of the hardcoded `/news/`), `index_url`,
1533
- `hero: false`, `archives_url`, and a `newsletter` block (endpoint-or-mailto,
1534
- or hidden entirely — the old markup shipped a form that submitted nowhere).
1535
-
1536
- ### Changed
1463
+ 3. `CHANGELOG.md` is a published page (`/CHANGELOG/`), so its prose is parsed
1464
+ as Liquid. Two entries quoted Liquid tags without a `raw` guard; one of
1465
+ them genuinely executed `include_relative` on `_config.yml`, which in turn
1466
+ tripped over a literal `mermaid` tag written inside a config comment.
1467
+ Both entries are now guarded and the config comment no longer spells the
1468
+ tag out.
1537
1469
 
1538
- - **The news/section layouts work with zero `_data`.** Section navigation
1539
- falls back from `_data/navigation/posts.yml` to `site.categories`; the hero
1540
- falls back to the newest post when nothing is marked `breaking`/`featured`;
1541
- tags links render only when a tags page exists (honoring `site.tags_page`);
1542
- archive month links render only when an archives page exists. A data-less
1543
- consumer now gets a fully working magazine homepage instead of empty chrome
1544
- and 404 links.
1545
- - **Zero-`_data` resilience across components.** The skin quick-select and
1546
- theme customizer fall back to the compiled skin registry when
1547
- `_data/theme_skins.yml` is absent; the page-feedback widget falls back to a
1548
- generic four-type taxonomy when `_data/feedback_types.yml` is absent; the
1549
- landing get-started band (theme install cards) renders only when the site
1550
- ships a `landing.get_started` block — a data-less consumer no longer
1551
- advertises the theme's own install methods.
1552
- - **`home` layout accepts `hide_intro` as an alias of `hide_title`**, matching
1553
- the flag other layouts use for "suppress the generated page header".
1554
- - **`halfmoon.js` honors `color_mode_default`.** The switcher no longer
1555
- overrides the configured default with the OS preference on first visit
1556
- (previously the page could flip modes right after load).
1557
- - The default skin fallback is now `air` everywhere (`root.html`,
1558
- controls bar, customizer) — `dark` was never a compiled skin, so the old
1559
- fallback selected a skin that doesn't exist.
1470
+ - **Production builds are now verified before merge.** `.github/workflows/pages.yml`
1471
+ builds (never deploys) on pull requests. No other job ran `jekyll build`
1472
+ against the production config, which is why all three faults above reached
1473
+ `main` with CI green.
1560
1474
 
1561
- ### Fixed
1475
+ - **GitHub Pages production deploy no longer fails on the translated config
1476
+ page.** The admin config utility `pages/_about/settings/config.md` embeds a
1477
+ raw `_config.yml` dump via {% raw %}`{% include_relative _config.yml %}`{% endraw %}, but the
1478
+ translation pipeline copied the page into `fr/about/settings/` without its
1479
+ sibling data file, so the production `github-pages` build died with
1480
+ `Could not locate the included file '_config.yml'`. The page is now excluded
1481
+ from translation (`translation.exclude` in `_config.yml`) — it has no
1482
+ prose to translate anyway — and the stale `fr/about/settings/config.md` plus
1483
+ its `_data/i18n/manifest.yml` entry are removed.
1484
+ - **Translation runs no longer break the `oneline` check.** `scripts/translate.rb`
1485
+ writes whatever the provider returns, and a provider is free to soft-wrap a
1486
+ translated paragraph across several lines — no prompt instruction reliably
1487
+ prevents it. Wrapped prose then landed in `fr/**`, turned
1488
+ `markdown-oneline.yml` red on the translation PR, and stayed red on `main`
1489
+ after it merged. Two of the currently committed translations were in exactly
1490
+ that state.
1491
+ - `translate.rb` now normalises every page it writes through
1492
+ `tools/unwrap-prose.py` — the same tool CI runs, so the rule has one source
1493
+ of truth instead of a reimplementation that could drift. Best-effort: a
1494
+ missing `python3` warns rather than failing an otherwise good run.
1495
+ - `.github/workflows/translate.yml` runs the same tool as a guaranteed
1496
+ backstop immediately before committing, then `--check`s its own work so a
1497
+ silent failure cannot just move the breakage to the PR's CI run.
1498
+ - The tool is resolved relative to `translate.rb` rather than `--root`;
1499
+ `--root` points at the content tree being translated, so the previous
1500
+ lookup missed the tool whenever the two differed.
1501
+ - Unwrapped the two affected files, so `oneline` passes on `main` again.
1502
+ - Regression test in `test/test_i18n.sh` backed by a new `stub-wrap`
1503
+ provider that soft-wraps at 40 columns. Asserting "output is unwrapped"
1504
+ against the plain stub would have passed whether or not normalisation ran.
1562
1505
 
1563
1506
  - **`_data/theme-manifest.yml` was 13 minors stale.** It sat at `1.14.0` while
1564
1507
  the theme shipped to `1.27.0`, so every SHA-256 in it was wrong and
@@ -1593,71 +1536,6 @@ feature docs with captured screenshots of both the broken widget and the giscus.
1593
1536
  `:root` (set by `auto-hide-nav.js`), so stylesheets can align overlays with
1594
1537
  the real header instead of hard-coding its pixel height.
1595
1538
 
1596
- ### Added
1597
-
1598
- - **ABC board books (`book-abc` layout) — the "ABC & Language" series.** A new
1599
- immersive, single-scroll toddler alphabet layout (`_layouts/book-abc.html`)
1600
- driven entirely by an `alphabet:` front-matter list — one big letter, one
1601
- word, one picture per card, with A–Z quick-jump anchors and a cover hero.
1602
- The big letter is HTML typography (never baked into the art), and a card
1603
- whose plate is still `planned` shows a tinted "illustration coming soon"
1604
- placeholder so a book reads end-to-end before any art renders. New
1605
- `_includes/components/abc-letter.html` renders each card; `_sass/components/_book.scss`
1606
- gains `.abc-board` / `.abc-letter-*` styles plus a `.abc-style--<id>` skin per
1607
- shared art-style id. `components/bookshelf.html` now lists both `book` and
1608
- `book-abc` books, grouped into series shelves from `_data/series.yml`;
1609
- `book-card.html` shows a letter count for ABC books. Ships a demo book
1610
- (`pages/_books/abc-demo/`) + default series metadata (`_data/series.yml`).
1611
- Regression test [`test/visual/features/book-abc.spec.js`](test/visual/features/book-abc.spec.js)
1612
- and before/after evidence in
1613
- [`test/visual/evidence/book-abc/`](test/visual/evidence/book-abc/). Docs:
1614
- [`docs/features/abc-books.md`](docs/features/abc-books.md).
1615
- - **`nav: pages` sidebar mode — auto-build the left tree from page URLs.** A new
1616
- sidebar mode (`_includes/navigation/sidebar-pagetree.html`) derives a
1617
- collapsible section tree purely from page permalinks under a `sidebar.base`
1618
- prefix — no curated `_data/navigation/*.yml` file to write or keep in sync.
1619
- Enable per page / collection / site with `sidebar: {nav: pages, base: /docs/,
1620
- order_by: nav_order, title: …}`. It covers plain pages (`site.html_pages`) and
1621
- collection documents (`site.documents`) alike, groups them by first path
1622
- segment, humanizes section labels from the URL (never leaking a generic
1623
- "Index" title), sorts each section by a numeric `order_by` (natural order),
1624
- supports per-page `sidebar_label` / `sidebar_exclude`, and marks only the
1625
- current page active with its section expanded server-side — pure Liquid, so
1626
- it is GitHub Pages / `remote_theme` safe. Documented at
1627
- `/docs/features/sidebar-page-tree/`.
1628
- - **Claude Code OAuth in the AI installer** — the spec-driven installer
1629
- (`scripts/bin/install`) is now multi-provider. `scripts/install/ai/client.sh`
1630
- resolves a provider via `ZER0_AI_PROVIDER` (default `auto`), preferring the
1631
- logged-in `claude` CLI (**Claude Code OAuth** — zero key handling), then the
1632
- Anthropic Messages API (`CLAUDE_CODE_OAUTH_TOKEN` OAuth bearer or
1633
- `ANTHROPIC_API_KEY`), then OpenAI. `install doctor` reports the active
1634
- provider; `--ai-provider` / `--ai-model` / `ZER0_AI_MODEL` tune it; `--no-ai`
1635
- / `ZER0_NO_AI=1` disable it. User context is sanitized before every call.
1636
- - **Config-file layer for the installer** — new `scripts/install/config.sh`
1637
- discovers and merges `~/.config/zer0/install.yml`, `<target>/zer0.install.yml`,
1638
- `<target>/.zer0/config.yml`, and `--config FILE` (precedence:
1639
- defaults < profile < config < env < flags). Recognises site / github / theme /
1640
- deploy / agents / tasks / ai keys; API keys stay environment-only.
1641
- - **`install suggest`** subcommand — recommend a profile + deploy target
1642
- (AI-assisted with rule-based fallback) — plus `--yes` as an alias for
1643
- `--auto-accept`.
1644
- - Installer regression matrix (`test/test_installer.sh`) now covers AI
1645
- provider resolution, text extraction across provider shapes, the config-file
1646
- layer + precedence, and the `doctor` AI check (all offline).
1647
-
1648
- ### Changed
1649
-
1650
- - Installer spec default `ai.provider` is now `auto` (was `openai`); the AI
1651
- wizard records the provider that actually served the run.
1652
- - **CI workflows now honor `CLAUDE_CODE_OAUTH_TOKEN`** — `ui-audit.yml`,
1653
- `ai-content-review.yml`, and `ci-self-repair.yml` previously gated their
1654
- Claude Code agent tier on `ANTHROPIC_API_KEY` alone, silently ignoring a
1655
- configured OAuth token. They now follow the same house convention as
1656
- `claude.yml` / `issue-autopilot.yml`: `CLAUDE_CODE_OAUTH_TOKEN` preferred,
1657
- `ANTHROPIC_API_KEY` used only when it's absent.
1658
-
1659
- ### Fixed
1660
-
1661
1539
  - **Left-sidebar nav highlighted every item as "active".** `nav-tree.html`
1662
1540
  used {% raw %}`{% assign is_active = page.url == item.url %}`{% endraw %}, but Liquid `assign`
1663
1541
  does not evaluate `==` — it stored `page.url` (always truthy), so every link
@@ -1688,6 +1566,134 @@ feature docs with captured screenshots of both the broken widget and the giscus.
1688
1566
  `_config.yml` template add `jekyll-include-cache` (required by the theme's
1689
1567
  layouts).
1690
1568
 
1569
+ ## [1.31.0](https://github.com/bamr87/zer0-mistakes/compare/v1.30.0...v1.31.0) (2026-10-07)
1570
+
1571
+
1572
+ ### Features
1573
+
1574
+ * **navigation:** two clean navbar label tiers, merged chevron, centred grid ([#495](https://github.com/bamr87/zer0-mistakes/issues/495)) ([9ebb29a](https://github.com/bamr87/zer0-mistakes/commit/9ebb29a4662fbc4c62d3fdd8df9c282bdd181204))
1575
+ * **theme:** four consumer extension points for notes/knowledge-base builds ([#492](https://github.com/bamr87/zer0-mistakes/issues/492)) ([1e2b228](https://github.com/bamr87/zer0-mistakes/commit/1e2b2283303012b28b9e81209fb66d0344aa0597))
1576
+
1577
+
1578
+ ### Bug Fixes
1579
+
1580
+ * **article:** eager-load the hero LCP image and reserve its box ([#498](https://github.com/bamr87/zer0-mistakes/issues/498)) ([529b84b](https://github.com/bamr87/zer0-mistakes/commit/529b84bd0f5ea8e79b7711187b52b2bb80c5b134)), closes [#485](https://github.com/bamr87/zer0-mistakes/issues/485)
1581
+ * **ci:** keep a LiveReload push from crashing the generic evidence generator ([#500](https://github.com/bamr87/zer0-mistakes/issues/500)) ([eb1b5ae](https://github.com/bamr87/zer0-mistakes/commit/eb1b5ae201ed0829e0c3fac05e629e55a6ea1040))
1582
+ * **ci:** Playwright 1.58 attachment names, docs front matter, test race ([#497](https://github.com/bamr87/zer0-mistakes/issues/497)) ([f1bdc7e](https://github.com/bamr87/zer0-mistakes/commit/f1bdc7ef22091f0506617b11a95e357c439cfa94))
1583
+ * **ci:** stop auto-merge guard failing on Dependabot Actions bumps ([#503](https://github.com/bamr87/zer0-mistakes/issues/503)) ([a998b2d](https://github.com/bamr87/zer0-mistakes/commit/a998b2dd9cf96bb1fd09ac3a95644908713afc37))
1584
+ * **features:** escape `<en-url>` in the translation card so the Features page renders ([#505](https://github.com/bamr87/zer0-mistakes/issues/505)) ([8e4345f](https://github.com/bamr87/zer0-mistakes/commit/8e4345f28dfc675b5efe95ba66ec72af613cbcf5))
1585
+ * **includes:** join preview paths without a leading slash under assets_prefix ([#509](https://github.com/bamr87/zer0-mistakes/issues/509)) ([406250c](https://github.com/bamr87/zer0-mistakes/commit/406250c2b5b94469cae9a0b6f93e8782cb8509f9))
1586
+ * **includes:** stop the draft chip shifting the Site Builder on first save ([#489](https://github.com/bamr87/zer0-mistakes/issues/489)) ([d472aa2](https://github.com/bamr87/zer0-mistakes/commit/d472aa267e164212cf8b844e701cfcdad08e4d8d))
1587
+ * **layouts:** one main landmark per page on post, notebook and note ([#487](https://github.com/bamr87/zer0-mistakes/issues/487)) ([78b0910](https://github.com/bamr87/zer0-mistakes/commit/78b0910fb2258591eab9ba13efb5ac037eee80c5))
1588
+ * **navigation:** stop Liquid whitespace-trim gluing navbar attributes ([#477](https://github.com/bamr87/zer0-mistakes/issues/477)) ([4349ea1](https://github.com/bamr87/zer0-mistakes/commit/4349ea17d690e5e6b8585f1d3a33b3ea4d3bf4fe))
1589
+ * **scripts:** default xAI preview renderer to grok-imagine-image-2.0 ([#499](https://github.com/bamr87/zer0-mistakes/issues/499)) ([95cebdb](https://github.com/bamr87/zer0-mistakes/commit/95cebdb535799544f05ed5929e528a074448ef63)), closes [#474](https://github.com/bamr87/zer0-mistakes/issues/474)
1590
+ * **tests:** make the Liquid nested-tag and gem-content checks able to fail ([#478](https://github.com/bamr87/zer0-mistakes/issues/478)) ([8510d08](https://github.com/bamr87/zer0-mistakes/commit/8510d0824f79823f66f765f12db9be9b67fff3ae))
1591
+ * **tokens:** let the active skin win over site theme_color for skin-owned tokens ([#502](https://github.com/bamr87/zer0-mistakes/issues/502)) ([af468e0](https://github.com/bamr87/zer0-mistakes/commit/af468e072543a448e7be80230f3d3a246b5e758e))
1592
+ * **ui:** make the cookie consent banner a compact bottom bar ([#510](https://github.com/bamr87/zer0-mistakes/issues/510)) ([2d7d0db](https://github.com/bamr87/zer0-mistakes/commit/2d7d0db7f9f6b1433b63d8be76103cce2fd7fb05))
1593
+
1594
+
1595
+ ### Performance Improvements
1596
+
1597
+ * **js:** lazy-load Mermaid and cytoscape, defer PostHog, stop nav reflows ([#508](https://github.com/bamr87/zer0-mistakes/issues/508)) ([702789c](https://github.com/bamr87/zer0-mistakes/commit/702789cecf93d8e9b9f7013168ca47f0294064eb))
1598
+ * **sitemap:** keep one page of the index in the DOM and build cards lazily ([#511](https://github.com/bamr87/zer0-mistakes/issues/511)) ([9e42fcb](https://github.com/bamr87/zer0-mistakes/commit/9e42fcbd9421a994fd22a347bbef95573ce340a9))
1599
+
1600
+ ## [1.30.0](https://github.com/bamr87/zer0-mistakes/compare/v1.29.0...v1.30.0) (2026-09-12)
1601
+
1602
+
1603
+ ### Features
1604
+
1605
+ * **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))
1606
+ * **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))
1607
+ * **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))
1608
+ * **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))
1609
+ * **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))
1610
+ * **includes:** themed Mermaid figures + toolbar ([#455](https://github.com/bamr87/zer0-mistakes/issues/455)) ([96776fa](https://github.com/bamr87/zer0-mistakes/commit/96776fa48bbe7b63058d1d246962019c745d7b93))
1611
+ * **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))
1612
+
1613
+
1614
+ ### Bug Fixes
1615
+
1616
+ * **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))
1617
+ * **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))
1618
+ * **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))
1619
+ * **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))
1620
+ * **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))
1621
+ * **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))
1622
+ * **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)
1623
+ * **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)
1624
+ * **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))
1625
+ * **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))
1626
+ * **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)
1627
+ * **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))
1628
+
1629
+
1630
+ ### Performance Improvements
1631
+
1632
+ * **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))
1633
+ * **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))
1634
+
1635
+ ## [1.29.0](https://github.com/bamr87/zer0-mistakes/compare/v1.28.0...v1.29.0) (2026-09-01)
1636
+
1637
+
1638
+ ### Features
1639
+
1640
+ * **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))
1641
+ * **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))
1642
+ * **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)
1643
+ * **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)
1644
+ * **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))
1645
+ * **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))
1646
+ * **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))
1647
+
1648
+
1649
+ ### Bug Fixes
1650
+
1651
+ * **a11y:** dialog titles use a bare &lt;h5&gt;, skipping heading levels ([#387](https://github.com/bamr87/zer0-mistakes/issues/387)) ([98fd6f9](https://github.com/bamr87/zer0-mistakes/commit/98fd6f9e63dfb75c58efc3b15a642fb3e4c4095b))
1652
+ * **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)
1653
+ * **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))
1654
+ * **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)
1655
+ * **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))
1656
+ * **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))
1657
+ * **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))
1658
+ * **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))
1659
+ * **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)
1660
+ * **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))
1661
+ * **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)
1662
+ * **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))
1663
+ * **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)
1664
+ * **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))
1665
+ * **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))
1666
+ * **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))
1667
+ * **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)
1668
+
1669
+ ## [1.28.0](https://github.com/bamr87/zer0-mistakes/compare/v1.27.0...v1.28.0) (2026-08-09)
1670
+
1671
+
1672
+ ### Features
1673
+
1674
+ * **books:** children's picture-book collection with immersive layouts and components ([#328](https://github.com/bamr87/zer0-mistakes/issues/328)) ([e059d1a](https://github.com/bamr87/zer0-mistakes/commit/e059d1afdc95073c5ce5bf9d1516b362979ad402))
1675
+ * **ci:** deploy the site via Actions so examples reach the live domain ([#353](https://github.com/bamr87/zer0-mistakes/issues/353)) ([959243c](https://github.com/bamr87/zer0-mistakes/commit/959243c0e8e504fe5d8dba44b89016b413bc88da))
1676
+ * **docs:** theme-canary reporting contract for downstream theme-scouts (FF-0019) ([#358](https://github.com/bamr87/zer0-mistakes/issues/358)) ([c1c1b30](https://github.com/bamr87/zer0-mistakes/commit/c1c1b309c5504b014602982fc96ec80c3f8f70de))
1677
+ * **examples:** add serialized reading-blog example site ([#339](https://github.com/bamr87/zer0-mistakes/issues/339)) ([aca0b53](https://github.com/bamr87/zer0-mistakes/commit/aca0b53b88067bda80fb299f9d5148ccb185d618))
1678
+ * **includes:** config-driven favicon, GTM gating, og:image fallback fix, sidebar nav aliases ([#327](https://github.com/bamr87/zer0-mistakes/issues/327)) ([fe8aacc](https://github.com/bamr87/zer0-mistakes/commit/fe8aacc234003f9f170fcb3b8b29c60ac18ed94b))
1679
+ * **install:** Claude Code OAuth + multi-provider AI + config-file layer ([#330](https://github.com/bamr87/zer0-mistakes/issues/330)) ([fde414c](https://github.com/bamr87/zer0-mistakes/commit/fde414cce5286f90de1ad02ca6ca40d634be5611))
1680
+ * **layouts:** book-abc board-book layout + ABC art-style skins ([#336](https://github.com/bamr87/zer0-mistakes/issues/336)) ([d88038e](https://github.com/bamr87/zer0-mistakes/commit/d88038e806e0d1a492e5d12b4e10e55fd240d692))
1681
+ * **navigation:** nav: pages sidebar mode — auto-build the tree from page URLs ([#333](https://github.com/bamr87/zer0-mistakes/issues/333)) ([c0c627d](https://github.com/bamr87/zer0-mistakes/commit/c0c627d5f50051e8f5eb4f0d6681244acaaa8d85))
1682
+ * **obsidian:** revamp graph frontend with Obsidian-style controls ([#366](https://github.com/bamr87/zer0-mistakes/issues/366)) ([7143d9c](https://github.com/bamr87/zer0-mistakes/commit/7143d9cd137408ad82981c9f9835204bab3a3db5))
1683
+ * **theme:** consumer-adoption hardening — mobile overlay fixes, zero-data resilience, extension hooks ([#338](https://github.com/bamr87/zer0-mistakes/issues/338)) ([84bcce1](https://github.com/bamr87/zer0-mistakes/commit/84bcce15e66b0bc312f513cef4a9dd11f972a049))
1684
+
1685
+
1686
+ ### Bug Fixes
1687
+
1688
+ * **books:** pin abc-demo permalink so its cover lands at /books/abc-demo/ ([#345](https://github.com/bamr87/zer0-mistakes/issues/345)) ([943f463](https://github.com/bamr87/zer0-mistakes/commit/943f463c73081e7cc3d914c28a2ec2eea3d1b5ae))
1689
+ * **ci:** give the Dependabot label job an explicit GH_REPO ([#370](https://github.com/bamr87/zer0-mistakes/issues/370)) ([808b20e](https://github.com/bamr87/zer0-mistakes/commit/808b20ebbdcd5b0bc139b90947297cec6d226787))
1690
+ * **ci:** prefer CLAUDE_CODE_OAUTH_TOKEN over ANTHROPIC_API_KEY in all AI workflows ([#334](https://github.com/bamr87/zer0-mistakes/issues/334)) ([043848e](https://github.com/bamr87/zer0-mistakes/commit/043848eb84222240efc121e8112ddc12ab959a64))
1691
+ * **ci:** unbreak the production Pages build, frozen since 2026-08-05 ([#356](https://github.com/bamr87/zer0-mistakes/issues/356)) ([05465cf](https://github.com/bamr87/zer0-mistakes/commit/05465cfd5480a6bdfb0d164d56a892f2825ec1cf))
1692
+ * **i18n:** exclude admin config page from translation to unbreak Pages deploy ([#349](https://github.com/bamr87/zer0-mistakes/issues/349)) ([3f4890c](https://github.com/bamr87/zer0-mistakes/commit/3f4890cee3dd59b7f1cbc6ec235a863017b075fb))
1693
+ * **i18n:** normalise generated prose so translation runs keep CI green ([#342](https://github.com/bamr87/zer0-mistakes/issues/342)) ([70a7234](https://github.com/bamr87/zer0-mistakes/commit/70a723425b8cc6a77e1b845b212a8a7d127a7779))
1694
+ * **navigation:** mark only the current page active in the sidebar nav-tree ([#331](https://github.com/bamr87/zer0-mistakes/issues/331)) ([3f1b723](https://github.com/bamr87/zer0-mistakes/commit/3f1b723169258993d4530dd5817740e62734ccd4))
1695
+ * **sass:** restore mobile navigation in the example, collapsed to a 60px sliver ([#363](https://github.com/bamr87/zer0-mistakes/issues/363)) ([8c7d87a](https://github.com/bamr87/zer0-mistakes/commit/8c7d87af4fd476f06c16ce58ca4d4cd0e2c4169d))
1696
+
1691
1697
  ## [1.27.0](https://github.com/bamr87/zer0-mistakes/compare/v1.26.0...v1.27.0) (2026-07-22)
1692
1698
 
1693
1699