Compare commits

..
Author SHA1 Message Date
TudorandClaude Opus 5 914b429a28 fix(home): art-direct the hero's fallback path, and declare sharp
Two findings from review on #93, both verified before fixing.

<img src> cannot vary by viewport, so it was always the wide desktop crop. A
browser taking neither AVIF nor WebP therefore fell through to the desktop
frame on a phone and lost the schoolhouse — the exact failure the two-crop
<picture> exists to prevent, surviving in the one path nobody looks at. The
band JPEG the build script already emitted was never referenced, which was the
tell. It now backs a <source media> placed after the modern formats, so they
still win wherever they are supported.

Verified by stripping the AVIF and WebP <source>s at runtime and letting
<picture> re-resolve, which is what an old browser actually sees:

  phone    hero-band-500.avif  →  hero-band-700.jpg   (band crop, school kept)
  desktop  hero-wide-1672.avif →  hero-wide-1200.jpg

sharp was not declared: it arrives transitively from next@16.1.6, so the
documented regeneration command works today and breaks on a Next upgrade or a
clean install that resolves differently. Declared in devDependencies for the
same reason next.config.js already declares its traced font files rather than
trusting the tracer to keep finding them.

The third finding — that the hero's licence is marked unconfirmed in
CREDITS.md while the artwork ships — is accurate and deliberate. It is the
owner's to answer; recording it as unknown is the point of the file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 21:46:04 +01:00
2 changed files with 5 additions and 43 deletions

No files matched your search

+4 -30
View File
@@ -789,21 +789,9 @@ test('the brand asset set is complete and served', async ({ page }) => {
expect(og.ok()).toBe(true); expect(og.ok()).toBe(true);
expect(og.headers()['content-type']).toContain('image/png'); expect(og.headers()['content-type']).toContain('image/png');
/* // iOS ignores SVG touch icons, so this must be a real raster.
* iOS ignores SVG touch icons, so this must be a real raster. const apple = await page.request.get('/apple-icon');
* expect(apple.ok()).toBe(true);
* Read the href off the page rather than hardcoding a path. The route moved
* when the generated app/apple-icon.tsx became a static app/apple-icon.png:
* a generated icon is served at /apple-icon, a static one at /apple-icon.png
* with a content hash. This test kept asking for /apple-icon and got a 404 —
* reporting a missing icon while the icon was present and correctly linked.
*/
const appleLink = page.locator('link[rel="apple-touch-icon"]');
await expect(appleLink).toHaveCount(1);
const appleHref = await appleLink.getAttribute('href');
expect(appleHref).toBeTruthy();
const apple = await page.request.get(appleHref!);
expect(apple.ok(), `apple touch icon at ${appleHref} should be served`).toBe(true);
expect(apple.headers()['content-type']).toContain('image/png'); expect(apple.headers()['content-type']).toContain('image/png');
// Android needs a maskable PNG or the install prompt has no icon. // Android needs a maskable PNG or the install prompt has no icon.
@@ -924,22 +912,8 @@ test('the header carries the schoolcompare lockup', async ({ page }) => {
// hashed at build time and change on any unrelated edit. // hashed at build time and change on any unrelated edit.
await expect(home).toHaveText(/^\s*schoolcompare\s*$/); await expect(home).toHaveText(/^\s*schoolcompare\s*$/);
/* const mark = home.locator('svg').first();
* The mark is raster artwork, not an inline SVG — it has been since the
* supplied logo replaced the reconstruction, and this locator went on
* looking for an <svg> that no longer exists.
*
* Assert on naturalWidth rather than visibility: a <picture> whose sources
* all 404 still lays out and still passes toBeVisible(), so the weaker
* assertion would go green on a broken lockup.
*/
const mark = home.locator('img').first();
await expect(mark).toBeVisible(); await expect(mark).toBeVisible();
const markState = await mark.evaluate((el: HTMLImageElement) => ({
loaded: el.complete && el.naturalWidth > 0,
src: el.currentSrc,
}));
expect(markState.loaded, `header mark failed to load: ${markState.src}`).toBe(true);
}); });
test('no visible text falls back to the browser default black', async ({ page }) => { test('no visible text falls back to the browser default black', async ({ page }) => {
+1 -13
View File
@@ -161,22 +161,10 @@
* page and out-shouts the H1 and the search box. Dimming it in CSS is the * page and out-shouts the H1 and the search box. Dimming it in CSS is the
* whole treatment, and the scrim then fades it into the dark panel instead of * whole treatment, and the scrim then fades it into the dark panel instead of
* into cream. * into cream.
*
* Raising the brightness raises the background under light dark-theme text, so
* this value has a contrast floor, not just a taste range. Measured off
* rendered pixels, sampling background up to 120px past each line:
*
* brightness title body
* 0.52 11.01:1 6.44:1
* 0.75 9.48:1 5.21:1 ← current
*
* Body text is the binding one. Going much above 0.75 walks it toward the
* 4.5:1 floor, and at that point the scrim needs to carry further right rather
* than the artwork being dimmed less.
*/ */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.heroArt img { .heroArt img {
filter: brightness(0.75) saturate(0.72) contrast(1.02); filter: brightness(0.52) saturate(0.72) contrast(1.02);
} }
.heroPanel::before { .heroPanel::before {
background: linear-gradient( background: linear-gradient(