# Changelog

## 0.11.0

- **A deck can declare its design size**, `"zerp": { "size": "WxH" }` in its own `package.json`, defaulting to `1920×1080` when unset. The declared size becomes the default `zerp check` viewport and the `zerp print` page size, so a deck built for a different screen no longer has to repeat `--size` on every command. **Breaking for `--json` consumers:** `viewport` gains a `source: "flag" | "deck" | "default"` field alongside the existing `width`/`height`/`defaulted`, and the CLI's "(default size)" marker and its `pass --size` hint now print only when the checked size is a true default (no `zerp.size` and no `--size` flag) — a deck-declared size is reported as "(deck size)" and gets neither. **Breaking for the library API:** `CheckOptions.sizeDefaulted` is removed — `checkPresentation` now derives the checked size and its `source` itself from the deck's own config plus whichever of `width`/`height` the caller passed, so there is nothing left for a caller to assert.
- **Slide frames render on a fixed-px stage that scales to fit the real window at runtime.** Every `[data-zerp-slide]` frame reads its size from `--zerp-stage-w`/`--zerp-stage-h` custom properties instead of `100vw`/`100vh`, so layout is computed once against the deck's design canvas and never reflows as the window resizes; the runtime instead applies a single `transform: scale(...)` to the stage, centered and letterboxed, refit on resize (an identity transform at exactly the design size, so nothing moves when a deck fills its own default). A deck that declares `zerp.size` gets a `<style data-zerp="size">` block emitting the two custom properties; a deck that declares nothing emits no extra style block at all, keeping the no-config output byte-identical. `container-type: size` on the stage sanctions `cqw`/`cqh` as the author-facing stage-relative units — `60cqh` always means 60% of the design height, at any window size, where `vh`/`vw` would read the real (scaled) viewport and be wrong. Presentation chrome (`.nav`, `.counter`, `.progress`, `.theme-toggle`, `.source-badge`) stays `position: fixed` outside the stage, unaffected by the transform. **Breaking** for anything that targeted the body's flat frame list — frames now sit inside a `<div data-zerp-stage>` wrapper — or that relied on `100vw`/`100vh` frames on screen.
- **Print keeps pagination viewport-driven regardless of the new stage transform.** The print block re-asserts `100vw`/`100vh` slide sizing and neutralizes the stage's `transform`, so one slide still fills exactly one printed page at any page size — size-agnostic printing is unchanged by scale-to-fit.
- **`zerp print [deck-dir] [--theme dark|light] [-o|--out out.pdf] [--timeout ms] [--browser-endpoint url]`** renders a deck directly to a PDF at its design size, skipping the browser print dialog and manual viewport setup; the library exports `printPresentation`. Default output is `<deck>/index.pdf`.
- **Headless sessions opt out of scale-to-fit by default.** Every `zerp check`/`zerp print` browser session now sets `window.__ZERP_NO_SCALE__` before the runtime initializes, so measurement and PDF rendering see the deck at its design size unscaled (the intended behavior for a fixed-viewport headless page), not scaled to whatever window size the headless browser happened to launch with.
- **`zerp verify` is gone; `zerp check` now does both jobs.** The two commands used to split the work by mechanism — `check` read static markup and CSS, `verify` drove a real browser — which meant a selector `check` could not parse (anything with `[`, `]`, `+`, `~` or `:`), or anything only resolvable inside `@media` or behind `!important`, was silently unchecked, and `casino` alone carried about ten such selectors. `check` is now itself browser-backed: one headless-Chrome pass per requested theme feeds computed styles, real font-fallback facts and frame/viewport geometry to the same rule set, so it sees exactly what a reader's browser would render, `@media` and `!important` included. `check` absorbs every `verify` flag — `--size` (default `1920×1080`, unchanged), `--safe-margin`, `--timeout`/`ZERP_VERIFY_TIMEOUT_MS`, `--browser-endpoint`/`ZERP_BROWSER_ENDPOINT` — alongside the `--theme` and `--strict` it already had. **Breaking:** `zerp verify` no longer exists; replace `zerp verify . --theme both --size 1920x1080` with `zerp check . --theme both --size 1920x1080`.
- **`zerp check` now always requires Chrome or Chromium.** There is no longer a browser-free mode, since the checks that used to run without one are now part of the same browser-backed pass as the ones that always needed one. Without a browser it errors and names the remedy — `zerp install-browser`, or (if `CHROME_BIN` is set but invalid) that path and the fix. `zerp build` is unaffected: its post-build check summary already tolerated a missing browser, and now prints an explicit one-line "check skipped: … run `zerp install-browser`" notice instead of a generic message, without failing the build.
- **Findings carry a `category`, and `--only` narrows a run to a subset.** Every finding is now one of `contrast`, `type-size`, `surface`, `glyph`, `svg-text`, `frame`, `overflow`, `safe-zone`, `console`, printed at the front of its report line (`[glyph] "…" — …`); `--only category,category` runs just those categories, and an unrecognized name is rejected with the full list rather than silently ignored. **Breaking for tooling that parsed report lines by fixed prefix or `--json` findings without a `category` field.**
- **`skippedSelectors` is gone, because nothing is skipped any more.** The old static engine rejected any selector containing `[`, `]`, `+`, `~` or `:` outright and reported the rejects in `skippedSelectors`; the browser-backed check has no such gap to report, and casino's ~10 previously-skipped selectors are now judged like any other. **Breaking for `--json` consumers reading `skippedSelectors`**, which no longer appears in the report shape.
- **Glyph findings now name an element and a fallback family, not a deck-level list of codepoints.** The mechanism changed along with the shape: instead of intersecting a static `cmap` against declared `unicode-range`s, the check asks the browser what actually painted each glyph (Chrome DevTools Protocol `CSS.getPlatformFontsForNode`) — since zerp inlines every font it ships as an `@font-face`, any font reported as not custom means the browser fell back. This reaches svg `<text>` and CSS-generated content the static engine could not judge for coverage at all, and reports one `[glyph]` warning per element rather than one warning per deck. `llms.txt` documents the granularity this trades away: attribution is per element (a deduplicated glyph-set count, not a per-codepoint sum) rather than per codepoint, so where a parent's fallback glyphs are a subset of a descendant's in the same family, the finding lands on the descendant rather than the parent — the slide is still flagged, just with a coarser pointer. Emoji stay exempt (a ZWJ sequence counts as several exempt characters against one drawn glyph, erring toward under- rather than over-reporting); `<svg>` subtrees stay excluded from element-level judging entirely, with `<foreignObject>` HTML as the one gap with no compensating warning; generated content is judged for glyph coverage only, never contrast or type-size. **Breaking for tooling that parsed the old deck-level glyph-coverage finding or its codepoint list.**
- **A deck can name a third font role, `display`, scoped to `h1` alone.** The two-role model made every heading and every headline share one voice, so a deck wanting a distinct display face for its title text had no way to say so. `"zerp": { "fonts": { "display": { "family": "Bebas Neue" } } }` in the deck's own `package.json` now bundles that family and points `--zerp-font-display` at it; a zero-specificity `:where(.slide h1)` rule reads the token, so a plain `h1 { font-family: var(--zerp-font-body) }` (or `h2 { font-family: var(--zerp-font-display) }`) in the deck's own CSS overrides it without `!important`. Left unset, `display` follows the _resolved_ `body` plan — family, package and weights together — so a deck naming only `body` gets that family on `h1` too, not the framework default. `mono` never follows `body`: the nav, code, tables and labels need real monospace metrics regardless of what body names. There are five stacks now, not four. **Breaking:** on 0.10, `display` was not a recognized key inside `zerp.fonts` at all — a deck's `package.json` that already had one there failed to build with an "unknown key" error. That deck now builds, with `display` read as the new role.
- **`zerp check`'s browser-backed default viewport is `1920×1080`**, was `1280×720` back when this was `zerp verify`'s default. It now matches the viewport the rest of `check` already assumes for `vh`/`vw` units, so every gate agrees on what "the screen" is without `--size`. CI that runs `check` without an explicit `--size` now checks a different screen: a deck that passed at the old default may newly overflow, or a deck that overflowed there may now pass.

## 0.10.0

- **A deck can choose its own typefaces.** Montserrat and Roboto Mono were written into fourteen `font-family` declarations, so a deck that wanted anything else had to fork the framework. `base-styles.css` now names four stacks once — `--zerp-font-body`, `-mono`, `-marker` and `-nav` — and every rule reads a token; a deck that says nothing gets byte-identical output. A deck names its families in an optional `zerp` key in its own `package.json` (`"zerp": { "fonts": { "body": { "family": "Inter" } } }`), which is the whole configuration surface: the manifest already exists, and the font packages must be dependencies of that same manifest to resolve. `fontsourcePackage` defaults to `@fontsource/` plus the slugified family and weights default to what zerp's styles ask for; resolution checks the deck's `node_modules` before zerp's, so a deck can also name a bundled family just to trim its weights. Weights a family does not ship are not emitted — browsers synthesize. Configuration mistakes are named rather than ignored: an unknown key, a family that disagrees with what the package declares, or a package the deck never installed (that one prints the install command).
- **Built decks carry only the font subsets their text needs.** Every deck used to ship the same 28 faces — five Montserrat weights and two Roboto Mono weights, each in latin, latin-ext, cyrillic and cyrillic-ext — from a hardcoded list of subset names, so an English deck carried the whole Cyrillic alphabet four times over and a Vietnamese or Greek deck could not have its subsets at all. A subset is now carried when the deck renders at least one character its `unicode-range` claims, which is exactly the condition under which a browser would fetch it; latin is the floor, since the framework's own chrome is latin. `examples/casino` drops from 714,533 to 251,750 bytes (64.8% smaller, 29 `@font-face` blocks down to 8) with no visible change. The gap this leaves is text a script derives at runtime, from a fetch or a computation: it exists in no source and cannot be selected for. String literals in slide scripts do count.
- **`zerp check` warns about characters the deck has no glyph for.** A built deck carries its fonts inside itself, which is what makes it one offline file and is also a hard edge: a character no bundled subset covers is drawn by whatever the viewing machine falls back to — a different shape per OS in the browser, re-resolved by the reader's machine on export. The check reads the cmap of every woff2 it would inline, intersects each with the `unicode-range` its `@font-face` declares, and warns about every slide character left over. Reading the file rather than trusting the range is the load-bearing part: ※ (U+203B) sits inside Montserrat's declared latin range and is absent from the subset. The finding is one deck-level warning listing the characters and their codepoints, printed under "deck" rather than a slide and escalated by `--strict`. Pictographs and regional indicators are exempt — no text font carries them, every platform draws them from its own color emoji font, and the warning would be true, universal and useless.
- **`zerp check` warns when slide text is sealed inside an `<svg>`.** A diagram's labels are the part a reader actually has to read, and inside `<svg>` they were the part nothing checked — worse than skipped, since SVG paints with a `fill` attribute and sizes with a `font-size` attribute, neither of which is CSS, so a label was scored as whatever it inherited. An 8px label painted `--zerp-faint` produced zero findings. `<text>` inside `<svg>` now warns, with the label's own words as the snippet; `aria-hidden` opts out, as it already does for every other check. The finding is structural rather than theme-dependent, so it is reported once against the first requested theme. `llms.txt` gains the authoring rule behind it: draw with SVG, label with HTML positioned over it.
- **A bundled one-glyph face makes → the same arrow everywhere.** zerp draws two markers with a rightwards arrow — the `ul` bullet and the `.flow` connector — and decks type one in prose constantly, but no bundled face covered it: Montserrat's subsets carry ↑, ↓ and • but not →, and Roboto Mono's carry no arrows at all. Every arrow zerp ever drew came from the viewing machine's fallback. `zerp-symbols.woff2` is 812 bytes holding exactly U+2192, subset from Noto Sans Symbols (OFL; renamed per the license, with provenance beside the file) and inlined as a data URL like every other face. Its `unicode-range: U+2192` is what makes it safe to name in a general stack. `.nav` is deliberately left alone: those buttons are ← and →, neither covered today, so both come from the same fallback and match.
- **The theme control is a two-state toggle.** The Light/Auto/Dark popover asked viewers to pick a policy when what they want mid-talk is the other scheme right now. Three states remain in the model — the deck default and a stored override — but only two are ever on screen. One press pins the literal opposite of what is displayed; the next removes the stored value and hands control back to the default. Nothing outside a press writes or clears storage, so an OS scheme change that makes a pin coincide with the system preference leaves the pin alone rather than silently demoting a deliberate choice. One button replaces the popover, carrying a sun and a moon SVG: the runtime sets `data-theme-target` to the scheme the next press produces and the CSS reveals the matching icon, so the button always names where it leads. Drawn rather than typed, because the sun/moon codepoints render as color emoji on some platforms and are in none of the bundled subsets. **Breaking for anything that targeted the chrome classes** `.theme-switch`, `.theme-trigger`, `.theme-options` or `[data-theme-choice]`, which are gone; `t` and the `zerp-theme` localStorage key are unchanged, and a stored `"system"` now reads as "no override".
- **Ordered-list rows lay out in block flow instead of flex.** An `ol` row was a flex container with the counter pseudo as a flex item, which reads the same on screen but is not the same document: a flex `::before` is a box with no text position of its own, and pipelines that re-flow a rendered deck into another format — PPTX export in particular — cannot place it. One flex `li` disqualifies a whole list from the path that inlines a marker into the text run beside it. The marker is now a plain in-flow inline box with the flex gap re-expressed as margin, and the counter, its mono accent typography, the row divider and padding are unchanged. The one visible difference: a wrapped line returns to the left edge under the marker rather than hanging beside it — the shape `ul` has always had.
- **`llms-pptx.txt` ships alongside `llms.txt`** (in `files` and the `exports` map, so it resolves as `@emirotin/zerp/llms-pptx.txt`). Some authoring patterns — inline pills, code/kbd chip borders, heavy font weights, SVG text, interactive step state, list-item media — render fine in the browser and degrade or vanish once a deck is exported to PPTX by a downstream pipeline. The new file collects those empirically verified restrictions, meant to be concatenated alongside `llms.txt` by the caller when a deck targets PPTX export; `llms.txt`'s printing/export section points at it.
- **`playwright-core` bumped to `^1.62.1`.** The pinned version decides which Chromium a `zerp install-browser` host downloads; 1.62.1 pins Chrome 151.0.7922.34, whose font fallback handles glyph gaps better — the path every character no bundled subset covers still takes.

## 0.9.2

- **The designer-facing style guide ships with the package** as `docs/style-system.pdf`, resolvable as `@emirotin/zerp/docs/style-system.pdf` for hosts that hand it to a designer or an agent. It is a printed artifact of the framework, not a hand-written document: the guide renders its examples with the built stylesheet and the bundled fonts, so a copy printed from an older tree describes a deck no one is authoring. The new `pnpm build:docs` reprints it with headless Chrome — resolving a browser exactly the way `zerp verify` does (`CHROME_BIN` → the managed Chromium → a system Chrome) — and `prepublishOnly` now runs it after the build, so the PDF in a published tarball is printed from that release's own stylesheet and stamped with its version and print date. Chrome is only needed to reprint the guide, never to install or use the package.

## 0.9.1

- **`zerp verify --timeout ms` (or `ZERP_VERIFY_TIMEOUT_MS`) sets the session budget.** The 20s bound covering browser launch, navigation, font activation and the probe was a constant, so a host slower than a developer laptop — a small or loaded container, or a deck carrying megabytes of imagery — had no way to raise it. The default is unchanged, an explicit option wins over the environment, and a malformed value throws rather than being ignored, since a silently discarded budget leaves the operator believing verification has room it does not have. The timeout message now names the budget that ran out and how to raise it. Worth restating for tooling that wraps verify: a session that overruns produces no report at all, which is a check that did not run, not a deck that passed.
- **`zerp verify --browser-endpoint url` (or `ZERP_BROWSER_ENDPOINT`) verifies in an already-running browser.** Every verification used to launch its own browser and close it again, so a host verifying repeatedly paid a cold Chromium start per check. `http(s)://` connects over CDP, `ws(s)://` over the playwright protocol; prefer CDP across independently versioned processes, since it is the browser's own protocol and needs no common playwright build on both sides. A borrowed browser belongs to whoever started it: verify creates a context, closes that context — explicitly and under its own bound, because a client that abandons a context leaks it into a process that outlives the run — and disconnects without closing the browser. No local browser is resolved in this mode, so a host can lend one to a container that has none. The host must keep its event loop responsive while verify runs; blocking it can stall the very browser it is lending out.

## 0.9.0

- **`zerp verify --safe-margin px` checks a print-safe inset.** When set, every top-level element of each authored slide must stay at least that many px inside all page edges; violations are reported as structured verify failures naming the element (id, class, or tag), the intruded edges, and the measured distances. Elements marked `data-zerp-bleed` — now a documented framework attribute for intentionally full-bleed content — are exempt, as are script/style tags and zero-size elements. Off by default (`0`), so existing verify behavior is unchanged; the report echoes the checked `safeMargin` the way it echoes the viewport, and `--json` carries the per-slide measurements in `safeZoneItems`. The mechanism formerly lived downstream as a separate probe (slide-bench's `_check_print_safe_zone` and its platform port) coupled to zerp's frame internals from outside; verify already steps and measures every slide, so the framework now owns the measurement and callers own only the threshold.

## 0.8.0

- **`zerp verify` failures are structured.** `VerifyReport.failures` entries are now `{ slide?, src?, message }` objects instead of pre-formatted strings — `--json` is for machines, and the slide number and source file were being composed into a label only for consumers to regex them back apart. Deck-level failures (browser errors, frame-count mismatches) carry only `message`. The human CLI output is unchanged: the new `formatVerifyFailure` renders the same `slide N (slides/foo.html): message` lines from the structured entries, making text a presentation of the data rather than the data itself. **Breaking for `--json` consumers** that treated `failures` as strings.

## 0.7.1

- **The package is resolvable from CommonJS and by path.** The `exports` map gains a `default` condition on the main entry (so `require("@emirotin/zerp")` works under Node ≥ 22.12 `require(esm)`) and explicit `./package.json`, `./llms.txt`, and `./dist/cli.js` subpath exports. Host applications that read the model-facing reference or spawn the CLI can now `require.resolve` those paths directly instead of hitting `ERR_PACKAGE_PATH_NOT_EXPORTED` and walking `node_modules` by hand.
- **`engines.node` declares the real floor, `>=22`, instead of the maintainer's exact toolchain version.** The CLI and the playwright-core verify transport run fine on Node 22 (the test suite passes under 22.22.2); development stays pinned through Volta and `packageManager`. The `engines.pnpm` pin is dropped for the same reason — it constrained consumers, not contributors.

## 0.7.0

- **`zerp verify` now drives the browser through `playwright-core`.** The hand-rolled DevTools-protocol client — Chrome over `--remote-debugging-pipe`, a bespoke CDP message pump, and device-metrics calibration — is replaced by `playwright-core`, a battle-tested browser driver. `playwright-core` bundles no browsers of its own, so the framework keeps its "bring your own browser" property: browsers stay external and optional, and the new dependency is pure JavaScript. The verify contract is unchanged — the same `VerifyReport` shape and failure strings, the same font-aware probe (it still measures after `document.fonts.ready` plus a paint settle and reports `fontsActive`), the same injected browser-error collector, the same exact-viewport measurement, temp-file handling, and 20s timeout.
- **New `zerp install-browser` command** downloads a managed Chromium for verification, for environments without a system Chrome. It hands off to `playwright-core`'s own installer (its package `bin`) and streams the download progress through, propagating the exit code.
- **`zerp verify` browser resolution order:** `CHROME_BIN` (used verbatim, so wrapper scripts keep working) → the `zerp install-browser` managed Chromium → a system Chrome/Chromium. When none is installed, verify explains how to get one.

## 0.6.1

- **`zerp verify` now measures slides with the real fonts.** The probe previously ran synchronously during page parse, before the inlined `@font-face` fonts activated, so every slide was measured with fallback metrics — font-dependent overflow (typically a few extra wrapped lines) passed verification and only showed up as clipped content when presenting or printing. The probe now waits for `document.fonts.ready` plus a paint settle before measuring, and reports `fontsActive` in the results so the wait is observable.
- `zerp verify` drives Chrome over a live DevTools-protocol session (`--remote-debugging-pipe`, still zero dependencies) instead of one-shot `--dump-dom`. The dump serialized the DOM around the load event, which is fundamentally incompatible with waiting on fonts (async results race the dump; `--timeout` never fires with `--user-data-dir`; `--virtual-time-budget` is ignored by new headless). The live session evaluates the probe after load with `awaitPromise`, sets the layout viewport exactly via device-metrics emulation (retiring the `--window-size` calibration pass), runs faster (~1.2s), and also works with Chrome-for-Testing builds, whose `--dump-dom` is broken.
- The temp `.zerp-verify-*.html` written into the deck directory during verification is now the plain built presentation (no injected probe markup); it is still removed afterwards (and `.gitignore`d against crashes).
- `zerp verify` states the checked viewport: the summary marks the default size explicitly (`1280×720 (default size)`), failing runs print how to re-verify at a deck's actual target screen (`--size WxH`), and `--json` reports carry a `viewport: { width, height, defaulted }` field. Overflow is only meaningful relative to a viewport — a larger-screen deck overflowing the default is a size mismatch, not a deck bug. `zerp check` stays size-independent (static analysis, no layout).

## 0.6.0

- Built decks are print-ready: a `@media print` block paginates one slide per page in deck order. Presentation chrome (nav, counter, progress, theme switch, source badge) is hidden, steps print in their final state (`data-step` shown, `data-until-step` gone), and backgrounds print (`print-color-adjust: exact`). In Chromium's print context each frame's inherited `100vh` resolves to one page, so pagination is size- and theme-agnostic — print at a page size equal to the presentation viewport (e.g. 1280×720 CSS px → 960×540 pt). The declarative step-hiding rules are scoped to `@media screen` so they do not fight the printed final state; `zerp check` is unaffected (it already skips at-rule contents).
- The base `.slide` aligns content with `justify-content: safe center` instead of `center`. Behavior change under overflow: an overflowing slide now spills below the frame and clips bottom-only, where before it clipped at both ends and hid the top of the overflow. Non-overflowing slides are unchanged, and engines without `safe` support fall back to plain `center`.
- `zerp check` gains `--json` (prints the report as JSON instead of the grouped text) and `--theme dark|light|both` (default both). `CheckReport` now carries a `themes` field naming the checked scope; `--strict` still composes with `--json`, and invalid themes are rejected with a clear error like `zerp verify`.
- `zerp verify` per-slide results carry `src` and `srcSlide` (the active slide's source file and in-file ordinal), included in `--json`. Human-readable per-slide failures are prefixed with the source file, e.g. `slide 3 (slides/10-intro.html): body height is 812px`, mirroring `zerp check`'s file attribution.

## 0.5.1

- Clarified the maintainer/downstream instruction boundary: `AGENTS.md` is framework-only, while the published `llms.txt` is a self-contained guide to the public deck interface.
- Downstream instructions now support local `zerp` execution through pnpm, npm, Yarn, or Bun; removed the obsolete local-path installation guidance and repository-maintainer commands.
- Package tarballs no longer include maintainer-only `AGENTS.md` or `CLAUDE.md`.

## 0.5.0

- Composition now discovers real slide elements with `htmlparser2` source offsets and wraps each one in a framework-owned `[data-zerp-slide]` frame. Authored `.slide` bytes and inline-script asset rewriting remain intact; script-looking markup is no longer annotated, and nested slide roots fail clearly.
- Slide visibility is controlled by the frame's `data-zerp-slide-active` attribute. The inner `.slide` is a full-size layout surface, so custom `display: grid`/`block`/`flex` roots cannot make inactive slides enter page flow or hide the active slide.
- Added `zerp verify [deck-dir] [--theme dark|light|both] [--size WxH] [--json]` for headless-browser frame, viewport, overflow, and browser-error checks.

## 0.4.0

- `zerp serve` live-reloads the browser on any change under `slides/` (sources and assets) — dependency-free SSE push with an mtime watcher. The page returns to the same slide via the URL hash and replays the steps of every slide you had stepped through; scripted slides re-run their `slide-next` sequences, so demos stay in sync. Built decks contain no reload client.
- The reload client also refreshes on SSE reconnect, so a restarted server — e.g. after a framework rebuild — is picked up automatically.
- `pnpm demo` is now a thin wrapper over the real `zerp serve`: it watches `src/`, rebuilds `dist/`, and respawns the server. One live-reload implementation instead of two, and the demo loop gets step preservation for free.
- The page `<title>` is derived from the first slide's highest-level heading (h1 before h2, regardless of document order); style- or script-only leading files are skipped. The `title` build option overrides; the deck folder name remains the fallback.
- Internal: the regex-based composition pipeline's quirks are pinned in tests and analyzed in `docs/composition-fidelity.md`; static string assembly moved from array joins to template literals (byte-identical output).

## 0.3.2

- Slide numbering is first-class: "slide N" means the 1-based deck position everywhere — runtime counter, URL hash, `zerp check`, and the new tooling below. File prefixes only order files.
- Source-tracing attributes on every composed slide div: `data-zerp-src` (source file, as before), plus new `data-zerp-src-slide` ("i/n" ordinal within the file) and `data-zerp-index` (deck position).
- New `zerp slides [deck-dir] [--json]` command and `listDeckSlides()` / `formatSlideList()` API: the position → source file / in-file ordinal / title mapping, for humans and agents.
- `zerp check` findings in multi-slide files point at the exact block: `slide 30 (slides/28-attention.html · 2/2 in file)`.
- Runtime: press `s` to toggle a source badge with the active slide's deck position and source file.
- Library: `composeSlidesHtml()` exported (annotated slides HTML without the page shell).

## 0.3.1

- Tables inside a `.row` are laid out by the row's gap — their auto-centering margins no longer swallow the row's free space and fling them apart.
- Decorative glyphs marked `aria-hidden="true"` are skipped by `zerp check` (documented; screen readers ignore them too). Recommended pattern for step markers: absolutely positioned, shown via the parent's `.revealed`, so they never skew centered content or shift layout.
- Casino specimen: the birthday slide no longer spoils its own vote (odds and live-test prompt are step reveals; the percentages are now the stat values); biased-wheel anomaly markers are paired ▲/▼ triangles, out of flow so the frequency column stays aligned, and the reveal no longer jiggles the slide.

## 0.3.0

- Utility classes now reliably override component defaults: component "soft defaults" (colors and sizes meant to be overridable) are declared via `:where()`, so `h3.accent`, `p.xl`, `.value.red`, `.year.xl` apply as written. Decks that carried previously-ignored utility classes will now render them — the intent taking effect. Inside `.card`/`.key-thought`/`.steps` cells an `h3` defaults to text color (card title), not muted.
- Declarative step reveals: `data-step="N"` / `data-until-step="N"` + optional `.swap`; the runtime keeps a per-slide step counter on ↓/↑, reserves space so stepping never reflows a centered slide, and still dispatches `slide-next`/`slide-prev` for scripted slides.
- New primitives validated across three real decks: `.sub` (detail line), `.narrow` (width-capped centered block), `.edge-<hue>` + `.edge-ok/.edge-warn/.edge-danger/.edge-accent` (border emphasis for surface components), `.concept` (icon + label definition header), `.slide.title` (title recipe with `.meta`), `.step-hint` (stepped-slide affordance line), `figure` entries inside `.img-row`, spaced `.key-thought` lines.
- `zerp check` understands `:where()` selectors (zero specificity contribution).
- Casino example rewritten on the new primitives: inline styles down ~2.7×, and five reveal scripts (Monty Hall, dice grid, expected value, frequency table, team answer) replaced with declarative markup the checker can see.
- Shot harness: Chrome is killed as soon as the PNG is stable — headless Chrome can hang on shutdown for minutes — and stale temp files from killed runs are swept.

## 0.2.0

Breaking — clean redesign of the styling layer; see MIGRATION.md.

- Design tokens generated from @evilmartians/harmony (OKLCH-designed, APCA-uniform palette); dark AND light themes in every build.
- Theme selection: `--theme dark|light|system` flag (default `system`), ◐ runtime switch + `t` key, persisted in localStorage.
- Richer defaults: styled tables, blockquotes, ordered lists, figures, code blocks; new components (.card, .cols-N, .stat/.stat-row, .compare, .flow, .steps, .pill) and bounded utilities.
- `zerp check`: built-in static APCA contrast + font-size checker covering both themes, including surface-blend detection; summary printed by build/serve; `--strict` promotes warnings.
- Bundled fonts: Montserrat + Roboto Mono (latin/cyrillic + ext) inlined as woff2 data URLs — built decks are single-file and fully offline, no Google Fonts requests.
- Removed: `.two-col`, `.big-number`, `.quote`, `.accent-<hue>` classes; hardcoded palette; always-dark default.

## 0.1.2

Initial public line: HTML/Markdown decks, lexicographic ordering, asset URL rewriting, default dark styles, navigation runtime, serve/build CLI.
