commit eac2db7b8625628fbba264739ea92ec2ec08cf48
parent 7b603d8cf8dbb6d0a64c1e976f630d849e3967ed
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 22:07:18 -0400
plans: brand + themes — slice S1 as shipped (mark, icons, wordmark); editor, export and homepage CHANGELOGs
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
4 files changed, 215 insertions(+), 0 deletions(-)
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- **Every site, the hub, the homepage and the editor wear the new Found-line mark, and a site's header splits its wordmark.** The mark is four transcript lines on a rounded square, the second lit and carrying a play head. The favicon, app icons and touch icon are no longer committed files: each build draws them from the mark and writes `/icons/icon.svg`, `maskable.svg`, `icon-32.png`, `icon-192.png`, `icon-512.png`, `maskable-512.png`, `apple-touch-icon.png` and `/favicon.ico` (16, 32 and 48 px). A site's icons are an ink tile lit with its own accent (Signal when it sets none; a custom colour is lightened until it reads on the tile). The hub, the homepage and the editor use the parent mark, bone on slate. The site header shows the mark and the header title split at the site's **Wordmark lead**, the lead heavy and the rest light (Jer|alyzer); with no lead the whole title is heavy. The header mark's lit line will follow the reader's accent once the theme picker lands; the favicon keeps the site's. The footer's "Built with Archilyzer" has the small parent mark in front of it, outside the link. The homepage header shows the parent mark and "Archi|lyzer", no longer in spaced capitals. An installed site's title bar is the dark theme's ground (`#0c0a08`) and its splash the icon's tile, where it used to fall back to blue for a site with a named accent. The service workers' shell cache is renamed to `shell-v2`, so an installed app drops the old icons; readers' offline channel downloads are kept. The editor's sidebar shows the parent mark beside the admin title, and the editor now has a favicon. Needs a rebuild and deploy of every site, the hub and the homepage.
- **A site's accent is one of seven named colours or a custom one, and a site can split its wordmark.** The site form's **Brand accent** is now a row of swatches: Signal (the family default), Brass, Vermilion, Violet, Sakura, Blue and Green, plus **Custom**, whose colour goes in the **Custom hex** field (typing there picks Custom). `site.json` stores the accent's id (`"accent": "brass"`) or the hex. Picking Signal stores no key, because no key means Signal. A custom colour is darkened or lightened for each reading theme until it reads at 4.5:1; the seven named colours already do. A new **Wordmark lead** field names the heavy first part of the header wordmark, e.g. `Jer` for Jeralyzer. The form refuses a lead that is not how the header title starts, and `site.json` keeps `wordmarkLead` only when it is. What other hubs read does not change: `/site.json`, the hub's `hub-sites.json` and the homepage summary still carry a hex, and an id goes out as its colour on dark. `SITE.md` lists both keys. The header, icons and theme picker that use them come with the rest of the brand work; until then a site with a named accent is tinted with that colour, as a custom hex is today.
- **The hub and the homepage drop their subtitles, and both carry a Ko-fi link.** The headings are in Title Case on both pages: **Official Instances**, **Archives You Added** (hub) and **What It Does** (homepage). The line "The archives I run. Anyone can run their own." is gone from both, and the official-instance cards no longer show the site's description under its name; the four figures stay. The footer of the homepage and of the hub has a plain link reading "Ko-fi" to `https://ko-fi.com/archilyzer`. A published site's footer does not: an archive someone else hosts never carries it. Needs a rebuild and deploy of the hub and the homepage.
- **A job that waited in a queue no longer ends `failed` after doing its work.** A sync, download or other job queued behind another on the same platform ran its final page refresh outside any request, where Next refuses it, so the job read `failed` and `pnpm ops … --wait` exited 1 even though the work was done (the teamrcn sync on 2026-09-25). The refresh is now skipped there with one warning in the server log; the pages re-read disk on their next load anyway.
diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md
@@ -1,5 +1,8 @@
# Changelog
+## [Unreleased]
+- **The Found-line mark, and a split wordmark.** The site header's rotated square is now the family mark — four transcript lines on an ink tile, the second lit with the site's accent and carrying a play head — and the header title splits at the site's `wordmarkLead` (heavy lead, light rest: Jer|alyzer). The hub wears the parent mark, bone on slate, and splits as Archi|lyzer. The footer credit has the small parent mark before "Built with"; the link's name is still exactly "Archilyzer". The icons are no longer committed: `app/icons/[file]/route.ts` and `app/favicon.ico/route.ts` render them from `common/lib/brand.ts` at build (static export writes `out/icons/*` and `out/favicon.ico`), lit with the site's accent. `<head>` links `icon.svg` first, then `icon-32.png`, 192, 512 and the touch icon. The manifest lists `icon.svg` (sizes "any"); its `theme_color` is the dark base's ground `#0c0a08` and its `background_color` the icon's tile. Both service workers rename only their shell cache (`shell-v2`) so installed apps drop the old icons; the data caches, and readers' offline downloads, are kept.
+
## [0.8.7] - 2026-08-12
- **Every archive now says what built it, and stopped shipping its own copy of the source.** The footer carried a `code.tar.gz` link on every site — `create-archives.sh` wrote a tarball into `export/public/` and each deployed archive served its own duplicate of the whole workspace (verified live: `200 application/gzip` on jeralyzer). That link is **gone**, replaced by **"Built with [Archilyzer](https://archilyzer.pages.dev)"** beside the social links. The source is now published once, centrally, with a checksum and a commit — rather than N times, undated, with no way to tell two copies apart. The back-link is deliberately **ungated by instance mode** (a hub is Archilyzer too, and a credit that appeared on sites but not hubs would make them look like different software), **independent of any configured hub URL** (that is the operator's family link, a different relationship), and opens **in the same tab**, matching the header's hub link — only social icons open new ones. *"Built with"* sits outside the anchor so the accessible name is exactly `Archilyzer`. One knock-on: the **Downloads** eyebrow used to be unconditional because that link always followed it; it now appears only when the transcript-archive or offline link is actually there, instead of captioning empty space.
- **`corpus.json` and `llms.txt` name the software that produced them.** Both the per-site and the hub builders stamp a `generator` string, and both `llms.txt` renderings end with a `Generated by …` line — so anything reading an archive machine-side can find the tool that built it. **The corpus spec version is deliberately unchanged at 3**, and a test asserts it: every prior bump announced a new *fetchable layer*, where a client that ignored it would miss retrievable data. An informational credit breaks no reader, so forcing every consumer to re-evaluate compatibility for a byline would be the wrong trade. The reasoning is recorded in the version-history comment rather than left for the next person to reconstruct.
diff --git a/homepage/CHANGELOG.md b/homepage/CHANGELOG.md
@@ -1,5 +1,14 @@
# Homepage Changelog
+## [Unreleased]
+
+- **The Found-line mark.** The header's CSS triangle is now the family's parent mark (bone
+ on slate) and "ARCHILYZER" is the split wordmark "Archi|lyzer" — heavy lead, light
+ suffix, no longer tracked uppercase; the link is still named "Archilyzer home". The
+ icons and `/favicon.ico` are no longer committed: `app/icons/[file]/route.ts` and
+ `app/favicon.ico/route.ts` render them from `common/lib/brand.ts` at build, and
+ `<head>` gains `icon-32.png`. The OG image is still `/icons/icon-512.png`.
+
## 2026-08-12
- **This package is the project's site now.** It was a shelf of one operator's archives
diff --git a/plans/brand-and-themes.md b/plans/brand-and-themes.md
@@ -557,6 +557,208 @@ operator picks an accent or a lead.
- The worktree is port block **#1** (editor 3101, test 3111, export 3110), not #2:
`pnpm wt list` sorts, and `diet-series` is #2.
+### Slice S1, as shipped — mark, icons, wordmark (2026-09-25)
+
+Branch `brand/mark` off S0's tip `02295a94` (worktree `../brand-mark`, port block #2). S1 draws the
+Found-line mark everywhere the family shows itself: every icon file is rendered from `markSvg` at
+build instead of committed, the site header is the mark plus the split wordmark, the hub, homepage
+and editor wear the parent mark, and the footer credit carries it. No `common/styles/**`, theme
+component, `MobileMenu`, theme spec, `site-branding` spec or C2 file was touched. In the two layouts
+only the `icons` metadata changed.
+
+**The spike passed on the plan's primary path in about 15 minutes; no fallback was taken**
+(`$T/s1-spike.md`).
+- `next/og` runs under tsx, so the unit tests check the PNG IHDR as well as the ICO directory. A
+ probe script needs `.mts`: tsx refuses top-level await in a `.ts` it compiles to CJS.
+- `app/icons/[file]/route.ts` (force-static, `dynamicParams = false`, `generateStaticParams` over
+ `ICON_FILES`) and `app/favicon.ico/route.ts` (force-static) emit the right files for both export
+ and homepage:
+ - `out/icons/*`: icon-192 is `8950 4e47 0d0a 1a0a … 4948 4452 0000 00c0 0000 00c0`, a 192×192
+ IHDR;
+ - `out/favicon.ico`: starts `0000 0100 0300`, three PNG entries.
+ - Both were looked at.
+- The favicon route is missing from the build's route table, because Next files `/favicon.ico` as a
+ metadata route, but it IS emitted.
+- `next dev`, which every e2e server runs, serves all of them too. `/icons/nope.png` is a 404.
+
+**`common/lib/brandIcons.ts` (new, server-only).**
+- `ICON_FILES`: the seven `/icons/` names, each with its format, variant, size and content type.
+ `iconFile(name)` looks one up.
+- `ICON_METADATA`: the one `<head>` link list both layouts use: `icon.svg` first, then `icon-32.png`,
+ 192, 512, and apple.
+- `FAVICON_SIZES`: 16, 32 and 48.
+- `siteIconPalette(accent)` = `childIconPalette(resolveAccent(accent).dark)`.
+- `renderIconPng(palette, {variant, size})`: an `ImageResponse` of a single `<img>` whose `src` is a
+ base64 data URI of `markSvg`, built with `createElement`, so the raster IS the SVG's geometry.
+- `pngToIco(images)`: a 6-byte ICONDIR, 16-byte entries (256 is written as 0), then the PNG payloads
+ verbatim. `renderFaviconIco(palette)` packs the three sizes.
+- `renderIconFile(name, palette)` returns null for any name not in the set; `iconResponse(body, type)`
+ wraps the result.
+
+**Routes.**
+- Export: `app/icons/[file]/route.ts` + `app/favicon.ico/route.ts`. The palette is `iconPalette()`
+ (`export/app/lib/brand.ts`): `ICON_PALETTES.archilyzer` when `instanceMode() === "hub"`, else
+ `siteIconPalette(currentSite().accent)`.
+- Homepage: the same two routes, always the parent palette.
+- Neither reads the request.
+- Deleted: `export/public/icons/*`, `homepage/public/icons/*`, and both `app/favicon.ico`.
+- Next no longer adds its own `/favicon.ico` link, which it did for the static file, so `<head>` is
+ exactly `ICON_METADATA`.
+- The homepage OG image is still `/icons/icon-512.png`.
+
+**Manifest** (`export/app/manifest.ts`):
+- `theme_color` = `BASE_GROUNDS.dark` (`#0c0a08`) on sites and the hub;
+- `background_color` = `iconPalette().ground` (`#0c0a08` on a site, `#151b20` on the hub);
+- a new first icon `{src:"/icons/icon.svg", sizes:"any", type:"image/svg+xml", purpose:"any"}`;
+- the 192/512/maskable PNG entries kept.
+
+The hex-only `parseAccent` fallback, which S0 flagged at `:19`, is gone.
+
+**Service workers.** `site-sw.js` and `sw-hub.js`: `const SHELL = "shell-v2"`, with a comment on why.
+`VERSION` stays `"v1"`, so `pages-v1` / `meta-v1`, the readers' offline downloads, survive activate.
+`contract.test.ts` still reads both files green.
+
+**Components.**
+- `common/components/BrandMark.tsx`:
+ - maps `MARK` (the list `markSvg` walks) to `<rect>`/`<polygon>`;
+ - `aria-hidden`, `focusable="false"`, `data-brand-mark`, and `data-tone` on each shape;
+ - every fill goes in `style`, so a slot can be `var(…)`;
+ - its palette type is `BrandMarkPalette` (any CSS colour).
+- `common/components/Wordmark.tsx`:
+ - `splitWordmark(title, lead)`: the lead at weight 720 in `var(--foreground)`, the suffix at 380 in
+ `var(--muted-foreground)`, `font-stretch: 118%`;
+ - two adjacent INLINE spans with no whitespace node, so the link's name is the title exactly;
+ - `data-wordmark(-lead|-suffix)` hooks;
+ - the face is `var(--font-grotesk, var(--font-display))`, which is Archivo both before S2 (the
+ grotesk variable) and after (S2 makes Archivo `--font-display`). S3 can drop the first term.
+- Export `Header.tsx`:
+ - `<BrandMark palette={headerMarkPalette()} className="size-7">` + `<Wordmark title={site.headerTitle}
+ lead={site.wordmarkLead}>`, truncating as before;
+ - `headerMarkPalette()` is the child ink tile with `lit: "var(--brand-mark, var(--brand))"` on a
+ site, and the parent mark on the hub.
+- Export `Footer.tsx`: `<span class="inline-flex …"><BrandMark archilyzer size-4/><span>Built with
+ <a>Archilyzer</a></span></span>`. The mark is outside the link.
+- Homepage `Header.tsx`: the parent mark + `Archi|lyzer` (`PROJECT_WORDMARK_LEAD`), no uppercase or
+ tracking; `aria-label="Archilyzer home"` kept.
+- Editor sidebar (`editor/app/layout.tsx`): the parent mark (`size-8`) before the title block,
+ outside the title link. The title is not split.
+- **Editor favicon:** `editor/app/icon.svg` = `markSvg(ICON_PALETTES.archilyzer)`. Next links it as
+ `/icon.svg?icon.<hash>.svg`. `editor/app/lib/brandIcon.test.ts` pins it byte for byte.
+ **There was no `editor/app/favicon.ico`** (the editor had no favicon at all), so nothing was
+ deleted.
+
+**Fixture:** `"wordmarkLead": "Fixture"` in `export/e2e/fixtures/sites/testsite/site.json`.
+
+**Tests.**
+- New `export/e2e/brand.spec.ts` (5 tests):
+ - the header link `{name:"Fixture Header", exact}` → `href="/"`, exactly two inline spans
+ `Fixture`/` Header` at 720/380, and an aria-hidden, non-focusable mark;
+ - the header mark's ground and dim are the ink tile, and its lit fill equals whatever
+ `var(--brand-mark, var(--brand))` resolves to, so it survives S2;
+ - the footer mark is the parent mark, the credit link has no svg, and `svg + span` holds the credit;
+ - `<head>` icon links in order;
+ - `/icons/icon.svg` and `maskable.svg` = `markSvg(childIconPalette(resolveAccent("#cc3366").dark))`
+ (`#d14574`).
+- `pwa.spec.ts`:
+ - the manifest carries the `icon.svg` entry, `theme_color` = `BASE_GROUNDS.dark` and
+ `background_color` = the child ground;
+ - each of the five PNGs has `content-type: image/png`, the PNG magic and its IHDR size;
+ - both SVGs have `image/svg+xml`;
+ - `favicon.ico` is `image/x-icon`, starts `00 00 01 00`, and its first entry is a PNG;
+ - `/icons/icon-64.png` is a 404.
+- New `export/e2e-hub/brand.spec.ts` (2 tests):
+ - `/icons/icon.svg` contains `fill="#151b20"` and `fill="#e7edf1"` and equals the parent markSvg;
+ - the hub PNG, ICO and manifest (`background_color` `#151b20`);
+ - the header `Archi|lyzer`, with mark fills slate / slate-dim / bone.
+- New `homepage/e2e/brand.spec.ts` (2 tests): the header (name, split, no uppercase, fills) and the
+ icon set.
+- Editor `branding.spec.ts` +1: the sidebar mark sits outside the "Cypress HQ" link, and the linked
+ `/icon.svg…` carries the parent colours.
+- Unit tests:
+ - `common/lib/brandIcons.test.ts` (7): ICON_FILES, ICON_METADATA, siteIconPalette, the pngToIco
+ header, entries and offsets, 256→0 and range errors, the renderIconPng IHDR for all three
+ variants, the renderFaviconIco directory ↔ IHDR, renderIconFile;
+ - `editor/app/lib/brandIcon.test.ts` (1).
+
+| sha | what |
+|---|---|
+| `446be496` | `common/lib/brandIcons.ts` (+ test): ICON_FILES, ICON_METADATA, siteIconPalette, renderIconPng (next/og), pngToIco, renderFaviconIco, renderIconFile |
+| `d69cf1b1` | export `app/icons/[file]` + `app/favicon.ico` routes, `lib/brand.ts` iconPalette, layout `icons: ICON_METADATA`, manifest (BASE_GROUNDS.dark / icon ground / icon.svg); committed icons + favicon.ico deleted |
+| `cee051f2` | homepage twin routes (parent palette), layout `icons: ICON_METADATA`; committed icons + favicon.ico deleted |
+| `fa7823fe` | `site-sw.js`, `sw-hub.js`: `SHELL = "shell-v2"`, VERSION unchanged |
+| `fee7394c` | `common/components/BrandMark.tsx`, `Wordmark.tsx` |
+| `4c90c7fc` | export Header (mark + Wordmark, `headerMarkPalette`), Footer (parent mark before the credit), fixture `wordmarkLead`, new `e2e/brand.spec.ts` + `e2e-hub/brand.spec.ts`, `pwa.spec.ts` extended |
+| `e1df9815` | homepage Header (parent mark + the Archi · lyzer split), new `e2e/brand.spec.ts` |
+| `adcd9482` | editor sidebar mark, `app/icon.svg` + parity test, `branding.spec` +1 |
+
+**Gates.**
+- tsc: clean before each code commit (runs 2 and 3; run 1 is under "Found and left"), and run 4 on the final tree.
+- Unit and script tests:
+ - common **1,881/1,881** (S0's 1,874 + 7);
+ - editor unit **79/79** (+1);
+ - `test:scripts` **162 + 1 skip**;
+ - mcp **219/219**.
+- Builds, all with `export/public` seeded (`sw.js` a copy, no dangling links) and `homepage/public`
+ data copied:
+ - `pnpm --filter editor exec next build`: ok, 46 s. `/icon.svg` is emitted and linked.
+ - `pnpm --filter export exec next build` (site mode, the worktree's default site): ok, 27 s. The
+ seven `out/icons` files plus `out/favicon.ico`, PNG magic and IHDR per file (180, 192, 32, 512,
+ 512), ICO `0000 0100 0300 1010`, icon.svg lit Signal `#5fa8a0`, manifest as above.
+ - `pnpm --filter homepage exec next build`:
+ - run 1 failed in 8 s, 24 Turbopack errors in `next/font/google` for IBM Plex Sans ("queries have
+ exactly one entry") from the untouched `fonts.ts`, while another worktree built at the same
+ time;
+ - the retry was ok, 16 s. Same file set and magic; icon.svg is the parent mark; og:image still
+ `/icons/icon-512.png`.
+ - Hub mode (`INSTANCE_MODE=hub pnpm --filter export exec next build`, the build step of
+ `build:hub` without compose): ok, 31 s. `out/icons/icon.svg` holds `#151b20` / `#3f4c56` /
+ `#e7edf1`; the manifest's `background_color` is `#151b20`.
+ - A demo site (`SITES_DIR=$T/s1-demo-sites`, "Jeralyzer", lead "Jer", accent brass): ok, 33 s;
+ icon.svg lit `#e3b15c`. Used for the shots.
+- e2e (`$T/s1-e2e.sh`, one run, 21:37–21:56):
+ - export full: **199 passed, 1 failed**, 11.2 min. The failure is `theme-family.spec.ts:18`, see
+ "Found and left". Rerun alone, `theme-family.spec.ts theme.spec.ts --repeat-each 3`: **6 passed**, 32.6 s (after a ~7.5 min queue wait behind `brand-themes`).
+ - `e2e:hub`: **14 passed**, 32 s (12 + the new 2).
+ - homepage full: **26 passed**, 1.0 min.
+ - editor `branding auto-refresh dashboard navigation widget`: **48 passed**, 4.0 min.
+ - `e2e:2origin` (`TWO_ORIGIN_REBUILD=1`): **3 passed**, 48 s. Before it, the seven entries compose
+ writes (`hub-sites.json site.json corpus.json llms.txt robots.txt sitemap.xml _headers`) were
+ swapped for copies; afterwards they and `hub-summary.json` were relinked and the seed re-run.
+- **The primary's `export/public` was never written:**
+ - `sw.js` is 10,027 B, mtime 20:47:37, before and after;
+ - `corpus.json`, `hub-sites.json`, `llms.txt`, `robots.txt`, `_headers`, `hub-summary.json` and
+ `sitemap.xml` kept their pre-run mtimes.
+- Screenshots (`$T/s1-*.png`, from served `out/` on localhost, all looked at):
+ - `site-header-{390,1280}-{light,dark}`: brass Jer|alyzer;
+ - `site-footer-light`, `site-footer-390-dark`;
+ - `hub-header-{1280,390}`, `hub-footer`;
+ - `homepage-header-{1280,390}`.
+
+ They match the canvas's *In context* board. On light the ink tile is a real icon tile; on the
+ dark base it coincides with the page ground and the lines carry the mark, as the canvas draws
+ Jeralyzer on dark and the parent mark on slate.
+- Numbers tools: none.
+
+**Found and left.**
+- **Importing the `next` root types into `common/` breaks common's tsc.** tsc run 1 put 10 errors in
+ existing tests (`bin/_cli.test.ts`, `publish/build.test.ts`: "Property 'NODE_ENV' is missing …
+ ProcessEnv"). The cause was `import type { Metadata } from "next"` in `brandIcons.ts`: `next/index.d.ts`
+ references `types/global`, which makes `NODE_ENV` required on every `ProcessEnv` in the program.
+ So `ICON_METADATA` is untyped; the layouts type-check it against `Metadata`. `next/og` does not
+ carry that reference.
+- **`theme-family.spec.ts:18` timed out** waiting for `menuitemradio "Swiss"`, in the second
+ `pickFamily`, right after `page.reload({waitUntil:"commit"})` + `themeReady`. The shot shows
+ Selenized applied and the menu closed: the click landed before hydration. `themeReady` is set by
+ the pre-paint script, not by React.
+ - It is the same reload timing `plans/deflake-e2e.md` §2 fixed for the read, which still races the
+ click after it.
+ - S1 touches no theme code.
+ - S2 deletes this spec.
+- **`export/playwright.config.ts:29` still says "exactly like the committed public/icons/*".** The
+ plan lists that comment under S2's leftover copy, so it was left for S2/S3.
+- **`export/.2origin/hubA`** is a hub build (gitignored, about 2.5 GB, because `out/` copies the
+ linked public data). It stays in the worktree for the next 2origin run.
+
## Operator rollout (after merge)
1. Restart the live :3001 editor on the new `main`; it needs S0's form.