Merge pull request 'feat(design): adopt the Cohort identity — palette, type, mark and dark theme' (#86) from design/cohort-identity into main
Stage (build -> staging -> E2E gate) / Build Backend (FastAPI) (push) Successful in 12s
Stage (build -> staging -> E2E gate) / Build Frontend (Next.js) (push) Successful in 54s
Stage (build -> staging -> E2E gate) / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Stage (build -> staging -> E2E gate) / Deploy to Staging (push) Successful in 1s
Stage (build -> staging -> E2E gate) / E2E Journeys against Staging (push) Failing after 1m1s

Reviewed-on: #86
This commit was merged in pull request #86.
This commit is contained in:
2026-08-06 11:36:02 +00:00
58 changed files with 2041 additions and 3074 deletions
+76
View File
@@ -626,3 +626,79 @@ test('sticky section nav jumps to server-rendered sections', async ({ page }) =>
const after = await page.evaluate(() => window.scrollY); const after = await page.evaluate(() => window.scrollY);
expect(after).toBeGreaterThan(before); expect(after).toBeGreaterThan(before);
}); });
/**
* Brand surface. The identity work replaced a favicon-only asset set that
* broke iOS home-screen icons, Android PWA installs, and every link preview.
* These are silent failures — nothing on the page looks wrong — so they need
* a gate.
*/
test('the brand asset set is complete and served', async ({ page }) => {
const response = await page.goto('/');
expect(response?.ok()).toBe(true);
// The share card: without it, every link pasted into a chat renders bare.
const ogImage = page.locator('meta[property="og:image"]');
await expect(ogImage).toHaveCount(1);
const ogUrl = await ogImage.getAttribute('content');
expect(ogUrl).toBeTruthy();
const og = await page.request.get(ogUrl!);
expect(og.ok()).toBe(true);
expect(og.headers()['content-type']).toContain('image/png');
// iOS ignores SVG touch icons, so this must be a real raster.
const apple = await page.request.get('/apple-icon');
expect(apple.ok()).toBe(true);
expect(apple.headers()['content-type']).toContain('image/png');
// Android needs a maskable PNG or the install prompt has no icon.
for (const icon of ['/icon-192.png', '/icon-512.png', '/icon-maskable-512.png']) {
const res = await page.request.get(icon);
expect(res.ok(), `${icon} should be served`).toBe(true);
}
});
test('theme colour matches the page background in both themes', async ({ browser }) => {
// A mismatch here paints a stripe of the wrong colour across the top of the
// screen on mobile. Previously the dark themeColor was declared with no dark
// styling behind it at all.
for (const colorScheme of ['light', 'dark'] as const) {
const context = await browser.newContext({ colorScheme });
const page = await context.newPage();
await page.goto('/');
const declared = await page
.locator(`meta[name="theme-color"][media*="${colorScheme}"]`)
.getAttribute('content');
expect(declared, `theme-color declared for ${colorScheme}`).toBeTruthy();
const painted = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue('--bg-primary').trim()
);
expect(painted.toLowerCase()).toBe(declared!.toLowerCase());
await context.close();
}
});
test('the dark theme actually repaints the page', async ({ browser }) => {
// Guards the token layer: if a component reintroduces a hardcoded colour,
// the surface below stays light while everything around it flips.
const read = async (colorScheme: 'light' | 'dark') => {
const context = await browser.newContext({ colorScheme });
const page = await context.newPage();
await page.goto('/');
await expect(page.locator('h1').first()).toBeVisible({ timeout: 15_000 });
const values = await page.evaluate(() => {
const s = getComputedStyle(document.body);
return { bg: s.backgroundColor, fg: s.color };
});
await context.close();
return values;
};
const light = await read('light');
const dark = await read('dark');
expect(dark.bg).not.toBe(light.bg);
expect(dark.fg).not.toBe(light.fg);
});
+6
View File
@@ -47,6 +47,12 @@ COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/.next/static ./.next/static
# Fonts for the generated share card. File tracing already places these in
# .next/standalone/assets, so the COPY above carries them — this makes the
# dependency explicit rather than implicit in the tracer's behaviour, because
# a miss here is a silent 500 on /opengraph-image, not a build failure.
COPY --from=builder /app/assets ./assets
# Set correct permissions # Set correct permissions
RUN chown -R nextjs:nodejs /app RUN chown -R nextjs:nodejs /app
+46
View File
@@ -0,0 +1,46 @@
import { ImageResponse } from 'next/og';
import { BARS } from '@/components/Logo';
// iOS ignores SVG touch icons — it needs a real raster, on an opaque ground,
// with no transparency (it composites its own rounded mask). Previously this
// pointed at favicon.svg, so add-to-home-screen produced a blank tile.
export const size = { width: 180, height: 180 };
export const contentType = 'image/png';
const COHORT = ['rgba(22,32,42,0.22)', 'rgba(22,32,42,0.38)', 'rgba(22,32,42,0.55)', '', 'rgba(22,32,42,0.22)'];
const SCALE = 3.6; // 40 → 144, leaving a 18px margin inside 180
export default function AppleIcon() {
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#f2f1ed',
}}
>
<div style={{ position: 'relative', width: 40 * SCALE, height: 40 * SCALE, display: 'flex' }}>
{BARS.map(([x, y, w, h, isSchool], i) => (
<div
key={x}
style={{
position: 'absolute',
left: x * SCALE,
top: y * SCALE,
width: w * SCALE,
height: h * SCALE,
borderRadius: 3,
background: isSchool ? '#584a9b' : COHORT[i],
}}
/>
))}
</div>
</div>
),
size
);
}
+289 -1926
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="26" width="6" height="12" rx="1" fill="#16202a" fill-opacity="0.22"/>
<rect x="10" y="16" width="6" height="22" rx="1" fill="#16202a" fill-opacity="0.38"/>
<rect x="18" y="8" width="6" height="30" rx="1" fill="#16202a" fill-opacity="0.55"/>
<rect x="26" y="14" width="6" height="24" rx="1" fill="#584a9b"/>
<rect x="34" y="28" width="4" height="10" rx="1" fill="#16202a" fill-opacity="0.22"/>
</svg>

After

Width:  |  Height:  |  Size: 521 B

+23 -16
View File
@@ -1,5 +1,5 @@
import type { Metadata, Viewport } from 'next'; import type { Metadata, Viewport } from 'next';
import { DM_Sans, Playfair_Display } from 'next/font/google'; import { Schibsted_Grotesk, Literata } from 'next/font/google';
import Script from 'next/script'; import Script from 'next/script';
import { Navigation } from '@/components/Navigation'; import { Navigation } from '@/components/Navigation';
import { Footer } from '@/components/Footer'; import { Footer } from '@/components/Footer';
@@ -7,17 +7,23 @@ import { ComparisonToast } from '@/components/ComparisonToast';
import { ComparisonProvider } from '@/context/ComparisonProvider'; import { ComparisonProvider } from '@/context/ComparisonProvider';
import './globals.css'; import './globals.css';
const dmSans = DM_Sans({ // Schibsted Grotesk carries the whole interface — headings, labels, and every
// figure. It was drawn for news brands, so its tabular numerals hold a column
// properly, which matters on a page that is mostly percentages.
const schibsted = Schibsted_Grotesk({
subsets: ['latin'], subsets: ['latin'],
weight: ['400', '500', '600', '700'], weight: ['400', '500', '600', '700'],
variable: '--font-dm-sans', variable: '--font-schibsted',
display: 'swap', display: 'swap',
}); });
const playfairDisplay = Playfair_Display({ // Literata is for prose only — the admissions guide, methodology notes, the
// editorial sections. It never touches a number or a control.
const literata = Literata({
subsets: ['latin'], subsets: ['latin'],
weight: ['600', '700'], weight: ['400', '600'],
variable: '--font-playfair', style: ['normal', 'italic'],
variable: '--font-literata',
display: 'swap', display: 'swap',
}); });
@@ -27,9 +33,11 @@ export const viewport: Viewport = {
// viewport-fit=cover lets us paint behind the notch / Dynamic Island so // viewport-fit=cover lets us paint behind the notch / Dynamic Island so
// env(safe-area-inset-*) values resolve to real numbers on iPhone. // env(safe-area-inset-*) values resolve to real numbers on iPhone.
viewportFit: 'cover', viewportFit: 'cover',
// These must match --bg-primary in globals.css for each theme, or the
// browser chrome and the page disagree at the top edge of the screen.
themeColor: [ themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#faf7f2' }, { media: '(prefers-color-scheme: light)', color: '#f2f1ed' },
{ media: '(prefers-color-scheme: dark)', color: '#1a1612' }, { media: '(prefers-color-scheme: dark)', color: '#10151b' },
], ],
}; };
@@ -47,11 +55,10 @@ export const metadata: Metadata = {
keywords: 'school comparison, KS2 results, KS4 results, primary school, secondary school, England schools, SATs results, GCSE results', keywords: 'school comparison, KS2 results, KS4 results, primary school, secondary school, England schools, SATs results, GCSE results',
authors: [{ name: 'SchoolCompare' }], authors: [{ name: 'SchoolCompare' }],
manifest: '/manifest.json', manifest: '/manifest.json',
icons: { // No `icons` key on purpose: setting it here would override the file
icon: '/favicon.svg', // conventions. app/icon.svg and app/apple-icon.tsx are the source, and
shortcut: '/favicon.svg', // app/opengraph-image.tsx supplies og:image and twitter:image.
apple: '/favicon.svg', metadataBase: new URL('https://schoolcompare.co.uk'),
},
openGraph: { openGraph: {
type: 'website', type: 'website',
title: 'SchoolCompare | Compare School Performance', title: 'SchoolCompare | Compare School Performance',
@@ -60,7 +67,8 @@ export const metadata: Metadata = {
siteName: 'SchoolCompare', siteName: 'SchoolCompare',
}, },
twitter: { twitter: {
card: 'summary', // summary_large_image now that there is an image worth showing.
card: 'summary_large_image',
title: 'SchoolCompare | Compare School Performance', title: 'SchoolCompare | Compare School Performance',
description: 'Compare primary and secondary school SATs and GCSE performance across England', description: 'Compare primary and secondary school SATs and GCSE performance across England',
}, },
@@ -94,8 +102,7 @@ export default function RootLayout({
strategy="afterInteractive" strategy="afterInteractive"
/> />
</head> </head>
<body className={`${dmSans.variable} ${playfairDisplay.variable}`}> <body className={`${schibsted.variable} ${literata.variable}`}>
<div className="noise-overlay" />
<ComparisonProvider> <ComparisonProvider>
<a href="#main-content" className="skip-link">Skip to main content</a> <a href="#main-content" className="skip-link">Skip to main content</a>
<Navigation /> <Navigation />
+132
View File
@@ -0,0 +1,132 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { ImageResponse } from 'next/og';
import { BARS } from '@/components/Logo';
/**
* The site had no og:image at all, so every link pasted into a class WhatsApp
* group or a Slack channel rendered as a bare grey text card — on the highest
* intent channel this product has.
*
* The card is the mark at poster scale: the cohort as a wide distribution with
* one school lit up. It states the proposition rather than decorating it.
*/
export const alt = 'SchoolCompare — compare every school in England against the national picture';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
const PAPER = '#f2f1ed';
const INK = '#16202a';
const MUTED = '#5c6570';
const BRAND = '#584a9b';
// A plausible national distribution, drawn once at poster scale. The lit bar
// is the same position the mark uses.
const SPREAD = [8, 12, 17, 24, 33, 44, 57, 70, 82, 92, 98, 100, 96, 88, 77, 65, 53, 41, 31, 22, 15, 10, 7, 5];
// Right of the peak: score runs left to right, so the lit school reads as
// above the national average. At index 9 it sat on the low side, which quietly
// said the opposite.
const LIT = 14;
async function font(file: string) {
return readFile(join(process.cwd(), 'assets', file));
}
export default async function OpengraphImage() {
const [regular, bold] = await Promise.all([
font('SchibstedGrotesk-Regular.ttf'),
font('SchibstedGrotesk-Bold.ttf'),
]);
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
background: PAPER,
padding: '58px 72px 52px',
fontFamily: 'Schibsted',
}}
>
{/* Lockup */}
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
<div style={{ position: 'relative', width: 40, height: 40, display: 'flex' }}>
{BARS.map(([x, y, w, h, isSchool], i) => (
<div
key={x}
style={{
position: 'absolute',
left: x,
top: y,
width: w,
height: h,
borderRadius: 1.5,
background: isSchool ? BRAND : INK,
opacity: isSchool ? 1 : [0.22, 0.38, 0.55, 1, 0.22][i],
}}
/>
))}
</div>
<div style={{ display: 'flex', fontSize: 29, fontWeight: 700, letterSpacing: '-0.028em' }}>
<span style={{ color: INK }}>School</span>
<span style={{ color: BRAND }}>Compare</span>
</div>
</div>
{/* Proposition. Sized so the headline holds two lines and the subhead
one — an orphaned word reads as an accident at this size. */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
<div
style={{
display: 'flex',
fontSize: 66,
fontWeight: 700,
letterSpacing: '-0.036em',
lineHeight: 1.03,
color: INK,
maxWidth: 1010,
}}
>
A score means nothing until you see the spread.
</div>
<div style={{ display: 'flex', fontSize: 26, color: MUTED, letterSpacing: '-0.005em' }}>
24,000+ schools in England · SATs, GCSEs, Ofsted and admissions
</div>
</div>
{/* The device, at poster scale. Same object as the mark above. */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 7, height: 120 }}>
{SPREAD.map((h, i) => (
<div
key={i}
style={{
flex: 1,
height: `${h}%`,
borderRadius: 2,
background: i === LIT ? BRAND : INK,
opacity: i === LIT ? 1 : 0.15,
}}
/>
))}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 18, color: MUTED, letterSpacing: '0.09em' }}>
<span>NATIONAL SPREAD</span>
<span style={{ letterSpacing: '-0.005em' }}>schoolcompare.co.uk</span>
</div>
</div>
</div>
),
{
...size,
fonts: [
{ name: 'Schibsted', data: regular, weight: 400, style: 'normal' },
{ name: 'Schibsted', data: bold, weight: 700, style: 'normal' },
],
}
);
}
Binary file not shown.
Binary file not shown.
+14 -6
View File
@@ -9,6 +9,7 @@
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import { ChartOptions } from 'chart.js'; import { ChartOptions } from 'chart.js';
import '@/lib/chartSetup'; import '@/lib/chartSetup';
import { useThemeTokens, alpha } from '@/lib/theme';
import { formatAcademicYear } from '@/lib/utils'; import { formatAcademicYear } from '@/lib/utils';
import { fillAcademicYears } from '@/lib/compareChartData'; import { fillAcademicYears } from '@/lib/compareChartData';
import type { SchoolAdmissions } from '@/lib/types'; import type { SchoolAdmissions } from '@/lib/types';
@@ -38,6 +39,11 @@ export default function AdmissionsTrendChart({ history }: { history: SchoolAdmis
const yMin = Math.max(0, Math.floor((lo - padded) / 5) * 5); const yMin = Math.max(0, Math.floor((lo - padded) / 5) * 5);
const yMax = Math.min(100, Math.ceil((hi + padded) / 5) * 5); const yMax = Math.min(100, Math.ceil((hi + padded) / 5) * 5);
// Canvas can't read var(), so pull the resolved tokens through the bridge.
const [cBrand, cGrid, cInverse, cInverseText, cCard] = useThemeTokens(
'--brand', '--chart-grid', '--surface-inverse', '--text-inverse', '--bg-card'
);
const options: ChartOptions<'line'> = { const options: ChartOptions<'line'> = {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
@@ -49,7 +55,9 @@ export default function AdmissionsTrendChart({ history }: { history: SchoolAdmis
legend: { display: false }, legend: { display: false },
title: { display: false }, title: { display: false },
tooltip: { tooltip: {
backgroundColor: 'rgba(26,22,18,0.92)', backgroundColor: cInverse,
titleColor: cInverseText,
bodyColor: cInverseText,
padding: 10, padding: 10,
titleFont: { size: 12 }, titleFont: { size: 12 },
bodyFont: { size: 12 }, bodyFont: { size: 12 },
@@ -62,7 +70,7 @@ export default function AdmissionsTrendChart({ history }: { history: SchoolAdmis
y: { y: {
min: yMin, min: yMin,
max: yMax, max: yMax,
grid: { color: 'rgba(0,0,0,0.05)' }, grid: { color: cGrid },
ticks: { font: { size: 11 }, maxTicksLimit: 5, callback: (v) => `${v}%` }, ticks: { font: { size: 11 }, maxTicksLimit: 5, callback: (v) => `${v}%` },
}, },
x: { x: {
@@ -80,14 +88,14 @@ export default function AdmissionsTrendChart({ history }: { history: SchoolAdmis
data: values, data: values,
clip: false as const, clip: false as const,
spanGaps: false, spanGaps: false,
borderColor: '#e07256', borderColor: cBrand,
backgroundColor: 'rgba(224,114,86,0.10)', backgroundColor: alpha('--brand', 0.10),
borderWidth: 2.5, borderWidth: 2.5,
tension: 0.3, tension: 0.3,
fill: true, fill: true,
pointRadius: values.map((_, i) => (i === lastIdx ? 5 : 3)), pointRadius: values.map((_, i) => (i === lastIdx ? 5 : 3)),
pointBackgroundColor: '#e07256', pointBackgroundColor: cBrand,
pointBorderColor: '#fff', pointBorderColor: cCard,
pointBorderWidth: values.map((_, i) => (i === lastIdx ? 2 : 0)), pointBorderWidth: values.map((_, i) => (i === lastIdx ? 2 : 0)),
pointHoverRadius: 6, pointHoverRadius: 6,
}, },
+79 -74
View File
@@ -27,7 +27,7 @@
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-muted, #6d685f); color: var(--text-muted);
margin-bottom: 0.85rem; margin-bottom: 0.85rem;
padding-left: 0.85rem; padding-left: 0.85rem;
} }
@@ -39,7 +39,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.1rem; gap: 0.1rem;
border-left: 2px solid var(--border-color, #e5dfd5); border-left: 2px solid var(--border);
} }
.navLink { .navLink {
@@ -51,33 +51,33 @@
border-left: 2px solid transparent; border-left: 2px solid transparent;
font-size: 0.88rem; font-size: 0.88rem;
font-weight: 500; font-weight: 500;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
transition: color 0.15s ease, border-color 0.15s ease; transition: color 0.15s ease, border-color 0.15s ease;
} }
.navLink:hover { .navLink:hover {
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
.navLinkActive { .navLinkActive {
color: var(--accent-teal, #2d7d7d); color: var(--brand);
font-weight: 700; font-weight: 700;
border-left-color: var(--accent-teal, #2d7d7d); border-left-color: var(--brand);
} }
.navDot { .navDot {
width: 6px; width: 6px;
height: 6px; height: 6px;
border-radius: 50%; border-radius: 50%;
background: var(--border-color, #e5dfd5); background: var(--border);
flex-shrink: 0; flex-shrink: 0;
transition: background 0.15s ease; transition: background 0.15s ease;
} }
.navLinkActive .navDot { .navLinkActive .navDot {
background: var(--accent-teal, #2d7d7d); background: var(--brand);
} }
/* ─── Hero ───────────────────────────────────────────── */ /* ─── Hero ───────────────────────────────────────────── */
@@ -95,8 +95,8 @@
font-weight: 600; font-weight: 600;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
color: var(--accent-teal, #2d7d7d); color: var(--brand);
background: rgba(45, 125, 125, 0.1); background: rgba(var(--brand-rgb), 0.1);
padding: 0.3rem 0.7rem; padding: 0.3rem 0.7rem;
border-radius: 999px; border-radius: 999px;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -107,22 +107,22 @@
width: 6px; width: 6px;
height: 6px; height: 6px;
border-radius: 50%; border-radius: 50%;
background: var(--accent-teal, #2d7d7d); background: var(--brand);
} }
.heroTitle { .heroTitle {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-size: 2.75rem; font-size: 2.75rem;
font-weight: 700; font-weight: 700;
line-height: 1.1; line-height: 1.1;
letter-spacing: -0.015em; letter-spacing: -0.015em;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin-bottom: 0.85rem; margin-bottom: 0.85rem;
} }
.heroSub { .heroSub {
font-size: 1.05rem; font-size: 1.05rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
max-width: 640px; max-width: 640px;
margin: 0 auto; margin: 0 auto;
line-height: 1.55; line-height: 1.55;
@@ -132,7 +132,7 @@
.countdownSection { .countdownSection {
padding: 0 0 2.5rem; padding: 0 0 2.5rem;
border-bottom: 1px solid var(--border-color, #e5dfd5); border-bottom: 1px solid var(--border);
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
} }
@@ -145,7 +145,7 @@
font-weight: 700; font-weight: 700;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-muted, #6d685f); color: var(--text-muted);
} }
.countdownRail { .countdownRail {
@@ -155,11 +155,11 @@
} }
.chip { .chip {
background: var(--bg-card, #fff); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;
padding: 1rem 1.1rem 0.9rem; padding: 1rem 1.1rem 0.9rem;
box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06); box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.06);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.2rem; gap: 0.2rem;
@@ -177,12 +177,12 @@
border-radius: 12px 12px 0 0; border-radius: 12px 12px 0 0;
} }
.chipDeadline::before { background: var(--accent-coral, #e07256); } .chipDeadline::before { background: var(--brand); }
.chipOffer::before { background: var(--accent-teal, #2d7d7d); } .chipOffer::before { background: var(--status-above); }
.chipUrgent { .chipUrgent {
border-color: rgba(224, 114, 86, 0.4); border-color: rgba(var(--status-below-rgb), 0.4);
background: rgba(224, 114, 86, 0.04); background: rgba(var(--status-below-rgb), 0.04);
} }
.chipTrack { .chipTrack {
@@ -196,8 +196,8 @@
margin-bottom: 0.15rem; margin-bottom: 0.15rem;
} }
.chipTrackDeadline { color: var(--accent-coral-dark, #b04a2e); } .chipTrackDeadline { color: var(--brand-strong); }
.chipTrackOffer { color: var(--accent-teal, #2d7d7d); } .chipTrackOffer { color: var(--status-above); }
.chipTrackDot { .chipTrackDot {
width: 5px; width: 5px;
@@ -208,7 +208,8 @@
} }
.chipDays { .chipDays {
font-family: var(--font-playfair), 'Playfair Display', serif; font-variant-numeric: tabular-nums;
font-family: var(--font-display);
font-size: 2.6rem; font-size: 2.6rem;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
@@ -217,17 +218,17 @@
.chipDeadline .chipDays, .chipDeadline .chipDays,
.chipUrgent .chipDays { .chipUrgent .chipDays {
color: var(--accent-coral-dark, #c45a3f); color: var(--status-below);
} }
.chipOffer .chipDays { .chipOffer .chipDays {
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
} }
.chipDaysUnit { .chipDaysUnit {
font-size: 0.78rem; font-size: 0.78rem;
font-weight: 500; font-weight: 500;
color: var(--text-muted, #6d685f); color: var(--text-muted);
margin-left: 0.2rem; margin-left: 0.2rem;
vertical-align: bottom; vertical-align: bottom;
line-height: 2; line-height: 2;
@@ -236,14 +237,14 @@
.chipMilestone { .chipMilestone {
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
line-height: 1.25; line-height: 1.25;
margin-top: 0.1rem; margin-top: 0.1rem;
} }
.chipDate { .chipDate {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
margin-top: 0.05rem; margin-top: 0.05rem;
} }
@@ -251,8 +252,8 @@
.track { .track {
margin-bottom: 3rem; margin-bottom: 3rem;
background: var(--bg-card, #fff); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 14px; border-radius: 14px;
overflow: hidden; overflow: hidden;
} }
@@ -263,7 +264,7 @@
justify-content: space-between; justify-content: space-between;
gap: 1.5rem; gap: 1.5rem;
padding: 1.75rem 2rem 1.5rem; padding: 1.75rem 2rem 1.5rem;
border-bottom: 1px solid var(--border-color, #e5dfd5); border-bottom: 1px solid var(--border);
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -278,22 +279,22 @@
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
margin-bottom: 0.35rem; margin-bottom: 0.35rem;
} }
.trackTitle { .trackTitle {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin-bottom: 0.4rem; margin-bottom: 0.4rem;
line-height: 1.2; line-height: 1.2;
} }
.trackSub { .trackSub {
font-size: 0.9rem; font-size: 0.9rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
line-height: 1.5; line-height: 1.5;
margin: 0; margin: 0;
} }
@@ -317,13 +318,13 @@
font-weight: 700; font-weight: 700;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-muted, #6d685f); color: var(--text-muted);
} }
.trackDateVal { .trackDateVal {
font-size: 0.88rem; font-size: 0.88rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
/* ─── Timeline ───────────────────────────────────────── */ /* ─── Timeline ───────────────────────────────────────── */
@@ -356,27 +357,27 @@
width: 14px; width: 14px;
height: 14px; height: 14px;
border-radius: 50%; border-radius: 50%;
background: var(--bg-card, #fff); background: var(--bg-card);
border: 2px solid var(--border-color, #e5dfd5); border: 2px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
transition: border-color 0.2s ease, background 0.2s ease; transition: border-color 0.2s ease, background 0.2s ease;
} }
.stepDeadline .stepDot { .stepDeadline .stepDot {
background: rgba(224, 114, 86, 0.15); background: rgba(var(--brand-rgb), 0.15);
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
} }
.stepOffer .stepDot { .stepOffer .stepDot {
background: rgba(45, 125, 125, 0.15); background: rgba(var(--status-above-rgb), 0.15);
border-color: var(--accent-teal, #2d7d7d); border-color: var(--status-above);
} }
.stepLine { .stepLine {
width: 2px; width: 2px;
flex: 1; flex: 1;
min-height: 24px; min-height: 24px;
background: var(--border-color, #e5dfd5); background: var(--border);
margin: 4px 0; margin: 4px 0;
} }
@@ -395,27 +396,28 @@
font-weight: 700; font-weight: 700;
letter-spacing: 0.06em; letter-spacing: 0.06em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-muted, #6d685f); color: var(--text-muted);
margin-bottom: 0.2rem; margin-bottom: 0.2rem;
} }
.stepDeadline .stepDate { color: var(--accent-coral-dark, #b04a2e); } .stepDeadline .stepDate { color: var(--brand-strong); }
.stepOffer .stepDate { color: var(--accent-teal, #2d7d7d); } .stepOffer .stepDate { color: var(--status-above); }
.stepTitle { .stepTitle {
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
line-height: 1.3; line-height: 1.3;
} }
.stepDeadline .stepTitle { color: var(--accent-coral-dark, #c45a3f); } .stepDeadline .stepTitle { color: var(--brand-strong); }
.stepOffer .stepTitle { color: var(--accent-teal, #2d7d7d); } .stepOffer .stepTitle { color: var(--status-above); }
.stepBody { .stepBody {
font-family: var(--font-prose);
font-size: 0.88rem; font-size: 0.88rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
line-height: 1.55; line-height: 1.55;
margin: 0; margin: 0;
} }
@@ -427,13 +429,13 @@
margin-top: 0.6rem; margin-top: 0.6rem;
font-size: 0.82rem; font-size: 0.82rem;
font-weight: 600; font-weight: 600;
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
text-decoration: none; text-decoration: none;
transition: gap 0.2s ease, color 0.2s ease; transition: gap 0.2s ease, color 0.2s ease;
} }
.stepTool:hover { .stepTool:hover {
color: var(--accent-coral-dark, #c45a3f); color: var(--brand-strong);
gap: 0.6rem; gap: 0.6rem;
} }
@@ -455,9 +457,9 @@
display: flex; display: flex;
gap: 0.55rem; gap: 0.55rem;
padding: 0.6rem 0.75rem; padding: 0.6rem 0.75rem;
background: var(--accent-gold-bg, rgba(201, 162, 39, 0.12)); background: var(--status-below-bg);
border: 1px solid rgba(201, 162, 39, 0.28); border: 1px solid rgba(var(--status-below-rgb), 0.28);
border-left: 3px solid var(--accent-gold, #c9a227); border-left: 3px solid var(--status-below);
border-radius: 8px; border-radius: 8px;
} }
@@ -467,8 +469,8 @@
height: 1.05rem; height: 1.05rem;
margin-top: 0.1rem; margin-top: 0.1rem;
border-radius: 50%; border-radius: 50%;
background: var(--accent-gold, #c9a227); background: var(--status-below);
color: #fff; color: var(--text-inverse);
font-size: 0.72rem; font-size: 0.72rem;
font-weight: 800; font-weight: 800;
line-height: 1.05rem; line-height: 1.05rem;
@@ -482,14 +484,15 @@
.calloutTitle { .calloutTitle {
font-size: 0.82rem; font-size: 0.82rem;
font-weight: 700; font-weight: 700;
color: var(--accent-gold-text, #7a6800); color: var(--status-below);
line-height: 1.3; line-height: 1.3;
margin-bottom: 0.15rem; margin-bottom: 0.15rem;
} }
.calloutBody { .calloutBody {
font-family: var(--font-prose);
font-size: 0.82rem; font-size: 0.82rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
line-height: 1.5; line-height: 1.5;
margin: 0; margin: 0;
} }
@@ -501,10 +504,10 @@
} }
.tipsHeading { .tipsHeading {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-size: 1.35rem; font-size: 1.35rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
@@ -515,16 +518,17 @@
} }
.tipCard { .tipCard {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-radius: 12px; border-radius: 12px;
padding: 1.25rem; padding: 1.25rem;
} }
.tipNumber { .tipNumber {
font-family: var(--font-playfair), 'Playfair Display', serif; font-variant-numeric: tabular-nums;
font-family: var(--font-display);
font-size: 1.75rem; font-size: 1.75rem;
font-weight: 700; font-weight: 700;
color: #7d766b; /* 3.8:1 on cream — AA large-text for this 28px numeral */ color: var(--text-muted); /* 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;
} }
@@ -532,14 +536,15 @@
.tipHeading { .tipHeading {
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin-bottom: 0.4rem; margin-bottom: 0.4rem;
line-height: 1.3; line-height: 1.3;
} }
.tipBody { .tipBody {
font-family: var(--font-prose);
font-size: 0.875rem; font-size: 0.875rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
line-height: 1.55; line-height: 1.55;
margin: 0; margin: 0;
} }
@@ -557,8 +562,8 @@
position: sticky; position: sticky;
top: 0; top: 0;
padding-top: 0; padding-top: 0;
background: var(--bg-primary, #faf8f3); background: var(--bg-primary);
border-bottom: 1px solid var(--border-color, #e5dfd5); border-bottom: 1px solid var(--border);
margin: 0 -1.25rem 1rem; margin: 0 -1.25rem 1rem;
padding: 0.6rem 1.25rem; padding: 0.6rem 1.25rem;
z-index: 10; z-index: 10;
@@ -590,8 +595,8 @@
} }
.navLinkActive { .navLinkActive {
background: var(--text-primary, #1a1612); background: var(--text-primary);
color: #fff; color: var(--text-inverse);
border-left: none; border-left: none;
} }
@@ -43,7 +43,7 @@
min-height: 40px; min-height: 40px;
min-width: 0; min-width: 0;
padding: 4px 10px; padding: 4px 10px;
border: 1px solid rgba(0, 0, 0, .12); border: 1px solid var(--border);
border-radius: 999px; border-radius: 999px;
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
@@ -52,8 +52,8 @@
} }
.chip[aria-pressed="true"] { .chip[aria-pressed="true"] {
background: rgba(0, 0, 0, .06); background: var(--bg-secondary);
border-color: rgba(0, 0, 0, .35); border-color: var(--border-strong);
} }
.chipDot { .chipDot {
+14 -4
View File
@@ -15,6 +15,7 @@ import { useEffect, useState } from 'react';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import { ChartOptions, ChartDataset, PointStyle } from 'chart.js'; import { ChartOptions, ChartDataset, PointStyle } from 'chart.js';
import '@/lib/chartSetup'; import '@/lib/chartSetup';
import { useSeriesColors, useThemeTokens } from '@/lib/theme';
import { buildCompareChart } from '@/lib/compareChartData'; import { buildCompareChart } from '@/lib/compareChartData';
import type { ComparisonData } from '@/lib/types'; import type { ComparisonData } from '@/lib/types';
import { import {
@@ -48,6 +49,13 @@ const POINT_STYLES: PointStyle[] = ['circle', 'triangle', 'rect', 'rectRot', 'st
export function ComparisonChart({ comparisonData, schools, metric, metricLabel, nationalByYear, isSecondary = false }: ComparisonChartProps) { export function ComparisonChart({ comparisonData, schools, metric, metricLabel, nationalByYear, isSecondary = false }: ComparisonChartProps) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
// Canvas can't resolve var(), so the series come through the token bridge.
// Same index order as CHART_COLORS, so a school's line and its DOM swatch
// stay the same colour.
const seriesColors = useSeriesColors();
const [cRef, cGrid, cInverse, cInverseText] = useThemeTokens(
'--chart-reference', '--chart-grid', '--surface-inverse', '--text-inverse'
);
const [focusedUrn, setFocusedUrn] = useState<number | null>(null); const [focusedUrn, setFocusedUrn] = useState<number | null>(null);
// A focused school that leaves the basket must not linger. // A focused school that leaves the basket must not linger.
@@ -67,7 +75,7 @@ export function ComparisonChart({ comparisonData, schools, metric, metricLabel,
const datasets: ChartDataset<'line'>[] = built.schoolDatasets.map((series) => { const datasets: ChartDataset<'line'>[] = built.schoolDatasets.map((series) => {
const school = schools[series.schoolIndex]; const school = schools[series.schoolIndex];
const color = CHART_COLORS[series.schoolIndex % CHART_COLORS.length]; const color = seriesColors[series.schoolIndex % seriesColors.length];
const dimmed = focusedUrn !== null && focusedUrn !== school.urn; const dimmed = focusedUrn !== null && focusedUrn !== school.urn;
return { return {
@@ -90,7 +98,7 @@ export function ComparisonChart({ comparisonData, schools, metric, metricLabel,
datasets.push({ datasets.push({
label: built.englandDataset.label, label: built.englandDataset.label,
data: built.englandDataset.data, data: built.englandDataset.data,
borderColor: 'rgba(109, 104, 95, 0.9)', borderColor: cRef,
backgroundColor: 'transparent', backgroundColor: 'transparent',
borderWidth: 1.5, borderWidth: 1.5,
borderDash: built.englandDataset.borderDash, borderDash: built.englandDataset.borderDash,
@@ -138,7 +146,9 @@ export function ComparisonChart({ comparisonData, schools, metric, metricLabel,
display: false, display: false,
}, },
tooltip: { tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)', backgroundColor: cInverse,
titleColor: cInverseText,
bodyColor: cInverseText,
padding: isMobile ? 10 : 12, padding: isMobile ? 10 : 12,
titleFont: { titleFont: {
size: isMobile ? 12 : 14, size: isMobile ? 12 : 14,
@@ -182,7 +192,7 @@ export function ComparisonChart({ comparisonData, schools, metric, metricLabel,
...(isMobile && { maxTicksLimit: 5 }), ...(isMobile && { maxTicksLimit: 5 }),
}, },
grid: { grid: {
color: 'rgba(0, 0, 0, 0.05)', color: cGrid,
}, },
}, },
x: { x: {
@@ -23,11 +23,11 @@
flex-direction: column; flex-direction: column;
gap: 0; gap: 0;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
background: var(--bg-primary, #faf7f2); background: var(--bg-primary);
color: var(--text-primary, #2c2420); color: var(--text-primary);
border-radius: 16px; border-radius: 16px;
box-shadow: 0 8px 32px rgba(44, 36, 32, 0.18), 0 2px 8px rgba(44, 36, 32, 0.08); box-shadow: 0 8px 32px rgba(var(--shadow-rgb), 0.18), 0 2px 8px rgba(var(--shadow-rgb), 0.08);
border: 1px solid var(--border-color, #e8ddd4); border: 1px solid var(--border);
min-width: 280px; min-width: 280px;
} }
@@ -37,8 +37,8 @@
justify-content: center; justify-content: center;
width: 28px; width: 28px;
height: 28px; height: 28px;
background: var(--accent-coral, #e07256); background: var(--brand);
color: white; color: var(--brand-on);
border-radius: 50%; border-radius: 50%;
font-weight: 700; font-weight: 700;
font-size: 0.9rem; font-size: 0.9rem;
@@ -62,13 +62,13 @@
gap: 0.5rem; gap: 0.5rem;
font-weight: 600; font-weight: 600;
font-size: 0.875rem; font-size: 0.875rem;
color: var(--text-primary, #2c2420); color: var(--text-primary);
} }
.collapseBtn { .collapseBtn {
background: none; background: none;
border: none; border: none;
color: var(--text-muted, #8a7a72); color: var(--text-muted);
cursor: pointer; cursor: pointer;
padding: 0.25rem; padding: 0.25rem;
line-height: 1; line-height: 1;
@@ -79,7 +79,7 @@
} }
.collapseBtn:hover { .collapseBtn:hover {
color: var(--text-primary, #2c2420); color: var(--text-primary);
} }
.schoolList { .schoolList {
@@ -95,13 +95,13 @@
justify-content: space-between; justify-content: space-between;
gap: 0.5rem; gap: 0.5rem;
padding: 0.3rem 0.5rem; padding: 0.3rem 0.5rem;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-radius: var(--radius-sm, 4px); border-radius: var(--radius-sm, 4px);
} }
.schoolName { .schoolName {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-primary, #2c2420); color: var(--text-primary);
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -111,7 +111,7 @@
.removeSchoolBtn { .removeSchoolBtn {
background: none; background: none;
border: none; border: none;
color: var(--text-muted, #8a7a72); color: var(--text-muted);
cursor: pointer; cursor: pointer;
font-size: 1rem; font-size: 1rem;
padding: 0 0.25rem; padding: 0 0.25rem;
@@ -121,7 +121,7 @@
} }
.removeSchoolBtn:hover { .removeSchoolBtn:hover {
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
} }
.toastActions { .toastActions {
@@ -129,13 +129,13 @@
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
padding-top: 0.625rem; padding-top: 0.625rem;
border-top: 1px solid var(--border-color, #e8ddd4); border-top: 1px solid var(--border);
} }
.btnClearAll { .btnClearAll {
background: none; background: none;
border: none; border: none;
color: var(--text-muted, #8a7a72); color: var(--text-muted);
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 500; font-weight: 500;
cursor: pointer; cursor: pointer;
@@ -145,13 +145,13 @@
} }
.btnClearAll:hover { .btnClearAll:hover {
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
} }
.btnCompare { .btnCompare {
flex: 1; flex: 1;
background: var(--accent-coral, #e07256); background: var(--brand);
color: white; color: var(--brand-on);
padding: 0.6rem 1.25rem; padding: 0.6rem 1.25rem;
border-radius: 25px; border-radius: 25px;
font-weight: 600; font-weight: 600;
@@ -164,7 +164,7 @@
.btnCompare:hover { .btnCompare:hover {
transform: translateY(-1px); transform: translateY(-1px);
background: var(--accent-coral-dark, #c9614a); background: var(--brand-strong);
} }
/* Hidden on phones — the bottom tab bar's Compare badge already /* Hidden on phones — the bottom tab bar's Compare badge already
+26 -26
View File
@@ -18,14 +18,14 @@
.header h1 { .header h1 {
font-size: 2.25rem; font-size: 2.25rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
} }
.subtitle { .subtitle {
font-size: 1rem; font-size: 1rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
margin: 0; margin: 0;
line-height: 1.6; line-height: 1.6;
max-width: 60ch; max-width: 60ch;
@@ -43,7 +43,7 @@
display: flex; display: flex;
gap: 0; gap: 0;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
width: fit-content; width: fit-content;
@@ -53,8 +53,8 @@
padding: 0.625rem 1.5rem; padding: 0.625rem 1.5rem;
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 500; font-weight: 500;
background: var(--bg-card, white); background: var(--bg-card);
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all 0.15s ease;
@@ -62,21 +62,21 @@
} }
.phaseTab:not(:last-child) { .phaseTab:not(:last-child) {
border-right: 1px solid var(--border-color, #e5dfd5); border-right: 1px solid var(--border);
} }
.phaseTab:hover { .phaseTab:hover {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
.phaseTabActive { .phaseTabActive {
background: var(--accent-coral-dark, #b04a2e); background: var(--brand-strong);
color: white; color: var(--brand-on);
font-weight: 600; font-weight: 600;
} }
.phaseTabActive:hover { .phaseTabActive:hover {
background: var(--accent-coral-darker, #9c3f26); background: var(--brand-stronger);
} }
/* Sticky school bar — column identity while scrolling; horizontal scroll on /* Sticky school bar — column identity while scrolling; horizontal scroll on
@@ -87,23 +87,23 @@
position: sticky; position: sticky;
top: 65px; top: 65px;
z-index: 10; z-index: 10;
background: var(--bg-primary, #faf7f2); background: var(--bg-primary);
display: flex; display: flex;
gap: 0.75rem; gap: 0.75rem;
overflow-x: auto; overflow-x: auto;
padding: 0.75rem 0; padding: 0.75rem 0;
border-bottom: 1px solid var(--border-light, #e5dfd5); border-bottom: 1px solid var(--border);
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
.schoolChip { .schoolChip {
flex: 1 1 0; flex: 1 1 0;
min-width: 180px; min-width: 180px;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-light, #e5dfd5); border: 1px solid var(--border);
border-top: 3px solid var(--accent-coral, #e07256); border-top: 3px solid var(--brand);
border-radius: 8px; border-radius: 8px;
box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.06)); box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.06));
padding: 0.55rem 0.75rem; padding: 0.55rem 0.75rem;
display: flex; display: flex;
gap: 0.55rem; gap: 0.55rem;
@@ -126,12 +126,12 @@
font-weight: 600; font-weight: 600;
font-size: 0.92rem; font-size: 0.92rem;
line-height: 1.25; line-height: 1.25;
color: var(--text-primary, #1a1612); color: var(--text-primary);
text-decoration: none; text-decoration: none;
} }
.chipName:hover { .chipName:hover {
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
} }
/* Full name on desktop, short name on the compact mobile pills. */ /* Full name on desktop, short name on the compact mobile pills. */
@@ -142,7 +142,7 @@
.chipMeta { .chipMeta {
display: block; display: block;
font-size: 0.78rem; font-size: 0.78rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -185,22 +185,22 @@
font-weight: 600; font-weight: 600;
letter-spacing: 0.06em; letter-spacing: 0.06em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-muted, #6d685f); color: var(--text-muted);
} }
.barCaptionCount { .barCaptionCount {
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 600; font-weight: 600;
line-height: 1.3; line-height: 1.3;
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
} }
.chipRemove { .chipRemove {
margin-left: auto; margin-left: auto;
border: none; border: none;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
color: var(--text-muted, #6d685f); color: var(--text-muted);
border-radius: 50%; border-radius: 50%;
width: 22px; width: 22px;
height: 22px; height: 22px;
@@ -212,9 +212,9 @@
.footnote { .footnote {
font-size: 0.78rem; font-size: 0.78rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
margin-top: 2.5rem; margin-top: 2.5rem;
border-top: 1px solid var(--border-light, #e5dfd5); border-top: 1px solid var(--border);
padding-top: 1rem; padding-top: 1rem;
max-width: 75ch; max-width: 75ch;
} }
+6 -6
View File
@@ -23,16 +23,16 @@
} }
.good { .good {
background: var(--accent-teal-bg, rgba(45, 125, 125, 0.12)); background: var(--status-above-bg);
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
} }
.bad { .bad {
background: var(--accent-coral-bg, rgba(224, 114, 86, 0.12)); background: var(--status-below-bg);
color: var(--accent-coral-dark, #b04a2e); color: var(--status-below);
} }
.neutral { .neutral {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
color: var(--text-muted, #8a847a); color: var(--text-muted);
} }
+1 -1
View File
@@ -61,7 +61,7 @@
border-radius: 50%; border-radius: 50%;
transform: translateX(-50%); transform: translateX(-50%);
border: 2px solid var(--bg-card); border: 2px solid var(--bg-card);
box-shadow: 0 0 0 1px rgba(26, 22, 18, 0.08); box-shadow: 0 0 0 1px rgba(var(--shadow-rgb), 0.08);
} }
.pointLabel { .pointLabel {
+11 -11
View File
@@ -5,15 +5,15 @@
justify-content: center; justify-content: center;
padding: 4rem 2rem; padding: 4rem 2rem;
text-align: center; text-align: center;
background: var(--bg-card, white); background: var(--bg-card);
border: 2px solid var(--border-color, #e5dfd5); border: 2px solid var(--border);
border-radius: 16px; border-radius: 16px;
min-height: 400px; min-height: 400px;
box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.06)); box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.06));
} }
.icon { .icon {
color: var(--text-muted, #8a847a); color: var(--text-muted);
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
opacity: 0.7; opacity: 0.7;
} }
@@ -22,14 +22,14 @@
margin: 0 0 0.75rem 0; margin: 0 0 0.75rem 0;
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
} }
.message { .message {
margin: 0 0 2rem 0; margin: 0 0 2rem 0;
font-size: 1rem; font-size: 1rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
max-width: 500px; max-width: 500px;
line-height: 1.6; line-height: 1.6;
} }
@@ -38,8 +38,8 @@
padding: 0.875rem 2rem; padding: 0.875rem 2rem;
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
background: var(--accent-coral, #e07256); background: var(--brand);
color: white; color: var(--brand-on);
border: none; border: none;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
@@ -47,9 +47,9 @@
} }
.button:hover { .button:hover {
background: var(--accent-coral-dark, #c45a3f); background: var(--brand-strong);
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(224, 114, 86, 0.3); box-shadow: 0 4px 12px rgba(var(--brand-rgb), 0.3);
} }
.button:active { .button:active {
+47 -47
View File
@@ -1,10 +1,10 @@
.filterBar { .filterBar {
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 10px; border-radius: 10px;
padding: 1.5rem; padding: 1.5rem;
margin-bottom: 2rem; margin-bottom: 2rem;
box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.06)); box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.06));
transition: opacity 0.2s ease; transition: opacity 0.2s ease;
} }
@@ -17,9 +17,9 @@
padding: 2.5rem; padding: 2.5rem;
max-width: 800px; max-width: 800px;
margin: 0 auto 3rem auto; margin: 0 auto 3rem auto;
box-shadow: 0 8px 24px rgba(26, 22, 18, 0.08); box-shadow: 0 8px 24px rgba(var(--shadow-rgb), 0.08);
border-width: 2px; border-width: 2px;
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
} }
.heroMode .omniInput { .heroMode .omniInput {
@@ -39,12 +39,12 @@
.searchHint { .searchHint {
margin: 0.875rem 0 0; margin: 0.875rem 0 0;
font-size: 0.95rem; font-size: 0.95rem;
color: var(--text-secondary, #5a554d); color: var(--text-secondary);
text-align: center; text-align: center;
} }
.searchHint strong { .searchHint strong {
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-weight: 600; font-weight: 600;
} }
@@ -68,8 +68,8 @@
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
padding: 0.625rem 1.375rem; padding: 0.625rem 1.375rem;
background: var(--accent-teal, #2d7d7d); background: var(--brand);
color: #fff; color: var(--brand-on);
border: none; border: none;
border-radius: 999px; border-radius: 999px;
font-size: 0.9375rem; font-size: 0.9375rem;
@@ -80,7 +80,7 @@
} }
.nearMeBtn:hover:not(:disabled) { .nearMeBtn:hover:not(:disabled) {
background: #235f5f; background: var(--status-above);
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -93,8 +93,8 @@
display: inline-block; display: inline-block;
width: 14px; width: 14px;
height: 14px; height: 14px;
border: 2px solid rgba(255, 255, 255, 0.35); border: 2px solid rgba(var(--text-inverse-rgb), 0.35);
border-top-color: #fff; border-top-color: var(--brand-on);
border-radius: 50%; border-radius: 50%;
animation: nearMeSpin 0.7s linear infinite; animation: nearMeSpin 0.7s linear infinite;
flex-shrink: 0; flex-shrink: 0;
@@ -108,7 +108,7 @@
.geoError { .geoError {
font-size: 0.8125rem; font-size: 0.8125rem;
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
margin: 0; margin: 0;
max-width: 340px; max-width: 340px;
text-align: center; text-align: center;
@@ -134,21 +134,21 @@
flex: 1; flex: 1;
padding: 0.875rem 1.25rem; padding: 0.875rem 1.25rem;
font-size: 1.05rem; font-size: 1.05rem;
border: 2px solid var(--border-color, #e5dfd5); border: 2px solid var(--border);
border-radius: 8px; border-radius: 8px;
outline: none; outline: none;
transition: all 0.2s ease; transition: all 0.2s ease;
background: var(--bg-card, white); background: var(--bg-card);
font-family: inherit; font-family: inherit;
} }
.omniInput:focus { .omniInput:focus {
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
box-shadow: 0 0 0 3px var(--accent-coral-bg); box-shadow: 0 0 0 3px var(--brand-bg);
} }
.omniInput::placeholder { .omniInput::placeholder {
color: var(--text-muted, #8a847a); color: var(--text-muted);
} }
.searchButton { .searchButton {
@@ -161,9 +161,9 @@
.spinner { .spinner {
width: 20px; width: 20px;
height: 20px; height: 20px;
border: 2px solid rgba(255, 255, 255, 0.3); border: 2px solid rgba(var(--text-inverse-rgb), 0.3);
border-radius: 50%; border-radius: 50%;
border-top-color: white; border-top-color: var(--brand-on);
animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite;
} }
@@ -177,7 +177,7 @@
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 0.625rem; margin-top: 0.625rem;
padding-top: 0.625rem; padding-top: 0.625rem;
border-top: 1px solid var(--border-color, #e5dfd5); border-top: 1px solid var(--border);
} }
.filterSelect { .filterSelect {
@@ -189,26 +189,26 @@
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
font-family: inherit; font-family: inherit;
border: 1.5px solid var(--border-color, #e5dfd5); border: 1.5px solid var(--border);
border-radius: 8px; border-radius: 8px;
background-color: var(--bg-card, white); background-color: var(--bg-card);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238a847a' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238a847a' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 0.875rem center; background-position: right 0.875rem center;
background-size: 10px 6px; background-size: 10px 6px;
cursor: pointer; cursor: pointer;
outline: none; outline: none;
color: var(--text-primary, #1a1612); color: var(--text-primary);
transition: border-color 0.15s ease, box-shadow 0.15s ease; transition: border-color 0.15s ease, box-shadow 0.15s ease;
} }
.filterSelect:hover { .filterSelect:hover {
border-color: var(--text-muted, #8a847a); border-color: var(--text-muted);
} }
.filterSelect:focus { .filterSelect:focus {
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
box-shadow: 0 0 0 3px rgba(224, 114, 86, 0.12); box-shadow: 0 0 0 3px rgba(var(--brand-rgb), 0.12);
} }
.clearButton { .clearButton {
@@ -263,7 +263,7 @@
.radiusLabel { .radiusLabel {
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 500;
color: var(--text-secondary, #5a554d); color: var(--text-secondary);
white-space: nowrap; white-space: nowrap;
} }
@@ -275,7 +275,7 @@
flex-wrap: wrap; flex-wrap: wrap;
margin-top: 0.875rem; margin-top: 0.875rem;
padding-top: 0.875rem; padding-top: 0.875rem;
border-top: 1px solid var(--border-color, #e5dfd5); border-top: 1px solid var(--border);
} }
.controlsRow .advancedToggle { .controlsRow .advancedToggle {
@@ -297,14 +297,14 @@
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 500;
font-family: inherit; font-family: inherit;
border: 1.5px solid var(--border-color, #e5dfd5); border: 1.5px solid var(--border);
border-radius: 999px; border-radius: 999px;
background-color: var(--bg-card, white); background-color: var(--bg-card);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238a847a' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238a847a' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 0.65rem center; background-position: right 0.65rem center;
background-size: 10px 6px; background-size: 10px 6px;
color: var(--text-primary, #1a1612); color: var(--text-primary);
cursor: pointer; cursor: pointer;
outline: none; outline: none;
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
@@ -312,13 +312,13 @@
} }
.controlSelect:hover { .controlSelect:hover {
border-color: var(--text-muted, #8a847a); border-color: var(--text-muted);
background-color: var(--bg-secondary, #f8f4ef); background-color: var(--bg-secondary);
} }
.controlSelect:focus { .controlSelect:focus {
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
box-shadow: 0 0 0 3px rgba(224, 114, 86, 0.12); box-shadow: 0 0 0 3px rgba(var(--brand-rgb), 0.12);
} }
/* ── Advanced filters toggle ─────────────────────────── */ /* ── Advanced filters toggle ─────────────────────────── */
@@ -328,12 +328,12 @@
align-items: center; align-items: center;
gap: 0.375rem; gap: 0.375rem;
background: none; background: none;
border: 1.5px solid var(--border-color, #e5dfd5); border: 1.5px solid var(--border);
border-radius: 999px; border-radius: 999px;
padding: 0.4rem 0.875rem; padding: 0.4rem 0.875rem;
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 500; font-weight: 500;
color: var(--text-secondary, #5a554d); color: var(--text-secondary);
cursor: pointer; cursor: pointer;
font-family: inherit; font-family: inherit;
transition: all 0.15s ease; transition: all 0.15s ease;
@@ -341,24 +341,24 @@
} }
.advancedToggle:hover { .advancedToggle:hover {
border-color: var(--text-muted, #8a847a); border-color: var(--text-muted);
background-color: var(--bg-secondary, #f8f4ef); background-color: var(--bg-secondary);
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
/* When filters are applied, promote the toggle to a coral pill so users /* When filters are applied, promote the toggle to a coral pill so users
can see at a glance that the result list is being narrowed. */ can see at a glance that the result list is being narrowed. */
.advancedToggleActive { .advancedToggleActive {
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
background: var(--accent-coral-bg, rgba(224, 114, 86, 0.12)); background: var(--brand-bg);
color: var(--accent-coral, #e07256); color: var(--brand);
font-weight: 600; font-weight: 600;
} }
.advancedToggleActive:hover { .advancedToggleActive:hover {
border-color: var(--accent-coral-dark, #c45a3f); border-color: var(--brand-strong);
background: var(--accent-coral-bg, rgba(224, 114, 86, 0.18)); background: var(--brand-bg);
color: var(--accent-coral-dark, #c45a3f); color: var(--brand-strong);
} }
.chevronDown, .chevronDown,
+13 -13
View File
@@ -1,6 +1,6 @@
.footer { .footer {
background: var(--accent-navy, #2c3e50); background: var(--text-primary);
color: var(--bg-secondary, #f3ede4); color: var(--bg-secondary);
margin-top: auto; margin-top: auto;
} }
@@ -27,22 +27,22 @@
margin: 0; margin: 0;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 700; font-weight: 700;
color: var(--bg-primary, #faf7f2); color: var(--bg-primary);
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
} }
.description { .description {
margin: 0; margin: 0;
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.6; line-height: 1.6;
color: rgba(250, 247, 242, 0.7); color: rgba(var(--text-inverse-rgb), 0.7);
} }
.sectionTitle { .sectionTitle {
margin: 0; margin: 0;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
color: var(--accent-gold, #c9a227); color: var(--status-below);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
@@ -58,24 +58,24 @@
.link { .link {
font-size: 0.875rem; font-size: 0.875rem;
color: rgba(250, 247, 242, 0.7); color: rgba(var(--text-inverse-rgb), 0.7);
text-decoration: none; text-decoration: none;
transition: color 0.2s ease; transition: color 0.2s ease;
} }
.link:hover { .link:hover {
color: var(--accent-gold, #c9a227); color: var(--status-below);
} }
.linkDisabled { .linkDisabled {
font-size: 0.875rem; font-size: 0.875rem;
color: rgba(250, 247, 242, 0.4); color: rgba(var(--text-inverse-rgb), 0.4);
cursor: not-allowed; cursor: not-allowed;
} }
.bottom { .bottom {
padding-top: 2rem; padding-top: 2rem;
border-top: 1px solid rgba(250, 247, 242, 0.15); border-top: 1px solid rgba(var(--text-inverse-rgb), 0.15);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 0.5rem;
@@ -85,15 +85,15 @@
.disclaimer { .disclaimer {
margin: 0; margin: 0;
font-size: 0.875rem; font-size: 0.875rem;
color: rgba(250, 247, 242, 0.6); color: rgba(var(--text-inverse-rgb), 0.6);
} }
.disclaimer .link { .disclaimer .link {
color: var(--accent-coral, #e07256); color: var(--brand);
} }
.disclaimer .link:hover { .disclaimer .link:hover {
color: var(--accent-gold, #c9a227); color: var(--status-below);
} }
@media (max-width: 768px) { @media (max-width: 768px) {
+163 -154
View File
@@ -16,8 +16,8 @@
font-weight: 600; font-weight: 600;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
color: var(--accent-teal, #2d7d7d); color: var(--brand);
background: rgba(45, 125, 125, 0.1); background: rgba(var(--brand-rgb), 0.1);
padding: 0.3rem 0.7rem; padding: 0.3rem 0.7rem;
border-radius: 999px; border-radius: 999px;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -28,37 +28,37 @@
width: 6px; width: 6px;
height: 6px; height: 6px;
border-radius: 50%; border-radius: 50%;
background: var(--accent-teal, #2d7d7d); background: var(--brand);
} }
.heroTitle { .heroTitle {
font-size: 3rem; font-size: 3rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin-bottom: 0.85rem; margin-bottom: 0.85rem;
line-height: 1.08; line-height: 1.08;
letter-spacing: -0.015em; letter-spacing: -0.015em;
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
max-width: 840px; max-width: 840px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.heroEmph { .heroEmph {
color: var(--accent-coral-dark, #c45a3f); color: var(--brand-strong);
font-style: italic; font-style: italic;
} }
.heroDescription { .heroDescription {
font-size: 1.05rem; font-size: 1.05rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
margin: 0 auto 0.5rem; margin: 0 auto 0.5rem;
max-width: 680px; max-width: 680px;
line-height: 1.55; line-height: 1.55;
} }
.heroDescription strong { .heroDescription strong {
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-weight: 700; font-weight: 700;
} }
@@ -111,7 +111,7 @@
.viewToggle { .viewToggle {
display: flex; display: flex;
gap: 0.2rem; gap: 0.2rem;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
padding: 0.2rem; padding: 0.2rem;
border-radius: 6px; border-radius: 6px;
} }
@@ -127,18 +127,18 @@
border: none; border: none;
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.viewToggleBtn:hover { .viewToggleBtn:hover {
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
.viewToggleBtn.active { .viewToggleBtn.active {
background: var(--bg-card, white); background: var(--bg-card);
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
box-shadow: 0 2px 4px rgba(26, 22, 18, 0.08); box-shadow: 0 2px 4px rgba(var(--shadow-rgb), 0.08);
} }
.viewToggleBtn svg { .viewToggleBtn svg {
@@ -166,7 +166,7 @@
.mapContainer { .mapContainer {
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
height: 100%; height: 100%;
} }
@@ -184,17 +184,17 @@
} }
.compactList::-webkit-scrollbar-track { .compactList::-webkit-scrollbar-track {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-radius: 3px; border-radius: 3px;
} }
.compactList::-webkit-scrollbar-thumb { .compactList::-webkit-scrollbar-thumb {
background: var(--border-color, #e5dfd5); background: var(--border);
border-radius: 3px; border-radius: 3px;
} }
.compactList::-webkit-scrollbar-thumb:hover { .compactList::-webkit-scrollbar-thumb:hover {
background: var(--text-muted, #8a847a); background: var(--text-muted);
} }
/* Compact School Item */ /* Compact School Item */
@@ -204,15 +204,15 @@
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
padding: 0.625rem 0.75rem; padding: 0.625rem 0.75rem;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.compactItem:hover { .compactItem:hover {
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
box-shadow: 0 2px 6px rgba(26, 22, 18, 0.05); box-shadow: 0 2px 6px rgba(var(--shadow-rgb), 0.05);
} }
.compactItemContent { .compactItemContent {
@@ -230,7 +230,7 @@
.compactItemName { .compactItemName {
font-weight: 600; font-weight: 600;
font-size: 0.8125rem; font-size: 0.8125rem;
color: var(--text-primary, #1a1612); color: var(--text-primary);
text-decoration: none; text-decoration: none;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -238,7 +238,7 @@
} }
.compactItemName:hover { .compactItemName:hover {
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
} }
.distanceBadge { .distanceBadge {
@@ -246,8 +246,8 @@
padding: 0.0625rem 0.375rem; padding: 0.0625rem 0.375rem;
font-size: 0.6875rem; font-size: 0.6875rem;
font-weight: 600; font-weight: 600;
background: var(--accent-teal, #2d7d7d); background: var(--brand);
color: white; color: var(--brand-on);
border-radius: 3px; border-radius: 3px;
} }
@@ -255,25 +255,25 @@
display: flex; display: flex;
gap: 0.375rem; gap: 0.375rem;
font-size: 0.6875rem; font-size: 0.6875rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
} }
.compactItemMeta span:not(:last-child)::after { .compactItemMeta span:not(:last-child)::after {
content: '·'; content: '·';
margin-left: 0.375rem; margin-left: 0.375rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
} }
.compactItemStats { .compactItemStats {
display: flex; display: flex;
gap: 0.75rem; gap: 0.75rem;
font-size: 0.6875rem; font-size: 0.6875rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
} }
.compactStat strong { .compactStat strong {
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
.compactItemActions { .compactItemActions {
@@ -292,8 +292,8 @@
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
} }
/* Decorative coral bar under section headings */ /* Decorative coral bar under section headings */
@@ -302,14 +302,14 @@
display: block; display: block;
width: 50px; width: 50px;
height: 2px; height: 2px;
background: var(--accent-coral, #e07256); background: var(--brand);
border-radius: 1px; border-radius: 1px;
margin-top: 0.5rem; margin-top: 0.5rem;
} }
.sectionDescription { .sectionDescription {
font-size: 0.9375rem; font-size: 0.9375rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
margin: 0; margin: 0;
line-height: 1.5; line-height: 1.5;
} }
@@ -336,22 +336,22 @@
.emptyState { .emptyState {
text-align: center; text-align: center;
padding: 2.5rem 1.5rem; padding: 2.5rem 1.5rem;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 10px; border-radius: 10px;
} }
.emptyStateTitle { .emptyStateTitle {
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin-bottom: 0.375rem; margin-bottom: 0.375rem;
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
} }
.emptyStateDescription { .emptyStateDescription {
font-size: 0.9375rem; font-size: 0.9375rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
max-width: 380px; max-width: 380px;
margin: 0 auto; margin: 0 auto;
} }
@@ -419,9 +419,9 @@
/* Highlighted List Item */ /* Highlighted List Item */
.highlightedItem .compactItem { .highlightedItem .compactItem {
border-color: var(--accent-teal, #2d7d7d); border-color: var(--brand);
box-shadow: 0 0 0 1px var(--accent-teal, #2d7d7d); box-shadow: 0 0 0 1px var(--brand);
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
/* Mobile Bottom Sheet */ /* Mobile Bottom Sheet */
@@ -443,9 +443,9 @@
.bottomSheet { .bottomSheet {
position: relative; position: relative;
background: var(--bg-card, white); background: var(--bg-card);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 -4px 24px rgba(26, 22, 18, 0.15); box-shadow: 0 -4px 24px rgba(var(--shadow-rgb), 0.15);
pointer-events: auto; pointer-events: auto;
animation: slideUpSheet 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; animation: slideUpSheet 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
} }
@@ -467,16 +467,16 @@
right: -12px; right: -12px;
width: 30px; width: 30px;
height: 30px; height: 30px;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1.25rem; font-size: 1.25rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
cursor: pointer; cursor: pointer;
box-shadow: 0 2px 8px rgba(26, 22, 18, 0.1); box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.1);
z-index: 10; z-index: 10;
} }
@@ -522,7 +522,7 @@
.quickSearchChip { .quickSearchChip {
padding: 0.375rem 0.875rem; padding: 0.375rem 0.875rem;
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border-color, #e0ddd8); border: 1px solid var(--border);
border-radius: 999px; border-radius: 999px;
font-size: 0.875rem; font-size: 0.875rem;
color: var(--text-secondary); color: var(--text-secondary);
@@ -531,9 +531,9 @@
} }
.quickSearchChip:hover { .quickSearchChip:hover {
background: var(--accent-coral-dark); background: var(--brand-strong);
color: white; color: var(--brand-on);
border-color: var(--accent-coral-dark); border-color: var(--brand-strong);
} }
.exploringRow { .exploringRow {
@@ -546,7 +546,7 @@
.exploringLabel { .exploringLabel {
font-size: 0.72rem; font-size: 0.72rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
font-weight: 500; font-weight: 500;
letter-spacing: 0.04em; letter-spacing: 0.04em;
text-transform: uppercase; text-transform: uppercase;
@@ -564,19 +564,19 @@
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
padding: 0.5rem 0.95rem; padding: 0.5rem 0.95rem;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 999px; border-radius: 999px;
font-size: 0.82rem; font-size: 0.82rem;
font-weight: 500; font-weight: 500;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
text-decoration: none; text-decoration: none;
transition: all 0.15s ease; transition: all 0.15s ease;
} }
.exploringChip:hover { .exploringChip:hover {
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
color: var(--accent-coral-dark, #c45a3f); color: var(--brand-strong);
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -606,16 +606,16 @@
} }
.hiwHeading { .hiwHeading {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-size: 1.75rem; font-size: 1.75rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin: 0; margin: 0;
} }
.hiwSub { .hiwSub {
font-size: 0.875rem; font-size: 0.875rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
} }
.hiwGrid { .hiwGrid {
@@ -625,18 +625,18 @@
} }
.hiwCard { .hiwCard {
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 14px; border-radius: 14px;
padding: 1.25rem; padding: 1.25rem;
box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06); box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.06);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.9rem; gap: 0.9rem;
} }
.hiwVisual { .hiwVisual {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-radius: 10px; border-radius: 10px;
padding: 0.9rem; padding: 0.9rem;
min-height: 180px; min-height: 180px;
@@ -657,11 +657,11 @@
font-weight: 600; font-weight: 600;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-muted, #6d685f); color: var(--text-muted);
} }
.hiwPhaseLabel strong { .hiwPhaseLabel strong {
color: var(--accent-teal, #2d7d7d); color: var(--brand);
font-weight: 700; font-weight: 700;
} }
@@ -676,20 +676,20 @@
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
} }
.hiwTitle { .hiwTitle {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
line-height: 1.25; line-height: 1.25;
} }
.hiwDesc { .hiwDesc {
font-size: 0.86rem; font-size: 0.86rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
line-height: 1.45; line-height: 1.45;
margin: 0; margin: 0;
} }
@@ -713,7 +713,7 @@
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-muted, #6d685f); color: var(--text-muted);
margin-bottom: 0.1rem; margin-bottom: 0.1rem;
} }
@@ -723,16 +723,16 @@
align-items: baseline; align-items: baseline;
gap: 0.2rem; gap: 0.2rem;
font-size: 0.44rem; font-size: 0.44rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.01em; letter-spacing: 0.01em;
white-space: nowrap; white-space: nowrap;
} }
.miniRowHead strong { .miniRowHead strong {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-size: 0.62rem; font-size: 0.62rem;
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-weight: 700; font-weight: 700;
letter-spacing: 0; letter-spacing: 0;
text-transform: none; text-transform: none;
@@ -741,7 +741,7 @@
.miniTrack { .miniTrack {
height: 5px; height: 5px;
border-radius: 2px; border-radius: 2px;
background: rgba(45, 125, 125, 0.08); background: rgba(var(--status-above-rgb), 0.08);
position: relative; position: relative;
overflow: visible; overflow: visible;
} }
@@ -749,21 +749,21 @@
.miniBarExp { .miniBarExp {
height: 100%; height: 100%;
border-radius: 2px; border-radius: 2px;
background: var(--accent-teal-light, #3a9e9e); background: var(--brand);
} }
.miniBarExc { .miniBarExc {
height: 100%; height: 100%;
border-radius: 2px; border-radius: 2px;
background: var(--accent-teal, #2d7d7d); background: var(--brand);
} }
.miniNatPill { .miniNatPill {
position: absolute; position: absolute;
top: -9px; top: -9px;
transform: translateX(-50%); transform: translateX(-50%);
background: var(--accent-coral, #e07256); background: var(--brand);
color: #fff; color: var(--text-inverse);
font-size: 0.4rem; font-size: 0.4rem;
font-weight: 700; font-weight: 700;
padding: 0.05rem 0.2rem; padding: 0.05rem 0.2rem;
@@ -791,21 +791,21 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
font-size: 0.5rem; font-size: 0.5rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
.att8Track { .att8Track {
height: 7px; height: 7px;
background: rgba(45, 125, 125, 0.08); background: rgba(var(--status-above-rgb), 0.08);
border-radius: 3px; border-radius: 3px;
position: relative; position: relative;
} }
.att8Fill { .att8Fill {
height: 100%; height: 100%;
background: var(--accent-teal, #2d7d7d); background: var(--brand);
border-radius: 3px; border-radius: 3px;
} }
@@ -814,25 +814,28 @@
top: -2px; top: -2px;
bottom: -2px; bottom: -2px;
width: 1.5px; width: 1.5px;
background: rgba(224, 114, 86, 0.6); background: rgba(var(--brand-rgb), 0.6);
z-index: 2; z-index: 2;
} }
.att8Score { .att8Score {
font-variant-numeric: tabular-nums;
text-align: right; text-align: right;
} }
.att8Value { .att8Value {
font-family: var(--font-playfair), 'Playfair Display', serif; font-variant-numeric: tabular-nums;
font-family: var(--font-display);
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
line-height: 1; line-height: 1;
} }
.att8Delta { .att8Delta {
font-variant-numeric: tabular-nums;
font-size: 0.55rem; font-size: 0.55rem;
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.04em; letter-spacing: 0.04em;
@@ -853,31 +856,31 @@
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
padding-bottom: 0.4rem; padding-bottom: 0.4rem;
border-bottom: 1.5px solid var(--border-color, #e5dfd5); border-bottom: 1.5px solid var(--border);
} }
.ofstedBullet { .ofstedBullet {
display: block; display: block;
width: 3px; width: 3px;
height: 1em; height: 1em;
background: var(--accent-coral, #e07256); background: var(--brand);
border-radius: 2px; border-radius: 2px;
flex-shrink: 0; flex-shrink: 0;
} }
.ofstedTitle { .ofstedTitle {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
.ofstedFramework { .ofstedFramework {
align-self: flex-start; align-self: flex-start;
padding: 0.15rem 0.5rem; padding: 0.15rem 0.5rem;
border-radius: 4px; border-radius: 4px;
background: rgba(90, 58, 110, 0.12); background: rgba(var(--phase-secondary-rgb), 0.12);
color: #5a3a6e; color: var(--phase-secondary-text);
font-size: 0.5rem; font-size: 0.5rem;
font-weight: 700; font-weight: 700;
letter-spacing: 0.05em; letter-spacing: 0.05em;
@@ -899,7 +902,7 @@
.rcArea { .rcArea {
font-size: 0.62rem; font-size: 0.62rem;
color: var(--text-secondary, #4a453d); color: var(--text-secondary);
} }
.rcChip { .rcChip {
@@ -913,13 +916,13 @@
} }
.rcChipGood { .rcChipGood {
background: rgba(45, 125, 125, 0.12); background: rgba(var(--status-above-rgb), 0.12);
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
} }
.rcChipNeutral { .rcChipNeutral {
background: var(--accent-gold-bg, rgba(180, 150, 40, 0.12)); background: var(--status-below-bg);
color: var(--accent-gold-text, #7a6800); color: var(--status-below);
} }
/* Safeguarding is a separate binary met/not-met judgement, not a graded area, /* Safeguarding is a separate binary met/not-met judgement, not a graded area,
@@ -931,30 +934,30 @@
gap: 0.5rem; gap: 0.5rem;
margin-top: 0.05rem; margin-top: 0.05rem;
padding-top: 0.35rem; padding-top: 0.35rem;
border-top: 1px dashed var(--border-color, #e5dfd5); border-top: 1px dashed var(--border);
} }
.rcSep { .rcSep {
color: var(--text-muted, #8a847a); color: var(--text-muted);
font-style: italic; font-style: italic;
} }
.rcChipMet { .rcChipMet {
background: transparent; background: transparent;
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
border: 1px solid rgba(45, 125, 125, 0.4); border: 1px solid rgba(var(--status-above-rgb), 0.4);
} }
.ofstedMeta { .ofstedMeta {
font-size: 0.6rem; font-size: 0.6rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
} }
/* Compare preview */ /* Compare preview */
.comparePreview { .comparePreview {
width: 100%; width: 100%;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
font-size: 0.6rem; font-size: 0.6rem;
@@ -963,23 +966,23 @@
.compareHead { .compareHead {
display: grid; display: grid;
grid-template-columns: 1fr repeat(2, 1fr); grid-template-columns: 1fr repeat(2, 1fr);
background: rgba(45, 125, 125, 0.1); background: rgba(var(--status-above-rgb), 0.1);
padding: 0.35rem 0.5rem; padding: 0.35rem 0.5rem;
gap: 0.35rem; gap: 0.35rem;
} }
.compareHeadCell { .compareHeadCell {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-size: 0.62rem; font-size: 0.62rem;
font-weight: 700; font-weight: 700;
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
line-height: 1.2; line-height: 1.2;
} }
.compareHeadLabel { .compareHeadLabel {
font-family: inherit; font-family: inherit;
font-size: 0.48rem; font-size: 0.48rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
font-weight: 500; font-weight: 500;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
@@ -990,37 +993,37 @@
grid-template-columns: 1fr repeat(2, 1fr); grid-template-columns: 1fr repeat(2, 1fr);
padding: 0.3rem 0.5rem; padding: 0.3rem 0.5rem;
gap: 0.35rem; gap: 0.35rem;
border-top: 1px solid var(--border-color, #e5dfd5); border-top: 1px solid var(--border);
align-items: baseline; align-items: baseline;
} }
.compareRowLabel { .compareRowLabel {
font-size: 0.55rem; font-size: 0.55rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
.compareRowVal { .compareRowVal {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-size: 0.72rem; font-size: 0.72rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
.compareRowValHi { .compareRowValHi {
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
} }
.compareFoot { .compareFoot {
font-size: 0.52rem; font-size: 0.52rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
padding: 0.35rem 0.5rem; padding: 0.35rem 0.5rem;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.06em; letter-spacing: 0.06em;
text-align: center; text-align: center;
border-top: 1px solid var(--border-color, #e5dfd5); border-top: 1px solid var(--border);
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -1065,8 +1068,8 @@
grid-template-columns: 1.4fr 1fr; grid-template-columns: 1.4fr 1fr;
gap: 2rem; gap: 2rem;
padding: 1.75rem; padding: 1.75rem;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 14px; border-radius: 14px;
} }
@@ -1078,30 +1081,34 @@
.editorialKicker { .editorialKicker {
font-size: 0.68rem; font-size: 0.68rem;
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
font-weight: 700; font-weight: 700;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
} }
.editorialHeading { .editorialHeading {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-size: 1.35rem; font-size: 1.35rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin: 0; margin: 0;
line-height: 1.25; line-height: 1.25;
} }
/* Running prose is the one place the serif appears. Slightly larger than the
UI around it because Literata's x-height sits lower than the grotesque's. */
.editorialText p { .editorialText p {
font-size: 0.92rem; font-family: var(--font-prose);
color: var(--text-secondary, #5c564d); font-size: 0.97rem;
line-height: 1.6; color: var(--text-secondary);
line-height: 1.65;
margin: 0; margin: 0;
max-width: 62ch;
} }
.factbox { .factbox {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-radius: 10px; border-radius: 10px;
padding: 1.25rem; padding: 1.25rem;
display: flex; display: flex;
@@ -1110,10 +1117,10 @@
} }
.factboxHeading { .factboxHeading {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin: 0 0 0.85rem; margin: 0 0 0.85rem;
} }
@@ -1122,7 +1129,7 @@
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
padding: 0.5rem 0; padding: 0.5rem 0;
border-bottom: 1px solid var(--border-color, #e5dfd5); border-bottom: 1px solid var(--border);
font-size: 0.85rem; font-size: 0.85rem;
gap: 0.5rem; gap: 0.5rem;
} }
@@ -1132,13 +1139,13 @@
} }
.factKey { .factKey {
color: var(--text-muted, #6d685f); color: var(--text-muted);
} }
.factVal { .factVal {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
text-align: right; text-align: right;
} }
@@ -1168,7 +1175,7 @@
.sortSelect { .sortSelect {
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
border: 1px solid var(--border-color, #e0ddd8); border: 1px solid var(--border);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: var(--bg-card); background: var(--bg-card);
color: var(--text-secondary); color: var(--text-secondary);
@@ -1189,7 +1196,7 @@
gap: 0.375rem; gap: 0.375rem;
padding: 0.25rem 0.625rem; padding: 0.25rem 0.625rem;
background: var(--bg-secondary); background: var(--bg-secondary);
border: 1px solid var(--border-color, #e0ddd8); border: 1px solid var(--border);
border-radius: 999px; border-radius: 999px;
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-secondary); color: var(--text-secondary);
@@ -1216,8 +1223,9 @@
} }
.loadMoreCount { .loadMoreCount {
font-variant-numeric: tabular-nums;
font-size: 0.875rem; font-size: 0.875rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
margin: 0; margin: 0;
} }
@@ -1231,7 +1239,7 @@
.admissionsStrip { .admissionsStrip {
padding: 1.5rem 0 2rem; padding: 1.5rem 0 2rem;
border-top: 1px solid var(--border-color, #e5dfd5); border-top: 1px solid var(--border);
} }
.stripHeader { .stripHeader {
@@ -1248,12 +1256,12 @@
font-weight: 700; font-weight: 700;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-muted, #6d685f); color: var(--text-muted);
} }
.stripCta { .stripCta {
font-size: 0.82rem; font-size: 0.82rem;
color: var(--accent-teal, #2d7d7d); color: var(--brand);
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
} }
@@ -1269,11 +1277,11 @@
} }
.countdownChip { .countdownChip {
background: var(--bg-card, #fff); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;
padding: 1rem 1.1rem 0.9rem; padding: 1rem 1.1rem 0.9rem;
box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06); box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.06);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.2rem; gap: 0.2rem;
@@ -1292,16 +1300,16 @@
} }
.countdownChipDeadline::before { .countdownChipDeadline::before {
background: var(--accent-coral, #e07256); background: var(--brand);
} }
.countdownChipOffer::before { .countdownChipOffer::before {
background: var(--accent-teal, #2d7d7d); background: var(--status-above);
} }
.countdownChipUrgent { .countdownChipUrgent {
border-color: rgba(224, 114, 86, 0.4); border-color: rgba(var(--status-below-rgb), 0.4);
background: rgba(224, 114, 86, 0.04); background: rgba(var(--status-below-rgb), 0.04);
} }
.chipTrack { .chipTrack {
@@ -1316,11 +1324,11 @@
} }
.chipTrackDeadline { .chipTrackDeadline {
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
} }
.chipTrackOffer { .chipTrackOffer {
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
} }
.chipTrackDot { .chipTrackDot {
@@ -1332,7 +1340,8 @@
} }
.chipDays { .chipDays {
font-family: var(--font-playfair), 'Playfair Display', serif; font-variant-numeric: tabular-nums;
font-family: var(--font-display);
font-size: 2.6rem; font-size: 2.6rem;
font-weight: 700; font-weight: 700;
line-height: 1; line-height: 1;
@@ -1341,18 +1350,18 @@
.countdownChipDeadline .chipDays, .countdownChipDeadline .chipDays,
.countdownChipUrgent .chipDays { .countdownChipUrgent .chipDays {
color: var(--accent-coral-dark, #c45a3f); color: var(--status-below);
} }
.countdownChipOffer .chipDays { .countdownChipOffer .chipDays {
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
} }
.chipDaysUnit { .chipDaysUnit {
font-family: 'DM Sans', sans-serif; font-family: var(--font-ui);
font-size: 0.78rem; font-size: 0.78rem;
font-weight: 500; font-weight: 500;
color: var(--text-muted, #6d685f); color: var(--text-muted);
margin-left: 0.2rem; margin-left: 0.2rem;
vertical-align: bottom; vertical-align: bottom;
line-height: 2; line-height: 2;
@@ -1361,14 +1370,14 @@
.chipMilestone { .chipMilestone {
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
line-height: 1.25; line-height: 1.25;
margin-top: 0.1rem; margin-top: 0.1rem;
} }
.chipDate { .chipDate {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-muted, #6d685f); color: var(--text-muted);
margin-top: 0.05rem; margin-top: 0.05rem;
} }
+10 -10
View File
@@ -573,10 +573,10 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare, nationalAvgRwm
rwmDelta == null rwmDelta == null
? {} ? {}
: rwmDelta >= 2 : rwmDelta >= 2
? { fontSize: '0.7rem', color: 'var(--accent-teal, #2d7d7d)', fontWeight: 600 } ? { fontSize: '0.7rem', color: 'var(--status-above)', fontWeight: 600 }
: rwmDelta <= -2 : rwmDelta <= -2
? { fontSize: '0.7rem', color: 'var(--accent-coral, #e07256)', fontWeight: 600 } ? { fontSize: '0.7rem', color: 'var(--brand)', fontWeight: 600 }
: { fontSize: '0.7rem', color: 'var(--text-muted, #8a847a)' }; : { fontSize: '0.7rem', color: 'var(--text-muted)' };
return ( return (
<div className={styles.compactItem}> <div className={styles.compactItem}>
@@ -601,13 +601,13 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare, nationalAvgRwm
fontWeight: 600, fontWeight: 600,
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(--status-above-bg)', color: 'var(--status-above)' } :
ofstedBadge.cssClass === 'ofsted2' ? { background: 'rgba(60,140,60,0.12)', color: '#2f7a2f' } : ofstedBadge.cssClass === 'ofsted2' ? { background: 'var(--status-above-bg)', color: 'var(--status-above)' } :
ofstedBadge.cssClass === 'ofsted3' ? { background: 'var(--accent-gold-bg)', color: 'var(--accent-gold-text, #7a6800)' } : ofstedBadge.cssClass === 'ofsted3' ? { background: 'var(--status-below-bg)', color: 'var(--status-below)' } :
ofstedBadge.cssClass === 'ofsted4' ? { background: 'var(--accent-coral-bg)', color: 'var(--accent-coral, #e07256)' } : ofstedBadge.cssClass === 'ofsted4' ? { background: 'var(--status-below)', color: 'var(--text-inverse)' } :
ofstedBadge.cssClass === 'ofstedRc' ? { background: '#5a3a6e', color: '#fff' } : ofstedBadge.cssClass === 'ofstedRc' ? { background: 'var(--phase-secondary-text)', color: 'var(--text-inverse)' } :
ofstedBadge.cssClass === 'ofstedPending' ? { background: '#e0e0e0', color: '#666' } : ofstedBadge.cssClass === 'ofstedPending' ? { background: 'var(--bg-secondary)', color: 'var(--text-muted)' } :
{ background: '#e0e0e0', color: '#666' }), { background: 'var(--bg-secondary)', color: 'var(--text-muted)' }),
}} }}
> >
{ofstedBadge.label} {ofstedBadge.label}
+10 -10
View File
@@ -11,7 +11,7 @@
/* font-size:0 hides the button's own "?" text node; the ::before glyph /* font-size:0 hides the button's own "?" text node; the ::before glyph
below carries the visible circled "?" at its own explicit size. */ below carries the visible circled "?" at its own explicit size. */
font-size: 0; font-size: 0;
color: var(--text-muted, #8a7a72); color: var(--text-muted);
cursor: help; cursor: help;
line-height: 1; line-height: 1;
user-select: none; user-select: none;
@@ -34,17 +34,17 @@
.icon:hover, .icon:hover,
.icon[aria-expanded='true'], .icon[aria-expanded='true'],
.icon:focus-visible { .icon:focus-visible {
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
} }
.tooltip { .tooltip {
z-index: 9999; z-index: 9999;
width: max-content; width: max-content;
max-width: min(260px, calc(100vw - 24px)); max-width: min(260px, calc(100vw - 24px));
background: var(--bg-primary, #faf7f2); background: var(--bg-primary);
border: 1px solid var(--border-color, #e8ddd4); border: 1px solid var(--border);
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 16px rgba(44, 36, 32, 0.15); box-shadow: 0 4px 16px rgba(var(--shadow-rgb), 0.15);
padding: 0.6rem 0.75rem; padding: 0.6rem 0.75rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -52,26 +52,26 @@
} }
.arrow { .arrow {
fill: var(--bg-primary, #faf7f2); fill: var(--bg-primary);
stroke: var(--border-color, #e8ddd4); stroke: var(--border);
stroke-width: 1px; stroke-width: 1px;
} }
.label { .label {
font-weight: 600; font-weight: 600;
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-primary, #2c2420); color: var(--text-primary);
} }
.plain { .plain {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-secondary, #5a4a44); color: var(--text-secondary);
line-height: 1.4; line-height: 1.4;
} }
.detail { .detail {
font-size: 0.7rem; font-size: 0.7rem;
color: var(--text-muted, #8a7a72); color: var(--text-muted);
line-height: 1.4; line-height: 1.4;
margin-top: 0.1rem; margin-top: 0.1rem;
} }
@@ -22,10 +22,10 @@ interface LeafletHeroMapInnerProps {
// reliably inside Leaflet's divIcon HTML string (no CSS-module dependency). // reliably inside Leaflet's divIcon HTML string (no CSS-module dependency).
const PIN_HTML = ` const PIN_HTML = `
<div style="position:relative;width:26px;height:26px"> <div style="position:relative;width:26px;height:26px">
<span style="position:absolute;left:50%;top:74%;width:32px;height:32px;transform:translate(-50%,-50%);border-radius:50%;background:rgba(224,114,86,.25)"></span> <span style="position:absolute;left:50%;top:74%;width:32px;height:32px;transform:translate(-50%,-50%);border-radius:50%;background:var(--brand-bg)"></span>
<svg width="26" height="34" viewBox="0 0 26 34" style="filter:drop-shadow(0 3px 4px rgba(0,0,0,.35))"> <svg width="26" height="34" viewBox="0 0 26 34" style="filter:drop-shadow(0 3px 4px rgba(0,0,0,.35))">
<path d="M13 0C5.8 0 0 5.8 0 13c0 9.2 13 21 13 21s13-11.8 13-21C26 5.8 20.2 0 13 0Z" fill="#e07256"/> <path d="M13 0C5.8 0 0 5.8 0 13c0 9.2 13 21 13 21s13-11.8 13-21C26 5.8 20.2 0 13 0Z" fill="var(--brand)"/>
<circle cx="13" cy="13" r="5" fill="#fff"/> <circle cx="13" cy="13" r="5" fill="var(--text-inverse)"/>
</svg> </svg>
</div>`; </div>`;
+16 -16
View File
@@ -48,21 +48,21 @@ function buildPopupBadge(school: School): PopupBadge {
// ofsted_framework is the raw event grouping, never "ReportCard"). // ofsted_framework is the raw event grouping, never "ReportCard").
if (school.ofsted_rc_date) { if (school.ofsted_rc_date) {
const rcYear = new Date(school.ofsted_rc_date).getFullYear(); const rcYear = new Date(school.ofsted_rc_date).getFullYear();
return { label: `Report Card · ${rcYear}`, style: 'background:#5a3a6e;color:#fff' }; return { label: `Report Card · ${rcYear}`, style: 'background:var(--phase-secondary-text);color:var(--text-inverse)' };
} }
const year = school.ofsted_date ? new Date(school.ofsted_date).getFullYear() : null; const year = school.ofsted_date ? new Date(school.ofsted_date).getFullYear() : null;
const yearStr = year ? ` · ${year}` : ''; const yearStr = year ? ` · ${year}` : '';
if (school.ofsted_grade) { if (school.ofsted_grade) {
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:var(--status-above-bg);color:var(--status-above)',
2: 'background:rgba(60,140,60,0.12);color:#2f7a2f', 2: 'background:var(--status-above-bg);color:var(--status-above)',
3: 'background:#fef3cd;color:#7a6800', 3: 'background:var(--status-below-bg);color:var(--status-below)',
4: 'background:#fde8e0;color:#e07256', 4: 'background:var(--status-below);color:var(--text-inverse)',
}; };
return { label: `${labels[school.ofsted_grade]}${yearStr}`, style: colours[school.ofsted_grade] }; return { label: `${labels[school.ofsted_grade]}${yearStr}`, style: colours[school.ofsted_grade] };
} }
return { label: 'Not yet inspected', style: 'background:#e0e0e0;color:#666' }; return { label: 'Not yet inspected', style: 'background:var(--bg-secondary);color:var(--text-muted)' };
} }
export default function LeafletMapInner({ schools, center, zoom, referencePoint, onMarkerClick, nationalAvgRwm, laAverages }: LeafletMapInnerProps) { export default function LeafletMapInner({ schools, center, zoom, referencePoint, onMarkerClick, nationalAvgRwm, laAverages }: LeafletMapInnerProps) {
@@ -99,7 +99,7 @@ export default function LeafletMapInner({ schools, center, zoom, referencePoint,
const refIcon = L.divIcon({ const refIcon = L.divIcon({
html: `<div style=" html: `<div style="
width: 20px; height: 20px; width: 20px; height: 20px;
background: #e07256; background: var(--brand);
border: 3px solid white; border: 3px solid white;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 2px 8px rgba(0,0,0,0.35); box-shadow: 0 2px 8px rgba(0,0,0,0.35);
@@ -145,13 +145,13 @@ export default function LeafletMapInner({ schools, center, zoom, referencePoint,
const sign = diff >= 0 ? '+' : ''; const sign = diff >= 0 ? '+' : '';
// Att8 scores range 090 in 0.1 increments; ±0.5 is meaningful here // Att8 scores range 090 in 0.1 increments; ±0.5 is meaningful here
// vs primary RWM % where ±2 pts is the threshold // vs primary RWM % where ±2 pts is the threshold
const colour = diff >= 0.5 ? '#2d7d7d' : diff <= -0.5 ? '#e07256' : '#8a847a'; const colour = diff >= 0.5 ? 'var(--status-above)' : diff <= -0.5 ? 'var(--brand)' : 'var(--text-muted)';
const laName = escapeHtml(school.local_authority ?? 'LA'); const laName = escapeHtml(school.local_authority ?? 'LA');
deltaLine = `<div style="font-size:11px;font-weight:600;color:${colour}">${sign}${diff} vs ${laName} avg</div>`; deltaLine = `<div style="font-size:11px;font-weight:600;color:${colour}">${sign}${diff} vs ${laName} avg</div>`;
} }
metricHtml = `<div style="margin-bottom:4px"> metricHtml = `<div style="margin-bottom:4px">
<span style="font-size:20px;font-weight:700;color:#1a1612;font-family:Georgia,serif">${score.toFixed(1)}</span> <span style="font-size:20px;font-weight:700;color:var(--text-primary);font-family:Georgia,serif">${score.toFixed(1)}</span>
<span style="font-size:11px;color:#8a847a;margin-left:4px">Attainment 8</span> <span style="font-size:11px;color:var(--text-muted);margin-left:4px">Attainment 8</span>
${deltaLine} ${deltaLine}
</div>`; </div>`;
} else if (school.rwm_expected_pct != null) { } else if (school.rwm_expected_pct != null) {
@@ -159,7 +159,7 @@ export default function LeafletMapInner({ schools, center, zoom, referencePoint,
let deltaLine = ''; let deltaLine = '';
if (nationalAvgRwm != null) { if (nationalAvgRwm != null) {
const diff = Math.round(rwm - nationalAvgRwm); const diff = Math.round(rwm - nationalAvgRwm);
const colour = diff >= 2 ? '#2d7d7d' : diff <= -2 ? '#e07256' : '#8a847a'; const colour = diff >= 2 ? 'var(--status-above)' : diff <= -2 ? 'var(--brand)' : 'var(--text-muted)';
const text = const text =
diff >= 2 ? `+${diff} pts vs national` : diff >= 2 ? `+${diff} pts vs national` :
diff <= -2 ? `${diff} pts vs national` : diff <= -2 ? `${diff} pts vs national` :
@@ -167,8 +167,8 @@ export default function LeafletMapInner({ schools, center, zoom, referencePoint,
deltaLine = `<div style="font-size:11px;font-weight:600;color:${colour}">${text}</div>`; deltaLine = `<div style="font-size:11px;font-weight:600;color:${colour}">${text}</div>`;
} }
metricHtml = `<div style="margin-bottom:4px"> metricHtml = `<div style="margin-bottom:4px">
<span style="font-size:20px;font-weight:700;color:#1a1612;font-family:Georgia,serif">${rwm}%</span> <span style="font-size:20px;font-weight:700;color:var(--text-primary);font-family:Georgia,serif">${rwm}%</span>
<span style="font-size:11px;color:#8a847a;margin-left:4px">Reading, Writing &amp; Maths</span> <span style="font-size:11px;color:var(--text-muted);margin-left:4px">Reading, Writing &amp; Maths</span>
${deltaLine} ${deltaLine}
</div>`; </div>`;
} }
@@ -177,14 +177,14 @@ export default function LeafletMapInner({ schools, center, zoom, referencePoint,
const popupContent = `<div style="font-family:system-ui,sans-serif;min-width:240px;max-width:280px;padding:0"> const popupContent = `<div style="font-family:system-ui,sans-serif;min-width:240px;max-width:280px;padding:0">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:6px"> <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:6px">
<strong style="font-size:13px;color:#1a1612;line-height:1.3">${escapeHtml(school.school_name)}</strong> <strong style="font-size:13px;color:var(--text-primary);line-height:1.3">${escapeHtml(school.school_name)}</strong>
<span style="font-size:10px;font-weight:700;padding:2px 6px;border-radius:3px;white-space:nowrap;flex-shrink:0;${badge.style}">${badge.label}</span> <span style="font-size:10px;font-weight:700;padding:2px 6px;border-radius:3px;white-space:nowrap;flex-shrink:0;${badge.style}">${badge.label}</span>
</div> </div>
<div style="font-size:11px;color:#8a847a;margin-bottom:8px"> <div style="font-size:11px;color:var(--text-muted);margin-bottom:8px">
${phaseLabel}${school.local_authority ? ` · ${escapeHtml(school.local_authority)}` : ''}${distanceStr} ${phaseLabel}${school.local_authority ? ` · ${escapeHtml(school.local_authority)}` : ''}${distanceStr}
</div> </div>
${metricHtml} ${metricHtml}
<a href="${slug}" style="display:block;text-align:center;padding:6px;background:#2d7d7d;color:white;border-radius:5px;text-decoration:none;font-size:12px;font-weight:600;margin-top:8px">View Details →</a> <a href="${slug}" style="display:block;text-align:center;padding:6px;background:var(--status-above);color:white;border-radius:5px;text-decoration:none;font-size:12px;font-weight:600;margin-top:8px">View Details →</a>
</div>`; </div>`;
marker.bindPopup(popupContent); marker.bindPopup(popupContent);
@@ -5,19 +5,19 @@
} }
.skeletonCard { .skeletonCard {
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;
padding: 1.5rem; padding: 1.5rem;
box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.06)); box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.06));
} }
.skeleton { .skeleton {
background: linear-gradient( background: linear-gradient(
90deg, 90deg,
var(--bg-secondary, #f3ede4) 25%, var(--bg-secondary) 25%,
rgba(224, 114, 86, 0.08) 50%, rgba(var(--brand-rgb), 0.08) 50%,
var(--bg-secondary, #f3ede4) 75% var(--bg-secondary) 75%
); );
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite; animation: shimmer 1.5s ease-in-out infinite;
@@ -56,7 +56,7 @@
gap: 1rem; gap: 1rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
padding: 1rem; padding: 1rem;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-radius: 8px; border-radius: 8px;
} }
@@ -82,11 +82,11 @@
} }
.skeletonListItem { .skeletonListItem {
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;
padding: 1.5rem; padding: 1.5rem;
box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.06)); box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.06));
} }
.listTitle { .listTitle {
+65
View File
@@ -0,0 +1,65 @@
/**
* The SchoolCompare mark.
*
* Five bars: a cohort, with one school picked out. It is the same object as
* the distribution strip inside a school row, at a different size — the logo
* teaches the chart and the chart reinforces the logo.
*
* Built from opacity steps over `currentColor` rather than fixed greys, so it
* inverts cleanly on any ground without a second artwork. The highlighted bar
* is the only element that takes the brand hue.
*
* This is the single source for the mark. The favicon and the generated icons
* derive from the same geometry (see BARS) — previously the header and the
* favicon had drifted into two different logos.
*/
/** x, y, width, height, and whether this bar is the highlighted school. */
export const BARS: ReadonlyArray<[number, number, number, number, boolean]> = [
[2, 26, 6, 12, false],
[10, 16, 6, 22, false],
[18, 8, 6, 30, false],
[26, 14, 6, 24, true],
[34, 28, 4, 10, false],
];
const COHORT_OPACITY = [0.22, 0.38, 0.55, 1, 0.22];
interface LogoMarkProps {
className?: string;
/** Rendered size in px. Defaults to inheriting via CSS. */
size?: number;
/** Colour of the highlighted bar. Defaults to the brand token. */
accent?: string;
title?: string;
}
export function LogoMark({ className, size, accent = 'var(--brand)', title }: LogoMarkProps) {
return (
<svg
className={className}
width={size}
height={size}
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role={title ? 'img' : undefined}
aria-hidden={title ? undefined : true}
aria-label={title}
>
{title ? <title>{title}</title> : null}
{BARS.map(([x, y, w, h, isSchool], i) => (
<rect
key={x}
x={x}
y={y}
width={w}
height={h}
rx={1}
fill={isSchool ? accent : 'currentColor'}
opacity={isSchool ? 1 : COHORT_OPACITY[i]}
/>
))}
</svg>
);
}
+13 -13
View File
@@ -1,7 +1,7 @@
.overlay { .overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(26, 22, 18, 0.6); background: rgba(var(--shadow-rgb), 0.6);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -20,15 +20,15 @@
} }
.modal { .modal {
background: var(--bg-card, white); background: var(--bg-card);
border-radius: 16px; border-radius: 16px;
box-shadow: 0 20px 40px rgba(26, 22, 18, 0.2); box-shadow: 0 20px 40px rgba(var(--shadow-rgb), 0.2);
max-height: 90vh; max-height: 90vh;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
animation: slideIn 0.3s ease; animation: slideIn 0.3s ease;
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
} }
@keyframes slideIn { @keyframes slideIn {
@@ -62,22 +62,22 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 1.5rem; padding: 1.5rem;
border-bottom: 1px solid var(--border-color, #e5dfd5); border-bottom: 1px solid var(--border);
} }
.title { .title {
margin: 0; margin: 0;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
} }
.closeButton { .closeButton {
padding: 0.5rem; padding: 0.5rem;
background: transparent; background: transparent;
border: none; border: none;
color: var(--text-muted, #8a847a); color: var(--text-muted);
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 8px;
transition: all 0.2s ease; transition: all 0.2s ease;
@@ -87,8 +87,8 @@
} }
.closeButton:hover { .closeButton:hover {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
color: var(--accent-coral, #e07256); color: var(--brand);
} }
.content { .content {
@@ -102,16 +102,16 @@
} }
.content::-webkit-scrollbar-track { .content::-webkit-scrollbar-track {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
.content::-webkit-scrollbar-thumb { .content::-webkit-scrollbar-thumb {
background: var(--border-color, #e5dfd5); background: var(--border);
border-radius: 4px; border-radius: 4px;
} }
.content::-webkit-scrollbar-thumb:hover { .content::-webkit-scrollbar-thumb:hover {
background: var(--text-muted, #8a847a); background: var(--text-muted);
} }
@media (max-width: 640px) { @media (max-width: 640px) {
+38 -28
View File
@@ -2,9 +2,9 @@
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 1000; z-index: 1000;
background: var(--bg-card, white); background: var(--bg-card);
border-bottom: 1px solid var(--border-color, #e5dfd5); border-bottom: 1px solid var(--border);
box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06); box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.06);
} }
.container { .container {
@@ -27,7 +27,7 @@
margin: -0.375rem -0.5rem; margin: -0.375rem -0.5rem;
padding: 0.375rem 0.5rem; padding: 0.375rem 0.5rem;
text-decoration: none; text-decoration: none;
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 700; font-weight: 700;
transition: color 0.2s ease; transition: color 0.2s ease;
@@ -35,14 +35,16 @@
} }
.logo:hover { .logo:hover {
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
} }
/* currentColor paints the four cohort bars; the mark supplies the brand hue
for the highlighted one itself. So this is ink, not brand. */
.logoIcon { .logoIcon {
display: inline-flex; display: inline-flex;
width: 36px; width: 32px;
height: 36px; height: 32px;
color: var(--accent-coral, #e07256); color: var(--text-primary);
} }
.logoIcon svg { .logoIcon svg {
@@ -51,9 +53,17 @@
} }
.logoText { .logoText {
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
font-weight: 700; font-weight: 700;
letter-spacing: -0.01em; letter-spacing: -0.022em;
}
.logoTextAccent {
color: var(--brand);
}
.logo:hover .logoTextAccent {
color: var(--brand-strong);
} }
.nav { .nav {
@@ -69,7 +79,7 @@
padding: 0.625rem 1rem; padding: 0.625rem 1rem;
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 500; font-weight: 500;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
text-decoration: none; text-decoration: none;
border-radius: 6px; border-radius: 6px;
transition: all 0.2s ease; transition: all 0.2s ease;
@@ -83,7 +93,7 @@
left: 1rem; left: 1rem;
right: 1rem; right: 1rem;
height: 2px; height: 2px;
background: var(--accent-coral, #e07256); background: var(--brand);
transform: scaleX(0); transform: scaleX(0);
transform-origin: left; transform-origin: left;
transition: transform 0.25s ease; transition: transform 0.25s ease;
@@ -91,8 +101,8 @@
} }
.navLink:hover { .navLink:hover {
color: var(--text-primary, #1a1612); color: var(--text-primary);
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
.navLink:hover::after { .navLink:hover::after {
@@ -100,8 +110,8 @@
} }
.navLink.active { .navLink.active {
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
background: var(--accent-coral-bg); background: var(--brand-bg);
} }
.navLink.active::after { .navLink.active::after {
@@ -117,11 +127,11 @@
padding: 0 0.375rem; padding: 0 0.375rem;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
color: white; color: var(--brand-on);
background: var(--accent-coral, #e07256); background: var(--brand);
border-radius: 9999px; border-radius: 9999px;
animation: badgePop 0.3s ease-out; animation: badgePop 0.3s ease-out;
box-shadow: 0 2px 6px rgba(224, 114, 86, 0.4); box-shadow: 0 2px 6px rgba(var(--brand-rgb), 0.4);
} }
@keyframes badgePop { @keyframes badgePop {
@@ -153,7 +163,7 @@
flex: 1; flex: 1;
min-height: 56px; min-height: 56px;
padding: 0.375rem 0.25rem; padding: 0.375rem 0.25rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
text-decoration: none; text-decoration: none;
font-size: 0.6875rem; font-size: 0.6875rem;
font-weight: 500; font-weight: 500;
@@ -163,11 +173,11 @@
} }
.tab:active { .tab:active {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
.tabActive { .tabActive {
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
} }
.tabIconWrap { .tabIconWrap {
@@ -200,9 +210,9 @@
padding: 0 5px; padding: 0 5px;
font-size: 0.6875rem; font-size: 0.6875rem;
font-weight: 700; font-weight: 700;
color: white; color: var(--brand-on);
background: var(--accent-coral, #e07256); background: var(--brand);
border: 2px solid var(--bg-card, white); border: 2px solid var(--bg-card);
border-radius: 9999px; border-radius: 9999px;
animation: badgePop 0.3s ease-out; animation: badgePop 0.3s ease-out;
} }
@@ -230,9 +240,9 @@
left: 0; left: 0;
right: 0; right: 0;
z-index: 1000; z-index: 1000;
background: var(--bg-card, white); background: var(--bg-card);
border-top: 1px solid var(--border-color, #e5dfd5); border-top: 1px solid var(--border);
box-shadow: 0 -2px 12px rgba(26, 22, 18, 0.06); box-shadow: 0 -2px 12px rgba(var(--shadow-rgb), 0.06);
/* Respect iPhone home-indicator (bottom) and notch (left/right in /* Respect iPhone home-indicator (bottom) and notch (left/right in
landscape) insets so the tab content never sits under system UI. */ landscape) insets so the tab content never sits under system UI. */
padding-bottom: env(safe-area-inset-bottom, 0); padding-bottom: env(safe-area-inset-bottom, 0);
+5 -6
View File
@@ -9,6 +9,7 @@ import { useEffect } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useComparison } from '@/hooks/useComparison'; import { useComparison } from '@/hooks/useComparison';
import { LogoMark } from './Logo';
import styles from './Navigation.module.css'; import styles from './Navigation.module.css';
type IconProps = { className?: string }; type IconProps = { className?: string };
@@ -91,13 +92,11 @@ export function Navigation() {
<div className={styles.container}> <div className={styles.container}>
<Link href="/" className={styles.logo} aria-label="SchoolCompare home"> <Link href="/" className={styles.logo} aria-label="SchoolCompare home">
<span className={styles.logoIcon}> <span className={styles.logoIcon}>
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> <LogoMark />
<circle cx="20" cy="20" r="18" stroke="currentColor" strokeWidth="2" /> </span>
<path d="M20 6L20 34M8 14L32 14M6 20L34 20M8 26L32 26" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> <span className={styles.logoText}>
<circle cx="20" cy="20" r="3" fill="currentColor" /> School<span className={styles.logoTextAccent}>Compare</span>
</svg>
</span> </span>
<span className={styles.logoText}>SchoolCompare</span>
</Link> </Link>
<nav className={styles.nav} aria-label="Main navigation"> <nav className={styles.nav} aria-label="Main navigation">
+19 -19
View File
@@ -8,7 +8,7 @@
.info { .info {
font-size: 0.875rem; font-size: 0.875rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
} }
.controls { .controls {
@@ -21,18 +21,18 @@
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
background: var(--bg-card, white); background: var(--bg-card);
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.navButton:hover:not(:disabled) { .navButton:hover:not(:disabled) {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
.navButton:disabled { .navButton:disabled {
@@ -51,32 +51,32 @@
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 500; font-weight: 500;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.pageButton { .pageButton {
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
} }
.pageButton:hover { .pageButton:hover {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
.pageButtonActive { .pageButtonActive {
background: var(--accent-coral, #e07256); background: var(--brand);
color: white; color: var(--brand-on);
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
} }
.pageButtonActive:hover { .pageButtonActive:hover {
background: var(--accent-coral-dark, #c45a3f); background: var(--brand-strong);
border-color: var(--accent-coral-dark, #c45a3f); border-color: var(--brand-strong);
} }
.ellipsis { .ellipsis {
@@ -84,7 +84,7 @@
align-items: center; align-items: center;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
font-size: 0.875rem; font-size: 0.875rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
} }
@media (max-width: 640px) { @media (max-width: 640px) {
@@ -12,10 +12,10 @@
.trendSummary { .trendSummary {
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
padding: 0.5rem 0.875rem; padding: 0.5rem 0.875rem;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-left: 3px solid var(--accent-teal, #2d7d7d); border-left: 3px solid var(--brand);
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
align-self: flex-start; align-self: flex-start;
} }
@@ -34,7 +34,7 @@
.covidNote, .covidNote,
.chartHint { .chartHint {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
margin: 0; margin: 0;
font-style: italic; font-style: italic;
} }
@@ -52,7 +52,7 @@
font-weight: 700; font-weight: 700;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-muted, #6d685f); color: var(--text-muted);
margin-bottom: 0.4rem; margin-bottom: 0.4rem;
} }
@@ -63,9 +63,9 @@
} }
.chip { .chip {
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
background: var(--bg-card, #fff); background: var(--bg-card);
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
padding: 0.4rem 0.75rem; padding: 0.4rem 0.75rem;
border-radius: 999px; border-radius: 999px;
font-size: 0.8125rem; font-size: 0.8125rem;
@@ -78,7 +78,7 @@
} }
.chip:active { .chip:active {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
.chip:disabled { .chip:disabled {
@@ -87,21 +87,21 @@
} }
.chipActive { .chipActive {
background: var(--accent-coral-dark, #b04a2e); background: var(--brand-strong);
color: white; color: var(--brand-on);
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
font-weight: 600; font-weight: 600;
} }
.chipActive:active { .chipActive:active {
background: var(--accent-coral-dark, #c45a3f); background: var(--brand-strong);
} }
.miniLegend { .miniLegend {
display: flex; display: flex;
gap: 0.875rem; gap: 0.875rem;
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
margin-top: -0.25rem; margin-top: -0.25rem;
} }
+37 -24
View File
@@ -11,6 +11,7 @@
'use client'; 'use client';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useThemeTokens, alpha } from '@/lib/theme';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import { ChartOptions, ChartDataset } from 'chart.js'; import { ChartOptions, ChartDataset } from 'chart.js';
import '@/lib/chartSetup'; import '@/lib/chartSetup';
@@ -124,9 +125,17 @@ export function PerformanceChart({
const gapYears = axisYears.filter(y => !byYear.has(y)); const gapYears = axisYears.filter(y => !byYear.has(y));
const hasGap = gapYears.length > 0; const hasGap = gapYears.length > 0;
// Chart.js paints to a canvas, so it needs resolved colours rather than
// var(). Reading them through the token bridge keeps the series on the
// brand palette and lets them follow the theme.
const [c1, c2, c3, c4, c5, c6, cGrid, cRef, cInverse, cInverseText] = useThemeTokens(
'--chart-1', '--chart-2', '--chart-3', '--chart-4', '--chart-5', '--chart-6',
'--chart-grid', '--chart-reference', '--surface-inverse', '--text-inverse'
);
// ── Datasets (full set; mobile filters them via the active chip) ───── // ── Datasets (full set; mobile filters them via the active chip) ─────
const refLineStyle = { const refLineStyle = {
borderColor: 'rgba(90,80,70,0.35)', borderColor: cRef,
backgroundColor: 'transparent', backgroundColor: 'transparent',
borderWidth: 1.5, borderWidth: 1.5,
borderDash: [6, 4] as number[], borderDash: [6, 4] as number[],
@@ -140,8 +149,8 @@ export function PerformanceChart({
{ {
label: 'Attainment 8', label: 'Attainment 8',
data: col('attainment_8_score'), data: col('attainment_8_score'),
borderColor: '#2d7d7d', borderColor: c1,
backgroundColor: 'rgba(45,125,125,0.08)', backgroundColor: alpha('--chart-1', 0.08),
borderWidth: 2.5, borderWidth: 2.5,
tension: 0.3, tension: 0.3,
pointRadius: 4, pointRadius: 4,
@@ -151,8 +160,8 @@ export function PerformanceChart({
{ {
label: 'English & Maths Grade 4+', label: 'English & Maths Grade 4+',
data: col('english_maths_standard_pass_pct'), data: col('english_maths_standard_pass_pct'),
borderColor: '#c9a227', borderColor: c2,
backgroundColor: 'rgba(201,162,39,0.08)', backgroundColor: alpha('--chart-2', 0.08),
borderWidth: 1.5, borderWidth: 1.5,
tension: 0.3, tension: 0.3,
pointRadius: 3, pointRadius: 3,
@@ -161,8 +170,8 @@ export function PerformanceChart({
{ {
label: 'Progress 8', label: 'Progress 8',
data: col('progress_8_score'), data: col('progress_8_score'),
borderColor: 'rgb(139,92,246)', borderColor: c3,
backgroundColor: 'rgba(139,92,246,0.08)', backgroundColor: alpha('--chart-3', 0.08),
borderWidth: 1.5, borderWidth: 1.5,
tension: 0.3, tension: 0.3,
pointRadius: 3, pointRadius: 3,
@@ -179,8 +188,8 @@ export function PerformanceChart({
{ {
label: 'Reading, Writing & Maths expected %', label: 'Reading, Writing & Maths expected %',
data: col('rwm_expected_pct'), data: col('rwm_expected_pct'),
borderColor: '#2d7d7d', borderColor: c1,
backgroundColor: 'rgba(45,125,125,0.08)', backgroundColor: alpha('--chart-1', 0.08),
borderWidth: 2.5, borderWidth: 2.5,
tension: 0.3, tension: 0.3,
pointRadius: 4, pointRadius: 4,
@@ -190,8 +199,8 @@ export function PerformanceChart({
{ {
label: 'Exceeding expected level', label: 'Exceeding expected level',
data: col('rwm_high_pct'), data: col('rwm_high_pct'),
borderColor: '#c9a227', borderColor: c2,
backgroundColor: 'rgba(201,162,39,0.08)', backgroundColor: alpha('--chart-2', 0.08),
borderWidth: 1.5, borderWidth: 1.5,
tension: 0.3, tension: 0.3,
pointRadius: 3, pointRadius: 3,
@@ -206,8 +215,8 @@ export function PerformanceChart({
{ {
label: 'Reading progress', label: 'Reading progress',
data: col('reading_progress'), data: col('reading_progress'),
borderColor: 'rgb(59,130,246)', borderColor: c4,
backgroundColor: 'rgba(59,130,246,0.08)', backgroundColor: alpha('--chart-4', 0.08),
borderWidth: 1.5, borderWidth: 1.5,
tension: 0.3, tension: 0.3,
pointRadius: 3, pointRadius: 3,
@@ -217,8 +226,8 @@ export function PerformanceChart({
{ {
label: 'Writing progress', label: 'Writing progress',
data: col('writing_progress'), data: col('writing_progress'),
borderColor: 'rgb(139,92,246)', borderColor: c3,
backgroundColor: 'rgba(139,92,246,0.08)', backgroundColor: alpha('--chart-3', 0.08),
borderWidth: 1.5, borderWidth: 1.5,
tension: 0.3, tension: 0.3,
pointRadius: 3, pointRadius: 3,
@@ -228,8 +237,8 @@ export function PerformanceChart({
{ {
label: 'Maths progress', label: 'Maths progress',
data: col('maths_progress'), data: col('maths_progress'),
borderColor: 'rgb(236,72,153)', borderColor: c5,
backgroundColor: 'rgba(236,72,153,0.08)', backgroundColor: alpha('--chart-5', 0.08),
borderWidth: 1.5, borderWidth: 1.5,
tension: 0.3, tension: 0.3,
pointRadius: 3, pointRadius: 3,
@@ -301,7 +310,9 @@ export function PerformanceChart({
}, },
title: { display: false }, title: { display: false },
tooltip: { tooltip: {
backgroundColor: 'rgba(26,22,18,0.92)', backgroundColor: cInverse,
titleColor: cInverseText,
bodyColor: cInverseText,
padding: 12, padding: 12,
titleFont: { size: 13 }, titleFont: { size: 13 },
bodyFont: { size: 12 }, bodyFont: { size: 12 },
@@ -321,7 +332,7 @@ export function PerformanceChart({
type: 'linear', display: true, position: 'left', type: 'linear', display: true, position: 'left',
title: { display: true, text: isSecondary ? 'Score / %' : 'Percentage (%)', font: { size: 11 } }, title: { display: true, text: isSecondary ? 'Score / %' : 'Percentage (%)', font: { size: 11 } },
min: 0, max: isSecondary ? undefined : 100, min: 0, max: isSecondary ? undefined : 100,
grid: { color: 'rgba(0,0,0,0.05)' }, grid: { color: cGrid },
ticks: { font: { size: 11 } }, ticks: { font: { size: 11 } },
}, },
y1: { y1: {
@@ -346,7 +357,9 @@ export function PerformanceChart({
legend: { display: false }, legend: { display: false },
title: { display: false }, title: { display: false },
tooltip: { tooltip: {
backgroundColor: 'rgba(26,22,18,0.92)', backgroundColor: cInverse,
titleColor: cInverseText,
bodyColor: cInverseText,
padding: 10, padding: 10,
titleFont: { size: 12 }, titleFont: { size: 12 },
bodyFont: { size: 11 }, bodyFont: { size: 11 },
@@ -364,7 +377,7 @@ export function PerformanceChart({
y: { y: {
type: 'linear', display: true, position: 'left', type: 'linear', display: true, position: 'left',
min: mobileYBounds.min, max: mobileYBounds.max, min: mobileYBounds.min, max: mobileYBounds.max,
grid: { color: 'rgba(0,0,0,0.05)' }, grid: { color: cGrid },
ticks: { font: { size: 10 }, maxTicksLimit: 5 }, ticks: { font: { size: 10 }, maxTicksLimit: 5 },
}, },
x: { x: {
@@ -429,9 +442,9 @@ export function PerformanceChart({
belong together. */} belong together. */}
{isMobile && activeChip === 'progress' && ( {isMobile && activeChip === 'progress' && (
<div className={styles.miniLegend}> <div className={styles.miniLegend}>
<span><span className={styles.miniDot} style={{ background: 'rgb(59,130,246)' }} />Reading</span> <span><span className={styles.miniDot} style={{ background: c4 }} />Reading</span>
<span><span className={styles.miniDot} style={{ background: 'rgb(139,92,246)' }} />Writing</span> <span><span className={styles.miniDot} style={{ background: c3 }} />Writing</span>
<span><span className={styles.miniDot} style={{ background: 'rgb(236,72,153)' }} />Maths</span> <span><span className={styles.miniDot} style={{ background: c5 }} />Maths</span>
</div> </div>
)} )}
+55 -54
View File
@@ -11,14 +11,14 @@
.header h1 { .header h1 {
font-size: 2.25rem; font-size: 2.25rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
} }
.subtitle { .subtitle {
font-size: 1rem; font-size: 1rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
margin: 0; margin: 0;
line-height: 1.6; line-height: 1.6;
} }
@@ -28,7 +28,7 @@
display: flex; display: flex;
gap: 0; gap: 0;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
width: fit-content; width: fit-content;
@@ -38,8 +38,8 @@
padding: 0.625rem 1.5rem; padding: 0.625rem 1.5rem;
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 500; font-weight: 500;
background: var(--bg-card, white); background: var(--bg-card);
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease; transition: all 0.15s ease;
@@ -47,34 +47,34 @@
} }
.phaseTab:not(:last-child) { .phaseTab:not(:last-child) {
border-right: 1px solid var(--border-color, #e5dfd5); border-right: 1px solid var(--border);
} }
.phaseTab:hover { .phaseTab:hover {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
.phaseTabActive { .phaseTabActive {
background: var(--accent-coral-dark, #b04a2e); background: var(--brand-strong);
color: white; color: var(--brand-on);
font-weight: 600; font-weight: 600;
} }
.phaseTabActive:hover { .phaseTabActive:hover {
background: var(--accent-coral-darker, #9c3f26); background: var(--brand-stronger);
} }
/* Filters */ /* Filters */
.filters { .filters {
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;
padding: 1.5rem; padding: 1.5rem;
margin-bottom: 2rem; margin-bottom: 2rem;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1.5rem; gap: 1.5rem;
box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.06)); box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.06));
} }
.filterGroup { .filterGroup {
@@ -88,7 +88,7 @@
.filterLabel { .filterLabel {
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
white-space: nowrap; white-space: nowrap;
} }
@@ -98,44 +98,44 @@
max-width: 100%; max-width: 100%;
padding: 0.625rem 1rem; padding: 0.625rem 1rem;
font-size: 0.9375rem; font-size: 0.9375rem;
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
background: var(--bg-card, white); background: var(--bg-card);
color: var(--text-primary, #1a1612); color: var(--text-primary);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.filterSelect:hover { .filterSelect:hover {
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
} }
.filterSelect:focus { .filterSelect:focus {
outline: none; outline: none;
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
box-shadow: 0 0 0 3px var(--accent-coral-bg); box-shadow: 0 0 0 3px var(--brand-bg);
} }
.filterSelect optgroup { .filterSelect optgroup {
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
padding: 0.5rem 0; padding: 0.5rem 0;
} }
.filterSelect option { .filterSelect option {
font-weight: 400; font-weight: 400;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
padding: 0.375rem 1rem; padding: 0.375rem 1rem;
} }
/* Rankings Section */ /* Rankings Section */
.rankingsSection { .rankingsSection {
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;
padding: 2rem; padding: 2rem;
box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.06)); box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.06));
} }
.tableWrapper { .tableWrapper {
@@ -151,15 +151,15 @@
} }
.rankingsTable thead { .rankingsTable thead {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
.rankingsTable th { .rankingsTable th {
padding: 1rem; padding: 1rem;
text-align: left; text-align: left;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
border-bottom: 2px solid var(--border-color, #e5dfd5); border-bottom: 2px solid var(--border);
white-space: nowrap; white-space: nowrap;
text-transform: uppercase; text-transform: uppercase;
font-size: 0.75rem; font-size: 0.75rem;
@@ -206,8 +206,8 @@
.rankingsTable td { .rankingsTable td {
padding: 1rem; padding: 1rem;
border-bottom: 1px solid var(--border-color, #e5dfd5); border-bottom: 1px solid var(--border);
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
} }
.rankingsTable tbody tr:last-child td { .rankingsTable tbody tr:last-child td {
@@ -216,24 +216,24 @@
/* Alternating row backgrounds for visual rhythm */ /* Alternating row backgrounds for visual rhythm */
.rankingsTable tbody tr:nth-child(even) { .rankingsTable tbody tr:nth-child(even) {
background: rgba(243, 237, 228, 0.5); background: rgba(var(--text-inverse-rgb), 0.5);
} }
.rankingsTable tbody tr:hover { .rankingsTable tbody tr:hover {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
/* Top 3 Highlighting with Gold */ /* Top 3 Highlighting with Gold */
.rank1 { .rank1 {
background: linear-gradient(90deg, rgba(201, 162, 39, 0.15) 0%, transparent 100%) !important; background: linear-gradient(90deg, rgba(var(--status-below-rgb), 0.15) 0%, transparent 100%) !important;
} }
.rank2 { .rank2 {
background: linear-gradient(90deg, rgba(192, 192, 192, 0.15) 0%, transparent 100%) !important; background: linear-gradient(90deg, rgba(var(--medal-silver-rgb), 0.15) 0%, transparent 100%) !important;
} }
.rank3 { .rank3 {
background: linear-gradient(90deg, rgba(205, 127, 50, 0.15) 0%, transparent 100%) !important; background: linear-gradient(90deg, rgba(var(--medal-bronze-rgb), 0.15) 0%, transparent 100%) !important;
} }
.rankCell { .rankCell {
@@ -241,7 +241,7 @@
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
/* Styled rank badges for top 3 */ /* Styled rank badges for top 3 */
@@ -254,8 +254,8 @@
border-radius: 50%; border-radius: 50%;
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 700;
color: white; color: var(--brand-on);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.15);
position: relative; position: relative;
} }
@@ -265,30 +265,31 @@
inset: -2px; inset: -2px;
border-radius: 50%; border-radius: 50%;
border: 2px solid transparent; border: 2px solid transparent;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.4), transparent) border-box; background: linear-gradient(135deg, rgba(var(--text-inverse-rgb), 0.4), transparent) border-box;
mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0); mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0);
mask-composite: exclude; mask-composite: exclude;
-webkit-mask-composite: xor; -webkit-mask-composite: xor;
} }
.rankBadge1 { .rankBadge1 {
background: linear-gradient(135deg, #c9a227 0%, #e8c547 50%, #c9a227 100%); background: linear-gradient(135deg, var(--status-below) 0%, var(--medal-gold) 50%, var(--status-below) 100%);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); text-shadow: 0 1px 2px rgba(var(--shadow-rgb), 0.2);
} }
.rankBadge2 { .rankBadge2 {
background: linear-gradient(135deg, #8c8c8c 0%, #c0c0c0 50%, #8c8c8c 100%); background: linear-gradient(135deg, var(--text-muted) 0%, var(--medal-silver) 50%, var(--text-muted) 100%);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); text-shadow: 0 1px 2px rgba(var(--shadow-rgb), 0.2);
} }
.rankBadge3 { .rankBadge3 {
background: linear-gradient(135deg, #a5673f 0%, #cd7f32 50%, #a5673f 100%); background: linear-gradient(135deg, var(--status-below) 0%, var(--medal-bronze) 50%, var(--status-below) 100%);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); text-shadow: 0 1px 2px rgba(var(--shadow-rgb), 0.2);
} }
.rankNumber { .rankNumber {
font-variant-numeric: tabular-nums;
font-size: 1rem; font-size: 1rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
} }
.schoolCell { .schoolCell {
@@ -296,18 +297,18 @@
} }
.schoolLink { .schoolLink {
color: var(--text-primary, #1a1612); color: var(--text-primary);
text-decoration: none; text-decoration: none;
transition: color 0.2s ease; transition: color 0.2s ease;
} }
.schoolLink:hover { .schoolLink:hover {
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
} }
.areaCell, .areaCell,
.typeCell { .typeCell {
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
} }
/* LA subline under the school name — mobile only (Area column hidden there). */ /* LA subline under the school name — mobile only (Area column hidden there). */
@@ -315,7 +316,7 @@
display: none; display: none;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 400; font-weight: 400;
color: var(--text-muted, #8a847a); color: var(--text-muted);
} }
.valueCell { .valueCell {
@@ -324,7 +325,7 @@
} }
.valueCell strong { .valueCell strong {
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
font-weight: 700; font-weight: 700;
} }
@@ -346,7 +347,7 @@
.noResults { .noResults {
text-align: center; text-align: center;
padding: 4rem 2rem; padding: 4rem 2rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
} }
.noResults p { .noResults p {
+14 -14
View File
@@ -17,7 +17,7 @@
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
color: var(--text-muted, #6d685f); color: var(--text-muted);
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
@@ -41,7 +41,7 @@
top: 0; top: 0;
height: 100%; height: 100%;
width: 1px; width: 1px;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
/* ── Per-bar national average marker ── /* ── Per-bar national average marker ──
@@ -54,7 +54,7 @@
bottom: -3px; bottom: -3px;
width: 2px; width: 2px;
transform: translateX(-50%); transform: translateX(-50%);
background: var(--accent-coral, #e07256); background: var(--brand);
border-radius: 2px; border-radius: 2px;
z-index: 4; z-index: 4;
pointer-events: none; pointer-events: none;
@@ -69,7 +69,7 @@
width: 5px; width: 5px;
height: 5px; height: 5px;
border-radius: 50%; border-radius: 50%;
background: var(--accent-coral, #e07256); background: var(--brand);
} }
.barHeaderRight { .barHeaderRight {
@@ -81,7 +81,7 @@
.natLabel { .natLabel {
font-size: 0.55rem; font-size: 0.55rem;
font-weight: 700; font-weight: 700;
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.03em; letter-spacing: 0.03em;
} }
@@ -123,18 +123,18 @@
} }
.barExpected { .barExpected {
background: var(--accent-teal-light, #3a9e9e); background: var(--brand);
} }
.barExceeding { .barExceeding {
background: var(--accent-teal, #2d7d7d); background: var(--brand);
} }
.barLabel { .barLabel {
font-family: var(--font-playfair), 'Playfair Display', Georgia, serif; font-family: var(--font-display);
font-size: 0.78rem; font-size: 0.78rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
white-space: nowrap; white-space: nowrap;
position: relative; position: relative;
z-index: 3; z-index: 3;
@@ -142,7 +142,7 @@
.barLabelSuffix { .barLabelSuffix {
font-weight: 500; font-weight: 500;
color: var(--text-muted, #6d685f); color: var(--text-muted);
font-size: 0.6rem; font-size: 0.6rem;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.06em; letter-spacing: 0.06em;
@@ -153,7 +153,7 @@
position: relative; position: relative;
height: 14px; height: 14px;
margin-top: 0.3rem; margin-top: 0.3rem;
border-top: 1px solid var(--border-color, #e5dfd5); border-top: 1px solid var(--border);
} }
.rulerTick { .rulerTick {
@@ -161,7 +161,7 @@
top: 0; top: 0;
width: 1px; width: 1px;
height: 3px; height: 3px;
background: var(--border-color, #e5dfd5); background: var(--border);
} }
.rulerLabel { .rulerLabel {
@@ -169,7 +169,7 @@
top: 5px; top: 5px;
font-size: 0.5rem; font-size: 0.5rem;
font-weight: 500; font-weight: 500;
color: var(--text-muted, #6d685f); color: var(--text-muted);
transform: translateX(-50%); transform: translateX(-50%);
letter-spacing: 0.01em; letter-spacing: 0.01em;
opacity: 0.7; opacity: 0.7;
@@ -198,7 +198,7 @@
gap: 0.35rem; gap: 0.35rem;
font-size: 0.65rem; font-size: 0.65rem;
font-weight: 500; font-weight: 500;
color: var(--text-muted, #6d685f); color: var(--text-muted);
} }
.legendSwatch { .legendSwatch {
+3 -3
View File
@@ -145,15 +145,15 @@ export default function SatsChart({ subjects }: SatsChartProps) {
</div> </div>
<div className={styles.legend}> <div className={styles.legend}>
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.legendSwatch} style={{ background: 'var(--accent-teal-light, #3a9e9e)' }} /> <div className={styles.legendSwatch} style={{ background: 'var(--status-above)' }} />
Expected standard Expected standard
</div> </div>
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.legendSwatch} style={{ background: 'var(--accent-teal, #2d7d7d)' }} /> <div className={styles.legendSwatch} style={{ background: 'var(--status-above)' }} />
Exceeding / high score Exceeding / high score
</div> </div>
<div className={styles.legendItem}> <div className={styles.legendItem}>
<div className={styles.legendSwatch} style={{ background: 'var(--accent-coral, #e07256)', width: '3px', height: '12px', borderRadius: '2px' }} /> <div className={styles.legendSwatch} style={{ background: 'var(--brand)', width: '3px', height: '12px', borderRadius: '2px' }} />
National average National average
</div> </div>
</div> </div>
+25 -24
View File
@@ -1,6 +1,6 @@
.card { .card {
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-left: 3px solid transparent; border-left: 3px solid transparent;
border-radius: 10px; border-radius: 10px;
padding: 1rem 1.125rem; padding: 1rem 1.125rem;
@@ -8,13 +8,13 @@
} }
.card.cardInCompare { .card.cardInCompare {
border-color: var(--accent-teal, #2d7d7d); border-color: var(--brand);
box-shadow: 0 0 0 1px var(--accent-teal, #2d7d7d); box-shadow: 0 0 0 1px var(--brand);
} }
.card:hover { .card:hover {
border-left-color: var(--accent-coral, #e07256); border-left-color: var(--brand);
box-shadow: var(--shadow-medium, 0 4px 20px rgba(26, 22, 18, 0.1)); box-shadow: var(--shadow-medium, 0 4px 20px rgba(var(--shadow-rgb), 0.1));
transform: translateY(-1px); transform: translateY(-1px);
} }
@@ -31,24 +31,24 @@
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
line-height: 1.35; line-height: 1.35;
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
} }
.title a { .title a {
color: var(--text-primary, #1a1612); color: var(--text-primary);
text-decoration: none; text-decoration: none;
transition: color 0.2s ease; transition: color 0.2s ease;
} }
.title a:hover { .title a:hover {
color: var(--accent-coral, #e07256); color: var(--brand);
} }
.distance { .distance {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--accent-teal, #2d7d7d); color: var(--brand);
white-space: nowrap; white-space: nowrap;
background: var(--accent-teal-bg); background: var(--brand-bg);
padding: 0.125rem 0.5rem; padding: 0.125rem 0.5rem;
border-radius: 12px; border-radius: 12px;
font-weight: 500; font-weight: 500;
@@ -63,9 +63,9 @@
.metaItem { .metaItem {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
padding: 0.125rem 0.5rem; padding: 0.125rem 0.5rem;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-radius: 3px; border-radius: 3px;
} }
@@ -75,9 +75,9 @@
gap: 0.625rem; gap: 0.625rem;
margin-bottom: 0.875rem; margin-bottom: 0.875rem;
padding: 0.625rem 0.75rem; padding: 0.625rem 0.75rem;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-radius: 6px; border-radius: 6px;
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
} }
.metric { .metric {
@@ -88,12 +88,13 @@
.metricLabel { .metricLabel {
font-size: 0.6875rem; font-size: 0.6875rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
} }
.metricValue { .metricValue {
font-variant-numeric: tabular-nums;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.375rem; gap: 0.375rem;
@@ -101,7 +102,7 @@
.metricValue strong { .metricValue strong {
font-size: 1rem; font-size: 1rem;
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-weight: 700; font-weight: 700;
} }
@@ -126,18 +127,18 @@
} }
.trendUp { .trendUp {
color: var(--accent-teal, #2d7d7d); color: var(--status-above);
background: var(--accent-teal-bg); background: var(--status-above-bg);
} }
.trendDown { .trendDown {
color: var(--accent-coral, #e07256); color: var(--status-below);
background: var(--accent-coral-bg); background: var(--status-below-bg);
} }
.trendStable { .trendStable {
color: var(--text-muted, #8a847a); color: var(--text-muted);
background: rgba(138, 132, 122, 0.15); background: rgba(var(--muted-rgb), 0.15);
} }
.actions { .actions {
@@ -156,7 +157,7 @@
.metricHint { .metricHint {
font-size: 0.7rem; font-size: 0.7rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
display: block; display: block;
margin-top: 1px; margin-top: 1px;
font-weight: 400; font-weight: 400;
@@ -6,7 +6,7 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: 210px; height: 210px;
background: #dfe6e2; background: var(--bg-secondary);
isolation: isolate; isolation: isolate;
} }
@@ -47,7 +47,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
background: background:
linear-gradient(100deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, .5) 50%, rgba(255, 255, 255, 0) 60%) #e7e2da; linear-gradient(100deg, rgba(255, 255, 255, 0) 40%, rgba(255, 255, 255, .5) 50%, rgba(255, 255, 255, 0) 60%) var(--bg-secondary);
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.4s infinite; animation: shimmer 1.4s infinite;
} }
@@ -85,11 +85,11 @@
border-radius: 999px; border-radius: 999px;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
background: rgba(255, 255, 255, .85); background: rgba(255, 255, 255, .85);
-webkit-backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
box-shadow: 0 2px 10px rgba(0, 0, 0, .16); box-shadow: 0 2px 10px rgba(var(--shadow-rgb), .16);
opacity: 0; opacity: 0;
transform: translateY(4px); transform: translateY(4px);
transition: opacity .16s ease, transform .16s ease; transition: opacity .16s ease, transform .16s ease;
@@ -118,7 +118,7 @@
rgba(255, 255, 255, .35) 35%, rgba(255, 255, 255, .35) 35%,
rgba(255, 255, 255, .75) 62%, rgba(255, 255, 255, .75) 62%,
rgba(255, 255, 255, .95) 82%, rgba(255, 255, 255, .95) 82%,
var(--bg-card, #fff) 100%); var(--bg-card) 100%);
} }
.closeBtn { .closeBtn {
@@ -134,9 +134,9 @@
border: none; border: none;
border-radius: 8px; border-radius: 8px;
background: rgba(255, 255, 255, .92); background: rgba(255, 255, 255, .92);
color: var(--text-primary, #1a1612); color: var(--text-primary);
cursor: pointer; cursor: pointer;
box-shadow: 0 2px 10px rgba(0, 0, 0, .2); box-shadow: 0 2px 10px rgba(var(--shadow-rgb), .2);
} }
.closeBtn:hover { .closeBtn:hover {
+4 -4
View File
@@ -33,12 +33,12 @@
border: 2px solid rgba(0, 0, 0, 0.2); border: 2px solid rgba(0, 0, 0, 0.2);
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
color: #333; color: var(--text-primary);
transition: background 0.15s ease, color 0.15s ease; transition: background 0.15s ease, color 0.15s ease;
} }
.fullscreenBtn:hover { .fullscreenBtn:hover {
background: #f4f4f4; background: var(--bg-secondary);
color: #000; color: #000;
} }
@@ -57,9 +57,9 @@
.spinner { .spinner {
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
border: 3px solid rgba(224, 114, 86, 0.3); border: 3px solid rgba(var(--brand-rgb), 0.3);
border-radius: 50%; border-radius: 50%;
border-top-color: var(--accent-coral, #e07256); border-top-color: var(--brand);
animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite;
} }
+36 -35
View File
@@ -2,8 +2,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-left: 3px solid transparent; border-left: 3px solid transparent;
border-radius: 8px; border-radius: 8px;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
@@ -12,17 +12,17 @@
} }
.row:hover { .row:hover {
box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06); box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.06);
} }
/* Phase border colours */ /* Phase border colours */
.phasePrimary { border-left-color: var(--phase-primary, #5b8cbf); } .phasePrimary { border-left-color: var(--phase-primary); }
.phaseAllThrough { border-left-color: var(--phase-all-through, #7a9a6d); } .phaseAllThrough { border-left-color: var(--phase-all-through); }
.phaseNursery { border-left-color: var(--phase-nursery, #e0a0b0); } .phaseNursery { border-left-color: var(--phase-nursery); }
.rowInCompare { .rowInCompare {
border-left-color: var(--accent-teal, #2d7d7d); border-left-color: var(--brand);
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
@keyframes rowFadeIn { @keyframes rowFadeIn {
@@ -50,7 +50,7 @@
.schoolName { .schoolName {
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
text-decoration: none; text-decoration: none;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -60,7 +60,7 @@
} }
.schoolName:hover { .schoolName:hover {
color: var(--accent-coral, #e07256); color: var(--brand);
} }
/* Phase label pill */ /* Phase label pill */
@@ -96,8 +96,8 @@
line-height: 1.4; line-height: 1.4;
border-radius: 4px; border-radius: 4px;
white-space: nowrap; white-space: nowrap;
background: rgba(26, 22, 18, 0.05); background: rgba(var(--shadow-rgb), 0.05);
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
} }
/* Line 3: stats */ /* Line 3: stats */
@@ -115,10 +115,11 @@
} }
.statValue { .statValue {
font-variant-numeric: tabular-nums;
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.2rem; gap: 0.2rem;
@@ -126,7 +127,7 @@
.statLabel { .statLabel {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
white-space: nowrap; white-space: nowrap;
} }
@@ -137,9 +138,9 @@
margin-left: 1px; margin-left: 1px;
} }
.trendUp { color: var(--accent-teal, #2d7d7d); } .trendUp { color: var(--status-above); }
.trendDown { color: var(--accent-coral, #e07256); } .trendDown { color: var(--status-below); }
.trendStable { color: var(--text-muted, #8a847a); } .trendStable { color: var(--text-muted); }
/* Line 4: location */ /* Line 4: location */
.line4 { .line4 {
@@ -148,13 +149,13 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: 0; gap: 0;
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
} }
.line4 span:not(:last-child)::after { .line4 span:not(:last-child)::after {
content: '·'; content: '·';
margin: 0 0.4rem; margin: 0 0.4rem;
color: var(--border-color, #e5dfd5); color: var(--border);
} }
.distanceBadge { .distanceBadge {
@@ -162,8 +163,8 @@
padding: 0.0625rem 0.375rem; padding: 0.0625rem 0.375rem;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
background: var(--accent-teal, #2d7d7d); background: var(--brand);
color: white; color: var(--brand-on);
border-radius: 3px; border-radius: 3px;
} }
@@ -200,23 +201,23 @@
font-weight: 400; font-weight: 400;
} }
.ofsted1 { background: var(--accent-teal-bg); color: var(--accent-teal, #2d7d7d); } .ofsted1 { background: var(--status-above-bg); color: var(--status-above); }
.ofsted2 { background: rgba(60, 140, 60, 0.12); color: #2f7a2f; } .ofsted2 { background: rgba(var(--status-above-rgb), 0.12); color: var(--status-above); }
.ofsted3 { background: var(--accent-gold-bg); color: var(--accent-gold-text, #7a6800); } .ofsted3 { background: var(--status-below-bg); color: var(--status-below); }
.ofsted4 { background: var(--accent-coral-bg); color: var(--accent-coral, #e07256); } .ofsted4 { background: var(--status-below); color: var(--text-inverse); }
/* ── Ofsted badge variants ──────────────────────────────────────────────── */ /* ── Ofsted badge variants ──────────────────────────────────────────────── */
/* ofsted14 already defined above; these cover the two new framework states */ /* ofsted14 already defined above; these cover the two new framework states */
.ofstedRc { background: #5a3a6e; color: #fff; } .ofstedRc { background: var(--phase-secondary-text); color: var(--text-inverse); }
/* Inspected but with no overall grade (OEIF post-Sept-2024) — neutral slate, /* Inspected but with no overall grade (OEIF post-Sept-2024) — neutral slate,
distinct from the grey "Not yet inspected" pending state. */ distinct from the grey "Not yet inspected" pending state. */
.ofstedInspected { background: #e6ecf2; color: #45586b; } .ofstedInspected { background: var(--phase-primary-bg); color: var(--phase-primary-text); }
.ofstedPending { background: #e0e0e0; color: #666; } .ofstedPending { background: var(--border); color: var(--text-muted); }
/* ── vs-national delta line (under RWM metric) ──────────────────────────── */ /* ── vs-national delta line (under RWM metric) ──────────────────────────── */
.vsNational { font-size: 0.7rem; color: var(--accent-teal, #2d7d7d); font-weight: 600; } .vsNational { font-size: 0.7rem; color: var(--status-above); font-weight: 600; }
.vsNationalNeg { font-size: 0.7rem; color: var(--accent-coral, #e07256); font-weight: 600; } .vsNationalNeg { font-size: 0.7rem; color: var(--brand); font-weight: 600; }
.vsNationalFlat { font-size: 0.7rem; color: var(--text-muted, #8a847a); } .vsNationalFlat { font-size: 0.7rem; color: var(--text-muted); }
/* ── Mobile ──────────────────────────────────────────── */ /* ── Mobile ──────────────────────────────────────────── */
@media (max-width: 640px) { @media (max-width: 640px) {
@@ -257,7 +258,7 @@
/* GIAS "Open, but proposed to close" marker */ /* GIAS "Open, but proposed to close" marker */
.attrClosing { .attrClosing {
background: #fdf6e3; background: var(--status-below-bg);
color: #8a6200; color: var(--status-below);
border: 1px solid #e2c96f; border: 1px solid var(--status-below-bg);
} }
@@ -3,9 +3,9 @@
} }
.warning { .warning {
background: var(--accent-gold-bg); background: var(--status-below-bg);
border: 1px solid var(--accent-gold, #c9a227); border: 1px solid var(--status-below);
color: var(--text-primary, #1a1612); color: var(--text-primary);
padding: 1rem; padding: 1rem;
border-radius: 8px; border-radius: 8px;
margin-bottom: 1rem; margin-bottom: 1rem;
@@ -21,21 +21,21 @@
width: 100%; width: 100%;
padding: 0.875rem 1rem; padding: 0.875rem 1rem;
font-size: 1rem; font-size: 1rem;
border: 2px solid var(--border-color, #e5dfd5); border: 2px solid var(--border);
border-radius: 8px; border-radius: 8px;
background: var(--bg-card, white); background: var(--bg-card);
color: var(--text-primary, #1a1612); color: var(--text-primary);
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.searchInput::placeholder { .searchInput::placeholder {
color: var(--text-muted, #8a847a); color: var(--text-muted);
} }
.searchInput:focus { .searchInput:focus {
outline: none; outline: none;
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
box-shadow: 0 0 0 3px var(--accent-coral-bg); box-shadow: 0 0 0 3px var(--brand-bg);
} }
.searchSpinner { .searchSpinner {
@@ -45,9 +45,9 @@
transform: translateY(-50%); transform: translateY(-50%);
width: 1.25rem; width: 1.25rem;
height: 1.25rem; height: 1.25rem;
border: 2px solid rgba(224, 114, 86, 0.3); border: 2px solid rgba(var(--brand-rgb), 0.3);
border-radius: 50%; border-radius: 50%;
border-top-color: var(--accent-coral, #e07256); border-top-color: var(--brand);
animation: spin 0.6s linear infinite; animation: spin 0.6s linear infinite;
} }
@@ -71,17 +71,17 @@
} }
.results::-webkit-scrollbar-track { .results::-webkit-scrollbar-track {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-radius: 4px; border-radius: 4px;
} }
.results::-webkit-scrollbar-thumb { .results::-webkit-scrollbar-thumb {
background: var(--border-color, #e5dfd5); background: var(--border);
border-radius: 4px; border-radius: 4px;
} }
.results::-webkit-scrollbar-thumb:hover { .results::-webkit-scrollbar-thumb:hover {
background: var(--text-muted, #8a847a); background: var(--text-muted);
} }
.resultItem { .resultItem {
@@ -90,16 +90,16 @@
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
padding: 1rem; padding: 1rem;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 10px; border-radius: 10px;
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.resultItem:hover { .resultItem:hover {
background: var(--bg-card, white); background: var(--bg-card);
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.06)); box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.06));
} }
.schoolInfo { .schoolInfo {
@@ -110,7 +110,7 @@
.schoolName { .schoolName {
font-size: 1rem; font-size: 1rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -125,7 +125,7 @@
display: flex; display: flex;
gap: 1rem; gap: 1rem;
font-size: 0.875rem; font-size: 0.875rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
} }
.schoolMeta span { .schoolMeta span {
@@ -138,14 +138,14 @@
.noResults { .noResults {
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
font-size: 0.9375rem; font-size: 0.9375rem;
} }
.hint { .hint {
text-align: center; text-align: center;
padding: 2rem; padding: 2rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
font-size: 0.9375rem; font-size: 0.9375rem;
} }
@@ -2,8 +2,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-left: 3px solid transparent; border-left: 3px solid transparent;
border-radius: 8px; border-radius: 8px;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
@@ -12,17 +12,17 @@
} }
.row:hover { .row:hover {
box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06); box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.06);
} }
/* Phase border colours */ /* Phase border colours */
.phaseSecondary { border-left-color: var(--phase-secondary, #9b6bb0); } .phaseSecondary { border-left-color: var(--phase-secondary); }
.phaseAllThrough { border-left-color: var(--phase-all-through, #7a9a6d); } .phaseAllThrough { border-left-color: var(--phase-all-through); }
.phasePost16 { border-left-color: var(--phase-post16, #c4915e); } .phasePost16 { border-left-color: var(--phase-post16); }
.rowInCompare { .rowInCompare {
border-left-color: var(--accent-teal, #2d7d7d); border-left-color: var(--brand);
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
} }
@keyframes rowFadeIn { @keyframes rowFadeIn {
@@ -50,7 +50,7 @@
.schoolName { .schoolName {
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
text-decoration: none; text-decoration: none;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@@ -60,7 +60,7 @@
} }
.schoolName:hover { .schoolName:hover {
color: var(--accent-coral, #e07256); color: var(--brand);
} }
/* Line 2: context tags */ /* Line 2: context tags */
@@ -81,8 +81,8 @@
line-height: 1.4; line-height: 1.4;
border-radius: 4px; border-radius: 4px;
white-space: nowrap; white-space: nowrap;
background: rgba(26, 22, 18, 0.05); background: rgba(var(--shadow-rgb), 0.05);
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
} }
/* Line 3: KS4 stats */ /* Line 3: KS4 stats */
@@ -100,22 +100,24 @@
} }
.statValueLarge { .statValueLarge {
font-variant-numeric: tabular-nums;
font-size: 1.125rem; font-size: 1.125rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
} }
.statValue { .statValue {
font-variant-numeric: tabular-nums;
font-size: 0.9375rem; font-size: 0.9375rem;
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-family: var(--font-playfair), 'Playfair Display', serif; font-family: var(--font-display);
} }
.statLabel { .statLabel {
font-size: 0.75rem; font-size: 0.75rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
white-space: nowrap; white-space: nowrap;
} }
@@ -125,8 +127,8 @@
white-space: nowrap; white-space: nowrap;
} }
.deltaPositive { color: #2f7a2f; } .deltaPositive { color: var(--status-above); }
.deltaNegative { color: var(--accent-coral, #e07256); } .deltaNegative { color: var(--status-below); }
/* Line 4: location + distance */ /* Line 4: location + distance */
.line4 { .line4 {
@@ -135,13 +137,13 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: 0; gap: 0;
font-size: 0.8rem; font-size: 0.8rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
} }
.line4 span:not(:last-child)::after { .line4 span:not(:last-child)::after {
content: '·'; content: '·';
margin: 0 0.4rem; margin: 0 0.4rem;
color: var(--border-color, #e5dfd5); color: var(--border);
} }
.distanceBadge { .distanceBadge {
@@ -149,8 +151,8 @@
padding: 0.0625rem 0.375rem; padding: 0.0625rem 0.375rem;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
background: var(--accent-teal, #2d7d7d); background: var(--brand);
color: white; color: var(--brand-on);
border-radius: 3px; border-radius: 3px;
} }
@@ -175,15 +177,15 @@
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
line-height: 1.4; line-height: 1.4;
background: rgba(26, 22, 18, 0.05); background: rgba(var(--shadow-rgb), 0.05);
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
border-radius: 4px; border-radius: 4px;
white-space: nowrap; white-space: nowrap;
} }
.selectiveTag { .selectiveTag {
background: rgba(180, 120, 0, 0.1); background: rgba(var(--status-below-rgb), 0.1);
color: #8a6200; color: var(--status-below);
} }
/* ── Ofsted badge ────────────────────────────────────── */ /* ── Ofsted badge ────────────────────────────────────── */
@@ -202,17 +204,17 @@
font-weight: 400; font-weight: 400;
} }
.ofsted1 { background: var(--accent-teal-bg); color: var(--accent-teal, #2d7d7d); } .ofsted1 { background: var(--status-above-bg); color: var(--status-above); }
.ofsted2 { background: rgba(60, 140, 60, 0.12); color: #2f7a2f; } .ofsted2 { background: rgba(var(--status-above-rgb), 0.12); color: var(--status-above); }
.ofsted3 { background: var(--accent-gold-bg); color: var(--accent-gold-text, #7a6800); } .ofsted3 { background: var(--status-below-bg); color: var(--status-below); }
.ofsted4 { background: var(--accent-coral-bg); color: var(--accent-coral, #e07256); } .ofsted4 { background: var(--status-below); color: var(--text-inverse); }
/* ── Ofsted badge variants ──────────────────────────────────────────────── */ /* ── Ofsted badge variants ──────────────────────────────────────────────── */
.ofstedRc { background: #5a3a6e; color: #fff; } .ofstedRc { background: var(--phase-secondary-text); color: var(--text-inverse); }
/* Inspected but with no overall grade (OEIF post-Sept-2024) — neutral slate, /* Inspected but with no overall grade (OEIF post-Sept-2024) — neutral slate,
distinct from the grey "Not yet inspected" pending state. */ distinct from the grey "Not yet inspected" pending state. */
.ofstedInspected { background: #e6ecf2; color: #45586b; } .ofstedInspected { background: var(--phase-primary-bg); color: var(--phase-primary-text); }
.ofstedPending { background: #e0e0e0; color: #666; } .ofstedPending { background: var(--border); color: var(--text-muted); }
/* ── Right actions column ────────────────────────────── */ /* ── Right actions column ────────────────────────────── */
.rowActions { .rowActions {
@@ -268,7 +270,7 @@
} }
.closingTag { .closingTag {
background: #fdf6e3; background: var(--status-below-bg);
color: #8a6200; color: var(--status-below);
border: 1px solid #e2c96f; border: 1px solid var(--status-below-bg);
} }
@@ -1,15 +1,15 @@
/* Special-school / PRU / AP context note: soft, informational (teal), not a /* Special-school / PRU / AP context note: soft, informational (teal), not a
warning. Shared by both detail views so the styling can't drift. */ warning. Shared by both detail views so the styling can't drift. */
.note { .note {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-left: 4px solid var(--accent-teal, #2d7d7d); border-left: 4px solid var(--brand);
border-radius: 0 6px 6px 0; border-radius: 0 6px 6px 0;
padding: 0.7rem 1rem; padding: 0.7rem 1rem;
margin: 0 0 1rem; margin: 0 0 1rem;
font-size: 0.9rem; font-size: 0.9rem;
line-height: 1.5; line-height: 1.5;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
} }
.note strong { .note strong {
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
@@ -1,6 +1,6 @@
.moreMeasures { .moreMeasures {
margin-top: 0.5rem; margin-top: 0.5rem;
border-top: 1px solid var(--border-light); border-top: 1px solid var(--border);
padding-top: 0.75rem; padding-top: 0.75rem;
} }
@@ -8,7 +8,7 @@
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
font-size: 0.88rem; font-size: 0.88rem;
color: var(--accent-coral-dark); color: var(--brand-strong);
} }
.stripNote { .stripNote {
@@ -88,7 +88,7 @@ export function CompareAtAGlance({
)} )}
{display.kind === 'transitional' && ( {display.kind === 'transitional' && (
<> <>
<span className={s.badge} style={{ backgroundColor: '#e2e8f0', color: '#475569' }}> <span className={s.badge} style={{ backgroundColor: 'var(--bg-secondary)', color: 'var(--text-secondary)' }}>
No overall grade No overall grade
</span> </span>
<span className={s.small}>Sub-judgements only</span> <span className={s.small}>Sub-judgements only</span>
@@ -54,7 +54,7 @@ function ResultCell({ display }: { display: OfstedDisplay }) {
if (display.kind === 'transitional') { if (display.kind === 'transitional') {
return ( return (
<> <>
<span className={s.badge} style={{ backgroundColor: '#e2e8f0', color: '#475569' }}> <span className={s.badge} style={{ backgroundColor: 'var(--bg-secondary)', color: 'var(--text-secondary)' }}>
No overall grade No overall grade
</span> </span>
<span className={s.small}> <span className={s.small}>
@@ -5,10 +5,10 @@
.explore summary { .explore summary {
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
color: var(--accent-coral-dark); color: var(--brand-strong);
padding: 0.85rem 1.1rem; padding: 0.85rem 1.1rem;
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border-light); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
} }
@@ -17,7 +17,7 @@
} }
.inner { .inner {
border: 1px solid var(--border-light); border: 1px solid var(--border);
border-top: none; border-top: none;
border-radius: 0 0 8px 8px; border-radius: 0 0 8px 8px;
background: var(--bg-card); background: var(--bg-card);
@@ -43,7 +43,7 @@
font-size: 0.9rem; font-size: 0.9rem;
padding: 0.4rem 0.6rem; padding: 0.4rem 0.6rem;
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--border-light); border: 1px solid var(--border);
background: var(--bg-card); background: var(--bg-card);
color: var(--text-primary); color: var(--text-primary);
max-width: 100%; max-width: 100%;
@@ -9,12 +9,12 @@
} }
.sectionTitle { .sectionTitle {
font-family: var(--font-playfair), 'Playfair Display', Georgia, serif; font-family: var(--font-display);
font-size: 1.45rem; font-size: 1.45rem;
font-weight: 700; font-weight: 700;
margin: 0; margin: 0;
padding-left: 0.75rem; padding-left: 0.75rem;
border-left: 3px solid var(--accent-coral-dark); border-left: 3px solid var(--brand-strong);
} }
.how { .how {
@@ -36,7 +36,7 @@
back into the shared grid. */ back into the shared grid. */
.measure { .measure {
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border-light); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;
box-shadow: var(--shadow-soft); box-shadow: var(--shadow-soft);
padding: 0.75rem 0.85rem; padding: 0.75rem 0.85rem;
@@ -59,7 +59,7 @@
gap: 0.35rem 0.5rem; gap: 0.35rem 0.5rem;
flex-wrap: wrap; flex-wrap: wrap;
padding: 0.5rem 0; padding: 0.5rem 0;
border-top: 1px solid var(--border-light); border-top: 1px solid var(--border);
margin-top: 0.5rem; margin-top: 0.5rem;
font-size: 0.95rem; font-size: 0.95rem;
} }
@@ -113,18 +113,18 @@
} }
.chipGood { .chipGood {
background: rgba(45, 125, 125, 0.14); background: rgba(var(--status-above-rgb), 0.14);
color: var(--accent-teal); color: var(--status-above);
} }
.chipWarn { .chipWarn {
background: var(--accent-gold-bg); background: var(--status-below-bg);
color: var(--accent-gold-text); color: var(--status-below);
} }
.chipBad { .chipBad {
background: var(--accent-coral-bg); background: var(--status-below-bg);
color: var(--accent-coral-dark); color: var(--status-below);
} }
.chipNeutral { .chipNeutral {
@@ -141,18 +141,18 @@
} }
.badgeGood { .badgeGood {
background: rgba(45, 125, 125, 0.14); background: rgba(var(--status-above-rgb), 0.14);
color: var(--accent-teal); color: var(--status-above);
} }
.badgeWarn { .badgeWarn {
background: var(--accent-gold-bg); background: var(--status-below-bg);
color: var(--accent-gold-text); color: var(--status-below);
} }
.badgeBad { .badgeBad {
background: var(--accent-coral-bg); background: var(--status-below-bg);
color: var(--accent-coral-dark); color: var(--status-below);
} }
.rcList { .rcList {
@@ -200,7 +200,7 @@
.card { .card {
background: var(--bg-card); background: var(--bg-card);
border: 1px solid var(--border-light); border: 1px solid var(--border);
border-radius: 16px; border-radius: 16px;
box-shadow: var(--shadow-soft); box-shadow: var(--shadow-soft);
padding: 1.25rem 1.5rem; padding: 1.25rem 1.5rem;
@@ -208,7 +208,7 @@
} }
.link { .link {
color: var(--accent-coral-dark); color: var(--brand-strong);
} }
@media (min-width: 761px) { @media (min-width: 761px) {
@@ -230,14 +230,14 @@
.rowLabel { .rowLabel {
color: var(--text-secondary); color: var(--text-secondary);
padding: 0.85rem 0.5rem 0.85rem 0; padding: 0.85rem 0.5rem 0.85rem 0;
border-bottom: 1px solid var(--border-light); border-bottom: 1px solid var(--border);
} }
.cell { .cell {
display: block; display: block;
padding: 0.85rem 0.25rem; padding: 0.85rem 0.25rem;
border-top: none; border-top: none;
border-bottom: 1px solid var(--border-light); border-bottom: 1px solid var(--border);
margin-top: 0; margin-top: 0;
} }
@@ -19,7 +19,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-dark, #b04a2e); color: var(--brand-strong);
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
@@ -29,7 +29,7 @@
.topBack:hover { .topBack:hover {
color: var(--accent-coral-dark, #c85a3e); color: var(--brand-strong);
text-decoration: underline; text-decoration: underline;
text-underline-offset: 2px; text-underline-offset: 2px;
} }
@@ -38,8 +38,8 @@
/* Header Section */ /* Header Section */
.header { .header {
position: relative; position: relative;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 10px; border-radius: 10px;
/* Padding lives on .headerContent so the map band can bleed to the edges. */ /* Padding lives on .headerContent so the map band can bleed to the edges. */
padding: 0; padding: 0;
@@ -88,12 +88,13 @@
.headerHasMap .actions .btnAdd { .headerHasMap .actions .btnAdd {
/* Sits on the map hero, which renders light in both themes. */
background: rgba(255, 255, 255, 0.9); background: rgba(255, 255, 255, 0.9);
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
border-color: transparent; border-color: transparent;
-webkit-backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px); backdrop-filter: blur(6px);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16); box-shadow: 0 2px 10px rgba(var(--shadow-rgb), 0.16);
} }
@@ -116,14 +117,14 @@
padding: 0; padding: 0;
font: inherit; font: inherit;
font-weight: 600; font-weight: 600;
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
} }
.mapLink:hover { .mapLink:hover {
color: var(--accent-coral-dark, #c45a3f); color: var(--brand-strong);
text-decoration: underline; text-decoration: underline;
text-underline-offset: 2px; text-underline-offset: 2px;
} }
@@ -137,11 +138,11 @@
.schoolName { .schoolName {
font-size: clamp(2rem, 5vw, 3.25rem); font-size: clamp(2rem, 5vw, 3.25rem);
font-weight: 700; font-weight: 700;
color: var(--text-primary, #1a1612); color: var(--text-primary);
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
line-height: 1.1; line-height: 1.1;
letter-spacing: -0.01em; letter-spacing: -0.01em;
font-family: var(--font-playfair), "Playfair Display", serif; font-family: var(--font-display);
} }
@@ -155,16 +156,16 @@
.metaItem { .metaItem {
font-size: 0.8125rem; font-size: 0.8125rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
padding: 0.125rem 0.5rem; padding: 0.125rem 0.5rem;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-radius: 3px; border-radius: 3px;
} }
.address { .address {
font-size: 0.875rem; font-size: 0.875rem;
color: var(--text-muted, #8a847a); color: var(--text-muted);
margin: 0 0 0.75rem; margin: 0 0 0.75rem;
} }
@@ -180,18 +181,18 @@
.headerDetail { .headerDetail {
font-size: 0.8125rem; font-size: 0.8125rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
} }
.headerDetail strong { .headerDetail strong {
color: var(--text-primary, #1a1612); color: var(--text-primary);
font-weight: 600; font-weight: 600;
} }
.headerDetail a { .headerDetail a {
color: var(--accent-teal, #2d7d7d); color: var(--brand);
text-decoration: none; text-decoration: none;
} }
@@ -213,7 +214,7 @@
border: none; border: none;
font-size: 0.8125rem; font-size: 0.8125rem;
font-weight: 600; font-weight: 600;
color: var(--accent-teal, #2d7d7d); color: var(--brand);
cursor: pointer; cursor: pointer;
} }
@@ -236,25 +237,25 @@
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
white-space: nowrap; white-space: nowrap;
box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.08)); box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.08));
} }
.btnAdd { .btnAdd {
background: var(--accent-coral-dark, #b04a2e); background: var(--brand-strong);
color: white; color: var(--brand-on);
} }
.btnAdd:hover { .btnAdd:hover {
background: var(--accent-coral-darker, #9c3f26); background: var(--brand-stronger);
transform: translateY(-1px); transform: translateY(-1px);
} }
.btnRemove { .btnRemove {
background: var(--accent-teal, #2d7d7d); background: var(--brand);
color: white; color: var(--brand-on);
} }
@@ -270,13 +271,13 @@
position: sticky; position: sticky;
top: 64px; /* global header height on desktop */ top: 64px; /* global header height on desktop */
z-index: 10; z-index: 10;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-top: none; border-top: none;
border-radius: 0 0 10px 10px; border-radius: 0 0 10px 10px;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
margin-bottom: 1rem; margin-bottom: 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); box-shadow: 0 2px 4px rgba(var(--shadow-rgb), 0.04);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
@@ -291,9 +292,9 @@
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-dark, #b04a2e); color: var(--brand-strong);
background: none; background: none;
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
@@ -302,8 +303,8 @@
.sectionNavBack:hover { .sectionNavBack:hover {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-color: var(--accent-coral, #e07256); border-color: var(--brand);
} }
@@ -334,7 +335,7 @@
padding: 0.3rem 0.625rem; padding: 0.3rem 0.625rem;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 500; font-weight: 500;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
text-decoration: none; text-decoration: none;
border-radius: 4px; border-radius: 4px;
transition: all 0.15s ease; transition: all 0.15s ease;
@@ -344,21 +345,21 @@
.sectionNavLink:hover { .sectionNavLink:hover {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
.sectionNavLinkActive { .sectionNavLinkActive {
background: var(--accent-coral-dark, #b04a2e); background: var(--brand-strong);
color: white; color: var(--brand-on);
font-weight: 600; font-weight: 600;
} }
.sectionNavLinkActive:hover { .sectionNavLinkActive:hover {
background: var(--accent-coral-dark, #c45a3f); background: var(--brand-strong);
color: white; color: var(--brand-on);
} }
@@ -373,12 +374,12 @@
gap: 0.5rem; gap: 0.5rem;
min-height: 38px; min-height: 38px;
padding: 0.34rem 0.7rem; padding: 0.34rem 0.7rem;
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
font-family: var(--font-dm-sans), "DM Sans", sans-serif; font-family: var(--font-ui);
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
@@ -396,7 +397,7 @@
font-weight: 600; font-weight: 600;
letter-spacing: 0.04em; letter-spacing: 0.04em;
text-transform: uppercase; text-transform: uppercase;
color: var(--text-muted, #6d685f); color: var(--text-muted);
} }
@@ -411,7 +412,7 @@
.sectionNavMenuChev { .sectionNavMenuChev {
flex: none; flex: none;
color: var(--text-muted, #6d685f); color: var(--text-muted);
font-size: 0.7rem; font-size: 0.7rem;
} }
@@ -426,9 +427,9 @@
width: 38px; width: 38px;
height: 38px; height: 38px;
border-radius: 9px; border-radius: 9px;
border: 1px solid var(--accent-coral-dark, #b04a2e); border: 1px solid var(--brand-strong);
background: var(--accent-coral-dark, #b04a2e); background: var(--brand-strong);
color: white; color: var(--brand-on);
cursor: pointer; cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease; transition: background 0.15s ease, border-color 0.15s ease;
} }
@@ -447,9 +448,9 @@
width: 16px; width: 16px;
height: 16px; height: 16px;
border-radius: 50%; border-radius: 50%;
background: var(--bg-card, white); background: var(--bg-card);
color: var(--accent-coral-dark, #c45a3f); color: var(--brand-strong);
border: 1.5px solid var(--accent-coral, #e07256); border: 1.5px solid var(--brand);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -460,9 +461,9 @@
.sectionNavCompareIconIn { .sectionNavCompareIconIn {
background: var(--bg-card, white); background: var(--bg-card);
border-color: var(--accent-teal, #2d7d7d); border-color: var(--brand);
color: var(--accent-teal, #2d7d7d); color: var(--brand);
} }
@@ -474,9 +475,9 @@
padding: 0.34rem 0.7rem; padding: 0.34rem 0.7rem;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
color: white; color: var(--brand-on);
background: var(--accent-coral-dark, #b04a2e); background: var(--brand-strong);
border: 1px solid var(--accent-coral-dark, #b04a2e); border: 1px solid var(--brand-strong);
border-radius: 999px; border-radius: 999px;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
@@ -485,21 +486,21 @@
.sectionNavCompare:hover { .sectionNavCompare:hover {
background: var(--accent-coral-darker, #9c3f26); background: var(--brand-stronger);
border-color: var(--accent-coral-darker, #9c3f26); border-color: var(--brand-stronger);
} }
.sectionNavCompareIn { .sectionNavCompareIn {
background: var(--bg-card, white); background: var(--bg-card);
color: var(--accent-teal, #2d7d7d); color: var(--brand);
border-color: var(--accent-teal, #2d7d7d); border-color: var(--brand);
} }
.sectionNavCompareIn:hover { .sectionNavCompareIn:hover {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border-color: var(--accent-teal, #2d7d7d); border-color: var(--brand);
} }
@@ -512,8 +513,8 @@
padding: 0.34rem 0.65rem; padding: 0.34rem 0.65rem;
font-size: 0.75rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
border: none; border: none;
border-radius: 999px; border-radius: 999px;
cursor: pointer; cursor: pointer;
@@ -523,7 +524,7 @@
.sectionNavAll:hover { .sectionNavAll:hover {
background: var(--border-color, #e5dfd5); background: var(--border);
} }
@@ -531,7 +532,7 @@
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 1500; z-index: 1500;
background: rgba(26, 22, 18, 0.28); background: rgba(var(--shadow-rgb), 0.28);
} }
@@ -543,19 +544,19 @@
width: 230px; width: 230px;
max-height: min(70vh, 460px); max-height: min(70vh, 460px);
overflow-y: auto; overflow-y: auto;
background: var(--bg-card, white); background: var(--bg-card);
border: 1px solid var(--border-color, #e5dfd5); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;
box-shadow: 0 18px 44px rgba(26, 22, 18, 0.2); box-shadow: 0 18px 44px rgba(var(--shadow-rgb), 0.2);
padding: 0.35rem; padding: 0.35rem;
} }
.sectionsPanelHead { .sectionsPanelHead {
font-family: var(--font-playfair), "Playfair Display", Georgia, serif; font-family: var(--font-display);
font-size: 0.9rem; font-size: 0.9rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1612); color: var(--text-primary);
padding: 0.4rem 0.6rem 0.5rem; padding: 0.4rem 0.6rem 0.5rem;
} }
@@ -568,44 +569,44 @@
padding: 0.55rem 0.6rem; padding: 0.55rem 0.6rem;
border-radius: 8px; border-radius: 8px;
font-size: 0.85rem; font-size: 0.85rem;
color: var(--text-secondary, #5c564d); color: var(--text-secondary);
text-decoration: none; text-decoration: none;
transition: background 0.12s ease; transition: background 0.12s ease;
} }
.sectionsItem:hover { .sectionsItem:hover {
background: var(--bg-secondary, #f3ede4); background: var(--bg-secondary);
color: var(--text-primary, #1a1612); color: var(--text-primary);
} }
.sectionsItemActive { .sectionsItemActive {
background: var(--accent-coral-bg, rgba(224, 114, 86, 0.12)); background: var(--brand-bg);
color: var(--accent-coral-dark, #c45a3f); color: var(--brand-strong);
font-weight: 600; font-weight: 600;
} }
.sectionsTick { .sectionsTick {
color: var(--accent-coral-dark, #b04a2e); color: var(--brand-strong);
} }
/* GIAS "Open, but proposed to close" notice strip */ /* GIAS "Open, but proposed to close" notice strip */
.closingStrip { .closingStrip {
background: #fdf6e3; background: var(--status-below-bg);
border-left: 4px solid #e2c96f; border-left: 4px solid var(--status-below-bg);
border-radius: 0 6px 6px 0; border-radius: 0 6px 6px 0;
padding: 0.55rem 0.9rem; padding: 0.55rem 0.9rem;
margin: 0.5rem 0; margin: 0.5rem 0;
font-size: 0.88rem; font-size: 0.88rem;
color: #6e5a00; color: var(--status-below);
max-width: 68ch; max-width: 68ch;
} }
.closingStrip strong { .closingStrip strong {
color: #8a6200; color: var(--status-below);
} }
@media (max-width: 640px) { @media (max-width: 640px) {
@@ -738,7 +739,7 @@
max-height: 74vh; max-height: 74vh;
border-radius: 16px 16px 0 0; border-radius: 16px 16px 0 0;
padding: 0.5rem 0.6rem calc(0.8rem + env(safe-area-inset-bottom, 0)); padding: 0.5rem 0.6rem calc(0.8rem + env(safe-area-inset-bottom, 0));
box-shadow: 0 -10px 40px rgba(26, 22, 18, 0.25); box-shadow: 0 -10px 40px rgba(var(--shadow-rgb), 0.25);
} }
.sectionsItem { .sectionsItem {
File diff suppressed because it is too large Load Diff
+163
View File
@@ -0,0 +1,163 @@
/**
* Bridge between the CSS token layer and the bits of the UI that are painted
* from JavaScript — Chart.js datasets and Leaflet markers.
*
* Those two can't use var(), so before this they carried their own hardcoded
* hexes. That is how the chart palette drifted off-brand (a stray purple and
* Flat UI's stock blue) and why charts would have ignored the dark theme
* entirely. Reading the computed custom property keeps one source of truth.
*/
'use client';
import { useEffect, useState } from 'react';
export type ThemeToken =
| '--brand'
| '--brand-strong'
| '--brand-bg'
| '--status-above'
| '--status-above-bg'
| '--status-below'
| '--status-below-bg'
| '--text-primary'
| '--text-secondary'
| '--text-muted'
| '--text-inverse'
| '--bg-card'
| '--bg-secondary'
| '--border'
| '--border-strong'
| '--surface-inverse'
| '--chart-1'
| '--chart-2'
| '--chart-3'
| '--chart-4'
| '--chart-5'
| '--chart-6'
| '--chart-grid'
| '--chart-reference'
| '--phase-primary'
| '--phase-secondary-text'
| '--medal-gold'
| '--medal-silver'
| '--medal-bronze';
/** Values used before hydration and in any non-DOM context (SSR, tests). */
const FALLBACK: Record<string, string> = {
'--brand': '#584a9b',
'--brand-strong': '#473c80',
'--brand-bg': 'rgba(88, 74, 155, 0.10)',
'--status-above': '#0e6e66',
'--status-above-bg': 'rgba(14, 110, 102, 0.10)',
'--status-below': '#9a5b00',
'--status-below-bg': 'rgba(154, 91, 0, 0.11)',
'--text-primary': '#16202a',
'--text-secondary': '#4a545f',
'--text-muted': '#5c6570',
'--text-inverse': '#f7f7f4',
'--bg-card': '#fcfcfa',
'--bg-secondary': '#e9e8e2',
'--border': '#dedcd5',
'--border-strong': '#cdcac1',
'--surface-inverse': '#16202a',
'--chart-1': '#584a9b',
'--chart-2': '#0e6e66',
'--chart-3': '#9a5b00',
'--chart-4': '#8a7cc9',
'--chart-5': '#3e9c92',
'--chart-6': '#c9903d',
'--chart-grid': '#dedcd5',
'--chart-reference': '#5c6570',
'--phase-primary': '#4a6072',
'--phase-secondary-text': '#4e4570',
'--medal-gold': '#a67c00',
'--medal-silver': '#6f7580',
'--medal-bronze': '#8a5a2b',
};
/** Read one token. Safe to call during SSR — returns the light-theme value. */
export function token(name: ThemeToken): string {
if (typeof window === 'undefined') return FALLBACK[name];
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return value || FALLBACK[name];
}
/**
* Same as `token`, but the component re-renders when the OS theme flips so
* canvas-painted colours don't get stranded in the previous theme.
*/
export function useThemeTokens<T extends readonly ThemeToken[]>(
...names: T
): { [K in keyof T]: string } {
const read = () => names.map((n) => token(n)) as { [K in keyof T]: string };
const [values, setValues] = useState(read);
useEffect(() => {
// Only commit when something actually changed. In the light theme the
// computed values equal the fallbacks, so this is a no-op — which matters
// because a state update here re-renders the chart and makes Chart.js
// resize a canvas that may not be laid out yet.
const sync = () => setValues((prev) => (sameValues(prev, read()) ? prev : read()));
sync();
const mq = window.matchMedia('(prefers-color-scheme: dark)');
mq.addEventListener('change', sync);
return () => mq.removeEventListener('change', sync);
// `names` is a fixed-length literal tuple at every call site.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return values;
}
function sameValues(a: readonly string[], b: readonly string[]): boolean {
return a.length === b.length && a.every((v, i) => v === b[i]);
}
const SERIES = [
'--series-1', '--series-2', '--series-3', '--series-4',
'--series-5', '--series-6', '--series-7', '--series-8',
] as const;
const SERIES_FALLBACK = [
'#584a9b', '#0e6e66', '#9a5b00', '#a03a5e',
'#2f5f8f', '#4a6b2f', '#7a3f7a', '#97442a',
];
/**
* Resolved series colours for Chart.js datasets, in the same index order as
* CHART_COLORS — so a school keeps its colour between the canvas and the DOM
* swatch beside it. Re-reads when the OS theme flips.
*/
export function useSeriesColors(): string[] {
const read = () =>
typeof window === 'undefined'
? SERIES_FALLBACK
: SERIES.map((n, i) => {
const v = getComputedStyle(document.documentElement).getPropertyValue(n).trim();
return v || SERIES_FALLBACK[i];
});
const [colors, setColors] = useState(read);
useEffect(() => {
// See useThemeTokens: skip the commit when nothing changed, so mounting a
// chart doesn't cost an extra render.
const sync = () => setColors((prev) => (sameValues(prev, read()) ? prev : read()));
sync();
const mq = window.matchMedia('(prefers-color-scheme: dark)');
mq.addEventListener('change', sync);
return () => mq.removeEventListener('change', sync);
}, []);
return colors;
}
/** rgba() built from a token's hex, for fills that need their own alpha. */
export function alpha(name: ThemeToken, a: number): string {
const hex = token(name);
const m = /^#([0-9a-f]{6})$/i.exec(hex);
if (!m) return hex;
const n = parseInt(m[1], 16);
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;
}
+40 -30
View File
@@ -273,17 +273,29 @@ export function debounce<T extends (...args: any[]) => any>(
// ============================================================================ // ============================================================================
/** /**
* Chart color palette (consistent with vanilla JS app) * Series palette for comparing up to eight schools at once.
*
* These were Chart.js's stock demo colours, which is why the comparison view
* never looked like the rest of the site. They now point at the --series-*
* tokens, so the palette lives in one place and follows the theme.
*
* Every step clears WCAG AA on both grounds, so the same value works as a
* chart line and as the legend text keyed to it — there is no longer a
* separate text ramp to keep in sync.
*
* These are var() strings: correct anywhere the value lands in the DOM
* (inline styles, CSS custom properties). Canvas can't resolve var(), so
* Chart.js datasets must use `useSeriesColors()` from lib/theme instead.
*/ */
export const CHART_COLORS = [ export const CHART_COLORS = [
'rgb(75, 192, 192)', 'var(--series-1)',
'rgb(255, 99, 132)', 'var(--series-2)',
'rgb(54, 162, 235)', 'var(--series-3)',
'rgb(255, 206, 86)', 'var(--series-4)',
'rgb(153, 102, 255)', 'var(--series-5)',
'rgb(255, 159, 64)', 'var(--series-6)',
'rgb(201, 203, 207)', 'var(--series-7)',
'rgb(255, 0, 255)', 'var(--series-8)',
]; ];
/** /**
@@ -294,44 +306,42 @@ export function getChartColor(index: number): string {
} }
/** /**
* Darker counterparts of CHART_COLORS for use as TEXT on the site's light * @deprecated The series palette is now AA on both grounds, so line and text
* backgrounds — the raw series colours fail WCAG AA (e.g. 1.9:1 on cream). * share one value. Kept as an alias so call sites can migrate incrementally.
* 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 = [ export const CHART_TEXT_COLORS = CHART_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 { export function getChartTextColor(index: number): string {
return CHART_TEXT_COLORS[index % CHART_TEXT_COLORS.length]; return CHART_TEXT_COLORS[index % CHART_TEXT_COLORS.length];
} }
/** /**
* Convert RGB color to RGBA with opacity * Add opacity to a colour. Accepts `rgb(...)` or `#rrggbb` — the series
* palette resolves to hex now that it comes from the token layer, but the
* rgb() form is still used by callers passing Chart.js literals.
*/ */
export function rgbToRgba(rgb: string, alpha: number): string { export function rgbToRgba(color: string, alpha: number): string {
return rgb.replace('rgb', 'rgba').replace(')', `, ${alpha})`); const hex = /^#([0-9a-f]{6})$/i.exec(color.trim());
if (hex) {
const n = parseInt(hex[1], 16);
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
}
return color.replace('rgb', 'rgba').replace(')', `, ${alpha})`);
} }
/** /**
* Get trend color based on direction * Trend direction colour. Teal/amber rather than green/red so the signal
* survives redgreen colour blindness; `at` is deliberately neutral so a
* school sitting on the average doesn't read as a verdict.
*/ */
export function getTrendColor(trend: 'up' | 'down' | 'stable'): string { export function getTrendColor(trend: 'up' | 'down' | 'stable'): string {
switch (trend) { switch (trend) {
case 'up': case 'up':
return '#22c55e'; // green return 'var(--status-above)';
case 'down': case 'down':
return '#ef4444'; // red return 'var(--status-below)';
case 'stable': case 'stable':
return '#6b7280'; // gray return 'var(--status-at)';
} }
} }
+12 -1
View File
@@ -3,6 +3,15 @@ const nextConfig = {
// Enable standalone output for Docker // Enable standalone output for Docker
output: 'standalone', output: 'standalone',
// app/opengraph-image.tsx reads the Schibsted Grotesk files off disk at
// request time (Satori needs a font buffer; it has no system fallback).
// File tracing currently picks these up on its own, but that relies on the
// tracer resolving a runtime join() — declare them so a Next upgrade can't
// silently drop them and turn every link preview into a 500.
outputFileTracingIncludes: {
'/opengraph-image': ['./assets/**'],
},
// The /api/* and /sitemap.xml proxies to the FastAPI backend are route // The /api/* and /sitemap.xml proxies to the FastAPI backend are route
// handlers (app/api/[...path]/route.ts, app/sitemap.xml/route.ts) rather // handlers (app/api/[...path]/route.ts, app/sitemap.xml/route.ts) rather
// than rewrites, so the backend host is read from FASTAPI_URL at runtime // than rewrites, so the backend host is read from FASTAPI_URL at runtime
@@ -72,7 +81,9 @@ const nextConfig = {
], ],
}, },
{ {
source: '/favicon.svg', // favicon.svg was replaced by app/icon.svg (the mark now has one
// source in components/Logo.tsx); this rule was caching a 404.
source: '/icon.svg',
headers: [ headers: [
{ {
key: 'Cache-Control', key: 'Cache-Control',
-6
View File
@@ -1,6 +0,0 @@
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" rx="8" fill="#1a1612"/>
<circle cx="20" cy="20" r="14" stroke="#e07256" stroke-width="2"/>
<path d="M20 8L20 32M12 14L28 14M10 20L30 20M12 26L28 26" stroke="#e07256" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="20" cy="20" r="3" fill="#e07256"/>
</svg>

Before

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

+21 -3
View File
@@ -4,13 +4,31 @@
"description": "Compare primary and secondary school performance across England", "description": "Compare primary and secondary school performance across England",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"background_color": "#faf7f2", "background_color": "#f2f1ed",
"theme_color": "#faf7f2", "theme_color": "#f2f1ed",
"icons": [ "icons": [
{ {
"src": "/favicon.svg", "src": "/icon.svg",
"sizes": "any", "sizes": "any",
"type": "image/svg+xml" "type": "image/svg+xml"
},
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
} }
] ]
} }