2026-08-07 18:05:49 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Brand illustration — the landing hero's landscape.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Guideline rules for illustration: no people or faces, soft shapes, rounded
|
|
|
|
|
|
* corners, minimal detail, maximum clarity. The scene reads as "the journey to
|
2026-08-14 17:15:09 +01:00
|
|
|
|
* a school" — a path climbing through layered hills to a small schoolhouse,
|
2026-08-07 18:05:49 +01:00
|
|
|
|
* with a pin marking where you are.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Deliberately server-safe: no 'use client', no hooks, no event handlers, so
|
|
|
|
|
|
* it renders in the RSC pass and never reaches the client bundle.
|
2026-08-14 17:15:09 +01:00
|
|
|
|
*
|
|
|
|
|
|
* TWO COMPOSITIONS, ON PURPOSE
|
|
|
|
|
|
* ----------------------------
|
|
|
|
|
|
* The hero art slot is not one shape. On a wide desktop it is roughly 1.5:1;
|
|
|
|
|
|
* at the 1024px breakpoint about 1.34:1; on a phone it collapses to a band
|
|
|
|
|
|
* near 2.6:1, and just above the one-column breakpoint it is nearly 5:1.
|
|
|
|
|
|
*
|
|
|
|
|
|
* A single drawing cannot cover 1.34:1 → 4.9:1. With `slice`, the SVG scales
|
|
|
|
|
|
* to cover and crops the overflow, anchored here to the bottom edge — so at
|
|
|
|
|
|
* 860×176 a 540×520 scene shows only its bottom 110 user units. That is what
|
|
|
|
|
|
* used to happen: the schoolhouse sat at y 218–324 and was cropped away
|
|
|
|
|
|
* entirely on phones, leaving bare hills and a pin pointing at nothing.
|
|
|
|
|
|
*
|
|
|
|
|
|
* So there are two drawings and CSS shows exactly one. They are both static
|
|
|
|
|
|
* server-rendered SVG, so this costs markup and nothing else — no JS, no
|
|
|
|
|
|
* second request, no layout shift. Each is composed against the crop window
|
|
|
|
|
|
* its own breakpoint actually produces; see the safe-zone note on each.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Colour comes from the --ill-* tokens in globals.css, which are re-graded for
|
|
|
|
|
|
* the dark theme. Everything else about the scene is identical in both themes.
|
2026-08-07 18:05:49 +01:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-08-14 17:15:09 +01:00
|
|
|
|
/** Scoped so the gradient ids can't collide with another inline SVG. */
|
|
|
|
|
|
const SKY_TALL_ID = 'sc-hero-sky-tall';
|
|
|
|
|
|
const SKY_BAND_ID = 'sc-hero-sky-band';
|
|
|
|
|
|
|
|
|
|
|
|
function SkyGradient({ id }: { id: string }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
|
|
|
|
|
|
<stop offset="0" stopColor="var(--ill-sky-high)" />
|
|
|
|
|
|
<stop offset="1" stopColor="var(--ill-sky-low)" />
|
|
|
|
|
|
</linearGradient>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* The schoolhouse. Drawn once, placed by each composition.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Detail deliberately below what it was: the gable clock, the flagpole
|
|
|
|
|
|
* pennant and the four-window grid are gone. At the sizes this actually
|
|
|
|
|
|
* renders they read as noise, and they were what tipped the drawing from
|
|
|
|
|
|
* "soft brand illustration" into clip art.
|
2026-08-07 18:05:49 +01:00
|
|
|
|
*/
|
2026-08-14 17:15:09 +01:00
|
|
|
|
function Schoolhouse({ x, y, scale = 1 }: { x: number; y: number; scale?: number }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<g transform={`translate(${x} ${y}) scale(${scale})`}>
|
|
|
|
|
|
<rect x="12" y="34" width="98" height="58" rx="6" fill="var(--ill-wall)" />
|
|
|
|
|
|
<path d="M2 36 L61 6 L120 36 Z" fill="var(--ill-roof)" />
|
|
|
|
|
|
<rect x="52" y="60" width="20" height="32" rx="3" fill="var(--ill-door)" />
|
|
|
|
|
|
<rect x="26" y="48" width="18" height="16" rx="4" fill="var(--ill-window)" />
|
|
|
|
|
|
<rect x="80" y="48" width="18" height="16" rx="4" fill="var(--ill-window)" />
|
|
|
|
|
|
</g>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-08-07 18:05:49 +01:00
|
|
|
|
|
2026-08-14 17:15:09 +01:00
|
|
|
|
/** The "you are here" pin. The only saturated thing in either composition. */
|
|
|
|
|
|
function Pin({ x, y, scale = 1 }: { x: number; y: number; scale?: number }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<g transform={`translate(${x} ${y}) scale(${scale})`}>
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M21 48 C21 48 42 26 42 15.5 A21 21 0 1 0 0 15.5 C0 26 21 48 21 48 Z"
|
|
|
|
|
|
fill="var(--ill-pin)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<circle cx="21" cy="15.5" r="8" fill="var(--ill-pin-eye)" />
|
|
|
|
|
|
</g>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-08-07 18:05:49 +01:00
|
|
|
|
|
2026-08-14 17:15:09 +01:00
|
|
|
|
/**
|
|
|
|
|
|
* Desktop composition — the two-column hero panel.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Crop window: `slice` anchored to the bottom, so the widest slot (about
|
|
|
|
|
|
* 590×384) reveals y ∈ [169, 520] and the narrowest (about 430×320) reveals
|
|
|
|
|
|
* y ∈ [118, 520]. Everything that carries meaning is therefore kept at
|
|
|
|
|
|
* y ≥ 200; the sky above it exists only to be cropped.
|
|
|
|
|
|
*/
|
2026-08-07 18:05:49 +01:00
|
|
|
|
export function HeroIllustration() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<svg viewBox="0 0 540 520" preserveAspectRatio="xMidYMax slice" aria-hidden="true" focusable="false">
|
|
|
|
|
|
<defs>
|
2026-08-14 17:15:09 +01:00
|
|
|
|
<SkyGradient id={SKY_TALL_ID} />
|
2026-08-07 18:05:49 +01:00
|
|
|
|
</defs>
|
|
|
|
|
|
|
2026-08-14 17:15:09 +01:00
|
|
|
|
<rect width="540" height="520" fill={`url(#${SKY_TALL_ID})`} />
|
2026-08-07 18:05:49 +01:00
|
|
|
|
|
2026-08-14 17:15:09 +01:00
|
|
|
|
{/* Two soft clouds. Low opacity so they never compete with the pin. */}
|
|
|
|
|
|
<g fill="var(--ill-cloud)" opacity="0.7">
|
|
|
|
|
|
<ellipse cx="96" cy="196" rx="34" ry="14" />
|
|
|
|
|
|
<ellipse cx="124" cy="189" rx="24" ry="17" />
|
|
|
|
|
|
<ellipse cx="424" cy="168" rx="28" ry="12" />
|
|
|
|
|
|
<ellipse cx="445" cy="162" rx="19" ry="14" />
|
2026-08-07 18:05:49 +01:00
|
|
|
|
</g>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Four layered rolling hills, palest and furthest first */}
|
2026-08-14 17:15:09 +01:00
|
|
|
|
<path d="M0 300 C 90 264 168 294 246 276 C 330 256 410 282 540 254 L540 520 L0 520 Z" fill="var(--ill-hill-far)" />
|
|
|
|
|
|
<path d="M0 344 C 104 308 186 340 268 324 C 356 306 452 332 540 306 L540 520 L0 520 Z" fill="var(--ill-hill-mid)" />
|
|
|
|
|
|
<path d="M0 408 C 118 378 214 410 306 392 C 400 374 470 398 540 382 L540 520 L0 520 Z" fill="var(--ill-hill-near)" />
|
|
|
|
|
|
<path d="M0 468 C 130 446 236 474 340 458 C 432 444 486 462 540 452 L540 520 L0 520 Z" fill="var(--ill-hill-front)" />
|
2026-08-07 18:05:49 +01:00
|
|
|
|
|
2026-08-14 17:15:09 +01:00
|
|
|
|
{/* The path climbs from the near edge up to the school */}
|
2026-08-07 18:05:49 +01:00
|
|
|
|
<path
|
|
|
|
|
|
d="M214 520 C 202 466 254 448 276 424 C 298 400 274 378 254 366 C 230 352 244 328 276 316"
|
2026-08-14 17:15:09 +01:00
|
|
|
|
stroke="var(--ill-path)"
|
2026-08-07 18:05:49 +01:00
|
|
|
|
strokeWidth="18"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M214 520 C 202 466 254 448 276 424 C 298 400 274 378 254 366 C 230 352 244 328 276 316"
|
2026-08-14 17:15:09 +01:00
|
|
|
|
stroke="var(--ill-path-edge)"
|
2026-08-07 18:05:49 +01:00
|
|
|
|
strokeWidth="18"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
strokeLinecap="butt"
|
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
|
strokeDasharray="0.5 26"
|
|
|
|
|
|
opacity="0.5"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-08-14 17:15:09 +01:00
|
|
|
|
<Schoolhouse x={276} y={232} />
|
2026-08-07 18:05:49 +01:00
|
|
|
|
|
|
|
|
|
|
{/* Scattered rounded trees */}
|
2026-08-14 17:15:09 +01:00
|
|
|
|
<g fill="var(--ill-tree-dark)">
|
2026-08-07 18:05:49 +01:00
|
|
|
|
<circle cx="104" cy="372" r="27" />
|
|
|
|
|
|
<circle cx="136" cy="386" r="19" />
|
|
|
|
|
|
<circle cx="470" cy="336" r="23" />
|
|
|
|
|
|
</g>
|
2026-08-14 17:15:09 +01:00
|
|
|
|
<g fill="var(--ill-tree-light)">
|
2026-08-07 18:05:49 +01:00
|
|
|
|
<circle cx="78" cy="394" r="21" />
|
|
|
|
|
|
<circle cx="492" cy="358" r="17" />
|
|
|
|
|
|
<circle cx="176" cy="420" r="18" />
|
|
|
|
|
|
</g>
|
2026-08-14 17:15:09 +01:00
|
|
|
|
<g fill="var(--ill-tree-mid)">
|
2026-08-07 18:05:49 +01:00
|
|
|
|
<ellipse cx="44" cy="336" rx="15" ry="22" />
|
|
|
|
|
|
<ellipse cx="508" cy="296" rx="13" ry="18" />
|
|
|
|
|
|
<ellipse cx="150" cy="342" rx="12" ry="17" />
|
|
|
|
|
|
</g>
|
2026-08-14 17:15:09 +01:00
|
|
|
|
<g fill="var(--ill-tree-pale)" opacity="0.9">
|
2026-08-07 18:05:49 +01:00
|
|
|
|
<circle cx="410" cy="420" r="16" />
|
|
|
|
|
|
<circle cx="438" cy="430" r="12" />
|
|
|
|
|
|
<circle cx="120" cy="470" r="14" />
|
|
|
|
|
|
</g>
|
|
|
|
|
|
|
2026-08-14 17:15:09 +01:00
|
|
|
|
<Pin x={196} y={372} />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Band composition — the one-column hero, where the art becomes a short strip
|
|
|
|
|
|
* beneath the search.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Crop window: at 358×136 (a phone) `slice` reveals x ∈ [42, 358] of the
|
|
|
|
|
|
* 400-unit width; at 860×176 (just under the one-column breakpoint) it reveals
|
|
|
|
|
|
* the full width but only y ∈ [38, 120]. The intersection — x ∈ [70, 330],
|
|
|
|
|
|
* y ∈ [38, 120] — is where the school and the pin sit. Hills and trees run to
|
|
|
|
|
|
* the edges precisely because they are the parts that may be cropped.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function HeroIllustrationBand() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<svg viewBox="0 0 400 120" preserveAspectRatio="xMidYMax slice" aria-hidden="true" focusable="false">
|
|
|
|
|
|
<defs>
|
|
|
|
|
|
<SkyGradient id={SKY_BAND_ID} />
|
|
|
|
|
|
</defs>
|
|
|
|
|
|
|
|
|
|
|
|
<rect width="400" height="120" fill={`url(#${SKY_BAND_ID})`} />
|
|
|
|
|
|
|
|
|
|
|
|
{/* Hills sit low so the safe zone stays clear for the building */}
|
|
|
|
|
|
<path d="M0 74 C 70 62 120 76 176 70 C 244 62 318 74 400 62 L400 120 L0 120 Z" fill="var(--ill-hill-far)" />
|
|
|
|
|
|
<path d="M0 92 C 84 82 140 94 208 88 C 286 80 340 92 400 84 L400 120 L0 120 Z" fill="var(--ill-hill-mid)" />
|
|
|
|
|
|
<path d="M0 108 C 96 100 168 110 248 104 C 322 98 356 106 400 102 L400 120 L0 120 Z" fill="var(--ill-hill-near)" />
|
|
|
|
|
|
|
|
|
|
|
|
{/* A shorter path — the same idea at band scale, without the switchback,
|
|
|
|
|
|
which turns to mush below about 40 user units of height. */}
|
|
|
|
|
|
<path
|
|
|
|
|
|
d="M150 120 C 156 104 186 100 202 92 C 216 84 210 76 216 70"
|
|
|
|
|
|
stroke="var(--ill-path)"
|
|
|
|
|
|
strokeWidth="9"
|
|
|
|
|
|
fill="none"
|
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<Schoolhouse x={186} y={30} scale={0.52} />
|
|
|
|
|
|
|
|
|
|
|
|
<g fill="var(--ill-tree-dark)">
|
|
|
|
|
|
<circle cx="58" cy="86" r="15" />
|
|
|
|
|
|
<circle cx="78" cy="94" r="11" />
|
|
|
|
|
|
<circle cx="342" cy="80" r="13" />
|
2026-08-07 18:05:49 +01:00
|
|
|
|
</g>
|
2026-08-14 17:15:09 +01:00
|
|
|
|
<g fill="var(--ill-tree-light)">
|
|
|
|
|
|
<circle cx="40" cy="98" r="12" />
|
|
|
|
|
|
<circle cx="360" cy="92" r="10" />
|
|
|
|
|
|
<circle cx="300" cy="100" r="11" />
|
|
|
|
|
|
</g>
|
|
|
|
|
|
<g fill="var(--ill-tree-pale)" opacity="0.9">
|
|
|
|
|
|
<circle cx="112" cy="104" r="9" />
|
|
|
|
|
|
<circle cx="266" cy="108" r="8" />
|
|
|
|
|
|
</g>
|
|
|
|
|
|
|
|
|
|
|
|
<Pin x={124} y={70} scale={0.62} />
|
2026-08-07 18:05:49 +01:00
|
|
|
|
</svg>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|