diff --git a/e2e/tests/journeys.spec.ts b/e2e/tests/journeys.spec.ts index 3321d18..6c4fbf2 100644 --- a/e2e/tests/journeys.spec.ts +++ b/e2e/tests/journeys.spec.ts @@ -626,3 +626,79 @@ test('sticky section nav jumps to server-rendered sections', async ({ page }) => const after = await page.evaluate(() => window.scrollY); 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); +}); diff --git a/nextjs-app/app/apple-icon.tsx b/nextjs-app/app/apple-icon.tsx new file mode 100644 index 0000000..ce161d4 --- /dev/null +++ b/nextjs-app/app/apple-icon.tsx @@ -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( + ( +
+
+ {BARS.map(([x, y, w, h, isSchool], i) => ( +
+ ))} +
+
+ ), + size + ); +} diff --git a/nextjs-app/app/globals.css b/nextjs-app/app/globals.css index 1b66d32..67ae427 100644 --- a/nextjs-app/app/globals.css +++ b/nextjs-app/app/globals.css @@ -1,81 +1,239 @@ -/* - * SchoolCompare.co.uk - * A warm, editorial design inspired by quality publications +/* + * SchoolCompare.co.uk — "Cohort" + * + * A school's number means nothing on its own; its position in the national + * distribution does. That idea drives the identity: the logo is the same + * five-bar spread that appears inside every school row. + * + * Colour has exactly three jobs and they never borrow each other's hues: + * brand (iris) interactive + identity. Never means "good". + * status (teal / amber) above / below the comparison point. Never a CTA. + * phase (desaturated) category chips, deliberately subordinate. + * + * Teal/amber rather than green/red so the above/below signal survives every + * form of colour blindness — the audience includes parents reading closely. */ :root { - /* Warm, sophisticated palette */ - --bg-primary: #faf7f2; - --bg-secondary: #f3ede4; - --bg-card: #ffffff; - --bg-accent: #1a1612; - - --text-primary: #1a1612; - --text-secondary: #5c564d; - --text-muted: #6d685f; /* Darkened for WCAG AA (4.6:1 on cream) */ - --text-inverse: #faf7f2; - - --accent-coral: #e07256; - --accent-coral-dark: #b04a2e; /* AA: 4.5+ as text on white/cream tints; 5.4:1 under white text */ - --accent-coral-darker: #9c3f26; /* hover for coral-dark fills */ - --accent-teal: #296f6f; /* AA: 4.8+ as text on the cream/tint backgrounds */ - --accent-teal-light: #3a9e9e; - --accent-gold: #c9a227; - --accent-gold-text: #7a6800; /* WCAG AA safe for text on white/cream */ - --accent-navy: #2c3e50; + color-scheme: light; - /* Semantic background tints (replaces hardcoded rgba values) */ - --accent-coral-bg: rgba(224, 114, 86, 0.12); - --accent-teal-bg: rgba(45, 125, 125, 0.12); - --accent-gold-bg: rgba(201, 162, 39, 0.12); + /* ── Ground ─────────────────────────────────────────────────────── */ + --bg-primary: #f2f1ed; /* paper, not cream — warm enough to read as a document */ + --bg-secondary: #e9e8e2; + --bg-card: #fcfcfa; + --surface-inverse: #16202a; /* dark fills: skip link, active nav, tooltips */ - /* Trend colours */ - --trend-up: #16a34a; - --trend-down: var(--accent-coral); - --trend-stable: var(--text-muted); + /* ── Ink ────────────────────────────────────────────────────────── */ + --text-primary: #16202a; + --text-secondary: #4a545f; + --text-muted: #5c6570; /* 5.3:1 on --bg-primary */ + --text-inverse: #f7f7f4; + + /* ── Line ───────────────────────────────────────────────────────── */ + --border: #dedcd5; + --border-strong: #cdcac1; + + /* ── Brand: iris ────────────────────────────────────────────────── */ + --brand: #584a9b; /* 6.9:1 on --bg-primary */ + --brand-strong: #473c80; /* hover on brand fills */ + --brand-stronger: #3a3069; /* active/pressed */ + --brand-bg: rgba(88, 74, 155, 0.10); + --brand-on: #fcfcfa; /* text/icon sitting on a brand fill */ + + /* ── Status: above / at / below the comparison point ────────────── */ + --status-above: #0e6e66; /* 5.2:1 on --bg-primary */ + --status-above-bg: rgba(14, 110, 102, 0.10); + --status-below: #9a5b00; /* 4.8:1 on --bg-primary */ + --status-below-bg: rgba(154, 91, 0, 0.11); + --status-at: var(--text-muted); + --status-at-bg: rgba(92, 101, 112, 0.10); + + /* ── Charts: the same three hues, extended by lightness only ────── */ + --chart-1: #584a9b; + --chart-2: #0e6e66; + --chart-3: #9a5b00; + --chart-4: #8a7cc9; + --chart-5: #3e9c92; + --chart-6: #c9903d; + --chart-grid: #dedcd5; + --chart-reference: #5c6570; /* national/LA reference lines */ + + /* ── Series: up to 8 schools compared at once ─────────────────────── + Categorical needs distinguishable hues, so this ramp is wider than the + three-hue rule — but every step is held to the same mid-dark tone and + moderate saturation, and every one clears AA on --bg-primary, so they + read as one family and work as legend text as well as chart lines. */ + --series-1: #584a9b; + --series-2: #0e6e66; + --series-3: #9a5b00; + --series-4: #a03a5e; + --series-5: #2f5f8f; + --series-6: #4a6b2f; + --series-7: #7a3f7a; + --series-8: #97442a; + + /* ── Phase: category, desaturated so it stays under the status hues ── */ + --phase-primary: #4a6072; + --phase-primary-bg: rgba(74, 96, 114, 0.10); + --phase-primary-text: #3b4e5e; + --phase-secondary: #5f5480; + --phase-secondary-bg: rgba(95, 84, 128, 0.10); + --phase-secondary-text: #4e4570; + --phase-all-through: #4b6b57; + --phase-all-through-bg: rgba(75, 107, 87, 0.10); + --phase-all-through-text: #3d5847; + --phase-post16: #7a6242; + --phase-post16-bg: rgba(122, 98, 66, 0.10); + --phase-post16-text: #645036; + --phase-nursery: #7a5560; + --phase-nursery-bg: rgba(122, 85, 96, 0.10); + --phase-nursery-text: #64454f; + + /* ── Medals: rankings podium. Metal, not palette — kept legible on + both grounds rather than literally gold/silver/bronze. ───────── */ + --medal-gold: #a67c00; + --medal-silver: #6f7580; + --medal-bronze: #8a5a2b; + + /* ── Channels ─────────────────────────────────────────────────────── + CSS can't interpolate a hex token into rgba(), so any tint that needs + an arbitrary alpha reads the raw channels from here. Keep these in + sync with the hex tokens above — they are the same colours. */ + --brand-rgb: 88, 74, 155; + --status-above-rgb: 14, 110, 102; + --status-below-rgb: 154, 91, 0; + --ink-rgb: 22, 32, 42; + --text-inverse-rgb: 247, 247, 244; + --shadow-rgb: 22, 32, 42; + --muted-rgb: 92, 101, 112; + --phase-secondary-rgb: 95, 84, 128; + --medal-silver-rgb: 111, 117, 128; + --medal-bronze-rgb: 138, 90, 43; + + /* ── Elevation ──────────────────────────────────────────────────── */ + --shadow-soft: 0 1px 2px rgba(22, 32, 42, 0.04), 0 2px 8px rgba(22, 32, 42, 0.05); + --shadow-medium: 0 4px 20px rgba(22, 32, 42, 0.09); + --shadow-strong: 0 8px 40px rgba(22, 32, 42, 0.14); + --scrim: rgba(22, 32, 42, 0.55); + + /* ── Type ───────────────────────────────────────────────────────── */ + --font-display: var(--font-schibsted), 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, sans-serif; + --font-ui: var(--font-schibsted), 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, sans-serif; + --font-data: var(--font-schibsted), 'Schibsted Grotesk', -apple-system, BlinkMacSystemFont, sans-serif; + --font-prose: var(--font-literata), 'Literata', Georgia, serif; + + /* Type scale, 1.2 ratio off a 1rem base. New work should use these + rather than inventing another font-size. */ + --step--2: 0.694rem; + --step--1: 0.833rem; + --step-0: 1rem; + --step-1: 1.2rem; + --step-2: 1.44rem; + --step-3: 1.728rem; + --step-4: 2.074rem; + --step-5: 2.488rem; + + /* ── Geometry & motion ──────────────────────────────────────────── */ + --radius-sm: 3px; + --radius-md: 6px; + --radius-lg: 10px; + --radius-xl: 16px; - /* Button/Action colors */ - --primary: #e07256; - --primary-dark: #c45a3f; - --success: #2d7d7d; - --border-light: #e5dfd5; - - /* Chart colors */ - --chart-1: #e07256; - --chart-2: #2d7d7d; - --chart-3: #c9a227; - --chart-4: #7b68a6; - --chart-5: #3498db; - - --border-color: #e5dfd5; - --shadow-soft: 0 2px 8px rgba(26, 22, 18, 0.06); - --shadow-medium: 0 4px 20px rgba(26, 22, 18, 0.1); - --shadow-strong: 0 8px 40px rgba(26, 22, 18, 0.15); - - --radius-sm: 4px; - --radius-md: 8px; - --radius-lg: 16px; - --radius-xl: 24px; - --transition: 0.2s ease; --transition-slow: 0.4s ease; +} - /* Phase indicators */ - --phase-primary: #5b8cbf; - --phase-primary-bg: rgba(91, 140, 191, 0.10); - --phase-primary-text: #3d6a99; - --phase-secondary: #9b6bb0; - --phase-secondary-bg: rgba(155, 107, 176, 0.10); - --phase-secondary-text: #7a4f93; - --phase-all-through: #7a9a6d; - --phase-all-through-bg: rgba(122, 154, 109, 0.10); - --phase-all-through-text: #5a7a4d; - --phase-post16: #c4915e; - --phase-post16-bg: rgba(196, 145, 94, 0.10); - --phase-post16-text: #9a6d3a; - --phase-nursery: #e0a0b0; - --phase-nursery-bg: rgba(224, 160, 176, 0.10); - --phase-nursery-text: #b06070; +/* + * Dark theme. Not an inversion — the accent and both status hues are lifted + * independently so each still clears AA on the dark ground, and --brand-on + * flips to dark because the brand fill is now the lighter of the pair. + * + * Tokens only. Never style a component from inside this block, or the two + * themes drift apart the first time someone edits one of them. + */ +@media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + + --bg-primary: #10151b; + --bg-secondary: #161c24; + --bg-card: #171d25; + --surface-inverse: #e4e9ee; + + --text-primary: #e4e9ee; + --text-secondary: #b4bec8; + --text-muted: #8d99a6; /* 6.4:1 on --bg-primary */ + --text-inverse: #10151b; + + --border: #28313b; + --border-strong: #38434f; + + --brand: #9c8ce8; /* 6.5:1 on --bg-primary */ + --brand-strong: #b0a3ee; + --brand-stronger: #c2b8f3; + --brand-bg: rgba(156, 140, 232, 0.14); + --brand-on: #10151b; + + --status-above: #3fb3a4; /* 7.2:1 */ + --status-above-bg: rgba(63, 179, 164, 0.14); + --status-below: #d99b2e; /* 7.7:1 */ + --status-below-bg: rgba(217, 155, 46, 0.14); + --status-at-bg: rgba(141, 153, 166, 0.14); + + --chart-1: #9c8ce8; + --chart-2: #3fb3a4; + --chart-3: #d99b2e; + --chart-4: #6f5fc4; + --chart-5: #2b8a7e; + --chart-6: #a97420; + --chart-grid: #28313b; + --chart-reference: #8d99a6; + + --series-1: #9c8ce8; + --series-2: #3fb3a4; + --series-3: #d99b2e; + --series-4: #e07a9d; + --series-5: #6fa8dc; + --series-6: #94c46b; + --series-7: #c48bc4; + --series-8: #e08a6b; + + --phase-primary: #8aa4b8; + --phase-primary-bg: rgba(138, 164, 184, 0.14); + --phase-primary-text: #a3bacd; + --phase-secondary: #a294c4; + --phase-secondary-bg: rgba(162, 148, 196, 0.14); + --phase-secondary-text: #b7abd6; + --phase-all-through: #8bb098; + --phase-all-through-bg: rgba(139, 176, 152, 0.14); + --phase-all-through-text: #a2c2ad; + --phase-post16: #bfa27c; + --phase-post16-bg: rgba(191, 162, 124, 0.14); + --phase-post16-text: #d0b795; + --phase-nursery: #bf98a3; + --phase-nursery-bg: rgba(191, 152, 163, 0.14); + --phase-nursery-text: #d1aeb8; + + --medal-gold: #d4a72c; + --medal-silver: #a8b0bb; + --medal-bronze: #c08552; + + --brand-rgb: 156, 140, 232; + --status-above-rgb: 63, 179, 164; + --status-below-rgb: 217, 155, 46; + --ink-rgb: 228, 233, 238; + --text-inverse-rgb: 16, 21, 27; + --shadow-rgb: 0, 0, 0; + --muted-rgb: 141, 153, 166; + --phase-secondary-rgb: 162, 148, 196; + --medal-silver-rgb: 168, 176, 187; + --medal-bronze-rgb: 192, 133, 82; + + --shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.3); + --shadow-medium: 0 4px 20px rgba(0, 0, 0, 0.45); + --shadow-strong: 0 8px 40px rgba(0, 0, 0, 0.6); + --scrim: rgba(4, 7, 10, 0.7); + } } * { @@ -89,7 +247,7 @@ html { } body { - font-family: var(--font-dm-sans), 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif; + font-family: var(--font-ui); background: var(--bg-primary); color: var(--text-primary); line-height: 1.6; @@ -102,9 +260,23 @@ body { -webkit-tap-highlight-color: transparent; } +/* Every digit that could line up in a column does. This is a data product; + proportional numerals in a results table read as amateur. */ +table, +[data-numeric] { + font-variant-numeric: tabular-nums; +} + +/* Prose gets the serif; the interface never does. */ +.prose { + font-family: var(--font-prose); + font-size: var(--step-0); + line-height: 1.65; +} + /* Provide a baseline press feedback for the most common interactive elements — replaces the suppressed default tap highlight. Buttons and - .btn-* classes carry their own :active styles already; this handles + .btn-* classes carry their own :active states already; this handles plain anchors used as inline links and bare button elements. */ @media (hover: none) and (pointer: coarse) { a:active, @@ -127,7 +299,7 @@ body { left: 1rem; z-index: 10000; padding: 0.5rem 1rem; - background: var(--bg-accent); + background: var(--surface-inverse); color: var(--text-inverse); font-size: 0.875rem; font-weight: 600; @@ -139,9 +311,9 @@ body { top: 0.5rem; } -/* Focus indicators — branded and visible on cream background */ +/* Focus indicators — brand hue, visible on both grounds */ :focus-visible { - outline: 2px solid var(--accent-coral); + outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--radius-sm); } @@ -155,14 +327,14 @@ body { justify-content: center; gap: 0.375rem; padding: 0.5rem 1rem; - font-family: inherit; + font-family: var(--font-ui); font-size: 0.875rem; font-weight: 600; line-height: 1; - border-radius: 6px; + border-radius: var(--radius-md); border: 1px solid transparent; cursor: pointer; - transition: all var(--transition); + transition: background var(--transition), border-color var(--transition), color var(--transition); text-decoration: none; white-space: nowrap; } @@ -171,53 +343,52 @@ body { cursor: not-allowed; } -/* Primary: coral background — main CTAs (Search, Compare Now) */ +/* Primary: brand fill — main CTAs (Search, Compare now) */ .btn-primary { - background: var(--accent-coral-dark); - color: white; - border-color: var(--accent-coral-dark); + background: var(--brand); + color: var(--brand-on); + border-color: var(--brand); } .btn-primary:hover:not(:disabled) { - background: var(--accent-coral-darker); - border-color: var(--accent-coral-darker); + background: var(--brand-strong); + border-color: var(--brand-strong); +} +.btn-primary:active:not(:disabled) { + background: var(--brand-stronger); + border-color: var(--brand-stronger); } -/* Secondary: teal outline — supporting actions (+ Compare) */ -/* NOTE: a duplicate `.btn` block further down this file sets `border: none`, - which wins over the base `.btn`'s `1px solid transparent`. The outline - variants below therefore declare the full `border` shorthand explicitly so - they don't depend on the base border-width — otherwise `border-color` alone - has no width and the outline never renders (buttons read as plain text). */ +/* Secondary: brand outline — supporting actions (Add to shortlist) */ .btn-secondary { background: transparent; - color: var(--accent-teal); - border: 1px solid var(--accent-teal); + color: var(--brand); + border: 1px solid var(--brand); } .btn-secondary:hover:not(:disabled) { - background: var(--accent-teal-bg); + background: var(--brand-bg); } -/* Tertiary: subtle gray — low-emphasis (View, Clear) */ +/* Tertiary: quiet — low-emphasis (View, Clear) */ .btn-tertiary { background: var(--bg-secondary); color: var(--text-secondary); - border: 1px solid var(--border-color); + border: 1px solid var(--border); } .btn-tertiary:hover:not(:disabled) { - background: var(--border-color); + background: var(--border); color: var(--text-primary); } -/* Danger/active: for remove/destructive actions or active toggle state */ +/* Active toggle state — the shortlist button once a school is on the list */ .btn-active { - background: var(--accent-teal-bg); - color: var(--accent-teal); - border: 1px solid var(--accent-teal); + background: var(--brand-bg); + color: var(--brand); + border: 1px solid var(--brand); } .btn-active:hover:not(:disabled) { background: transparent; - color: var(--accent-coral-dark); - border-color: var(--accent-coral-dark); + color: var(--text-secondary); + border-color: var(--border-strong); } /* Small variant */ @@ -226,97 +397,7 @@ body { font-size: 0.8125rem; } -/* Subtle noise texture overlay - editorial paper feel */ -.noise-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; - opacity: 0.06; - z-index: 1000; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); -} - -/* Header */ -.header { - background: var(--bg-card); - border-bottom: 1px solid var(--border-color); - position: sticky; - top: 0; - z-index: 1000; -} - -.header-content { - max-width: 1400px; - margin: 0 auto; - padding: 0.625rem 1.5rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -.logo { - display: flex; - align-items: center; - gap: 0.5rem; - text-decoration: none; - color: inherit; -} - -.logo-icon { - width: 32px; - height: 32px; - color: var(--accent-coral); -} - -.logo-text { - display: flex; - flex-direction: column; -} - -.logo-title { - font-family: var(--font-playfair), 'Playfair Display', Georgia, serif; - font-size: 1.125rem; - font-weight: 700; - color: var(--text-primary); - line-height: 1.2; -} - -.logo-subtitle { - font-size: 0.625rem; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.1em; -} - -.nav { - display: flex; - gap: 0.25rem; -} - -.nav-link { - padding: 0.4rem 0.875rem; - text-decoration: none; - color: var(--text-secondary); - font-weight: 500; - font-size: 0.8125rem; - border-radius: var(--radius-md); - transition: var(--transition); -} - -.nav-link:hover { - color: var(--text-primary); - background: var(--bg-secondary); -} - -.nav-link.active { - background: var(--bg-accent); - color: var(--text-inverse); -} - -/* Main Content */ +/* Main content column */ .main { max-width: 1400px; margin: 0 auto; @@ -325,1746 +406,24 @@ body { overflow-x: clip; } -.view { - display: none; - animation: fadeIn 0.3s ease; -} - -.view.active { - display: block; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); } -} - -/* Hero Section */ -.hero { - text-align: center; - padding: 1.5rem 0 1rem; -} - -.hero-title { - font-family: var(--font-playfair), 'Playfair Display', Georgia, serif; - font-size: clamp(2rem, 5vw, 3.5rem); - font-weight: 700; - color: var(--text-primary); - margin-bottom: 0.75rem; - line-height: 1.2; -} - -.hero-subtitle { - font-size: 1.1rem; - color: var(--text-secondary); - max-width: 600px; - margin: 0 auto; -} - -/* Search Section */ -.search-section { - max-width: 700px; - margin: 2rem auto 3rem; -} - -/* Search Mode Toggle */ -.search-mode-toggle { - display: flex; - justify-content: center; - gap: 0; - margin-bottom: 1.5rem; - background: var(--bg-card); - border: 2px solid var(--border-color); - border-radius: var(--radius-lg); - padding: 4px; - max-width: 400px; - margin-left: auto; - margin-right: auto; -} - -.search-mode-btn { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.75rem 1.25rem; - font-size: 0.95rem; - font-family: inherit; - font-weight: 500; - border: none; - border-radius: var(--radius-md); - background: transparent; - color: var(--text-muted); - cursor: pointer; - transition: var(--transition); -} - -.search-mode-btn:hover { - color: var(--text-primary); -} - -.search-mode-btn.active { - background: var(--accent-coral); - color: white; - box-shadow: 0 2px 8px rgba(224, 114, 86, 0.3); -} - -.search-mode-btn svg { - flex-shrink: 0; -} - -/* Search Panels */ -.search-panel { - display: none; -} - -.search-panel.active { - display: block; -} - -.search-container { - position: relative; - margin-bottom: 1rem; -} - -.search-input { - width: 100%; - padding: 1rem 1.25rem 1rem 3.5rem; - font-size: 1rem; - font-family: inherit; - border: 2px solid var(--border-color); - border-radius: var(--radius-lg); - background: var(--bg-card); - color: var(--text-primary); - transition: var(--transition); -} - -.search-input:focus { - outline: none; - border-color: var(--accent-coral); - box-shadow: 0 0 0 4px rgba(224, 114, 86, 0.1); -} - -.search-input::placeholder { - color: var(--text-muted); -} - -.search-icon { - position: absolute; - left: 1.25rem; - top: 50%; - transform: translateY(-50%); - width: 20px; - height: 20px; - color: var(--text-muted); -} - -.filter-row { - display: flex; - gap: 1rem; - justify-content: center; -} - -.filter-select { - padding: 0.6rem 2rem 0.6rem 1rem; - font-size: 0.9rem; - font-family: inherit; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-card); - color: var(--text-primary); - cursor: pointer; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%235c564d' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.75rem center; - transition: var(--transition); -} - -/* Location Search */ -.location-input-group { - margin-bottom: 1rem; - display: flex; - gap: 0.75rem; - align-items: center; - justify-content: center; - flex-wrap: nowrap; -} - -.postcode-input { - width: auto; - max-width: 180px; - padding: 0.6rem 1rem; - font-size: 0.95rem; -} - -.postcode-input::placeholder { - text-transform: none; -} - -.radius-select { - width: auto; - min-width: 100px; -} - -.location-btn { - padding: 0.6rem 1.25rem; - font-size: 0.9rem; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.location-clear-btn { - padding: 0.6rem 1rem; - font-size: 0.9rem; - background: var(--bg-secondary); - color: var(--text-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - cursor: pointer; - transition: var(--transition); -} - -.location-clear-btn:hover { - background: var(--border-color); - color: var(--text-primary); -} - -.location-info { - text-align: center; - margin-bottom: 1rem; - padding: 0.75rem 1.25rem; - background: var(--accent-teal); - color: var(--text-inverse); - border-radius: var(--radius-md); - font-size: 0.9rem; - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; -} - -.location-info svg { - width: 16px; - height: 16px; -} - -.distance-badge { - display: inline-block; - padding: 0.25rem 0.5rem; - background: var(--accent-teal); - color: var(--text-inverse); - border-radius: var(--radius-sm); - font-size: 0.75rem; - font-weight: 600; - margin-left: 0.5rem; -} - -.filter-select:focus { - outline: none; - border-color: var(--accent-teal); -} - -/* View Toggle */ -.view-toggle { - display: flex; - gap: 0.5rem; - justify-content: center; - margin-bottom: 1.5rem; -} - -.view-toggle-btn { - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.6rem 1rem; - font-size: 0.9rem; - font-family: inherit; - font-weight: 500; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--bg-card); - color: var(--text-muted); - cursor: pointer; - transition: var(--transition); -} - -.view-toggle-btn:hover { - color: var(--text-primary); - border-color: var(--text-muted); -} - -.view-toggle-btn.active { - background: var(--accent-teal); - color: white; - border-color: var(--accent-teal); -} - -.view-toggle-btn svg { - flex-shrink: 0; -} - -/* Results Container */ -.results-container { - display: block; -} - -.results-container .results-map { - display: none; -} - -.results-container.map-view { - display: grid; - grid-template-columns: 1fr 400px; - gap: 1.5rem; - height: 600px; -} - -.results-container.map-view .results-map { - display: block; - border-radius: var(--radius-lg); - overflow: hidden; - border: 1px solid var(--border-color); - height: 100%; - position: relative; - z-index: 1; -} - -.results-container.map-view .schools-grid { - display: flex; - flex-direction: column; - gap: 0.75rem; - overflow-y: auto; - height: 100%; - padding-right: 0.5rem; -} - -.results-container.map-view .schools-grid::-webkit-scrollbar { - width: 6px; -} - -.results-container.map-view .schools-grid::-webkit-scrollbar-track { - background: var(--bg-main); - border-radius: 3px; -} - -.results-container.map-view .schools-grid::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 3px; -} - -.results-container.map-view .schools-grid::-webkit-scrollbar-thumb:hover { - background: var(--text-muted); -} - -/* Highlighted card in map view */ -.school-card.highlighted, -.school-list-item.highlighted { - border-color: var(--accent-teal); - box-shadow: 0 0 0 2px rgba(45, 125, 125, 0.2); -} - -/* Compact school list items for map view */ -.school-list-item { - display: flex; - align-items: center; - gap: 1rem; - padding: 0.875rem 1rem; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - cursor: pointer; - transition: var(--transition); -} - -.school-list-item:hover { - border-color: var(--text-muted); -} - -.school-list-item-content { - flex: 1; - min-width: 0; -} - -.school-list-item-header { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.25rem; -} - -.school-list-item-name { - font-size: 0.95rem; - font-weight: 600; - color: var(--text-primary); - margin: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.school-list-item-header .distance-badge { - flex-shrink: 0; - font-size: 0.75rem; - padding: 0.15rem 0.5rem; -} - -.school-list-item-meta { - display: flex; - gap: 0.5rem; - font-size: 0.8rem; - color: var(--text-muted); - margin-bottom: 0.375rem; -} - -.school-list-item-meta span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.school-list-item-stats { - display: flex; - gap: 1rem; - font-size: 0.8rem; - color: var(--text-secondary); -} - -.school-list-item-stat strong { - color: var(--text-primary); -} - -.school-list-item-actions { - display: flex; - flex-direction: column; - gap: 0.375rem; - flex-shrink: 0; -} - -.school-list-item-actions .btn { - padding: 0.4rem 0.75rem; - font-size: 0.75rem; - white-space: nowrap; -} - -.btn-compare { - background: var(--accent-coral-dark); - color: white; - border: 1px solid var(--accent-coral-dark); -} - -.btn-compare:hover { - background: var(--accent-coral-darker); - border-color: var(--accent-coral-darker); -} - -.btn-compare.active { - background: var(--text-muted); - border-color: var(--text-muted); -} - -/* Search location marker on map */ -.search-location-marker { - background: transparent; -} - -/* Schools Grid */ -.schools-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 1.5rem; -} - -/* Featured Schools Header */ -.featured-header { - grid-column: 1 / -1; - text-align: center; - padding: 1rem 0 2rem; -} - -.featured-header h3 { - font-family: var(--font-playfair), 'Playfair Display', Georgia, serif; - font-size: 1.5rem; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 0.5rem; -} - -.featured-header p { - color: var(--text-muted); - font-size: 0.95rem; -} - -.school-card.featured { - border-color: var(--accent-coral); - border-width: 2px; -} - -.school-card { - display: flex; - flex-direction: column; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: 1.5rem; - cursor: pointer; - transition: var(--transition); - position: relative; - overflow: hidden; - z-index: 0; -} - -.school-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 4px; - height: 100%; - background: var(--accent-coral); - transform: scaleY(0); - transition: var(--transition); -} - -.school-card:hover { - border-color: var(--accent-coral); - box-shadow: var(--shadow-medium); - transform: translateY(-2px); -} - -.school-card:hover::before { - transform: scaleY(1); -} - -.school-name { - font-family: var(--font-playfair), 'Playfair Display', Georgia, serif; - font-size: 1.15rem; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 0.5rem; -} - -.school-meta { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-bottom: 1rem; -} - -.school-tag { - font-size: 0.75rem; - padding: 0.25rem 0.6rem; - background: var(--bg-secondary); - color: var(--text-secondary); - border-radius: var(--radius-sm); -} - -.school-tag.type { - background: rgba(45, 125, 125, 0.1); - color: var(--accent-teal); -} - -.school-tag.faith { - background: rgba(138, 43, 226, 0.1); - color: #8a2be2; -} - -.school-address { - font-size: 0.85rem; - color: var(--text-muted); - margin-bottom: 0.5rem; -} - -.school-details { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-bottom: 1rem; - flex-grow: 1; -} - -.age-range { - font-size: 0.75rem; - color: var(--text-secondary); - padding: 0.2rem 0.5rem; - background: var(--bg-secondary); - border-radius: var(--radius-sm); -} - -.school-stats { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 0.5rem; - padding-top: 1rem; - border-top: 1px solid var(--border-color); - margin-top: auto; -} - -.stat { - text-align: center; -} - -.stat-value { - font-size: 1.1rem; - font-weight: 700; - color: var(--text-primary); - display: flex; - align-items: center; - justify-content: center; - gap: 0.25rem; -} - -.stat-value.positive { - color: var(--accent-teal); -} - -.stat-value.negative { - color: var(--accent-coral-dark); -} - -/* Trend indicators */ -.trend-indicator { - font-size: 0.75rem; - cursor: help; -} - -.trend-up { - color: var(--accent-teal); -} - -.trend-down { - color: var(--accent-coral-dark); -} - -.trend-stable { - color: var(--text-muted); - font-size: 0.6rem; -} - -.stat-label { - font-size: 0.65rem; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.03em; -} - -/* School Card Map */ -.school-map { - height: 150px; - margin-top: 1rem; - border-radius: var(--radius-md); - overflow: hidden; - cursor: pointer; - border: 1px solid var(--border-color); - transition: var(--transition); -} - -.school-map:hover { - border-color: var(--accent-coral); - box-shadow: var(--shadow-small); -} - -/* Fullscreen Map Modal */ -.map-modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.8); - z-index: 2000; - display: flex; - align-items: center; - justify-content: center; - padding: 1rem; - animation: fadeIn 0.2s ease; -} - -.map-modal { - background: var(--bg-card); - border-radius: var(--radius-lg); - width: 100%; - max-width: 900px; - max-height: 90vh; - display: flex; - flex-direction: column; - overflow: hidden; - box-shadow: var(--shadow-large); - animation: slideUp 0.3s ease; -} - -.map-modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 1.5rem; - border-bottom: 1px solid var(--border-color); -} - -.map-modal-header h3 { - font-family: var(--font-playfair), 'Playfair Display', Georgia, serif; - font-size: 1.25rem; - font-weight: 600; - color: var(--text-primary); - margin: 0; -} - -.map-modal-close { - background: none; - border: none; - font-size: 1.75rem; - color: var(--text-muted); - cursor: pointer; - padding: 0.25rem 0.5rem; - line-height: 1; - transition: var(--transition); -} - -.map-modal-close:hover { - color: var(--accent-coral-dark); -} - -.map-modal-content { - height: 500px; - width: 100%; -} - @media (max-width: 768px) { - .map-modal { - max-height: 80vh; - } - - .map-modal-content { - height: 400px; - } - - .results-container.map-view { - grid-template-columns: 1fr; - grid-template-rows: 350px auto; - height: auto; - } - - .results-container.map-view .results-map { - height: 350px; - } - - .results-container.map-view .schools-grid { - height: auto; - max-height: 400px; - overflow-y: auto; - } -} - -/* Section Titles */ -.section-title { - font-family: var(--font-playfair), 'Playfair Display', Georgia, serif; - font-size: 2rem; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 0.5rem; -} - -.section-subtitle { - font-size: 1rem; - color: var(--text-secondary); - margin-bottom: 2rem; -} - -/* Compare View */ -.compare-header { - text-align: center; - padding: 2rem 0; -} - -.selected-schools { - display: flex; - flex-wrap: wrap; - gap: 1rem; - margin-bottom: 2rem; - min-height: 100px; - padding: 1.5rem; - background: var(--bg-secondary); - border-radius: var(--radius-lg); - justify-content: center; - align-items: center; -} - -.empty-selection { - text-align: center; - color: var(--text-muted); -} - -.empty-icon { - width: 48px; - height: 48px; - margin: 0 auto 0.5rem; - color: var(--text-muted); -} - -.selected-school-tag { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.6rem 1rem; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - font-size: 0.9rem; - color: var(--text-primary); - animation: slideIn 0.2s ease; -} - -@keyframes slideIn { - from { opacity: 0; transform: scale(0.9); } - to { opacity: 1; transform: scale(1); } -} - -.selected-school-tag .remove { - width: 18px; - height: 18px; - border: none; - background: var(--bg-secondary); - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: var(--transition); -} - -.selected-school-tag .remove:hover { - background: var(--accent-coral); - color: white; -} - -.compare-search-section { - max-width: 500px; - margin: 0 auto 2rem; - position: relative; -} - -.compare-results { - position: absolute; - top: 100%; - left: 0; - right: 0; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - box-shadow: var(--shadow-medium); - max-height: 300px; - overflow-y: auto; - z-index: 50; - display: none; -} - -.compare-results.active { - display: block; -} - -.compare-results-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.6rem 1rem; - background: var(--bg-secondary); - border-bottom: 1px solid var(--border-color); - font-size: 0.8rem; - color: var(--text-muted); - position: sticky; - top: 0; -} - -.compare-results-close { - width: 24px; - height: 24px; - border: none; - background: transparent; - border-radius: var(--radius-sm); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - color: var(--text-muted); - transition: var(--transition); -} - -.compare-results-close:hover { - background: var(--accent-coral); - color: white; -} - -.compare-result-item { - padding: 0.75rem 1rem; - cursor: pointer; - border-bottom: 1px solid var(--border-color); - transition: var(--transition); -} - -.compare-result-item:last-child { - border-bottom: none; -} - -.compare-result-item:hover { - background: var(--bg-secondary); -} - -.compare-result-item .name { - font-weight: 500; - color: var(--text-primary); -} - -.compare-result-item .location { - font-size: 0.8rem; - color: var(--text-muted); -} - -/* Charts Section */ -.charts-section { - margin-top: 2rem; -} - -.metric-selector { - display: flex; - align-items: center; - gap: 1rem; - margin-bottom: 1.5rem; -} - -.metric-selector label { - font-weight: 500; - color: var(--text-secondary); -} - -.chart-container { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); - padding: 2rem; - margin-bottom: 2rem; -} - -/* Data Table */ -.data-table-container { - overflow-x: auto; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-lg); -} - -.data-table { - width: 100%; - border-collapse: collapse; -} - -.data-table th, -.data-table td { - padding: 1rem; - text-align: left; - border-bottom: 1px solid var(--border-color); -} - -.data-table th { - background: var(--bg-secondary); - font-weight: 600; - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-secondary); -} - -.data-table tr:last-child td { - border-bottom: none; -} - -.data-table tr:hover td { - background: rgba(224, 114, 86, 0.03); -} - -/* Rankings View */ -.rankings-header { - text-align: center; - padding: 2rem 0; -} - -.rankings-controls { - display: flex; - justify-content: center; - gap: 1rem; - margin-bottom: 2rem; -} - -.rankings-list { - max-width: 800px; - margin: 0 auto; -} - -.ranking-item { - display: flex; - align-items: center; - gap: 1.5rem; - padding: 1.25rem 1.5rem; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - margin-bottom: 0.75rem; - transition: var(--transition); - cursor: pointer; -} - -.ranking-item:hover { - border-color: var(--accent-coral); - box-shadow: var(--shadow-soft); -} - -.ranking-position { - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - font-weight: 700; - font-size: 1rem; -} - -.ranking-position.top-3 { - background: linear-gradient(135deg, var(--accent-gold), #d4af37); - color: white; -} - -.ranking-position:not(.top-3) { - background: var(--bg-secondary); - color: var(--text-secondary); -} - -.ranking-info { - flex: 1; -} - -.ranking-name { - font-family: var(--font-playfair), 'Playfair Display', Georgia, serif; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 0.25rem; -} - -.ranking-location { - font-size: 0.85rem; - color: var(--text-muted); -} - -.ranking-score { - text-align: right; -} - -.ranking-score-value { - font-size: 1.5rem; - font-weight: 700; - color: var(--accent-teal); -} - -.ranking-score-label { - font-size: 0.7rem; - color: var(--text-muted); - text-transform: uppercase; -} - -/* Modal */ -.modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 2000; - display: none; - align-items: center; - justify-content: center; - padding: 2rem; -} - -.modal.active { - display: flex; -} - -.modal-backdrop { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(26, 22, 18, 0.6); - backdrop-filter: blur(4px); -} - -.modal-content { - position: relative; - background: var(--bg-card); - border-radius: var(--radius-xl); - max-width: 800px; - width: 100%; - max-height: 90vh; - overflow-y: auto; - box-shadow: var(--shadow-strong); - animation: modalIn 0.3s ease; -} - -@keyframes modalIn { - from { opacity: 0; transform: scale(0.95) translateY(20px); } - to { opacity: 1; transform: scale(1) translateY(0); } -} - -.modal-close { - position: absolute; - top: 1rem; - right: 1rem; - width: 40px; - height: 40px; - border: none; - background: var(--bg-secondary); - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: var(--transition); - z-index: 10; -} - -.modal-close:hover { - background: var(--accent-coral); - color: white; -} - -.modal-close svg { - width: 20px; - height: 20px; -} - -.modal-header { - position: relative; - padding: 2rem 2rem 1rem; - border-bottom: 1px solid var(--border-color); -} - -.modal-header h2 { - font-family: var(--font-playfair), 'Playfair Display', Georgia, serif; - font-size: 1.75rem; - font-weight: 700; - color: var(--text-primary); - margin-bottom: 0.5rem; - padding-right: 10rem; -} - -.modal-meta { - display: flex; - gap: 0.75rem; - flex-wrap: wrap; -} - -.modal-details { - margin-top: 0.75rem; - font-size: 0.875rem; - color: var(--text-secondary); -} - -.modal-details .modal-address { - margin-bottom: 0.25rem; -} - -.modal-compare-btn { - position: absolute; - top: 1rem; - right: 4rem; -} - -.modal-details .modal-age-range { - color: var(--text-muted); -} - -.modal-body { - padding: 2rem; -} - -.modal-chart-container { - margin-bottom: 2rem; -} - -.modal-stats { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.modal-stats-section { - padding: 1rem; - background: var(--bg-secondary); - border-radius: var(--radius-md); -} - -.modal-stats-section h4 { - margin: 0 0 0.75rem 0; - font-size: 0.85rem; - font-weight: 600; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.modal-stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); - gap: 0.75rem; -} - -.modal-stat { - text-align: center; - padding: 1rem; - background: var(--bg-secondary); - border-radius: var(--radius-md); -} - -.modal-stat-value { - font-size: 1.5rem; - font-weight: 700; - color: var(--text-primary); -} - -.modal-stat-label { - font-size: 0.75rem; - color: var(--text-muted); - text-transform: uppercase; - margin-top: 0.25rem; -} - -.modal-map-container { - margin-top: 2rem; -} - -.modal-map-container h4 { - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 0.75rem; -} - -.modal-map { - height: 200px; - border-radius: var(--radius-md); - overflow: hidden; - cursor: pointer; -} - - -/* Buttons */ -.btn { - padding: 0.75rem 1.5rem; - font-size: 0.9rem; - font-family: inherit; - font-weight: 600; - border: none; - border-radius: var(--radius-md); - cursor: pointer; - transition: var(--transition); -} - -.btn-primary { - background: var(--accent-coral-dark); - color: white; -} - -.btn-primary:hover { - background: var(--accent-coral-darker); - transform: translateY(-1px); -} - -/* Footer */ -.footer { - margin-top: 3rem; - border-top: 1px solid var(--border-color); - color: var(--text-muted); - font-size: 0.85rem; -} - -.footer-content { - max-width: 600px; - margin: 0 auto; - padding: 2rem; -} - -.footer-contact { - margin-bottom: 1.5rem; - text-align: center; -} - -.footer-source { - text-align: center; - padding-top: 1.5rem; - border-top: 1px solid var(--border-color); -} - -.footer a { - color: var(--accent-teal); - text-decoration: none; -} - -.footer a:hover { - text-decoration: underline; -} - -/* Loading State */ -.loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 3rem; - color: var(--text-muted); -} - -.loading-spinner { - width: 40px; - height: 40px; - border: 3px solid var(--border-color); - border-top-color: var(--accent-coral); - border-radius: 50%; - animation: spin 0.8s linear infinite; - margin-bottom: 1rem; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* Empty State */ -.empty-state { - text-align: center; - padding: 4rem 2rem; - color: var(--text-muted); -} - -.empty-state svg { - width: 64px; - height: 64px; - margin-bottom: 1rem; - opacity: 0.5; -} - -/* Skeleton Loading */ -.skeleton { - pointer-events: none; -} - -.school-card.skeleton { - background: var(--bg-card); - padding: 1.5rem; -} - -.skeleton-line { - height: 1rem; - background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--border-color) 50%, var(--bg-secondary) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s infinite; - border-radius: var(--radius-sm); - margin-bottom: 0.75rem; -} - -.skeleton-line.short { - height: 0.75rem; -} - -.ranking-item.skeleton { - background: var(--bg-card); -} - -.skeleton-circle { - width: 40px; - height: 40px; - border-radius: 50%; - background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--border-color) 50%, var(--bg-secondary) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s infinite; -} - -.skeleton-content { - flex: 1; - padding: 0 1rem; -} - -.skeleton-score { - width: 60px; - height: 40px; - background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--border-color) 50%, var(--bg-secondary) 75%); - background-size: 200% 100%; - animation: shimmer 1.5s infinite; - border-radius: var(--radius-sm); -} - -@keyframes shimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } -} - -/* Pagination */ -.pagination-info { - grid-column: 1 / -1; - display: flex; - justify-content: center; - align-items: center; - gap: 1rem; - padding: 2rem; - color: var(--text-muted); - font-size: 0.9rem; -} - -.btn-load-more { - padding: 0.6rem 1.5rem; - font-size: 0.9rem; - font-family: inherit; - font-weight: 500; - border: 2px solid var(--accent-coral); - border-radius: var(--radius-md); - background: transparent; - color: var(--accent-coral); - cursor: pointer; - transition: var(--transition); -} - -.btn-load-more:hover { - background: var(--accent-coral); - color: white; -} - -/* Responsive */ -@media (max-width: 768px) { - .header-content { - flex-direction: column; - gap: 1rem; - } - - .nav { - width: 100%; - justify-content: center; - } - .main { padding: 1rem; } - - .hero-title { - font-size: 1.75rem; - } - - .filter-row { - flex-direction: column; - } - - .location-input-group { - flex-direction: column; - width: 100%; - } - - .postcode-input { - max-width: 100%; - width: 100%; - } - - .radius-select { - width: 100%; - } - - .location-btn, - .location-clear-btn { - width: 100%; - justify-content: center; - } - - .schools-grid { - grid-template-columns: 1fr; - } - - .modal-content { - margin: 1rem; - max-height: calc(100dvh - 2rem); - } - - .modal-header { - display: flex; - flex-direction: column; - } - - .modal-header h2 { - padding-right: 0; - order: 1; - } - - .modal-meta { - order: 2; - } - - .modal-details { - order: 3; - } - - .modal-compare-btn { - position: static; - order: 4; - margin-top: 1rem; - width: 100%; - } - - .modal-body { - padding: 1rem; - } - - .modal-chart-container { - margin-bottom: 1.5rem; - padding: 0.5rem; - background: var(--bg-secondary); - border-radius: var(--radius-md); - } - - .modal-chart-container canvas { - max-height: 280px; - } - - .modal-stats-grid { - grid-template-columns: repeat(2, 1fr); - gap: 0.5rem; - } - - .modal-stat { - padding: 0.75rem 0.5rem; - } - - .modal-stat-value { - font-size: 1.25rem; - } - - .modal-stat-label { - font-size: 0.65rem; - } - - .rankings-controls { - flex-direction: column; - align-items: stretch; - } } -/* Extra small screens */ -@media (max-width: 480px) { - .modal-content { - margin: 0.5rem; - max-height: calc(100dvh - 1rem); +/* Honour the OS setting. Transitions collapse to near-instant rather than + zero so state changes still register as changes. */ +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; } - - .modal-header { - padding: 1rem; - } - - .modal-header h2 { - font-size: 1.25rem; - } - - .modal-chart-container canvas { - max-height: 260px; - } - - .modal-stats-grid { - grid-template-columns: repeat(2, 1fr); - } - - .modal-stat-value { - font-size: 1.1rem; - } - - .modal-map { - height: 160px; + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; } } - -/* ============================================================================= - TOOLTIP SYSTEM - ============================================================================= */ - -/* Info Icon Trigger */ -.info-trigger { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - margin-left: 0.25rem; - background: none; - border: none; - cursor: help; - color: var(--text-muted); - opacity: 0.6; - transition: var(--transition); - vertical-align: middle; - border-radius: 50%; -} - -.info-trigger:hover, -.info-trigger:focus { - color: var(--accent-teal); - opacity: 1; -} - -.info-trigger:focus { - outline: none; - box-shadow: 0 0 0 2px var(--accent-teal); -} - -.info-trigger:focus:not(:focus-visible) { - box-shadow: none; -} - -.info-trigger:focus-visible { - box-shadow: 0 0 0 2px var(--accent-teal); -} - -/* Info Icon SVG */ -.info-icon { - width: 10px; - height: 10px; - flex-shrink: 0; -} - -.modal-stat-label .info-icon { - width: 14px; - height: 14px; -} - -/* Tooltip Container */ -.tooltip { - position: absolute; - z-index: 3000; - min-width: 200px; - max-width: 280px; - padding: 0.75rem 1rem; - background: var(--bg-accent); - color: var(--text-inverse); - border-radius: var(--radius-md); - box-shadow: var(--shadow-medium); - font-family: var(--font-dm-sans), 'DM Sans', sans-serif; - font-size: 0.8125rem; - line-height: 1.5; - text-transform: none; - letter-spacing: normal; - text-align: left; - opacity: 0; - visibility: hidden; - transition: opacity 150ms ease, visibility 150ms ease; - pointer-events: none; -} - -.tooltip.visible { - opacity: 1; - visibility: visible; - pointer-events: auto; -} - -/* Tooltip Arrow - Top Placement (arrow points down) */ -.tooltip[data-placement="top"]::after { - content: ''; - position: absolute; - top: 100%; - left: 50%; - transform: translateX(-50%); - border: 8px solid transparent; - border-top-color: var(--bg-accent); -} - -/* Tooltip Arrow - Bottom Placement (arrow points up) */ -.tooltip[data-placement="bottom"]::after { - content: ''; - position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(-50%); - border: 8px solid transparent; - border-bottom-color: var(--bg-accent); -} - -/* Tooltip Title */ -.tooltip-title { - font-weight: 600; - margin-bottom: 0.25rem; - font-size: 0.875rem; -} - -/* Tooltip Note (for context like national average) */ -.tooltip-note { - margin-top: 0.5rem; - padding-top: 0.5rem; - border-top: 1px solid rgba(250, 247, 242, 0.2); - font-size: 0.75rem; - opacity: 0.8; -} - -/* Warning Trigger Button */ -.warning-trigger { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - margin-left: 0.5rem; - background: none; - border: none; - cursor: help; - color: var(--accent-coral); - opacity: 0.8; - transition: var(--transition); - vertical-align: middle; - border-radius: 4px; -} - -.warning-trigger:hover, -.warning-trigger:focus { - opacity: 1; -} - -.warning-trigger:focus { - outline: none; - box-shadow: 0 0 0 2px var(--accent-coral); -} - -.warning-trigger:focus:not(:focus-visible) { - box-shadow: none; -} - -.warning-trigger:focus-visible { - box-shadow: 0 0 0 2px var(--accent-coral); -} - -/* Warning Icon SVG */ -.warning-icon { - width: 14px; - height: 14px; - flex-shrink: 0; -} - -/* Warning Tooltip Styling */ -.tooltip.tooltip-warning { - background: #8b4513; - border-left: 3px solid var(--accent-coral); -} - -.tooltip.tooltip-warning[data-placement="top"]::after { - border-top-color: #8b4513; -} - -.tooltip.tooltip-warning[data-placement="bottom"]::after { - border-bottom-color: #8b4513; -} - -/* Label wrapper for inline icon */ -.stat-label-with-info { - display: inline-flex; - align-items: center; - justify-content: center; -} diff --git a/nextjs-app/app/icon.svg b/nextjs-app/app/icon.svg new file mode 100644 index 0000000..d9ebdf2 --- /dev/null +++ b/nextjs-app/app/icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/nextjs-app/app/layout.tsx b/nextjs-app/app/layout.tsx index c72cd69..1d979ec 100644 --- a/nextjs-app/app/layout.tsx +++ b/nextjs-app/app/layout.tsx @@ -1,5 +1,5 @@ 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 { Navigation } from '@/components/Navigation'; import { Footer } from '@/components/Footer'; @@ -7,17 +7,23 @@ import { ComparisonToast } from '@/components/ComparisonToast'; import { ComparisonProvider } from '@/context/ComparisonProvider'; 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'], weight: ['400', '500', '600', '700'], - variable: '--font-dm-sans', + variable: '--font-schibsted', 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'], - weight: ['600', '700'], - variable: '--font-playfair', + weight: ['400', '600'], + style: ['normal', 'italic'], + variable: '--font-literata', display: 'swap', }); @@ -27,9 +33,11 @@ export const viewport: Viewport = { // viewport-fit=cover lets us paint behind the notch / Dynamic Island so // env(safe-area-inset-*) values resolve to real numbers on iPhone. 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: [ - { media: '(prefers-color-scheme: light)', color: '#faf7f2' }, - { media: '(prefers-color-scheme: dark)', color: '#1a1612' }, + { media: '(prefers-color-scheme: light)', color: '#f2f1ed' }, + { 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', authors: [{ name: 'SchoolCompare' }], manifest: '/manifest.json', - icons: { - icon: '/favicon.svg', - shortcut: '/favicon.svg', - apple: '/favicon.svg', - }, + // No `icons` key on purpose: setting it here would override the file + // conventions. app/icon.svg and app/apple-icon.tsx are the source, and + // app/opengraph-image.tsx supplies og:image and twitter:image. + metadataBase: new URL('https://schoolcompare.co.uk'), openGraph: { type: 'website', title: 'SchoolCompare | Compare School Performance', @@ -60,7 +67,8 @@ export const metadata: Metadata = { siteName: 'SchoolCompare', }, twitter: { - card: 'summary', + // summary_large_image now that there is an image worth showing. + card: 'summary_large_image', title: 'SchoolCompare | Compare School Performance', description: 'Compare primary and secondary school SATs and GCSE performance across England', }, @@ -94,8 +102,7 @@ export default function RootLayout({ strategy="afterInteractive" /> - -
+ Skip to main content diff --git a/nextjs-app/app/opengraph-image.tsx b/nextjs-app/app/opengraph-image.tsx new file mode 100644 index 0000000..f79e1ec --- /dev/null +++ b/nextjs-app/app/opengraph-image.tsx @@ -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( + ( +
+ {/* Lockup */} +
+
+ {BARS.map(([x, y, w, h, isSchool], i) => ( +
+ ))} +
+
+ School + Compare +
+
+ + {/* Proposition. Sized so the headline holds two lines and the subhead + one — an orphaned word reads as an accident at this size. */} +
+
+ A score means nothing until you see the spread. +
+
+ 24,000+ schools in England · SATs, GCSEs, Ofsted and admissions +
+
+ + {/* The device, at poster scale. Same object as the mark above. */} +
+
+ {SPREAD.map((h, i) => ( +
+ ))} +
+
+ NATIONAL SPREAD + schoolcompare.co.uk +
+
+
+ ), + { + ...size, + fonts: [ + { name: 'Schibsted', data: regular, weight: 400, style: 'normal' }, + { name: 'Schibsted', data: bold, weight: 700, style: 'normal' }, + ], + } + ); +} diff --git a/nextjs-app/assets/SchibstedGrotesk-Bold.ttf b/nextjs-app/assets/SchibstedGrotesk-Bold.ttf new file mode 100644 index 0000000..5425be0 Binary files /dev/null and b/nextjs-app/assets/SchibstedGrotesk-Bold.ttf differ diff --git a/nextjs-app/assets/SchibstedGrotesk-Regular.ttf b/nextjs-app/assets/SchibstedGrotesk-Regular.ttf new file mode 100644 index 0000000..97ef234 Binary files /dev/null and b/nextjs-app/assets/SchibstedGrotesk-Regular.ttf differ diff --git a/nextjs-app/components/AdmissionsTrendChart.tsx b/nextjs-app/components/AdmissionsTrendChart.tsx index b5d2d71..0d279f2 100644 --- a/nextjs-app/components/AdmissionsTrendChart.tsx +++ b/nextjs-app/components/AdmissionsTrendChart.tsx @@ -9,6 +9,7 @@ import { Line } from 'react-chartjs-2'; import { ChartOptions } from 'chart.js'; import '@/lib/chartSetup'; +import { useThemeTokens, alpha } from '@/lib/theme'; import { formatAcademicYear } from '@/lib/utils'; import { fillAcademicYears } from '@/lib/compareChartData'; 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 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'> = { responsive: true, maintainAspectRatio: false, @@ -49,7 +55,9 @@ export default function AdmissionsTrendChart({ history }: { history: SchoolAdmis legend: { display: false }, title: { display: false }, tooltip: { - backgroundColor: 'rgba(26,22,18,0.92)', + backgroundColor: cInverse, + titleColor: cInverseText, + bodyColor: cInverseText, padding: 10, titleFont: { size: 12 }, bodyFont: { size: 12 }, @@ -62,7 +70,7 @@ export default function AdmissionsTrendChart({ history }: { history: SchoolAdmis y: { min: yMin, max: yMax, - grid: { color: 'rgba(0,0,0,0.05)' }, + grid: { color: cGrid }, ticks: { font: { size: 11 }, maxTicksLimit: 5, callback: (v) => `${v}%` }, }, x: { @@ -80,14 +88,14 @@ export default function AdmissionsTrendChart({ history }: { history: SchoolAdmis data: values, clip: false as const, spanGaps: false, - borderColor: '#e07256', - backgroundColor: 'rgba(224,114,86,0.10)', + borderColor: cBrand, + backgroundColor: alpha('--brand', 0.10), borderWidth: 2.5, tension: 0.3, fill: true, pointRadius: values.map((_, i) => (i === lastIdx ? 5 : 3)), - pointBackgroundColor: '#e07256', - pointBorderColor: '#fff', + pointBackgroundColor: cBrand, + pointBorderColor: cCard, pointBorderWidth: values.map((_, i) => (i === lastIdx ? 2 : 0)), pointHoverRadius: 6, }, diff --git a/nextjs-app/components/AdmissionsView.module.css b/nextjs-app/components/AdmissionsView.module.css index 06b457d..f80a0c5 100644 --- a/nextjs-app/components/AdmissionsView.module.css +++ b/nextjs-app/components/AdmissionsView.module.css @@ -27,7 +27,7 @@ font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-bottom: 0.85rem; padding-left: 0.85rem; } @@ -39,7 +39,7 @@ display: flex; flex-direction: column; gap: 0.1rem; - border-left: 2px solid var(--border-color, #e5dfd5); + border-left: 2px solid var(--border); } .navLink { @@ -51,33 +51,33 @@ border-left: 2px solid transparent; font-size: 0.88rem; font-weight: 500; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); text-decoration: none; cursor: pointer; transition: color 0.15s ease, border-color 0.15s ease; } .navLink:hover { - color: var(--text-primary, #1a1612); + color: var(--text-primary); } .navLinkActive { - color: var(--accent-teal, #2d7d7d); + color: var(--brand); font-weight: 700; - border-left-color: var(--accent-teal, #2d7d7d); + border-left-color: var(--brand); } .navDot { width: 6px; height: 6px; border-radius: 50%; - background: var(--border-color, #e5dfd5); + background: var(--border); flex-shrink: 0; transition: background 0.15s ease; } .navLinkActive .navDot { - background: var(--accent-teal, #2d7d7d); + background: var(--brand); } /* ─── Hero ───────────────────────────────────────────── */ @@ -95,8 +95,8 @@ font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; - color: var(--accent-teal, #2d7d7d); - background: rgba(45, 125, 125, 0.1); + color: var(--brand); + background: rgba(var(--brand-rgb), 0.1); padding: 0.3rem 0.7rem; border-radius: 999px; margin-bottom: 1rem; @@ -107,22 +107,22 @@ width: 6px; height: 6px; border-radius: 50%; - background: var(--accent-teal, #2d7d7d); + background: var(--brand); } .heroTitle { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-size: 2.75rem; font-weight: 700; line-height: 1.1; letter-spacing: -0.015em; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.85rem; } .heroSub { font-size: 1.05rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); max-width: 640px; margin: 0 auto; line-height: 1.55; @@ -132,7 +132,7 @@ .countdownSection { padding: 0 0 2.5rem; - border-bottom: 1px solid var(--border-color, #e5dfd5); + border-bottom: 1px solid var(--border); margin-bottom: 2.5rem; } @@ -145,7 +145,7 @@ font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } .countdownRail { @@ -155,11 +155,11 @@ } .chip { - background: var(--bg-card, #fff); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 12px; 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; flex-direction: column; gap: 0.2rem; @@ -177,12 +177,12 @@ border-radius: 12px 12px 0 0; } -.chipDeadline::before { background: var(--accent-coral, #e07256); } -.chipOffer::before { background: var(--accent-teal, #2d7d7d); } +.chipDeadline::before { background: var(--brand); } +.chipOffer::before { background: var(--status-above); } .chipUrgent { - border-color: rgba(224, 114, 86, 0.4); - background: rgba(224, 114, 86, 0.04); + border-color: rgba(var(--brand-rgb), 0.4); + background: rgba(var(--brand-rgb), 0.04); } .chipTrack { @@ -196,8 +196,8 @@ margin-bottom: 0.15rem; } -.chipTrackDeadline { color: var(--accent-coral-dark, #b04a2e); } -.chipTrackOffer { color: var(--accent-teal, #2d7d7d); } +.chipTrackDeadline { color: var(--brand-strong); } +.chipTrackOffer { color: var(--status-above); } .chipTrackDot { width: 5px; @@ -208,7 +208,8 @@ } .chipDays { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-variant-numeric: tabular-nums; + font-family: var(--font-display); font-size: 2.6rem; font-weight: 700; line-height: 1; @@ -217,17 +218,17 @@ .chipDeadline .chipDays, .chipUrgent .chipDays { - color: var(--accent-coral-dark, #c45a3f); + color: var(--brand-strong); } .chipOffer .chipDays { - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); } .chipDaysUnit { font-size: 0.78rem; font-weight: 500; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-left: 0.2rem; vertical-align: bottom; line-height: 2; @@ -236,14 +237,14 @@ .chipMilestone { font-size: 0.85rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); line-height: 1.25; margin-top: 0.1rem; } .chipDate { font-size: 0.75rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-top: 0.05rem; } @@ -251,8 +252,8 @@ .track { margin-bottom: 3rem; - background: var(--bg-card, #fff); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 14px; overflow: hidden; } @@ -263,7 +264,7 @@ justify-content: space-between; gap: 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; } @@ -278,22 +279,22 @@ font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); margin-bottom: 0.35rem; } .trackTitle { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-size: 1.5rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.4rem; line-height: 1.2; } .trackSub { font-size: 0.9rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); line-height: 1.5; margin: 0; } @@ -317,13 +318,13 @@ font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } .trackDateVal { font-size: 0.88rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); } /* ─── Timeline ───────────────────────────────────────── */ @@ -356,27 +357,27 @@ width: 14px; height: 14px; border-radius: 50%; - background: var(--bg-card, #fff); - border: 2px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 2px solid var(--border); flex-shrink: 0; transition: border-color 0.2s ease, background 0.2s ease; } .stepDeadline .stepDot { - background: rgba(224, 114, 86, 0.15); - border-color: var(--accent-coral, #e07256); + background: rgba(var(--brand-rgb), 0.15); + border-color: var(--brand); } .stepOffer .stepDot { - background: rgba(45, 125, 125, 0.15); - border-color: var(--accent-teal, #2d7d7d); + background: rgba(var(--status-above-rgb), 0.15); + border-color: var(--status-above); } .stepLine { width: 2px; flex: 1; min-height: 24px; - background: var(--border-color, #e5dfd5); + background: var(--border); margin: 4px 0; } @@ -395,27 +396,28 @@ font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-bottom: 0.2rem; } -.stepDeadline .stepDate { color: var(--accent-coral-dark, #b04a2e); } -.stepOffer .stepDate { color: var(--accent-teal, #2d7d7d); } +.stepDeadline .stepDate { color: var(--brand-strong); } +.stepOffer .stepDate { color: var(--status-above); } .stepTitle { font-size: 0.95rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.3rem; line-height: 1.3; } -.stepDeadline .stepTitle { color: var(--accent-coral-dark, #c45a3f); } -.stepOffer .stepTitle { color: var(--accent-teal, #2d7d7d); } +.stepDeadline .stepTitle { color: var(--brand-strong); } +.stepOffer .stepTitle { color: var(--status-above); } .stepBody { + font-family: var(--font-prose); font-size: 0.88rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); line-height: 1.55; margin: 0; } @@ -427,13 +429,13 @@ margin-top: 0.6rem; font-size: 0.82rem; font-weight: 600; - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); text-decoration: none; transition: gap 0.2s ease, color 0.2s ease; } .stepTool:hover { - color: var(--accent-coral-dark, #c45a3f); + color: var(--brand-strong); gap: 0.6rem; } @@ -455,9 +457,9 @@ display: flex; gap: 0.55rem; padding: 0.6rem 0.75rem; - background: var(--accent-gold-bg, rgba(201, 162, 39, 0.12)); - border: 1px solid rgba(201, 162, 39, 0.28); - border-left: 3px solid var(--accent-gold, #c9a227); + background: var(--status-below-bg); + border: 1px solid rgba(var(--status-below-rgb), 0.28); + border-left: 3px solid var(--status-below); border-radius: 8px; } @@ -467,8 +469,8 @@ height: 1.05rem; margin-top: 0.1rem; border-radius: 50%; - background: var(--accent-gold, #c9a227); - color: #fff; + background: var(--status-below); + color: var(--text-inverse); font-size: 0.72rem; font-weight: 800; line-height: 1.05rem; @@ -482,14 +484,15 @@ .calloutTitle { font-size: 0.82rem; font-weight: 700; - color: var(--accent-gold-text, #7a6800); + color: var(--status-below); line-height: 1.3; margin-bottom: 0.15rem; } .calloutBody { + font-family: var(--font-prose); font-size: 0.82rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); line-height: 1.5; margin: 0; } @@ -501,10 +504,10 @@ } .tipsHeading { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-size: 1.35rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 1.25rem; } @@ -515,16 +518,17 @@ } .tipCard { - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 12px; padding: 1.25rem; } .tipNumber { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-variant-numeric: tabular-nums; + font-family: var(--font-display); font-size: 1.75rem; 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; margin-bottom: 0.6rem; } @@ -532,14 +536,15 @@ .tipHeading { font-size: 0.95rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.4rem; line-height: 1.3; } .tipBody { + font-family: var(--font-prose); font-size: 0.875rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); line-height: 1.55; margin: 0; } @@ -557,8 +562,8 @@ position: sticky; top: 0; padding-top: 0; - background: var(--bg-primary, #faf8f3); - border-bottom: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-primary); + border-bottom: 1px solid var(--border); margin: 0 -1.25rem 1rem; padding: 0.6rem 1.25rem; z-index: 10; @@ -590,8 +595,8 @@ } .navLinkActive { - background: var(--text-primary, #1a1612); - color: #fff; + background: var(--text-primary); + color: var(--text-inverse); border-left: none; } diff --git a/nextjs-app/components/ComparisonChart.module.css b/nextjs-app/components/ComparisonChart.module.css index 43acaf0..3fec9a1 100644 --- a/nextjs-app/components/ComparisonChart.module.css +++ b/nextjs-app/components/ComparisonChart.module.css @@ -43,7 +43,7 @@ min-height: 40px; min-width: 0; padding: 4px 10px; - border: 1px solid rgba(0, 0, 0, .12); + border: 1px solid var(--border); border-radius: 999px; background: transparent; cursor: pointer; @@ -52,8 +52,8 @@ } .chip[aria-pressed="true"] { - background: rgba(0, 0, 0, .06); - border-color: rgba(0, 0, 0, .35); + background: var(--bg-secondary); + border-color: var(--border-strong); } .chipDot { diff --git a/nextjs-app/components/ComparisonChart.tsx b/nextjs-app/components/ComparisonChart.tsx index 23a96f5..cd8af93 100644 --- a/nextjs-app/components/ComparisonChart.tsx +++ b/nextjs-app/components/ComparisonChart.tsx @@ -15,6 +15,7 @@ import { useEffect, useState } from 'react'; import { Line } from 'react-chartjs-2'; import { ChartOptions, ChartDataset, PointStyle } from 'chart.js'; import '@/lib/chartSetup'; +import { useSeriesColors, useThemeTokens } from '@/lib/theme'; import { buildCompareChart } from '@/lib/compareChartData'; import type { ComparisonData } from '@/lib/types'; 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) { 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(null); // 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 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; return { @@ -90,7 +98,7 @@ export function ComparisonChart({ comparisonData, schools, metric, metricLabel, datasets.push({ label: built.englandDataset.label, data: built.englandDataset.data, - borderColor: 'rgba(109, 104, 95, 0.9)', + borderColor: cRef, backgroundColor: 'transparent', borderWidth: 1.5, borderDash: built.englandDataset.borderDash, @@ -138,7 +146,9 @@ export function ComparisonChart({ comparisonData, schools, metric, metricLabel, display: false, }, tooltip: { - backgroundColor: 'rgba(0, 0, 0, 0.8)', + backgroundColor: cInverse, + titleColor: cInverseText, + bodyColor: cInverseText, padding: isMobile ? 10 : 12, titleFont: { size: isMobile ? 12 : 14, @@ -182,7 +192,7 @@ export function ComparisonChart({ comparisonData, schools, metric, metricLabel, ...(isMobile && { maxTicksLimit: 5 }), }, grid: { - color: 'rgba(0, 0, 0, 0.05)', + color: cGrid, }, }, x: { diff --git a/nextjs-app/components/ComparisonToast.module.css b/nextjs-app/components/ComparisonToast.module.css index fea8a4f..cbdf970 100644 --- a/nextjs-app/components/ComparisonToast.module.css +++ b/nextjs-app/components/ComparisonToast.module.css @@ -23,11 +23,11 @@ flex-direction: column; gap: 0; padding: 1rem 1.25rem; - background: var(--bg-primary, #faf7f2); - color: var(--text-primary, #2c2420); + background: var(--bg-primary); + color: var(--text-primary); border-radius: 16px; - box-shadow: 0 8px 32px rgba(44, 36, 32, 0.18), 0 2px 8px rgba(44, 36, 32, 0.08); - border: 1px solid var(--border-color, #e8ddd4); + 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); min-width: 280px; } @@ -37,8 +37,8 @@ justify-content: center; width: 28px; height: 28px; - background: var(--accent-coral, #e07256); - color: white; + background: var(--brand); + color: var(--brand-on); border-radius: 50%; font-weight: 700; font-size: 0.9rem; @@ -62,13 +62,13 @@ gap: 0.5rem; font-weight: 600; font-size: 0.875rem; - color: var(--text-primary, #2c2420); + color: var(--text-primary); } .collapseBtn { background: none; border: none; - color: var(--text-muted, #8a7a72); + color: var(--text-muted); cursor: pointer; padding: 0.25rem; line-height: 1; @@ -79,7 +79,7 @@ } .collapseBtn:hover { - color: var(--text-primary, #2c2420); + color: var(--text-primary); } .schoolList { @@ -95,13 +95,13 @@ justify-content: space-between; gap: 0.5rem; padding: 0.3rem 0.5rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: var(--radius-sm, 4px); } .schoolName { font-size: 0.8rem; - color: var(--text-primary, #2c2420); + color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -111,7 +111,7 @@ .removeSchoolBtn { background: none; border: none; - color: var(--text-muted, #8a7a72); + color: var(--text-muted); cursor: pointer; font-size: 1rem; padding: 0 0.25rem; @@ -121,7 +121,7 @@ } .removeSchoolBtn:hover { - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); } .toastActions { @@ -129,13 +129,13 @@ align-items: center; gap: 0.75rem; padding-top: 0.625rem; - border-top: 1px solid var(--border-color, #e8ddd4); + border-top: 1px solid var(--border); } .btnClearAll { background: none; border: none; - color: var(--text-muted, #8a7a72); + color: var(--text-muted); font-size: 0.85rem; font-weight: 500; cursor: pointer; @@ -145,13 +145,13 @@ } .btnClearAll:hover { - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); } .btnCompare { flex: 1; - background: var(--accent-coral, #e07256); - color: white; + background: var(--brand); + color: var(--brand-on); padding: 0.6rem 1.25rem; border-radius: 25px; font-weight: 600; @@ -164,7 +164,7 @@ .btnCompare:hover { transform: translateY(-1px); - background: var(--accent-coral-dark, #c9614a); + background: var(--brand-strong); } /* Hidden on phones — the bottom tab bar's Compare badge already diff --git a/nextjs-app/components/ComparisonView.module.css b/nextjs-app/components/ComparisonView.module.css index 932b132..dfd5611 100644 --- a/nextjs-app/components/ComparisonView.module.css +++ b/nextjs-app/components/ComparisonView.module.css @@ -18,14 +18,14 @@ .header h1 { font-size: 2.25rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.5rem; - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); } .subtitle { font-size: 1rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); margin: 0; line-height: 1.6; max-width: 60ch; @@ -43,7 +43,7 @@ display: flex; gap: 0; margin-bottom: 1.5rem; - border: 1px solid var(--border-color, #e5dfd5); + border: 1px solid var(--border); border-radius: 8px; overflow: hidden; width: fit-content; @@ -53,8 +53,8 @@ padding: 0.625rem 1.5rem; font-size: 0.9375rem; font-weight: 500; - background: var(--bg-card, white); - color: var(--text-secondary, #5c564d); + background: var(--bg-card); + color: var(--text-secondary); border: none; cursor: pointer; transition: all 0.15s ease; @@ -62,21 +62,21 @@ } .phaseTab:not(:last-child) { - border-right: 1px solid var(--border-color, #e5dfd5); + border-right: 1px solid var(--border); } .phaseTab:hover { - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); } .phaseTabActive { - background: var(--accent-coral-dark, #b04a2e); - color: white; + background: var(--brand-strong); + color: var(--brand-on); font-weight: 600; } .phaseTabActive:hover { - background: var(--accent-coral-darker, #9c3f26); + background: var(--brand-stronger); } /* Sticky school bar — column identity while scrolling; horizontal scroll on @@ -87,23 +87,23 @@ position: sticky; top: 65px; z-index: 10; - background: var(--bg-primary, #faf7f2); + background: var(--bg-primary); display: flex; gap: 0.75rem; overflow-x: auto; padding: 0.75rem 0; - border-bottom: 1px solid var(--border-light, #e5dfd5); + border-bottom: 1px solid var(--border); -webkit-overflow-scrolling: touch; } .schoolChip { flex: 1 1 0; min-width: 180px; - background: var(--bg-card, white); - border: 1px solid var(--border-light, #e5dfd5); - border-top: 3px solid var(--accent-coral, #e07256); + background: var(--bg-card); + border: 1px solid var(--border); + border-top: 3px solid var(--brand); 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; display: flex; gap: 0.55rem; @@ -126,12 +126,12 @@ font-weight: 600; font-size: 0.92rem; line-height: 1.25; - color: var(--text-primary, #1a1612); + color: var(--text-primary); text-decoration: none; } .chipName:hover { - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); } /* Full name on desktop, short name on the compact mobile pills. */ @@ -142,7 +142,7 @@ .chipMeta { display: block; font-size: 0.78rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -185,22 +185,22 @@ font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } .barCaptionCount { font-size: 0.95rem; font-weight: 600; line-height: 1.3; - color: var(--text-primary, #1a1612); + color: var(--text-primary); } } .chipRemove { margin-left: auto; border: none; - background: var(--bg-secondary, #f3ede4); - color: var(--text-muted, #6d685f); + background: var(--bg-secondary); + color: var(--text-muted); border-radius: 50%; width: 22px; height: 22px; @@ -212,9 +212,9 @@ .footnote { font-size: 0.78rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-top: 2.5rem; - border-top: 1px solid var(--border-light, #e5dfd5); + border-top: 1px solid var(--border); padding-top: 1rem; max-width: 75ch; } diff --git a/nextjs-app/components/DeltaChip.module.css b/nextjs-app/components/DeltaChip.module.css index 01ca30b..974270e 100644 --- a/nextjs-app/components/DeltaChip.module.css +++ b/nextjs-app/components/DeltaChip.module.css @@ -23,16 +23,16 @@ } .good { - background: var(--accent-teal-bg, rgba(45, 125, 125, 0.12)); - color: var(--accent-teal, #2d7d7d); + background: var(--status-above-bg); + color: var(--status-above); } .bad { - background: var(--accent-coral-bg, rgba(224, 114, 86, 0.12)); - color: var(--accent-coral-dark, #b04a2e); + background: var(--brand-bg); + color: var(--brand-strong); } .neutral { - background: var(--bg-secondary, #f3ede4); - color: var(--text-muted, #8a847a); + background: var(--bg-secondary); + color: var(--text-muted); } diff --git a/nextjs-app/components/DotStrip.module.css b/nextjs-app/components/DotStrip.module.css index 2b7f380..0742d3e 100644 --- a/nextjs-app/components/DotStrip.module.css +++ b/nextjs-app/components/DotStrip.module.css @@ -61,7 +61,7 @@ border-radius: 50%; transform: translateX(-50%); 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 { diff --git a/nextjs-app/components/EmptyState.module.css b/nextjs-app/components/EmptyState.module.css index c2cfa38..28f64b6 100644 --- a/nextjs-app/components/EmptyState.module.css +++ b/nextjs-app/components/EmptyState.module.css @@ -5,15 +5,15 @@ justify-content: center; padding: 4rem 2rem; text-align: center; - background: var(--bg-card, white); - border: 2px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 2px solid var(--border); border-radius: 16px; 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 { - color: var(--text-muted, #8a847a); + color: var(--text-muted); margin-bottom: 1.5rem; opacity: 0.7; } @@ -22,14 +22,14 @@ margin: 0 0 0.75rem 0; font-size: 1.5rem; font-weight: 700; - color: var(--text-primary, #1a1612); - font-family: var(--font-playfair), 'Playfair Display', serif; + color: var(--text-primary); + font-family: var(--font-display); } .message { margin: 0 0 2rem 0; font-size: 1rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); max-width: 500px; line-height: 1.6; } @@ -38,8 +38,8 @@ padding: 0.875rem 2rem; font-size: 1rem; font-weight: 600; - background: var(--accent-coral, #e07256); - color: white; + background: var(--brand); + color: var(--brand-on); border: none; border-radius: 8px; cursor: pointer; @@ -47,9 +47,9 @@ } .button:hover { - background: var(--accent-coral-dark, #c45a3f); + background: var(--brand-strong); 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 { diff --git a/nextjs-app/components/FilterBar.module.css b/nextjs-app/components/FilterBar.module.css index ac74702..50772b4 100644 --- a/nextjs-app/components/FilterBar.module.css +++ b/nextjs-app/components/FilterBar.module.css @@ -1,10 +1,10 @@ .filterBar { - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 10px; padding: 1.5rem; 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; } @@ -17,9 +17,9 @@ padding: 2.5rem; max-width: 800px; 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-color: var(--accent-coral, #e07256); + border-color: var(--brand); } .heroMode .omniInput { @@ -39,12 +39,12 @@ .searchHint { margin: 0.875rem 0 0; font-size: 0.95rem; - color: var(--text-secondary, #5a554d); + color: var(--text-secondary); text-align: center; } .searchHint strong { - color: var(--text-primary, #1a1612); + color: var(--text-primary); font-weight: 600; } @@ -68,8 +68,8 @@ align-items: center; gap: 0.5rem; padding: 0.625rem 1.375rem; - background: var(--accent-teal, #2d7d7d); - color: #fff; + background: var(--brand); + color: var(--brand-on); border: none; border-radius: 999px; font-size: 0.9375rem; @@ -80,7 +80,7 @@ } .nearMeBtn:hover:not(:disabled) { - background: #235f5f; + background: var(--status-above); transform: translateY(-1px); } @@ -93,8 +93,8 @@ display: inline-block; width: 14px; height: 14px; - border: 2px solid rgba(255, 255, 255, 0.35); - border-top-color: #fff; + border: 2px solid rgba(var(--text-inverse-rgb), 0.35); + border-top-color: var(--brand-on); border-radius: 50%; animation: nearMeSpin 0.7s linear infinite; flex-shrink: 0; @@ -108,7 +108,7 @@ .geoError { font-size: 0.8125rem; - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); margin: 0; max-width: 340px; text-align: center; @@ -134,21 +134,21 @@ flex: 1; padding: 0.875rem 1.25rem; font-size: 1.05rem; - border: 2px solid var(--border-color, #e5dfd5); + border: 2px solid var(--border); border-radius: 8px; outline: none; transition: all 0.2s ease; - background: var(--bg-card, white); + background: var(--bg-card); font-family: inherit; } .omniInput:focus { - border-color: var(--accent-coral, #e07256); - box-shadow: 0 0 0 3px var(--accent-coral-bg); + border-color: var(--brand); + box-shadow: 0 0 0 3px var(--brand-bg); } .omniInput::placeholder { - color: var(--text-muted, #8a847a); + color: var(--text-muted); } .searchButton { @@ -161,9 +161,9 @@ .spinner { width: 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-top-color: white; + border-top-color: var(--brand-on); animation: spin 0.8s linear infinite; } @@ -177,7 +177,7 @@ flex-wrap: wrap; margin-top: 0.625rem; padding-top: 0.625rem; - border-top: 1px solid var(--border-color, #e5dfd5); + border-top: 1px solid var(--border); } .filterSelect { @@ -189,26 +189,26 @@ font-size: 0.875rem; font-weight: 500; font-family: inherit; - border: 1.5px solid var(--border-color, #e5dfd5); + border: 1.5px solid var(--border); 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-repeat: no-repeat; background-position: right 0.875rem center; background-size: 10px 6px; cursor: pointer; outline: none; - color: var(--text-primary, #1a1612); + color: var(--text-primary); transition: border-color 0.15s ease, box-shadow 0.15s ease; } .filterSelect:hover { - border-color: var(--text-muted, #8a847a); + border-color: var(--text-muted); } .filterSelect:focus { - border-color: var(--accent-coral, #e07256); - box-shadow: 0 0 0 3px rgba(224, 114, 86, 0.12); + border-color: var(--brand); + box-shadow: 0 0 0 3px rgba(var(--brand-rgb), 0.12); } .clearButton { @@ -263,7 +263,7 @@ .radiusLabel { font-size: 0.8125rem; font-weight: 500; - color: var(--text-secondary, #5a554d); + color: var(--text-secondary); white-space: nowrap; } @@ -275,7 +275,7 @@ flex-wrap: wrap; margin-top: 0.875rem; padding-top: 0.875rem; - border-top: 1px solid var(--border-color, #e5dfd5); + border-top: 1px solid var(--border); } .controlsRow .advancedToggle { @@ -297,14 +297,14 @@ font-size: 0.8125rem; font-weight: 500; font-family: inherit; - border: 1.5px solid var(--border-color, #e5dfd5); + border: 1.5px solid var(--border); 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-repeat: no-repeat; background-position: right 0.65rem center; background-size: 10px 6px; - color: var(--text-primary, #1a1612); + color: var(--text-primary); cursor: pointer; outline: none; transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease; @@ -312,13 +312,13 @@ } .controlSelect:hover { - border-color: var(--text-muted, #8a847a); - background-color: var(--bg-secondary, #f8f4ef); + border-color: var(--text-muted); + background-color: var(--bg-secondary); } .controlSelect:focus { - border-color: var(--accent-coral, #e07256); - box-shadow: 0 0 0 3px rgba(224, 114, 86, 0.12); + border-color: var(--brand); + box-shadow: 0 0 0 3px rgba(var(--brand-rgb), 0.12); } /* ── Advanced filters toggle ─────────────────────────── */ @@ -328,12 +328,12 @@ align-items: center; gap: 0.375rem; background: none; - border: 1.5px solid var(--border-color, #e5dfd5); + border: 1.5px solid var(--border); border-radius: 999px; padding: 0.4rem 0.875rem; font-size: 0.8125rem; font-weight: 500; - color: var(--text-secondary, #5a554d); + color: var(--text-secondary); cursor: pointer; font-family: inherit; transition: all 0.15s ease; @@ -341,24 +341,24 @@ } .advancedToggle:hover { - border-color: var(--text-muted, #8a847a); - background-color: var(--bg-secondary, #f8f4ef); - color: var(--text-primary, #1a1612); + border-color: var(--text-muted); + background-color: var(--bg-secondary); + color: var(--text-primary); } /* 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. */ .advancedToggleActive { - border-color: var(--accent-coral, #e07256); - background: var(--accent-coral-bg, rgba(224, 114, 86, 0.12)); - color: var(--accent-coral, #e07256); + border-color: var(--brand); + background: var(--brand-bg); + color: var(--brand); font-weight: 600; } .advancedToggleActive:hover { - border-color: var(--accent-coral-dark, #c45a3f); - background: var(--accent-coral-bg, rgba(224, 114, 86, 0.18)); - color: var(--accent-coral-dark, #c45a3f); + border-color: var(--brand-strong); + background: var(--brand-bg); + color: var(--brand-strong); } .chevronDown, diff --git a/nextjs-app/components/Footer.module.css b/nextjs-app/components/Footer.module.css index 2843146..8380a98 100644 --- a/nextjs-app/components/Footer.module.css +++ b/nextjs-app/components/Footer.module.css @@ -1,6 +1,6 @@ .footer { - background: var(--accent-navy, #2c3e50); - color: var(--bg-secondary, #f3ede4); + background: var(--text-primary); + color: var(--bg-secondary); margin-top: auto; } @@ -27,22 +27,22 @@ margin: 0; font-size: 1.25rem; font-weight: 700; - color: var(--bg-primary, #faf7f2); - font-family: var(--font-playfair), 'Playfair Display', serif; + color: var(--bg-primary); + font-family: var(--font-display); } .description { margin: 0; font-size: 0.875rem; line-height: 1.6; - color: rgba(250, 247, 242, 0.7); + color: rgba(var(--text-inverse-rgb), 0.7); } .sectionTitle { margin: 0; font-size: 0.875rem; font-weight: 600; - color: var(--accent-gold, #c9a227); + color: var(--status-below); text-transform: uppercase; letter-spacing: 0.05em; } @@ -58,24 +58,24 @@ .link { font-size: 0.875rem; - color: rgba(250, 247, 242, 0.7); + color: rgba(var(--text-inverse-rgb), 0.7); text-decoration: none; transition: color 0.2s ease; } .link:hover { - color: var(--accent-gold, #c9a227); + color: var(--status-below); } .linkDisabled { font-size: 0.875rem; - color: rgba(250, 247, 242, 0.4); + color: rgba(var(--text-inverse-rgb), 0.4); cursor: not-allowed; } .bottom { 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; flex-direction: column; gap: 0.5rem; @@ -85,15 +85,15 @@ .disclaimer { margin: 0; font-size: 0.875rem; - color: rgba(250, 247, 242, 0.6); + color: rgba(var(--text-inverse-rgb), 0.6); } .disclaimer .link { - color: var(--accent-coral, #e07256); + color: var(--brand); } .disclaimer .link:hover { - color: var(--accent-gold, #c9a227); + color: var(--status-below); } @media (max-width: 768px) { diff --git a/nextjs-app/components/HomeView.module.css b/nextjs-app/components/HomeView.module.css index cbe4fc0..fffce2b 100644 --- a/nextjs-app/components/HomeView.module.css +++ b/nextjs-app/components/HomeView.module.css @@ -16,8 +16,8 @@ font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; - color: var(--accent-teal, #2d7d7d); - background: rgba(45, 125, 125, 0.1); + color: var(--brand); + background: rgba(var(--brand-rgb), 0.1); padding: 0.3rem 0.7rem; border-radius: 999px; margin-bottom: 1rem; @@ -28,37 +28,37 @@ width: 6px; height: 6px; border-radius: 50%; - background: var(--accent-teal, #2d7d7d); + background: var(--brand); } .heroTitle { font-size: 3rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.85rem; line-height: 1.08; letter-spacing: -0.015em; - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); max-width: 840px; margin-left: auto; margin-right: auto; } .heroEmph { - color: var(--accent-coral-dark, #c45a3f); + color: var(--brand-strong); font-style: italic; } .heroDescription { font-size: 1.05rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); margin: 0 auto 0.5rem; max-width: 680px; line-height: 1.55; } .heroDescription strong { - color: var(--text-primary, #1a1612); + color: var(--text-primary); font-weight: 700; } @@ -111,7 +111,7 @@ .viewToggle { display: flex; gap: 0.2rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); padding: 0.2rem; border-radius: 6px; } @@ -127,18 +127,18 @@ border: none; border-radius: 5px; cursor: pointer; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); transition: all 0.2s ease; } .viewToggleBtn:hover { - color: var(--text-primary, #1a1612); + color: var(--text-primary); } .viewToggleBtn.active { - background: var(--bg-card, white); - color: var(--accent-coral-dark, #b04a2e); - box-shadow: 0 2px 4px rgba(26, 22, 18, 0.08); + background: var(--bg-card); + color: var(--brand-strong); + box-shadow: 0 2px 4px rgba(var(--shadow-rgb), 0.08); } .viewToggleBtn svg { @@ -166,7 +166,7 @@ .mapContainer { border-radius: 10px; overflow: hidden; - border: 1px solid var(--border-color, #e5dfd5); + border: 1px solid var(--border); height: 100%; } @@ -184,17 +184,17 @@ } .compactList::-webkit-scrollbar-track { - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 3px; } .compactList::-webkit-scrollbar-thumb { - background: var(--border-color, #e5dfd5); + background: var(--border); border-radius: 3px; } .compactList::-webkit-scrollbar-thumb:hover { - background: var(--text-muted, #8a847a); + background: var(--text-muted); } /* Compact School Item */ @@ -204,15 +204,15 @@ align-items: center; gap: 0.75rem; padding: 0.625rem 0.75rem; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 8px; transition: all 0.2s ease; } .compactItem:hover { - border-color: var(--accent-coral, #e07256); - box-shadow: 0 2px 6px rgba(26, 22, 18, 0.05); + border-color: var(--brand); + box-shadow: 0 2px 6px rgba(var(--shadow-rgb), 0.05); } .compactItemContent { @@ -230,7 +230,7 @@ .compactItemName { font-weight: 600; font-size: 0.8125rem; - color: var(--text-primary, #1a1612); + color: var(--text-primary); text-decoration: none; white-space: nowrap; overflow: hidden; @@ -238,7 +238,7 @@ } .compactItemName:hover { - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); } .distanceBadge { @@ -246,8 +246,8 @@ padding: 0.0625rem 0.375rem; font-size: 0.6875rem; font-weight: 600; - background: var(--accent-teal, #2d7d7d); - color: white; + background: var(--brand); + color: var(--brand-on); border-radius: 3px; } @@ -255,25 +255,25 @@ display: flex; gap: 0.375rem; font-size: 0.6875rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); margin-bottom: 0.25rem; } .compactItemMeta span:not(:last-child)::after { content: '·'; margin-left: 0.375rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); } .compactItemStats { display: flex; gap: 0.75rem; font-size: 0.6875rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); } .compactStat strong { - color: var(--text-primary, #1a1612); + color: var(--text-primary); } .compactItemActions { @@ -292,8 +292,8 @@ font-size: 1.5rem; font-weight: 700; margin-bottom: 0.5rem; - color: var(--text-primary, #1a1612); - font-family: var(--font-playfair), 'Playfair Display', serif; + color: var(--text-primary); + font-family: var(--font-display); } /* Decorative coral bar under section headings */ @@ -302,14 +302,14 @@ display: block; width: 50px; height: 2px; - background: var(--accent-coral, #e07256); + background: var(--brand); border-radius: 1px; margin-top: 0.5rem; } .sectionDescription { font-size: 0.9375rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); margin: 0; line-height: 1.5; } @@ -336,22 +336,22 @@ .emptyState { text-align: center; padding: 2.5rem 1.5rem; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 10px; } .emptyStateTitle { font-size: 1.25rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.375rem; - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); } .emptyStateDescription { font-size: 0.9375rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); max-width: 380px; margin: 0 auto; } @@ -419,9 +419,9 @@ /* Highlighted List Item */ .highlightedItem .compactItem { - border-color: var(--accent-teal, #2d7d7d); - box-shadow: 0 0 0 1px var(--accent-teal, #2d7d7d); - background: var(--bg-secondary, #f3ede4); + border-color: var(--brand); + box-shadow: 0 0 0 1px var(--brand); + background: var(--bg-secondary); } /* Mobile Bottom Sheet */ @@ -443,9 +443,9 @@ .bottomSheet { position: relative; - background: var(--bg-card, white); + background: var(--bg-card); 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; animation: slideUpSheet 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards; } @@ -467,16 +467,16 @@ right: -12px; width: 30px; height: 30px; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); 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; } @@ -522,7 +522,7 @@ .quickSearchChip { padding: 0.375rem 0.875rem; background: var(--bg-card); - border: 1px solid var(--border-color, #e0ddd8); + border: 1px solid var(--border); border-radius: 999px; font-size: 0.875rem; color: var(--text-secondary); @@ -531,9 +531,9 @@ } .quickSearchChip:hover { - background: var(--accent-coral-dark); - color: white; - border-color: var(--accent-coral-dark); + background: var(--brand-strong); + color: var(--brand-on); + border-color: var(--brand-strong); } .exploringRow { @@ -546,7 +546,7 @@ .exploringLabel { font-size: 0.72rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; @@ -564,19 +564,19 @@ align-items: center; gap: 0.4rem; padding: 0.5rem 0.95rem; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 999px; font-size: 0.82rem; font-weight: 500; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); text-decoration: none; transition: all 0.15s ease; } .exploringChip:hover { - border-color: var(--accent-coral, #e07256); - color: var(--accent-coral-dark, #c45a3f); + border-color: var(--brand); + color: var(--brand-strong); transform: translateY(-1px); } @@ -606,16 +606,16 @@ } .hiwHeading { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-size: 1.75rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin: 0; } .hiwSub { font-size: 0.875rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } .hiwGrid { @@ -625,18 +625,18 @@ } .hiwCard { - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 14px; 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; flex-direction: column; gap: 0.9rem; } .hiwVisual { - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 10px; padding: 0.9rem; min-height: 180px; @@ -657,11 +657,11 @@ font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } .hiwPhaseLabel strong { - color: var(--accent-teal, #2d7d7d); + color: var(--brand); font-weight: 700; } @@ -676,20 +676,20 @@ font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); } .hiwTitle { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-size: 1.1rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); line-height: 1.25; } .hiwDesc { font-size: 0.86rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); line-height: 1.45; margin: 0; } @@ -713,7 +713,7 @@ font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-bottom: 0.1rem; } @@ -723,16 +723,16 @@ align-items: baseline; gap: 0.2rem; font-size: 0.44rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.01em; white-space: nowrap; } .miniRowHead strong { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-size: 0.62rem; - color: var(--text-primary, #1a1612); + color: var(--text-primary); font-weight: 700; letter-spacing: 0; text-transform: none; @@ -741,7 +741,7 @@ .miniTrack { height: 5px; border-radius: 2px; - background: rgba(45, 125, 125, 0.08); + background: rgba(var(--status-above-rgb), 0.08); position: relative; overflow: visible; } @@ -749,21 +749,21 @@ .miniBarExp { height: 100%; border-radius: 2px; - background: var(--accent-teal-light, #3a9e9e); + background: var(--brand); } .miniBarExc { height: 100%; border-radius: 2px; - background: var(--accent-teal, #2d7d7d); + background: var(--brand); } .miniNatPill { position: absolute; top: -9px; transform: translateX(-50%); - background: var(--accent-coral, #e07256); - color: #fff; + background: var(--brand); + color: var(--text-inverse); font-size: 0.4rem; font-weight: 700; padding: 0.05rem 0.2rem; @@ -791,21 +791,21 @@ display: flex; justify-content: space-between; font-size: 0.5rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; } .att8Track { height: 7px; - background: rgba(45, 125, 125, 0.08); + background: rgba(var(--status-above-rgb), 0.08); border-radius: 3px; position: relative; } .att8Fill { height: 100%; - background: var(--accent-teal, #2d7d7d); + background: var(--brand); border-radius: 3px; } @@ -814,25 +814,28 @@ top: -2px; bottom: -2px; width: 1.5px; - background: rgba(224, 114, 86, 0.6); + background: rgba(var(--brand-rgb), 0.6); z-index: 2; } .att8Score { + font-variant-numeric: tabular-nums; text-align: right; } .att8Value { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-variant-numeric: tabular-nums; + font-family: var(--font-display); font-size: 1.5rem; font-weight: 700; - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); line-height: 1; } .att8Delta { + font-variant-numeric: tabular-nums; font-size: 0.55rem; - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; @@ -853,31 +856,31 @@ align-items: center; gap: 0.5rem; padding-bottom: 0.4rem; - border-bottom: 1.5px solid var(--border-color, #e5dfd5); + border-bottom: 1.5px solid var(--border); } .ofstedBullet { display: block; width: 3px; height: 1em; - background: var(--accent-coral, #e07256); + background: var(--brand); border-radius: 2px; flex-shrink: 0; } .ofstedTitle { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-size: 0.85rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); } .ofstedFramework { align-self: flex-start; padding: 0.15rem 0.5rem; border-radius: 4px; - background: rgba(90, 58, 110, 0.12); - color: #5a3a6e; + background: rgba(var(--phase-secondary-rgb), 0.12); + color: var(--phase-secondary-text); font-size: 0.5rem; font-weight: 700; letter-spacing: 0.05em; @@ -899,7 +902,7 @@ .rcArea { font-size: 0.62rem; - color: var(--text-secondary, #4a453d); + color: var(--text-secondary); } .rcChip { @@ -913,13 +916,13 @@ } .rcChipGood { - background: rgba(45, 125, 125, 0.12); - color: var(--accent-teal, #2d7d7d); + background: rgba(var(--status-above-rgb), 0.12); + color: var(--status-above); } .rcChipNeutral { - background: var(--accent-gold-bg, rgba(180, 150, 40, 0.12)); - color: var(--accent-gold-text, #7a6800); + background: var(--status-below-bg); + color: var(--status-below); } /* Safeguarding is a separate binary met/not-met judgement, not a graded area, @@ -931,30 +934,30 @@ gap: 0.5rem; margin-top: 0.05rem; padding-top: 0.35rem; - border-top: 1px dashed var(--border-color, #e5dfd5); + border-top: 1px dashed var(--border); } .rcSep { - color: var(--text-muted, #8a847a); + color: var(--text-muted); font-style: italic; } .rcChipMet { background: transparent; - color: var(--accent-teal, #2d7d7d); - border: 1px solid rgba(45, 125, 125, 0.4); + color: var(--status-above); + border: 1px solid rgba(var(--status-above-rgb), 0.4); } .ofstedMeta { font-size: 0.6rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } /* Compare preview */ .comparePreview { width: 100%; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 8px; overflow: hidden; font-size: 0.6rem; @@ -963,23 +966,23 @@ .compareHead { display: grid; 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; gap: 0.35rem; } .compareHeadCell { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-size: 0.62rem; font-weight: 700; - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); line-height: 1.2; } .compareHeadLabel { font-family: inherit; font-size: 0.48rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase; @@ -990,37 +993,37 @@ grid-template-columns: 1fr repeat(2, 1fr); padding: 0.3rem 0.5rem; gap: 0.35rem; - border-top: 1px solid var(--border-color, #e5dfd5); + border-top: 1px solid var(--border); align-items: baseline; } .compareRowLabel { font-size: 0.55rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; } .compareRowVal { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-size: 0.72rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); } .compareRowValHi { - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); } .compareFoot { font-size: 0.52rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); padding: 0.35rem 0.5rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); text-transform: uppercase; letter-spacing: 0.06em; text-align: center; - border-top: 1px solid var(--border-color, #e5dfd5); + border-top: 1px solid var(--border); } @media (max-width: 768px) { @@ -1065,8 +1068,8 @@ grid-template-columns: 1.4fr 1fr; gap: 2rem; padding: 1.75rem; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 14px; } @@ -1078,30 +1081,34 @@ .editorialKicker { font-size: 0.68rem; - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; } .editorialHeading { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-size: 1.35rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin: 0; 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 { - font-size: 0.92rem; - color: var(--text-secondary, #5c564d); - line-height: 1.6; + font-family: var(--font-prose); + font-size: 0.97rem; + color: var(--text-secondary); + line-height: 1.65; margin: 0; + max-width: 62ch; } .factbox { - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 10px; padding: 1.25rem; display: flex; @@ -1110,10 +1117,10 @@ } .factboxHeading { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-size: 1rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin: 0 0 0.85rem; } @@ -1122,7 +1129,7 @@ justify-content: space-between; align-items: baseline; padding: 0.5rem 0; - border-bottom: 1px solid var(--border-color, #e5dfd5); + border-bottom: 1px solid var(--border); font-size: 0.85rem; gap: 0.5rem; } @@ -1132,13 +1139,13 @@ } .factKey { - color: var(--text-muted, #6d685f); + color: var(--text-muted); } .factVal { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); text-align: right; } @@ -1168,7 +1175,7 @@ .sortSelect { padding: 0.375rem 0.75rem; - border: 1px solid var(--border-color, #e0ddd8); + border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--bg-card); color: var(--text-secondary); @@ -1189,7 +1196,7 @@ gap: 0.375rem; padding: 0.25rem 0.625rem; background: var(--bg-secondary); - border: 1px solid var(--border-color, #e0ddd8); + border: 1px solid var(--border); border-radius: 999px; font-size: 0.8rem; color: var(--text-secondary); @@ -1216,8 +1223,9 @@ } .loadMoreCount { + font-variant-numeric: tabular-nums; font-size: 0.875rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); margin: 0; } @@ -1231,7 +1239,7 @@ .admissionsStrip { padding: 1.5rem 0 2rem; - border-top: 1px solid var(--border-color, #e5dfd5); + border-top: 1px solid var(--border); } .stripHeader { @@ -1248,12 +1256,12 @@ font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } .stripCta { font-size: 0.82rem; - color: var(--accent-teal, #2d7d7d); + color: var(--brand); font-weight: 600; text-decoration: none; } @@ -1269,11 +1277,11 @@ } .countdownChip { - background: var(--bg-card, #fff); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 12px; 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; flex-direction: column; gap: 0.2rem; @@ -1292,16 +1300,16 @@ } .countdownChipDeadline::before { - background: var(--accent-coral, #e07256); + background: var(--brand); } .countdownChipOffer::before { - background: var(--accent-teal, #2d7d7d); + background: var(--status-above); } .countdownChipUrgent { - border-color: rgba(224, 114, 86, 0.4); - background: rgba(224, 114, 86, 0.04); + border-color: rgba(var(--brand-rgb), 0.4); + background: rgba(var(--brand-rgb), 0.04); } .chipTrack { @@ -1316,11 +1324,11 @@ } .chipTrackDeadline { - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); } .chipTrackOffer { - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); } .chipTrackDot { @@ -1332,7 +1340,8 @@ } .chipDays { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-variant-numeric: tabular-nums; + font-family: var(--font-display); font-size: 2.6rem; font-weight: 700; line-height: 1; @@ -1341,18 +1350,18 @@ .countdownChipDeadline .chipDays, .countdownChipUrgent .chipDays { - color: var(--accent-coral-dark, #c45a3f); + color: var(--brand-strong); } .countdownChipOffer .chipDays { - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); } .chipDaysUnit { - font-family: 'DM Sans', sans-serif; + font-family: var(--font-ui); font-size: 0.78rem; font-weight: 500; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-left: 0.2rem; vertical-align: bottom; line-height: 2; @@ -1361,14 +1370,14 @@ .chipMilestone { font-size: 0.85rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); line-height: 1.25; margin-top: 0.1rem; } .chipDate { font-size: 0.75rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-top: 0.05rem; } diff --git a/nextjs-app/components/HomeView.tsx b/nextjs-app/components/HomeView.tsx index 4662a26..ae9c459 100644 --- a/nextjs-app/components/HomeView.tsx +++ b/nextjs-app/components/HomeView.tsx @@ -573,10 +573,10 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare, nationalAvgRwm rwmDelta == null ? {} : rwmDelta >= 2 - ? { fontSize: '0.7rem', color: 'var(--accent-teal, #2d7d7d)', fontWeight: 600 } + ? { fontSize: '0.7rem', color: 'var(--status-above)', fontWeight: 600 } : rwmDelta <= -2 - ? { fontSize: '0.7rem', color: 'var(--accent-coral, #e07256)', fontWeight: 600 } - : { fontSize: '0.7rem', color: 'var(--text-muted, #8a847a)' }; + ? { fontSize: '0.7rem', color: 'var(--brand)', fontWeight: 600 } + : { fontSize: '0.7rem', color: 'var(--text-muted)' }; return (
@@ -601,13 +601,13 @@ function CompactSchoolItem({ school, onAddToCompare, isInCompare, nationalAvgRwm fontWeight: 600, borderRadius: '3px', whiteSpace: 'nowrap', - ...(ofstedBadge.cssClass === 'ofsted1' ? { background: 'var(--accent-teal-bg)', color: 'var(--accent-teal, #2d7d7d)' } : - ofstedBadge.cssClass === 'ofsted2' ? { background: 'rgba(60,140,60,0.12)', color: '#2f7a2f' } : - ofstedBadge.cssClass === 'ofsted3' ? { background: 'var(--accent-gold-bg)', color: 'var(--accent-gold-text, #7a6800)' } : - ofstedBadge.cssClass === 'ofsted4' ? { background: 'var(--accent-coral-bg)', color: 'var(--accent-coral, #e07256)' } : - ofstedBadge.cssClass === 'ofstedRc' ? { background: '#5a3a6e', color: '#fff' } : - ofstedBadge.cssClass === 'ofstedPending' ? { background: '#e0e0e0', color: '#666' } : - { background: '#e0e0e0', color: '#666' }), + ...(ofstedBadge.cssClass === 'ofsted1' ? { background: 'var(--status-above-bg)', color: 'var(--status-above)' } : + ofstedBadge.cssClass === 'ofsted2' ? { background: 'var(--status-above-bg)', color: 'var(--status-above)' } : + ofstedBadge.cssClass === 'ofsted3' ? { background: 'var(--status-below-bg)', color: 'var(--status-below)' } : + ofstedBadge.cssClass === 'ofsted4' ? { background: 'var(--brand-bg)', color: 'var(--brand)' } : + ofstedBadge.cssClass === 'ofstedRc' ? { background: 'var(--phase-secondary-text)', color: 'var(--text-inverse)' } : + ofstedBadge.cssClass === 'ofstedPending' ? { background: 'var(--bg-secondary)', color: 'var(--text-muted)' } : + { background: 'var(--bg-secondary)', color: 'var(--text-muted)' }), }} > {ofstedBadge.label} diff --git a/nextjs-app/components/InfoPopover.module.css b/nextjs-app/components/InfoPopover.module.css index 297a184..1c01cfb 100644 --- a/nextjs-app/components/InfoPopover.module.css +++ b/nextjs-app/components/InfoPopover.module.css @@ -11,7 +11,7 @@ /* font-size:0 hides the button's own "?" text node; the ::before glyph below carries the visible circled "?" at its own explicit size. */ font-size: 0; - color: var(--text-muted, #8a7a72); + color: var(--text-muted); cursor: help; line-height: 1; user-select: none; @@ -34,17 +34,17 @@ .icon:hover, .icon[aria-expanded='true'], .icon:focus-visible { - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); } .tooltip { z-index: 9999; width: max-content; max-width: min(260px, calc(100vw - 24px)); - background: var(--bg-primary, #faf7f2); - border: 1px solid var(--border-color, #e8ddd4); + background: var(--bg-primary); + border: 1px solid var(--border); 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; display: flex; flex-direction: column; @@ -52,26 +52,26 @@ } .arrow { - fill: var(--bg-primary, #faf7f2); - stroke: var(--border-color, #e8ddd4); + fill: var(--bg-primary); + stroke: var(--border); stroke-width: 1px; } .label { font-weight: 600; font-size: 0.75rem; - color: var(--text-primary, #2c2420); + color: var(--text-primary); } .plain { font-size: 0.75rem; - color: var(--text-secondary, #5a4a44); + color: var(--text-secondary); line-height: 1.4; } .detail { font-size: 0.7rem; - color: var(--text-muted, #8a7a72); + color: var(--text-muted); line-height: 1.4; margin-top: 0.1rem; } diff --git a/nextjs-app/components/LeafletHeroMapInner.tsx b/nextjs-app/components/LeafletHeroMapInner.tsx index 0f8d7c4..7f2377a 100644 --- a/nextjs-app/components/LeafletHeroMapInner.tsx +++ b/nextjs-app/components/LeafletHeroMapInner.tsx @@ -22,10 +22,10 @@ interface LeafletHeroMapInnerProps { // reliably inside Leaflet's divIcon HTML string (no CSS-module dependency). const PIN_HTML = `
- + - - + +
`; diff --git a/nextjs-app/components/LeafletMapInner.tsx b/nextjs-app/components/LeafletMapInner.tsx index b88d1a5..0f0c488 100644 --- a/nextjs-app/components/LeafletMapInner.tsx +++ b/nextjs-app/components/LeafletMapInner.tsx @@ -48,21 +48,21 @@ function buildPopupBadge(school: School): PopupBadge { // ofsted_framework is the raw event grouping, never "ReportCard"). if (school.ofsted_rc_date) { 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 yearStr = year ? ` · ${year}` : ''; if (school.ofsted_grade) { const labels: Record = { 1: 'Outstanding', 2: 'Good', 3: 'Req. Improvement', 4: 'Inadequate' }; const colours: Record = { - 1: 'background:#d4f0ea;color:#2d7d7d', - 2: 'background:rgba(60,140,60,0.12);color:#2f7a2f', - 3: 'background:#fef3cd;color:#7a6800', - 4: 'background:#fde8e0;color:#e07256', + 1: 'background:var(--status-above-bg);color:var(--status-above)', + 2: 'background:var(--status-above-bg);color:var(--status-above)', + 3: 'background:var(--status-below-bg);color:var(--status-below)', + 4: 'background:var(--brand-bg);color:var(--brand)', }; 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) { @@ -99,7 +99,7 @@ export default function LeafletMapInner({ schools, center, zoom, referencePoint, const refIcon = L.divIcon({ html: `
${sign}${diff} vs ${laName} avg
`; } metricHtml = `
- ${score.toFixed(1)} - Attainment 8 + ${score.toFixed(1)} + Attainment 8 ${deltaLine}
`; } else if (school.rwm_expected_pct != null) { @@ -159,7 +159,7 @@ export default function LeafletMapInner({ schools, center, zoom, referencePoint, let deltaLine = ''; if (nationalAvgRwm != null) { 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 = 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 = `
${text}
`; } metricHtml = `
- ${rwm}% - Reading, Writing & Maths + ${rwm}% + Reading, Writing & Maths ${deltaLine}
`; } @@ -177,14 +177,14 @@ export default function LeafletMapInner({ schools, center, zoom, referencePoint, const popupContent = `
- ${escapeHtml(school.school_name)} + ${escapeHtml(school.school_name)} ${badge.label}
-
+
${phaseLabel}${school.local_authority ? ` · ${escapeHtml(school.local_authority)}` : ''}${distanceStr}
${metricHtml} - View Details → + View Details →
`; marker.bindPopup(popupContent); diff --git a/nextjs-app/components/LoadingSkeleton.module.css b/nextjs-app/components/LoadingSkeleton.module.css index ba0da7f..5907874 100644 --- a/nextjs-app/components/LoadingSkeleton.module.css +++ b/nextjs-app/components/LoadingSkeleton.module.css @@ -5,19 +5,19 @@ } .skeletonCard { - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 12px; 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 { background: linear-gradient( 90deg, - var(--bg-secondary, #f3ede4) 25%, - rgba(224, 114, 86, 0.08) 50%, - var(--bg-secondary, #f3ede4) 75% + var(--bg-secondary) 25%, + rgba(var(--brand-rgb), 0.08) 50%, + var(--bg-secondary) 75% ); background-size: 200% 100%; animation: shimmer 1.5s ease-in-out infinite; @@ -56,7 +56,7 @@ gap: 1rem; margin-bottom: 1.5rem; padding: 1rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 8px; } @@ -82,11 +82,11 @@ } .skeletonListItem { - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 12px; 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 { diff --git a/nextjs-app/components/Logo.tsx b/nextjs-app/components/Logo.tsx new file mode 100644 index 0000000..bd2adaf --- /dev/null +++ b/nextjs-app/components/Logo.tsx @@ -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 ( + + {title ? {title} : null} + {BARS.map(([x, y, w, h, isSchool], i) => ( + + ))} + + ); +} diff --git a/nextjs-app/components/Modal.module.css b/nextjs-app/components/Modal.module.css index fb4bf92..05b8275 100644 --- a/nextjs-app/components/Modal.module.css +++ b/nextjs-app/components/Modal.module.css @@ -1,7 +1,7 @@ .overlay { position: fixed; inset: 0; - background: rgba(26, 22, 18, 0.6); + background: rgba(var(--shadow-rgb), 0.6); display: flex; align-items: center; justify-content: center; @@ -20,15 +20,15 @@ } .modal { - background: var(--bg-card, white); + background: var(--bg-card); 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; overflow: hidden; display: flex; flex-direction: column; animation: slideIn 0.3s ease; - border: 1px solid var(--border-color, #e5dfd5); + border: 1px solid var(--border); } @keyframes slideIn { @@ -62,22 +62,22 @@ justify-content: space-between; align-items: center; padding: 1.5rem; - border-bottom: 1px solid var(--border-color, #e5dfd5); + border-bottom: 1px solid var(--border); } .title { margin: 0; font-size: 1.25rem; font-weight: 600; - color: var(--text-primary, #1a1612); - font-family: var(--font-playfair), 'Playfair Display', serif; + color: var(--text-primary); + font-family: var(--font-display); } .closeButton { padding: 0.5rem; background: transparent; border: none; - color: var(--text-muted, #8a847a); + color: var(--text-muted); cursor: pointer; border-radius: 8px; transition: all 0.2s ease; @@ -87,8 +87,8 @@ } .closeButton:hover { - background: var(--bg-secondary, #f3ede4); - color: var(--accent-coral, #e07256); + background: var(--bg-secondary); + color: var(--brand); } .content { @@ -102,16 +102,16 @@ } .content::-webkit-scrollbar-track { - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); } .content::-webkit-scrollbar-thumb { - background: var(--border-color, #e5dfd5); + background: var(--border); border-radius: 4px; } .content::-webkit-scrollbar-thumb:hover { - background: var(--text-muted, #8a847a); + background: var(--text-muted); } @media (max-width: 640px) { diff --git a/nextjs-app/components/Navigation.module.css b/nextjs-app/components/Navigation.module.css index 9a14b2d..2d0073b 100644 --- a/nextjs-app/components/Navigation.module.css +++ b/nextjs-app/components/Navigation.module.css @@ -2,9 +2,9 @@ position: sticky; top: 0; z-index: 1000; - background: var(--bg-card, white); - border-bottom: 1px solid var(--border-color, #e5dfd5); - box-shadow: 0 2px 8px rgba(26, 22, 18, 0.06); + background: var(--bg-card); + border-bottom: 1px solid var(--border); + box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.06); } .container { @@ -27,7 +27,7 @@ margin: -0.375rem -0.5rem; padding: 0.375rem 0.5rem; text-decoration: none; - color: var(--text-primary, #1a1612); + color: var(--text-primary); font-size: 1.25rem; font-weight: 700; transition: color 0.2s ease; @@ -35,14 +35,16 @@ } .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 { display: inline-flex; - width: 36px; - height: 36px; - color: var(--accent-coral, #e07256); + width: 32px; + height: 32px; + color: var(--text-primary); } .logoIcon svg { @@ -51,9 +53,17 @@ } .logoText { - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); font-weight: 700; - letter-spacing: -0.01em; + letter-spacing: -0.022em; +} + +.logoTextAccent { + color: var(--brand); +} + +.logo:hover .logoTextAccent { + color: var(--brand-strong); } .nav { @@ -69,7 +79,7 @@ padding: 0.625rem 1rem; font-size: 0.9375rem; font-weight: 500; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); text-decoration: none; border-radius: 6px; transition: all 0.2s ease; @@ -83,7 +93,7 @@ left: 1rem; right: 1rem; height: 2px; - background: var(--accent-coral, #e07256); + background: var(--brand); transform: scaleX(0); transform-origin: left; transition: transform 0.25s ease; @@ -91,8 +101,8 @@ } .navLink:hover { - color: var(--text-primary, #1a1612); - background: var(--bg-secondary, #f3ede4); + color: var(--text-primary); + background: var(--bg-secondary); } .navLink:hover::after { @@ -100,8 +110,8 @@ } .navLink.active { - color: var(--accent-coral-dark, #b04a2e); - background: var(--accent-coral-bg); + color: var(--brand-strong); + background: var(--brand-bg); } .navLink.active::after { @@ -117,11 +127,11 @@ padding: 0 0.375rem; font-size: 0.75rem; font-weight: 600; - color: white; - background: var(--accent-coral, #e07256); + color: var(--brand-on); + background: var(--brand); border-radius: 9999px; 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 { @@ -153,7 +163,7 @@ flex: 1; min-height: 56px; padding: 0.375rem 0.25rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); text-decoration: none; font-size: 0.6875rem; font-weight: 500; @@ -163,11 +173,11 @@ } .tab:active { - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); } .tabActive { - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); } .tabIconWrap { @@ -200,9 +210,9 @@ padding: 0 5px; font-size: 0.6875rem; font-weight: 700; - color: white; - background: var(--accent-coral, #e07256); - border: 2px solid var(--bg-card, white); + color: var(--brand-on); + background: var(--brand); + border: 2px solid var(--bg-card); border-radius: 9999px; animation: badgePop 0.3s ease-out; } @@ -230,9 +240,9 @@ left: 0; right: 0; z-index: 1000; - background: var(--bg-card, white); - border-top: 1px solid var(--border-color, #e5dfd5); - box-shadow: 0 -2px 12px rgba(26, 22, 18, 0.06); + background: var(--bg-card); + border-top: 1px solid var(--border); + box-shadow: 0 -2px 12px rgba(var(--shadow-rgb), 0.06); /* Respect iPhone home-indicator (bottom) and notch (left/right in landscape) insets so the tab content never sits under system UI. */ padding-bottom: env(safe-area-inset-bottom, 0); diff --git a/nextjs-app/components/Navigation.tsx b/nextjs-app/components/Navigation.tsx index 24bbdc8..407261a 100644 --- a/nextjs-app/components/Navigation.tsx +++ b/nextjs-app/components/Navigation.tsx @@ -9,6 +9,7 @@ import { useEffect } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { useComparison } from '@/hooks/useComparison'; +import { LogoMark } from './Logo'; import styles from './Navigation.module.css'; type IconProps = { className?: string }; @@ -91,13 +92,11 @@ export function Navigation() {
- + + + + SchoolCompare - SchoolCompare
-
+
Expected standard
-
+
Exceeding / high score
-
+
National average
diff --git a/nextjs-app/components/SchoolCard.module.css b/nextjs-app/components/SchoolCard.module.css index d4f1f12..8fb2897 100644 --- a/nextjs-app/components/SchoolCard.module.css +++ b/nextjs-app/components/SchoolCard.module.css @@ -1,6 +1,6 @@ .card { - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-left: 3px solid transparent; border-radius: 10px; padding: 1rem 1.125rem; @@ -8,13 +8,13 @@ } .card.cardInCompare { - border-color: var(--accent-teal, #2d7d7d); - box-shadow: 0 0 0 1px var(--accent-teal, #2d7d7d); + border-color: var(--brand); + box-shadow: 0 0 0 1px var(--brand); } .card:hover { - border-left-color: var(--accent-coral, #e07256); - box-shadow: var(--shadow-medium, 0 4px 20px rgba(26, 22, 18, 0.1)); + border-left-color: var(--brand); + box-shadow: var(--shadow-medium, 0 4px 20px rgba(var(--shadow-rgb), 0.1)); transform: translateY(-1px); } @@ -31,24 +31,24 @@ font-size: 1rem; font-weight: 600; line-height: 1.35; - font-family: var(--font-playfair), 'Playfair Display', serif; + font-family: var(--font-display); } .title a { - color: var(--text-primary, #1a1612); + color: var(--text-primary); text-decoration: none; transition: color 0.2s ease; } .title a:hover { - color: var(--accent-coral, #e07256); + color: var(--brand); } .distance { font-size: 0.75rem; - color: var(--accent-teal, #2d7d7d); + color: var(--brand); white-space: nowrap; - background: var(--accent-teal-bg); + background: var(--brand-bg); padding: 0.125rem 0.5rem; border-radius: 12px; font-weight: 500; @@ -63,9 +63,9 @@ .metaItem { font-size: 0.75rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); padding: 0.125rem 0.5rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 3px; } @@ -75,9 +75,9 @@ gap: 0.625rem; margin-bottom: 0.875rem; padding: 0.625rem 0.75rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 6px; - border: 1px solid var(--border-color, #e5dfd5); + border: 1px solid var(--border); } .metric { @@ -88,12 +88,13 @@ .metricLabel { font-size: 0.6875rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; } .metricValue { + font-variant-numeric: tabular-nums; display: flex; align-items: center; gap: 0.375rem; @@ -101,7 +102,7 @@ .metricValue strong { font-size: 1rem; - color: var(--text-primary, #1a1612); + color: var(--text-primary); font-weight: 700; } @@ -126,18 +127,18 @@ } .trendUp { - color: var(--accent-teal, #2d7d7d); - background: var(--accent-teal-bg); + color: var(--status-above); + background: var(--status-above-bg); } .trendDown { - color: var(--accent-coral, #e07256); - background: var(--accent-coral-bg); + color: var(--brand); + background: var(--brand-bg); } .trendStable { - color: var(--text-muted, #8a847a); - background: rgba(138, 132, 122, 0.15); + color: var(--text-muted); + background: rgba(var(--muted-rgb), 0.15); } .actions { @@ -156,7 +157,7 @@ .metricHint { font-size: 0.7rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); display: block; margin-top: 1px; font-weight: 400; diff --git a/nextjs-app/components/SchoolHeroMap.module.css b/nextjs-app/components/SchoolHeroMap.module.css index d7e33b9..8904521 100644 --- a/nextjs-app/components/SchoolHeroMap.module.css +++ b/nextjs-app/components/SchoolHeroMap.module.css @@ -6,7 +6,7 @@ position: relative; width: 100%; height: 210px; - background: #dfe6e2; + background: var(--bg-secondary); isolation: isolate; } @@ -47,7 +47,7 @@ width: 100%; height: 100%; 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%; animation: shimmer 1.4s infinite; } @@ -85,11 +85,11 @@ border-radius: 999px; font-size: 13px; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); background: rgba(255, 255, 255, .85); -webkit-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; transform: translateY(4px); transition: opacity .16s ease, transform .16s ease; @@ -118,7 +118,7 @@ rgba(255, 255, 255, .35) 35%, rgba(255, 255, 255, .75) 62%, rgba(255, 255, 255, .95) 82%, - var(--bg-card, #fff) 100%); + var(--bg-card) 100%); } .closeBtn { @@ -134,9 +134,9 @@ border: none; border-radius: 8px; background: rgba(255, 255, 255, .92); - color: var(--text-primary, #1a1612); + color: var(--text-primary); cursor: pointer; - box-shadow: 0 2px 10px rgba(0, 0, 0, .2); + box-shadow: 0 2px 10px rgba(var(--shadow-rgb), .2); } .closeBtn:hover { diff --git a/nextjs-app/components/SchoolMap.module.css b/nextjs-app/components/SchoolMap.module.css index e4a4e9c..cb61c65 100644 --- a/nextjs-app/components/SchoolMap.module.css +++ b/nextjs-app/components/SchoolMap.module.css @@ -33,12 +33,12 @@ border: 2px solid rgba(0, 0, 0, 0.2); border-radius: 4px; cursor: pointer; - color: #333; + color: var(--text-primary); transition: background 0.15s ease, color 0.15s ease; } .fullscreenBtn:hover { - background: #f4f4f4; + background: var(--bg-secondary); color: #000; } @@ -57,9 +57,9 @@ .spinner { width: 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-top-color: var(--accent-coral, #e07256); + border-top-color: var(--brand); animation: spin 0.8s linear infinite; } diff --git a/nextjs-app/components/SchoolRow.module.css b/nextjs-app/components/SchoolRow.module.css index 20f03e8..fd4bed7 100644 --- a/nextjs-app/components/SchoolRow.module.css +++ b/nextjs-app/components/SchoolRow.module.css @@ -2,8 +2,8 @@ display: flex; align-items: center; gap: 1rem; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-left: 3px solid transparent; border-radius: 8px; padding: 1rem 1.25rem; @@ -12,17 +12,17 @@ } .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 */ -.phasePrimary { border-left-color: var(--phase-primary, #5b8cbf); } -.phaseAllThrough { border-left-color: var(--phase-all-through, #7a9a6d); } -.phaseNursery { border-left-color: var(--phase-nursery, #e0a0b0); } +.phasePrimary { border-left-color: var(--phase-primary); } +.phaseAllThrough { border-left-color: var(--phase-all-through); } +.phaseNursery { border-left-color: var(--phase-nursery); } .rowInCompare { - border-left-color: var(--accent-teal, #2d7d7d); - background: var(--bg-secondary, #f3ede4); + border-left-color: var(--brand); + background: var(--bg-secondary); } @keyframes rowFadeIn { @@ -50,7 +50,7 @@ .schoolName { font-size: 0.9375rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); text-decoration: none; white-space: nowrap; overflow: hidden; @@ -60,7 +60,7 @@ } .schoolName:hover { - color: var(--accent-coral, #e07256); + color: var(--brand); } /* Phase label pill */ @@ -96,8 +96,8 @@ line-height: 1.4; border-radius: 4px; white-space: nowrap; - background: rgba(26, 22, 18, 0.05); - color: var(--text-secondary, #5c564d); + background: rgba(var(--shadow-rgb), 0.05); + color: var(--text-secondary); } /* Line 3: stats */ @@ -115,10 +115,11 @@ } .statValue { + font-variant-numeric: tabular-nums; font-size: 0.9375rem; font-weight: 700; - color: var(--text-primary, #1a1612); - font-family: var(--font-playfair), 'Playfair Display', serif; + color: var(--text-primary); + font-family: var(--font-display); display: inline-flex; align-items: center; gap: 0.2rem; @@ -126,7 +127,7 @@ .statLabel { font-size: 0.75rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); white-space: nowrap; } @@ -137,9 +138,9 @@ margin-left: 1px; } -.trendUp { color: var(--accent-teal, #2d7d7d); } -.trendDown { color: var(--accent-coral, #e07256); } -.trendStable { color: var(--text-muted, #8a847a); } +.trendUp { color: var(--status-above); } +.trendDown { color: var(--brand); } +.trendStable { color: var(--text-muted); } /* Line 4: location */ .line4 { @@ -148,13 +149,13 @@ flex-wrap: wrap; gap: 0; font-size: 0.8rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); } .line4 span:not(:last-child)::after { content: '·'; margin: 0 0.4rem; - color: var(--border-color, #e5dfd5); + color: var(--border); } .distanceBadge { @@ -162,8 +163,8 @@ padding: 0.0625rem 0.375rem; font-size: 0.75rem; font-weight: 600; - background: var(--accent-teal, #2d7d7d); - color: white; + background: var(--brand); + color: var(--brand-on); border-radius: 3px; } @@ -200,23 +201,23 @@ font-weight: 400; } -.ofsted1 { background: var(--accent-teal-bg); color: var(--accent-teal, #2d7d7d); } -.ofsted2 { background: rgba(60, 140, 60, 0.12); color: #2f7a2f; } -.ofsted3 { background: var(--accent-gold-bg); color: var(--accent-gold-text, #7a6800); } -.ofsted4 { background: var(--accent-coral-bg); color: var(--accent-coral, #e07256); } +.ofsted1 { background: var(--status-above-bg); color: var(--status-above); } +.ofsted2 { background: rgba(var(--status-above-rgb), 0.12); color: var(--status-above); } +.ofsted3 { background: var(--status-below-bg); color: var(--status-below); } +.ofsted4 { background: var(--brand-bg); color: var(--brand); } /* ── Ofsted badge variants ──────────────────────────────────────────────── */ /* ofsted1–4 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, distinct from the grey "Not yet inspected" pending state. */ -.ofstedInspected { background: #e6ecf2; color: #45586b; } -.ofstedPending { background: #e0e0e0; color: #666; } +.ofstedInspected { background: var(--phase-primary-bg); color: var(--phase-primary-text); } +.ofstedPending { background: var(--border); color: var(--text-muted); } /* ── vs-national delta line (under RWM metric) ──────────────────────────── */ -.vsNational { font-size: 0.7rem; color: var(--accent-teal, #2d7d7d); font-weight: 600; } -.vsNationalNeg { font-size: 0.7rem; color: var(--accent-coral, #e07256); font-weight: 600; } -.vsNationalFlat { font-size: 0.7rem; color: var(--text-muted, #8a847a); } +.vsNational { font-size: 0.7rem; color: var(--status-above); font-weight: 600; } +.vsNationalNeg { font-size: 0.7rem; color: var(--brand); font-weight: 600; } +.vsNationalFlat { font-size: 0.7rem; color: var(--text-muted); } /* ── Mobile ──────────────────────────────────────────── */ @media (max-width: 640px) { @@ -257,7 +258,7 @@ /* GIAS "Open, but proposed to close" marker */ .attrClosing { - background: #fdf6e3; - color: #8a6200; - border: 1px solid #e2c96f; + background: var(--status-below-bg); + color: var(--status-below); + border: 1px solid var(--status-below-bg); } diff --git a/nextjs-app/components/SchoolSearchModal.module.css b/nextjs-app/components/SchoolSearchModal.module.css index 2bbc8a5..288d776 100644 --- a/nextjs-app/components/SchoolSearchModal.module.css +++ b/nextjs-app/components/SchoolSearchModal.module.css @@ -3,9 +3,9 @@ } .warning { - background: var(--accent-gold-bg); - border: 1px solid var(--accent-gold, #c9a227); - color: var(--text-primary, #1a1612); + background: var(--status-below-bg); + border: 1px solid var(--status-below); + color: var(--text-primary); padding: 1rem; border-radius: 8px; margin-bottom: 1rem; @@ -21,21 +21,21 @@ width: 100%; padding: 0.875rem 1rem; font-size: 1rem; - border: 2px solid var(--border-color, #e5dfd5); + border: 2px solid var(--border); border-radius: 8px; - background: var(--bg-card, white); - color: var(--text-primary, #1a1612); + background: var(--bg-card); + color: var(--text-primary); transition: all 0.2s ease; } .searchInput::placeholder { - color: var(--text-muted, #8a847a); + color: var(--text-muted); } .searchInput:focus { outline: none; - border-color: var(--accent-coral, #e07256); - box-shadow: 0 0 0 3px var(--accent-coral-bg); + border-color: var(--brand); + box-shadow: 0 0 0 3px var(--brand-bg); } .searchSpinner { @@ -45,9 +45,9 @@ transform: translateY(-50%); width: 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-top-color: var(--accent-coral, #e07256); + border-top-color: var(--brand); animation: spin 0.6s linear infinite; } @@ -71,17 +71,17 @@ } .results::-webkit-scrollbar-track { - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 4px; } .results::-webkit-scrollbar-thumb { - background: var(--border-color, #e5dfd5); + background: var(--border); border-radius: 4px; } .results::-webkit-scrollbar-thumb:hover { - background: var(--text-muted, #8a847a); + background: var(--text-muted); } .resultItem { @@ -90,16 +90,16 @@ align-items: center; gap: 1rem; padding: 1rem; - background: var(--bg-secondary, #f3ede4); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-secondary); + border: 1px solid var(--border); border-radius: 10px; transition: all 0.2s ease; } .resultItem:hover { - background: var(--bg-card, white); - border-color: var(--accent-coral, #e07256); - box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.06)); + background: var(--bg-card); + border-color: var(--brand); + box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.06)); } .schoolInfo { @@ -110,7 +110,7 @@ .schoolName { font-size: 1rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.25rem; white-space: nowrap; overflow: hidden; @@ -125,7 +125,7 @@ display: flex; gap: 1rem; font-size: 0.875rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); } .schoolMeta span { @@ -138,14 +138,14 @@ .noResults { text-align: center; padding: 2rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); font-size: 0.9375rem; } .hint { text-align: center; padding: 2rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); font-size: 0.9375rem; } diff --git a/nextjs-app/components/SecondarySchoolRow.module.css b/nextjs-app/components/SecondarySchoolRow.module.css index ae64de6..8ecccf5 100644 --- a/nextjs-app/components/SecondarySchoolRow.module.css +++ b/nextjs-app/components/SecondarySchoolRow.module.css @@ -2,8 +2,8 @@ display: flex; align-items: center; gap: 1rem; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-left: 3px solid transparent; border-radius: 8px; padding: 1rem 1.25rem; @@ -12,17 +12,17 @@ } .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 */ -.phaseSecondary { border-left-color: var(--phase-secondary, #9b6bb0); } -.phaseAllThrough { border-left-color: var(--phase-all-through, #7a9a6d); } -.phasePost16 { border-left-color: var(--phase-post16, #c4915e); } +.phaseSecondary { border-left-color: var(--phase-secondary); } +.phaseAllThrough { border-left-color: var(--phase-all-through); } +.phasePost16 { border-left-color: var(--phase-post16); } .rowInCompare { - border-left-color: var(--accent-teal, #2d7d7d); - background: var(--bg-secondary, #f3ede4); + border-left-color: var(--brand); + background: var(--bg-secondary); } @keyframes rowFadeIn { @@ -50,7 +50,7 @@ .schoolName { font-size: 0.9375rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); text-decoration: none; white-space: nowrap; overflow: hidden; @@ -60,7 +60,7 @@ } .schoolName:hover { - color: var(--accent-coral, #e07256); + color: var(--brand); } /* Line 2: context tags */ @@ -81,8 +81,8 @@ line-height: 1.4; border-radius: 4px; white-space: nowrap; - background: rgba(26, 22, 18, 0.05); - color: var(--text-secondary, #5c564d); + background: rgba(var(--shadow-rgb), 0.05); + color: var(--text-secondary); } /* Line 3: KS4 stats */ @@ -100,22 +100,24 @@ } .statValueLarge { + font-variant-numeric: tabular-nums; font-size: 1.125rem; font-weight: 700; - color: var(--text-primary, #1a1612); - font-family: var(--font-playfair), 'Playfair Display', serif; + color: var(--text-primary); + font-family: var(--font-display); } .statValue { + font-variant-numeric: tabular-nums; font-size: 0.9375rem; font-weight: 700; - color: var(--text-primary, #1a1612); - font-family: var(--font-playfair), 'Playfair Display', serif; + color: var(--text-primary); + font-family: var(--font-display); } .statLabel { font-size: 0.75rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); white-space: nowrap; } @@ -125,8 +127,8 @@ white-space: nowrap; } -.deltaPositive { color: #2f7a2f; } -.deltaNegative { color: var(--accent-coral, #e07256); } +.deltaPositive { color: var(--status-above); } +.deltaNegative { color: var(--brand); } /* Line 4: location + distance */ .line4 { @@ -135,13 +137,13 @@ flex-wrap: wrap; gap: 0; font-size: 0.8rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); } .line4 span:not(:last-child)::after { content: '·'; margin: 0 0.4rem; - color: var(--border-color, #e5dfd5); + color: var(--border); } .distanceBadge { @@ -149,8 +151,8 @@ padding: 0.0625rem 0.375rem; font-size: 0.75rem; font-weight: 600; - background: var(--accent-teal, #2d7d7d); - color: white; + background: var(--brand); + color: var(--brand-on); border-radius: 3px; } @@ -175,15 +177,15 @@ font-size: 0.75rem; font-weight: 600; line-height: 1.4; - background: rgba(26, 22, 18, 0.05); - color: var(--text-secondary, #5c564d); + background: rgba(var(--shadow-rgb), 0.05); + color: var(--text-secondary); border-radius: 4px; white-space: nowrap; } .selectiveTag { - background: rgba(180, 120, 0, 0.1); - color: #8a6200; + background: rgba(var(--status-below-rgb), 0.1); + color: var(--status-below); } /* ── Ofsted badge ────────────────────────────────────── */ @@ -202,17 +204,17 @@ font-weight: 400; } -.ofsted1 { background: var(--accent-teal-bg); color: var(--accent-teal, #2d7d7d); } -.ofsted2 { background: rgba(60, 140, 60, 0.12); color: #2f7a2f; } -.ofsted3 { background: var(--accent-gold-bg); color: var(--accent-gold-text, #7a6800); } -.ofsted4 { background: var(--accent-coral-bg); color: var(--accent-coral, #e07256); } +.ofsted1 { background: var(--status-above-bg); color: var(--status-above); } +.ofsted2 { background: rgba(var(--status-above-rgb), 0.12); color: var(--status-above); } +.ofsted3 { background: var(--status-below-bg); color: var(--status-below); } +.ofsted4 { background: var(--brand-bg); color: var(--brand); } /* ── 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, distinct from the grey "Not yet inspected" pending state. */ -.ofstedInspected { background: #e6ecf2; color: #45586b; } -.ofstedPending { background: #e0e0e0; color: #666; } +.ofstedInspected { background: var(--phase-primary-bg); color: var(--phase-primary-text); } +.ofstedPending { background: var(--border); color: var(--text-muted); } /* ── Right actions column ────────────────────────────── */ .rowActions { @@ -268,7 +270,7 @@ } .closingTag { - background: #fdf6e3; - color: #8a6200; - border: 1px solid #e2c96f; + background: var(--status-below-bg); + color: var(--status-below); + border: 1px solid var(--status-below-bg); } diff --git a/nextjs-app/components/SpecialSchoolNote.module.css b/nextjs-app/components/SpecialSchoolNote.module.css index b9c7a36..19ab95f 100644 --- a/nextjs-app/components/SpecialSchoolNote.module.css +++ b/nextjs-app/components/SpecialSchoolNote.module.css @@ -1,15 +1,15 @@ /* Special-school / PRU / AP context note: soft, informational (teal), not a warning. Shared by both detail views so the styling can't drift. */ .note { - background: var(--bg-secondary, #f3ede4); - border-left: 4px solid var(--accent-teal, #2d7d7d); + background: var(--bg-secondary); + border-left: 4px solid var(--brand); border-radius: 0 6px 6px 0; padding: 0.7rem 1rem; margin: 0 0 1rem; font-size: 0.9rem; line-height: 1.5; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); } .note strong { - color: var(--text-primary, #1a1612); + color: var(--text-primary); } diff --git a/nextjs-app/components/compare/CompareAcademics.module.css b/nextjs-app/components/compare/CompareAcademics.module.css index 42b36ef..e9bb90e 100644 --- a/nextjs-app/components/compare/CompareAcademics.module.css +++ b/nextjs-app/components/compare/CompareAcademics.module.css @@ -1,6 +1,6 @@ .moreMeasures { margin-top: 0.5rem; - border-top: 1px solid var(--border-light); + border-top: 1px solid var(--border); padding-top: 0.75rem; } @@ -8,7 +8,7 @@ cursor: pointer; font-weight: 600; font-size: 0.88rem; - color: var(--accent-coral-dark); + color: var(--brand-strong); } .stripNote { diff --git a/nextjs-app/components/compare/CompareAtAGlance.tsx b/nextjs-app/components/compare/CompareAtAGlance.tsx index bc9322f..147ef06 100644 --- a/nextjs-app/components/compare/CompareAtAGlance.tsx +++ b/nextjs-app/components/compare/CompareAtAGlance.tsx @@ -88,7 +88,7 @@ export function CompareAtAGlance({ )} {display.kind === 'transitional' && ( <> - + No overall grade Sub-judgements only diff --git a/nextjs-app/components/compare/CompareOfsted.tsx b/nextjs-app/components/compare/CompareOfsted.tsx index 9875a6b..0d0f86e 100644 --- a/nextjs-app/components/compare/CompareOfsted.tsx +++ b/nextjs-app/components/compare/CompareOfsted.tsx @@ -54,7 +54,7 @@ function ResultCell({ display }: { display: OfstedDisplay }) { if (display.kind === 'transitional') { return ( <> - + No overall grade diff --git a/nextjs-app/components/compare/TrendsExplorer.module.css b/nextjs-app/components/compare/TrendsExplorer.module.css index e5e007b..481d709 100644 --- a/nextjs-app/components/compare/TrendsExplorer.module.css +++ b/nextjs-app/components/compare/TrendsExplorer.module.css @@ -5,10 +5,10 @@ .explore summary { cursor: pointer; font-weight: 600; - color: var(--accent-coral-dark); + color: var(--brand-strong); padding: 0.85rem 1.1rem; background: var(--bg-card); - border: 1px solid var(--border-light); + border: 1px solid var(--border); border-radius: 8px; } @@ -17,7 +17,7 @@ } .inner { - border: 1px solid var(--border-light); + border: 1px solid var(--border); border-top: none; border-radius: 0 0 8px 8px; background: var(--bg-card); @@ -43,7 +43,7 @@ font-size: 0.9rem; padding: 0.4rem 0.6rem; border-radius: 8px; - border: 1px solid var(--border-light); + border: 1px solid var(--border); background: var(--bg-card); color: var(--text-primary); max-width: 100%; diff --git a/nextjs-app/components/compare/compareSections.module.css b/nextjs-app/components/compare/compareSections.module.css index af7712f..fc5df84 100644 --- a/nextjs-app/components/compare/compareSections.module.css +++ b/nextjs-app/components/compare/compareSections.module.css @@ -9,12 +9,12 @@ } .sectionTitle { - font-family: var(--font-playfair), 'Playfair Display', Georgia, serif; + font-family: var(--font-display); font-size: 1.45rem; font-weight: 700; margin: 0; padding-left: 0.75rem; - border-left: 3px solid var(--accent-coral-dark); + border-left: 3px solid var(--brand-strong); } .how { @@ -36,7 +36,7 @@ back into the shared grid. */ .measure { background: var(--bg-card); - border: 1px solid var(--border-light); + border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow-soft); padding: 0.75rem 0.85rem; @@ -59,7 +59,7 @@ gap: 0.35rem 0.5rem; flex-wrap: wrap; padding: 0.5rem 0; - border-top: 1px solid var(--border-light); + border-top: 1px solid var(--border); margin-top: 0.5rem; font-size: 0.95rem; } @@ -113,18 +113,18 @@ } .chipGood { - background: rgba(45, 125, 125, 0.14); - color: var(--accent-teal); + background: rgba(var(--status-above-rgb), 0.14); + color: var(--status-above); } .chipWarn { - background: var(--accent-gold-bg); - color: var(--accent-gold-text); + background: var(--status-below-bg); + color: var(--status-below); } .chipBad { - background: var(--accent-coral-bg); - color: var(--accent-coral-dark); + background: var(--brand-bg); + color: var(--brand-strong); } .chipNeutral { @@ -141,18 +141,18 @@ } .badgeGood { - background: rgba(45, 125, 125, 0.14); - color: var(--accent-teal); + background: rgba(var(--status-above-rgb), 0.14); + color: var(--status-above); } .badgeWarn { - background: var(--accent-gold-bg); - color: var(--accent-gold-text); + background: var(--status-below-bg); + color: var(--status-below); } .badgeBad { - background: var(--accent-coral-bg); - color: var(--accent-coral-dark); + background: var(--brand-bg); + color: var(--brand-strong); } .rcList { @@ -200,7 +200,7 @@ .card { background: var(--bg-card); - border: 1px solid var(--border-light); + border: 1px solid var(--border); border-radius: 16px; box-shadow: var(--shadow-soft); padding: 1.25rem 1.5rem; @@ -208,7 +208,7 @@ } .link { - color: var(--accent-coral-dark); + color: var(--brand-strong); } @media (min-width: 761px) { @@ -230,14 +230,14 @@ .rowLabel { color: var(--text-secondary); padding: 0.85rem 0.5rem 0.85rem 0; - border-bottom: 1px solid var(--border-light); + border-bottom: 1px solid var(--border); } .cell { display: block; padding: 0.85rem 0.25rem; border-top: none; - border-bottom: 1px solid var(--border-light); + border-bottom: 1px solid var(--border); margin-top: 0; } diff --git a/nextjs-app/components/school/SchoolDetailShell.module.css b/nextjs-app/components/school/SchoolDetailShell.module.css index d1c38fe..3286ded 100644 --- a/nextjs-app/components/school/SchoolDetailShell.module.css +++ b/nextjs-app/components/school/SchoolDetailShell.module.css @@ -19,7 +19,7 @@ padding: 0.25rem 0; font-size: 1.0625rem; font-weight: 600; - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); background: none; border: none; cursor: pointer; @@ -29,7 +29,7 @@ .topBack:hover { - color: var(--accent-coral-dark, #c85a3e); + color: var(--brand-strong); text-decoration: underline; text-underline-offset: 2px; } @@ -38,8 +38,8 @@ /* Header Section */ .header { position: relative; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 10px; /* Padding lives on .headerContent so the map band can bleed to the edges. */ padding: 0; @@ -88,12 +88,13 @@ .headerHasMap .actions .btnAdd { + /* Sits on the map hero, which renders light in both themes. */ background: rgba(255, 255, 255, 0.9); - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); border-color: transparent; -webkit-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; font: inherit; font-weight: 600; - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); cursor: pointer; white-space: nowrap; } .mapLink:hover { - color: var(--accent-coral-dark, #c45a3f); + color: var(--brand-strong); text-decoration: underline; text-underline-offset: 2px; } @@ -137,11 +138,11 @@ .schoolName { font-size: clamp(2rem, 5vw, 3.25rem); font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.5rem; line-height: 1.1; letter-spacing: -0.01em; - font-family: var(--font-playfair), "Playfair Display", serif; + font-family: var(--font-display); } @@ -155,16 +156,16 @@ .metaItem { font-size: 0.8125rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); padding: 0.125rem 0.5rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 3px; } .address { font-size: 0.875rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); margin: 0 0 0.75rem; } @@ -180,18 +181,18 @@ .headerDetail { font-size: 0.8125rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); } .headerDetail strong { - color: var(--text-primary, #1a1612); + color: var(--text-primary); font-weight: 600; } .headerDetail a { - color: var(--accent-teal, #2d7d7d); + color: var(--brand); text-decoration: none; } @@ -213,7 +214,7 @@ border: none; font-size: 0.8125rem; font-weight: 600; - color: var(--accent-teal, #2d7d7d); + color: var(--brand); cursor: pointer; } @@ -236,25 +237,25 @@ cursor: pointer; transition: all 0.2s ease; 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 { - background: var(--accent-coral-dark, #b04a2e); - color: white; + background: var(--brand-strong); + color: var(--brand-on); } .btnAdd:hover { - background: var(--accent-coral-darker, #9c3f26); + background: var(--brand-stronger); transform: translateY(-1px); } .btnRemove { - background: var(--accent-teal, #2d7d7d); - color: white; + background: var(--brand); + color: var(--brand-on); } @@ -270,13 +271,13 @@ position: sticky; top: 64px; /* global header height on desktop */ z-index: 10; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-top: none; border-radius: 0 0 10px 10px; padding: 0.5rem 0.75rem; 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; align-items: center; gap: 0.5rem; @@ -291,9 +292,9 @@ padding: 0.3rem 0.625rem; font-size: 0.75rem; font-weight: 600; - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); background: none; - border: 1px solid var(--border-color, #e5dfd5); + border: 1px solid var(--border); border-radius: 4px; cursor: pointer; white-space: nowrap; @@ -302,8 +303,8 @@ .sectionNavBack:hover { - background: var(--bg-secondary, #f3ede4); - border-color: var(--accent-coral, #e07256); + background: var(--bg-secondary); + border-color: var(--brand); } @@ -334,7 +335,7 @@ padding: 0.3rem 0.625rem; font-size: 0.75rem; font-weight: 500; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); text-decoration: none; border-radius: 4px; transition: all 0.15s ease; @@ -344,21 +345,21 @@ .sectionNavLink:hover { - background: var(--bg-secondary, #f3ede4); - color: var(--text-primary, #1a1612); + background: var(--bg-secondary); + color: var(--text-primary); } .sectionNavLinkActive { - background: var(--accent-coral-dark, #b04a2e); - color: white; + background: var(--brand-strong); + color: var(--brand-on); font-weight: 600; } .sectionNavLinkActive:hover { - background: var(--accent-coral-dark, #c45a3f); - color: white; + background: var(--brand-strong); + color: var(--brand-on); } @@ -373,12 +374,12 @@ gap: 0.5rem; min-height: 38px; padding: 0.34rem 0.7rem; - background: var(--bg-secondary, #f3ede4); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-secondary); + border: 1px solid var(--border); border-radius: 8px; cursor: pointer; - font-family: var(--font-dm-sans), "DM Sans", sans-serif; - color: var(--text-primary, #1a1612); + font-family: var(--font-ui); + color: var(--text-primary); } @@ -396,7 +397,7 @@ font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } @@ -411,7 +412,7 @@ .sectionNavMenuChev { flex: none; - color: var(--text-muted, #6d685f); + color: var(--text-muted); font-size: 0.7rem; } @@ -426,9 +427,9 @@ width: 38px; height: 38px; border-radius: 9px; - border: 1px solid var(--accent-coral-dark, #b04a2e); - background: var(--accent-coral-dark, #b04a2e); - color: white; + border: 1px solid var(--brand-strong); + background: var(--brand-strong); + color: var(--brand-on); cursor: pointer; transition: background 0.15s ease, border-color 0.15s ease; } @@ -447,9 +448,9 @@ width: 16px; height: 16px; border-radius: 50%; - background: var(--bg-card, white); - color: var(--accent-coral-dark, #c45a3f); - border: 1.5px solid var(--accent-coral, #e07256); + background: var(--bg-card); + color: var(--brand-strong); + border: 1.5px solid var(--brand); display: flex; align-items: center; justify-content: center; @@ -460,9 +461,9 @@ .sectionNavCompareIconIn { - background: var(--bg-card, white); - border-color: var(--accent-teal, #2d7d7d); - color: var(--accent-teal, #2d7d7d); + background: var(--bg-card); + border-color: var(--brand); + color: var(--brand); } @@ -474,9 +475,9 @@ padding: 0.34rem 0.7rem; font-size: 0.75rem; font-weight: 600; - color: white; - background: var(--accent-coral-dark, #b04a2e); - border: 1px solid var(--accent-coral-dark, #b04a2e); + color: var(--brand-on); + background: var(--brand-strong); + border: 1px solid var(--brand-strong); border-radius: 999px; cursor: pointer; white-space: nowrap; @@ -485,21 +486,21 @@ .sectionNavCompare:hover { - background: var(--accent-coral-darker, #9c3f26); - border-color: var(--accent-coral-darker, #9c3f26); + background: var(--brand-stronger); + border-color: var(--brand-stronger); } .sectionNavCompareIn { - background: var(--bg-card, white); - color: var(--accent-teal, #2d7d7d); - border-color: var(--accent-teal, #2d7d7d); + background: var(--bg-card); + color: var(--brand); + border-color: var(--brand); } .sectionNavCompareIn:hover { - background: var(--bg-secondary, #f3ede4); - border-color: var(--accent-teal, #2d7d7d); + background: var(--bg-secondary); + border-color: var(--brand); } @@ -512,8 +513,8 @@ padding: 0.34rem 0.65rem; font-size: 0.75rem; font-weight: 600; - color: var(--text-primary, #1a1612); - background: var(--bg-secondary, #f3ede4); + color: var(--text-primary); + background: var(--bg-secondary); border: none; border-radius: 999px; cursor: pointer; @@ -523,7 +524,7 @@ .sectionNavAll:hover { - background: var(--border-color, #e5dfd5); + background: var(--border); } @@ -531,7 +532,7 @@ position: fixed; inset: 0; z-index: 1500; - background: rgba(26, 22, 18, 0.28); + background: rgba(var(--shadow-rgb), 0.28); } @@ -543,19 +544,19 @@ width: 230px; max-height: min(70vh, 460px); overflow-y: auto; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); 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; } .sectionsPanelHead { - font-family: var(--font-playfair), "Playfair Display", Georgia, serif; + font-family: var(--font-display); font-size: 0.9rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); padding: 0.4rem 0.6rem 0.5rem; } @@ -568,44 +569,44 @@ padding: 0.55rem 0.6rem; border-radius: 8px; font-size: 0.85rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); text-decoration: none; transition: background 0.12s ease; } .sectionsItem:hover { - background: var(--bg-secondary, #f3ede4); - color: var(--text-primary, #1a1612); + background: var(--bg-secondary); + color: var(--text-primary); } .sectionsItemActive { - background: var(--accent-coral-bg, rgba(224, 114, 86, 0.12)); - color: var(--accent-coral-dark, #c45a3f); + background: var(--brand-bg); + color: var(--brand-strong); font-weight: 600; } .sectionsTick { - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); } /* GIAS "Open, but proposed to close" notice strip */ .closingStrip { - background: #fdf6e3; - border-left: 4px solid #e2c96f; + background: var(--status-below-bg); + border-left: 4px solid var(--status-below-bg); border-radius: 0 6px 6px 0; padding: 0.55rem 0.9rem; margin: 0.5rem 0; font-size: 0.88rem; - color: #6e5a00; + color: var(--status-below); max-width: 68ch; } .closingStrip strong { - color: #8a6200; + color: var(--status-below); } @media (max-width: 640px) { @@ -738,7 +739,7 @@ max-height: 74vh; border-radius: 16px 16px 0 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 { diff --git a/nextjs-app/components/school/schoolSections.module.css b/nextjs-app/components/school/schoolSections.module.css index 8ec039a..fc781f8 100644 --- a/nextjs-app/components/school/schoolSections.module.css +++ b/nextjs-app/components/school/schoolSections.module.css @@ -13,7 +13,7 @@ padding: 0.25rem 0; font-size: 1.0625rem; font-weight: 600; - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); background: none; border: none; cursor: pointer; @@ -22,7 +22,7 @@ } .topBack:hover { - color: var(--accent-coral-dark, #c85a3e); + color: var(--brand-strong); text-decoration: underline; text-underline-offset: 2px; } @@ -30,8 +30,8 @@ /* Header Section */ .header { position: relative; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 10px; /* Padding lives on .headerContent so the map band can bleed to the edges. */ padding: 0; @@ -75,12 +75,13 @@ } .headerHasMap .actions .btnAdd { + /* Sits on the map hero, which renders light in both themes. */ background: rgba(255, 255, 255, 0.9); - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); border-color: transparent; -webkit-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); } .headerHasMap .actions .btnAdd:hover { @@ -124,13 +125,13 @@ padding: 0; font: inherit; font-weight: 600; - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); cursor: pointer; white-space: nowrap; } .mapLink:hover { - color: var(--accent-coral-dark, #c45a3f); + color: var(--brand-strong); text-decoration: underline; text-underline-offset: 2px; } @@ -142,11 +143,11 @@ .schoolName { font-size: clamp(2rem, 5vw, 3.25rem); font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.5rem; line-height: 1.1; letter-spacing: -0.01em; - font-family: var(--font-playfair), "Playfair Display", serif; + font-family: var(--font-display); overflow-wrap: break-word; } @@ -159,15 +160,15 @@ .metaItem { font-size: 0.8125rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); padding: 0.125rem 0.5rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 3px; } .address { font-size: 0.875rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); margin: 0 0 0.75rem; overflow-wrap: break-word; } @@ -182,16 +183,16 @@ .headerDetail { font-size: 0.8125rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); } .headerDetail strong { - color: var(--text-primary, #1a1612); + color: var(--text-primary); font-weight: 600; } .headerDetail a { - color: var(--accent-teal, #2d7d7d); + color: var(--brand); text-decoration: none; } @@ -211,16 +212,17 @@ border: none; font-size: 0.8125rem; font-weight: 600; - color: var(--accent-teal, #2d7d7d); + color: var(--brand); cursor: pointer; } /* Gender split card — sits in the Pupils & Inclusion heroStatGrid */ .genderSplitValue { + font-variant-numeric: tabular-nums; display: flex; align-items: baseline; gap: 0.3rem; - font-family: var(--font-playfair), "Playfair Display", Georgia, serif; + font-family: var(--font-display); font-size: 1.55rem; font-weight: 700; line-height: 1; @@ -228,23 +230,23 @@ } .genderSplitGirls { - color: #a04a68; + color: var(--phase-nursery-text); } .genderSplitBoys { - color: var(--accent-teal, #2d7d7d); + color: var(--phase-primary-text); } .genderSplitLabel { - font-family: var(--font-body, inherit); + font-family: var(--font-ui); font-size: 0.78rem; font-weight: 500; - color: var(--text-muted, #6d685f); + color: var(--text-muted); letter-spacing: 0; } .genderSplitSep { - color: var(--border-color, #c8beb0); + color: var(--border); font-weight: 400; font-size: 1.2rem; padding: 0 0.1rem; @@ -255,16 +257,16 @@ height: 6px; border-radius: 999px; overflow: hidden; - background: var(--border-color, #e5dfd5); + background: var(--border); width: 100%; } .genderBarGirls { - background: #a04a68; + background: var(--phase-nursery-text); } .genderBarBoys { - background: var(--accent-teal, #2d7d7d); + background: var(--phase-primary); } .actions { @@ -284,22 +286,22 @@ cursor: pointer; transition: all 0.2s ease; 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 { - background: var(--accent-coral-dark, #b04a2e); - color: white; + background: var(--brand-strong); + color: var(--brand-on); } .btnAdd:hover { - background: var(--accent-coral-darker, #9c3f26); + background: var(--brand-stronger); transform: translateY(-1px); } .btnRemove { - background: var(--accent-teal, #2d7d7d); - color: white; + background: var(--brand); + color: var(--brand-on); } .btnRemove:hover { @@ -313,13 +315,13 @@ position: sticky; top: 64px; /* global header height on desktop */ z-index: 10; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-top: none; border-radius: 0 0 10px 10px; padding: 0.5rem 0.75rem; 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; align-items: center; gap: 0.5rem; @@ -341,9 +343,9 @@ padding: 0.3rem 0.625rem; font-size: 0.75rem; font-weight: 600; - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); background: none; - border: 1px solid var(--border-color, #e5dfd5); + border: 1px solid var(--border); border-radius: 4px; cursor: pointer; white-space: nowrap; @@ -351,8 +353,8 @@ } .sectionNavBack:hover { - background: var(--bg-secondary, #f3ede4); - border-color: var(--accent-coral, #e07256); + background: var(--bg-secondary); + border-color: var(--brand); } /* The scrolling middle: section links only. */ @@ -394,7 +396,7 @@ padding: 0.3rem 0.625rem; font-size: 0.75rem; font-weight: 500; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); text-decoration: none; border-radius: 4px; transition: all 0.15s ease; @@ -412,19 +414,19 @@ } .sectionNavLink:hover { - background: var(--bg-secondary, #f3ede4); - color: var(--text-primary, #1a1612); + background: var(--bg-secondary); + color: var(--text-primary); } .sectionNavLinkActive { - background: var(--accent-coral-dark, #b04a2e); - color: white; + background: var(--brand-strong); + color: var(--brand-on); font-weight: 600; } .sectionNavLinkActive:hover { - background: var(--accent-coral-dark, #c45a3f); - color: white; + background: var(--brand-strong); + color: var(--brand-on); } /* ── Mobile: the scrolling links collapse into one "section" menu button ── @@ -438,12 +440,12 @@ gap: 0.5rem; min-height: 38px; padding: 0.34rem 0.7rem; - background: var(--bg-secondary, #f3ede4); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-secondary); + border: 1px solid var(--border); border-radius: 8px; cursor: pointer; - font-family: var(--font-dm-sans), "DM Sans", sans-serif; - color: var(--text-primary, #1a1612); + font-family: var(--font-ui); + color: var(--text-primary); } .sectionNavMenuCur { @@ -459,7 +461,7 @@ font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } .sectionNavMenuNow { @@ -472,7 +474,7 @@ .sectionNavMenuChev { flex: none; - color: var(--text-muted, #6d685f); + color: var(--text-muted); font-size: 0.7rem; } @@ -486,9 +488,9 @@ width: 38px; height: 38px; border-radius: 9px; - border: 1px solid var(--accent-coral-dark, #b04a2e); - background: var(--accent-coral-dark, #b04a2e); - color: white; + border: 1px solid var(--brand-strong); + background: var(--brand-strong); + color: var(--brand-on); cursor: pointer; transition: background 0.15s ease, border-color 0.15s ease; } @@ -505,9 +507,9 @@ width: 16px; height: 16px; border-radius: 50%; - background: var(--bg-card, white); - color: var(--accent-coral-dark, #c45a3f); - border: 1.5px solid var(--accent-coral, #e07256); + background: var(--bg-card); + color: var(--brand-strong); + border: 1.5px solid var(--brand); display: flex; align-items: center; justify-content: center; @@ -517,9 +519,9 @@ } .sectionNavCompareIconIn { - background: var(--bg-card, white); - border-color: var(--accent-teal, #2d7d7d); - color: var(--accent-teal, #2d7d7d); + background: var(--bg-card); + border-color: var(--brand); + color: var(--brand); } /* Compare CTA carried into the bar once the hero's button scrolls away. */ @@ -530,9 +532,9 @@ padding: 0.34rem 0.7rem; font-size: 0.75rem; font-weight: 600; - color: white; - background: var(--accent-coral-dark, #b04a2e); - border: 1px solid var(--accent-coral-dark, #b04a2e); + color: var(--brand-on); + background: var(--brand-strong); + border: 1px solid var(--brand-strong); border-radius: 999px; cursor: pointer; white-space: nowrap; @@ -540,19 +542,19 @@ } .sectionNavCompare:hover { - background: var(--accent-coral-darker, #9c3f26); - border-color: var(--accent-coral-darker, #9c3f26); + background: var(--brand-stronger); + border-color: var(--brand-stronger); } .sectionNavCompareIn { - background: var(--bg-card, white); - color: var(--accent-teal, #2d7d7d); - border-color: var(--accent-teal, #2d7d7d); + background: var(--bg-card); + color: var(--brand); + border-color: var(--brand); } .sectionNavCompareIn:hover { - background: var(--bg-secondary, #f3ede4); - border-color: var(--accent-teal, #2d7d7d); + background: var(--bg-secondary); + border-color: var(--brand); } @media (max-width: 640px) { @@ -570,8 +572,8 @@ padding: 0.34rem 0.65rem; font-size: 0.75rem; font-weight: 600; - color: var(--text-primary, #1a1612); - background: var(--bg-secondary, #f3ede4); + color: var(--text-primary); + background: var(--bg-secondary); border: none; border-radius: 999px; cursor: pointer; @@ -580,7 +582,7 @@ } .sectionNavAll:hover { - background: var(--border-color, #e5dfd5); + background: var(--border); } @media (max-width: 640px) { @@ -593,7 +595,7 @@ position: fixed; inset: 0; z-index: 1500; - background: rgba(26, 22, 18, 0.28); + background: rgba(var(--shadow-rgb), 0.28); } .sectionsPanel { @@ -604,18 +606,18 @@ width: 230px; max-height: min(70vh, 460px); overflow-y: auto; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); 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; } .sectionsPanelHead { - font-family: var(--font-playfair), "Playfair Display", Georgia, serif; + font-family: var(--font-display); font-size: 0.9rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); padding: 0.4rem 0.6rem 0.5rem; } @@ -627,24 +629,24 @@ padding: 0.55rem 0.6rem; border-radius: 8px; font-size: 0.85rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); text-decoration: none; transition: background 0.12s ease; } .sectionsItem:hover { - background: var(--bg-secondary, #f3ede4); - color: var(--text-primary, #1a1612); + background: var(--bg-secondary); + color: var(--text-primary); } .sectionsItemActive { - background: var(--accent-coral-bg, rgba(224, 114, 86, 0.12)); - color: var(--accent-coral-dark, #c45a3f); + background: var(--brand-bg); + color: var(--brand-strong); font-weight: 600; } .sectionsTick { - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); } /* On phones the menu becomes a bottom sheet. */ @@ -659,7 +661,7 @@ max-height: 74vh; border-radius: 16px 16px 0 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 { @@ -689,8 +691,8 @@ /* Unified card for all content sections */ .card { - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 10px; padding: 1.25rem 1.5rem; margin-bottom: 1rem; @@ -704,11 +706,11 @@ .sectionTitle { font-size: 1.125rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.875rem; padding-bottom: 0.5rem; - border-bottom: 2px solid var(--border-color, #e5dfd5); - font-family: var(--font-playfair), "Playfair Display", serif; + border-bottom: 2px solid var(--border); + font-family: var(--font-display); display: flex; align-items: center; gap: 0.375rem; @@ -722,21 +724,21 @@ display: inline-block; width: 3px; height: 1em; - background: var(--accent-coral, #e07256); + background: var(--brand); border-radius: 2px; flex-shrink: 0; } .sectionSubtitle { font-size: 0.85rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); margin: -0.5rem 0 1rem; } .subSectionTitle { font-size: 0.875rem; font-weight: 600; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); margin: 1.25rem 0 0.75rem; } @@ -748,8 +750,8 @@ } .metricCard { - background: var(--bg-secondary, #f3ede4); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-secondary); + border: 1px solid var(--border); border-radius: 6px; padding: 0.75rem; text-align: center; @@ -757,7 +759,7 @@ .metricLabel { font-size: 0.6875rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); margin-bottom: 0.25rem; font-weight: 500; text-transform: uppercase; @@ -765,9 +767,10 @@ } .metricValue { + font-variant-numeric: tabular-nums; font-size: 1.25rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); display: flex; align-items: center; justify-content: center; @@ -778,13 +781,13 @@ .metricHint { font-size: 0.7rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); margin-top: 0.3rem; font-style: italic; } /* ── Hero stat cards (RWM combined, Pupils & Inclusion, etc.) ── */ -/* Larger teal-tinted cards with Playfair serif numbers — reserved for +/* Larger brand-tinted cards with display-weight numbers — reserved for the top-of-section headline metrics. Use .metricCard for denser secondary metrics. */ .heroStatGrid { @@ -795,8 +798,9 @@ } .heroStatCard { - background: var(--accent-teal-bg, rgba(45, 125, 125, 0.12)); - border: 1px solid rgba(45, 125, 125, 0.2); + font-variant-numeric: tabular-nums; + background: var(--status-above-bg); + border: 1px solid rgba(var(--status-above-rgb), 0.2); border-radius: 12px; padding: 1rem 1.1rem; display: flex; @@ -810,7 +814,7 @@ font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; - color: var(--text-muted, #6d685f); + color: var(--text-muted); display: flex; align-items: center; gap: 0.3rem; @@ -818,11 +822,12 @@ } .heroStatValue { - font-family: var(--font-playfair), "Playfair Display", Georgia, serif; + font-variant-numeric: tabular-nums; + font-family: var(--font-display); font-size: 2.1rem; font-weight: 700; line-height: 1; - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); display: flex; align-items: baseline; gap: 0.5rem; @@ -831,8 +836,9 @@ } .heroStatHint { + font-variant-numeric: tabular-nums; font-size: 0.7rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); font-style: normal; margin-top: 0; } @@ -849,29 +855,29 @@ /* Progress score colour coding */ .progressPositive { - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); font-weight: 700; } .progressNegative { - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); font-weight: 700; } /* ── Semantic status colours (unified) ────────────── */ .statusGood { - background: var(--accent-teal-bg); - color: var(--accent-teal, #2d7d7d); + background: var(--status-above-bg); + color: var(--status-above); } .statusWarn { - background: var(--accent-gold-bg); - color: var(--accent-gold-text, #7a6800); + background: var(--status-below-bg); + color: var(--status-below); } .statusBad { - background: var(--accent-coral-bg); - color: var(--accent-coral-dark, #b04a2e); + background: var(--brand-bg); + color: var(--brand-strong); } /* Charts Section */ @@ -898,10 +904,10 @@ .metricGroupTitle { font-size: 0.875rem; font-weight: 600; - color: var(--text-primary, #1a1612); + color: var(--text-primary); margin-bottom: 0.5rem; padding-bottom: 0.375rem; - border-bottom: 1px solid var(--border-color, #e5dfd5); + border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 0.375rem; @@ -918,19 +924,19 @@ justify-content: space-between; align-items: center; padding: 0.375rem 0.625rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 4px; } .metricName { font-size: 0.75rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); } .metricRow .metricValue { font-size: 0.875rem; font-weight: 600; - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); } /* History Table */ @@ -943,7 +949,7 @@ .historicalSubtitle { font-size: 0.8rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); margin: 1.25rem 0 0.25rem; } @@ -954,7 +960,7 @@ } .dataTable thead { - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); } .dataTable th { @@ -964,14 +970,14 @@ font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.03em; - color: var(--text-primary, #1a1612); - border-bottom: 2px solid var(--border-color, #e5dfd5); + color: var(--text-primary); + border-bottom: 2px solid var(--border); } .dataTable td { padding: 0.5rem 0.75rem; - border-bottom: 1px solid var(--border-color, #e5dfd5); - color: var(--text-secondary, #5c564d); + border-bottom: 1px solid var(--border); + color: var(--text-secondary); } .dataTable tbody tr:last-child td { @@ -979,12 +985,12 @@ } .dataTable tbody tr:hover { - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); } .yearCell { font-weight: 600; - color: var(--accent-gold, #c9a227); + color: var(--status-below); } /* Ofsted */ @@ -1007,42 +1013,42 @@ } .ofstedGrade1 { - background: var(--accent-teal-bg); - color: var(--accent-teal, #2d7d7d); + background: var(--status-above-bg); + color: var(--status-above); } .ofstedGrade2 { - background: rgba(60, 140, 60, 0.12); - color: #2f7a2f; + background: rgba(var(--status-above-rgb), 0.12); + color: var(--status-above); } .ofstedGrade3 { - background: var(--accent-gold-bg); - color: var(--accent-gold-text, #7a6800); + background: var(--status-below-bg); + color: var(--status-below); } .ofstedGrade4 { - background: var(--accent-coral-bg); - color: var(--accent-coral-dark, #b04a2e); + background: var(--brand-bg); + color: var(--brand-strong); } /* Report Card grade colours (5-level scale, lower = better) */ .rcGrade1 { - background: var(--accent-teal-bg); - color: var(--accent-teal, #2d7d7d); + background: var(--status-above-bg); + color: var(--status-above); } /* Exceptional */ .rcGrade2 { - background: rgba(60, 140, 60, 0.12); - color: #2f7a2f; + background: rgba(var(--status-above-rgb), 0.12); + color: var(--status-above); } /* Strong */ .rcGrade3 { - background: var(--accent-gold-bg); - color: var(--accent-gold-text, #7a6800); + background: var(--status-below-bg); + color: var(--status-below); } /* Expected standard */ .rcGrade4 { - background: rgba(249, 115, 22, 0.12); - color: #c2410c; + background: rgba(var(--status-below-rgb), 0.12); + color: var(--status-below); } /* Needs attention */ .rcGrade5 { - background: var(--accent-coral-bg); - color: var(--accent-coral-dark, #b04a2e); + background: var(--brand-bg); + color: var(--brand-strong); } /* Urgent improvement */ /* Safeguarding value (used inside a standard metricCard) */ @@ -1052,8 +1058,8 @@ border-radius: 4px; font-size: 0.8125rem; font-weight: 600; - background: var(--accent-teal-bg); - color: var(--accent-teal, #2d7d7d); + background: var(--status-above-bg); + color: var(--status-above); } .safeguardingNotMet { display: inline-block; @@ -1061,8 +1067,8 @@ border-radius: 4px; font-size: 0.8125rem; font-weight: 700; - background: var(--accent-coral-bg); - color: var(--accent-coral-dark, #b04a2e); + background: var(--brand-bg); + color: var(--brand-strong); } /* ── Ofsted grade grids (Report Card + OEIF) ── @@ -1102,25 +1108,25 @@ .ofstedDisclaimer { font-size: 0.8rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); font-style: italic; margin: 0 0 1rem; } .ofstedDate { font-size: 0.85rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); } .ofstedPrevious { font-size: 0.8125rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); font-style: italic; } .ofstedReportLink { font-size: 0.8125rem; - color: var(--accent-teal, #2d7d7d); + color: var(--brand); text-decoration: none; margin-left: auto; white-space: nowrap; @@ -1154,19 +1160,19 @@ width: 1.25rem; height: 1.25rem; border-radius: 50%; - background: var(--bg-secondary, #f3ede4); - border: 2px solid var(--border-color, #e5dfd5); + background: var(--bg-secondary); + border: 2px solid var(--border); flex-shrink: 0; } .deprivationDotFilled { - background: var(--accent-teal, #2d7d7d); - border-color: var(--accent-teal, #2d7d7d); + background: var(--brand); + border-color: var(--brand); } .deprivationDesc { font-size: 0.875rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); line-height: 1.5; margin: 0; } @@ -1175,20 +1181,20 @@ display: flex; justify-content: space-between; font-size: 0.7rem; - color: var(--text-muted, #8a847a); + color: var(--text-muted); margin-top: 0.25rem; } /* Progress note */ .ofstedAllSame { font-size: 0.9375rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); margin: 0.5rem 0 0; line-height: 1.5; } .ofstedAllSame strong { - color: var(--text-primary, #1a1612); + color: var(--text-primary); } .progressNote { @@ -1284,7 +1290,7 @@ font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); } @media (max-width: 640px) { @@ -1301,7 +1307,7 @@ gap: 0.75rem; margin: 0.5rem 0 1.5rem; padding: 0.9rem 1rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 8px; } @@ -1310,14 +1316,14 @@ width: 22px; height: 22px; border-radius: 50%; - background: var(--accent-coral, #e07256); - color: #fff; + background: var(--brand); + color: var(--text-inverse); display: inline-flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.8rem; - font-family: var(--font-playfair), "Playfair Display", Georgia, serif; + font-family: var(--font-display); margin-top: 0.05rem; } @@ -1328,12 +1334,12 @@ .rwmBridgeText { font-size: 0.85rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); line-height: 1.5; } .rwmBridgeText strong { - color: var(--text-primary, #1a1612); + color: var(--text-primary); font-weight: 700; } @@ -1342,15 +1348,15 @@ gap: 0.35rem; margin-top: 0.35rem; flex-wrap: wrap; - font-family: var(--font-playfair), "Playfair Display", Georgia, serif; - color: var(--text-muted, #6d685f); + font-family: var(--font-display); + color: var(--text-muted); font-size: 0.78rem; font-style: italic; align-items: baseline; } .rwmBridgeMath strong { - color: var(--text-primary, #1a1612); + color: var(--text-primary); font-weight: 700; font-style: normal; } @@ -1363,7 +1369,7 @@ .progressScoresRow { margin-top: 1.25rem; padding-top: 1rem; - border-top: 1px solid var(--border-color, #e5dfd5); + border-top: 1px solid var(--border); } .progressScoresGrid { @@ -1381,13 +1387,14 @@ .progressScoreLabel { font-size: 0.78rem; font-weight: 500; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } .progressScoreValue { + font-variant-numeric: tabular-nums; font-size: 0.9rem; font-weight: 700; - color: var(--text-primary, #1a1612); + color: var(--text-primary); font-variant-numeric: tabular-nums; } @@ -1398,26 +1405,27 @@ display: grid; grid-template-columns: 1fr 1fr; gap: 1px; - background: var(--border-color, #e5dfd5); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--border); + border: 1px solid var(--border); border-radius: 10px; overflow: hidden; margin: 0; } .admissionsTile { - background: var(--bg-card, #fff); + background: var(--bg-card); padding: 1.15rem 1.25rem; display: flex; flex-direction: column; } .admissionsTileNum { - font-family: var(--font-playfair), "Playfair Display", Georgia, serif; + font-variant-numeric: tabular-nums; + font-family: var(--font-display); font-size: 2.4rem; font-weight: 700; line-height: 1; - color: var(--text-primary, #1a1612); + color: var(--text-primary); font-variant-numeric: tabular-nums; margin: 0; display: flex; @@ -1427,10 +1435,10 @@ } .admissionsTileSub { - font-family: var(--font-dm-sans), "DM Sans", sans-serif; + font-family: var(--font-ui); font-size: 0.85rem; font-weight: 500; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } .admissionsTileLabel { @@ -1439,11 +1447,11 @@ font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } .admissionsTileAccent .admissionsTileNum { - color: var(--accent-coral-dark, #c45a3f); + color: var(--brand-strong); } .admissionsVerdict { @@ -1452,24 +1460,24 @@ } .admissionsVerdictHeadline { - font-family: var(--font-playfair), "Playfair Display", Georgia, serif; + font-family: var(--font-display); font-size: 1.35rem; font-weight: 700; line-height: 1.2; - color: var(--text-primary, #1a1612); + color: var(--text-primary); } .admissionsVerdictOver { - color: var(--accent-coral-dark, #c45a3f); + color: var(--brand-strong); } .admissionsVerdictUnder { - color: var(--accent-teal, #2d7d7d); + color: var(--status-above); } .admissionsVerdictSub { font-size: 0.8rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); line-height: 1.4; margin-top: 0.2rem; } @@ -1500,7 +1508,7 @@ .admissionsSeg { display: inline-flex; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 999px; padding: 3px; gap: 2px; @@ -1515,7 +1523,7 @@ font: inherit; font-size: 0.8125rem; font-weight: 600; - color: var(--text-muted, #6d685f); + color: var(--text-muted); padding: 0.4rem 0.9rem; border-radius: 999px; white-space: nowrap; @@ -1523,13 +1531,13 @@ } .admissionsSeg button[aria-pressed="true"] { - background: var(--bg-card, #fff); - color: var(--text-primary, #1a1612); - box-shadow: var(--shadow-soft, 0 2px 8px rgba(26, 22, 18, 0.06)); + background: var(--bg-card); + color: var(--text-primary); + box-shadow: var(--shadow-soft, 0 2px 8px rgba(var(--shadow-rgb), 0.06)); } .admissionsSeg button:hover[aria-pressed="false"] { - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); } /* Stack both views in one grid cell so the card sizes to the taller view — @@ -1566,21 +1574,21 @@ font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-bottom: 0.5rem; } .admissionsTrendSummary { font-size: 1rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); margin: 1.25rem 0 0; padding-top: 1.1rem; - border-top: 1px solid var(--border-color, #e5dfd5); + border-top: 1px solid var(--border); line-height: 1.5; } .admissionsTrendSummary strong { - color: var(--text-primary, #1a1612); + color: var(--text-primary); } /* ── History accordion ── */ @@ -1591,7 +1599,7 @@ .historyToggle { font-size: 0.8125rem; font-weight: 600; - color: var(--text-muted, #6d685f); + color: var(--text-muted); cursor: pointer; padding: 0.5rem 0; list-style: none; @@ -1618,17 +1626,17 @@ /* GIAS "Open, but proposed to close" notice strip */ .closingStrip { - background: #fdf6e3; - border-left: 4px solid #e2c96f; + background: var(--status-below-bg); + border-left: 4px solid var(--status-below-bg); border-radius: 0 6px 6px 0; padding: 0.55rem 0.9rem; margin: 0.5rem 0; font-size: 0.88rem; - color: #6e5a00; + color: var(--status-below); max-width: 68ch; } .closingStrip strong { - color: #8a6200; + color: var(--status-below); } /* ── Secondary-only rules, merged from SecondarySchoolDetailView.module.css ── */ .badges { @@ -1639,26 +1647,26 @@ } .badge { font-size: 0.8125rem; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); padding: 0.125rem 0.5rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 3px; } .badgeSelective { - background: rgba(180, 120, 0, 0.1); - color: #8a6200; + background: rgba(var(--status-below-rgb), 0.1); + color: var(--status-below); } .badgeFaith { - background: rgba(45, 125, 125, 0.1); - color: var(--accent-teal, #2d7d7d); + background: rgba(var(--status-above-rgb), 0.1); + color: var(--phase-secondary-text); } /* ── Tab Navigation (sticky) ─────────────────────────── */ .tabNav { position: sticky; top: 4rem; z-index: 10; - background: var(--bg-card, white); - border: 1px solid var(--border-color, #e5dfd5); + background: var(--bg-card); + border: 1px solid var(--border); border-top: none; border-radius: 0 0 10px 10px; padding: 0.5rem 1rem; @@ -1667,7 +1675,7 @@ white-space: nowrap; -webkit-overflow-scrolling: touch; scrollbar-width: none; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); + box-shadow: 0 2px 4px rgba(var(--shadow-rgb), 0.04); } .tabNav::-webkit-scrollbar { display: none; @@ -1683,9 +1691,9 @@ padding: 0.3rem 0.625rem; font-size: 0.75rem; font-weight: 600; - color: var(--accent-coral-dark, #b04a2e); + color: var(--brand-strong); background: none; - border: 1px solid var(--border-color, #e5dfd5); + border: 1px solid var(--border); border-radius: 4px; cursor: pointer; white-space: nowrap; @@ -1693,13 +1701,13 @@ margin-right: 0.25rem; } .backBtn:hover { - background: var(--bg-secondary, #f3ede4); - border-color: var(--accent-coral, #e07256); + background: var(--bg-secondary); + border-color: var(--brand); } .tabNavDivider { width: 1px; height: 1rem; - background: var(--border-color, #e5dfd5); + background: var(--border); margin: 0 0.25rem; flex-shrink: 0; } @@ -1708,7 +1716,7 @@ padding: 0.3rem 0.75rem; font-size: 0.75rem; font-weight: 500; - color: var(--text-secondary, #5c564d); + color: var(--text-secondary); background: none; border: none; border-radius: 4px; @@ -1718,23 +1726,23 @@ text-decoration: none; } .tabBtn:hover { - background: var(--bg-secondary, #f3ede4); - color: var(--text-primary, #1a1612); + background: var(--bg-secondary); + color: var(--text-primary); } .tabBtnActive { - background: var(--accent-coral-dark, #b04a2e); - color: white; + background: var(--brand-strong); + color: var(--brand-on); font-weight: 600; } .tabBtnActive:hover { - background: var(--accent-coral-darker, #9c3f26); - color: white; + background: var(--brand-stronger); + color: var(--brand-on); } /* ── Progress 8 suspension banner ───────────────────── */ .p8Banner { - background: rgba(180, 120, 0, 0.1); - border: 1px solid rgba(180, 120, 0, 0.3); - color: #8a6200; + background: rgba(var(--status-below-rgb), 0.1); + border: 1px solid rgba(var(--status-below-rgb), 0.3); + color: var(--status-below); border-radius: 6px; padding: 0.625rem 0.875rem; font-size: 0.825rem; @@ -1750,27 +1758,27 @@ line-height: 1.5; } .admissionsSelective { - background: rgba(180, 120, 0, 0.1); - color: #8a6200; - border: 1px solid rgba(180, 120, 0, 0.25); + background: rgba(var(--status-below-rgb), 0.1); + color: var(--status-below); + border: 1px solid rgba(var(--status-below-rgb), 0.25); } .admissionsFaith { - background: rgba(45, 125, 125, 0.08); - color: var(--accent-teal, #2d7d7d); - border: 1px solid rgba(45, 125, 125, 0.2); + background: rgba(var(--status-above-rgb), 0.08); + color: var(--phase-secondary-text); + border: 1px solid rgba(var(--status-above-rgb), 0.2); } .sixthFormNote { margin-top: 1rem; padding: 0.625rem 0.875rem; - background: var(--bg-secondary, #f3ede4); + background: var(--bg-secondary); border-radius: 6px; font-size: 0.825rem; - color: var(--text-secondary, #5c564d); - border-left: 3px solid var(--accent-teal, #2d7d7d); + color: var(--text-secondary); + border-left: 3px solid var(--brand); } .genderSplitHint { font-size: 0.7rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-top: 0.35rem; font-weight: 500; } @@ -1783,20 +1791,20 @@ font-weight: 600; text-transform: uppercase; letter-spacing: 0.07em; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-bottom: 0.5rem; } .att8VizTrack { position: relative; height: 14px; - background: rgba(45, 125, 125, 0.08); - border: 1px solid var(--border-color, #e5dfd5); + background: rgba(var(--status-above-rgb), 0.08); + border: 1px solid var(--border); border-radius: 4px; overflow: visible; } .att8VizFill { height: 100%; - background: var(--accent-teal, #2d7d7d); + background: var(--brand); border-radius: 4px 0 0 4px; transition: width 0.6s ease; } @@ -1805,7 +1813,7 @@ top: -4px; bottom: -4px; width: 2px; - background: var(--accent-coral, #e07256); + background: var(--brand); border-radius: 2px; z-index: 2; } @@ -1813,8 +1821,8 @@ position: absolute; top: -20px; transform: translateX(-50%); - background: var(--accent-coral-dark, #b04a2e); - color: #fff; + background: var(--brand-strong); + color: var(--text-inverse); font-size: 0.6rem; font-weight: 700; padding: 0.1rem 0.3rem; @@ -1826,7 +1834,7 @@ justify-content: space-between; margin-top: 0.25rem; font-size: 0.6rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } /* ── Progress 8 number line ──────────────────────────── */ .p8Viz { @@ -1837,14 +1845,14 @@ font-weight: 600; text-transform: uppercase; letter-spacing: 0.07em; - color: var(--text-muted, #6d685f); + color: var(--text-muted); margin-bottom: 0.5rem; } .p8VizTrack { position: relative; height: 14px; - background: rgba(45, 125, 125, 0.06); - border: 1px solid var(--border-color, #e5dfd5); + background: rgba(var(--status-above-rgb), 0.06); + border: 1px solid var(--border); border-radius: 4px; overflow: visible; } @@ -1852,7 +1860,7 @@ position: absolute; top: 0; bottom: 0; - background: rgba(45, 125, 125, 0.18); + background: rgba(var(--status-above-rgb), 0.18); border-radius: 3px; } .p8VizZero { @@ -1860,7 +1868,7 @@ top: -4px; bottom: -4px; width: 2px; - background: var(--border-color, #e5dfd5); + background: var(--border); z-index: 1; } .p8VizDot { @@ -1870,20 +1878,20 @@ width: 12px; height: 12px; border-radius: 50%; - background: var(--accent-teal, #2d7d7d); + background: var(--brand); border: 2px solid white; - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + box-shadow: 0 1px 4px rgba(var(--shadow-rgb), 0.2); z-index: 3; } .p8VizDotNeg { - background: var(--accent-coral, #e07256); + background: var(--brand); } .p8VizTicks { display: flex; justify-content: space-between; margin-top: 0.25rem; font-size: 0.6rem; - color: var(--text-muted, #6d685f); + color: var(--text-muted); } /* ── Secondary variants ─────────────────────────────────────────────────── @@ -1896,7 +1904,7 @@ /* Secondary renders a slimmer gender bar on a translucent track. */ .genderBarSecondary { height: 4px; - background: rgba(0, 0, 0, 0.08); + background: var(--border); margin-top: 0.45rem; } @@ -1911,8 +1919,9 @@ /* Secondary hero stats sit tighter and omit the Georgia fallback. */ .heroStatValueSecondary { + font-variant-numeric: tabular-nums; gap: 0.4rem; - font-family: var(--font-playfair), "Playfair Display", serif; + font-family: var(--font-display); } /* ── Secondary-scoped overrides ────────────────────────────────────────── @@ -1922,9 +1931,9 @@ class that only SecondarySchoolSections carries. Same principle as the variant classes above, applied at rule scope. */ -.secondaryScope .heroStatCard .heroStatLabel { font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted, #6d685f); } -.secondaryScope .heroStatCard .heroStatValue { font-family: var(--font-playfair), "Playfair Display", serif; font-size: 2.1rem; font-weight: 700; line-height: 1; color: var(--accent-teal, #2d7d7d); display: flex; align-items: baseline; gap: 0.4rem; flex-wrap: wrap; } -.secondaryScope .heroStatCard .heroStatHint { font-size: 0.7rem; color: var(--text-muted, #6d685f); font-style: normal; margin-top: 0; } +.secondaryScope .heroStatCard .heroStatLabel { font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); } +.secondaryScope .heroStatCard .heroStatValue { font-family: var(--font-display); font-size: 2.1rem; font-weight: 700; line-height: 1; color: var(--status-above); display: flex; align-items: baseline; gap: 0.4rem; flex-wrap: wrap; } +.secondaryScope .heroStatCard .heroStatHint { font-size: 0.7rem; color: var(--text-muted); font-style: normal; margin-top: 0; } .secondaryScope .historyDisclosure[open] .historyToggle::before { transform: rotate(90deg); } @media (max-width: 480px) { .secondaryScope .metricsGrid { grid-template-columns: 1fr 1fr; gap: 0.5rem; } diff --git a/nextjs-app/lib/theme.ts b/nextjs-app/lib/theme.ts new file mode 100644 index 0000000..31032c9 --- /dev/null +++ b/nextjs-app/lib/theme.ts @@ -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 = { + '--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( + ...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})`; +} diff --git a/nextjs-app/lib/utils.ts b/nextjs-app/lib/utils.ts index 11e4f3f..fcbaf41 100644 --- a/nextjs-app/lib/utils.ts +++ b/nextjs-app/lib/utils.ts @@ -273,17 +273,29 @@ export function debounce 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 = [ - 'rgb(75, 192, 192)', - 'rgb(255, 99, 132)', - 'rgb(54, 162, 235)', - 'rgb(255, 206, 86)', - 'rgb(153, 102, 255)', - 'rgb(255, 159, 64)', - 'rgb(201, 203, 207)', - 'rgb(255, 0, 255)', + 'var(--series-1)', + 'var(--series-2)', + 'var(--series-3)', + 'var(--series-4)', + 'var(--series-5)', + 'var(--series-6)', + 'var(--series-7)', + '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 - * backgrounds — the raw series colours fail WCAG AA (e.g. 1.9:1 on cream). - * Same hue per index so text still keys to its chart line. All ≥4.5:1 on - * #f3ede4 and lighter. + * @deprecated The series palette is now AA on both grounds, so line and text + * share one value. Kept as an alias so call sites can migrate incrementally. */ -export const CHART_TEXT_COLORS = [ - '#256868', // teal - '#c2255c', // pink/red - '#2069ad', // blue - '#8a5a00', // yellow → ochre - '#6d43c8', // purple - '#8f4e00', // orange → brown - '#5f5952', // grey - '#a300a3', // magenta -]; +export const CHART_TEXT_COLORS = CHART_COLORS; export function getChartTextColor(index: number): string { 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 { - return rgb.replace('rgb', 'rgba').replace(')', `, ${alpha})`); +export function rgbToRgba(color: string, alpha: number): string { + 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 red–green 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 { switch (trend) { case 'up': - return '#22c55e'; // green + return 'var(--status-above)'; case 'down': - return '#ef4444'; // red + return 'var(--status-below)'; case 'stable': - return '#6b7280'; // gray + return 'var(--status-at)'; } } diff --git a/nextjs-app/public/favicon.svg b/nextjs-app/public/favicon.svg deleted file mode 100644 index 7b533fc..0000000 --- a/nextjs-app/public/favicon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/nextjs-app/public/icon-192.png b/nextjs-app/public/icon-192.png new file mode 100644 index 0000000..3aeabc2 Binary files /dev/null and b/nextjs-app/public/icon-192.png differ diff --git a/nextjs-app/public/icon-512.png b/nextjs-app/public/icon-512.png new file mode 100644 index 0000000..969395c Binary files /dev/null and b/nextjs-app/public/icon-512.png differ diff --git a/nextjs-app/public/icon-maskable-512.png b/nextjs-app/public/icon-maskable-512.png new file mode 100644 index 0000000..612182a Binary files /dev/null and b/nextjs-app/public/icon-maskable-512.png differ diff --git a/nextjs-app/public/manifest.json b/nextjs-app/public/manifest.json index 740ecbf..413af82 100644 --- a/nextjs-app/public/manifest.json +++ b/nextjs-app/public/manifest.json @@ -4,13 +4,31 @@ "description": "Compare primary and secondary school performance across England", "start_url": "/", "display": "standalone", - "background_color": "#faf7f2", - "theme_color": "#faf7f2", + "background_color": "#f2f1ed", + "theme_color": "#f2f1ed", "icons": [ { - "src": "/favicon.svg", + "src": "/icon.svg", "sizes": "any", "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" } ] }