-
v2026.09.09 — a new mark, and both clients stop borrowing their fonts
Stabletest-web / test (push) Successful in 54sandroid / Build + lint + test (push) Successful in 5m36srelease / Build signed APK (tag releases only) (push) Successful in 4m36srelease / Build + push container image (push) Successful in 24srelease / Verify release artifacts (tag releases only) (push) Successful in 2sreleased this
2026-09-09 15:36:09 -04:00 | 0 commits to main since this releasePer-day CalVer. Server + web + Android, and there is no schema migration this time. Merged via PR #128;
mainat237380b1, CI green across every lane.Five commits. One of them changes what the app looks like; the other four fix a thing that was quietly broken everywhere the app runs without internet.
The mark
Minstrel had a placeholder: a Didone M whose right leg was an eighth note. It is now a feathered bard's hat with an arc of notes rising over it, traced from your artwork at 99.74% IoU.
The hat takes the surrounding text colour and the note arc carries the accent. That split is not decoration — it is the whole mechanism. Parchment on a light surface is invisible, so the silhouette has to flip with whatever it sits on, and only the accent stays constant because teal reads against both.
This deliberately overrides the design system. Minstrel's own record argued the mark had to be subject-neutral by requirement — that "Minstrel" pulls toward a lute, a bard, a troubadour, and depicting one tells a new user this is a player for renaissance-faire music. It had rejected a hat twice on exactly those grounds. That objection was put on the table before anything was traced, and you overruled it. Both design-system records were rewritten to match rather than left contradicting what ships, and the house style's account of that test now says plainly that it is an argument the app's owner may overrule — what it forbids is failing it by accident.
Two measured decisions came out of it. The whole-mark-in-accent and hat-in-accent treatments were both rejected because
#4A6B5Cmeasures 2.70:1 on the raised-surface iron, under the 3:1 floor for graphics — an accent-filled hat goes out of contrast the moment it sits on a card. And the source artwork's own deep-forest green is not a Minstrel colour; it was the reference image's background, and it appears nowhere.The favicon is a different drawing
The hat carries far more detail than the M did, and it stops resolving below about 32px. The header lockup was sized up from 20px to 28px to accommodate it. A browser tab cannot be sized up — it renders at 16px and does not negotiate, and there the mark was an unreadable blob.
Deriving a smaller version from the traced art does not work, and every mechanical route was tried first: filling the interior holes, morphological smoothing, dropping components. All of them preserve the overall silhouette, and the overall silhouette — dominated by a long diagonal plume — is exactly what fails. Each attempt produced a diagonal smear that reads as no object at all.
So the small mark is drawn, not shrunk: a strong horizontal brim under a crown that peaks left of centre, a band slit so the two do not fuse into one lump, and a short plume. Same lean and proportions as the full mark, with the detail removed rather than minified. The favicon uses it; every larger consumer keeps the real thing.
Both clients were borrowing their fonts
This is the part that was actually broken.
The web linked its stylesheet from
fonts.googleapis.com. Android resolved all three families through the Play Services font provider. Both fetch over the network, at runtime, on first use.A deployed instance is not guaranteed a network. Those requests never arrive, and the entire UI renders in fallback faces — Georgia standing in for Fraunces on the web, the platform default on the phone. Nothing errors. It reads as a styling regression rather than a missing dependency.
It survived this long because it is invisible while developing: this machine has internet, the fonts load, everything looks correct.
Android carried a second failure on top of that one. The Play Services font provider is absent entirely on devices without Play Services — so on those, text fell back to the system face regardless of how good the network was.
All three families are now vendored and served from the app's own origin: 32 subsetted
woff2for the web (embedded in the Go binary) and 5 static TTFs on Android. The runtime path is removed rather than kept as a fallback — theui-text-google-fontsdependency, its catalogue entry and the provider certificate hashes are all gone. Fraunces, Inter and JetBrains Mono are all SIL Open Font License, which is what makes bundling them the operator's call rather than a question.Two things about fetching fonts turned out to fail by succeeding, and both are now asserted rather than trusted:
Google picks the format from your User-Agent, with no parameter to ask for one. A modern browser string returns
woff2, which Android'sres/fontcannot load at all. The obvious fix — pretend to be an old browser — returns EOT, an Internet Explorer format that downloads happily, has an entirely plausible file size, and is useless here. It took checking magic bytes to notice.And the endpoint silently collapses a multi-weight request to 400 for older clients. Ask for Medium, get Regular: a perfectly valid font file that renders at the wrong weight everywhere, forever.
OS/2 usWeightClassis the only field that tells the two apart, so each weight is now fetched separately and verified against what the code declares beside it.Both clients have guards now, and both were falsified against the broken versions before being committed — the Android one reads its expectations out of
Typography.ktitself, so adding a typeface without vendoring it fails the build.
Install
No migration this time. Redeploy the server image.
Update the Android app. The bundled typefaces and the new launcher icon only arrive with this APK — a
mainpush moves:latestbut does not build one, so nothing before this release carried them.What to look at afterwards
- The header, and the browser tab. The tab icon is the separate small drawing; it should read as a hat at 16px rather than a smudge.
- The launcher icon, including a themed/monochrome launcher if you use one — the mark's base layer doubles as the monochrome version.
- Switch the web UI to light mode. The mark should flip to dark ink while the note arc stays teal. If the hat vanishes into the background, that is the exact bug the construction exists to prevent.
- An instance with no internet, if you have one handy. Headings should be Fraunces, not Georgia. That is the whole point of this release and the only place the old bug was ever visible.
Known, and deliberate
The mark fails the house style's two-second test on purpose. It is recorded that way in both design-system records, with the argument against it kept intact rather than deleted.
The favicon is not a scaled copy of the app icon. It is a separate drawing, so the two are family members rather than identical — that is the intended result, not drift.
The APK grew by about 0.86 MB, which is the cost of not depending on Play Services to draw text.
The Android font guard has never been falsified locally — there is no Gradle run on this workstation, so its predicates were verified by mirroring them against the real files, and CI was the first thing to actually compile it. It passes, but it has only ever been proven to pass, not watched to fail in place.
🤖 Generated with Claude Code
Downloads