From 7ae9ecdc363287c0ecd74afd6486bcc7d7cf1bf8 Mon Sep 17 00:00:00 2001 From: Tudor Date: Fri, 28 Aug 2026 15:58:25 +0100 Subject: [PATCH] feat(destinations): colour tokens, with the absence hatched not coloured Activity not captured includes independent schools and moving abroad, so a red segment would be a factual error. The hatch doubles as the secondary encoding that rescues the neutral/blue pair, which separates at only dE 7.6 as flat fills. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BvdDKvFFSZuMVDH5fEyTob --- .../components/darkThemeSafety.test.ts | 28 +++++++++++++++++++ nextjs-app/app/globals.css | 28 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/nextjs-app/__tests__/components/darkThemeSafety.test.ts b/nextjs-app/__tests__/components/darkThemeSafety.test.ts index 60445ee..3d6d889 100644 --- a/nextjs-app/__tests__/components/darkThemeSafety.test.ts +++ b/nextjs-app/__tests__/components/darkThemeSafety.test.ts @@ -76,3 +76,31 @@ describe('dark-theme safety', () => { expect(offenders).toEqual([]); }); }); + +/** + * Destination measures add the first new colour family since the palette was + * set. The tokens have to exist in both blocks or the section renders one + * theme's fills on the other theme's ground — the exact failure the suite + * above exists to catch, but for tokens rather than literals. + */ +describe('destination tokens', () => { + const css = fs.readFileSync( + path.join(__dirname, '..', '..', 'app', 'globals.css'), 'utf8'); + + const TOKENS = [ + '--dest-sixthform', '--dest-sfcollege', '--dest-fecollege', + '--dest-apprentice', '--dest-employment', '--dest-none', '--dest-none-hatch', + ]; + + const DARK_AT = css.indexOf('@media (prefers-color-scheme: dark)'); + + it('defines every destination token in the light palette', () => { + const light = css.slice(0, DARK_AT); + expect(TOKENS.filter((t) => !light.includes(`${t}:`))).toEqual([]); + }); + + it('redefines every destination token for dark', () => { + const dark = css.slice(DARK_AT); + expect(TOKENS.filter((t) => !dark.includes(`${t}:`))).toEqual([]); + }); +}); diff --git a/nextjs-app/app/globals.css b/nextjs-app/app/globals.css index 096e4f6..18ad53c 100644 --- a/nextjs-app/app/globals.css +++ b/nextjs-app/app/globals.css @@ -105,6 +105,23 @@ --series-7: #0E7A86; --series-8: #8A4A6B; + /* ── Destination measures ─────────────────────────────────────────── + Education is one hue in three steps (school-like -> college-like) so the + education destinations read as one family; apprenticeship and employment + are separate hues. The absence is neutral and HATCHED, never a colour: + "activity not captured" covers independent schools, moving abroad and + training DfE holds no data on, so rendering it as a bad outcome would be + a factual error. The hatch is also the secondary encoding that rescues + the neutral/blue pair, which separates at only dE 7.6 as flat fills. + Every other adjacent pair clears dE 10.9 under protanopia. */ + --dest-sixthform: #0F766E; + --dest-sfcollege: #4A9E96; + --dest-fecollege: #7CBFB8; + --dest-apprentice: #806200; + --dest-employment: #2F6F8F; + --dest-none: #6B7580; + --dest-none-hatch: rgba(107, 117, 128, 0.34); + /* ── Phase: category, desaturated so it stays under the status hues ── */ --phase-primary: #0F766E; --phase-primary-bg: rgba(167, 215, 197, 0.40); @@ -293,6 +310,17 @@ --series-7: #6FD0DC; --series-8: #D99BB8; + /* Destinations. Not a naive inversion: the education ramp reverses + direction so its darkest step stays the one furthest from the + school, and each step is re-checked against the dark card. */ + --dest-sixthform: #5FC7BB; + --dest-sfcollege: #3E9B92; + --dest-fecollege: #2A716B; + --dest-apprentice: #EFC658; + --dest-employment: #8FB4D9; + --dest-none: #8B9AA1; + --dest-none-hatch: rgba(139, 154, 161, 0.34); + --phase-primary: #5FC7BB; --phase-primary-bg: rgba(95, 199, 187, 0.16); --phase-primary-text: #8ADACF;