From 043506cb6b1d5ac696176a4cf33099465a55fcdb Mon Sep 17 00:00:00 2001 From: Tudor Date: Fri, 14 Aug 2026 21:46:04 +0100 Subject: [PATCH 1/2] fix(home): art-direct the hero's fallback path, and declare sharp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two findings from review on #93, both verified before fixing. 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 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 placed after the modern formats, so they still win wherever they are supported. Verified by stripping the AVIF and WebP s at runtime and letting 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 --- nextjs-app/components/Illustration.tsx | 12 ++++++++++++ nextjs-app/package-lock.json | 11 ++++++----- nextjs-app/package.json | 3 ++- nextjs-app/scripts/build-hero-images.js | 12 ++++++++++-- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/nextjs-app/components/Illustration.tsx b/nextjs-app/components/Illustration.tsx index cc3963c..2f85d6a 100644 --- a/nextjs-app/components/Illustration.tsx +++ b/nextjs-app/components/Illustration.tsx @@ -60,6 +60,18 @@ export function HeroIllustration() { srcSet="/brand/hero-band-500.webp 500w, /brand/hero-band-700.webp 700w, /brand/hero-band-972.webp 972w" sizes={SIZES} /> + {/* + The band's JPEG, and the reason it exists. + + cannot vary by viewport, so it is always the wide crop. A + browser that takes neither AVIF nor WebP would therefore fall through + to the desktop frame on a phone and lose the schoolhouse — the exact + failure this whole component is arranged to prevent, surviving in the + one path nobody looks at. No `type` here, so it matches anywhere the + media query does, and it sits after the modern formats so they still + win where supported. + */} + =18" } @@ -4160,8 +4161,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -8584,9 +8585,9 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", @@ -8629,8 +8630,8 @@ "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "devOptional": true, "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, diff --git a/nextjs-app/package.json b/nextjs-app/package.json index 45a2434..c2ff2ea 100644 --- a/nextjs-app/package.json +++ b/nextjs-app/package.json @@ -36,6 +36,7 @@ "@types/jest": "^30.0.0", "@types/leaflet": "^1.9.21", "jest": "^30.2.0", - "jest-environment-jsdom": "^30.2.0" + "jest-environment-jsdom": "^30.2.0", + "sharp": "^0.34.5" } } diff --git a/nextjs-app/scripts/build-hero-images.js b/nextjs-app/scripts/build-hero-images.js index 91b7043..a87f458 100644 --- a/nextjs-app/scripts/build-hero-images.js +++ b/nextjs-app/scripts/build-hero-images.js @@ -25,8 +25,16 @@ * squeezing further toward 4.9:1 crops the empty sides, not the subject. * * AVIF and WebP are both emitted. This is flat-shaded vector-style artwork, so - * AVIF lands roughly an order of magnitude under the source PNG; the JPEG is - * only the fallback and is never served to a current browser. + * AVIF lands roughly an order of magnitude under the source PNG. Both crops + * also get a JPEG: the wide one backs the , and the band one backs a + * — without it a browser taking neither modern format would + * fall through to the desktop frame on a phone, since cannot vary by + * viewport. + * + * sharp is declared in devDependencies rather than leaned on as a transitive + * dependency of next. It does currently arrive that way, but relying on that + * means a Next upgrade can silently break this script — the same reasoning as + * the outputFileTracingIncludes note in next.config.js. */ const path = require('path'); From bdaa05cd542f563ef74c45307cd8f7fc465193c9 Mon Sep 17 00:00:00 2001 From: Tudor Date: Fri, 14 Aug 2026 22:26:23 +0100 Subject: [PATCH 2/2] style(home): lift the hero artwork's dark-theme brightness to 0.75 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At 0.52 the scene was legible but heavily suppressed; 0.75 lets the hills, path and schoolhouse read while the white H1 stays comfortably the brightest thing on the panel. Re-measured rather than assumed, because in the dark theme the text is light and the artwork is behind it — brightening the image lowers text contrast rather than raising it. Off rendered pixels, sampling background up to 120px past each line's right edge: brightness title body 0.52 11.01:1 6.44:1 0.75 9.48:1 5.21:1 Both still clear the 4.5:1 floor, body being the binding one. The trade is recorded next to the value so the next person to reach for it knows it has a floor and not just a taste range. Co-Authored-By: Claude Opus 5 --- nextjs-app/components/HomeView.module.css | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/nextjs-app/components/HomeView.module.css b/nextjs-app/components/HomeView.module.css index a964d3e..a710b98 100644 --- a/nextjs-app/components/HomeView.module.css +++ b/nextjs-app/components/HomeView.module.css @@ -161,10 +161,22 @@ * 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 * 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) { .heroArt img { - filter: brightness(0.52) saturate(0.72) contrast(1.02); + filter: brightness(0.75) saturate(0.72) contrast(1.02); } .heroPanel::before { background: linear-gradient(