Compare commits

...
3 Commits
Author SHA1 Message Date
TudorandClaude Fable 5 29f79fe948 feat(rankings,metrics): score visible on phones; definitions usable everywhere (P2.2, P2.3)
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 13s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 48s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 13s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 0s
Rankings mobile: the metric value — the point of the page — sat behind
a sideways swipe at 390px. The Area column now folds into a subline
under the school name, leaving Rank | School | Value to fit the
viewport with no horizontal scroll.

Rankings default metric becomes 'expected standard' (rwm_expected_pct);
'higher standard' stays available but no longer frames every school's
headline number in the terms parents least understand.

MetricTooltip was hover-only and display:none on phones — the mobile-
primary audience had zero access to the Attainment 8 / Progress 8 /
EBacc definitions. It is now a real button: tap/click/keyboard
toggleable with outside-click and Escape dismissal, 24px target,
shown at all viewports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:34:07 +01:00
TudorandClaude Fable 5 0294038fd3 fix(compare): shared ?urns= links win over the visitor's stored selection (P1.3)
The seed effect only adopted the URL's schools when localStorage was
empty, so a recipient who had ever used compare silently saw their own
old shortlist instead of the shared one. Explicit URL urns now replace
the stored selection on load (then persist as usual); bare /compare
still restores the visitor's own selection. Adds replaceSchools() to
the comparison context. Card values also switch to CHART_TEXT_COLORS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:34:07 +01:00
TudorandClaude Fable 5 a1fa4fe874 fix(a11y): WCAG AA colour contrast across every route (P0.1)
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>
2026-07-02 21:33:51 +01:00
24 changed files with 242 additions and 131 deletions
+18 -17
View File
@@ -16,8 +16,9 @@
--text-inverse: #faf7f2; --text-inverse: #faf7f2;
--accent-coral: #e07256; --accent-coral: #e07256;
--accent-coral-dark: #c45a3f; --accent-coral-dark: #b04a2e; /* AA: 4.5+ as text on white/cream tints; 5.4:1 under white text */
--accent-teal: #2d7d7d; --accent-coral-darker: #9c3f26; /* hover for coral-dark fills */
--accent-teal: #296f6f; /* AA: 4.8+ as text on the cream/tint backgrounds */
--accent-teal-light: #3a9e9e; --accent-teal-light: #3a9e9e;
--accent-gold: #c9a227; --accent-gold: #c9a227;
--accent-gold-text: #7a6800; /* WCAG AA safe for text on white/cream */ --accent-gold-text: #7a6800; /* WCAG AA safe for text on white/cream */
@@ -172,13 +173,13 @@ body {
/* Primary: coral background — main CTAs (Search, Compare Now) */ /* Primary: coral background — main CTAs (Search, Compare Now) */
.btn-primary { .btn-primary {
background: var(--accent-coral); background: var(--accent-coral-dark);
color: white; color: white;
border-color: var(--accent-coral); border-color: var(--accent-coral-dark);
} }
.btn-primary:hover:not(:disabled) { .btn-primary:hover:not(:disabled) {
background: var(--accent-coral-dark); background: var(--accent-coral-darker);
border-color: var(--accent-coral-dark); border-color: var(--accent-coral-darker);
} }
/* Secondary: teal outline — supporting actions (+ Compare) */ /* Secondary: teal outline — supporting actions (+ Compare) */
@@ -210,8 +211,8 @@ body {
} }
.btn-active:hover:not(:disabled) { .btn-active:hover:not(:disabled) {
background: transparent; background: transparent;
color: var(--accent-coral); color: var(--accent-coral-dark);
border-color: var(--accent-coral); border-color: var(--accent-coral-dark);
} }
/* Small variant */ /* Small variant */
@@ -745,14 +746,14 @@ body {
} }
.btn-compare { .btn-compare {
background: var(--accent-coral); background: var(--accent-coral-dark);
color: white; color: white;
border: 1px solid var(--accent-coral); border: 1px solid var(--accent-coral-dark);
} }
.btn-compare:hover { .btn-compare:hover {
background: #d4654a; background: var(--accent-coral-darker);
border-color: #d4654a; border-color: var(--accent-coral-darker);
} }
.btn-compare.active { .btn-compare.active {
@@ -916,7 +917,7 @@ body {
} }
.stat-value.negative { .stat-value.negative {
color: var(--accent-coral); color: var(--accent-coral-dark);
} }
/* Trend indicators */ /* Trend indicators */
@@ -930,7 +931,7 @@ body {
} }
.trend-down { .trend-down {
color: var(--accent-coral); color: var(--accent-coral-dark);
} }
.trend-stable { .trend-stable {
@@ -1018,7 +1019,7 @@ body {
} }
.map-modal-close:hover { .map-modal-close:hover {
color: var(--accent-coral); color: var(--accent-coral-dark);
} }
.map-modal-content { .map-modal-content {
@@ -1567,12 +1568,12 @@ body {
} }
.btn-primary { .btn-primary {
background: var(--accent-coral); background: var(--accent-coral-dark);
color: white; color: white;
} }
.btn-primary:hover { .btn-primary:hover {
background: var(--accent-coral-dark); background: var(--accent-coral-darker);
transform: translateY(-1px); transform: translateY(-1px);
} }
+1 -1
View File
@@ -29,7 +29,7 @@ export default async function RankingsPage({ searchParams }: RankingsPageProps)
const { metric: metricParam, local_authority, year: yearParam, phase: phaseParam } = await searchParams; const { metric: metricParam, local_authority, year: yearParam, phase: phaseParam } = await searchParams;
const phase = phaseParam || 'primary'; const phase = phaseParam || 'primary';
const metric = metricParam || (phase === 'secondary' ? 'attainment_8_score' : 'rwm_high_pct'); const metric = metricParam || (phase === 'secondary' ? 'attainment_8_score' : 'rwm_expected_pct');
const year = yearParam ? parseInt(yearParam) : undefined; const year = yearParam ? parseInt(yearParam) : undefined;
// Fetch rankings data with error handling // Fetch rankings data with error handling
@@ -196,7 +196,7 @@
margin-bottom: 0.15rem; margin-bottom: 0.15rem;
} }
.chipTrackDeadline { color: var(--accent-coral, #e07256); } .chipTrackDeadline { color: var(--accent-coral-dark, #b04a2e); }
.chipTrackOffer { color: var(--accent-teal, #2d7d7d); } .chipTrackOffer { color: var(--accent-teal, #2d7d7d); }
.chipTrackDot { .chipTrackDot {
@@ -278,7 +278,7 @@
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
margin-bottom: 0.35rem; margin-bottom: 0.35rem;
} }
@@ -399,7 +399,7 @@
margin-bottom: 0.2rem; margin-bottom: 0.2rem;
} }
.stepDeadline .stepDate { color: var(--accent-coral, #e07256); } .stepDeadline .stepDate { color: var(--accent-coral-dark, #b04a2e); }
.stepOffer .stepDate { color: var(--accent-teal, #2d7d7d); } .stepOffer .stepDate { color: var(--accent-teal, #2d7d7d); }
.stepTitle { .stepTitle {
@@ -472,7 +472,7 @@
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-playfair), 'Playfair Display', serif;
font-size: 1.75rem; font-size: 1.75rem;
font-weight: 700; font-weight: 700;
color: var(--border-color, #e5dfd5); color: #7d766b; /* 3.8:1 on cream — AA large-text for this 28px numeral */
line-height: 1; line-height: 1;
margin-bottom: 0.6rem; margin-bottom: 0.6rem;
} }
@@ -121,7 +121,7 @@
} }
.removeSchoolBtn:hover { .removeSchoolBtn:hover {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.toastActions { .toastActions {
@@ -145,7 +145,7 @@
} }
.btnClearAll:hover { .btnClearAll:hover {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.btnCompare { .btnCompare {
@@ -63,13 +63,13 @@
} }
.phaseTabActive { .phaseTabActive {
background: var(--accent-coral, #e07256); background: var(--accent-coral-dark, #b04a2e);
color: white; color: white;
font-weight: 600; font-weight: 600;
} }
.phaseTabActive:hover { .phaseTabActive:hover {
background: var(--accent-coral, #e07256); background: var(--accent-coral-darker, #9c3f26);
} }
/* Metric Selector */ /* Metric Selector */
@@ -198,7 +198,7 @@
} }
.schoolName a:hover { .schoolName a:hover {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.schoolMeta { .schoolMeta {
+17 -10
View File
@@ -18,7 +18,7 @@ import { SchoolSearchModal } from './SchoolSearchModal';
import { EmptyState } from './EmptyState'; import { EmptyState } from './EmptyState';
import { LoadingSkeleton } from './LoadingSkeleton'; import { LoadingSkeleton } from './LoadingSkeleton';
import type { ComparisonData, MetricDefinition, School } from '@/lib/types'; import type { ComparisonData, MetricDefinition, School } from '@/lib/types';
import { formatPercentage, formatProgress, formatAcademicYear, CHART_COLORS, schoolUrl } from '@/lib/utils'; import { formatPercentage, formatProgress, formatAcademicYear, CHART_COLORS, CHART_TEXT_COLORS, schoolUrl } from '@/lib/utils';
import { fetchComparison } from '@/lib/api'; import { fetchComparison } from '@/lib/api';
import { track } from '@/lib/analytics'; import { track } from '@/lib/analytics';
import styles from './ComparisonView.module.css'; import styles from './ComparisonView.module.css';
@@ -58,7 +58,7 @@ export function ComparisonView({
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { selectedSchools, removeSchool, addSchool, isInitialized } = useComparison(); const { selectedSchools, removeSchool, addSchool, replaceSchools, isInitialized } = useComparison();
const [selectedMetric, setSelectedMetric] = useState(initialMetric); const [selectedMetric, setSelectedMetric] = useState(initialMetric);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
@@ -69,16 +69,22 @@ export function ComparisonView({
// While true, auto-phase detection is suppressed so manual selections aren't overridden. // While true, auto-phase detection is suppressed so manual selections aren't overridden.
const phaseLockedByUser = useRef(false); const phaseLockedByUser = useRef(false);
// Seed context from initialData when component mounts and localStorage is empty // Seed context from the URL on mount. An explicit ?urns=… (e.g. a link a
// parent shared with their partner) always wins over this visitor's stored
// selection — otherwise the recipient silently sees their own old schools.
// The replacement is then persisted like any other selection change.
useEffect(() => { useEffect(() => {
if (!isInitialized) return; if (!isInitialized) return;
if (selectedSchools.length === 0 && initialUrns.length > 0 && initialData) { if (initialUrns.length > 0 && initialData) {
initialUrns.forEach(urn => { const urlSchools = initialUrns
const data = initialData[String(urn)]; .map(urn => initialData[String(urn)]?.school_info)
if (data?.school_info) { .filter((info): info is NonNullable<typeof info> => Boolean(info));
addSchool(data.school_info); const sameSet =
urlSchools.length === selectedSchools.length &&
urlSchools.every(s => selectedSchools.some(sel => sel.urn === s.urn));
if (urlSchools.length > 0 && !sameSet) {
replaceSchools(urlSchools);
} }
});
} }
}, [isInitialized]); // eslint-disable-line react-hooks/exhaustive-deps }, [isInitialized]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -378,7 +384,8 @@ export function ComparisonView({
{activeComparisonData[school.urn] && ( {activeComparisonData[school.urn] && (
<div className={styles.latestValue}> <div className={styles.latestValue}>
<div className={styles.latestLabel}>{metricLabel}</div> <div className={styles.latestLabel}>{metricLabel}</div>
<div className={styles.latestNumber} style={{ color: CHART_COLORS[index % CHART_COLORS.length] }}> {/* Text uses the AA-dark variant; the swatch dot keeps the true series colour */}
<div className={styles.latestNumber} style={{ color: CHART_TEXT_COLORS[index % CHART_TEXT_COLORS.length] }}>
<span <span
style={{ style={{
display: 'inline-block', display: 'inline-block',
+1 -1
View File
@@ -29,7 +29,7 @@
.bad { .bad {
background: var(--accent-coral-bg, rgba(224, 114, 86, 0.12)); background: var(--accent-coral-bg, rgba(224, 114, 86, 0.12));
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.neutral { .neutral {
+8 -8
View File
@@ -121,7 +121,7 @@
.viewToggleBtn.active { .viewToggleBtn.active {
background: var(--bg-card, white); background: var(--bg-card, white);
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
box-shadow: 0 2px 4px rgba(26, 22, 18, 0.08); box-shadow: 0 2px 4px rgba(26, 22, 18, 0.08);
} }
@@ -222,7 +222,7 @@
} }
.compactItemName:hover { .compactItemName:hover {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.distanceBadge { .distanceBadge {
@@ -536,7 +536,7 @@
.geoError { .geoError {
font-size: 0.8125rem; font-size: 0.8125rem;
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
margin: 0; margin: 0;
max-width: 340px; max-width: 340px;
text-align: center; text-align: center;
@@ -567,9 +567,9 @@
} }
.quickSearchChip:hover { .quickSearchChip:hover {
background: var(--accent-coral); background: var(--accent-coral-dark);
color: white; color: white;
border-color: var(--accent-coral); border-color: var(--accent-coral-dark);
} }
.exploringRow { .exploringRow {
@@ -712,7 +712,7 @@
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.hiwTitle { .hiwTitle {
@@ -1062,7 +1062,7 @@
.editorialKicker { .editorialKicker {
font-size: 0.68rem; font-size: 0.68rem;
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
@@ -1300,7 +1300,7 @@
} }
.chipTrackDeadline { .chipTrackDeadline {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.chipTrackOffer { .chipTrackOffer {
+2 -2
View File
@@ -620,8 +620,8 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare, nationalAvgRwm
borderRadius: '3px', borderRadius: '3px',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
...(ofstedBadge.cssClass === 'ofsted1' ? { background: 'var(--accent-teal-bg)', color: 'var(--accent-teal, #2d7d7d)' } : ...(ofstedBadge.cssClass === 'ofsted1' ? { background: 'var(--accent-teal-bg)', color: 'var(--accent-teal, #2d7d7d)' } :
ofstedBadge.cssClass === 'ofsted2' ? { background: 'rgba(60,140,60,0.12)', color: '#3c8c3c' } : ofstedBadge.cssClass === 'ofsted2' ? { background: 'rgba(60,140,60,0.12)', color: '#2f7a2f' } :
ofstedBadge.cssClass === 'ofsted3' ? { background: 'var(--accent-gold-bg)', color: '#b8920e' } : ofstedBadge.cssClass === 'ofsted3' ? { background: 'var(--accent-gold-bg)', color: 'var(--accent-gold-text, #7a6800)' } :
ofstedBadge.cssClass === 'ofsted4' ? { background: 'var(--accent-coral-bg)', color: 'var(--accent-coral, #e07256)' } : ofstedBadge.cssClass === 'ofsted4' ? { background: 'var(--accent-coral-bg)', color: 'var(--accent-coral, #e07256)' } :
ofstedBadge.cssClass === 'ofstedRc' ? { background: '#5a3a6e', color: '#fff' } : ofstedBadge.cssClass === 'ofstedRc' ? { background: '#5a3a6e', color: '#fff' } :
ofstedBadge.cssClass === 'ofstedPending' ? { background: '#e0e0e0', color: '#666' } : ofstedBadge.cssClass === 'ofstedPending' ? { background: '#e0e0e0', color: '#666' } :
+2 -2
View File
@@ -49,8 +49,8 @@ function buildPopupBadge(school: School): PopupBadge {
const labels: Record<number, string> = { 1: 'Outstanding', 2: 'Good', 3: 'Req. Improvement', 4: 'Inadequate' }; const labels: Record<number, string> = { 1: 'Outstanding', 2: 'Good', 3: 'Req. Improvement', 4: 'Inadequate' };
const colours: Record<number, string> = { const colours: Record<number, string> = {
1: 'background:#d4f0ea;color:#2d7d7d', 1: 'background:#d4f0ea;color:#2d7d7d',
2: 'background:rgba(60,140,60,0.12);color:#3c8c3c', 2: 'background:rgba(60,140,60,0.12);color:#2f7a2f',
3: 'background:#fef3cd;color:#b8920e', 3: 'background:#fef3cd;color:#7a6800',
4: 'background:#fde8e0;color:#e07256', 4: 'background:#fde8e0;color:#e07256',
}; };
return { label: `${labels[school.ofsted_grade]}${yearStr}`, style: colours[school.ofsted_grade] }; return { label: `${labels[school.ofsted_grade]}${yearStr}`, style: colours[school.ofsted_grade] };
+33 -12
View File
@@ -6,7 +6,17 @@
} }
.icon { .icon {
font-size: 0.85em; /* A real button: 24px tap target (WCAG 2.5.8) drawn as the small glyph. */
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
min-height: 24px;
margin: -6px 0;
padding: 0;
border: none;
background: none;
font-size: 0.9em;
color: var(--text-muted, #8a7a72); color: var(--text-muted, #8a7a72);
cursor: help; cursor: help;
line-height: 1; line-height: 1;
@@ -14,8 +24,9 @@
transition: color 0.15s ease; transition: color 0.15s ease;
} }
.wrapper:hover .icon { .wrapper:hover .icon,
color: var(--accent-coral, #e07256); .icon[aria-expanded="true"] {
color: var(--accent-coral-dark, #b04a2e);
} }
.tooltip { .tooltip {
@@ -39,12 +50,18 @@
transition: opacity 0.15s ease, visibility 0.15s ease; transition: opacity 0.15s ease, visibility 0.15s ease;
} }
/* Keep tooltip visible when hovering over it */ /* Reveal on hover (desktop), keyboard focus, or explicit tap/click toggle. */
.wrapper:hover .tooltip { .wrapper:hover .tooltip,
.wrapper:focus-within .tooltip,
.tooltipOpen {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
} }
.tooltipOpen {
pointer-events: auto;
}
/* Small arrow pointing down */ /* Small arrow pointing down */
.tooltip::after { .tooltip::after {
content: ''; content: '';
@@ -75,19 +92,23 @@
margin-top: 0.1rem; margin-top: 0.1rem;
} }
/* Flip tooltip below when near top of screen */
@media (max-width: 480px) { @media (max-width: 480px) {
.tooltip { .tooltip {
width: 180px; width: 180px;
} }
} }
/* On phones the icon was rendering at ~9px and the tooltip relied on /* Anchor the bubble to open rightward on phones — icons follow their labels,
:hover, which doesn't fire on touch. Rather than build a tap-to-show which start at the left edge, so centring pushed the bubble off-screen. */
layer with backdrop dismissal, hide the helper entirely — the metric
labels themselves carry the meaning. */
@media (max-width: 640px) { @media (max-width: 640px) {
.wrapper { .tooltip {
display: none; left: -12px;
right: auto;
transform: none;
}
.tooltip::after {
left: 16px;
transform: none;
} }
} }
+35 -3
View File
@@ -1,5 +1,6 @@
'use client'; 'use client';
import { useEffect, useRef, useState } from 'react';
import { METRIC_EXPLANATIONS } from '@/lib/metrics'; import { METRIC_EXPLANATIONS } from '@/lib/metrics';
import styles from './MetricTooltip.module.css'; import styles from './MetricTooltip.module.css';
@@ -16,12 +17,43 @@ export function MetricTooltip({ metricKey, label, plain, detail }: MetricTooltip
const tooltipPlain = plain ?? explanation?.plain; const tooltipPlain = plain ?? explanation?.plain;
const tooltipDetail = detail ?? explanation?.detail; const tooltipDetail = detail ?? explanation?.detail;
// Tap/click/keyboard toggle so the definition is reachable on touch devices
// and by keyboard, not just mouse hover (hover still works on desktop).
const [open, setOpen] = useState(false);
const wrapperRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (!open) return;
const dismiss = (e: Event) => {
if (wrapperRef.current && e.target instanceof Node && !wrapperRef.current.contains(e.target)) {
setOpen(false);
}
};
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false);
};
document.addEventListener('click', dismiss);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('click', dismiss);
document.removeEventListener('keydown', onKey);
};
}, [open]);
if (!tooltipPlain) return null; if (!tooltipPlain) return null;
return ( return (
<span className={styles.wrapper}> <span className={styles.wrapper} ref={wrapperRef}>
<span className={styles.icon} aria-label={tooltipLabel ?? 'More information'} role="img"></span> <button
<span className={styles.tooltip} role="tooltip"> type="button"
className={styles.icon}
aria-expanded={open}
aria-label={`What does ${tooltipLabel ?? 'this metric'} mean?`}
onClick={() => setOpen((o) => !o)}
>
</button>
<span className={`${styles.tooltip}${open ? ` ${styles.tooltipOpen}` : ''}`} role="tooltip">
{tooltipLabel && <span className={styles.tooltipLabel}>{tooltipLabel}</span>} {tooltipLabel && <span className={styles.tooltipLabel}>{tooltipLabel}</span>}
<span className={styles.tooltipPlain}>{tooltipPlain}</span> <span className={styles.tooltipPlain}>{tooltipPlain}</span>
{tooltipDetail && <span className={styles.tooltipDetail}>{tooltipDetail}</span>} {tooltipDetail && <span className={styles.tooltipDetail}>{tooltipDetail}</span>}
+3 -3
View File
@@ -35,7 +35,7 @@
} }
.logo:hover { .logo:hover {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.logoIcon { .logoIcon {
@@ -100,7 +100,7 @@
} }
.navLink.active { .navLink.active {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
background: var(--accent-coral-bg); background: var(--accent-coral-bg);
} }
@@ -167,7 +167,7 @@
} }
.tabActive { .tabActive {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.tabIconWrap { .tabIconWrap {
@@ -87,7 +87,7 @@
} }
.chipActive { .chipActive {
background: var(--accent-coral, #e07256); background: var(--accent-coral-dark, #b04a2e);
color: white; color: white;
border-color: var(--accent-coral, #e07256); border-color: var(--accent-coral, #e07256);
font-weight: 600; font-weight: 600;
+23 -9
View File
@@ -55,13 +55,13 @@
} }
.phaseTabActive { .phaseTabActive {
background: var(--accent-coral, #e07256); background: var(--accent-coral-dark, #b04a2e);
color: white; color: white;
font-weight: 600; font-weight: 600;
} }
.phaseTabActive:hover { .phaseTabActive:hover {
background: var(--accent-coral, #e07256); background: var(--accent-coral-darker, #9c3f26);
} }
/* Filters */ /* Filters */
@@ -290,7 +290,7 @@
} }
.schoolLink:hover { .schoolLink:hover {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.areaCell, .areaCell,
@@ -298,6 +298,14 @@
color: var(--text-secondary, #5c564d); color: var(--text-secondary, #5c564d);
} }
/* LA subline under the school name — mobile only (Area column hidden there). */
.schoolCellArea {
display: none;
font-size: 0.75rem;
font-weight: 400;
color: var(--text-muted, #8a847a);
}
.valueCell { .valueCell {
text-align: center; text-align: center;
font-size: 1rem; font-size: 1rem;
@@ -374,20 +382,26 @@
font-size: 0.875rem; font-size: 0.875rem;
} }
/* Hide less-critical columns on mobile so the metric value stays visible */ /* Hide less-critical columns on mobile so the metric value stays visible.
Area moves to a subline under the school name (.schoolCellArea) — with a
four-column layout the value still overflowed a 390px viewport and the
whole point of the page (the score) needed a sideways swipe to see. */
.typeHeader, .typeHeader,
.typeCell, .typeCell,
.actionHeader, .actionHeader,
.actionCell { .actionCell,
.areaHeader,
.areaCell {
display: none; display: none;
} }
.schoolHeader { .schoolCellArea {
min-width: 140px; display: block;
margin-top: 0.15rem;
} }
.areaHeader { .schoolHeader {
min-width: 80px; min-width: 0;
} }
.valueHeader, .valueHeader,
+6 -1
View File
@@ -75,7 +75,7 @@ export function RankingsView({
}; };
const handlePhaseChange = (phase: string) => { const handlePhaseChange = (phase: string) => {
const defaultMetric = phase === 'secondary' ? 'attainment_8_score' : 'rwm_high_pct'; const defaultMetric = phase === 'secondary' ? 'attainment_8_score' : 'rwm_expected_pct';
updateFilters({ phase, metric: defaultMetric }); updateFilters({ phase, metric: defaultMetric });
}; };
@@ -273,6 +273,11 @@ export function RankingsView({
<a href={schoolUrl(ranking.urn, ranking.school_name)} className={styles.schoolLink}> <a href={schoolUrl(ranking.urn, ranking.school_name)} className={styles.schoolLink}>
{ranking.school_name} {ranking.school_name}
</a> </a>
{/* On phones the Area column is hidden; the LA moves here
so the metric value fits on screen without swiping. */}
{ranking.local_authority && (
<span className={styles.schoolCellArea}>{ranking.local_authority}</span>
)}
</td> </td>
<td className={styles.areaCell}>{ranking.local_authority || '-'}</td> <td className={styles.areaCell}>{ranking.local_authority || '-'}</td>
<td className={styles.typeCell}>{ranking.school_type || '-'}</td> <td className={styles.typeCell}>{ranking.school_type || '-'}</td>
@@ -13,7 +13,7 @@
padding: 0.25rem 0; padding: 0.25rem 0;
font-size: 1.0625rem; font-size: 1.0625rem;
font-weight: 600; font-weight: 600;
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
@@ -76,7 +76,7 @@
.headerHasMap .actions .btnAdd { .headerHasMap .actions .btnAdd {
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
border-color: transparent; border-color: transparent;
-webkit-backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
@@ -124,7 +124,7 @@
padding: 0; padding: 0;
font: inherit; font: inherit;
font-weight: 600; font-weight: 600;
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
} }
@@ -210,7 +210,7 @@
} }
.genderSplitGirls { .genderSplitGirls {
color: #b45778; color: #a04a68;
} }
.genderSplitBoys { .genderSplitBoys {
@@ -242,7 +242,7 @@
} }
.genderBarGirls { .genderBarGirls {
background: #b45778; background: #a04a68;
} }
.genderBarBoys { .genderBarBoys {
@@ -270,12 +270,12 @@
} }
.btnAdd { .btnAdd {
background: var(--accent-coral, #e07256); background: var(--accent-coral-dark, #b04a2e);
color: white; color: white;
} }
.btnAdd:hover { .btnAdd:hover {
background: var(--accent-coral-dark, #c45a3f); background: var(--accent-coral-darker, #9c3f26);
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -323,7 +323,7 @@
padding: 0.3rem 0.625rem; padding: 0.3rem 0.625rem;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
background: none; background: none;
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border-color, #e5dfd5);
border-radius: 4px; border-radius: 4px;
@@ -399,7 +399,7 @@
} }
.sectionNavLinkActive { .sectionNavLinkActive {
background: var(--accent-coral, #e07256); background: var(--accent-coral-dark, #b04a2e);
color: white; color: white;
font-weight: 600; font-weight: 600;
} }
@@ -468,8 +468,8 @@
width: 38px; width: 38px;
height: 38px; height: 38px;
border-radius: 9px; border-radius: 9px;
border: 1px solid var(--accent-coral, #e07256); border: 1px solid var(--accent-coral-dark, #b04a2e);
background: var(--accent-coral, #e07256); background: var(--accent-coral-dark, #b04a2e);
color: white; color: white;
cursor: pointer; cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease; transition: background 0.15s ease, border-color 0.15s ease;
@@ -513,8 +513,8 @@
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
color: white; color: white;
background: var(--accent-coral, #e07256); background: var(--accent-coral-dark, #b04a2e);
border: 1px solid var(--accent-coral, #e07256); border: 1px solid var(--accent-coral-dark, #b04a2e);
border-radius: 999px; border-radius: 999px;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
@@ -522,8 +522,8 @@
} }
.sectionNavCompare:hover { .sectionNavCompare:hover {
background: var(--accent-coral-dark, #c45a3f); background: var(--accent-coral-darker, #9c3f26);
border-color: var(--accent-coral-dark, #c45a3f); border-color: var(--accent-coral-darker, #9c3f26);
} }
.sectionNavCompareIn { .sectionNavCompareIn {
@@ -626,7 +626,7 @@
} }
.sectionsTick { .sectionsTick {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
/* On phones the menu becomes a bottom sheet. */ /* On phones the menu becomes a bottom sheet. */
@@ -856,7 +856,7 @@
} }
.progressNegative { .progressNegative {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
font-weight: 700; font-weight: 700;
} }
@@ -868,12 +868,12 @@
.statusWarn { .statusWarn {
background: var(--accent-gold-bg); background: var(--accent-gold-bg);
color: #b8920e; color: var(--accent-gold-text, #7a6800);
} }
.statusBad { .statusBad {
background: var(--accent-coral-bg); background: var(--accent-coral-bg);
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
/* Charts Section */ /* Charts Section */
@@ -1011,15 +1011,15 @@
} }
.ofstedGrade2 { .ofstedGrade2 {
background: rgba(60, 140, 60, 0.12); background: rgba(60, 140, 60, 0.12);
color: #3c8c3c; color: #2f7a2f;
} }
.ofstedGrade3 { .ofstedGrade3 {
background: var(--accent-gold-bg); background: var(--accent-gold-bg);
color: #b8920e; color: var(--accent-gold-text, #7a6800);
} }
.ofstedGrade4 { .ofstedGrade4 {
background: var(--accent-coral-bg); background: var(--accent-coral-bg);
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
/* Report Card grade colours (5-level scale, lower = better) */ /* Report Card grade colours (5-level scale, lower = better) */
@@ -1029,11 +1029,11 @@
} /* Exceptional */ } /* Exceptional */
.rcGrade2 { .rcGrade2 {
background: rgba(60, 140, 60, 0.12); background: rgba(60, 140, 60, 0.12);
color: #3c8c3c; color: #2f7a2f;
} /* Strong */ } /* Strong */
.rcGrade3 { .rcGrade3 {
background: var(--accent-gold-bg); background: var(--accent-gold-bg);
color: #b8920e; color: var(--accent-gold-text, #7a6800);
} /* Expected standard */ } /* Expected standard */
.rcGrade4 { .rcGrade4 {
background: rgba(249, 115, 22, 0.12); background: rgba(249, 115, 22, 0.12);
@@ -1041,7 +1041,7 @@
} /* Needs attention */ } /* Needs attention */
.rcGrade5 { .rcGrade5 {
background: var(--accent-coral-bg); background: var(--accent-coral-bg);
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} /* Urgent improvement */ } /* Urgent improvement */
/* Safeguarding value (used inside a standard metricCard) */ /* Safeguarding value (used inside a standard metricCard) */
@@ -1061,7 +1061,7 @@
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 700; font-weight: 700;
background: var(--accent-coral-bg); background: var(--accent-coral-bg);
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.ofstedDisclaimer { .ofstedDisclaimer {
+2 -2
View File
@@ -201,8 +201,8 @@
} }
.ofsted1 { background: var(--accent-teal-bg); color: var(--accent-teal, #2d7d7d); } .ofsted1 { background: var(--accent-teal-bg); color: var(--accent-teal, #2d7d7d); }
.ofsted2 { background: rgba(60, 140, 60, 0.12); color: #3c8c3c; } .ofsted2 { background: rgba(60, 140, 60, 0.12); color: #2f7a2f; }
.ofsted3 { background: var(--accent-gold-bg); color: #b8920e; } .ofsted3 { background: var(--accent-gold-bg); color: var(--accent-gold-text, #7a6800); }
.ofsted4 { background: var(--accent-coral-bg); color: var(--accent-coral, #e07256); } .ofsted4 { background: var(--accent-coral-bg); color: var(--accent-coral, #e07256); }
/* ── Ofsted badge variants ──────────────────────────────────────────────── */ /* ── Ofsted badge variants ──────────────────────────────────────────────── */
@@ -13,7 +13,7 @@
padding: 0.25rem 0; padding: 0.25rem 0;
font-size: 1.0625rem; font-size: 1.0625rem;
font-weight: 600; font-weight: 600;
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
@@ -76,7 +76,7 @@
.headerHasMap .actions .btnAdd { .headerHasMap .actions .btnAdd {
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
border-color: transparent; border-color: transparent;
-webkit-backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
@@ -124,7 +124,7 @@
padding: 0; padding: 0;
font: inherit; font: inherit;
font-weight: 600; font-weight: 600;
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
} }
@@ -226,12 +226,12 @@
} }
.btnAdd { .btnAdd {
background: var(--accent-coral, #e07256); background: var(--accent-coral-dark, #b04a2e);
color: white; color: white;
} }
.btnAdd:hover { .btnAdd:hover {
background: var(--accent-coral-dark, #c45a3f); background: var(--accent-coral-darker, #9c3f26);
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -278,7 +278,7 @@
padding: 0.3rem 0.625rem; padding: 0.3rem 0.625rem;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
background: none; background: none;
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border-color, #e5dfd5);
border-radius: 4px; border-radius: 4px;
@@ -322,13 +322,13 @@
} }
.tabBtnActive { .tabBtnActive {
background: var(--accent-coral, #e07256); background: var(--accent-coral-dark, #b04a2e);
color: white; color: white;
font-weight: 600; font-weight: 600;
} }
.tabBtnActive:hover { .tabBtnActive:hover {
background: var(--accent-coral-dark, #c45a3f); background: var(--accent-coral-darker, #9c3f26);
color: white; color: white;
} }
@@ -456,7 +456,7 @@
} }
.progressNegative { .progressNegative {
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
font-weight: 700; font-weight: 700;
} }
@@ -468,7 +468,7 @@
.statusWarn { .statusWarn {
background: var(--accent-gold-bg); background: var(--accent-gold-bg);
color: #b8920e; color: var(--accent-gold-text, #7a6800);
} }
/* ── Metric table (row-based) ────────────────────────── */ /* ── Metric table (row-based) ────────────────────────── */
@@ -582,15 +582,15 @@
} }
.ofstedGrade2 { .ofstedGrade2 {
background: rgba(60, 140, 60, 0.12); background: rgba(60, 140, 60, 0.12);
color: #3c8c3c; color: #2f7a2f;
} }
.ofstedGrade3 { .ofstedGrade3 {
background: var(--accent-gold-bg); background: var(--accent-gold-bg);
color: #b8920e; color: var(--accent-gold-text, #7a6800);
} }
.ofstedGrade4 { .ofstedGrade4 {
background: var(--accent-coral-bg); background: var(--accent-coral-bg);
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.rcGrade1 { .rcGrade1 {
@@ -599,11 +599,11 @@
} }
.rcGrade2 { .rcGrade2 {
background: rgba(60, 140, 60, 0.12); background: rgba(60, 140, 60, 0.12);
color: #3c8c3c; color: #2f7a2f;
} }
.rcGrade3 { .rcGrade3 {
background: var(--accent-gold-bg); background: var(--accent-gold-bg);
color: #b8920e; color: var(--accent-gold-text, #7a6800);
} }
.rcGrade4 { .rcGrade4 {
background: rgba(249, 115, 22, 0.12); background: rgba(249, 115, 22, 0.12);
@@ -611,7 +611,7 @@
} }
.rcGrade5 { .rcGrade5 {
background: var(--accent-coral-bg); background: var(--accent-coral-bg);
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.safeguardingMet { .safeguardingMet {
@@ -631,7 +631,7 @@
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 700; font-weight: 700;
background: var(--accent-coral-bg); background: var(--accent-coral-bg);
color: var(--accent-coral, #e07256); color: var(--accent-coral-dark, #b04a2e);
} }
.ofstedDisclaimer { .ofstedDisclaimer {
@@ -874,7 +874,7 @@
} }
.genderBarGirls { .genderBarGirls {
background: #b45778; background: #a04a68;
} }
.genderBarBoys { .genderBarBoys {
@@ -889,7 +889,7 @@
} }
.genderSplitGirls { .genderSplitGirls {
color: #b45778; color: #a04a68;
font-weight: 600; font-weight: 600;
} }
@@ -946,7 +946,7 @@
position: absolute; position: absolute;
top: -20px; top: -20px;
transform: translateX(-50%); transform: translateX(-50%);
background: var(--accent-coral, #e07256); background: var(--accent-coral-dark, #b04a2e);
color: #fff; color: #fff;
font-size: 0.6rem; font-size: 0.6rem;
font-weight: 700; font-weight: 700;
@@ -125,7 +125,7 @@
white-space: nowrap; white-space: nowrap;
} }
.deltaPositive { color: #3c8c3c; } .deltaPositive { color: #2f7a2f; }
.deltaNegative { color: var(--accent-coral, #e07256); } .deltaNegative { color: var(--accent-coral, #e07256); }
/* Line 4: location + distance */ /* Line 4: location + distance */
@@ -203,8 +203,8 @@
} }
.ofsted1 { background: var(--accent-teal-bg); color: var(--accent-teal, #2d7d7d); } .ofsted1 { background: var(--accent-teal-bg); color: var(--accent-teal, #2d7d7d); }
.ofsted2 { background: rgba(60, 140, 60, 0.12); color: #3c8c3c; } .ofsted2 { background: rgba(60, 140, 60, 0.12); color: #2f7a2f; }
.ofsted3 { background: var(--accent-gold-bg); color: #b8920e; } .ofsted3 { background: var(--accent-gold-bg); color: var(--accent-gold-text, #7a6800); }
.ofsted4 { background: var(--accent-coral-bg); color: var(--accent-coral, #e07256); } .ofsted4 { background: var(--accent-coral-bg); color: var(--accent-coral, #e07256); }
/* ── Ofsted badge variants ──────────────────────────────────────────────── */ /* ── Ofsted badge variants ──────────────────────────────────────────────── */
+1
View File
@@ -15,6 +15,7 @@ interface ComparisonContextType {
error: any; error: any;
addSchool: (school: School) => void; addSchool: (school: School) => void;
removeSchool: (urn: number) => void; removeSchool: (urn: number) => void;
replaceSchools: (schools: School[]) => void;
clearAll: () => void; clearAll: () => void;
isSelected: (urn: number) => boolean; isSelected: (urn: number) => boolean;
canAddMore: boolean; canAddMore: boolean;
@@ -65,6 +65,12 @@ export function ComparisonProvider({ children }: { children: React.ReactNode })
setSelectedSchools((prev) => prev.filter((s) => s.urn !== urn)); setSelectedSchools((prev) => prev.filter((s) => s.urn !== urn));
}, []); }, []);
// Replace the whole selection — used when a shared /compare?urns=… link
// must take precedence over whatever this visitor had stored.
const replaceSchools = useCallback((schools: School[]) => {
setSelectedSchools(schools.slice(0, MAX_SCHOOLS));
}, []);
const clearAll = useCallback(() => { const clearAll = useCallback(() => {
setSelectedSchools([]); setSelectedSchools([]);
}, []); }, []);
@@ -86,6 +92,7 @@ export function ComparisonProvider({ children }: { children: React.ReactNode })
error: null, error: null,
addSchool, addSchool,
removeSchool, removeSchool,
replaceSchools,
clearAll, clearAll,
isSelected, isSelected,
canAddMore: selectedSchools.length < MAX_SCHOOLS, canAddMore: selectedSchools.length < MAX_SCHOOLS,
+2
View File
@@ -15,6 +15,7 @@ export function useComparison() {
selectedSchools, selectedSchools,
addSchool, addSchool,
removeSchool, removeSchool,
replaceSchools,
clearAll, clearAll,
isSelected, isSelected,
canAddMore, canAddMore,
@@ -39,6 +40,7 @@ export function useComparison() {
error, error,
addSchool, addSchool,
removeSchool, removeSchool,
replaceSchools,
clearAll, clearAll,
isSelected, isSelected,
canAddMore, canAddMore,
+21
View File
@@ -275,6 +275,27 @@ export function getChartColor(index: number): string {
return CHART_COLORS[index % CHART_COLORS.length]; return CHART_COLORS[index % CHART_COLORS.length];
} }
/**
* Darker counterparts of CHART_COLORS for use as TEXT on the site's light
* backgrounds — the raw series colours fail WCAG AA (e.g. 1.9:1 on cream).
* Same hue per index so text still keys to its chart line. All ≥4.5:1 on
* #f3ede4 and lighter.
*/
export const CHART_TEXT_COLORS = [
'#256868', // teal
'#c2255c', // pink/red
'#2069ad', // blue
'#8a5a00', // yellow → ochre
'#6d43c8', // purple
'#8f4e00', // orange → brown
'#5f5952', // grey
'#a300a3', // magenta
];
export function getChartTextColor(index: number): string {
return CHART_TEXT_COLORS[index % CHART_TEXT_COLORS.length];
}
/** /**
* Convert RGB color to RGBA with opacity * Convert RGB color to RGBA with opacity
*/ */