feat(home): use the supplied hero artwork instead of a drawn one
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 46s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m29s

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>
This commit is contained in:
TudorandClaude Opus 5 committed 2026-08-14 21:36:40 +01:00
1 parent f1819e9c4d
commit 3f0e05cc99
22 files changed
+379 -333

No files matched your search

+105 -45
View File
@@ -77,52 +77,104 @@
}
/* ── Hero ──────────────────────────────────────────────────────────────────
A Sand panel: proposition and search on the left, the brand landscape
bleeding to the panel's right edge. The illustration is decorative, so on
phones it drops to a short band under the content rather than competing
with the search for the fold. */
The artwork fills the panel and the proposition sits on top of it, in the
empty cream area the illustration reserves on its left. It is not a
two-column grid: splitting the panel would crop that reserved area off and
shrink the scene into a thumbnail, which is the one thing the artwork is
composed not to be.
Below the one-column breakpoint the artwork leaves the background and
becomes a band under the search — see the 860px block. */
.heroPanel {
display: grid;
grid-template-columns: 1.12fr 0.88fr;
align-items: stretch;
background: var(--bg-secondary);
position: relative;
isolation: isolate;
background: var(--hero-ground);
border-radius: var(--radius-xl);
overflow: hidden;
}
.heroContent {
align-self: center;
padding: 3rem 1.5rem 3rem 3rem;
position: relative;
z-index: 2;
padding: 3.5rem 1.5rem 3.5rem 3rem;
/* Held inside the artwork's cream area. Beyond roughly this width the text
runs onto the hillside, where the scrim below is doing all the work. */
max-width: min(56%, 40rem);
min-width: 0;
}
.heroArt {
position: relative;
min-width: 0;
/* The scene needs height to read; without a floor it collapses to the
content column's height on short viewports. */
min-height: 24rem;
}
/* Both compositions fill the slot; the breakpoint below picks one. */
.heroArtWide,
.heroArtBand {
position: absolute;
inset: 0;
z-index: 0;
}
.heroArt svg {
position: absolute;
inset: 0;
.heroArt picture,
.heroArt img {
display: block;
width: 100%;
height: 100%;
display: block;
}
/* The band composition only exists below the one-column breakpoint — above
it the slot is tall and the wide drawing is the right picture. */
.heroArtBand {
display: none;
.heroArt img {
object-fit: cover;
/* Centre keeps the schoolhouse in frame at every desktop panel ratio: the
panel runs about 2.1:1 to 2.7:1 against a 1.78:1 image, so cover crops
height only, and the school sits well inside the surviving band. */
object-position: center;
}
/*
* Contrast guarantee.
*
* The artwork's cream area is generous but it is a fixed image on a fluid
* panel — as the panel widens the text block and the cream drift apart, and at
* some width the headline would sit on hillside green. Rather than tune the
* text width per breakpoint and hope, the panel fades its own ground across
* the left of the artwork, so the copy always has a known surface under it.
*
* The gradient starts from --hero-ground, which is sampled from the artwork
* itself, so in the light theme this is invisible — it reads as the
* illustration, not as an overlay on it.
*/
.heroPanel::before {
content: '';
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background: linear-gradient(
to right,
var(--hero-ground) 0%,
rgba(var(--hero-ground-rgb), 0.97) 26%,
rgba(var(--hero-ground-rgb), 0.72) 44%,
rgba(var(--hero-ground-rgb), 0) 62%
);
}
/*
* Dark theme.
*
* The artwork is a fixed raster, so unlike the drawn hero it cannot be
* re-graded token by token — but the failure it would otherwise reproduce is
* the same one: a bright illustration is the brightest object on a near-black
* 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.
*/
@media (prefers-color-scheme: dark) {
.heroArt img {
filter: brightness(0.52) saturate(0.72) contrast(1.02);
}
.heroPanel::before {
background: linear-gradient(
to right,
var(--hero-ground) 0%,
rgba(var(--hero-ground-rgb), 0.96) 30%,
rgba(var(--hero-ground-rgb), 0.66) 50%,
rgba(var(--hero-ground-rgb), 0.15) 78%
);
}
}
.heroEyebrow {
@@ -184,33 +236,41 @@
@media (max-width: 1024px) {
.heroContent {
padding: 2.25rem 1.5rem 2.25rem 2rem;
}
.heroArt {
min-height: 20rem;
padding: 2.75rem 1.5rem 2.75rem 2rem;
max-width: min(64%, 34rem);
}
}
/* One column: content first, then a short band of landscape. */
/*
* One column: the artwork stops being a background and becomes a band under
* the search.
*
* Overlaying at this width does not work — the panel is too narrow for the
* copy to stay inside the artwork's cream area, so the scrim would have to
* cover nearly the whole image, which is just a tinted rectangle with a
* picture faintly behind it. Below the band, <picture> has already switched
* to the pre-cropped file (see Illustration.tsx); this is the layout half of
* the same decision.
*/
@media (max-width: 860px) {
.heroPanel {
grid-template-columns: 1fr;
}
.heroContent {
padding: 2rem 1.5rem 1.75rem;
max-width: none;
}
/* One column: the slot becomes a wide, short band, so swap in the drawing
composed for that crop. See the note at the top of Illustration.tsx. */
.heroArt {
min-height: 11rem;
order: 2;
position: static;
height: 11rem;
}
.heroArtWide {
/* The band crop centres the schoolhouse at about 65% across, so squeezing
toward 4.9:1 crops the empty sides rather than the subject. */
.heroArt img {
object-position: 65% center;
}
/* The scrim exists to protect text sitting on the artwork. Nothing sits on
it here, and leaving it would wash the band out. */
.heroPanel::before {
display: none;
}
.heroArtBand {
display: block;
}
.heroTitle {
font-size: var(--step-3);
max-width: none;
@@ -246,7 +306,7 @@
margin-bottom: 0.5rem;
}
.heroArt {
min-height: 8.5rem;
height: 8.5rem;
}
}