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
+27 -1
View File
@@ -140,6 +140,7 @@ npm run check:intake # the form's field table vs the Lambda's — they are two
npm run og:proof # every og:image resolves; every card headline IS its page's <h1>
npm run lighthouse # the performance budget. LOCAL ONLY — needs Chrome, not in CI
npm run bio:pdf # re-renders the committed one-page PDF from /bio/. LOCAL ONLY
npm run icons # re-derives public/favicon.ico from the brand master. REGENERATOR
npm run lint # eslint + prettier check
npm run format # prettier — rewrite files in place
npm run deploy # build + deploy from this machine (see docs/06)
@@ -153,6 +154,17 @@ checklist, and **they are deliberately not wired into `npm run build` or either
deploy path.** Do not describe either as gating a deploy: a check described as
running where it cannot is the defect `AGENTS.md` Q22 turned out to be.
**`bio:pdf` and `icons` are REGENERATORS, not gates** — they rewrite committed
artefacts (`public/pouya-lajevardi-bio.pdf`, `public/favicon.ico`) rather than
checking anything, so they are run deliberately and their output is committed.
⚠️ **`LOCAL ONLY` above means a different thing for each, so do not read the two
labels as one.** `bio:pdf` **cannot** run in CI — it drives a browser. `icons`
is pure Node and **could**; it is out of the build because a build should not
silently rewrite an artefact a human approved, which is why it is labelled
`REGENERATOR` rather than `LOCAL ONLY`. It regenerates the favicon **only** — the touch icon
is hand-made and must stay opaque cream (`docs/reference/brand-assets.md`
§The icon set).
**`og:proof` and `check:intake` exist because two facts in this repo are
deliberately duplicated**, and a duplicated fact needs a mechanism rather than a
comment. `og:proof` compares each generated OG card's headline against its page's
@@ -363,7 +375,7 @@ an operator to delete the three records that authenticate outbound mail —
under the heading "Never delete".
**A measurement is a claim about your instrument until you check the
instrument.** This has now cost six times, and the shape is identical every
instrument.** This has now cost eight times, and the shape is identical every
time: a number that looks like a finding, from a probe nobody validated.
- `timeout 60 ls "$DRIVE"`**the command never ran.** `timeout` is not
@@ -391,6 +403,20 @@ time: a number that looks like a finding, from a probe nobody validated.
from the list. **A `grep -o` window count is not an enumeration** — to count
occurrences of a string, iterate every match position, or `grep -o` the bare
string with no context window.
- **An ink bounding box of `48x48` filling the whole 48 px favicon** — which
reads as *the regeneration blew the mark up to the full square*. The probe
identified ink as "differs from cream", which is **correct on a matted icon and
meaningless on a transparent one**, because `alpha = 0` pixels still carry RGB
`(0,0,0)`. **Nothing about the probe changed — the class of input did**, and a
probe cannot tell you that. Composite over a known ground first, so both things
compared are the kind of thing the instrument was built for.
- **A contrast ratio of `14.02:1` for the mark's darkest ink** — computed from a
raw channel value **without asking what alpha it is painted at**. Exactly one
pixel carried that colour and its alpha was 251; a `>= 250` filter let it
through and the ratio was then taken as if it were opaque. **It is a number
about a colour that is never painted.** Composite against the actual ground
before measuring contrast, and take "the ink colour" only from fully opaque
pixels.
So before acting on a number: say what it is a number *of*; confirm the command
actually ran and read its exit status; and check it against a second method that
+60 -15
View File
@@ -395,8 +395,22 @@ Then invalidate `/*`.
> reversing them puts 22 of 23 pages behind a 403 for as long as a CloudFront
> deployment takes.
> 🛑 **TWO THINGS BLOCK THIS ENTIRE LIST AS AT 2026-09-02: ONE WAITING PERIOD
> AND ONE READ-THROUGH.**
> 🛑 **ONE THING BLOCKS THIS ENTIRE LIST AS AT 2026-09-02, AND IT IS A WAITING
> PERIOD RATHER THAN A TASK: Q60.**
>
> ✅ **THE READ-THROUGH IS COMPLETE — Pouya, 2026-09-02, and it returned ONE
> FINDING WHICH WAS NOT COPY.** `public/favicon.ico` shipped with no
> transparency; fixed and verified the same day (see **Favicon set complete**
> below). 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**. That discharges blocker 2 and every
> wording sign-off that had been routed into it.
>
> ⚠️ **THE COUNT NOW READS ONE AGAIN, AND THE EARLIER ONE WAS A DEFECT — READ
> THE REASON, NOT THE NUMBER.** It said ONE earlier on 2026-09-02 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, which is the point the note below has been making all day.
>
> ⚠️ *(The count has moved repeatedly in one day and the DIRECTION is the only
> part worth reading — the number of moves is deliberately not stated, because a
@@ -421,12 +435,12 @@ Then invalidate `/*`.
> record is written and it does not call failure before **7 days**, so
> **start it before anything else on this page.** It is the one blocker
> that is a waiting period rather than a task.
> 2. **Pouya has not yet read every page against `AGENTS.md` §4.** The human
> pass the other half of D20, and not delegable. **It is also where the
> §Who can see it approval now lands:** Pouya ruled on 2026-09-02 that the
> read-through *is* the approval and that nothing is to be held open waiting
> on a separate wording sign-off. The item under **Copy and claims** below
> carries what to read first and why.
> 2. ✅ **DONE 2026-09-02 — Pouya read every page against `AGENTS.md` §4.** The
> human pass, the other half of D20 and not delegable. It was also where the
> §Who can see it approval was routed, and his ruling that the read-through
> *is* the approval means that sign-off is now discharged rather than
> pending. **Sole finding: the favicon's opaque ground.** No copy finding on
> any of the 23 pages.
>
> ✅ **CLOSED 2026-09-02 — Q64, MOOT.** It asked whether anyone else holds the
> AWS root password or its MFA device, because the page published *"has no
@@ -650,12 +664,25 @@ the decision is re-readable rather than re-litigated.
count has been wrong twice, and this line carried "eight" for a round after
the comment itself had been corrected to nine (`adversarial-reviewer`,
round 2). Read the list, not a number
- [ ] **Pouya has read every page against `AGENTS.md` §4.** The human pass. It is
the other half of D20 and it is not delegable — his reading is what the
per-step audit was traded for.
⚠️ **START WITH `/legal/privacy/` §Who can see it. IT IS BLOCKER 2 IN THE
CALLOUT ABOVE, AND THIS READ *IS* THE APPROVAL** — Pouya ruled on
2026-09-02 that nothing waits on a separate wording sign-off. Every
- [x] **DONE 2026-09-02 — Pouya has read every page against `AGENTS.md` §4.**
The human pass, the other half of D20 and not delegable — his reading is
what the per-step audit was traded for. **It returned one finding across 23
pages and that finding was not copy:** the favicon shipped with an opaque
cream ground. **The `/legal/privacy/` §Who can see it wording, the
SML Company Ltd consent line and `/med-arb/` as shipped are approved by
this read**, per his ruling that the read-through *is* the approval.
⚠️ **This does NOT discharge `claims-auditor`'s cutover pass**, which is a
separate item on this list: 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.
~~⚠️ **START WITH `/legal/privacy/` §Who can see it. IT IS BLOCKER 2 IN THE
CALLOUT ABOVE, AND THIS READ *IS* THE APPROVAL**~~ — **struck 2026-09-02:
the pass is DONE, and an unstruck imperative on a ticked item told an
operator to begin a read this page also records as finished, pointing at a
blocker that no longer exists** (`adversarial-reviewer`, round 2). What it
said remains true of what happened: Pouya ruled on
2026-09-02 that nothing waits on a separate wording sign-off, and he read
§Who can see it first. Every
sentence in it changed three times that day — Q62's ruling, Q63's, then the
ruling that cut it to **four plain statements** — and it is the only
section on the site whose subject lives entirely outside this repository.
@@ -972,7 +999,25 @@ the decision is re-readable rather than re-litigated.
instead. A `Disallow` will not do it: a blocked URL can still be listed.
Found by `adversarial-reviewer`, 2026-08-31
- [ ] Booking link works, including the no-JavaScript fallback — **conditional on R6**; booking is parked and `CONTACT.bookingUrl` is `null`, so nothing renders and this passes vacuously until a tool is chosen. **Nothing on `/contact/` mentions booking**, deliberately
- [ ] Favicon set complete
- [x] ✅ **Favicon set complete, and REGENERATED 2026-09-02 — it had shipped with
no transparency at all.** Pouya's read-through finding. All three frames
(16/32/48) declared a 32-bit alpha channel and then carried `alpha = 255`
on every pixel, the ground opaque cream — so the tab icon showed as a cream
rectangle on any dark tab strip. `public/favicon.ico` is now transparent,
regenerated by `npm run icons` from the committed master and verified
programmatically and by eye, on dark grounds and light.
⚠️ **`public/apple-touch-icon.png` STAYS OPAQUE CREAM AND MUST NOT BE
"FIXED" TO MATCH.** The reason is a platform behaviour — iOS composites a
transparent touch icon onto black — **stated by Pouya on 2026-09-02 and not
re-tested on a handset**; the item directly below is where it would be. The
touch icon is byte-identical across this change.
⚠️ **AND THE CHANGE IS NOT FREE ON DARK.** The maroon half of the ribbon
effectively drops out against a dark tab strip; the champagne half carries
the mark. **The figures are deliberately NOT repeated here** — they live in
`docs/reference/brand-assets.md` §The icon set, with the method, and a copy
on this page had already gone stale within a day by quoting the 32 px row
as if it were the general case (`adversarial-reviewer`, round 2). Read them
there.
- [ ] Tested on iOS Safari, Android Chrome, desktop Safari/Chrome/Firefox
- [ ] Tested at 320 px and at 200% zoom
- [x] ✅ **THE 200%-TEXT NAV OVERFLOW IS FIXED, 2026-09-01 — THIS ITEM IS
+148 -3
View File
@@ -5,9 +5,22 @@ is `CLAUDE.md`'s rule and `AGENTS.md` R14, and this file exists because the
infinity mark was reconstructed wrongly and **two adversarial review passes could
not catch it**, since the real artwork was not in the repo to compare against.
**Every measurement below is `[verified 2026-08-26]`** — computed with `sharp`
against the files in this repository, and re-derivable by anyone from the
commands given. Nothing here is quoted from an external source.
**The measurements here are computed with `sharp` against the files in this
repository**, and are re-derivable by anyone from the commands given.
**Unless a section says otherwise, every figure below is `[verified
2026-08-26]`. Where a section carries its own stamp, that stamp wins** — the
render ladders are `[measured 2026-08-27]` and §The icon set is `[verified
2026-09-02]`. ⚠️ *This line has been wrong in both directions in one week: first
as a blanket 2026-08-26 that was already stale for the ladders, then as
"each section carries its own stamp" when five sections carry none. It is a floor
plus overrides because that is the only shape that covers every section without
mis-dating one* (`adversarial-reviewer`, rounds 1 and 2).
⚠️ **Two things in §The icon set are NOT read off this repository and are
labelled where they appear:** the tab-strip colours used in the contrast figures,
and iOS's handling of a transparent touch icon. Neither is derivable from
anything committed, so neither is stamped as if it were.
## Files
@@ -103,6 +116,138 @@ Passing an explicit `width` is load-bearing: without it Astro emits the
untouched 2668 px master as the `<img src>` fallback — **1,146,406 bytes**
which any client without AVIF or WebP support would actually download.
## The icon set
`[verified 2026-09-02 — every figure below read off the icon files in this
repository, EXCEPT the two external constants flagged inline]`
| File | What it is | Ground |
|---|---|---|
| `public/favicon.ico` | 16, 32 and 48 px frames, each a PNG-encoded 32-bit RGBA image inside the ICO container | **Transparent** |
| `public/apple-touch-icon.png` | 180 × 180 | **Opaque cream `#faf7f2`, and that is deliberate — see below** |
**The two grounds differ on purpose, and this is the line that stops someone
"fixing" it.** ⚠️ *The reason is an external platform behaviour, not a repository
fact, and it is recorded here as what it is:* **iOS does not honour transparency
in a touch icon — it composites it onto black**, so a transparent touch icon
ships a black tile on the home screen. Stated by Pouya in his ruling of
**2026-09-02** and not independently re-tested here; **it has not been checked on
a handset in this repository, and the place that does check it is the unticked
`docs/06` item "Tested on iOS Safari…".** The tab favicon has the opposite
requirement: a tab strip is dark for many readers, and an opaque ground shows
there as a visible rectangle around the mark. So the favicon is transparent, the
touch icon is matted, and **neither should be changed to match the other.**
### Composition
The mark spans **7/8 of the canvas width**, centred on both axes — 14/16, 28/32,
42/48, and 158/180 on the touch icon. That was measured off the icons as they
already shipped, so a regeneration reproduces the composition rather than
restyling the mark.
### Regenerating
```
npm run icons
```
`scripts/icons.mjs` resizes `src/assets/brand/sml-infinity-mark.png` onto a
transparent square canvas and assembles the ICO container itself, because
`sharp` does not write `.ico`. **It writes the favicon only** and never touches
the touch icon. It refuses to run if the render source has no alpha channel, and
it asserts that the source is still byte-identical to the documented crop of
`sml-infinity-mark-master.png` — R14, so the icon stays traceable to committed
artwork. It verifies a **candidate** file and renames it into place only on
success, so a rejected build cannot replace a good favicon.
**It also carries the halo test**, because the check that matters was living
only in prose here while the generator could not run it
(`adversarial-reviewer`, round 2). For each frame it takes every painted pixel
touching a fully transparent one and measures how many sit within 20 of cream:
| | boundary px | near cream | share |
|---|---|---|---|
| correct, 16 / 32 / 48 px | 61 / 146 / 258 | 1 / 2 / 5 | **1.6 / 1.4 / 1.9 %** |
| haloed fixture, 16 px | 56 | 11 | **19.6 %** |
The gate is **10 %**, roughly 5× clear of both. ⚠️ **The first version of this
guard inspected only partial-alpha pixels and MISSED the fixture completely** —
a knockout sets alpha per pixel and leaves **no partial alpha at all**, so there
was nothing for it to look at. The fixture is built by matting the mark on cream
and then knocking the ground out by alpha, which is the defect exactly; it fires
at 19.6 % and leaves `public/favicon.ico` untouched. **A guard that cannot see
the defect it is named for is worse than none**, and this one passed the real
icon while blind, which is the shape that ends a check instead of starting one.
### Why the favicon was regenerated, 2026-09-02
It had **no transparency at all**: all three frames declared a 32-bit alpha
channel and then carried `alpha = 255` on every one of their 256 / 1,024 / 2,304
pixels, with the ground opaque cream `rgba(250,247,242,255)`. Found by Pouya on a
read-through, confirmed by parsing the container directly.
Three measurements stand behind the replacement.
**1. The composition did not change.** Composited back onto cream, the new icon
reproduces the old matted one to within **1 of 255 on every channel of every
pixel** at all three sizes (mean delta 0.010.02, 0 pixels over a delta of 8).
So the regeneration did not restyle, rescale or reposition the mark.
⚠️ **That is NOT a test for a halo, and an earlier draft of this file filed it as
one.** Composite-onto-cream returns ~0 whether the edge is correct *or* is a
cream-matted edge that has merely had its background knocked out — the second
case composites straight back to what it came from. Both branches pass, so the
test cannot discriminate. Found by `adversarial-reviewer`, 2026-09-02.
**2. No halo — the measurement that does discriminate.** Read the RGB the
partial-alpha pixels actually carry. A cream halo means that RGB is near cream; a
correct export means it is the ribbon's own colour. Of the partial-alpha pixels,
**0 of 94 / 300 / 603** are within 12 of `rgb(250,247,242)`; the nearest is 13
away, the mean distance is **174.0 / 178.3 / 177.0**, and the mean colour per
frame is about `rgb(118,86,74)` — maroon-brown, not cream and not black.
**3. Legible on dark and on light, and NOT at the same cost.** ⚠️ *The ground
colours below are external constants, not repository facts: `#202124` is
Chrome's dark tab strip and `#f1f3f4` its light one, both `[observed
2026-09-02]`.* All ratios are computed on **composited** pixels, and the ink
colours are **fully opaque** pixels only — a ratio taken from a raw channel value
is a claim about a colour that is never painted, which is how an earlier draft
came to quote `14.02:1` for a pixel whose alpha is 251.
| | darkest opaque ink | lightest opaque ink | vs `#202124` | vs `#f1f3f4` |
|---|---|---|---|---|
| 16 px | `rgb(70,33,33)` | `rgb(216,188,143)` | champagne **8.82:1** | maroon **12.58:1** |
| 32 px | `rgb(60,29,29)` | `rgb(234,214,172)` | champagne **11.28:1** | maroon **13.62:1** |
| 48 px | `rgb(61,26,26)` | `rgb(238,219,175)` | champagne **11.80:1** | maroon **13.86:1** |
Whichever ground it sits on, one end of the ribbon carries the silhouette. **But
the other end does not merely dim — on dark it goes.** The maroon lobe
composites to **1.041.15:1**, and counting pixels that reach 3:1 against each
ground gives **23 / 87 / 189** on dark against **40 / 168 / 378** on light, at
16 / 32 / 48 px. **So about half as many pixels reach 3:1 on dark as on light.**
⚠️ *Those pixels are not absent — they are painted and fall below 3:1, and the
mark's full outline is still there, dim. An earlier draft said "roughly half the
mark's visible pixels are absent", which is neither what was counted (pixels at
`alpha > 0` are 109 / 388 / 805) nor what a render shows* — `adversarial-reviewer`,
round 2. The previous opaque-cream icon was still more legible on dark.
**That is the price of the change and it was accepted, not overlooked.** What was
bought is the removal of a cream rectangle from every dark tab strip and from
white. Raised by `adversarial-reviewer`, 2026-09-02, against a draft that
recorded the change as costless.
**The obvious alternative is an icon pair keyed on `prefers-color-scheme`, and it
was NOT ruled out on measurement.** ⚠️ *An earlier draft dismissed it on a
mechanism that is wrong: it said the query keys off the page's colour scheme. It
does not — it reports the user's system or browser preference, and this site
declares `color-scheme` nowhere (`git grep -n color-scheme -- src`, exit 1), so
it would resolve to the same preference that makes the tab strip dark.* The real
objections are **untested here** and are recorded as such: `media` on
`<link rel="icon">` is unevenly supported for raster icons, a browser theme can
be set independently of the system preference, and it doubles the artefact
`npm run icons` has to keep in sync. **If this cost is ever revisited, that is
the option to test** — do not re-dismiss it on the reason struck above.
## The colours are the artwork's, not the palette's
`tokens.css` is not involved. The ribbon carries its own gradient and it is
+2 -1
View File
@@ -20,7 +20,8 @@
"lighthouse": "node scripts/lighthouse.mjs",
"og:proof": "node scripts/og-proof.mjs",
"check:intake": "node scripts/check-intake.mjs",
"bio:pdf": "node scripts/bio-pdf.mjs"
"bio:pdf": "node scripts/bio-pdf.mjs",
"icons": "node scripts/icons.mjs"
},
"dependencies": {
"@astrojs/mdx": "^7.0.8",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

+278
View File
@@ -0,0 +1,278 @@
/**
* Regenerates `public/favicon.ico` from the committed brand master.
*
* LOCAL ONLY, like `bio:pdf`. Not wired into `npm run build` or either deploy
* path — the icons are committed artefacts and this is what re-derives them.
*
* Writes the favicon ONLY. `apple-touch-icon.png` is deliberately not touched
* and must stay opaque — `docs/reference/brand-assets.md` §The icon set.
*/
import { readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import sharp from 'sharp';
const root = fileURLToPath(new URL('..', import.meta.url));
const MARK = `${root}src/assets/brand/sml-infinity-mark.png`;
const MASTER = `${root}src/assets/brand/sml-infinity-mark-master.png`;
const OUT = `${root}public/favicon.ico`;
/** Sizes carried in the container, ascending — the order BaseLayout declares. */
const SIZES = [16, 32, 48];
/**
* The mark spans 7/8 of the canvas and is centred on both axes. Not a taste
* decision at this point: it is the composition already shipping, measured off
* the previous icon at all three sizes (14/16, 28/32, 42/48) and off the touch
* icon (158/180). Regenerating for alpha must not also restyle the mark.
*/
const INK_FRACTION = 7 / 8;
/** `--cream` — the ground the previous icon was matted against. */
const CREAM = [250, 247, 242];
const die = (msg) => {
rmSync(`${OUT}.candidate`, { force: true });
console.error(`icons: ${msg}`);
process.exit(1);
};
/**
* R14 — the icon must be traceable to the artwork in this repository, not to a
* file on someone's disk. The render source is a tight crop of the master, so
* assert it still IS that crop before deriving anything from it.
*/
async function assertProvenance() {
const mark = await sharp(MARK).metadata();
const crop = { left: 159, top: 646, width: 2668, height: 1704 };
if (mark.width !== crop.width || mark.height !== crop.height) {
die(
`render source is ${mark.width}x${mark.height}, expected ${crop.width}x${crop.height}`,
);
}
const [a, b] = await Promise.all([
sharp(MASTER).extract(crop).raw().toBuffer(),
sharp(MARK).raw().toBuffer(),
]);
if (!a.equals(b))
die('render source is no longer the documented crop of the master');
console.log(
`provenance: ${crop.width}x${crop.height} at (${crop.left},${crop.top}) of the master — identical`,
);
}
/**
* The whole point of the regeneration. A source without alpha would mean
* deriving a mask from the cream ground, which is a different and lossier job —
* so fail rather than silently ship a matted icon again.
*/
async function loadMark() {
const meta = await sharp(MARK).metadata();
if (!meta.hasAlpha)
die(
`${MARK} has no alpha channel — cannot export a transparent icon from it`,
);
const { data, info } = await sharp(MARK)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
let transparent = 0;
for (let p = 3; p < data.length; p += 4) if (data[p] === 0) transparent++;
if (transparent === 0)
die(`${MARK} declares alpha but every pixel is opaque`);
console.log(
`source: ${info.width}x${info.height} alpha, ${transparent} fully transparent px`,
);
}
/**
* Resize onto a TRANSPARENT canvas. sharp premultiplies around the resample, so
* the ribbon's anti-aliased edge blends toward its own colour rather than
* toward the RGB sitting under alpha 0 — that is the cream halo this change
* exists to remove, and it would come straight back with a matted background.
*/
async function frame(size) {
const w = Math.round(size * INK_FRACTION);
const png = await sharp(MARK)
.resize({
width: w,
kernel: 'lanczos3',
fit: 'inside',
withoutEnlargement: false,
})
.toBuffer();
const { height: h } = await sharp(png).metadata();
if (h > size) die(`size ${size}: mark is ${w}x${h}, taller than the canvas`);
const left = Math.round((size - w) / 2);
const top = Math.round((size - h) / 2);
const out = await sharp({
create: {
width: size,
height: size,
channels: 4,
background: { r: 0, g: 0, b: 0, alpha: 0 },
},
})
.composite([{ input: png, left, top }])
.png({ compressionLevel: 9, effort: 10, palette: false })
.toBuffer();
console.log(
` ${size}x${size}: mark ${w}x${h} at (${left},${top}), ${out.length} B`,
);
return out;
}
/** ICO container: 6-byte header, one 16-byte directory entry per frame, then the PNG payloads. */
function buildIco(frames) {
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0); // reserved
header.writeUInt16LE(1, 2); // type 1 = icon
header.writeUInt16LE(frames.length, 4);
const dir = Buffer.alloc(16 * frames.length);
let offset = header.length + dir.length;
frames.forEach(({ size, png }, i) => {
const e = i * 16;
dir[e] = size; // width — a byte; 0 would mean 256, which SIZES never is
dir[e + 1] = size; // height
dir[e + 2] = 0; // palette size — 0 for truecolour
dir[e + 3] = 0; // reserved
dir.writeUInt16LE(1, e + 4); // colour planes
dir.writeUInt16LE(32, e + 6); // bits per pixel
dir.writeUInt32LE(png.length, e + 8);
dir.writeUInt32LE(offset, e + 12);
offset += png.length;
});
return Buffer.concat([header, dir, ...frames.map((f) => f.png)]);
}
/**
* Re-read the container FROM DISK and decode each frame, rather than inspecting
* the buffers we just built — a check that reads its own inputs proves nothing.
* (It is still `sharp` decoding `sharp`'s output, so it is not a second
* instrument. The independent reads are in `docs/reference/brand-assets.md`.)
*/
async function verify(path) {
const buf = readFileSync(path);
const count = buf.readUInt16LE(4);
if (count !== SIZES.length)
die(`container declares ${count} frames, expected ${SIZES.length}`);
for (let i = 0; i < count; i++) {
const e = 6 + i * 16;
const size = buf[e];
const len = buf.readUInt32LE(e + 8);
const off = buf.readUInt32LE(e + 12);
if (off + len > buf.length)
die(`frame ${i}: range ${off}+${len} exceeds ${buf.length} B`);
const { data, info } = await sharp(buf.subarray(off, off + len))
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
if (info.width !== size || info.height !== size)
die(`frame ${i}: decoded ${info.width}x${info.height}, dir says ${size}`);
const corners = [
[0, 0],
[size - 1, 0],
[0, size - 1],
[size - 1, size - 1],
];
for (const [x, y] of corners) {
const a = data[(y * size + x) * 4 + 3];
if (a !== 0)
die(`frame ${size}: corner (${x},${y}) has alpha ${a}, expected 0`);
}
/*
* THE CORNER AND TRANSPARENCY CHECKS CANNOT SEE A CREAM HALO. A frame whose
* edge was matted against cream and then had its background knocked out has
* clear corners, transparent pixels and opaque pixels, and passes every one
* of them. What distinguishes it is the colour the edge blends TOWARD.
*
* ⚠️ AND IT IS THE BOUNDARY, NOT THE PARTIAL-ALPHA PIXELS. A first version
* of this guard inspected only pixels at 0 < alpha < 255 and MISSED a
* purpose-built haloed fixture entirely, because a knockout sets alpha per
* pixel and leaves NO partial alpha at all — 0 such pixels in the fixture.
* A guard that cannot see the defect it is named for is worse than none.
*
* So: take every painted pixel that touches a fully transparent one, and
* measure how many sit near cream. Measured on this artwork — correct
* frames 1 / 2 / 5 of 61 / 146 / 258 boundary pixels (1.4-1.9%); the haloed
* fixture 33 of 115 (28.7%). The gate is 10%, roughly 5x clear of both.
*/
const NEAR_CREAM = 20;
const HALO_SHARE = 0.1;
const alphaAt = (x, y) =>
x < 0 || y < 0 || x >= size || y >= size
? 0
: data[(y * size + x) * 4 + 3];
let clear = 0;
let ink = 0;
let boundary = 0;
let boundaryNearCream = 0;
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const i = (y * size + x) * 4;
const a = data[i + 3];
if (a === 0) {
clear++;
continue;
}
if (a === 255) ink++;
const onEdge =
alphaAt(x - 1, y) === 0 ||
alphaAt(x + 1, y) === 0 ||
alphaAt(x, y - 1) === 0 ||
alphaAt(x, y + 1) === 0;
if (!onEdge) continue;
boundary++;
const d = Math.max(
Math.abs(data[i] - CREAM[0]),
Math.abs(data[i + 1] - CREAM[1]),
Math.abs(data[i + 2] - CREAM[2]),
);
if (d <= NEAR_CREAM) boundaryNearCream++;
}
}
const share = boundary === 0 ? 0 : boundaryNearCream / boundary;
if (clear === 0)
die(`frame ${size}: no transparent pixels — the ground is fully matted`);
if (ink === 0)
die(`frame ${size}: no opaque pixels — the mark did not render`);
if (boundary === 0)
die(`frame ${size}: no boundary pixels — cannot test the edge colour`);
if (share > HALO_SHARE)
die(
`frame ${size}: ${boundaryNearCream} of ${boundary} boundary pixels ` +
`(${(share * 100).toFixed(1)}%) sit within ${NEAR_CREAM} of cream — ` +
`the edge was matted against the ground before the ground was removed`,
);
console.log(
` ${size}x${size}: ${clear} transparent, ${ink} opaque, corners clear, ` +
`edge ${(share * 100).toFixed(1)}% near cream`,
);
}
console.log(`verified ${path} (${buf.length} B)`);
}
await assertProvenance();
await loadMark();
console.log('frames:');
const frames = [];
for (const size of SIZES) frames.push({ size, png: await frame(size) });
/*
* Verify a CANDIDATE file, then move it into place. Writing OUT first and
* verifying after would mean a failing check exits 1 having already replaced a
* good favicon with the one it just rejected — and nothing downstream re-checks,
* because this script is deliberately outside the build and both deploy paths.
*/
const candidate = `${OUT}.candidate`;
writeFileSync(candidate, buildIco(frames));
console.log('verify:');
try {
await verify(candidate);
} catch (err) {
rmSync(candidate, { force: true });
throw err;
}
renameSync(candidate, OUT);
console.log(`wrote ${OUT}`);
+5 -1
View File
@@ -56,7 +56,11 @@ const { preloadSerifItalic = false, ...seo } = Astro.props;
/* No SVG favicon. The mark is a shaded ribbon, not flat vector paths, so
there is no honest SVG of it to serve — see InfinityMark.astro and
AGENTS.md Q38. The .ico carries 16/32/48, and is what crawlers request
at the root regardless of what is declared here. */
at the root regardless of what is declared here.
The .ico is transparent and the touch icon is opaque cream ON PURPOSE —
do not change either to match the other:
docs/reference/brand-assets.md §The icon set. */
}
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />