2026-08-07 18:05:49 +01:00
|
|
|
/**
|
2026-08-14 21:35:10 +01:00
|
|
|
* The landing hero artwork.
|
2026-08-07 18:05:49 +01:00
|
|
|
*
|
2026-08-14 21:35:10 +01:00
|
|
|
* 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.
|
2026-08-07 18:05:49 +01:00
|
|
|
*
|
2026-08-14 21:35:10 +01:00
|
|
|
* Server-safe by construction: no 'use client', no hooks, no handlers, so it
|
|
|
|
|
* renders in the RSC pass and never reaches the client bundle.
|
2026-08-14 17:15:09 +01:00
|
|
|
*
|
2026-08-14 21:35:10 +01:00
|
|
|
* 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.
|
2026-08-14 17:15:09 +01:00
|
|
|
*
|
2026-08-14 21:35:10 +01:00
|
|
|
* 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.
|
2026-08-14 17:15:09 +01:00
|
|
|
*
|
2026-08-14 21:35:10 +01:00
|
|
|
* `object-position` is set in CSS per breakpoint and is load-bearing — see
|
|
|
|
|
* .heroArt in HomeView.module.css.
|
2026-08-14 17:15:09 +01:00
|
|
|
*
|
2026-08-14 21:35:10 +01:00
|
|
|
* 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.
|
2026-08-07 18:05:49 +01:00
|
|
|
*/
|
|
|
|
|
|
2026-08-14 21:35:10 +01:00
|
|
|
/*
|
|
|
|
|
* 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.
|
2026-08-07 18:05:49 +01:00
|
|
|
*/
|
2026-08-14 21:35:10 +01:00
|
|
|
const SIZES = '(min-width: 1400px) 1352px, (min-width: 769px) calc(100vw - 3rem), calc(100vw - 2rem)';
|
2026-08-07 18:05:49 +01:00
|
|
|
|
2026-08-14 21:35:10 +01:00
|
|
|
/** Intrinsic size of the wide crop — the aspect hint that prevents reflow. */
|
2026-08-15 09:22:37 +01:00
|
|
|
const WIDE_W = 2000;
|
|
|
|
|
const WIDE_H = 1125;
|
2026-08-07 18:05:49 +01:00
|
|
|
|
|
|
|
|
export function HeroIllustration() {
|
|
|
|
|
return (
|
2026-08-14 21:35:10 +01:00
|
|
|
<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"
|
2026-08-15 09:22:37 +01:00
|
|
|
srcSet="/brand/hero-band-600.avif 600w, /brand/hero-band-900.avif 900w, /brand/hero-band-1344.avif 1344w"
|
2026-08-14 21:35:10 +01:00
|
|
|
sizes={SIZES}
|
2026-08-07 18:05:49 +01:00
|
|
|
/>
|
2026-08-14 21:35:10 +01:00
|
|
|
<source
|
|
|
|
|
media="(max-width: 860px)"
|
|
|
|
|
type="image/webp"
|
2026-08-15 09:22:37 +01:00
|
|
|
srcSet="/brand/hero-band-600.webp 600w, /brand/hero-band-900.webp 900w, /brand/hero-band-1344.webp 1344w"
|
2026-08-14 21:35:10 +01:00
|
|
|
sizes={SIZES}
|
2026-08-07 18:05:49 +01:00
|
|
|
/>
|
2026-08-14 21:46:04 +01:00
|
|
|
{/*
|
|
|
|
|
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.
|
|
|
|
|
*/}
|
2026-08-15 09:22:37 +01:00
|
|
|
<source media="(max-width: 860px)" srcSet="/brand/hero-band-900.jpg" />
|
2026-08-14 21:35:10 +01:00
|
|
|
<source
|
|
|
|
|
type="image/avif"
|
2026-08-15 09:22:37 +01:00
|
|
|
srcSet="/brand/hero-wide-1000.avif 1000w, /brand/hero-wide-1400.avif 1400w, /brand/hero-wide-2000.avif 2000w"
|
2026-08-14 21:35:10 +01:00
|
|
|
sizes={SIZES}
|
2026-08-14 17:15:09 +01:00
|
|
|
/>
|
2026-08-14 21:35:10 +01:00
|
|
|
<source
|
|
|
|
|
type="image/webp"
|
2026-08-15 09:22:37 +01:00
|
|
|
srcSet="/brand/hero-wide-1000.webp 1000w, /brand/hero-wide-1400.webp 1400w, /brand/hero-wide-2000.webp 2000w"
|
2026-08-14 21:35:10 +01:00
|
|
|
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
|
2026-08-15 09:22:37 +01:00
|
|
|
src="/brand/hero-wide-1400.jpg"
|
2026-08-14 21:35:10 +01:00
|
|
|
alt=""
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
width={WIDE_W}
|
|
|
|
|
height={WIDE_H}
|
|
|
|
|
decoding="async"
|
|
|
|
|
fetchPriority="high"
|
|
|
|
|
/>
|
|
|
|
|
</picture>
|
2026-08-07 18:05:49 +01:00
|
|
|
);
|
|
|
|
|
}
|