/* * schoolcompare — "Find the right school. For their future." * * Implements the schoolcompare brand guideline. Colour has four jobs and they * never borrow each other's hues: * brand (deep teal) identity, navigation, headings, links, primary buttons * action (coral) the one decisive action on a page — search, submit * status (green / valence against the England average. Never a CTA. * terracotta) * phase (desaturated) category chips, deliberately subordinate. * * Two deviations from the supplied swatches, both forced by contrast, both * confined to text and fills. Coral #F97360 carrying a white label measures * 2.75:1 where 4.5 is the floor, so --action is a darkened #BE3C27; the * original coral survives as --coral for tints, borders and illustration. * Mustard #F2C94C is 1.5:1 as text, so --mustard-ink carries any label while * --mustard stays exact for fills, rings and badges. * * Every value below clears WCAG AA on all three light grounds — Warm White, * white cards and Sand. Sand is the binding one; several values are a step * darker than they would need to be on white alone. */ :root { color-scheme: light; /* ── Ground ─────────────────────────────────────────────────────── */ --bg-primary: #FAFAF8; /* Warm White */ --bg-secondary: #F5EFE6; /* Sand — hero panels, sunken rows */ --bg-card: #FFFFFF; --surface-inverse: #0F766E; /* ── Ink ────────────────────────────────────────────────────────── */ --text-primary: #1C2731; /* Ink */ --text-secondary: #4A5560; --text-muted: #5F6A75; /* 4.8:1 on Sand, the worst ground */ --text-inverse: #FFFFFF; /* ── Line ───────────────────────────────────────────────────────── */ --border: #E5E7EB; /* Light Grey */ --border-strong: #D3D7DD; /* ── Brand: deep teal ───────────────────────────────────────────── */ --brand: #0F766E; /* exact brand value; 4.8:1 on Sand */ --brand-strong: #0C5F58; --brand-stronger: #0A4D47; --brand-bg: rgba(15, 118, 110, 0.10); --brand-on: #FFFFFF; /* ── Action: coral. One per screen, and only for the decisive one. ── */ --action: #BE3C27; /* darkened coral — 5.4:1 under a white label */ --action-strong: #A33320; --action-stronger: #8C2B1B; /* Tint of --action, not of --coral, so a coral ghost button's label and ground share a hue. Unused today; the dark value was already correct. */ --action-bg: rgba(190, 60, 39, 0.14); --action-on: #FFFFFF; /* ── Palette accents: exact brand values, for fills and tints only ── */ --coral: #F97360; --mustard: #F2C94C; --mustard-ink: #806200; /* any label that has to sit on or beside mustard */ --sage: #A7D7C5; --sky: #C7EBF5; --lavender: #DDD8F5; --sand: #F5EFE6; /* ── Status: above / at / below the England average ─────────────── */ --status-above: #36743F; --status-above-bg: rgba(54, 116, 63, 0.12); --status-below: #A9481F; /* The tint is the solid hue at low alpha, exactly as every other pair in this file is. It briefly carried the raw --coral tuple instead, which put a coral-pink wash behind terracotta text — the one mismatched pair in the palette. Alpha matches --status-above-bg so the two chips read as equal weight; at 0.15 the darker terracotta tint drops the label to 4.45:1 on Warm White. */ --status-below-bg: rgba(169, 72, 31, 0.12); --status-at: #5F6A75; --status-at-bg: rgba(95, 106, 117, 0.10); /* ── Charts ─────────────────────────────────────────────────────── */ --chart-1: #0F766E; --chart-2: #36743F; --chart-3: #806200; --chart-4: #A9481F; --chart-5: #2F6F8F; --chart-6: #5F4FA8; --chart-grid: #E5E7EB; --chart-reference: #5F6A75; /* ── Series: up to 8 schools compared at once ─────────────────────── Categorical, so wider than the four-hue rule — but every step is held to the same mid-dark tone and clears AA on the card, so they read as one family and work as legend text as well as chart lines. */ --series-1: #0F766E; --series-2: #36743F; --series-3: #806200; --series-4: #A9481F; --series-5: #2F6F8F; --series-6: #5F4FA8; --series-7: #0E7A86; --series-8: #8A4A6B; /* ── Phase: category, desaturated so it stays under the status hues ── */ --phase-primary: #0F766E; --phase-primary-bg: rgba(167, 215, 197, 0.40); --phase-primary-text: #0C5F58; --phase-secondary: #2F6F8F; --phase-secondary-bg: rgba(199, 235, 245, 0.55); --phase-secondary-text: #2A6180; --phase-all-through: #36743F; --phase-all-through-bg: rgba(54, 116, 63, 0.12); --phase-all-through-text: #2F6738; --phase-post16: #806200; --phase-post16-bg: rgba(242, 201, 76, 0.26); --phase-post16-text: #6E5500; --phase-nursery: #5F4FA8; --phase-nursery-bg: rgba(221, 216, 245, 0.55); --phase-nursery-text: #544396; /* ── Sunken surface: the footer, and anything meant to read as a deep teal anchor band. Stays teal in both themes on purpose — it is the "brand in action" card from the guideline. ─────────────────── */ --surface-sunken: #0C5F58; --on-sunken: #FFFFFF; --on-sunken-muted: #C9E4DB; --on-sunken-faint: #A9D6CA; --on-sunken-link: #C7EBF5; /* ── Medals: rankings podium ────────────────────────────────────── */ --medal-gold: #806200; --medal-silver: #6B7580; --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: 15, 118, 110; --action-rgb: 190, 60, 39; --coral-rgb: 249, 115, 96; --sage-rgb: 167, 215, 197; --status-above-rgb: 54, 116, 63; --status-below-rgb: 169, 72, 31; --ink-rgb: 28, 39, 49; --text-inverse-rgb: 255, 255, 255; --shadow-rgb: 28, 39, 49; --muted-rgb: 95, 106, 117; --phase-secondary-rgb: 47, 111, 143; --medal-silver-rgb: 107, 117, 128; --medal-bronze-rgb: 138, 90, 43; /* ── Elevation ──────────────────────────────────────────────────── */ --shadow-soft: 0 1px 2px rgba(28, 39, 49, 0.04), 0 2px 8px rgba(28, 39, 49, 0.05); --shadow-medium: 0 4px 18px rgba(28, 39, 49, 0.09); --shadow-strong: 0 10px 34px rgba(28, 39, 49, 0.13); --scrim: rgba(28, 39, 49, 0.5); /* ── Type ─────────────────────────────────────────────────────────── Manrope for headings and key messaging; Inter for body copy, every control and every figure. next/font expands each variable to the family plus its metric-matched fallback, and the classes sit on so :root can see them — see app/layout.tsx. */ --font-display: var(--font-manrope), -apple-system, BlinkMacSystemFont, sans-serif; --font-ui: var(--font-inter), -apple-system, BlinkMacSystemFont, sans-serif; --font-data: var(--font-inter), -apple-system, BlinkMacSystemFont, sans-serif; --font-prose: var(--font-inter), -apple-system, BlinkMacSystemFont, sans-serif; /* Type scale, straight from the guideline: H1 40/48, H2 32/40, H3 24/32, body large 16/24, body 14/20, small 12/16. */ --step--2: 0.75rem; --step--1: 0.875rem; --step-0: 1rem; --step-1: 1.125rem; --step-2: 1.5rem; --step-3: 2rem; --step-4: 2.5rem; --step-5: 3rem; /* ── Geometry & motion ────────────────────────────────────────────── "Soft shapes, rounded corners" — the guideline's geometry is markedly rounder than the old system's 3/6/10/16. */ --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 22px; --transition: 0.2s ease; --transition-slow: 0.4s ease; } /* * Dark theme. Not in the guideline, which is light-only — but the site already * ships one, so removing it would be a visible regression. Built from the same * tokens: teal and coral are lifted independently so each still clears AA on * the dark ground, and the ground itself keeps the Ink hue rather than going * to black. * * 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: #111A20; --bg-secondary: #16222A; --bg-card: #18242C; --surface-inverse: #E9EEF0; --text-primary: #E9EEF0; --text-secondary: #B4C2C7; --text-muted: #8B9AA1; /* 5.5:1 on the card */ --text-inverse: #111A20; --border: #26343D; --border-strong: #35454F; --brand: #5FC7BB; --brand-strong: #7BD6CC; --brand-stronger: #9BE3DB; --brand-bg: rgba(95, 199, 187, 0.14); --brand-on: #0A1418; --action: #F08A72; --action-strong: #F5A492; --action-stronger: #F8BFB1; --action-bg: rgba(240, 138, 114, 0.16); --action-on: #241009; --coral: #F08A72; --mustard: #F2C94C; --mustard-ink: #EFC658; --sage: #8FCBB7; --sky: #9CD4E6; --lavender: #C2BBEA; --sand: #1B2730; --status-above: #7FCB8A; --status-above-bg: rgba(127, 203, 138, 0.14); --status-below: #EFA184; --status-below-bg: rgba(239, 161, 132, 0.14); --status-at: #8B9AA1; --status-at-bg: rgba(139, 154, 161, 0.14); --chart-1: #5FC7BB; --chart-2: #7FCB8A; --chart-3: #EFC658; --chart-4: #EFA184; --chart-5: #8FB4D9; --chart-6: #B6A9DD; --chart-grid: #26343D; --chart-reference: #8B9AA1; --series-1: #5FC7BB; --series-2: #7FCB8A; --series-3: #EFC658; --series-4: #EFA184; --series-5: #8FB4D9; --series-6: #B6A9DD; --series-7: #6FD0DC; --series-8: #D99BB8; --phase-primary: #5FC7BB; --phase-primary-bg: rgba(95, 199, 187, 0.16); --phase-primary-text: #8ADACF; --phase-secondary: #8FB4D9; --phase-secondary-bg: rgba(143, 180, 217, 0.16); --phase-secondary-text: #AAC8E4; --phase-all-through: #7FCB8A; --phase-all-through-bg: rgba(127, 203, 138, 0.16); --phase-all-through-text: #9AD8A3; --phase-post16: #EFC658; --phase-post16-bg: rgba(242, 201, 76, 0.16); --phase-post16-text: #F2D179; --phase-nursery: #B6A9DD; --phase-nursery-bg: rgba(182, 169, 221, 0.16); --phase-nursery-text: #C8BEE6; --surface-sunken: #0C5F58; --on-sunken: #FFFFFF; --on-sunken-muted: #C9E4DB; --on-sunken-faint: #A9D6CA; --on-sunken-link: #C7EBF5; --medal-gold: #EFC658; --medal-silver: #A9B6BC; --medal-bronze: #C99070; --brand-rgb: 95, 199, 187; --action-rgb: 240, 138, 114; --coral-rgb: 240, 138, 114; --sage-rgb: 143, 203, 183; --status-above-rgb: 127, 203, 138; --status-below-rgb: 239, 161, 132; --ink-rgb: 233, 238, 240; --text-inverse-rgb: 17, 26, 32; --shadow-rgb: 0, 0, 0; --muted-rgb: 139, 154, 161; --phase-secondary-rgb: 143, 180, 217; --medal-silver-rgb: 169, 182, 188; --medal-bronze-rgb: 201, 144, 112; --shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.3); --shadow-medium: 0 4px 18px rgba(0, 0, 0, 0.45); --shadow-strong: 0 10px 34px rgba(0, 0, 0, 0.55); --scrim: rgba(4, 8, 10, 0.65); } } * { margin: 0; padding: 0; box-sizing: border-box; } html { scroll-behavior: smooth; } body { font-family: var(--font-ui); background: var(--bg-primary); color: var(--text-primary); line-height: 1.6; /* dvh (dynamic viewport) accounts for iOS Safari's collapsing toolbar; fall back to vh on older engines that don't recognise dvh. */ min-height: 100vh; min-height: 100dvh; /* Suppress the iOS Safari grey tap flash — explicit :active states below carry the press feedback instead. */ -webkit-tap-highlight-color: transparent; } /* Form controls don't inherit font or colour from their parent — the UA supplies its own. Without this they render in the system font at pure black, which is nearly invisible against --text-primary in light mode and completely invisible on the dark ground. */ input, select, textarea, button { font-family: inherit; color: inherit; } /* Every digit that could line up in a column does. This is a data product; proportional numerals in a results table read as amateur. */ /* The interface is mostly figures, so tabular is the default here rather than something each component has to remember. Running prose opts back out below — proportional figures read better in a sentence. */ .main, table, [data-numeric] { font-variant-numeric: tabular-nums; } /* Prose gets the serif; the interface never does. */ .prose { font-family: var(--font-prose); font-variant-numeric: normal; 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 states already; this handles plain anchors used as inline links and bare button elements. */ @media (hover: none) and (pointer: coarse) { a:active, button:active { opacity: 0.7; } } /* Reserve space for the fixed mobile bottom tab bar (56px + safe-area inset). */ @media (max-width: 640px) { body { padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px)); } } /* Skip link — visible only on focus for keyboard users */ .skip-link { position: absolute; top: -100px; left: 1rem; z-index: 10000; padding: 0.5rem 1rem; background: var(--surface-inverse); color: var(--text-inverse); font-size: 0.875rem; font-weight: 600; border-radius: var(--radius-md); text-decoration: none; transition: top 0.15s ease; } .skip-link:focus { top: 0.5rem; } /* Focus indicators — brand hue, visible on both grounds */ :focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: var(--radius-sm); } /* ================================================================ Shared button classes — use these across all components ================================================================ */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.375rem; padding: 0.5rem 1rem; font-family: var(--font-ui); font-size: 0.875rem; font-weight: 600; line-height: 1; border-radius: var(--radius-md); border: 1px solid transparent; cursor: pointer; transition: background var(--transition), border-color var(--transition), color var(--transition); text-decoration: none; white-space: nowrap; } .btn:disabled { opacity: 0.5; cursor: not-allowed; } /* Primary: brand fill — main CTAs (Search, Compare now) */ .btn-primary { background: var(--brand); color: var(--brand-on); border-color: var(--brand); } .btn-primary:hover:not(:disabled) { background: var(--brand-strong); border-color: var(--brand-strong); } .btn-primary:active:not(:disabled) { background: var(--brand-stronger); border-color: var(--brand-stronger); } /* * Action: coral fill — the one decisive action on a page, and only that. * * The guideline gives coral to "calls to action, highlights, important * buttons" while its own UI-components sheet draws the primary button in * teal. Both are honoured by making this a scarce, separate class: teal is * the default primary, coral marks the single action a page exists for — * the search submit. Use it more than once per screen and it stops meaning * anything. */ .btn-action { background: var(--action); color: var(--action-on); border-color: var(--action); } .btn-action:hover:not(:disabled) { background: var(--action-strong); border-color: var(--action-strong); } .btn-action:active:not(:disabled) { background: var(--action-stronger); border-color: var(--action-stronger); } /* Secondary: brand outline — supporting actions (Add to shortlist) */ .btn-secondary { background: transparent; color: var(--brand); border: 1px solid var(--brand); } .btn-secondary:hover:not(:disabled) { background: var(--brand-bg); } /* Tertiary: quiet — low-emphasis (View, Clear) */ .btn-tertiary { background: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border); } .btn-tertiary:hover:not(:disabled) { background: var(--border); color: var(--text-primary); } /* Active toggle state — the shortlist button once a school is on the list */ .btn-active { background: var(--brand-bg); color: var(--brand); border: 1px solid var(--brand); } .btn-active:hover:not(:disabled) { background: transparent; color: var(--text-secondary); border-color: var(--border-strong); } /* Small variant */ .btn-sm { padding: 0.3rem 0.625rem; font-size: 0.8125rem; } /* * Leaflet ships its own palette — a #ddd tile backdrop, #333 attribution text * and a #0078A8 link blue that is the most saturated colour anywhere on the * site and belongs to no part of this system. Pull the map chrome onto the * tokens. This matters most in dark mode, where Leaflet's white attribution * bar would otherwise sit on a near-black page. * * The tiles themselves stay as OSM renders them; only the chrome is ours. * * Every selector here is prefixed with `html` on purpose. leaflet.css is * imported from a client component, so its chunk loads AFTER globals.css; at * equal specificity the later sheet wins and these overrides lose silently. * The `html` prefix takes them to 0,1,1 so load order stops mattering. */ html .leaflet-container { background: var(--bg-secondary); font-family: var(--font-ui); } html .leaflet-control-attribution { background: rgba(var(--text-inverse-rgb), 0.82); color: var(--text-muted); } html .leaflet-control-attribution a { color: var(--brand); } html .leaflet-bar a { background: var(--bg-card); color: var(--text-primary); border-bottom-color: var(--border); } html .leaflet-bar a:hover { background: var(--bg-secondary); color: var(--text-primary); } /* Main content column */ .main { max-width: 1400px; margin: 0 auto; padding: 1.25rem 1.5rem; min-width: 0; overflow-x: clip; } @media (max-width: 768px) { .main { padding: 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; } *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } }