commit c21344c6650011774cdef32063429cd56db25fd6
parent 609033036978a1c2121be5257ece368f04900a93
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 28 Sep 2026 03:13:58 -0400
plans: slice O2 as shipped — the fitted custom hex, the sixth chart slot (validated; the gone question), the forced-colours outline, the synthetic homepage e2e summary; FACTS' colour entry; dated notes in brand-and-themes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 188 insertions(+), 4 deletions(-)
diff --git a/plans/FACTS.md b/plans/FACTS.md
@@ -6720,9 +6720,12 @@ S4, as shipped"; `release-10.md` "Slice L2 / L1, as shipped". Every anchor below
it goes through `siteColor` (`common/lib/siteColor.ts`): a NAMED accent is
`var(--swatch-<id>)` (per base) from the summary's `accentId`, used only when `hub-sites.json`'s
hex (on-dark only) is absent or equal to the summary's; the homepage's `ArchiveCards` use the
- same helper. With no accent the fallback is the site's CHART colour, `siteChartColors(sites)`:
- the `--chart-N` of its accent's hue family (blue 1, green 2, violet 3, brass 4, sakura 5), else
- `seriesColor(i)` when free, else the lowest free slot — never two sites in one colour. The growth
+ same helper. A CUSTOM hex is fitted per base (release 11, slice O2): `perBaseColor(resolveAccent(hex))`,
+ one `rgb(calc(…))` over tokens.css's one-hot `--base-light|sepia|dark`. With no accent the
+ fallback is the site's CHART colour, `siteChartColors(sites)`: the `--chart-N` of its accent's hue
+ family (blue 1, green 2, violet 3, brass 4, sakura 5, vermilion 6 — `--chart-6` is a rust since
+ release 11), else `seriesColor(i)` when free (the first SIX are `var(--chart-1..6)`), else the
+ lowest free slot — never two sites in one colour, and any six wear the six validated slots. The growth
chart, its legend and `/stats` (`SiteGrid`, `homepageChartData`, the By-site leaderboard) wear
it; the accents themselves fail the dataviz validator as a chart palette (no five with Brass
and Blue pass). `useHubSites` lists the official
diff --git a/plans/brand-and-themes.md b/plans/brand-and-themes.md
@@ -69,6 +69,11 @@ tile, following its corner (`border-radius` 21.875% = rx 112 of 512), in the pal
(`#6b5d47`, `#586977`: 3.09:1 and 3.48:1 on the ink page), as a box-shadow under `.dark` only, so no
layout moves and light and sepia are unchanged. The icon files carry no ring.
+**Under forced colours the tile has a 1px `CanvasText` outline, on every base** (2026-09-28,
+release 11, slice O2; `release-11.md`): the reader's Canvas replaces the page there and may be as
+dark as the tile. Same place and corner as the ring (outline offset 0); outside forced colours the
+classes match nothing, and the marks render pixel-identical on light, sepia and dark.
+
Icon variants:
- `any`: `rx=112`.
- `maskable`: full-bleed ground, the mark scaled 0.8 about (256,256).
@@ -119,7 +124,8 @@ Each base declares all 48 colour tokens listed in `homepage/e2e/theme.spec.ts:63
- **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
+- **Charts do not follow the accent:** each base has its own fixed `--chart-1..5` (`--chart-6`, a
+ rust, since release 11: light and sepia `#823c10`, dark `#a54a08`; `release-11.md`, slice O2). 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.
diff --git a/plans/release-11.md b/plans/release-11.md
@@ -394,6 +394,181 @@ mounted is fine as a rule, keep `untouched` optional tonight).
the new row; with `persistKept` counting every return, the new persistKept test fails; the other
8 in the two files pass.
+### Slice O2, as shipped — homepage and brand lows (2026-09-28)
+
+Release 10 left four lows on the homepage and the mark: AC's "a custom hex is painted unfitted on
+every base" and "a sixth published site would nearly repeat Anilyzer's magenta", and MR's "the ring
+is a box-shadow, which forced-colours mode removes" and "a fresh worktree's homepage e2e skips 8
+specs". Branch `r11/homepage-lows` off `main` `2162db92`, `main` `c1d4790a` merged in (a fast-forward)
+before the first commit, worktree `/home/user/Projects/r11-homepage-lows` (block #3), one Opus
+implementer. Scratch files `o2-*` in the job's `tmp/overnight`.
+
+**What shipped**
+
+- **A custom hex is fitted per base, on the homepage cards and the hub's official cards.**
+ - `siteColor` paints one through `resolveAccent`, the fit the site's own pages use: 4.5:1 on the
+ ground and with its ink.
+ - The three values travel as ONE CSS value, `perBaseColor` (`common/lib/siteColor.ts`): each
+ channel is a `calc()` over new one-hot tokens `--base-light|sepia|dark`. They are 1 on their own
+ base and 0 on the others, declared in each base block of `tokens.css`. The browser resolves the
+ value to a plain `rgb()` and switches it with the base.
+ - A single string was needed because `siteColor`'s result goes, as is, into every hub surface:
+ card, chip, result stripe. So the fix is in the shared function, and no file under
+ `export/app/components/hub/**` changed.
+ - The fallbacks paint the light value on a page with no token sheet.
+ - The e2e's pale `#f4c2d7` (1.43:1 on the light ground as published) paints `#846974` on light,
+ `#7c636e` on sepia and `#f4c2d7` on dark.
+- **A sixth validated chart slot, `--chart-6`, a rust.** Light and sepia `#823c10`, dark `#a54a08`.
+ - `seriesColor(5)` is `var(--chart-6)` (`CHART_SLOTS = 6`), no longer the golden angle's
+ `hsl(328 64% 55%)`.
+ - `ACCENT_CHART_SLOT.vermilion = 5`. The rust is Vermilion's hue family: 8–10° of OKLCH hue from
+ its swatches, and 27–33° from the nearest other slot (amber).
+ - So Jasolyzer, once published, wears the rust at any rank, and any six sites wear the six
+ validated slots. Signal, a custom hex and no accent still have no family; they take the lowest
+ free slot, and the sixth when it is free.
+ - `REQUIRED_TOKENS` is 50 and `--color-chart-6` is in `@theme`. The homepage `theme.spec` checks
+ that every base declares it.
+- **Forced colours: a 1px `CanvasText` outline on the mark's tile, on every base.** It is two classes
+ on `BRAND_MARK_RING_CLASS`:
+ - `forced-colors:outline` and `forced-colors:outline-[color:CanvasText]`.
+ - They compile to `@media (forced-colors:active){…outline-width:1px…outline-color:canvastext}`.
+ - The outline has offset 0, so it sits where the dark ring does and follows the same corner.
+ - Outside forced colours the two classes match nothing.
+ - Every `BrandMark` has it: the export header and footer credit, the hub, the homepage and the
+ editor sidebar.
+- **The homepage e2e reads a synthetic summary.**
+ - `homepage/e2e/fixture-summary.ts` replaces `fixture-accents.ts`. It runs the real
+ `buildHomepageSummary` over made-up recordings and is deterministic (`FIXTURE_NOW` 2026-09-15).
+ - Its six public sites are, in order: Brass; the pale custom hex; three with no accent; Vermilion.
+ - The first site has a 12,000-transcript week in May, so the All-time axis reaches 10K. Every site
+ transcribes every day of the last 200.
+ - `playwright.config.ts` writes it to `e2e/.e2e-summary.json` before the server starts. That path is
+ gitignored, as is a killed run's temp copy, and is not linked. It always passes
+ `HOMEPAGE_SUMMARY_FILE`.
+ - Nothing is read from `public/` or from the primary.
+ - `stats.spec` no longer skips, and its site count is the fixture's six. `marketing.spec` reads the
+ fixture, the file the server reads. `instance-colours.spec` checks the fitted pale hex per base,
+ Vermilion's card in its rust layer's hue, and the six legend swatches as the six validated slots.
+
+**The sixth slot, validated.** Slice AC's method (`ac-validate.sh` / `ac-layers.mts`): the dataviz
+skill's validator on each base's own values, read from `tokens.css`. It runs over all pairs, since
+the homepage order moves with transcript counts. Contrast is checked against `--chart-surface` AND
+the page ground. The command, from `common/`, is `pnpm exec tsx $T/o2-validate.mts`. For every run it
+calls `node $T/dataviz/scripts/validate_palette.js "<chart-1..6>" --mode light|dark --surface <chart
+surface | page ground> --pairs all|adjacent`. Log: `o2-validate.log`.
+
+| base | `--chart-1..6` | surface / ground | band | chroma | CVD, all pairs | normal, all pairs | contrast | exit |
+|---|---|---|---|---|---|---|---|---|
+| light | `#3a7de0 #2f8a57 #5e3aa8 #a8741a #c24a8a` **`#823c10`** | `#ffffff` / `#f3f6f7` | PASS | PASS | WARN 6.2 (amber↔green) | PASS 15.4 | PASS / PASS | 0 / 0 |
+| sepia | `#3574d6 #2a7d4f #5e3aa8 #a8741a #bb4585` **`#823c10`** | `#faf4e6` / `#f4ecd8` | PASS | PASS | WARN 6.1 (amber↔green) | PASS 15.3 | PASS / PASS | 0 / 0 |
+| dark | `#3561c8 #3fa577 #9a7ee6 #b98a2a #c24c8a` **`#a54a08`** | `#16110a` / `#0c0a08` | PASS | PASS | WARN 6.9 (magenta↔green) | PASS 15.4 | PASS / PASS | 0 / 0 |
+
+- **The WARN pairs are the five's own floor band, which release 10 recorded; none involves the
+ sixth.** The sixth's own pairs, each run alone against `--chart-1..5`, are:
+ - worst CVD 12.5 / 9.1 / 13.9 (light / sepia / dark; against green, green, magenta);
+ - worst normal 16.6 / 16.6 / 16.3 (against amber);
+ - all above the target of 8 and the floor of 15.
+- **Contrast:** 8.1 / 7.4 / 3.2 to 1 on the chart surface and 7.4 / 6.9 / 3.4 to 1 on the ground.
+- **The stack (adjacent), as `siteChartColors` resolves it** for the operator's mapping, with
+ Jasolyzer (Vermilion) at each of the six places: PASS on every base. Worst CVD 12.5 / 9.1 / 11.0,
+ normal 16.6 / 15.3 / 16.3. Before this slice the same run could not even resolve:
+ `jasolyzer→hsl(328 64% 55%)` (`o2-validate-pre.log`).
+- **Why a rust.** A search over every OKLCH hue took each base's best L and C inside the band at
+ ≥ 3:1 on both the surface and the ground (`o2-chart6-search.mjs`, `-hue`, `-margin`; logs of the
+ same names). Only the red-browns, h ≈ 15–60, clear every pair with `--chart-1..5` on all three
+ bases. With 0.01 of L margin and ≥ 3.2:1, the best is h 48. The other hues fail:
+ - teal fails CVD against blue and green (4.0–6.4), and would sit on Signal;
+ - olive fails the normal floor on light and sepia (11–14.9);
+ - an electric blue or violet fails on dark, or reads as a second chart-1 or chart-3.
+- **A question for the operator: the rust sits near `--state-gone`.** The validator FAILs that pair,
+ so it is information, not a gate:
+ - normal ΔE 8.4 / 7.2 / 13.1, CVD 5.4 / 3.6 / 11.7;
+ - release 10 (S2's review) accepted amber and magenta at 11.8–14.5, because gone is only ever
+ labelled text, never a chart mark; the rust is closer;
+ - no sixth hue clears both `--chart-1..5` and gone ≥ 15 on every base; the gone-constrained
+ search's best, an electric violet, fails the palette's own normal floor (14.4 on light and
+ sepia; `o2-chart6-hue-gone.log`);
+ - on the homepage, gone is the FamilyStats figure and the rail's badge.
+
+**Forced colours, as rendered** (`o2-shots.mjs`, `o2-shots.log`, `o2-forced.png`).
+- **Normal modes are unchanged.** Each mark was shot as built and again with the two new classes
+ removed, which is `main`'s class string, then compared by pixel (`magick compare -metric AE`).
+ All 15 pairs differ by **0 px**: the homepage header, the site header and footer, and the hub header
+ and footer, on light, sepia and dark.
+- **The same pairs under forced colours differ,** on a light (white Canvas) and a dark (black Canvas)
+ forced palette: all 18.
+- **The worst case before, row 2 of `o2-forced.png`.** A site's ink tile on light or sepia under a
+ black Canvas had no edge at all. Now it has a white outline.
+- **Found: Chromium keeps the svg's box-shadow in forced colours.** Its UA sheet gives `svg`
+ `forced-color-adjust: preserve-parent-color`, so MR's premise ("which forced-colours mode removes")
+ does not hold there. The dark ring stays, and the outline is painted over the same pixel ring. What
+ the mode does do is put the reader's Canvas under the tile on every base, which is why the outline
+ is on every base.
+- **Playwright applies the forced palette** (`page.emulateMedia({ forcedColors: "active" })`). This
+ version's `test.use` has no `forcedColors`.
+
+| sha | what |
+|---|---|
+| `fbd6318a` | `common:` `--chart-6` + the base flags (`tokens.css`), `seriesColor` six, `ACCENT_CHART_SLOT.vermilion`, `perBaseColor`, `siteColor` fits a custom hex; `REQUIRED_TOKENS` 50; unit tests (siteColor, hubSummary, themeTokens); the homepage chart and card headers' notes |
+| `e253e244` | `common:` the forced-colours outline on `BRAND_MARK_RING_CLASS`; `BrandMark.test.ts` +1; the homepage `brand.spec` forced-colours test + outline none in the normal-mode test |
+| `c37d620e` | `homepage(e2e):` `fixture-summary.ts` (replaces `fixture-accents.ts`), `playwright.config.ts`, `stats`/`marketing`/`instance-colours` specs, `.gitignore` |
+| `7685e701` | `changelogs:` export and editor `[Unreleased]` bullets (the existing sections), the homepage's four |
+| _this_ | `plans:` this record; `FACTS.md`'s official-instance colour entry; dated notes in `brand-and-themes.md` (mark, charts) |
+
+**Gates** (logs `o2-*.log`):
+- **tsc** clean before each code commit (33–71 s; `o2-tsc{1..4}.log`). The one failure along the way
+ was `forcedColors` in `test.use`; the spec now uses `emulateMedia`.
+- **Units:**
+ - common **2,056/2,056** (2,051 on `main` + 5);
+ - editor unit **85/85**, `test:scripts` **175 + 1 skipped**, mcp **269/269**, homepage unit **2/2**.
+- **Builds:**
+ - Timings: editor ok (38 s), export site ok (23 s), export hub ok (25 s; `out/ask` present),
+ homepage ok (12 s; no `public/` data, so the no-data page and the header mark).
+ - Each stylesheet carries the `forced-colors:active` outline rule, `--chart-6` (×2 `#823c10`,
+ ×1 `#a54a08`) and the three flags per base.
+ - The primary's `export/public` is byte-identical before and after the builds and the e2e: 392
+ entries, 380 files, tree md5 `5c90d8e63753`, content md5 `4a0c46b39eeb`.
+- **e2e** (queued behind O1 for about a minute; nothing failed, so no re-runs):
+ - homepage FULL, from this worktree with NO `homepage/public` data (only its tracked `_headers`):
+ **30 passed, 0 skipped** (42 s). Release 10's fresh-worktree run was 21 passed / 8 skipped, and
+ 29 passed with the primary's data copied in; 30 is those 29 plus the forced-colours test.
+ - export `brand` + `site-branding` + `theme` + `theme-accent`: **20 passed** (50 s).
+ - `e2e:hub`: **26 passed** (1.0 min).
+ - editor `branding.spec`: **4 passed** (25 s).
+
+**They bite** (`o2-bite.sh`, `o2-bite*.log`). Each item was reverted to the old code on its own,
+with the new tests kept:
+1. **Custom hex as published.** Unit: 4 fail (the two `officialInstances` custom-hex cases, the fitted
+ test, and the unrecognised-id fall-through). e2e `instance-colours`: expected `rgb(132, 105, 116)`,
+ received `rgb(244, 194, 215)`.
+2. **Five slots, no vermilion family, no `--chart-6`.** Unit: 6 fail (the required tokens, the
+ charts test, the family slots, `seriesColor` six, the seventh-site case, and any-six-wear-six).
+ e2e: expected `var(--chart-6)`, received `hsl(328 64% 55%)`.
+3. **No outline.** Unit: 1 fails. e2e forced-colours test: `isCanvasText: false`.
+4. **`main`'s e2e setup** (its specs, config and `fixture-accents.ts`) in this worktree: **21 passed,
+ 8 skipped**. The skips are `stats.spec` ×7 and `instance-colours.spec`.
+
+**Found and left**
+- **The hub's ADDED archives still paint a custom hex as set.** This is `ArchiveShelf`'s
+ `site.accent || FALLBACK_ACCENT`, and the chips and stripes that read the registry's raw `accent`.
+ It is `export/app/components/hub/**`, O1's, so it was not touched here. The fix is one call where
+ it parses: `perBaseColor(resolveAccent(hex))`. For O1 or the parent.
+- **The rust near `--state-gone`:** the operator question above.
+- **Two things do not reach the sixth slot:**
+ - `ChartView.tsx` (export and editor charts) cycles `var(--chart-${(i % 5) + 1})`, so it never
+ wears `--chart-6`; its sixth series repeats chart-1, as before. Not a homepage surface.
+ - Past six, `seriesColor`'s golden angle still gives hue 105 (7th) and 243 (8th, near chart-1's
+ blue). Only `/stats`' by-channel views reach them (up to 8 channels plus Other).
+- **The no-data paths have no spec now.** The e2e server always has the fixture, so `/` and `/stats`
+ without a summary go untested; `marketing.spec` keeps its absent branches. Before, those paths ran
+ only in a fresh worktree, where the data specs skipped instead. A second web server without the
+ fixture would cover them.
+- **O6's `E2E_` rename will meet `HOMEPAGE_SUMMARY_FILE`,** which `app/lib/summary.ts` reads and
+ `playwright.config.ts` sets.
+- **The live sites carry none of this until rebuilt:** every export site and the hub (the outline,
+ and the hub's fitted hex), the homepage (all four), and the editor on its next restart (the
+ sidebar outline).
+
## Rollout
Nothing is rolled out tonight. The morning runbook lists what is owed: the :3001 editor restart,