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(
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');