Files
school_compare/nextjs-app/components/Illustration.tsx
TudorandClaude Opus 5 eddf74745f
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m2s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 43s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 11s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 13s
feat(home): swap in the higher-fidelity hero artwork
Same 16:9 frame, but not a drop-in replacement — the composition, palette and
copy area all moved, and each one drives a change here.

COPY AREA

The old artwork was a flat #FDF9F3 down its whole left side. This one runs
peach #FEE8D2 at the top to cream #FDF3E7 around 43% height, and below ~48%
the left edge is foliage rather than cream at all. --hero-ground is resampled
to #FEF2E1, the middle of the pale run where the headline and search actually
sit; the scrim covers the foliage further down.

Measured on rendered pixels with the text hidden, sampling the real glyph runs
(via Range, not the element boxes) plus a 120px growth margin:

           title      body      byline
  light    13.61:1    5.74:1    6.66:1
  dark      9.49:1    5.45:1    8.35:1

Body drops from 6.52:1 to 5.74:1 as the foliage comes closer, still clear.

BAND CROP

The schoolhouse moved to ~82% across the frame, leaving only ~140px of artwork
to its right, so the band crop is anchored to the right edge and takes 1344px
back — putting the school at 73%, which the build script now derives and
prints rather than leaving it to drift from the CSS.

The crop is also 3.2:1 rather than matching the phone. The band is not one
ratio: it runs 2.4:1 on a phone to about 3.8:1 under the one-column
breakpoint. Cropping at the narrow end means the wide end throws away height,
which cut the flag off the roof and the base off the building. Sitting above
the middle costs a little width on phones — where the crop's left is hillside —
and keeps the building whole where it matters.

BAND HEIGHTS

Raised to 13rem (861–860px) and 10rem (≤640px). The band was widest-per-height
at exactly 640px, where an 8.5rem band measured 4.2:1 — worse than any wider
viewport, because the height steps down at that breakpoint while the width does
not. Ratios across the range are now 2.0 / 2.6 / 3.6 / 3.1 / 3.8 rather than
spiking. The search still lands at 442px on a 667px viewport.

WEIGHT

Source is 3.1MB; a browser fetches one file — 29–59kB on desktop, 10–14kB on a
phone. Widths re-cut for the larger master: 2000/1400/1000 wide, 1344/900/600
band.

Verified: 0 AA failures across both themes at 1440 / 860 / 390, every srcset
entry present on disk with no orphans, tsc clean, 159/159, build green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 09:22:37 +01:00

104 lines
4.2 KiB
TypeScript

/**
* The landing hero artwork.
*
* This is the supplied illustration, not a drawing of one — see
* assets/hero-source.png and scripts/build-hero-images.js, which produces
* everything under public/brand/hero-*. Regenerate rather than hand-editing.
*
* Server-safe by construction: no 'use client', no hooks, no handlers, so it
* renders in the RSC pass and never reaches the client bundle.
*
* TWO CROPS, ONE <picture>
* ------------------------
* The slot is two different shapes. On desktop the artwork sits behind the
* whole panel at roughly 2.1:1 → 2.7:1, with the headline over the empty cream
* area the illustration reserves on its left. Below the one-column breakpoint
* it becomes a band between 2.6:1 and 4.9:1, under the search rather than
* behind it.
*
* A single file under `object-fit: cover` centre-crops, and at the band's
* extreme that slices a strip through the middle of the scene and loses the
* schoolhouse — exactly how the previous SVG hero failed on phones. So the
* <source media> switches crop, not just resolution: `band` is pre-cropped
* around the school and is already near 2.6:1, so the narrow band barely
* crops it further.
*
* `object-position` is set in CSS per breakpoint and is load-bearing — see
* .heroArt in HomeView.module.css.
*
* The artwork is decorative: the proposition beside it carries the meaning, so
* alt is empty and it is hidden from assistive tech rather than described.
*/
/*
* The panel is not the viewport. `.main` is capped at 1400px with 1.5rem of
* padding (1rem under 768px), so the artwork's box is that minus the padding —
* describing it as 100vw over-requests by ~48px worth of candidate at every
* width, which on a slow connection is a larger file than needed for the LCP
* element.
*/
const SIZES = '(min-width: 1400px) 1352px, (min-width: 769px) calc(100vw - 3rem), calc(100vw - 2rem)';
/** Intrinsic size of the wide crop — the aspect hint that prevents reflow. */
const WIDE_W = 2000;
const WIDE_H = 1125;
export function HeroIllustration() {
return (
<picture>
{/* Band crop first: <source> is first-match-wins, so the narrow-viewport
rules have to precede the unconstrained desktop ones. */}
<source
media="(max-width: 860px)"
type="image/avif"
srcSet="/brand/hero-band-600.avif 600w, /brand/hero-band-900.avif 900w, /brand/hero-band-1344.avif 1344w"
sizes={SIZES}
/>
<source
media="(max-width: 860px)"
type="image/webp"
srcSet="/brand/hero-band-600.webp 600w, /brand/hero-band-900.webp 900w, /brand/hero-band-1344.webp 1344w"
sizes={SIZES}
/>
{/*
The band's JPEG, and the reason it exists.
<img src> 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.
*/}
<source media="(max-width: 860px)" srcSet="/brand/hero-band-900.jpg" />
<source
type="image/avif"
srcSet="/brand/hero-wide-1000.avif 1000w, /brand/hero-wide-1400.avif 1400w, /brand/hero-wide-2000.avif 2000w"
sizes={SIZES}
/>
<source
type="image/webp"
srcSet="/brand/hero-wide-1000.webp 1000w, /brand/hero-wide-1400.webp 1400w, /brand/hero-wide-2000.webp 2000w"
sizes={SIZES}
/>
{/*
The hero is the largest thing above the fold, so it is almost certainly
the LCP element: fetchPriority high, and never lazy. eslint's
no-img-element wants next/image, which cannot art-direct between two
different crops — that is the whole point of the <picture> above.
*/}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/brand/hero-wide-1400.jpg"
alt=""
aria-hidden="true"
width={WIDE_W}
height={WIDE_H}
decoding="async"
fetchPriority="high"
/>
</picture>
);
}