Replaces the hand-drawn SVG landscape with the illustration supplied by the project owner. Master is nextjs-app/assets/hero-source.png; everything under public/brand/hero-* is produced by scripts/build-hero-images.js and should never be hand-edited.
This is the one commit that missed #92 — it landed on that branch just after it merged.
Checked the artwork before building on it
Sampled rather than eyeballed. It is genuinely in the palette already:
in the artwork
our token
hills #0C8986
--brand#0F766E
sky #C4E6F7
--sky#C7EBF5
The one thing that was not: its copy area is #FDF9F3, while Sand is #F5EFE6 — 8 to 13 points darker per channel. Fading the panel's Sand into the image leaves a visible seam straight down the hero, so --hero-ground is sampled from the artwork itself.
Layout
The artwork reserves an empty cream area down its left side for the headline. The old hero was a two-column grid with art in the right 0.88fr, which would have cropped that reserved area off and shrunk the scene into a thumbnail — the one thing the picture is composed not to be. The panel is now a single layered block: artwork behind, copy on top.
That does not stretch to phones. Below 860px the panel is too narrow for the copy to stay inside the cream, so the scrim would have to cover nearly the whole image and you would be left with a tinted rectangle with a picture faintly behind it. There, the artwork leaves the background and becomes a band under the search.
Two crops, because the slot is two shapes
Roughly 2.1:1–2.7:1 behind the desktop panel, 2.6:1–4.9:1 as the band. A single file under object-fit: cover centre-crops, and at the band's extreme that slices a strip through the scene and loses the schoolhouse — exactly how the drawn hero failed on phones.
So <picture> switches crop, not just resolution. The band file is pre-cropped around the school, already near 2.6:1, with the subject at ~65% across, so squeezing toward 4.9:1 eats the empty sides instead of the subject. Verified in a browser at 4.53:1: school fully in frame.
Contrast, measured on rendered pixels
The CSS-based probe cannot see through a partly-transparent scrim, so this was measured off the rendered image — sampling background up to 120px beyond each line's right edge, so a longer line still has margin:
title
body
light
14.36:1
6.52:1
dark
11.01:1
6.44:1
Worst light case is hillside green showing through the scrim, at 6.52:1.
Weight
1.6 MB PNG in → 23–33 kB on desktop, 9–18 kB on a phone. A browser fetches exactly one file; the JPEG is only the <img> fallback. sizes now describes the real panel box (max-width: 1400 minus padding) rather than 100vw, which was over-requesting a candidate at every width on what is almost certainly the LCP element.
Dark theme
A raster cannot be re-graded token by token the way the drawing was, but undimmed it reproduces the same failure the drawn hero shipped with — a bright illustration is the brightest object on a near-black page and out-shouts the H1 and the search box. It is dimmed in CSS to read as dusk, and the scrim fades it into the dark panel rather than into cream.
Tests
The two illustration journeys are replaced by three, each covering a failure that still renders a valid-looking page:
the artwork actually loads (asserts naturalWidth, not src — a 404 leaves an empty box and a perfectly good src)
the crop switches at the breakpoint (hero-wide vs hero-band), not just the resolution
a modern format wins negotiation — falling back to the JPEG costs ~24kB on the LCP element and looks identical
the dark theme dims it
Verification
No local server, so this was verified through the offline render harness: real compiled CSS, real server-rendered markup, screenshotted at 1440 / 1024 / 860 / 390 in both themes. tsc clean, 159/159 unit tests, build green, 41 e2e journeys parse — all re-run after the cherry-pick onto current main.
One thing for you
public/brand/CREDITS.md records provenance for every asset in that folder. The hero's licence line is marked unconfirmed — if the artwork was bought, generated or commissioned, that is worth recording now rather than trying to reconstruct later. It is the most prominent image on the site.
Replaces the hand-drawn SVG landscape with the illustration supplied by the project owner. Master is `nextjs-app/assets/hero-source.png`; everything under `public/brand/hero-*` is produced by `scripts/build-hero-images.js` and should never be hand-edited.
This is the one commit that missed #92 — it landed on that branch just after it merged.
## Checked the artwork before building on it
Sampled rather than eyeballed. It is genuinely in the palette already:
| in the artwork | our token |
|---|---|
| hills `#0C8986` | `--brand` `#0F766E` |
| sky `#C4E6F7` | `--sky` `#C7EBF5` |
The one thing that was not: its copy area is `#FDF9F3`, while Sand is `#F5EFE6` — 8 to 13 points darker per channel. Fading the panel's Sand into the image leaves a visible seam straight down the hero, so `--hero-ground` is sampled from the artwork itself.
## Layout
The artwork reserves an empty cream area down its left side for the headline. The old hero was a two-column grid with art in the right `0.88fr`, which would have cropped that reserved area off and shrunk the scene into a thumbnail — the one thing the picture is composed not to be. The panel is now a single layered block: artwork behind, copy on top.
That does not stretch to phones. Below 860px the panel is too narrow for the copy to stay inside the cream, so the scrim would have to cover nearly the whole image and you would be left with a tinted rectangle with a picture faintly behind it. There, the artwork leaves the background and becomes a band under the search.
## Two crops, because the slot is two shapes
Roughly **2.1:1–2.7:1** behind the desktop panel, **2.6:1–4.9:1** as the band. A single file under `object-fit: cover` centre-crops, and at the band's extreme that slices a strip through the scene and loses the schoolhouse — exactly how the drawn hero failed on phones.
So `<picture>` switches **crop, not just resolution**. The band file is pre-cropped around the school, already near 2.6:1, with the subject at ~65% across, so squeezing toward 4.9:1 eats the empty sides instead of the subject. Verified in a browser at 4.53:1: school fully in frame.
## Contrast, measured on rendered pixels
The CSS-based probe cannot see through a partly-transparent scrim, so this was measured off the rendered image — sampling background up to **120px beyond** each line's right edge, so a longer line still has margin:
| | title | body |
|---|---|---|
| light | 14.36:1 | 6.52:1 |
| dark | 11.01:1 | 6.44:1 |
Worst light case is hillside green showing through the scrim, at 6.52:1.
## Weight
**1.6 MB PNG in → 23–33 kB on desktop, 9–18 kB on a phone.** A browser fetches exactly one file; the JPEG is only the `<img>` fallback. `sizes` now describes the real panel box (`max-width: 1400` minus padding) rather than `100vw`, which was over-requesting a candidate at every width on what is almost certainly the LCP element.
## Dark theme
A raster cannot be re-graded token by token the way the drawing was, but undimmed it reproduces the same failure the drawn hero shipped with — a bright illustration is the brightest object on a near-black page and out-shouts the H1 and the search box. It is dimmed in CSS to read as dusk, and the scrim fades it into the dark panel rather than into cream.
## Tests
The two illustration journeys are replaced by three, each covering a failure that still renders a valid-looking page:
- the artwork actually **loads** (asserts `naturalWidth`, not `src` — a 404 leaves an empty box and a perfectly good `src`)
- the **crop switches** at the breakpoint (`hero-wide` vs `hero-band`), not just the resolution
- a **modern format** wins negotiation — falling back to the JPEG costs ~24kB on the LCP element and looks identical
- the dark theme **dims** it
## Verification
No local server, so this was verified through the offline render harness: real compiled CSS, real server-rendered markup, screenshotted at 1440 / 1024 / 860 / 390 in both themes. `tsc` clean, 159/159 unit tests, build green, 41 e2e journeys parse — all re-run after the cherry-pick onto current main.
## One thing for you
`public/brand/CREDITS.md` records provenance for every asset in that folder. The hero's **licence line is marked unconfirmed** — if the artwork was bought, generated or commissioned, that is worth recording now rather than trying to reconstruct later. It is the most prominent image on the site.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Replaces the hand-drawn SVG landscape with the illustration supplied by the
project owner. Master is assets/hero-source.png; everything under
public/brand/hero-* comes from scripts/build-hero-images.js and should never be
hand-edited.
LAYOUT
The artwork is composed as a full-bleed hero: it reserves an empty cream area
down its left side for the headline. The old hero was a two-column grid with
art in the right 0.88fr, which would have cropped that reserved area off and
shrunk the scene into a thumbnail — the one thing the picture is built not to
be. So the panel is now a single layered block: artwork behind, copy on top,
and below the one-column breakpoint the artwork leaves the background and
becomes a band under the search.
Overlaying does not survive down to phone widths — the panel gets too narrow
for the copy to stay inside the cream, so the scrim would have to cover nearly
the whole image and you would be left with a tinted rectangle. Hence the
switch at 860px rather than a single treatment stretched across every width.
CONTRAST
The copy sits on a gradient of --hero-ground, a new token sampled from the
artwork's own cream (#FDF9F3) rather than from Sand. Sand is eight to thirteen
points darker per channel, which leaves a visible seam straight down the hero.
The scrim exists because the artwork is a fixed image on a fluid panel: past
some width the headline would otherwise land on hillside green.
Measured on rendered pixels, sampling background up to 120px beyond the right
edge of each line, so a longer line still has margin:
light title 14.36:1 body 6.52:1
dark title 11.01:1 body 6.44:1
The worst light case is hillside green showing through the scrim at 6.52:1.
TWO CROPS
The slot is two shapes: roughly 2.1:1–2.7:1 behind the desktop panel, and
2.6:1–4.9:1 as the band. A single file under object-fit: cover centre-crops,
and at the band's extreme that slices a strip through the scene and loses the
schoolhouse — exactly how the drawn hero failed on phones. <picture> switches
crop, not just resolution: the band file is pre-cropped around the school and
is already near 2.6:1, with the subject at ~65% across so squeezing toward
4.9:1 crops the empty sides instead.
WEIGHT
1.6 MB PNG in, AVIF and WebP out; a browser fetches exactly one file — about
23–33 kB on desktop, 9–18 kB on a phone. The JPEG is only the <img> fallback.
`sizes` describes the real panel box (max-width 1400 minus padding) rather than
100vw, which over-requested a candidate at every width on the LCP element.
DARK THEME
A raster cannot be re-graded token by token the way the drawing was, but it
would reproduce the same failure — a bright illustration is the brightest
object on a near-black page. It is dimmed in CSS to read as dusk, and the
scrim fades it into the dark panel rather than into cream.
TESTS
The two illustration journeys are replaced by three, each covering a failure
that still renders a valid-looking page: the artwork actually loading
(naturalWidth, not src), the crop switching at the breakpoint, a modern format
winning negotiation, and the dark theme dimming it.
public/brand/CREDITS.md records provenance for every asset in that folder. The
hero's licence line is marked unconfirmed — that one needs the owner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tudor
merged commit 0466986195 into main2026-08-14 20:41:46 +00:00
This PR replaces the hand-drawn SVG hero landscape with a supplied raster illustration, wiring up a two-crop (wide desktop background vs. pre-cropped mobile band) with AVIF/WebP/JPEG fallbacks, a matching build script, updated CSS tokens/scrim for the new artwork, and rewritten e2e tests. The change is self-contained to the frontend hero and is otherwise solid — layout, dark-mode gating (both consistently keyed off prefers-color-scheme), sizes attributes, and token cleanup all check out — but has a few non-blocking rough edges.
🟡 Minor
nextjs-app/components/Illustration.tsx: The 's fallback always points at /brand/hero-wide-1200.jpg regardless of viewport, so any browser without AVIF/WebP support gets the wide desktop crop even on narrow viewports, reintroducing the 'schoolhouse cropped off on phones' failure this PR exists to fix for that fallback path. hero-band-700.jpg is generated by the build script but never referenced.
nextjs-app/scripts/build-hero-images.js: The script requires 'sharp', but it isn't declared in nextjs-app/package.json (dependencies or devDependencies), so the documented regeneration command (node scripts/build-hero-images.js) will fail with module-not-found on a clean install.
nextjs-app/public/brand/CREDITS.md: The hero artwork's licence is explicitly flagged 'to be confirmed by the project owner' while the derived assets are already committed and shipped to production as the site's most prominent image.
## 🤖 AI Code Review (Claude Code)
This PR replaces the hand-drawn SVG hero landscape with a supplied raster illustration, wiring up a two-crop <picture> (wide desktop background vs. pre-cropped mobile band) with AVIF/WebP/JPEG fallbacks, a matching build script, updated CSS tokens/scrim for the new artwork, and rewritten e2e tests. The change is self-contained to the frontend hero and is otherwise solid — layout, dark-mode gating (both consistently keyed off prefers-color-scheme), sizes attributes, and token cleanup all check out — but has a few non-blocking rough edges.
### 🟡 Minor
- **nextjs-app/components/Illustration.tsx**: The <picture>'s fallback <img> always points at /brand/hero-wide-1200.jpg regardless of viewport, so any browser without AVIF/WebP support gets the wide desktop crop even on narrow viewports, reintroducing the 'schoolhouse cropped off on phones' failure this PR exists to fix for that fallback path. hero-band-700.jpg is generated by the build script but never referenced.
- **nextjs-app/scripts/build-hero-images.js**: The script requires 'sharp', but it isn't declared in nextjs-app/package.json (dependencies or devDependencies), so the documented regeneration command (`node scripts/build-hero-images.js`) will fail with module-not-found on a clean install.
- **nextjs-app/public/brand/CREDITS.md**: The hero artwork's licence is explicitly flagged 'to be confirmed by the project owner' while the derived assets are already committed and shipped to production as the site's most prominent image.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Replaces the hand-drawn SVG landscape with the illustration supplied by the project owner. Master is
nextjs-app/assets/hero-source.png; everything underpublic/brand/hero-*is produced byscripts/build-hero-images.jsand should never be hand-edited.This is the one commit that missed #92 — it landed on that branch just after it merged.
Checked the artwork before building on it
Sampled rather than eyeballed. It is genuinely in the palette already:
#0C8986--brand#0F766E#C4E6F7--sky#C7EBF5The one thing that was not: its copy area is
#FDF9F3, while Sand is#F5EFE6— 8 to 13 points darker per channel. Fading the panel's Sand into the image leaves a visible seam straight down the hero, so--hero-groundis sampled from the artwork itself.Layout
The artwork reserves an empty cream area down its left side for the headline. The old hero was a two-column grid with art in the right
0.88fr, which would have cropped that reserved area off and shrunk the scene into a thumbnail — the one thing the picture is composed not to be. The panel is now a single layered block: artwork behind, copy on top.That does not stretch to phones. Below 860px the panel is too narrow for the copy to stay inside the cream, so the scrim would have to cover nearly the whole image and you would be left with a tinted rectangle with a picture faintly behind it. There, the artwork leaves the background and becomes a band under the search.
Two crops, because the slot is two shapes
Roughly 2.1:1–2.7:1 behind the desktop panel, 2.6:1–4.9:1 as the band. A single file under
object-fit: covercentre-crops, and at the band's extreme that slices a strip through the scene and loses the schoolhouse — exactly how the drawn hero failed on phones.So
<picture>switches crop, not just resolution. The band file is pre-cropped around the school, already near 2.6:1, with the subject at ~65% across, so squeezing toward 4.9:1 eats the empty sides instead of the subject. Verified in a browser at 4.53:1: school fully in frame.Contrast, measured on rendered pixels
The CSS-based probe cannot see through a partly-transparent scrim, so this was measured off the rendered image — sampling background up to 120px beyond each line's right edge, so a longer line still has margin:
Worst light case is hillside green showing through the scrim, at 6.52:1.
Weight
1.6 MB PNG in → 23–33 kB on desktop, 9–18 kB on a phone. A browser fetches exactly one file; the JPEG is only the
<img>fallback.sizesnow describes the real panel box (max-width: 1400minus padding) rather than100vw, which was over-requesting a candidate at every width on what is almost certainly the LCP element.Dark theme
A raster cannot be re-graded token by token the way the drawing was, but undimmed it reproduces the same failure the drawn hero shipped with — a bright illustration is the brightest object on a near-black page and out-shouts the H1 and the search box. It is dimmed in CSS to read as dusk, and the scrim fades it into the dark panel rather than into cream.
Tests
The two illustration journeys are replaced by three, each covering a failure that still renders a valid-looking page:
naturalWidth, notsrc— a 404 leaves an empty box and a perfectly goodsrc)hero-widevshero-band), not just the resolutionVerification
No local server, so this was verified through the offline render harness: real compiled CSS, real server-rendered markup, screenshotted at 1440 / 1024 / 860 / 390 in both themes.
tscclean, 159/159 unit tests, build green, 41 e2e journeys parse — all re-run after the cherry-pick onto current main.One thing for you
public/brand/CREDITS.mdrecords provenance for every asset in that folder. The hero's licence line is marked unconfirmed — if the artwork was bought, generated or commissioned, that is worth recording now rather than trying to reconstruct later. It is the most prominent image on the site.🤖 Generated with Claude Code
🤖 AI Code Review (Claude Code)
This PR replaces the hand-drawn SVG hero landscape with a supplied raster illustration, wiring up a two-crop (wide desktop background vs. pre-cropped mobile band) with AVIF/WebP/JPEG fallbacks, a matching build script, updated CSS tokens/scrim for the new artwork, and rewritten e2e tests. The change is self-contained to the frontend hero and is otherwise solid — layout, dark-mode gating (both consistently keyed off prefers-color-scheme), sizes attributes, and token cleanup all check out — but has a few non-blocking rough edges.
🟡 Minor
node scripts/build-hero-images.js) will fail with module-not-found on a clean install.