From 45a3e7fb8f0909bb5910980619ee05f774737b02 Mon Sep 17 00:00:00 2001 From: Tudor Date: Thu, 6 Aug 2026 14:04:36 +0100 Subject: [PATCH] fix(design): correct the font assertion and make the Leaflet overrides win MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- e2e/tests/journeys.spec.ts | 32 +++++++++++++++++++++++--------- nextjs-app/app/globals.css | 30 +++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index 0c8fea2..467f222 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -722,9 +722,16 @@ test('the brand typefaces actually load and apply', async ({ page }) => { const fonts = await page.evaluate(() => { 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 { - body: getComputedStyle(document.body).fontFamily, - heading: getComputedStyle(document.querySelector('h1')!).fontFamily, + body: first(document.body), + heading: first(document.querySelector('h1')!), + prose: prose ? first(prose) : null, + bodyStack: getComputedStyle(document.body).fontFamily, displayToken: root.getPropertyValue('--font-display').trim(), uiToken: root.getPropertyValue('--font-ui').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 - // 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.uiToken, '--font-ui resolved').not.toBe(''); expect(fonts.proseToken, '--font-prose resolved').not.toBe(''); - expect(fonts.body).toContain('Schibsted'); - expect(fonts.heading).toContain('Schibsted'); - // Times/serif anywhere in the interface means we fell back. - expect(fonts.body).not.toMatch(/Times|serif$/); + expect(fonts.body, 'body uses the UI face').toBe('Schibsted Grotesk'); + expect(fonts.heading, 'headings use the display face').toBe('Schibsted Grotesk'); + if (fonts.prose) { + 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 }) => { @@ -754,7 +768,7 @@ test('no visible text falls back to the browser default black', async ({ page }) .filter((el) => { const r = el.getBoundingClientRect(); 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)'; }) .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 *')) { const box = el.getBoundingClientRect(); 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 checks: Array<[string, string]> = [['color', s.color]]; if (s.backgroundColor !== 'rgba(0, 0, 0, 0)') checks.push(['background', s.backgroundColor]); diff --git a/nextjs-app/app/globals.css b/nextjs-app/app/globals.css index 86bd1dc..fc1d7b8 100644 --- a/nextjs-app/app/globals.css +++ b/nextjs-app/app/globals.css @@ -121,10 +121,17 @@ --scrim: rgba(22, 32, 42, 0.55); /* ── Type ───────────────────────────────────────────────────────── */ - --font-display: var(--font-schibsted), 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, sans-serif; - --font-ui: var(--font-schibsted), 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, sans-serif; - --font-data: var(--font-schibsted), 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, sans-serif; - --font-prose: var(--font-literata), 'Literata', Georgia, serif; + /* next/font already expands --font-schibsted to the family plus its + metric-matched fallback, so naming the family again here only made the + stack say it twice. It read like a safety net but wasn't one: a var() + 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 , 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 rather than inventing another font-size. */ @@ -421,28 +428,33 @@ table, * bar would otherwise sit on a near-black page. * * 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); font-family: var(--font-ui); } -.leaflet-control-attribution { +html .leaflet-control-attribution { background: rgba(var(--text-inverse-rgb), 0.82); color: var(--text-muted); } -.leaflet-control-attribution a { +html .leaflet-control-attribution a { color: var(--brand); } -.leaflet-bar a { +html .leaflet-bar a { background: var(--bg-card); color: var(--text-primary); border-bottom-color: var(--border); } -.leaflet-bar a:hover { +html .leaflet-bar a:hover { background: var(--bg-secondary); color: var(--text-primary); }