Files
Tudor 9ee4d45a94
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 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 50s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 9s
fix(brand): give both logo colourways one canvas so the aspect hint is right
LogoMark derived width from a single hardcoded 153/189, taken from mark.png,
while mark-on-dark.png was 98×112. Both call sites size the mark with
height:100%/width:auto, so the rendered size came out right in the end — but
the width/height attributes are the pre-load aspect hint, so the on-dark
artwork reserved the wrong box and reflowed on load. Any future call site
without that CSS would have rendered it stretched outright.

Per-variant attributes would not have fixed it. variant="auto" is a single
<img> whose srcset swaps the file underneath it, so one set of attributes has
to serve both colourways; the only correct fix is for the two files to share
a ratio.

They also had different artwork ratios (0.810 against 0.833) and different
padding, which the review did not reach: the logo changed size slightly
whenever the OS theme flipped. Both are now written onto one 176×208 canvas
with the artwork at the same height, centred, so they are drop-in swappable.

Measured on the built output, header mark, before → after:
  light   attr 31×38, rendered 30.75×38   →   attr 32×38, rendered 32.15×38
  dark    attr 31×38, rendered 33.25×38   →   attr 32×38, rendered 32.15×38
A 2.25px reflow and a cross-theme size jump both become 0.15px of rounding.

The share card reads the same file and had its own hardcoded 45×56, which the
re-crop would have stretched; it is now 47×56, with a comment tying it to the
canvas. The comment on ASPECT records that re-cropping either file means
re-normalising both.

Verified: tsc clean, 159/159 tests, build green, and both files report
naturalWidth 176 × naturalHeight 208 in the browser.
2026-08-08 22:39:28 +01:00

103 lines
3.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The schoolcompare mark.
*
* This is the supplied brand artwork, not a reconstruction of it. Two
* colourways were extracted from the logo sheet and live in public/brand:
*
* mark.png the primary lockup's mark — teal pin, white window and
* path, green leaves. Reads correctly on Warm White,
* white cards and Sand, and on both dark-theme grounds.
* mark-on-dark.png the on-dark colourway — white pin with the counter
* knocked through to the ground. Required wherever the
* ground is teal, because there the teal pin's silhouette
* disappears and only the white window survives.
*
* Both are written onto one 176×208 canvas with the artwork at the same
* height, so they are drop-in swappable — see ASPECT below for why that
* matters rather than merely being neat.
*
* `variant="auto"` serves the on-dark artwork to dark-theme viewers via a
* <picture> source, so this needs no JavaScript and no client boundary.
*
* The wordmark beside it is live text in Manrope rather than the sheet's
* raster: the written style guide specifies Manrope, and live text stays
* selectable, scales cleanly and recolours with the theme.
*
* RESOLUTION CAVEAT: the largest instance on the supplied sheet is 153×189,
* which is ample for the header (36px), the favicon and the share card, but
* short of a 512px PWA icon — that one is upscaled and is slightly soft. Drop
* a vector (SVG/AI/EPS) into public/brand and regenerate to fix it; every
* consumer goes through this component or public/brand, so it is one swap.
*/
const MARK_LIGHT = '/brand/mark.png';
const MARK_ON_DARK = '/brand/mark-on-dark.png';
/**
* Intrinsic size of the artwork files, used to derive width from height.
*
* Both colourways are deliberately written onto the SAME canvas at the same
* artwork height, so this one ratio is correct for either of them. That is not
* tidiness — it is required. `variant="auto"` renders a single <img> whose
* srcset swaps the file underneath it, so the width/height attributes are
* shared by both colourways and cannot be varied per file. When the two were
* tightly cropped they had different ratios (0.810 and 0.875) and different
* padding, which meant a wrong aspect hint before load — a reflow on load
* under `width: auto` — and a visible jump in logo size whenever the OS theme
* flipped. Re-crop or replace one file and you must re-normalise both.
*/
const ART_WIDTH = 176;
const ART_HEIGHT = 208;
const ASPECT = ART_WIDTH / ART_HEIGHT;
interface LogoMarkProps {
className?: string;
/** Rendered height in px. Width follows the artwork's aspect ratio. */
size?: number;
/**
* Which colourway to serve.
* - `auto` teal pin, swapping to the on-dark artwork in the dark theme
* - `onDark` always the white pin — for teal grounds, which are teal in
* both themes (the footer band)
*/
variant?: 'auto' | 'onDark';
title?: string;
}
export function LogoMark({ className, size = 36, variant = 'auto', title }: LogoMarkProps) {
const height = size;
const width = Math.round(size * ASPECT);
const alt = title ?? '';
if (variant === 'onDark') {
return (
// eslint-disable-next-line @next/next/no-img-element
<img
className={className}
src={MARK_ON_DARK}
width={width}
height={height}
alt={alt}
aria-hidden={title ? undefined : true}
decoding="async"
/>
);
}
return (
<picture>
<source media="(prefers-color-scheme: dark)" srcSet={MARK_ON_DARK} />
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className={className}
src={MARK_LIGHT}
width={width}
height={height}
alt={alt}
aria-hidden={title ? undefined : true}
decoding="async"
/>
</picture>
);
}