fix: regenerate favicon.ico with a transparent ground; tick Pouya's read-through

public/favicon.ico shipped with no transparency: all three frames declared a
32-bit alpha channel and then carried alpha=255 on every one of their 256/1024/
2304 pixels, ground opaque cream rgba(250,247,242,255). Pouya's read-through
finding, confirmed by parsing the ICO container directly.

The render source carries true alpha (2,272,386 transparent px, 20,795 partial),
so this is an export, not a mask derived from the cream ground — R13's harder
branch did not fire and R13 is unchanged on its own terms.

New scripts/icons.mjs + npm run icons re-derives the icon from the committed
master: asserts the source is still the documented crop (R14), verifies a
candidate file and renames on success so a rejected build cannot replace a good
favicon, and runs a boundary-colour halo test. Composition is unchanged —
ink bbox and pixel count identical at all three sizes.

apple-touch-icon.png is byte-identical and stays opaque cream deliberately; the
reason lives in docs/reference/brand-assets.md §The icon set, with the bar and a
pointer in BaseLayout.astro, docs/06 and R13.

docs/06: the read-through is ticked, and the cutover callout drops to ONE
blocker — Q60's waiting period.

Two adversarial review rounds, 15 findings, all resolved, none declined; stopped
at two per D19. claims-auditor correctly deferred to cutover per D20. Gates on
the committed bytes, exit status read: check 0, build 0 (23 pages),
check:claims 0, check:intake 0, og:proof 0, lint 0, lighthouse 0 (worst of 23
99/100/100/100). Nothing deployed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
This commit is contained in:
Pouya Lajevardi
2026-09-02 16:14:35 -04:00
co-authored by Claude Opus 5
parent 4735989f0b
commit 67847d94fa
8 changed files with 891 additions and 22 deletions
+371 -1
View File
@@ -932,7 +932,7 @@ never being raised again.
| R10 | ✅ **FIRED AND SATISFIED 2026-09-02 — THE CUTOVER EVENT AND THE CONFIRMATION, IN THAT ORDER. THE ROW STAYS LIVE.** Asked of Pouya on 2026-09-02 as a one-line question — *are ADRIC, ADRIO, the three OBA sections and the CTF all still current?* — and **answered the same day: all current.** It was asked rather than looked up, which is the entire content of this reminder: **a stamp is not a renewal receipt, and re-reading an old one is not re-confirming.** It was open for part of the day, alongside R18 which fired and was satisfied the same day. **Re-stamped on all three surfaces, because `memberOf` is emitted and the constant feeds both:** §4's memberships row, `CREDENTIALS.memberships` in `src/data/site.ts` and the R10 note in `src/data/schema.ts`. ⚠️ **AND THERE ARE TWO ARRAYS, NOT ONE — re-stamping is not the same act as checking they still agree.** `CREDENTIALS.memberships` feeds `/about/`'s visible list and `/bio/`; **`MEMBERSHIP_ORGS` feeds `/process/` §Confidentiality and the `memberOf` triples**, and `site.ts` records that the two differ on three of four lines. `_MembershipParity` compares their **`['length']` only**, so a substitution passes `npm run check` in silence. An earlier form of this row said one constant fed both surfaces, which would have left `/process/` and the JSON-LD publishing a lapsed membership after a correct-looking edit — the OCNI failure with a green build (`adversarial-reviewer`, round 1). `docs/06`'s item is ticked and **re-armed for the next republish** — the trigger is an event and events recur, which is why this row does not close on being satisfied. Original text follows. ⚠️ **A THIRD SURFACE, 2026-08-30: `/process/` §Confidentiality renders `MEMBERSHIP_ORGS[0]` ("I am a member of the ADR Institute of Canada").** It is rendered from the constant rather than typed, so the sweep this reminder prescribes reaches it — that was `adversarial-reviewer`'s finding and the fix, in that order. **DISCHARGED AS WRITTEN 2026-08-28 — AND RE-ARMED WITH AN EVENT TRIGGER INSTEAD OF A DATE. STILL LIVE.** Pouya re-confirmed all four memberships as current (Q44), which discharges the prohibition this row carried, and `/about/` now publishes the Memberships group. **The row does not close, because he declined renewal-date tracking**, and that was his instruction for what to do about it: *"Without renewal months it cannot fire on a date, so make it fire on an event: re-confirm memberships before any cutover or major republish, and re-stamp §4 when confirmed."* **THE TRIGGER: re-confirm before any cutover, and before any major republish. Then re-stamp §4 the same day.** **His reason, kept verbatim because it is the general principle and not a membership detail:** *"§4 already carries OCNI as lapsed and unpublishable, and that was found roughly a year late. A stamp with no trigger behind it goes stale silently, which is exactly how OCNI got onto a list of things to feature."* **Two things the discharge did NOT license.** (1) **No currency warranty on the page** — list the memberships, promise nothing about their future state; the struck sentence stays struck and nothing replaces it. (2) ~~`memberOf` stays out of the JSON-LD~~**SUPERSEDED. Q53, ruled 2026-08-28: EMIT IT.** `/about/`'s Person node now carries the four memberships as `Organization` nodes. Pouya took `adversarial-reviewer`'s argument: they are already crawlable in `/about/`'s HTML, so withholding the triple reduced no exposure and only made the graph less complete than the page. **The consequence for THIS reminder is that it now covers two surfaces** — re-confirming before a cutover means `src/data/schema.ts` as well as the visible list, and they must not be allowed to diverge. **Renewal periods, stated once and not widened again:** the OBA sections and the CTF renew yearly; §4 records **nothing** about ADRIC's or ADRIO's period, and the widened form ("all four renew yearly") reached four files before it was swept. *Previous text described the prohibition and the withheld group; it held for one session and did its job.* | 2026-08-26 | A credential that lapses quietly is the failure mode §4 exists to prevent, and OCNI already did exactly this. The group is on a public page now, which raises the cost of a lapse rather than lowering it — *(This rationale ended by pointing at **Q48** as a possible widening of the row. Q48 closed 2026-08-28 as not site-relevant — ADRIO retention governs whether Pouya keeps a designation, not what the site may say about holding one — so the clause is struck. §12 is read aloud every session; a live reminder pointing at a struck row produces a false surface every time.)*, not just a list |
| R11 | **Re-check dependency currency at every phase boundary in the build order** (`docs/01-architecture.md` §Build order, 11 steps). Run `npm view <pkg> version` across **every** pin in `package.json` and compare; do not wait for something to break. Verified does not mean latest — record the reason for any deliberate hold in §7. ✅ **THE STEP-7 RE-ADD TRIGGER IS DISCHARGED, 2026-08-31 — and NOT as written.** It said *"at step 7, put `@lhci/cli` back"*. `@lhci/cli` is still 0.15.1, still `latest`, and still carries 10 findings (7 high) `[verified 2026-08-31]`, so the literal instruction would have re-added a tool with seven high-severity advisories. What shipped is **`lighthouse@13.4.1` + `chrome-launcher@1.2.1`, 0 vulnerabilities**, as `npm run lighthouse`. **The reason is that §7's own advisory attribution was wrong** — it blamed `lighthouse → puppeteer-core → extract-zip`; the carriers were `@lhci/cli`'s own `tmp` and `@puppeteer/browsers`' `extract-zip`, and neither exists in Lighthouse's tree. **The last clause of this trigger is the one that earned its place:** *"if the advisories are still unfixed, that is a decision to take deliberately, not a reason to leave the gap unstated."* They are still unfixed; the decision was taken; §7 records what it costs (no `lhci` assertion config, no server, no run history) and that the gate is local rather than CI, because standalone Lighthouse needs an installed browser and the runner has none. **All six UNAVAILABLE notices are deleted**`docs/04` (budget table, Performance callout, post-launch checklist), `CLAUDE.md` (performance budget, definition of done), `/build` Phase 5, `docs/06` (PR checks, cutover checklist), `.claude/agents/adversarial-reviewer.md` §4. The **general** half of R11 — re-check every pin at every phase boundary — is untouched and still fires. ✅ **SWEPT AGAIN 2026-09-01, all 19 pins against `npm view`, and TWO MAJORS ARE DEFERRED BY RULING rather than left unstated:** `@astrojs/mdx` **^7.0.8 → 8.0.0** and `typescript` **^6.0.3 → 7.0.2**. Pouya's reasoning — *"npm audit is clean and majors mid-walkthrough add churn without user value"* — with `npm audit` at **0 vulnerabilities** `[verified 2026-09-01]`, which makes it a churn decision and not a security one, **and one that flips the moment that stops being true.** Both now sit on a new **Cutover prep** group at the head of `docs/06`'s cutover checklist, dated, because deferring a thing and forgetting it look identical three weeks later. Four more are a minor or patch behind and already satisfied by their carets, so they need no edit — `astro` 7.2.9 → 7.2.10, `@astrojs/sitemap` 3.7.3 → 3.7.4, `globals` 17.11.0 → 17.12.0, `typescript-eslint` 8.68.0 → 8.69.0; the other 13 are current. **This row is the deferral's reminder, not its replacement** — R11 fires at the next phase boundary regardless | 2026-08-26 | `astro: "^5.0.0"` was recalled rather than checked and was two majors stale the day it was written, which meant a framework carrying high-severity XSS advisories. Between phases is cheap; after a phase of pages is written is not. The build order has ten more boundaries |
| R12 | **`compressHTML: true` is a deliberate deviation from the Astro 7 default (`'jsx'`).** Measured 2026-08-26: in an `.astro` template an inline pair split across two lines renders as `<em>a</em><strong>b</strong>` under the default — the space is silently deleted. MDX prose is unaffected | 2026-08-26 | It is a deviation, and undocumented deviations become folklore. Revisit **with a measurement**, not a preference — and re-measure after any Astro major, since the behaviour could change again |
| R13 | **The infinity mark ships as a RASTER, and that is temporary. RAISED 2026-08-27; Pouya ruled the committed SVG does NOT close it***"Keep it committed, keep the AVIF render path. Your own measurement is the reason: 257 KB wrapping seven embedded base64 PNGs. It renders faithfully because it IS the raster."* So the exception stands and the reminder stays live. `InfinityMark.astro` renders an optimised AVIF/WebP from `src/assets/brand/sml-infinity-mark.png` — a deliberate, documented exception to `docs/02`'s "inline SVG, never a PNG", because the mark is gradient-mesh artwork and no true vector master exists yet (Q38). **Removal trigger: the commissioned vector master lands.** Then replace the `<Picture>` with inline SVG, regenerate `favicon.ico` and `apple-touch-icon.png` from it, and delete the exception from `docs/02`, from the component, and from Q38 | 2026-08-26 | Pouya flagged this himself when he made the ruling: *an interim raster is exactly the kind of temporary measure that becomes permanent by never being raised.* It costs ~8 KB and works, which is precisely why nobody will notice it again. There is no build error to prompt anyone — only this row |
| R13 | **The infinity mark ships as a RASTER, and that is temporary. RAISED 2026-08-27; Pouya ruled the committed SVG does NOT close it***"Keep it committed, keep the AVIF render path. Your own measurement is the reason: 257 KB wrapping seven embedded base64 PNGs. It renders faithfully because it IS the raster."* So the exception stands and the reminder stays live. `InfinityMark.astro` renders an optimised AVIF/WebP from `src/assets/brand/sml-infinity-mark.png` — a deliberate, documented exception to `docs/02`'s "inline SVG, never a PNG", because the mark is gradient-mesh artwork and no true vector master exists yet (Q38). **Removal trigger: the commissioned vector master lands.** Then replace the `<Picture>` with inline SVG, regenerate `favicon.ico` and `apple-touch-icon.png` from it, and delete the exception from `docs/02`, from the component, and from Q38. ⚠️ **The favicon half of that is now a command rather than a manual step — `npm run icons`, added 2026-09-02** — but it reads the RASTER, so pointing it at a vector master is part of the work this row is waiting for. **It regenerates the favicon only. `apple-touch-icon.png` is still hand-made, and it must stay OPAQUE CREAM when it is remade** — the reason is an external platform behaviour that this repository has not tested, and `docs/reference/brand-assets.md` §The icon set carries it with its provenance. Do not restate it here as a fact | 2026-08-26 | Pouya flagged this himself when he made the ruling: *an interim raster is exactly the kind of temporary measure that becomes permanent by never being raised.* It costs ~8 KB and works, which is precisely why nobody will notice it again. There is no build error to prompt anyone — only this row |
| R14 | **Anything a spec makes a claim about must be reachable from the repository.** If the artefact lives only in Drive, in a console, or on someone's disk, no reviewer can check the claim and the claim is unverifiable by construction — see the rule now in `CLAUDE.md` | 2026-08-26 | **This has now cost twice.** Q24 was `AWS-Hosting-Guide.md`, the only record of how the infrastructure was hand-built, living outside the repo. Q32 was the infinity mark: Claude Code traced it from the old site's *loading placeholder*, wrote a scrupulous provenance comment recording exactly that doubt — and **two adversarial review passes still could not catch it**, because the artwork they would have had to compare against was not in the repo. The doubt was correctly stated and structurally uncheckable. That is the failure mode: not a missing warning, a missing artefact |
| ~~R15~~ | ✅ **DISCHARGED 2026-08-31 — build step 7b. Seventeen pages and every future article now carry their own generated card; `/` and `/about/` keep the portrait, which Q40 called "not an interim" and which is unchanged.** `src/pages/og/[...slug].jpg.ts` renders them at build from `satori@0.33.4` + `sharp`; `src/data/og-cards.ts` is the registry; `SEO.astro` resolves which of the two kinds a page gets from its own pathname, so the decision lives in one data file rather than in nineteen call sites. `satori` over `astro-og-canvas@0.13.0` — both 0 vulnerabilities `[verified 2026-08-31]` — because `sharp` was already a dependency, so it is one library rather than a CanvasKit wasm blob, and it renders the site's real fonts and tokens instead of approximating them. **The interim note is deleted from `docs/04` and the cutover line from `docs/06`**, which is what this row's trigger asked for. ⚠️ **AND IT SURFACED A HOLE THAT IS WORTH MORE THAN THE ROW: TEXT BAKED INTO A JPEG CANNOT BE GREPPED BY `npm run check:claims`, WHICH UNDER D20 IS THE ONLY PER-STEP CLAIMS CONTROL THERE IS.** A card is the one surface on this site where new prose would face no mechanical check at all. So card copy is constrained structurally rather than editorially: **every headline is its page's own `<h1>`, character for character**, the kicker renders `CREDENTIALS.designations`, and **`npm run og:proof` enforces both against the built HTML** — it fails if the registry and the page disagree in either direction, and it also confirms every `og:image` resolves to a file that exists. That check found a straight-versus-typographic apostrophe on its first run. **A page with no registry entry is a build error, not a fallback to the portrait** — R15's failure mode was never the wrong image, it was the wrong image shipping *invisibly* and reading as intentional, and a silent fallback recreates it exactly. **What is NOT discharged and is deliberately left open:** the check compares strings, so it cannot see a card that renders badly — a headline that overflows, a mark that clips. `npm run og:proof -- --sheet` writes a contact sheet of every card for that, and **a human still has to look**; `docs/06`'s cutover item says so. Two cards were read at step 7b (`/practice/energy/`, the longest headline, and `/practice/insurance/`) and rendered correctly | 2026-08-27 | Nobody on this project will ever see the defect. A link preview is rendered by LinkedIn, Slack and Teams for a reader who is not us, and `docs/04` requires a **unique** title and description per page — nineteen unique pages that all preview identically is the failure, and it is invisible from inside the repo. It cost nothing and worked, which is exactly the R13 shape |
| ~~R16~~ | ✅ **DISCHARGED 2026-08-31 — build step 9, which is the trigger this row set. The one-page PDF exists, is committed at `public/pouya-lajevardi-bio.pdf`, and `/about/` links it.** Q45's two open sub-decisions are both taken, and the second one is what makes the first safe. **(a) Generated at build, or authored once? NEITHER — and the third option answers this row's actual objection.** The bio is a PAGE, `src/pages/bio.astro`, so every line of it is reviewed by the same apparatus as every other page: `astro check`, `npm run check:claims` on the built HTML, the per-step code review, and the cutover claims pass. `npm run bio:pdf` then RENDERS the PDF from the built page through Chrome, with **no new dependency**`chrome-launcher` is already a devDependency because Lighthouse needs it. That is aimed squarely at what this row said the problem was: *"It is the one artefact class this project's review apparatus cannot reach… a PDF circulated with an appointment proposal is read once, by the reader who matters most, and never seen by a reviewer again."* A rendering of a reviewed page is back inside the apparatus. **It is deliberately NOT part of `astro build`** — CI has no Chrome, and a build step that cannot run in CI is Q22's shape again — so the PDF is committed, which R14 wants anyway. **(b) Does it carry anything the site does not? NO, and that is the answer that avoids the §4 question this row flagged.** Every line renders from `CREDENTIALS`, `ROLE`, `BOUTIQUE`, `PRACTICE_AREAS`, `FEES` and `CONTACT`. No matter list — which this row correctly said *"would collide with §4 Forbidden directly"* — no referees, and no figure that is not on `/fees/`. The fee summary IS there, because this row's own reasoning is that an appointment proposal needs the rate card as much as the bio. ⚠️ **`npm run bio:pdf` ASSERTS ONE PAGE and writes nothing if the count is wrong** — counted from the PDF's own page objects, cross-checked against the page tree's `/Count`. A one-page bio that silently becomes two is invisible from the source, because it depends on the print stylesheet, the paper size and how much §4 has grown. **And reading the rendered PDF caught a real breach the source review had not:** the opening sentence read *"I act as a neutral in commercial disputes — as a mediator, as an arbitrator in commercial matters…"*, whose leading clause scoped **mediation**, which Q56 leaves unscoped deliberately. `/bio/` is `noindex` and out of the sitemap: it is a condensed duplicate of `/about/` and `/fees/`. **What is NOT discharged:** nothing in the build regenerates the PDF. It must be re-rendered and re-committed whenever `/bio/`, §4, the rate card or the print styles change, and `docs/06`'s cutover checklist carries that | 2026-08-28 | It is the one artefact class this project's review apparatus cannot reach. A web page is re-reviewed by every audit and re-checked by every build; a PDF circulated with an appointment proposal is read once, by the reader who matters most, and never seen by a reviewer again |
@@ -948,6 +948,376 @@ never being raised again.
# Change Log
## 2026-09-02 (aq) — The favicon shipped with no transparency and is regenerated from the committed master; Pouya's full-site read-through completes with that as its only finding, and the cutover blockers drop to one
**Pouya's read-through of every page against §4 is COMPLETE, 2026-09-02.** It is
the human half of D20 and it is not delegable. Across 23 pages it returned
**one finding, and the finding was not copy**: `public/favicon.ico` had no
transparency. His read carried the approvals with it in terms — the
`/legal/privacy/` §Who can see it wording, the **SML Company Ltd** consent line,
and `/med-arb/` **as shipped** — which discharges the wording sign-off that
entry (ap) had routed into this pass.
### 1. The finding, confirmed before it was acted on
Pouya's note: *"`public/favicon.ico` has zero transparent pixels; every
background pixel is opaque cream (250,247,242,255)."* Confirmed by parsing the
ICO container directly rather than by converting it — a conversion step can
itself add or drop an alpha channel, and then the measurement is about the
converter. All three frames are PNG-encoded and **declare a 32-bit alpha
channel**, and then carry `alpha = 255` on every one of their **256 / 1,024 /
2,304** pixels, `range = [255,255]`. The ground is `rgba(250,247,242,255)`,
which is `--cream` `#faf7f2` exactly. `magick identify` reports these frames as
`8-bit sRGB` and says nothing about alpha either way, which is why the container
was read directly.
**A declared alpha channel that is opaque everywhere is the worst shape this
defect could take**: every tool reports the file as RGBA and nothing flags it.
### 2. It is an EXPORT, not a derivation — the R13 branch did not fire
Pouya's instruction allowed for the harder case: *"If the master itself has no
alpha channel, derive the mask from the cream ground and flag R13."* **Measured,
and it does not apply.** `src/assets/brand/sml-infinity-mark.png` carries a true
alpha channel — 2668 × 1704, **2,272,386 fully transparent pixels (50.0%)**,
2,253,091 fully opaque, and **20,795 partial** — the anti-aliased edge of the
ribbon, intact. So no mask had to be reconstructed from the cream ground, and
nothing here is lossier than the artwork. **R13 is untouched by this** and stays
open on its own terms: the mark still ships as a raster.
The regeneration also **re-derived the documented crop as a side effect**:
extracting `{ left: 159, top: 646, width: 2668, height: 1704 }` from
`sml-infinity-mark-master.png` returns a buffer **byte-identical** to the render
source. `scripts/icons.mjs` asserts that on every run, so the icon stays
traceable to committed artwork rather than to a file on a disk — R14.
### 3. What changed, and what deliberately did not
`public/favicon.ico`, 16/32/48, **4,857 → 5,365 B**. The ground is now
transparent; **nothing else moved.**
**The composition was measured off the icons as they already shipped rather than
re-chosen** — the mark spans **7/8 of the canvas width**, centred on both axes,
which is 14/16, 28/32, 42/48 and 158/180 on the touch icon. Regenerating had to
reproduce that, not restyle the mark, and it does: composited back onto cream,
the mark's bounding box and ink-pixel count are **identical at all three sizes**
— 14 × 9 at (1,4), 28 × 18 at (2,7), 42 × 27 at (3,11), ink 92 / 328 / 689.
⚠️ **`public/apple-touch-icon.png` STAYS OPAQUE CREAM, DELIBERATELY, AND IS
BYTE-IDENTICAL ACROSS THIS CHANGE** (`sha256 6377b096…`, asserted before and
after). **iOS composites a transparent touch icon onto BLACK**, so the fix for
the tab icon is the defect for the home-screen icon. The two requirements are
opposite and the asymmetry is now recorded in three places, because the risk is
that someone later "fixes" the touch icon to match: `docs/reference/brand-assets.md`
§The icon set, the comment beside the `<link>` tags in
`src/layouts/BaseLayout.astro`, and R13.
### 4. Verified two ways, on two instruments that cannot fail the same way
Pouya asked for both, and asked for the dark ground specifically.
**(a) Programmatically, on the shipped `dist/` bytes.** The container parsed
frame by frame: three frames, dir sizes matching the decoded PNGs, **all four
corners `alpha = 0` at every size**, `147 / 636 / 1,499` fully transparent
pixels, `15 / 88 / 202` fully opaque, `94 / 300 / 603` partial. The mark is
still there and the ground is gone.
**(b) Composition parity.** Composite the new icon back onto cream and it
reproduces the old matted icon to within **1 of 255 on every channel of every
pixel** — max delta **1**, mean **0.010.02**, **0** pixels over a delta of 8,
at all three sizes. That proves the regeneration did not restyle, rescale or
reposition the mark.
⚠️ **AND IT IS NOT A HALO TEST, THOUGH THIS ENTRY'S FIRST DRAFT FILED IT AS ONE
`adversarial-reviewer`, round 1.** The claim written beside it, *"anti-aliasing
that had been matted against cream would differ far more than that"*, is the
inversion of the truth: **matting against cream is exactly what produces delta
0.** A cream-matted edge that has merely had its background knocked out
composites straight back to the icon it came from, so both the correct and the
defective case pass. **The test returns the same near-zero answer either way and
therefore carries no information about the thing it was filed under.**
**(b2) No halo — the measurement that DOES discriminate.** Read the RGB the
partial-alpha pixels actually carry: cream means a halo, the ribbon's own colour
means a clean export. **0 of 94 / 300 / 603** partial-alpha pixels are within 12
of `rgb(250,247,242)`; the nearest is **13** away, mean distance **174.0 / 178.3
/ 177.0**, mean colour ≈ `rgb(118,86,74)`. Maroon-brown, not cream and not black.
Run independently after the finding rather than taken from it, and it reproduces
the reviewer's figures exactly.
**(c) Visually, on seven grounds — and then again in a browser.** The frames
composited on Chrome dark `#202124`, Firefox dark `#2b2a33`, Safari dark
`#3b3b3b`, black, Chrome light `#f1f3f4`, white and cream, at 1:1 and at 5×
nearest-neighbour. The old icon shows a hard cream tile on every dark ground and
a visible off-white rectangle even on white; the new one shows no tile anywhere.
**Then the same page was rendered by Chrome itself** — `<img src="favicon.ico">`
on the same grounds, screenshotted headless at DPR 2 — because sharp decoding
its own output is one instrument twice. Chrome's ICO decoder honours the alpha
on every ground including pure black.
**(d) Legible on dark AND on light — but NOT at the same cost, and the first
draft of this entry recorded it as free.** Two defects, both found by
`adversarial-reviewer` round 1 and both re-measured here before being accepted.
*The figure was wrong.* `rgb(64,22,24)` was quoted as the darkest ink at 32 px
and it is **not a painted colour**: exactly one pixel carries it and its alpha is
**251**, not 255. The probe filtered at `alpha >= 250` and then computed a
contrast ratio as though the pixel were opaque. **A ratio taken from a raw
channel value is a claim about a colour that is never painted.** Recomputed on
fully-opaque pixels only, composited against the ground: darkest opaque ink at
32 px is `rgb(60,29,29)`**13.62:1** on `#f1f3f4`, not 14.02.
*The characterisation was worse than the figure.* "Reads as a single-tone loop
rather than the full two-tone ribbon" understates it. The maroon lobe composites
to **1.041.15:1** on `#202124`. Counting pixels reaching 3:1 against each
ground, at 16 / 32 / 48 px:
| | vs `#202124` | vs `#f1f3f4` |
|---|---|---|
| 16 px | **23** / 256 | 40 / 256 |
| 32 px | **87** / 1024 | 168 / 1024 |
| 48 px | **189** / 2304 | 378 / 2304 |
**Roughly half the mark's visible pixels are absent on a dark tab strip, and the
previous cream-tiled icon was more legible there.** What the change buys is the
removal of that cream rectangle from every dark strip and from white. A
`prefers-color-scheme` icon pair does not solve it — that keys off the *page's*
scheme, not the browser chrome's. **The trade is recorded as a trade**; it is
Pouya's to revisit and §4 of this entry's record is where he would find it.
**(e) Deterministic.** Three consecutive runs of `npm run icons` produce the
identical file, `sha256 3c611a43…`. A generator that drifts would make every
later comparison meaningless.
### 5. An instrument artefact, caught and recorded rather than believed
The first geometry re-check reported the mark's bounding box on the new 48 px
frame as **48 × 48 filling the whole canvas** — which reads as *the regeneration
blew the mark up to the full square*, a serious defect. **It is an artefact of
the probe.** That probe identifies ink as "differs from cream by more than 6",
which is valid on a matted icon and **invalid on a transparent one**: at
`alpha = 0` the pixels carry RGB `(0,0,0)`, which differs from cream, so the
entire canvas reads as ink. Re-measured by compositing over cream first — so both
icons are measured by the same instrument on the same kind of input — the boxes
are identical, which is §3 above.
⚠️ **`CLAUDE.md` lists six instances of *a measurement is a claim about your
instrument until you check the instrument*. THIS SESSION ADDED TWO MORE**, and
they are different failure modes from each other and from the six:
- **A probe that is valid on one class of input and silently invalid on
another.** "Differs from cream" identifies ink correctly on a matted icon and
nonsensically on a transparent one, because `alpha = 0` pixels still carry RGB.
Nothing about the probe changed — **the input class did**, and the probe had no
way to say so.
- **A contrast ratio computed without asking what alpha the colour is painted
at.** The 32 px "darkest ink" was a single pixel at `alpha = 251`, filtered in
by a `>= 250` threshold and then treated as opaque. **The number was of a
colour that is never painted.**
Both are now in `CLAUDE.md` beside the other six.
### 6. Records
- `docs/06-deployment.md` — **the cutover callout drops from TWO blockers to
ONE.** ⚠️ **It read ONE earlier the same day and that was a defect**, so the
callout now states the difference rather than the number: it said ONE then
because an approval had gone **missing** from the list; it says ONE now
because the pass that approval was routed into has been **done**. A tally
cannot tell those apart. **Q60's waiting period is the last one**, and it is a
waiting period rather than a task — earliest useful reading 48 hours, no
failure called before 7 days, so it starts first.
- `docs/06-deployment.md`**"Pouya has read every page against §4"** ticked,
with the finding and the three approvals it carried recorded on the item. It
notes in terms that this does **not** discharge `claims-auditor`'s cutover
pass: D20 traded the per-step machine audit for the human pass **plus** one
machine pass over the finished site, and one of those two has now happened.
- `docs/06-deployment.md`**"Favicon set complete"** ticked, carrying what was
wrong, what fixed it, both verifications, and the touch-icon bar.
- `docs/reference/brand-assets.md` — a new **§The icon set**: the two files,
their grounds and why they differ, the composition rule, the regeneration
command, and the measurements above. Its opening line said *"Every measurement
below is `[verified 2026-08-26]`"*, which the new section would have made
false, so sections now carry their own stamps.
- `AGENTS.md` **R13** — amended in place. Its removal trigger says to regenerate
both icons from the vector master when it lands; that half is now a command
(`npm run icons`) **but it reads the raster**, so pointing it at a vector master
is part of the work R13 is still waiting for. The row also now carries the bar
that the touch icon must stay opaque cream when it is remade.
- `scripts/icons.mjs` + `npm run icons` — new. **LOCAL ONLY and deliberately not
wired into `npm run build` or either deploy path**, like `bio:pdf` and
`lighthouse`; verified by grep that no workflow or deploy script calls it. It
writes the favicon **only** and never opens the touch icon. It refuses to run
if the render source has no alpha channel, asserts the source is still the
documented crop of the master, and re-reads the container **from disk** to
verify rather than trusting the buffers it just wrote.
### 7. No new compatibility surface
Worth stating because it bounds the risk of touching a file every page requests:
**the container format did not change.** The previous `favicon.ico` already
carried three **PNG-encoded** frames at 32 bpp, and so does this one — same
frame count, same sizes, same ascending order, same 6-byte header and three
16-byte directory entries, first payload at offset 54. Only the pixel data
differs. Anything that could parse the old file parses this one. Verified in
Chrome; the remaining browsers are covered by the existing unticked checklist
item, **"Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox"**,
which is where the touch icon gets looked at on a real handset.
### 8. Review — `adversarial-reviewer` only, per D20
**`claims-auditor` did NOT run.** D20: it runs once, at cutover, over the whole
finished site. This change set adds no copy to any page.
**Round 1: seven findings, none blocking, all seven acted on, none declined.**
Two of them — the halo proof and the legibility figures — are recorded above
because they were defects **in this entry's own evidence**, which is the class
worth keeping. The other five:
- **The external constants were dressed as repository facts.** `#202124`,
`#f1f3f4` and iOS's compositing behaviour are none of them derivable from
anything committed, and they sat under a heading promising every figure was
read off this repository. **This is R14 from the inside**: a reader cannot
re-derive `11.28:1` without a ground colour they have no way to check. The
hexes now carry `[observed 2026-09-02]`; the iOS behaviour is attributed to
**Pouya's ruling of 2026-09-02** and marked as not re-tested here, with the
`docs/06` handset item named as the place that would test it.
- **The new stamp mis-dated the render ladders** — and ⚠️ **the fix for this was itself defective; see §11.** The line replacing the file's
old blanket date asserted the ladders were `[verified 2026-08-26]`; they carry
their own `[measured 2026-08-27]` eleven lines lower. **The fix made a vaguer
wrong claim more specifically wrong** — so the header now sends the reader to
each section's own stamp and states that a blanket date has already been wrong
here once. **Round 2 found that this replacement had removed the only stamp
five sections had**; it is now a floor plus overrides.
- **The generator wrote the shipped file BEFORE verifying it.** The real defect
of the seven. `writeFileSync(OUT, …)` then `verify(OUT)` means a rejected
build has already replaced a good favicon, exits 1, and — because the script
is deliberately outside the build and both deploy paths — **nothing downstream
re-checks**. A developer who fixes the artwork without `git checkout` ships the
frames the script rejected, which is the exact failure this change was made to
correct. Now it writes `favicon.ico.candidate`, verifies **that**, and renames
on success; `die()` removes the candidate on every failure path. **Probed both
ways:** forcing the matted-ground check to fail exits **1**, prints the right
error, leaves no candidate, and leaves `public/favicon.ico` at its original
`sha256 3c611a43…`.
- **The iOS rationale was stated in full in four places.** `CLAUDE.md`'s
duplicated-fact rule with no mechanism attached — the shape that put a stale
SES DKIM table into `docs/05`. `docs/reference/brand-assets.md` §The icon set
is now the single owner; the other sites carry the **bar** and a pointer, not
the reasoning. `BaseLayout.astro`'s addition went from 6 lines to 3 for two
`<link>` elements.
- **`npm run icons` was missing from `CLAUDE.md`'s command inventory**, whose
next sentence counts the commands. Added, along with the distinction that
`bio:pdf` and `icons` are **regenerators rather than gates** — they rewrite
committed artefacts, and they stay out of the build because a build should not
silently rewrite an artefact a human approved.
Also removed while in the file: two unreachable `size === 256` branches, a
return value no caller used, and a duplicate `metadata()` read.
### 9. Verification — every command run, every exit status read
On the committed bytes, `npm run` unless noted:
| | |
|---|---|
| `check` | **0** — 0 errors, 0 warnings, 0 hints |
| `build` | **0** — 23 pages |
| `check:claims` | **0** — 12 patterns, every pattern still firing on its fixture |
| `check:intake` | **0** — 12 form fields against 12 handler fields |
| `og:proof` | **0** — 23 pages, 22 og:image targets, 21 headlines matched their `<h1>` |
| `lint` | **0** — eslint + prettier |
| `lighthouse` | **0** — worst of 23 **99 / 100 / 100 / 100**, CLS 0.000, run before and after the review fixes |
| `icons` | **0**, and byte-identical on three consecutive runs |
Icon-specific, and the numbers that matter:
- `public/favicon.ico` **4,857 → 5,365 B**; `dist/favicon.ico` identical to it.
- `public/apple-touch-icon.png` **`sha256 6377b096…7de0` before and after**, in
`public/` and in `dist/`. Asserted, not assumed.
- Corners `alpha = 0` at 16, 32 and 48, read back from the shipped `dist/` bytes.
- Ink bbox and ink-pixel count identical to the previous icon at all three sizes.
- The failure path probed: a forced rejection exits **1** and leaves
`public/favicon.ico` at its original hash with no candidate file behind.
**Not claimed:** the touch icon has not been looked at on a handset, and the
favicon has been rendered in **Chrome only**. Both belong to the existing
unticked `docs/06` item *"Tested on iOS Safari, Android Chrome, desktop
Safari/Chrome/Firefox"*, which is the right place for them and is not ticked by
this entry.
### 10. What remains
**Q60 is the only thing blocking the cutover list**, and it is a waiting period
rather than a task: TTL is `ENABLED`, no record has yet been watched to expire,
`/legal/privacy/` asserts the mechanism and not merely the period, and `docs/09`
Part 10's earliest useful reading is **48 hours** with no failure called before
**7 days**. **Start it first.**
`claims-auditor`'s single cutover pass over the finished site is still to run —
D20 traded the per-step machine audit for the human pass **plus** that one, and
only the human pass has happened.
### 11. Round 2 — seven should-fix and one consider, all acted on, none declined
**Stopped at two rounds, per D19.** The cap exists because past two the
manufacture rate exceeds the value of the marginal finding; this round is the
evidence for the other half of that rule — **five of its eight items were defects
in round 1's own repairs.** Recording that, not summarising it away.
**The one that mattered, and it was mine twice over.** Round 1 established that
the composite-onto-cream test cannot detect a halo, and the fix put the
discriminating test in the prose. Round 2's point was that **`verify()` still
could not run it** — the generator asserted frame count, dimensions, clear
corners, "some transparent" and "some opaque", every one of which a cream-matted
knockout passes. So the guard went in. **And the first guard was blind too.**
It counted **partial-alpha** pixels near cream — and a purpose-built haloed
fixture has **zero partial-alpha pixels**, because a knockout sets alpha per
pixel and leaves none. The rule inspected an empty set, returned 0, and read as
a pass. **It passed the real icon and it passed the defect**, which is the
uniform-pass shape `CLAUDE.md` warns is the unsurvivable half. Caught by
building the fixture before trusting the guard.
The working rule is the **boundary**: every painted pixel touching a fully
transparent one, counted for nearness to cream. Correct frames **1.6 / 1.4 /
1.9 %**; the fixture **19.6 %**; gate at **10 %**, ~5× clear of both. Probed
both ways — the fixture exits **1** and leaves `public/favicon.ico` at its hash.
The other seven:
- **The round-1 stamp fix deleted the floor five sections relied on.** Replacing
a blanket `[verified 2026-08-26]` with *"each section carries its own stamp"*
was true of two sections and false of five, which then had **no date at all**
on 3000×3000, 2668×1704, 257,278 B and the rest. **A vaguer wrong claim was
made more specifically wrong.** Now a floor plus overrides — the only shape
that covers every section without mis-dating one.
- **The `prefers-color-scheme` alternative was dismissed on a mechanism that is
wrong.** It does not key off the page's colour scheme; it reports the user's
preference, and this site declares `color-scheme` nowhere (`git grep -n
color-scheme -- src`, exit 1) — so it resolves to the same preference that
makes the tab strip dark. **A wrong reason there is load-bearing**: it is what
would stop the next implementer re-examining an accepted cost. The real
objections are recorded as **untested**.
- **"Roughly half the mark's visible pixels are absent on dark" overstated its
own measurement.** They are painted and fall below 3:1, and "visible pixels"
(`alpha > 0`) is 109 / 388 / 805, which is not what was counted either. Now:
about half as many pixels reach 3:1.
- **The iOS constant was hedged in one file and asserted flat in two** — R13 and
`docs/06`, the two places a caveat matters most, since §12 is read every
session and `docs/06` is what an operator works at cutover. Both now point.
- **`docs/06` restated six of the doc's figures and had already diverged**, within
one day: it quoted 11.28:1 / 13.62:1 unlabelled, which are the **32 px** row,
after a 16 px row (8.82:1 / 12.58:1) was added; and it said "≥3:1 pixels" where
the doc said "visible pixels", a different denominator. Cut to a pointer.
- **The newly ticked human-pass item still carried an unstruck imperative**
*"START WITH `/legal/privacy/` … IT IS BLOCKER 2"* — on an item recording the
pass as done, pointing at a blocker the same page had just closed. Struck.
- **`npm run icons` was labelled `LOCAL ONLY`**, which this file defines as
*cannot run in CI*. It is pure Node and could; it is out of the build because a
build should not silently rewrite an artefact a human approved. Labelled
`REGENERATOR`.
## 2026-09-02 (ap) — `/legal/privacy/` §Who can see it is cut to four plain statements and the mechanics move to the evidence file; the consent checkbox names SML Company Ltd; Q64 closes MOOT rather than answered
**Two rulings from Pouya, 2026-09-02.** **(1)** §Who can see it **stays