fix(design): correct the font assertion and make the Leaflet overrides win
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 8s
PR Checks / Build Backend (no push) (pull_request) Successful in 10s
PR Checks / Build Frontend (no push) (pull_request) Successful in 49s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m10s

Two follow-ups from watching the staging E2E run and re-auditing the live site.

The typeface gate failed, but the site was right. Staging now serves
"Schibsted Grotesk", "Schibsted Grotesk Fallback", …, -apple-system, sans-serif
— exactly what it should. My assertion was /Times|serif$/, and `serif$` matches
the tail of `sans-serif`, so a healthy stack could never pass. It now checks
the FIRST family in the stack (the only one actually requested) and matches
Times as a whole word. Also asserts running prose resolves to Literata, which
the earlier version never covered.

The Leaflet overrides from the previous fix never took effect. leaflet.css is
imported from a client component, so its chunk loads after globals.css; at
equal specificity (both bare `.leaflet-container`) the later sheet wins.
Verified on the live page: my rule sat at stylesheet index 0, Leaflet's at
index 3, and the attribution link was still #0078A8. Prefixing the overrides
with `html` takes them to 0,1,1, which beats a bare class regardless of load
order — injecting that on the live page turned the background to
rgb(233,232,226) and the link to rgb(88,74,155).

The palette gate would have caught this, except it skipped everything inside
.leaflet-container to avoid flagging OSM tile imagery — which also skipped
Leaflet's own chrome. Narrowed the exemption to .leaflet-tile-pane so the map
controls are now covered.

Also dropped the duplicated family name from the font tokens: next/font
already expands the variable to the family plus its metric-matched fallback,
so the stack was naming Schibsted Grotesk twice. It looked like a safety net
but wasn't — an unresolvable var() with no fallback invalidates the whole
declaration, so the literal after it never gets a turn.

Verified against live staging: body and headings resolve to Schibsted Grotesk,
prose to Literata, the search input is --text-primary rather than pure black,
and the higher-specificity Leaflet rules win when injected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Tudor
2026-08-06 14:04:36 +01:00
co-authored by Claude Opus 5
parent 2433101fa0
commit 45a3e7fb8f
2 changed files with 44 additions and 18 deletions
+23 -9
View File
@@ -722,9 +722,16 @@ test('the brand typefaces actually load and apply', async ({ page }) => {
const fonts = await page.evaluate(() => { const fonts = await page.evaluate(() => {
const root = getComputedStyle(document.documentElement); const root = getComputedStyle(document.documentElement);
// Only the FIRST family in the stack is the one actually asked for; the
// rest are fallbacks and always end in a generic like sans-serif.
const first = (el: Element) =>
getComputedStyle(el).fontFamily.split(',')[0].replace(/["']/g, '').trim();
const prose = document.querySelector('[class*="editorialText"] p');
return { return {
body: getComputedStyle(document.body).fontFamily, body: first(document.body),
heading: getComputedStyle(document.querySelector('h1')!).fontFamily, heading: first(document.querySelector('h1')!),
prose: prose ? first(prose) : null,
bodyStack: getComputedStyle(document.body).fontFamily,
displayToken: root.getPropertyValue('--font-display').trim(), displayToken: root.getPropertyValue('--font-display').trim(),
uiToken: root.getPropertyValue('--font-ui').trim(), uiToken: root.getPropertyValue('--font-ui').trim(),
proseToken: root.getPropertyValue('--font-prose').trim(), proseToken: root.getPropertyValue('--font-prose').trim(),
@@ -732,15 +739,22 @@ test('the brand typefaces actually load and apply', async ({ page }) => {
}); });
// An empty token means the var() chain broke, which is the exact failure // An empty token means the var() chain broke, which is the exact failure
// mode above — the computed font-family would look plausible either way. // mode this guards — the computed font-family would look plausible either
// way, because an invalid font-family just inherits.
expect(fonts.displayToken, '--font-display resolved').not.toBe(''); expect(fonts.displayToken, '--font-display resolved').not.toBe('');
expect(fonts.uiToken, '--font-ui resolved').not.toBe(''); expect(fonts.uiToken, '--font-ui resolved').not.toBe('');
expect(fonts.proseToken, '--font-prose resolved').not.toBe(''); expect(fonts.proseToken, '--font-prose resolved').not.toBe('');
expect(fonts.body).toContain('Schibsted'); expect(fonts.body, 'body uses the UI face').toBe('Schibsted Grotesk');
expect(fonts.heading).toContain('Schibsted'); expect(fonts.heading, 'headings use the display face').toBe('Schibsted Grotesk');
// Times/serif anywhere in the interface means we fell back. if (fonts.prose) {
expect(fonts.body).not.toMatch(/Times|serif$/); expect(fonts.prose, 'running prose uses the serif').toBe('Literata');
}
// The Times fallback is the specific failure that shipped. Match the family
// name only — a stack legitimately ends in sans-serif, so anchoring on
// /serif$/ would flag a perfectly healthy page.
expect(fonts.bodyStack).not.toMatch(/\bTimes\b/);
}); });
test('no visible text falls back to the browser default black', async ({ page }) => { test('no visible text falls back to the browser default black', async ({ page }) => {
@@ -754,7 +768,7 @@ test('no visible text falls back to the browser default black', async ({ page })
.filter((el) => { .filter((el) => {
const r = el.getBoundingClientRect(); const r = el.getBoundingClientRect();
if (r.width < 2 || r.height < 2) return false; if (r.width < 2 || r.height < 2) return false;
if (el.closest('.leaflet-container')) return false; if (el.closest('.leaflet-tile-pane')) return false;
return getComputedStyle(el).color === 'rgb(0, 0, 0)'; return getComputedStyle(el).color === 'rgb(0, 0, 0)';
}) })
.map((el) => el.tagName.toLowerCase() + '.' + (el.getAttribute('class') || '').split(' ')[0]) .map((el) => el.tagName.toLowerCase() + '.' + (el.getAttribute('class') || '').split(' ')[0])
@@ -801,7 +815,7 @@ test('rendered colours all come from the token palette', async ({ page }) => {
for (const el of document.querySelectorAll('body *')) { for (const el of document.querySelectorAll('body *')) {
const box = el.getBoundingClientRect(); const box = el.getBoundingClientRect();
if (box.width < 2 || box.height < 2) continue; if (box.width < 2 || box.height < 2) continue;
if (el.closest('.leaflet-container')) continue; // OSM tiles are imagery if (el.closest('.leaflet-tile-pane')) continue; // OSM tiles are imagery, not palette
const s = getComputedStyle(el); const s = getComputedStyle(el);
const checks: Array<[string, string]> = [['color', s.color]]; const checks: Array<[string, string]> = [['color', s.color]];
if (s.backgroundColor !== 'rgba(0, 0, 0, 0)') checks.push(['background', s.backgroundColor]); if (s.backgroundColor !== 'rgba(0, 0, 0, 0)') checks.push(['background', s.backgroundColor]);
+21 -9
View File
@@ -121,10 +121,17 @@
--scrim: rgba(22, 32, 42, 0.55); --scrim: rgba(22, 32, 42, 0.55);
/* ── Type ───────────────────────────────────────────────────────── */ /* ── Type ───────────────────────────────────────────────────────── */
--font-display: var(--font-schibsted), 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, sans-serif; /* next/font already expands --font-schibsted to the family plus its
--font-ui: var(--font-schibsted), 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, sans-serif; metric-matched fallback, so naming the family again here only made the
--font-data: var(--font-schibsted), 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, sans-serif; stack say it twice. It read like a safety net but wasn't one: a var()
--font-prose: var(--font-literata), 'Literata', Georgia, serif; with no fallback that resolves to nothing invalidates the whole
declaration, so the literal after it never gets a turn. The real
safeguard is that these classes sit on <html>, where :root can see
them — see app/layout.tsx. */
--font-display: var(--font-schibsted), -apple-system, BlinkMacSystemFont, sans-serif;
--font-ui: var(--font-schibsted), -apple-system, BlinkMacSystemFont, sans-serif;
--font-data: var(--font-schibsted), -apple-system, BlinkMacSystemFont, sans-serif;
--font-prose: var(--font-literata), Georgia, serif;
/* Type scale, 1.2 ratio off a 1rem base. New work should use these /* Type scale, 1.2 ratio off a 1rem base. New work should use these
rather than inventing another font-size. */ rather than inventing another font-size. */
@@ -421,28 +428,33 @@ table,
* bar would otherwise sit on a near-black page. * bar would otherwise sit on a near-black page.
* *
* The tiles themselves stay as OSM renders them; only the chrome is ours. * The tiles themselves stay as OSM renders them; only the chrome is ours.
*
* Every selector here is prefixed with `html` on purpose. leaflet.css is
* imported from a client component, so its chunk loads AFTER globals.css; at
* equal specificity the later sheet wins and these overrides lose silently.
* The `html` prefix takes them to 0,1,1 so load order stops mattering.
*/ */
.leaflet-container { html .leaflet-container {
background: var(--bg-secondary); background: var(--bg-secondary);
font-family: var(--font-ui); font-family: var(--font-ui);
} }
.leaflet-control-attribution { html .leaflet-control-attribution {
background: rgba(var(--text-inverse-rgb), 0.82); background: rgba(var(--text-inverse-rgb), 0.82);
color: var(--text-muted); color: var(--text-muted);
} }
.leaflet-control-attribution a { html .leaflet-control-attribution a {
color: var(--brand); color: var(--brand);
} }
.leaflet-bar a { html .leaflet-bar a {
background: var(--bg-card); background: var(--bg-card);
color: var(--text-primary); color: var(--text-primary);
border-bottom-color: var(--border); border-bottom-color: var(--border);
} }
.leaflet-bar a:hover { html .leaflet-bar a:hover {
background: var(--bg-secondary); background: var(--bg-secondary);
color: var(--text-primary); color: var(--text-primary);
} }