commit f5e8cc2fa223184ac96d355b206e45a6ab3e5718
parent 41bc8118433f6b88a4ac9d720678dcd7f69d103d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 20:43:18 -0400
plans: brand + themes — the Found-line mark, the subject-split wordmark, and base × accent reader themes (S0 → S1 ‖ S2 → S3)
Operator decisions of 2026-09-25 are final: mark D, ink dark base, Anilyzer
Sakura / Jasolyzer Green / Archilyzer keeps Signal, all five theme families
retire. Contrast-corrected light/sepia accents in this plan beat the canvas.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
1 file changed, 414 insertions(+), 0 deletions(-)
diff --git a/plans/brand-and-themes.md b/plans/brand-and-themes.md
@@ -0,0 +1,414 @@
+# 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`.
+
+Design canvas: https://claude.ai/artifact/UsUxwgRkP5a3m4jZXucAvG (boards *Family*, *In context*,
+*Themes · base × accent*, and the rejected directions A–C). Read it with Artifact
+`action:"read"`; republish by staging under the primary checkout's gitignored
+`node_modules/.brand-canvas/project/` (Artifact publish only accepts paths inside the repo).
+**Where this plan and the canvas differ, the plan wins** — notably the contrast-corrected
+on-light and on-sepia accent values below.
+
+## What the operator asked for (2026-09-25)
+
+1. One logo and brand for Archilyzer and every official child site.
+2. A reader theme made of two independent choices: a **base** (Light / Sepia / Dark, plus
+ System) and an **accent**. Every site defaults to its own accent; a reader can pick any other.
+
+## Decisions (final)
+
+- **Mark D · Found line.** Four transcript lines on a rounded square; the second is lit and
+ carries a play head.
+- **The wordmark splits on the subject's name,** not on "lyzer".
+- **The dark base is warm ink,** not graphite.
+- **Accent swaps:** Anilyzer takes Sakura and Jasolyzer takes Green, because Archilyzer keeps
+ its teal ("Signal").
+- **All five theme families retire:** base, archive, selenized, swiss and archilyzer.
+- **No new dependencies expected.** `rsvg-convert`, `magick` and `inkscape` are on the machine,
+ and `next/og` bundles satori and resvg. sharp's native build failed in the worktree; nothing
+ here needs it.
+
+## The design (source of truth)
+
+### Mark
+
+Everything is in a 512 viewBox. The shapes, back to front:
+
+| Part | Shape | Colour |
+|---|---|---|
+| ground | rect, `rx=112` | ground |
+| line 1 | rect x112 y128 w288 h44 rx22 | dim |
+| play head | polygon `112,210 172,234 112,258` | lit |
+| line 2 (the found line) | rect x188 y212 w212 h44 rx22 | lit |
+| line 3 | rect x112 y296 w232 h44 rx22 | dim |
+| line 4 | rect x112 y380 w152 h44 rx22 | dim |
+
+Icon palettes:
+- **Child site:** ground `#0c0a08`, dim `#3b3327`, lit = the site accent's on-dark value.
+- **Archilyzer** (homepage, hub, editor): ground `#151b20`, dim `#3f4c56`, lit `#e7edf1`
+ (bone). The parent mark is achromatic.
+
+Icon variants:
+- `any`: `rx=112`.
+- `maskable`: full-bleed ground, the mark scaled 0.8 about (256,256).
+- `apple`: full-bleed ground at scale 1.
+
+### Wordmark
+
+Archivo at `font-stretch:118%`. The lead is weight 720 in `--foreground`; the suffix is weight
+380 in `--muted-foreground`. Two adjacent spans with no whitespace between them; the link's
+accessible name is the full title.
+
+| Site | Lead | Suffix |
+|---|---|---|
+| Archilyzer | Archi | lyzer |
+| Jeralyzer | Jer | alyzer |
+| Rekietalyzer | Rekieta | lyzer |
+| Hasanalyzer | Hasan | alyzer |
+| Anilyzer | Ani | lyzer |
+| Bonnellyzer | Bonnell | yzer |
+| Jasolyzer | Jaso | lyzer |
+
+### Accents
+
+The on-light and on-sepia values are contrast-corrected: each must reach ≥4.5:1 against its
+ground and with white ink. A unit test enforces it.
+
+| id | Name | Site default | On dark | On light | On sepia |
+|---|---|---|---|---|---|
+| `signal` | Signal | Archilyzer (homepage, hub, editor) | `#5fa8a0` | `#2e7b73` | `#2b756e` |
+| `brass` | Brass | jeralyzer | `#e3b15c` | `#95661a` | `#8e6119` |
+| `vermilion` | Vermilion | rekietalyzer | `#ec7a52` | `#b3431f` | `#b3431f` |
+| `violet` | Violet | hasanalyzer | `#b49cf2` | `#6a4bc4` | `#6a4bc4` |
+| `sakura` | Sakura | anilyzer | `#ee8fb5` | `#a83a6a` | `#a83a6a` |
+| `blue` | Blue | bonnellyzer | `#74a9f2` | `#2d5fb8` | `#2d5fb8` |
+| `green` | Green | jasolyzer | `#7cc46a` | `#3f7a2c` | `#3d772b` |
+
+### Bases
+
+Each base declares all 48 colour tokens listed in `homepage/e2e/theme.spec.ts:63-112`, plus
+`color-scheme`.
+
+| Base | Source | Values |
+|---|---|---|
+| Light | today's `archilyzer` light block | bg `#f3f6f7`, surface `#e8edef`, fg `#161c21`, muted-fg `#55646e`, border `#d2dade`, border-strong `#aeb9c0`, faint `#78868f` |
+| Sepia | new | bg `#f4ecd8`, card `#faf4e6`, fg `#33281a`, muted-fg `#6b5c43`, border `#dccdaa` |
+| Dark | today's `archive` dark (ink) | bg `#0c0a08`, surface `#141009`, fg `#efe7d8`, muted-fg `#a39a86`, border `rgba(233,220,197,.1)` |
+
+- **Sepia** derives its status, panel and chart values from the archive paper block, darkened
+ for a light ground.
+- **Dark** adds the `--destructive-soft` and `--state-gone(-soft)` it lacks today.
+- **Charts do not follow the accent:** each base has its own fixed `--chart-1..5`. Re-validate
+ chart-3 against `--state-gone` with the dataviz skill's validator (FACTS.md ~6442 records a
+ real past collision), and keep chart-1 ≠ Signal.
+
+### Type and radius
+
+Three faces everywhere:
+- Archivo as `--font-display`, with the `wdth` axis. `.font-display` gets `font-stretch:112%`.
+- IBM Plex Sans as `--font-sans`, weights 400–700, normal + italic.
+- IBM Plex Mono as `--font-mono`.
+
+`--radius: 0.375rem` on `:root`.
+
+## Slices
+
+```
+S0 palette + data ──┬──> S1 brand (mark, icons, wordmark) ──┐
+ └──> S2 themes (tokens, runtime, picker) ┴──> S3 integrate + gates ──> operator rollout
+```
+
+Cadence: the planner/reviewer orchestrates; one Opus general-purpose implementer per slice
+writes the code, under [`tools/implementer-rules.md`](tools/implementer-rules.md). S1 and S2 run
+as two parallel implementers, each in its own worktree cut from S0's tip
+(`pnpm wt add brand/mark --from brand/found-line`, `pnpm wt add brand/themes --from brand/found-line`).
+Review fixes go back to the same agent. Sonnet or Haiku do exploration and extraction.
+
+**Conflict avoidance between S1 and S2:**
+- In `export/app/layout.tsx` and `homepage/app/layout.tsx`, S1 edits only the `icons` metadata;
+ S2 edits only the theme constants, `generateViewport` and the `RootLayout` `<html>`.
+- `Header.tsx` and `Footer.tsx` belong to S1; `ThemeMenu`, `MobileMenu` and `ThemeToggle` belong
+ to S2.
+- S1 colours the header mark with `var(--brand-mark, var(--brand))`, so it works before S2 lands.
+- S1 puts new assertions in a new `export/e2e/brand.spec.ts`; S2 owns
+ `site-branding.spec.ts:75-86`.
+- Whichever of S1/S2 merges second merges `main` and re-gates.
+- Release 9 slice C2 (`one-core/c2-federated-search`) is in flight on the hub search
+ components; neither slice touches `SearchResults.tsx`, `FiltersPanel.tsx` or
+ `ArchiveShelf.tsx` (they receive hexes and keep working unchanged).
+
+### S0 — palette + data (`brand/found-line`, alone)
+
+**New `common/lib/brand.ts` (pure):**
+- `ACCENT_IDS` and an `ACCENTS` table, `{id, name, onDark, onLight, onSepia}` per accent.
+- `DEFAULT_ACCENT = "signal"`.
+- `BASE_GROUNDS = {light: "#f3f6f7", sepia: "#f4ecd8", dark: "#0c0a08"}`.
+- `ICON_PALETTES` for child sites and for Archilyzer.
+- The `MARK` shape list and `markSvg(palette, {variant})`.
+- `splitWordmark(title, lead?)`.
+
+`common/lib/project.ts` gains `PROJECT_WORDMARK_LEAD = "Archi"`.
+
+**`common/lib/accent.ts`:**
+- Keep `parseAccent`.
+- Add `parseAccentSetting(v)`: an accent id, a `"#rrggbb"`, or undefined.
+- Add `resolveAccent(v)`: `{id | "custom", light, sepia, dark}`. A custom hex is darkened or
+ lightened per ground until it reaches 4.5:1.
+- Add `accentHex(v)`. The published value is always a hex: an id becomes its on-dark value.
+- Add `customAccentVars()`.
+- `siteAccentVars` is deleted in S2.
+
+**`common/lib/siteSchema.ts`:**
+- `accent` parses through `parseAccentSetting` (around :238, :302, :321), with new doc text
+ (:110).
+- New optional `wordmarkLead`, after `headerTitle`. Stored only when it is a proper prefix of
+ `headerTitle`; needs type, `SITE_FIELD_DOCS`, schema and `siteToDisk` entries.
+- Regenerate SITE.md: `pnpm --filter yt-dlp-transcript-common exec tsx bin/file-schemas-docs.ts`,
+ checked with `--check`.
+
+**The published accent stays a hex.** Third-party hubs read other sites' `/site.json`, so these
+route through `accentHex`:
+- `common/lib/siteDescriptor.ts:~87`
+- `common/bin/compose-hub.ts:~94`
+- `common/lib/homepageSummary.ts:~431`
+
+**Editor form** (`editor/app/sites/components/SiteForm.tsx:229-234`,
+`editor/app/sites/actions.ts:~45-51,169-191`):
+- "Brand accent" becomes a native radio group of the 7 swatches plus "Custom", with a hex field.
+- A "Wordmark lead" field. The single writer is `writeSite` in `common/lib/site.ts`.
+- `Field` puts the hint inside the `<label>`, so the new labels and hints must not contain
+ "header title", "site title", "site id" or "public url" — `sites-crud`'s `getByLabel` would hit
+ strict-mode violations.
+- `hubSite()` (`export/app/lib/site.ts:~29`) sets `wordmarkLead: PROJECT_WORDMARK_LEAD`.
+
+**Tests:** `brand.test.ts` (contrast ≥4.5 everywhere, `markSvg` geometry); `accent.test.ts`; a
+siteSchema round-trip with an id and `wordmarkLead`; siteDescriptor publishes a hex for an id;
+`fileSchemaDocs.test.ts` passes on the regenerated SITE.md.
+
+**e2e:** editor `sites-crud` and `branding` (the root `pnpm e2e` is the editor suite); export
+`site-branding`; `pnpm --filter export run e2e:hub`.
+
+### S1 — mark, icons, wordmark (`brand/mark`)
+
+**Step 1 is a spike, timeboxed to about an hour:**
+1. Seed the `export/public` symlinks (implementer-rules.md, under `sh`).
+2. Build a PNG icon via `app/icons/[file]/route.ts`.
+3. `pnpm --filter export exec next build`.
+4. Check `out/icons/icon-192.png`: PNG magic, a 192×192 IHDR, and look at it.
+5. Check `out/favicon.ico` starts with `00 00 01 00`.
+6. Repeat for the homepage.
+
+**If the spike fails:**
+- The PNG route fails: call the same renderer from compose-site / compose-hub /
+ compose-homepage and write into `public/icons`. The last resort is `rsvg-convert`.
+- Only the ICO fails: check in a `public/favicon.ico` family mark generated once by
+ `common/bin/brand-icons.ts`. Next reserves `/favicon.ico` as a metadata route
+ (`is-metadata-route.js:114`).
+
+**Static route handlers are safe under static export.** Under `output:"export"` a GET route
+handler with `generateStaticParams` is copied byte-for-byte to its exact path
+(`next/dist/export/index.js:~722-737`). `app/icon.tsx` is unusable: its URLs are always hashed.
+
+**`common/lib/brandIcons.ts`:**
+- `ICON_FILES`: `icon.svg`, `maskable.svg`, `icon-32.png`, `icon-192.png`, `icon-512.png`,
+ `maskable-512.png`, `apple-touch-icon.png`.
+- `renderIconPng()` — `ImageResponse` from `next/og` on the node runtime, with an `<img>` SVG
+ data URI via `createElement`.
+- `pngToIco()` — a 6-byte header, 16-byte entries, then the PNG payloads.
+
+**Routes:** `export/app/icons/[file]/route.ts` and a homepage twin.
+- `dynamic = "force-static"`, `dynamicParams = false`; never read the request.
+- The palette is Archilyzer in hub mode (`instanceMode()`), otherwise the site's resolved accent
+ via `currentSite()`.
+- Delete `export/public/icons/*`, `homepage/public/icons/*` and both `app/favicon.ico`.
+- Metadata declares `icon.svg` (type `image/svg+xml`), `icon-32.png`, 192, 512 and apple.
+- The homepage OG image keeps `/icons/icon-512.png`.
+
+**Manifest** (`export/app/manifest.ts`): `theme_color = BASE_GROUNDS.dark`; `background_color` is
+the icon ground; add `icon.svg` with sizes "any". `pwa.spec` requires the 192/512/maskable
+entries and the four PNG paths.
+
+**Service workers:** in `export/service-worker/site-sw.js:~28` and `sw-hub.js:~20`, rename
+**only** the `SHELL` cache to `shell-v2`. Bumping `VERSION` would make activate delete readers'
+offline channel downloads.
+
+**Components:**
+- New `common/components/BrandMark.tsx`: inline SVG, `aria-hidden`, `focusable="false"`, colours
+ through `style`, not `fill="var()"`.
+- New `common/components/Wordmark.tsx`.
+- `export/app/components/Header.tsx:68-73`: the rotated square becomes an ink tile plus
+ `var(--brand-mark)` — the header follows the reader's accent while the favicon keeps the site
+ default. The Wordmark uses `site.wordmarkLead`. The hub reuses this header and gets the
+ Archilyzer mark.
+- `homepage/app/components/Header.tsx:39-51`: the CSS triangle becomes the bone mark, and
+ "Archilyzer" becomes the split wordmark, no longer tracked uppercase. Keep
+ `aria-label="Archilyzer home"`.
+- `export/app/components/Footer.tsx`: a small Archilyzer mark before the credit, outside the
+ link. The link's name stays exactly "Archilyzer".
+- Editor sidebar (`editor/app/layout.tsx:~133-144`): a mark beside `adminTitle`, not split.
+- Editor favicon: a static `editor/app/icon.svg`, with a parity test against `markSvg`.
+
+**Fixture:** `"wordmarkLead":"Fixture"` in `export/e2e/fixtures/sites/testsite/site.json`.
+
+**Tests:** new `export/e2e/brand.spec.ts` (split spans, link name, mark present, footer credit);
+`pwa.spec.ts:30-42` adds content-type, PNG magic, `icon.svg` and `favicon.ico`; the hub's
+`/icons/icon.svg` contains `#151b20` / `#e7edf1`; unit tests for the ICO header, and the PNG IHDR
+if `next/og` runs under tsx.
+
+### S2 — base × accent (`brand/themes`)
+
+**`common/styles/tokens.css`:**
+- Keep `@custom-variant dark (&:where(.dark, .dark *))` (:28). `.dark` is on `<html>` iff the
+ resolved base is dark, so the 47 `dark:` usages keep working and sepia styles as light.
+- Delete every `[data-theme=…]` block, the `.dark{}` token block and the per-family font/radius
+ rules (:99-141).
+- Base blocks, each declaring all 48 tokens: `:root, html[data-base="light"]`,
+ `html[data-base="sepia"]`, `html[data-base="dark"]`. Each also declares:
+ - `--swatch-<id>`: that ground's value for each of the 7 accents;
+ - `--swatch-custom: var(--accent-custom-<base>, var(--swatch-signal))`;
+ - `--brand: var(--swatch-signal)` as the default;
+ - `--brand-strong`: `color-mix` toward white on dark, toward black on light/sepia;
+ - `--brand-soft`: `color-mix` at 16% on dark, 12% on light/sepia;
+ - `--brand-ink`: `#0c0a08` on dark, `#fff` on light/sepia.
+- **After** the base blocks (same specificity, so later wins):
+ `html[data-accent="<id>"]{--brand:var(--swatch-<id>); --brand-mark:<onDark>}` for each of the
+ 7, and `html[data-accent="custom"]{--brand:var(--swatch-custom); --brand-mark:var(--accent-custom-dark)}`.
+- `--primary` stays **neutral**, not the accent: light `#202a31`, sepia `#33281a`, dark
+ `#efe7d8`, each with a ground-coloured foreground. `--ring: var(--brand)`.
+
+**`common/styles/fonts.ts`:** Archivo, Plex Sans and Plex Mono only — Source Serif, Source Sans,
+Source Code and JetBrains Mono go. Update the comments in both `globals.css` files.
+
+**`common/components/themeConfig.ts`:**
+- `BASE_KEY = "ytdlp-tb:base"`; `ACCENT_KEY` now stores an accent id; `LEGACY_THEME_KEY` /
+ `LEGACY_MODE_KEY` are the old theme/mode keys.
+- `ThemeBase = light | sepia | dark | system`.
+- `REQUIRED_TOKENS`, which the homepage spec imports.
+- A pure `migrateLegacy({theme, mode})`, after which both legacy keys are deleted:
+
+ | Stored mode | Result |
+ |---|---|
+ | light | `light`, or `sepia` when the old theme was `archive` |
+ | dark | `dark` |
+ | system | `system` |
+ | absent | nothing stored |
+
+- `buildThemeScript({defaultBase})`.
+
+**Pre-paint `ThemeScript.tsx`** (replacing :27-56), in this order:
+1. Migrate if needed.
+2. Validate the base, falling back to the default.
+3. Set `data-base` to the resolved light/sepia/dark and toggle `.dark`.
+4. Set `data-accent` only from a valid stored id; otherwise the server-rendered default stays.
+5. Update `meta[name=theme-color]`.
+6. Last: `data-theme-ready="1"`, the e2e no-flash marker.
+
+A unit test runs the script string in `node:vm` over the whole migration matrix.
+
+**`ThemeProvider.tsx`:**
+- Props `{defaultBase = "system", siteAccent = DEFAULT_ACCENT}`.
+- Context `{base, resolvedBase, isDark, accent, siteAccent, setBase, cycleBase, setAccent}`.
+- A `useLayoutEffect` re-asserts `data-base`, `.dark`, `data-accent` and theme-color. Hydration
+ can reset `<html>` attributes; without it the site default overwrites a reader's pick.
+- A live `systemDark` state from the `matchMedia` listener (today `isDark` goes stale after an
+ OS change).
+- `setAccent(<site default>)` **removes** the stored key, so the reader follows the site default.
+- `cycleBase`: system → light → sepia → dark.
+
+**Consumers:**
+- `ThemeMenu.tsx`: keeps `aria-label="Choose theme"`. A "Base" radio group of `menuitemradio`s
+ named exactly System, Light, Sepia and Dark. An "Accent" radio group: a swatch dot
+ (`background: var(--swatch-<id>)`), the name, and a visible "default" tag on the site's own
+ option. A custom-hex site adds a first "Site colour" option.
+- `ThemeToggle.tsx`: labels "Switch to …" (the `/switch to/i` selector keeps working); icons
+ Monitor / Sun / BookOpen / Moon.
+- `export/app/components/MobileMenu.tsx:~121-141`: native radio groups "Base" and "Accent".
+- `common/components/ui/sonner.tsx:17`: `theme={isDark ? "dark" : "light"}`.
+
+**Layout defaults:**
+
+| App | Base | Accent | Where |
+|---|---|---|---|
+| Export site | system | from site.json | `<html data-accent>` rendered server-side; a custom hex adds the inline `--accent-custom-*` style |
+| Hub | dark | signal | export layout |
+| Homepage | dark | signal | homepage layout |
+| Editor | system | signal | no layout change |
+
+**Browser chrome colour** (`generateViewport`): sites get
+`[{media: light, BASE_GROUNDS.light}, {media: dark, BASE_GROUNDS.dark}]`; the hub and homepage
+get `BASE_GROUNDS.dark`. `FALLBACK_THEME_COLOR`, `HUB_THEME_COLOR` and the homepage
+`THEME_COLOR` go.
+
+**Tests:**
+
+| Spec | Change |
+|---|---|
+| export `theme.spec.ts` | Rewrite: default `data-base`, live `emulateMedia`, cycling through 4 states, persistence across a `commit` reload plus `data-theme-ready`. |
+| export `theme-family.spec.ts` | Delete; new `theme-accent.spec.ts`. Pick Violet: it persists and the computed `--brand` is correct. Picking the default removes the key. Also pick Sepia. |
+| export `site-branding.spec.ts:75-86` | `data-accent="custom"`, and the computed `--brand` is `#cc3366` on light. |
+| export `responsive.spec.ts:~95` | "Archive" becomes "Sepia". |
+| homepage `theme.spec.ts` | Dark + signal defaults; `REQUIRED_TOKENS` complete across all 3 bases; the backgrounds differ. |
+| editor `theme.spec.ts` | Seed `BASE_KEY`; migration cases selenized+dark → dark (legacy keys removed) and archive+light → sepia. |
+| unit | Tokens completeness and parity: parse tokens.css; every base declares every token; `--swatch-*` / `--brand-mark` equal `ACCENTS`. |
+
+**Leftover copy:** `homepage/content/docs/operate.md:~84` and the comment at
+`export/playwright.config.ts:~29` still describe the old accent and the committed icons.
+
+### S3 — integrate (after S1 + S2 merge to `main`)
+
+- Full gates.
+- Verified facts appended to `plans/FACTS.md`; `plans/STATE.md` rewritten.
+- `export/CHANGELOG.md` and `homepage/CHANGELOG.md` `[Unreleased]` bullets.
+- The release recorded in the next `plans/release-N.md`.
+- The operator runbook as **generated HTML** (operator rule).
+
+## Verification
+
+**Gates, every slice:**
+- `pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit`.
+- `pnpm --filter yt-dlp-transcript-common test`.
+- Editor unit: `pnpm exec tsx --test "app/**/*.test.ts"` in `editor/`.
+- `pnpm test:scripts`; mcp tests.
+- `pnpm --filter export exec next build`, the homepage `next build` and the editor `next build`.
+- `ls out/icons` plus the PNG/ICO magic checks.
+
+**e2e** is queue-locked machine-wide; run it detached (`setsid nohup … &`). The parent session
+holds a Monitor; a subagent waits in foreground loops of ≤100 s.
+- `pnpm --filter export run e2e`, `e2e:hub`, `e2e:2origin`.
+- `pnpm --filter homepage run e2e`.
+- The root `pnpm e2e` (the editor suite).
+
+**Visual check:** build one site, serve `out/`, and screenshot the header, the picker open, and
+each base × two accents at 390 px and 1280 px (use `localhost`, not 127.0.0.1). Compare with the
+canvas's *In context* and *Themes* boards.
+
+**Caution:** `export/out` belongs to the checkout, and an e2e run can start a real deploy
+(release-7 lessons). Never deploy from a worktree without the operator.
+
+## As shipped
+
+(Each slice appends its "### Slice S<n>, as shipped" record here.)
+
+## 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:
+
+ | Site | Accent | Wordmark lead |
+ |---|---|---|
+ | jeralyzer | brass | Jer |
+ | rekietalyzer | vermilion | Rekieta |
+ | hasanalyzer | violet | Hasan |
+ | anilyzer | sakura | Ani |
+ | 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`).