Commit Graph
5 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5 16005054eb fix(web): vendor the web fonts instead of loading them from Google
test-web / test (push) Successful in 42s
app.html linked its stylesheet straight from fonts.googleapis.com, with
preconnects to that host and fonts.gstatic.com. A deployed instance has
no outbound network, so those requests never arrive and the whole UI
renders in fallback faces — Georgia for the display face, whatever the
system has for Inter and JetBrains Mono.

This is invisible in development, which is why it survived: the dev
machine has internet, so the fonts load and everything looks right. Only
a real deployment shows the failure.

tools/vendor-fonts.py fetches the three families once and writes them
under web/static/fonts with a generated stylesheet. static/ is copied
into the SvelteKit build, which Go embeds, so the faces travel inside the
binary. 32 woff2 files, 912K.

Two details that matter for correctness rather than size:

Urls in the generated CSS are relative (./Inter-400-latin.woff2), not
absolute. A url() resolves against the stylesheet's own address, so the
directory keeps working when the app is served under a base path;
/fonts/... would not.

Every subset Google slices is kept, with unicode-range intact. The
browser still fetches only the ranges a page uses, so this costs
repository bytes rather than request bytes — and a library full of
Cyrillic or Greek artist names renders instead of falling back mid-list.

The guard asserts the property, not the vendor: any absolute url in a
resource-loading attribute fails, whoever hosts it, since naming Google
would pass the day someone reached for a different CDN. It also checks
preconnect separately (those carry no fetch of their own, so the url
check misses them), strips HTML comments before asserting an absence so
prose describing the forbidden thing cannot satisfy the check, and pins
the font families to tokens.json rather than a hardcoded list.

Falsified against the pre-change app.html: it trips both the external-url
and preconnect assertions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
2026-09-09 13:49:47 -04:00
bvandeusenandClaude Opus 5 b06a1adfe8 feat(brand): draw a reduced mark so the favicon reads at 16px
The full hat does not resolve below ~32px, which the header worked around
by sizing up. A browser tab cannot: it renders the favicon at 16px and
does not ask. There the mark was a blob.

Deriving a small form from the traced art does not work, and this is the
non-obvious part. Hole-filling, morphological smoothing and dropping
components were all tried; every one of them preserves the overall
silhouette, and the overall silhouette — dominated by a long diagonal
plume — is precisely what fails. The result each time was a diagonal
smear that reads as no object at all.

So the reduced form is drawn rather than derived: a strong horizontal
brim under a crown that peaks left of centre, a band slit so the two do
not fuse, and a short pointed plume. Same lean and proportions as the
full mark, detail removed instead of minified.

favicon.svg and favicon.png now use it; apple-touch, icon-512 and the
Android launcher icons keep the full art, being large enough for it. The
plume carries the accent, which measures 3.04:1 on obsidian and 5.43:1 on
the light ground — both clear of the 3:1 graphics floor.

Also corrects the accent-on-iron figure in the generator's comment from
2.80:1 to 2.70:1. The real --fs-iron is #1E2228; 2.80 came from measuring
against a value I had guessed rather than read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
2026-09-09 13:49:31 -04:00
bvandeusenandClaude Opus 5 5593f7ce17 feat(brand): replace the M mark with the traced bard-hat logo
test-web / test (push) Successful in 44s
android / Build + lint + test (push) Successful in 5m9s
The mark is now a feathered hat with an arc of eighth notes, traced from
the operator's reference artwork at 99.74% IoU. The hat takes the text
colour and the note arc holds the accent — the same construction the M
used, and for the same reason: parchment on a light surface is invisible,
so the silhouette has to flip with its background while the accent stays
constant.

This reverses the subject-neutrality argument recorded in Minstrel's
design system, which held that depicting a bard would tell a new user the
app is for renaissance-faire music and had twice rejected a hat. The
operator commissioned this artwork and chose it with that objection on the
table; the record is updated rather than silently contradicted.

Both accent-filled alternatives were measured and rejected: #4A6B5C is
3.04:1 on obsidian and 2.80:1 on the raised iron, so an accent hat drops
under the 3:1 graphics floor as soon as it sits on a card.

tools/gen-brand-assets.py is the single source for the four copies, which
cannot share a file because each needs a different colour mechanism —
currentColor inlined, a prefers-color-scheme swap in the favicon, literal
fills in mark.svg, flat pixels in the rasters. Hand-copying 20KB of path
data four ways is how a silhouette change lands in three of them.

Two notes on the trace, both non-obvious: it runs on the original
antialiased greyscale rather than a binary mask, because tracing a
supersampled mask scores ~100% IoU by reproducing the pixel staircase
exactly — a perfect number for jagged art at 120KB of path, versus 99.74%
at 20KB. And potrace reads PBM where bit 1 is black, so the ink mask is
inverted going in; backwards, it traces the background and still emits a
plausible-looking SVG.

The header lockup moves 20px → 28px: the hat carries far more detail than
the M and does not resolve below ~32px. The 16px browser-tab favicon is
still a blob at that size and is not addressed here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
2026-09-09 12:52:57 -04:00
bvandeusenandClaude Opus 4.7 750bc69204 chore(deps): Tier A cross-ecosystem patch/minor sweep
No expected behavior changes; CI verifies. Tracked in Fable #463
under audit note #460.

Flutter (pubspec.yaml):
- flutter_secure_storage ^10.1.0 → ^10.2.0
- mocktail ^1.0.4 → ^1.0.5

Web (package.json + package-lock.json):
- @sveltejs/adapter-static ^3.0.6 → ^3.0.10
- @types/node ^25.6.0 → ^25.9.1
- autoprefixer ^10.4.20 → ^10.5.0
- postcss ^8.4.49 → ^8.5.15
- svelte-check ^4.0.5 → ^4.4.8

Tools (package.json + package-lock.json):
- sharp ^0.33.5 → ^0.34.5 (the lockfile diff is large because
  sharp ships pre-built binaries for many platform/arch combos;
  each version revs all those entries)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:58:24 -04:00
bvandeusen 574bf30504 feat(web/m7-363): OG share-image placeholder + one-off generator 2026-05-03 17:47:05 -04:00