Implements the direction agreed from the identity board: Route C ("Cohort")
with the paper ground from C1, the Schibsted Grotesk / Literata pairing from
C2, and the iris accent from C3. Dark theme is in scope from the start rather
than retrofitted.
The audit found three things wrong beyond taste:
* No brand asset set. og:image was absent entirely, so every link shared into
a class WhatsApp group rendered as a bare grey card. apple-touch-icon pointed
at an SVG, which iOS ignores, and the manifest shipped no PNGs, so Android
installs had no icon. The header mark and the favicon had also drifted into
two different logos.
* No colour discipline. --primary and --trend-down were the same coral, so the
main CTA and "below average" shared a hue. 58 distinct hex values were spread
across component CSS, and the chart palette was still Chart.js's stock demo
colours.
* A dark theme that was declared but never built — themeColor announced a dark
variant with no dark styling behind it.
What changed:
Colour now has exactly three jobs that never borrow each other's hues: brand
(iris) for interactive and identity, status (teal/amber) for above/below a
comparison point, and phase for categories. Teal/amber rather than green/red
keeps the above/below signal readable for every form of colour blindness.
Every chromatic literal in component CSS is now a token, and the JS-painted
surfaces (Chart.js, Leaflet) read the tokens through lib/theme so they follow
the theme instead of ignoring it.
The mark is the five-bar cohort spread — the same object as the distribution
strip inside a school row, built from opacity steps so it inverts cleanly.
components/Logo.tsx is the single source; the favicon, apple-icon and share
card all derive from its geometry.
globals.css drops 123 dead global classes left over from the vanilla-JS app
(only the btn family, .skip-link and .main were still referenced), along with
the noise overlay. It also gains prefers-reduced-motion support, which was
missing entirely, and a type scale so the 54 ad-hoc font sizes have somewhere
to converge.
Verified: tsc clean, 159 unit tests pass, production build succeeds and
prerenders /icon.svg, /apple-icon and /opengraph-image. Three e2e journeys
added for the asset set, the themeColor/background match, and the dark theme
actually repainting — all silent failures that nothing on the page reveals.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Live axe-core inventory found 13 failing fg/bg pairs (3.12:1 coral text/
fills, 4.0-4.5 teal near-misses, 2.7 gold badge, 1.9 chart-colour text).
All replacement values validated ≥4.5:1 against every background they
sit on:
- --accent-coral-dark deepened to #b04a2e and used for coral text roles
(nav active, back/map links, kickers, chips) and coral fills under
white text (btn-primary, phase tabs, section-nav compare, chart chips);
new --accent-coral-darker #9c3f26 for their hovers. Decorative coral
(pins, bars, borders, focus ring) keeps the brand #e07256.
- --accent-teal darkened to #296f6f (rankings values, metric rows,
ofsted grades, eyebrows all pass).
- Ofsted green #3c8c3c→#2f7a2f; off-token gold #b8920e→--accent-gold-text.
- Gender-split pink text #b45778→#a04a68; admissions tip numerals to a
3.8:1 muted brown (28px display text).
- New CHART_TEXT_COLORS: AA-dark counterparts of the Chart.js series
palette for compare-card values (swatch dots keep true series colour).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
.chartWrapper had height:100% but its parent .chartOuter (a flex
column) had no explicit height — so the canvas couldn't resolve a
real height and fell back to Chart.js's small default, leaving a
big empty band between the plot and the "View raw year-by-year data"
disclosure on desktop.
Give .chartOuter height:100% so it fills the 280px .chartContainer,
and switch .chartWrapper to flex:1 1 auto / min-height:0 so the
canvas fills whatever space remains after the (primary-only) trend
banner. Mobile's explicit .chartWrapper height:220px still wins
inside the responsive override.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous commit added the chip strip and subtitle inside the
.chartOuter, but .chartContainer on the parent SchoolDetailView still
had a fixed height: 220px. With the new content stacking above the
canvas the chart + COVID footnote overflowed past 220px, and the
<details> "View raw year-by-year data" disclosure (rendered just
after the container) landed on top of the plot. It also pushed the
COVID footnote out of the card onto the page background.
- .chartContainer at ≤640px now flows naturally (height: auto)
- .chartWrapper at ≤640px gets an explicit 220px height so the canvas
itself still has a known size for Chart.js to render into.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Results Over Time chart on the school detail page was overcrowded
on phones — 5-item legend wrapping over the plot area, a hidden right
y-axis still rendered for the (collapsed) progress series, fixed
0–100 percentage scale that flattened all variation, angled x-axis
labels eating vertical space.
At ≤640px the chart now renders one metric at a time, selected via a
chip strip above the plot. No more legend. No more dual y-axis. The
y-axis auto-tightens around the actual data range so variation is
visible (a school sitting in the 70–90% band now uses a 65–95 axis
instead of squashing onto a 0–100 line). A small subtitle above the
chips sets the subject context ("KS2 SATs · Reading, Writing & Maths"
or "GCSE results · Year 11") so chip labels can describe the *view*
rather than re-stating the subject.
Chip labels are spelled out in parent-facing language — no internal
shorthand like "RWM":
Primary (KS2): At expected level (default)
Above expected level
Pupil progress (shows 3 series + mini-legend)
Secondary (KS4): Attainment 8 (default)
English & Maths grade 4+
Progress 8
Chips disable themselves (greyed, with a "No data for this school"
title) when the underlying series has no data points. Desktop
behaviour is unchanged — the full multi-series chart with dual y-axis
still renders >640px.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
History chart
- Strip the redundant school-name title (already the page heading)
- Default to 2 visible lines: Reading, Writing & Maths expected % (teal,
bold) + Exceeding (gold, lighter); progress score lines hidden by
default, togglable via legend
- Add dashed national average reference line for RWM (primary) or
Attainment 8 (secondary) so the school's trajectory is always in
context
- Add trend summary chip above the chart computed from the data
("↓ Peaked at 90% (2016/17), currently 70%")
- Add COVID footnote when 2019/20 and 2020/21 data is absent
Ofsted section
- Collapse the four identical "Outstanding / Outstanding / Outstanding /
Outstanding" boxes into a single prose line when all sub-grades match
the overall verdict; show individual cards only when grades differ
SATs sub-metrics
- DeltaChip vs national average on Expected level row for Reading,
Writing and Maths (national averages already in the API response)
Admissions
- Fix label: "Year 3 places per year" → "Reception places per year" for
primary schools
Pupils & Inclusion
- DeltaChip + national avg hint on Eligible for pupil premium and
Pupils receiving SEN support (both keys present in /api/national-averages)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Migrate from vanilla JavaScript SPA to Next.js 16 with App Router
- Add server-side rendering for all pages (Home, Compare, Rankings)
- Create individual school pages with dynamic routing (/school/[urn])
- Implement Chart.js and Leaflet map integrations
- Add comprehensive SEO with sitemap, robots.txt, and JSON-LD
- Set up Docker multi-service architecture (PostgreSQL, FastAPI, Next.js)
- Update CI/CD pipeline to build both backend and frontend images
- Fix Dockerfile to include devDependencies for TypeScript compilation
- Add Jest testing configuration
- Implement performance optimizations (code splitting, caching)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>