commit c38b78cbb36aeba32b6505cf28f68e1b3dee8eb7
parent 7b56e84e53f50611726927b02a6468776ab318c5
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 26 Sep 2026 00:32:23 -0400
plans: brand + themes — slice S3 as shipped (carry-overs, the Plex Sans l/font build defect, full gates), release 10's record, FACTS, STATE; the rollout section checked against the repo
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
| M | plans/FACTS.md | | | 123 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | plans/STATE.md | | | 80 | +++++++++++++++++++++++++++++++++++++++++-------------------------------------- |
| M | plans/brand-and-themes.md | | | 196 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- |
| A | plans/release-10.md | | | 196 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 545 insertions(+), 50 deletions(-)
diff --git a/plans/FACTS.md b/plans/FACTS.md
@@ -6453,3 +6453,126 @@ Line numbers are `plans/FACTS.md` lines at `e172749b`, before this record's in-p
- **The homepage build refuses symlinks that point outside the project**, so a worktree's
`homepage/public` data (~60 MB, `homepage-summary.json` + stats pages) must be COPIED from the
primary, not linked.
+
+## Brand + themes — slices S0–S3 (verified 2026-09-25, `main` @ `b9772e53` + S3 `brand/found-line` @ `2c76f6b2`)
+
+Next is 16.2.3 (`export/node_modules/next/package.json`). The plan and every slice record are in
+[`brand-and-themes.md`](brand-and-themes.md) "As shipped"; the release is
+[`release-10.md`](release-10.md).
+
+- **A force-static GET route handler is copied to its EXACT path under `output: "export"`.**
+ `next/dist/export/index.js:712` marks an app route handler, and `:728-736` copies its prerendered
+ `<route>.body` byte for byte to `out/<route>` — no `.html`, no hash. So
+ `export/app/icons/[file]/route.ts:9-14` (`dynamic = "force-static"`, `dynamicParams = false`,
+ `generateStaticParams` over `ICON_FILES`) writes `out/icons/icon-192.png` etc., and
+ `homepage/app/icons/[file]/route.ts:9-14` is its twin. `app/icon.tsx` cannot do this: its URLs
+ are always hashed. Checked on every S3 build: seven files, PNG magic `89504e47…`, IHDR
+ 180/192/32/512/512. Such a route must never read the request.
+- **`next/og` runs under tsx**, so a unit test can render a real PNG and read its IHDR
+ (`common/lib/brandIcons.test.ts:129-148`, which also reads pixel (0,0) out of the inflated IDAT to
+ tell the variants apart). A probe script that uses top-level await must be `.mts`: tsx refuses it
+ in a `.ts` it compiles to CJS.
+- **`/favicon.ico` can be a route handler** (`export/app/favicon.ico/route.ts:7`,
+ `homepage/app/favicon.ico/route.ts:6`, force-static). Next files `/favicon.ico` as a metadata
+ route (`next/dist/lib/metadata/is-metadata-route.js:114`, `FAVICON_REGEX`), so it is **missing
+ from the build's route table but IS emitted** (`out/favicon.ico`, starting `00 00 01 00 03 00`:
+ three PNG entries, 16/32/48). Next adds no `<link>` for it; `<head>` is exactly `ICON_METADATA`
+ (`common/lib/brandIconFiles.ts:35-43`).
+- **Importing Next's root types into `common/` breaks common's tsc.** `import type { Metadata }
+ from "next"` pulls `next/index.d.ts:1` (`/// <reference types="./types/global" />`), whose
+ `ProcessEnv` augmentation makes `NODE_ENV` required (`next/types/global.d.ts:23`) on every
+ `process.env` object in common's program: 10 errors in untouched tests (S1, tsc run 1). So
+ `ICON_METADATA` is untyped (`brandIconFiles.ts:32-34`) and the layouts type it. `next/og` does not
+ carry the reference.
+- **The service workers: `SHELL` is `"shell-v2"`, `VERSION` stays `"v1"`, `/icons/` is
+ network-first.** `export/service-worker/site-sw.js:37-40` (`sw-hub.js:28-31`); activate keeps only
+ `{SHELL, PAGES, META}` (`:64` / `:54`). **Never bump `VERSION` for a shell change:** `PAGES` and
+ `META` are `pages-${VERSION}` / `meta-${VERSION}`, so a bump deletes every reader's offline channel
+ downloads. `/_next/static/` is cache-first (content-hashed); `/icons/` is `networkFirst(req,
+ SHELL)` (`:107-108` / `:97-98`) because the icons keep their URL across an accent change.
+ `networkFirst` stores only an `ok` response and falls back to the cache when the fetch throws
+ (`site-sw.js:134-144`); `common/lib/archive/serviceWorkerRouting.test.ts` runs both workers in
+ `node:vm` and pins fresh-online, a non-ok response not replacing the cached icon, and the offline
+ fallback. **The rename's one cost:** activate also drops shell-v1's cached HTML and chunks, so right
+ after the update an installed app opens offline only after one more online visit.
+- **The reader's theme is two localStorage keys** (`common/components/themeConfig.ts:27-36`):
+ `ytdlp-tb:base` (`system|light|sepia|dark`) and `ytdlp-tb:accent` (an accent id, stored ONLY
+ while it differs from the site's: `setAccent(siteAccent)` removes it, `ThemeProvider.tsx:178-190`;
+ `"custom"` is never stored). The retired `ytdlp-tb:theme` / `ytdlp-tb:mode` are read once and
+ deleted. **The migration** (`migrateLegacy`, `:167-181`, and the same table inside the pre-paint
+ script, `:207-213`) runs only when no base is stored: mode `light` → `light`, or `sepia` when the
+ old theme was `archive`; `dark` → `dark`; `system` → `system`; absent → nothing stored. The family
+ is dropped. The pre-paint script (`buildThemeScript`, `:196-226`) sets `data-base` + `.dark`, then
+ `data-accent` only from a valid stored id, then every `meta[name=theme-color]`, and **LAST
+ `data-theme-ready="1"`** — the e2e no-flash marker. After a `reload({waitUntil: "commit"})` the new
+ document can have no root yet, so a spec waits on `document.documentElement?.dataset.themeReady`
+ (S2 `e3e3fb26`). `ThemeProvider`'s `useLayoutEffect` (`:137-140`) adopts the stored choice and
+ re-asserts the attributes before paint, because hydration can reset them.
+- **`data-base` / `data-accent` and the cascade** (`common/styles/tokens.css`). `.dark` is on
+ `<html>` iff the resolved base is dark; `@custom-variant dark (&:where(.dark, .dark *))` (`:39`)
+ keeps Tailwind's `dark:` on the class, so sepia styles as light. The light block is
+ `:root, html[data-base="light"]` (`:155-156`) and doubles as the no-script fallback; sepia
+ (`:247`) and dark (`:325`) are `html[data-base=…]`, (0,1,1), and win over `:root`'s (0,1,0). The
+ eight `html[data-accent=…]` rules (`:403-437`) come AFTER all three bases at the same
+ specificity, so the accent wins `--brand` on every base; `--brand` reads the base's `--swatch-<id>`,
+ so an accent is always its contrast-corrected value for the ground it is on. `--brand-mark` is the
+ accent's on-dark value on every base (the header mark sits on an ink tile), with a Signal default
+ on `:root` (`:116-122`) for the editor, which renders no `data-accent`. `--primary` is neutral ink
+ on every base; `--ring: var(--brand)`. `themeTokens.test.ts` parses the sheet and pins all of it,
+ including the swatches against `lib/brand.ts` `ACCENTS`.
+- **The hub and the homepage render dark on the server; a site cannot.**
+ `export/app/layout.tsx:109-120` adds a literal `… dark` class and `data-base="dark"` only when
+ `defaultBase === "dark"` (the hub); `homepage/app/layout.tsx:67-70` always does. A site's default
+ is `system`, which only the browser can resolve, so a site's server HTML carries `data-accent`
+ (and a custom hex's inline `--accent-custom-*`) but no `data-base`, and a no-JS reader gets the
+ light block. `suppressHydrationWarning` is on both `<html>`s; React does not patch attributes on
+ hydration.
+- **Chromium on Linux splits IBM Plex Sans words at 1.5x and up.** The Google-served font has no
+ TrueType instructions (no `fpgm`/`prep`, no glyph programs) but its `gasp` table asks for
+ grid-fitting at every size (`{9: 10, 65535: 15}`), so FreeType hints it and each advance is
+ rounded to a whole CSS pixel: at 14px the `g` of "Signal" is 9 px against a 7.41 px design width,
+ at every DPR (S3 probe, `$T/s3-probe2.log`; S2's "ships TrueType hinting" was inexact). At
+ 1x and 1.25x the hinted glyph fills the advance and reads clean; **from 1.5x (1.5, 1.75, 2, 3) it
+ reads "Sig nal"**. `text-rendering: geometricPrecision` turns hinting off (advance 7.41). It is
+ scoped `@media (min-resolution: 1.5dppx)` (`tokens.css:141-149`; built as `(min-resolution:1.5x)`)
+ on `html, button, input, select, textarea` — the UA sheet resets form controls to `auto`.
+- **Google Fonts can answer a static-weight request with `l/font?kit=…&skey=…` URLs, and Turbopack
+ cannot load them.** IBM Plex Sans is a variable master on Google (`wght` 100–700; next's
+ `font-data.json` lists `variable`). A request for static weights is cut from it — usually a cached
+ `https://fonts.gstatic.com/s/ibmplexsans/v23/<instance>.woff2`, but at times through the dynamic
+ instancer, `https://fonts.gstatic.com/l/font?kit=…&skey=…&v=v23`. Turbopack's `next/font/google`
+ puts the font URL in a `?{"url":…}` import query and requires exactly one query entry, so the `&`
+ breaks it: `next/font/google queries have exactly one entry` → `Module not found:
+ '@vercel/turbopack-next/internal/font/google/font'`, and `next dev` never answers (the homepage e2e
+ webServer timed out at 120 s, S3, 23:30) or `next build` fails (S1, the homepage build). Twice on
+ 2026-09-25, both on IBM Plex Sans; 26 later static-weight requests all got `/s/` URLs. **Request
+ a variable family with no `weight`** (`common/styles/fonts.ts:42-46`, the reason in the comment
+ above it): the variable file is always a pre-built `/s/` URL. Its `wght=400` instance has the
+ static file's advances and outlines exactly (fontTools, 54 glyphs). IBM Plex Mono is static-only
+ on Google, and Archivo was already requested variable.
+- **Worktree e2e and builds write through the `export/public` links into the primary.** Besides
+ `sw.js` (the release 9 fact above), `e2e:2origin` with `TWO_ORIGIN_REBUILD=1` runs `pnpm run
+ build:hub` (`export/e2e-2origin/globalSetup.ts:142`), whose `compose:hub` `writeFile`s
+ `hub-sites.json`, `corpus.json`, `llms.txt`, `robots.txt`, `_headers` and `sw.js` into
+ `paths.exportPublicDir` (`common/bin/compose-hub.ts:79-137`). `writeFile` follows a symlink, so a
+ worktree with no corpus would overwrite the primary's live hub pool with an empty one. Swap those
+ links for plain copies before the run and relink after (every brand slice did; the primary's files
+ kept their size and mtime). `rm` of the no-index `hub-summary.json` (`:59-62`) removes the link,
+ not the target.
+- **`pnpm wt` port blocks are `git worktree list` order**, which is the admin dirs under
+ `.git/worktrees/` sorted by name (`scripts/worktree.mjs:101-109`). Adding `brand-found-line` put it
+ at #1 ahead of `diet-series`, which moved to #2 (reconfirms the release 9 fact above); removing
+ `brand-mark` and `brand-themes` will move `diet-series` again.
+- **The published accent is always a hex, and an absent accent publishes no key** — a decision, not
+ an accident (2026-09-25, S3). `accentHex` (`common/lib/accent.ts:107`) maps an id to its on-dark
+ value;
+ `siteDescriptor.ts`, `compose-hub.ts` and `homepageSummary.ts` omit `accent` when the site sets
+ none, although a reader's page then shows Signal. Third-party hubs keep their own fallback for such
+ a site, as before; the official sites get explicit accents at rollout.
+
+### Anchors this release made stale
+
+- `:6441-6443` (Release 9 facts): "The hub defaults to the `archilyzer` theme, dark". The theme
+ families are gone; the hub opens on the dark base in Signal (`export/app/layout.tsx:36-44`,
+ `defaultBase: "dark", accent: DEFAULT_ACCENT`), server-rendered, and `--chart-3` is fixed per base
+ (a violet), no longer the archilyzer block's green. Historical, left in place.
diff --git a/plans/STATE.md b/plans/STATE.md
@@ -3,9 +3,30 @@
The working memory for the local-AI derived-corpus work. Rewritten at the end of every
session, before context is cleared. See [`README.md`](README.md) for the protocol.
-**Live on :3001 (2026-09-25, 19:40):** `0213f6c8` (release 9 slice F + hub C1), `BUILD_ID`
-`P0VMdKX7gbdsaiS5GvorF`; umtool untouched (`a9YAe_dwhuM6DiCPzIwMD`). `main` is `eebcaa90` (+ C1b,
-C2, C3 — export/homepage only, no editor restart needed). Live sites:
+**Now (2026-09-26, just after midnight): the brand is merged to `main` and NOT rolled out.** `main` =
+`2c76f6b2` + the S3 `plans:` record commit. It is release 10's first content, the Found-line mark and
+base × accent reader themes: S0 `7c9e3bdf`, S1 `575ae1d4`, S2 `b9772e53`, then S3 (integrate: review
+carry-overs, a coherence pass, the full gates, the records, the runbook) fast-forwarded. Record:
+[`release-10.md`](release-10.md); the plan and the per-slice records:
+[`brand-and-themes.md`](brand-and-themes.md).
+- **The live :3001 editor still runs the pre-brand build**, `0213f6c8` / `BUILD_ID`
+ `P0VMdKX7gbdsaiS5GvorF` (umtool `a9YAe_dwhuM6DiCPzIwMD`, untouched; the brand does not touch
+ umtool). The five sites, the hub and the homepage still serve pre-brand builds (below).
+- **Next action: the operator's rollout, per `~/reports/release-10/RUNBOOK.html`** (`make-runbook.py`
+ beside it; the rollout scripts in `scripts/`). In order: cut the export `[Unreleased]` notes (they
+ are public on every site's `/changelog`); restart :3001 on the new `main` (one editor restart, md5
+ before / pre-restart / after-boot, smoke); set each site's accent + wordmark lead in the site form;
+ preview Anilyzer (`https://brand.anilyzer.pages.dev`) and check it on a phone; then the five sites
+ by name, the hub, the homepage.
+- **Archilyzer Media (the YouTube channel) awaits the operator's picks** on the canvas
+ (https://claude.ai/artifact/UsUxwgRkP5a3m4jZXucAvG): mark M1 bone / **M2 Signal**, lockup inline /
+ **imprint** (recommended in bold). Then slice S4 (`brand-and-themes.md` "Proposed"); it needs
+ `ttf-ibm-plex` and Archivo installed as system fonts.
+- Worktrees: `brand-mark` and `brand-themes` are merged and can go; `brand-found-line` once `main`
+ carries its tip. Removing any of them re-sorts the port blocks (`diet-series` moves up).
+
+**Live on :3001 — unchanged since 2026-09-25, 19:40:** `0213f6c8` (release 9 slice F + hub C1),
+`BUILD_ID` `P0VMdKX7gbdsaiS5GvorF`; umtool untouched (`a9YAe_dwhuM6DiCPzIwMD`). Live sites:
- https://archilyzer-hub.pages.dev — C1 + C1b + C2 (archilyzer theme, "Official Instances" with
the homepage's figures, scope chips, per-archive state, "N of M archives answered", archive named
on every card, bare Ko-fi footer link), deploy `6ef7f472` (20:50);
@@ -13,45 +34,28 @@ C2, C3 — export/homepage only, no editor restart needed). Live sites:
deploy `e09900af` (20:51);
- the five official sites at release 8's slices Z + E (17:45 / 18:10 deploys); they carry no Ko-fi.
-**The 2026-09-25 evening plan is complete through step C.** Next = the release 10 candidates below
-(plus: `retry: false` on the hub's subs query — C2 re-read LOW), then Phase 4 slice 3.
-
-**Release 10 candidates (found 2026-09-25 evening, none started):** `retry: false` on the hub's subs
-query (a member with no subs corpus is ready ~1 s late); `/ask` on the hub honours the scope chips
-(`useHubScope().isOn` in `AskHub`); official-site accents on the hub (shared `seriesColor` fallback
-for result stripes + chip dots, needs `hub-summary.json`); the hub orders its cards alphabetically,
-the homepage by transcripts; `export/playwright.config.ts` must unlink `public/sw.js` before copying
-(a worktree run overwrites the primary's hub SW); YouTube's soft block "try again later" classifies
-`deleted`/per_video so it never backs off; the boot pass's wait on the storage pass has no timeout;
-`/jobs` does not show `cancelReason`; the safeRevalidate warning is once per bundle. Then Phase 4
-slice 3 (`plans/one-core.md`), the LM chat-only tier config, the `Dockerfile.build` image refresh.
-
-See the [release-9 rollout record](release-9.md#rollout-2026-09-25-evening--0213f6c8-live-on-3001-third-restart-of-the-day).
-
-**Next (the 2026-09-25 evening plan, step C2 onward) — the operator ruled at ~19:58:** C2 goes as
-proposed (all official sites by default with per-site chips, per-site loading/failed state, "N of M
-archives answered", a text source label on each result card, staged loading) — implementing on
-`one-core/c2-federated-search`. Ko-fi (ko-fi.com/archilyzer) is a BARE link, no copy at all (the
-operator manages the pitch on Ko-fi): homepage + hub footers, sites untouched — on
-`one-core/c1b-hub-copy` with the copy rulings: "Official Instances" in Title Case, the sentence under
-it removed on hub AND homepage, no subtitles unless needed, the per-site descriptions dropped from
-the instance cards. The Paramount Tactical filter clear was the operator's; a full download is wanted
-(handling `youtube`, captions arrive with each video). Then C3 (`HUB_LINK_ENABLED = true`) in the
-same homepage deploy as C1b/C2; then release 10 lows and Phase 4 slice 3.
+**Release 10 candidates (found 2026-09-25 evening, none started; any may join release 10 before its
+rollout):** `retry: false` on the hub's subs query (a member with no subs corpus is ready ~1 s late);
+`/ask` on the hub honours the scope chips (`useHubScope().isOn` in `AskHub`); official-site accents
+on the hub (shared `seriesColor` fallback for result stripes + chip dots, needs `hub-summary.json` —
+setting the accents at rollout already fixes the cards' stripes); the hub orders its cards
+alphabetically, the homepage by transcripts; `export/playwright.config.ts` must unlink
+`public/sw.js` before copying (a worktree run overwrites the primary's hub SW); YouTube's soft block
+"try again later" classifies `deleted`/per_video so it never backs off; the boot pass's wait on the
+storage pass has no timeout; `/jobs` does not show `cancelReason`; the safeRevalidate warning is once
+per bundle. Then Phase 4 slice 3 (`plans/one-core.md`), the LM chat-only tier config, the
+`Dockerfile.build` image refresh.
+
+**The 2026-09-25 evening plan is complete** (steps A–C: release 9 F + C1 live on :3001; C1b, C2, C3
+on the hub and the homepage). The operator's rulings (C2's federated search as proposed; a bare Ko-fi
+link on the homepage and hub footers only; the Paramount Tactical filter clear) are recorded in
+`release-9.md`. Paramount Tactical downloads in full at lane pace since the operator cleared its
+`Quartering` include filter (19:35). See the
+[release-9 rollout record](release-9.md#rollout-2026-09-25-evening--0213f6c8-live-on-3001-third-restart-of-the-day).
See the
[release-8 rollout record](release-8.md#rollout-2026-09-25-late-afternoon--0e72ef73-live-on-3001-second-restart-of-the-day).
-**Evening 2026-09-25 (in progress).** Homepage refresh is production (`a684a0ce`). Paramount Tactical
-finished on the operator's "queue step 1": metadata-scan job `01M3DBFV07NC0Z6KWZ1BG3K9BX` scanned the
-last 201 (0 errors; store 1,370 entries + 12 members-only errors of 1,473); download-missing
-`01M3DD3WBVYPPXB1NCQ1HDX8BM` found 1,381 missing, 92 complete, excluded 12 members-only, and
-**settled 1,369 by the include filter with no request and no sleep**; `keep-videos` matched 1
-(`NV1QqS9NOiU`, already kept). Both ops jobs read `failed` on the revalidate invariant (release 9 B1).
-The include filter is still `Quartering`: clearing it (Configure) re-evaluates the 1,369 for the
-auto-download lane — the operator's call. Release 9 (`one-core/r9-fixes`) and hub slice C1
-(`one-core/c1-hub-look`) are reviewed and in fixes; see `plans/release-9.md` once merged.
-
**Last updated:** 2026-09-25 (late afternoon). **Release 8 is live, the day's second restart.**
- Release 8 (`bb3dbb4c` → `0e72ef73`) has two slices, each Opus-reviewed: SHIP AFTER FIXES, then
SHIP.
diff --git a/plans/brand-and-themes.md b/plans/brand-and-themes.md
@@ -1,7 +1,9 @@
# Brand + themes: the Found-line mark, and base × accent
-Status: APPROVED 2026-09-25 — implementing on `brand/found-line` (worktree `../brand-found-line`,
-pnpm wt block #2). Slices S1 and S2 branch from S0's tip as `brand/mark` and `brand/themes`.
+Status: SHIPPED to main 2026-09-25 (S0 `7c9e3bdf`, S1 `575ae1d4`, S2 `b9772e53`, S3 `2c76f6b2` + its
+`plans:` record commit), not rolled out. Worked on `brand/found-line` (worktree `../brand-found-line`,
+pnpm wt block #1), with S1 and S2 as `brand/mark` and `brand/themes` off S0's tip. The release is
+[`release-10.md`](release-10.md); the operator's runbook is `~/reports/release-10/RUNBOOK.html`.
Design canvas: https://claude.ai/artifact/UsUxwgRkP5a3m4jZXucAvG (boards *Family*, *In context*,
*Themes · base × accent*, and the rejected directions A–C). Read it with Artifact
@@ -1033,11 +1035,177 @@ describes the committed icons, which is S1's, so it was left.
- the hub on its dark default: the parent mark, and `data-accent="signal"`;
- the 390 px phone menus on sepia and dark at DPR 2, after `fonts.ready`: no glyph gaps.
+### Slice S3, as shipped — integrate (2026-09-25)
+
+Branch `brand/found-line`, fast-forwarded to `main` at `b9772e53` (S0 + S1 + S2 merged), worktree
+`../brand-found-line`, port block #1. S3 is the integrated tree: the review carry-overs
+(`$T/s3-carry.md`), a coherence pass over the merged code and docs, the full gates, the records and
+the operator's runbook. Nothing was rolled out.
+
+**Carry-overs, all done.**
+- **Test pins** (S0 review). `brand.test.ts` pins the bar as a literal (`MIN_ACCENT_CONTRAST === 4.5`,
+ and both contrast loops compare against `4.5`), the sRGB curve at its boundary pair on white
+ (`#767676` 4.54 passes, `#777777` 4.48 fails — a naive gamma 2.2 would not land there), and the whole
+ 21-value `ACCENTS` table as literals. `accent.test.ts` pins the literal too.
+- **`export/app/lib/site.ts`**: `hubSite()`'s comment re-wrapped at ~80 columns.
+- **Service-worker test** (S1 re-read). `serviceWorkerRouting.test.ts`'s fake response gained `ok`, and
+ both workers get two cases: an online non-ok `/icons/` response is passed through but never replaces
+ the cached icon (and the next offline launch still gets it), and a failed fetch falls back to the
+ cached icon. Removing `if (res.ok)` from `site-sw.js`'s `networkFirst` turns the first red.
+- **Light base status colours** (S2 left). On their own `-soft` fills over the ground and a card:
+ success `#1f7a4d` → **`#1c7046`** (4.18 / 4.52 → **4.74 / 5.12**), warning `#96650b` → **`#825809`**
+ (3.89 / 4.18 → **4.75 / 5.14**); the `-soft` rgba values follow; info (5.20 / 5.62), destructive and
+ gone (4.69 / 5.08) already passed. On the plain ground they read 5.60 and 5.77. `themeTokens.test`'s
+ text-on-soft check now runs on all three bases (30 checks). No other file used the old hexes.
+- **`text-rendering: geometricPrecision` scoped to high density** — at **1.5dppx**, not the 2dppx the
+ review suggested. A probe of the built hub (`$T/s3-probe*.log`, `s3-probe-montage*.png`) measured the
+ `g` of "Signal" at 9 px against its 7.41 px design width at every DPR without the rule. At 1x and 1.25x
+ the hinted glyph fills the advance and the word reads clean; at 1.5x and 1.75x (150 % and 175 %
+ desktop scaling) it already reads "Sig nal", as at 2x and 3x. `@media (min-resolution: 1.5dppx)`
+ (built as `(min-resolution:1.5x)`) keeps 1x/1.25x screens hinted and fixes every density that split;
+ re-probed clean at 1, 1.25, 1.5, 1.75 and 2. `themeTokens.test` pins the media rule, its selector list,
+ and that no top-level rule sets `text-rendering`.
+- **`export/playwright.config.ts`**: the `sw.js` staging comment sat above an unrelated block and said
+ "exactly like the committed public/icons/*". It now sits on the copy it explains and says the icons
+ need no staging (route handlers render them).
+- **The absent-accent contract** (S0 review "consider"; parent decision): no change — recorded in
+ `release-10.md` and FACTS, not in code.
+
+**Coherence pass.** A grep of code, specs, docs and the editor UI for the retired families (`archive`,
+`selenized`, `swiss`, `archilyzer` as a theme, `data-theme`), the retired fonts, `siteAccentVars`,
+`FALLBACK_THEME_COLOR`, `HUB_THEME_COLOR` and `public/icons` found only legitimate uses (the
+migration's legacy keys and tests, the `data-theme-ready` marker, the no-retired-family test) plus four
+stale comments and one hint, fixed: `ui/alert.tsx` and `WorkersView.tsx` ("every theme family"),
+`views/laneState.ts` ("four theme families × light/dark", and a `flow/tone.ts` path that is
+`pipeline/tone.ts`), and the site form's accent hint ("per theme" → "on each base (light, sepia,
+dark)"). `homepage/content/docs/**`, the root `*.md` and SITE.md were already right.
+**Changelogs:** the three `[Unreleased]` sections now read as one release. The editor's S1 bullet
+promised the header mark would follow the reader's accent "once the theme picker lands" and S0's
+promised the header, icons and picker "with the rest of the brand work" — both stale; they now state
+what shipped. The icons' network-first refresh and the one-visit offline gap are in the editor and
+export bullets; the light status contrast and the homepage chart's stack order are named. Release 9's
+bullets are untouched.
+
+**A build defect the gates found: IBM Plex Sans is now requested variable.** The first homepage e2e run
+never started. `next dev` failed on every IBM Plex Sans `@font-face` with `next/font/google queries have
+exactly one entry` → `Module not found: '@vercel/turbopack-next/internal/font/google/font'`, and the
+webServer timed out at 120 s. The generated CSS (`homepage/.next/dev/static/chunks/…ibm_plex_sans…css`)
+held `https://fonts.gstatic.com/l/font?kit=…&skey=…&v=v23` URLs: Google had cut the four static weights
+through its dynamic instancer, and Turbopack carries the font URL in a one-entry import query that the
+`&` splits. It is the error that failed S1's first homepage build too; 26 static-weight requests made
+afterwards all got `/s/` files, so it is intermittent upstream, and a site build during the rollout
+could fail the same way. `fonts.ts` now asks for Plex Sans with no `weight` (variable, `wght`
+100–700): the variable file is always a pre-built `/s/` URL, and its `wght=400` instance has the static
+file's advances and outlines exactly (fontTools, 54 glyphs; both files carry no TrueType instructions
+and the same `gasp`). The UI's 400–700 are unchanged; the build now emits 12 Plex Sans faces
+(`font-weight: 100 700`) instead of 48, and no `l/font` URL. The glyph probe after the change reads the
+same advances at every DPR. Archivo was already requested variable; IBM Plex Mono is static-only on
+Google and never goes through the instancer. While there, the `tokens.css` comment is corrected: S2
+wrote "IBM Plex Sans ships TrueType hinting", but the Google-served font has no instructions; its
+`gasp` table asks for grid-fitting and FreeType hints it.
+
+| sha | what |
+|---|---|
+| `6497439d` | brand tests: the literal 4.5 bar, the sRGB boundary pair, all 21 accent values; `accent.test` the literal bar |
+| `53a692ac` | `serviceWorkerRouting.test`: a non-ok and a failed online icon fetch leave the cached icon, both workers |
+| `1da6a265` | light success `#1c7046` / warning `#825809` (+ soft fills); `themeTokens.test` text-on-soft on every base |
+| `408120a3` | `geometricPrecision` under `@media (min-resolution: 2dppx)` + the test (superseded by `dc7ff1cf`) |
+| `58e71a88` | `export/app/lib/site.ts` comment wrap; `playwright.config.ts`'s sw.js comment by its code, no committed icons |
+| `a0790d39` | stale "theme family" comments (`ui/alert`, `laneState`, `WorkersView`) and the site form's accent hint |
+| `25952f12` | the editor/export/homepage `[Unreleased]` brand bullets as one coherent release |
+| `dc7ff1cf` | the scope starts at 1.5dppx (measured), test updated |
+| `a4542721` | `fonts.ts`: IBM Plex Sans requested variable (the `l/font` build failure) |
+| `2c76f6b2` | the `tokens.css` comment: why Plex hints on Linux, why the scope starts at 1.5x |
+| _this_ | `plans:` this record, `release-10.md`, FACTS, STATE, the rollout section |
+
+**Gates** (the code tip is `2c76f6b2`).
+- **tsc** clean before every code commit (four runs: `s3-tsc-1..4.log`; the changelog commit is markdown).
+- **Unit and script tests** on `25952f12`: common **1,913/1,913** (1,907 + 6: brand +1, service worker
+ +4, tokens +1), editor unit **79/79**, `test:scripts` **162 + 1 skip**, mcp **219/219**;
+ `file-schemas-docs.ts --check` clean. On `2c76f6b2`: common **1,913/1,913** again (the font request
+ touches nothing the other three test).
+- **Builds**, `export/public` seeded under sh with `sw.js` a plain copy and no dangling links,
+ `homepage/public` data copied from the primary:
+ - on `25952f12` (`s3-gates.log`): editor ok (33 s), export site ok (22 s), export hub ok (19 s),
+ homepage ok (13 s);
+ - on `2c76f6b2` (`s3-regate.log`): editor ok (43 s), export site ok (26 s), homepage ok (14 s), export
+ hub ok (27 s);
+ - every export/homepage build: `out/icons` = the seven files, PNG magic `89504e470d0a1a0a` and IHDR
+ 180/192/32/512/512, `favicon.ico` `00 00 01 00 03 00`; the site's `icon.svg` lit Signal `#5fa8a0`,
+ the hub's and homepage's the parent mark; the site `<html>` carries `data-accent="signal"` and no
+ `data-base` (theme-color pair `#f3f6f7` / `#0c0a08`), the hub and homepage `dark
+ data-base="dark" data-accent="signal"`; the built CSS has `@media (min-resolution:1.5x){html,button,
+ input,select,textarea{text-rendering:geometricprecision}}`.
+- **e2e, the full sequence on `dc7ff1cf`** (`s3-e2e.log`, 23:22 → 00:10, no queue wait):
+ - export full **204 passed**, 6.4 min;
+ - `e2e:hub` **19 passed**, 37 s;
+ - `e2e:2origin` (`TWO_ORIGIN_REBUILD=1`, the seven compose outputs + `sw.js` swapped for copies,
+ relinked after) **3 passed**, 33 s;
+ - homepage full: **did not start** — the webServer timed out at 120 s on the font defect above;
+ - **editor full (`pnpm e2e`) 638 passed, 1 failed, 12 skipped, 38.2 min** (651 tests). The failure
+ is `pipeline.spec.ts:164` "channel list sync button runs the platform-queue sync", 355 ms:
+ `EEXIST: file already exists, mkdir '…/editor/test-transcripts/channels'` inside `resetData`'s
+ fixture copy (`editor/e2e/helpers.ts`), before the test touched the page. **Pre-existing,
+ flaky:** it is the fixture-reset race the helper's own comment describes (a server runner
+ re-creating a path while the tree is reset; the same class hit `channel-work.spec` and
+ `pipeline.spec:106` before), and the brand diff touches no helper, runner, channel list or job
+ code (`git diff --stat c9223978 HEAD -- editor/e2e/helpers.ts editor/app/channels common/jobs
+ common/controller` is empty). Rerun alone ×3 on the final tree: **3 passed**.
+- **e2e re-gate on `2c76f6b2`** (`s3-regate.log`, 00:14 → 00:26, after the font fix):
+ - homepage full **27 passed**, 42 s;
+ - editor `pipeline.spec.ts:164 --repeat-each 3` **3 passed**, 39 s;
+ - editor `theme.spec.ts branding.spec.ts sites-crud.spec.ts` **23 passed**, 58 s;
+ - export full **204 passed**, 7.2 min;
+ - `e2e:hub` **19 passed**, 43 s;
+ - `e2e:2origin` (copies swapped in and relinked as above) **3 passed**, 41 s.
+ - The full editor suite was not re-run after `a4542721`/`2c76f6b2`: they change only which
+ Google Fonts URL `next/font` requests (same outlines) and a CSS comment. The suites that compile
+ those fonts in `next dev` (export, homepage) ran in full on the final tree.
+- **The primary checkout's `export/public` was never written:** `sw.js` 10,027 B, mtime 20:47:37, and
+ `hub-summary.json`, `hub-sites.json`, `corpus.json`, `llms.txt`, `robots.txt`, `sitemap.xml` and
+ `_headers` kept their sizes and mtimes before, between and after every run.
+- Numbers tools: none.
+
+**Visual set** (`$T/s3-*.png`, served `out/` on localhost, all looked at; embedded in the runbook). A
+brass site (`$T/s3-sites/shotsite`, the fixture copy titled Jeralyzer with lead `Jer`): light + brass
+(`--brand #95661a`, mark `#e3b15c`), sepia + violet (`#6a4bc4` / `#b49cf2`), dark + brass, the theme
+menu open (Base + Accent, Brass tagged DEFAULT), the 390 px phone sheet on sepia at DPR 2 (no split
+words); the hub on dark (`data-accent="signal"`, the parent mark, Archi|lyzer; built in the worktree,
+so it read the primary's `hub-sites.json` through the link and loaded the live members); the homepage
+header. They match the S2 post-merge shots and the canvas.
+
+**Found and left.**
+- **The absent-accent contract stays as it is** (decided; `release-10.md`).
+- **The release-10 lows are unstarted**, including `playwright.config.ts` copying over `public/sw.js`
+ without unlinking it: S3 replaced the worktree's link with a copy before every run instead.
+- **The 1.5dppx threshold was measured on Linux Chromium only.** Chromium on Windows (DirectWrite) and
+ macOS were not probed; the rule changes nothing where text is not hinted.
+- **`e2e:2origin` builds the hub twice per run**: `stageTwoOrigins()` runs at config load, and
+ Playwright loads the config in the runner and again in the worker (two `compose-hub … Compiled`
+ passes in `s3-e2e-2origin.log`). Pre-existing; it costs ~30 s.
+- **Worktree state:** `export/out` holds the last shot build (the hub, reading the primary's
+ `hub-sites.json` through the link); `export/.2origin/` holds a hub build (~2.5 GB, gitignored);
+ `homepage/public` holds copied data (gitignored). The worktree's `export/public` links are intact
+ and `sw.js` is a copy.
+
+**Runbook:** `~/reports/release-10/RUNBOOK.html`, generated by `~/reports/release-10/make-runbook.py`
+(the release 8/9 generator's shape; screenshots embedded as base64). It writes the rollout scripts to
+`~/reports/release-10/scripts/` (`r10-build`, `r10-restart`, `r10-preview`, `r10-sites`, `r10-home`,
+`r10-live-check`), release 9's procedure parameterised, logging to `~/reports/release-10/tmp/`.
+
## Operator rollout (after merge)
-1. Restart the live :3001 editor on the new `main`; it needs S0's form.
-2. In the editor's site form (the one writer; never hand-edit `transcripts/`), set accent and
- wordmark lead:
+The runbook is `~/reports/release-10/RUNBOOK.html` (generated by `make-runbook.py` beside it, which
+also writes the rollout scripts to `scripts/`); the release record is
+[`release-10.md`](release-10.md). The commands below were checked against the repo (S3).
+
+0. Cut `export/CHANGELOG.md`'s `[Unreleased]`: it is rendered on every site's public `/changelog`.
+ `/sites` → Release notes → Cut release; untick "Commit changelog" while the primary has untracked
+ files (it refuses a dirty tree) and commit `export/CHANGELOG.md` by hand.
+1. Restart the live :3001 editor on the new `main` (release 9's procedure: md5 before / pre-restart /
+ after-boot, one editor restart, smoke; umtool untouched); it needs S0's form.
+2. In the editor's site form (`/sites/<id>`, the one writer; never hand-edit `transcripts/`), set
+ accent and wordmark lead:
| Site | Accent | Wordmark lead |
|---|---|---|
@@ -1048,13 +1216,17 @@ describes the committed icons, which is S1's, so it was left.
| bonnellyzer | blue | Bonnell |
| jasolyzer | green | Jaso |
-3. `archilyzer deploy site anilyzer --preview brand` (or
- `pnpm ops deploy-site --json '{"siteId":"anilyzer","preview":"brand"}' --wait`). On a phone,
- check the favicon and installed-PWA icon update (SW `shell-v2`), the picker, and migration
- from a stored old theme.
-4. Build and deploy every site: `pnpm ops build-deploy --json '{"siteIds":[…]}' --wait`; then the
- hub (`pnpm ops build-hub --json '{"deploy":true}' --wait`) and the homepage
- (`archilyzer deploy homepage`).
+3. Preview Anilyzer: `pnpm ops build-deploy --json
+ '{"siteId":"anilyzer","skipArchives":true,"preview":"brand"}' --wait` →
+ `https://brand.anilyzer.pages.dev` (`deploy-site` / `archilyzer deploy site` ship an ALREADY-built
+ `export/out`, so they need a build first). Check the favicon, the picker, and the migration from a
+ stored old theme (on the preview origin, seed the legacy keys in the console; the real phone test
+ is on the production origin after step 4). The installed PWA's icon updates only on production.
+4. Build and deploy every site by name: `pnpm ops build-deploy --json
+ '{"siteIds":["jeralyzer","anilyzer","bonnellyzer","hasanalyzer","rekietalyzer"],"skipArchives":true}'
+ --wait --wait-timeout 7200`; then the hub (`pnpm ops build-hub --json '{"deploy":true}' --wait`)
+ and the homepage (`pnpm --filter yt-dlp-transcript-common exec tsx bin/archilyzer.ts build
+ homepage`, then `… deploy homepage`). `pnpm ops` needs `WORKER_TOKEN` from `editor/.env`.
## Proposed — Archilyzer Media, the YouTube channel (awaiting the operator's picks)
diff --git a/plans/release-10.md b/plans/release-10.md
@@ -0,0 +1,196 @@
+# Release 10 — the brand: the Found-line mark, and base × accent reader themes
+
+`main` at `c9223978` (release 9 live on :3001 since 2026-09-25 19:40 as `0213f6c8`; C1b/C2/C3
+deployed to the hub and the homepage the same evening). Release 10's first content is the brand
+the operator asked for on 2026-09-25: one mark and one wordmark for Archilyzer and every official
+site, and a reader theme made of two independent choices, a **base** (System / Light / Sepia /
+Dark) and an **accent** (the site's own by default, any of seven by choice). The five theme
+families retire. The plan, the design decisions and the per-slice records are
+[`brand-and-themes.md`](brand-and-themes.md); this file records the release as a whole. Rules:
+`plans/tools/implementer-rules.md`.
+
+**The release-10 "lows" in `STATE.md` are still unstarted** (`retry: false` on the hub's subs
+query, `/ask` honouring the hub's scope chips, the shared `shelfAccent` fallback, the hub's card
+order, `playwright.config.ts` unlinking `public/sw.js` before its copy, the YouTube soft-block
+classification, the boot pass's storage wait, `cancelReason` on `/jobs`, the safeRevalidate
+warning). Any of them may join this release before it is rolled out.
+
+## Record
+
+### The brand, slices S0–S3 (2026-09-25)
+
+Four slices, each one Opus implementer and one Opus review, under the plan's dependency graph:
+S0 alone, then S1 and S2 in parallel worktrees off S0's tip, then S3 on the merged tree.
+
+| Slice | Branch → tip | Review | Merged to `main` |
+|---|---|---|---|
+| **S0** palette + data | `brand/found-line` off `d8dd98b8` → `02295a94` (+ `dbf12219`, the Archilyzer Media proposal) | SHIP | `7c9e3bdf` |
+| **S1** mark, icons, wordmark | `brand/mark` off `02295a94` → `03a0ce06` | SHIP (fixes `541a46e0`, `405fccd5`, `951a0ff5`, `4a7641af`; re-read SHIP) | `575ae1d4` |
+| **S2** base × accent | `brand/themes` off `02295a94` → `42e84f68` (merged `main` as `6456ac6c`) | SHIP AFTER FIXES (7 fixes; re-read SHIP) | `b9772e53` |
+| **S3** integrate | `brand/found-line` fast-forwarded to `b9772e53` → `2c76f6b2` + the `plans:` record | — | fast-forward |
+
+**Gates per slice** (full numbers in each slice's record in `brand-and-themes.md`):
+
+| Slice | Unit + script tests | Builds | e2e |
+|---|---|---|---|
+| S0 | common 1,874, editor unit 78, scripts 162 + 1 skip, mcp 219; SITE.md `--check` clean | editor, export, homepage ok | editor `sites-crud` + `branding` 16/16 (run 2; run 1's failure was the new test's own locator), export `site-branding` 7/7, `e2e:hub` 12/12 |
+| S1 | common 1,881 → 1,887 (review fixes), editor unit 79, scripts 162 + 1, mcp 219 | editor, export (site, hub, a brass demo), homepage ok; icons + ICO magic checked | export full 199 + 1 (`theme-family.spec:18`, a click-before-hydration race in a spec S2 deleted; rerun 6/6), `e2e:hub` 14, homepage 26, editor 48, `e2e:2origin` 3; review re-gate 8 + 14 |
+| S2 | common 1,893 → 1,907 after merging `main`, editor unit 79, scripts 162 + 1, mcp 219 | editor, export (site, hub), homepage ok | pre-merge: export full 199, hub 12, homepage 24, editor 22; post-merge: export full 203 + 1 (a ready-marker race, fixed `e3e3fb26`, then 39/39 over three repeats), hub 19, 2origin 3, homepage 27, editor 23 |
+| S3 | common 1,913, editor unit 79, scripts 162 + 1, mcp 219 | editor, export (site, hub), homepage ok, twice | below: export 204, hub 19, 2origin 3, homepage 27, **editor full 638 + 1 flaky (rerun 3/3), 12 skipped** |
+
+**What shipped, in one place:**
+- **S0 — the brand as data.** `common/lib/brand.ts` (seven named accents with a value per ground,
+ each ≥ 4.5:1 against its ground and its ink; the three grounds; the Found-line mark as a shape
+ list; `markSvg`; the subject-split wordmark). `site.json` takes `accent` as an id or a hex and a
+ new `wordmarkLead` (a proper prefix of `headerTitle`, else dropped). The published accent stays a
+ hex (`accentHex` in `/site.json`, `hub-sites.json`, the homepage summary). The editor's site form
+ has the swatch radio group + Custom hex and the Wordmark lead field.
+- **S1 — the mark everywhere.** Every icon file (`/icons/*`, `/favicon.ico`) is rendered at build by
+ force-static route handlers (`next/og` PNGs, a PNG-payload ICO), lit with the site's accent; the
+ hub, the homepage and the editor wear the parent mark (bone on slate). Header = `BrandMark` +
+ `Wordmark`; the footer credit carries the parent mark; the editor gets a favicon. The manifest is
+ on ink. Both service workers: `SHELL = "shell-v2"`, `/icons/` network-first, `VERSION` unchanged.
+- **S2 — base × accent.** `tokens.css` is three base blocks × eight accent rules (the seven + custom),
+ with fixed validated charts per base; Archivo / IBM Plex Sans / IBM Plex Mono; the pre-paint
+ script migrates the retired theme/mode keys once; `ThemeMenu` (Base + Accent), `ThemeToggle`
+ (System → Light → Sepia → Dark), the phone sheet's two native radio groups; the hub and homepage
+ render dark on the server.
+- **S3 — integrate** (below).
+
+**The absent-accent contract: no change (decided 2026-09-25, S3).** A site with no `accent` renders
+in Signal, but its published `/site.json`, `hub-sites.json` entry and homepage-summary entry still
+carry **no** `accent` key, rather than Signal's hex. Publishing one would change what third-party
+hubs draw for every accent-less site they already list; today they keep their own fallback, as
+before the brand. The official sites get explicit accents at rollout (the settings table below), so
+the question is moot for them. Revisit only if a hub asks for it.
+
+### Slice S3, as shipped — integrate (2026-09-25)
+
+The full record — every carry-over with its numbers, the coherence pass, the visual set, found and
+left — is [`brand-and-themes.md`](brand-and-themes.md) "Slice S3, as shipped". In short:
+- **Review carry-overs, all done:** the brand tests pin the literal 4.5 bar, the sRGB boundary pair
+ and all 21 accent values; the service-worker test covers a non-ok and a failed icon fetch; Light's
+ success and warning read ≥ 4.5:1 on their own soft fills (`#1c7046` 4.74 / 5.12, `#825809`
+ 4.75 / 5.14); `geometricPrecision` is scoped to **1.5dppx** (measured: words split from 1.5x, not
+ only 2x); two stale comments fixed; the absent-accent decision recorded above.
+- **Coherence pass:** four stale "theme family" comments and the accent hint fixed; the three
+ `[Unreleased]` changelogs rewritten as one release (release 9's bullets untouched).
+- **A build defect the gates found, fixed:** Google Fonts at times serves IBM Plex Sans's static
+ weights through `https://fonts.gstatic.com/l/font?kit=…&skey=…`, which Turbopack's
+ `next/font/google` cannot load, so `next dev` / `next build` failed intermittently (S1's first
+ homepage build, S3's first homepage e2e). Plex Sans is now requested variable (`wght` 100–700,
+ identical outlines and advances), whose file is always a pre-built `/s/` URL.
+
+| sha | what |
+|---|---|
+| `6497439d` | brand tests: the literal 4.5 bar, the sRGB boundary pair, all 21 accent values; `accent.test` the literal bar |
+| `53a692ac` | `serviceWorkerRouting.test`: a non-ok and a failed online icon fetch leave the cached icon, both workers |
+| `1da6a265` | light success `#1c7046` / warning `#825809` (+ soft fills); `themeTokens.test` text-on-soft on every base |
+| `408120a3` | `geometricPrecision` under `@media (min-resolution: 2dppx)` + the test (superseded by `dc7ff1cf`) |
+| `58e71a88` | `export/app/lib/site.ts` comment wrap; `playwright.config.ts`'s sw.js comment by its code, no committed icons |
+| `a0790d39` | stale "theme family" comments (`ui/alert`, `laneState`, `WorkersView`) and the site form's accent hint |
+| `25952f12` | the editor/export/homepage `[Unreleased]` brand bullets as one coherent release |
+| `dc7ff1cf` | the scope starts at 1.5dppx (measured), test updated |
+| `a4542721` | `fonts.ts`: IBM Plex Sans requested variable (the `l/font` build failure) |
+| `2c76f6b2` | the `tokens.css` comment: why Plex hints on Linux, why the scope starts at 1.5x |
+| _this_ | `plans:` this record, `release-10.md`, FACTS, STATE, the rollout section |
+
+**Gates** (the code tip is `2c76f6b2`).
+- **tsc** clean before every code commit (four runs: `s3-tsc-1..4.log`; the changelog commit is markdown).
+- **Unit and script tests** on `25952f12`: common **1,913/1,913** (1,907 + 6: brand +1, service worker
+ +4, tokens +1), editor unit **79/79**, `test:scripts` **162 + 1 skip**, mcp **219/219**;
+ `file-schemas-docs.ts --check` clean. On `2c76f6b2`: common **1,913/1,913** again (the font request
+ touches nothing the other three test).
+- **Builds**, `export/public` seeded under sh with `sw.js` a plain copy and no dangling links,
+ `homepage/public` data copied from the primary:
+ - on `25952f12` (`s3-gates.log`): editor ok (33 s), export site ok (22 s), export hub ok (19 s),
+ homepage ok (13 s);
+ - on `2c76f6b2` (`s3-regate.log`): editor ok (43 s), export site ok (26 s), homepage ok (14 s), export
+ hub ok (27 s);
+ - every export/homepage build: `out/icons` = the seven files, PNG magic `89504e470d0a1a0a` and IHDR
+ 180/192/32/512/512, `favicon.ico` `00 00 01 00 03 00`; the site's `icon.svg` lit Signal `#5fa8a0`,
+ the hub's and homepage's the parent mark; the site `<html>` carries `data-accent="signal"` and no
+ `data-base` (theme-color pair `#f3f6f7` / `#0c0a08`), the hub and homepage `dark
+ data-base="dark" data-accent="signal"`; the built CSS has `@media (min-resolution:1.5x){html,button,
+ input,select,textarea{text-rendering:geometricprecision}}`.
+- **e2e, the full sequence on `dc7ff1cf`** (`s3-e2e.log`, 23:22 → 00:10, no queue wait):
+ - export full **204 passed**, 6.4 min;
+ - `e2e:hub` **19 passed**, 37 s;
+ - `e2e:2origin` (`TWO_ORIGIN_REBUILD=1`, the seven compose outputs + `sw.js` swapped for copies,
+ relinked after) **3 passed**, 33 s;
+ - homepage full: **did not start** — the webServer timed out at 120 s on the font defect above;
+ - **editor full (`pnpm e2e`) 638 passed, 1 failed, 12 skipped, 38.2 min** (651 tests). The failure
+ is `pipeline.spec.ts:164` "channel list sync button runs the platform-queue sync", 355 ms:
+ `EEXIST: file already exists, mkdir '…/editor/test-transcripts/channels'` inside `resetData`'s
+ fixture copy (`editor/e2e/helpers.ts`), before the test touched the page. **Pre-existing,
+ flaky:** it is the fixture-reset race the helper's own comment describes (a server runner
+ re-creating a path while the tree is reset; the same class hit `channel-work.spec` and
+ `pipeline.spec:106` before), and the brand diff touches no helper, runner, channel list or job
+ code (`git diff --stat c9223978 HEAD -- editor/e2e/helpers.ts editor/app/channels common/jobs
+ common/controller` is empty). Rerun alone ×3 on the final tree: **3 passed**.
+- **e2e re-gate on `2c76f6b2`** (`s3-regate.log`, 00:14 → 00:26, after the font fix):
+ - homepage full **27 passed**, 42 s;
+ - editor `pipeline.spec.ts:164 --repeat-each 3` **3 passed**, 39 s;
+ - editor `theme.spec.ts branding.spec.ts sites-crud.spec.ts` **23 passed**, 58 s;
+ - export full **204 passed**, 7.2 min;
+ - `e2e:hub` **19 passed**, 43 s;
+ - `e2e:2origin` (copies swapped in and relinked as above) **3 passed**, 41 s.
+ - The full editor suite was not re-run after `a4542721`/`2c76f6b2`: they change only which
+ Google Fonts URL `next/font` requests (same outlines) and a CSS comment. The suites that compile
+ those fonts in `next dev` (export, homepage) ran in full on the final tree.
+- **The primary checkout's `export/public` was never written:** `sw.js` 10,027 B, mtime 20:47:37, and
+ `hub-summary.json`, `hub-sites.json`, `corpus.json`, `llms.txt`, `robots.txt`, `sitemap.xml` and
+ `_headers` kept their sizes and mtimes before, between and after every run.
+- Numbers tools: none.
+
+## Rollout
+
+Nothing is rolled out. The live :3001 editor still runs `0213f6c8` (the pre-brand build); the five
+official sites are at release 8's Z + E; the hub and homepage at release 9's C1b/C2/C3. The
+operator's runbook is `~/reports/release-10/RUNBOOK.html` (generated by `make-runbook.py` beside it,
+with the rollout scripts in `scripts/`). The steps:
+
+1. **Cut the export release notes first.** `export/CHANGELOG.md`'s `[Unreleased]` is rendered on
+ every site's public `/changelog` until it is cut: `/sites` → Release notes → Cut release (the
+ form suggests `0.8.8`). With "Commit changelog" ticked the form refuses a dirty tree, and the
+ primary has an untracked `settings.json.pre-priority-*`, so untick it and commit the changelog by
+ hand. The
+ editor's own notes (`/changelog`) can be cut the same way; they are not public.
+2. **Restart the live :3001 editor on the new `main`** (release 9's procedure): check `/jobs` for an
+ operator-started job; md5 `before`; the editor build into the live `.next` (umtool untouched, not
+ rebuilt or restarted); md5 `pre-restart`; ONE editor restart; md5 `after-boot` — the three must
+ match; the smoke (`SMOKE_FAIL=0`, or every flag explained). The editor itself changes look: the
+ parent mark and a favicon, Archivo/Plex, and the operator's stored theme migrates once.
+3. **Set each site's accent and wordmark lead** in the site form (`/sites/<id>`, the one writer; never
+ hand-edit `transcripts/`):
+
+ | Site | Accent | Wordmark lead |
+ |---|---|---|
+ | jeralyzer | brass | Jer |
+ | rekietalyzer | vermilion | Rekieta |
+ | hasanalyzer | violet | Hasan |
+ | anilyzer | sakura | Ani |
+ | bonnellyzer | blue | Bonnell |
+ | jasolyzer | green | Jaso |
+
+ Six `site.json` files change, and nothing else (an md5 sweep shows it).
+4. **Preview Anilyzer**: `pnpm ops build-deploy --json
+ '{"siteId":"anilyzer","skipArchives":true,"preview":"brand"}' --wait` →
+ `https://brand.anilyzer.pages.dev`. On a phone and the desktop: the favicon (Sakura lit line),
+ the picker (Base + Accent, Sakura tagged default), and the migration — the preview is its own
+ origin with no stored theme, so seed the legacy keys in the console (`ytdlp-tb:theme=archive`,
+ `ytdlp-tb:mode=light`) and expect Sepia. The real phone test is on production: pick an old theme
+ on `anilyzer.pages.dev` before step 5, expect Sepia after it; an installed app's icon updates only
+ from production (SW `shell-v2`, `/icons/` network-first).
+5. **Every official site**: `pnpm ops build-deploy --json
+ '{"siteIds":["jeralyzer","anilyzer","bonnellyzer","hasanalyzer","rekietalyzer"],"skipArchives":true}'
+ --wait --wait-timeout 7200` (by name, never `all`).
+6. **The hub**: `pnpm ops build-hub --json '{"deploy":true}' --wait`. **The homepage**:
+ `pnpm --filter yt-dlp-transcript-common exec tsx bin/archilyzer.ts build homepage`, then
+ `… deploy homepage`.
+7. Live checks per surface, and the record of this rollout under this heading.
+
+**What readers will notice:** a stored theme migrates once (light stays light, Archive light becomes
+Sepia, dark stays dark, system stays system; the family is dropped). After the service-worker update
+an installed app opens offline only after one more online visit (`shell-v2` drops the old shell);
+offline channel downloads are kept.