feat(brand): adopt the schoolcompare identity across the site
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 13s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 4m29s
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m3s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 13s
PR Checks / Build Frontend (no push) (pull_request) Successful in 44s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 4m29s
Implements the supplied brand guideline: the pin-and-leaf mark, the lowercase schoolcompare wordmark, Manrope for headings and Inter for everything a person reads a number from, the deep-teal/coral/mustard/sage palette, and the rounder geometry the guideline asks for. The landing hero is rebuilt to the concept: a Sand panel carrying the proposition, the search and a trust line, with the brand landscape bleeding to the panel's right edge, followed by the four value props. Concept vs reality — where the guideline describes something this product does not have, the real feature takes its place and the decision is recorded in a code comment next to it: - The Thriving Score is not implemented. Four of its five sub-scores (pastoral, enrichment, community, facilities) have no source in DfE or Ofsted data, and its band names are Ofsted's own, which would sit beside the real Ofsted grade and be indistinguishable from it. - Guides, About, Favourites and Sign in are not added to the nav — there are no such pages and no accounts system. Rankings and Admissions, which the guideline omits, keep their places because they exist. - The fourth value prop is "Never miss a deadline" rather than "Save & revisit", which would need shortlisting. Two deviations from the supplied swatches, both forced by contrast and both confined to text and fills. Coral #F97360 under a white label measures 2.75:1 against a 4.5 floor, so --action is a darkened #BE3C27 and the original coral survives as --coral for tints, borders and illustration. Mustard #F2C94C is 1.5:1 as text, so --mustard-ink carries labels while --mustard stays exact for fills and rings. Every token clears AA on all three light grounds; Sand is the binding one, so several values sit a step darker than white alone would need. The guideline is light-only, but the site already ships a dark theme, so it is rebuilt from the same tokens rather than dropped. Verified: tsc clean, 159/159 unit tests, production build green, and the generated share card and app icon render from the new mark. The hero and the site chrome were rendered from the compiled CSS and the prerendered markup to confirm Manrope and Inter actually resolve — a font token that silently falls back has shipped here before.
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { BARS } from '@/components/Logo';
|
||||
import { markDataUri } 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.
|
||||
//
|
||||
// Teal ground with the mark knocked out in white, matching the app icon in the
|
||||
// guideline's "brand in action" row.
|
||||
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
|
||||
const TEAL = '#0F766E';
|
||||
|
||||
export default function AppleIcon() {
|
||||
return new ImageResponse(
|
||||
@@ -20,25 +22,11 @@ export default function AppleIcon() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#f2f1ed',
|
||||
background: TEAL,
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative', width: 40 * SCALE, height: 40 * SCALE, display: 'flex' }}>
|
||||
{BARS.map(([x, y, w, h, isSchool], i) => (
|
||||
<div
|
||||
key={x}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: x * SCALE,
|
||||
top: y * SCALE,
|
||||
width: w * SCALE,
|
||||
height: h * SCALE,
|
||||
borderRadius: 3,
|
||||
background: isSchool ? '#584a9b' : COHORT[i],
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={markDataUri('#FFFFFF', TEAL)} width={116} height={116} alt="" />
|
||||
</div>
|
||||
),
|
||||
size
|
||||
|
||||
+262
-200
@@ -1,173 +1,193 @@
|
||||
/*
|
||||
* SchoolCompare.co.uk — "Cohort"
|
||||
* SchoolCompare — "Find the right school. For their future."
|
||||
*
|
||||
* 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.
|
||||
* 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.
|
||||
*
|
||||
* Colour has exactly three jobs and they never borrow each other's hues:
|
||||
* brand (iris) interactive + identity. Never carries valence.
|
||||
* status (teal / amber) valence — teal is above/good, amber is
|
||||
* below/needs-attention. Never a CTA.
|
||||
* 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.
|
||||
*
|
||||
* Escalate within status by weight, not by reaching for a new hue: a tinted
|
||||
* amber chip for "requires improvement", a solid amber one for "inadequate".
|
||||
*
|
||||
* Teal/amber rather than green/red so the above/below signal survives every
|
||||
* form of colour blindness — the audience includes parents reading closely.
|
||||
* 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: #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 */
|
||||
--bg-primary: #FAFAF8; /* Warm White */
|
||||
--bg-secondary: #F5EFE6; /* Sand — hero panels, sunken rows */
|
||||
--bg-card: #FFFFFF;
|
||||
--surface-inverse: #0F766E;
|
||||
|
||||
/* ── Ink ────────────────────────────────────────────────────────── */
|
||||
--text-primary: #16202a;
|
||||
--text-secondary: #4a545f;
|
||||
--text-muted: #5c6570; /* 5.3:1 on --bg-primary */
|
||||
--text-inverse: #f7f7f4;
|
||||
--text-primary: #1C2731; /* Ink */
|
||||
--text-secondary: #4A5560;
|
||||
--text-muted: #5F6A75; /* 4.8:1 on Sand, the worst ground */
|
||||
--text-inverse: #FFFFFF;
|
||||
|
||||
/* ── Line ───────────────────────────────────────────────────────── */
|
||||
--border: #dedcd5;
|
||||
--border-strong: #cdcac1;
|
||||
--border: #E5E7EB; /* Light Grey */
|
||||
--border-strong: #D3D7DD;
|
||||
|
||||
/* ── 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 */
|
||||
/* ── 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;
|
||||
|
||||
/* ── Status: above / at / below the comparison point ────────────── */
|
||||
--status-above: #0b625a; /* 6.6:1 on --bg-primary, 5.1:1 on its own tint */
|
||||
--status-above-bg: rgba(11, 98, 90, 0.10);
|
||||
--status-below: #7f4a00; /* 6.3:1 on --bg-primary, 5.1:1 on its own tint */
|
||||
--status-below-bg: rgba(127, 74, 0, 0.11);
|
||||
--status-at: var(--text-muted);
|
||||
--status-at-bg: rgba(92, 101, 112, 0.10);
|
||||
/* ── 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;
|
||||
--action-bg: rgba(249, 115, 96, 0.14);
|
||||
--action-on: #FFFFFF;
|
||||
|
||||
/* ── Charts: the same three hues, extended by lightness only ────── */
|
||||
--chart-1: #584a9b;
|
||||
--chart-2: #0b625a;
|
||||
--chart-3: #7f4a00;
|
||||
--chart-4: #8a7cc9;
|
||||
--chart-5: #3e9c92;
|
||||
--chart-6: #c9903d;
|
||||
--chart-grid: #dedcd5;
|
||||
--chart-reference: #5c6570; /* national/LA reference lines */
|
||||
/* ── 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;
|
||||
--status-below-bg: rgba(249, 115, 96, 0.15);
|
||||
--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 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: #0b625a;
|
||||
--series-3: #7f4a00;
|
||||
--series-4: #a03a5e;
|
||||
--series-5: #2f5f8f;
|
||||
--series-6: #4a6b2f;
|
||||
--series-7: #7a3f7a;
|
||||
--series-8: #97442a;
|
||||
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: #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;
|
||||
--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 dark
|
||||
anchor band. Deliberately NOT --surface-inverse: "inverse" flips
|
||||
with the theme, which turned the footer into a light slab at the
|
||||
bottom of a dark page. This stays dark in both. ──────────────── */
|
||||
--surface-sunken: #16202a;
|
||||
--on-sunken: #e4e2dc;
|
||||
--on-sunken-muted: #b3b8bd;
|
||||
--on-sunken-faint: #7f878f;
|
||||
--on-sunken-link: #b9aeef;
|
||||
/* ── 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. Metal, not palette — kept legible on
|
||||
both grounds rather than literally gold/silver/bronze. ───────── */
|
||||
--medal-gold: #a67c00;
|
||||
--medal-silver: #6f7580;
|
||||
--medal-bronze: #8a5a2b;
|
||||
/* ── 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: 88, 74, 155;
|
||||
--status-above-rgb: 11, 98, 90;
|
||||
--status-below-rgb: 127, 74, 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;
|
||||
--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(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);
|
||||
--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 ───────────────────────────────────────────────────────── */
|
||||
/* next/font already expands --font-schibsted to the family plus its
|
||||
metric-matched fallback, so naming the family again here only made the
|
||||
stack say it twice. It read like a safety net but wasn't one: a var()
|
||||
with no fallback that resolves to nothing invalidates the whole
|
||||
declaration, so the literal after it never gets a turn. The real
|
||||
safeguard is that these classes sit on <html>, where :root can see
|
||||
them — see app/layout.tsx. */
|
||||
--font-display: var(--font-schibsted), -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--font-ui: var(--font-schibsted), -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--font-data: var(--font-schibsted), -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--font-prose: var(--font-literata), Georgia, serif;
|
||||
/* ── 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
|
||||
<html> 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, 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;
|
||||
/* 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.2rem;
|
||||
--step-2: 1.44rem;
|
||||
--step-3: 1.728rem;
|
||||
--step-4: 2.074rem;
|
||||
--step-5: 2.488rem;
|
||||
--step-1: 1.125rem;
|
||||
--step-2: 1.5rem;
|
||||
--step-3: 2rem;
|
||||
--step-4: 2.5rem;
|
||||
--step-5: 3rem;
|
||||
|
||||
/* ── Geometry & motion ──────────────────────────────────────────── */
|
||||
--radius-sm: 3px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 10px;
|
||||
--radius-xl: 16px;
|
||||
/* ── 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 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.
|
||||
* 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.
|
||||
@@ -176,90 +196,108 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
--bg-primary: #10151b;
|
||||
--bg-secondary: #161c24;
|
||||
--bg-card: #171d25;
|
||||
--surface-inverse: #e4e9ee;
|
||||
--bg-primary: #111A20;
|
||||
--bg-secondary: #16222A;
|
||||
--bg-card: #18242C;
|
||||
--surface-inverse: #E9EEF0;
|
||||
|
||||
--text-primary: #e4e9ee;
|
||||
--text-secondary: #b4bec8;
|
||||
--text-muted: #8d99a6; /* 6.4:1 on --bg-primary */
|
||||
--text-inverse: #10151b;
|
||||
--text-primary: #E9EEF0;
|
||||
--text-secondary: #B4C2C7;
|
||||
--text-muted: #8B9AA1; /* 5.5:1 on the card */
|
||||
--text-inverse: #111A20;
|
||||
|
||||
--border: #28313b;
|
||||
--border-strong: #38434f;
|
||||
--border: #26343D;
|
||||
--border-strong: #35454F;
|
||||
|
||||
--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;
|
||||
--brand: #5FC7BB;
|
||||
--brand-strong: #7BD6CC;
|
||||
--brand-stronger: #9BE3DB;
|
||||
--brand-bg: rgba(95, 199, 187, 0.14);
|
||||
--brand-on: #0A1418;
|
||||
|
||||
--status-above: #4fc0b0; /* 8.4:1 on --bg-primary, 4.8:1 on its own tint */
|
||||
--status-above-bg: rgba(79, 192, 176, 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);
|
||||
--action: #F08A72;
|
||||
--action-strong: #F5A492;
|
||||
--action-stronger: #F8BFB1;
|
||||
--action-bg: rgba(240, 138, 114, 0.16);
|
||||
--action-on: #241009;
|
||||
|
||||
--chart-1: #9c8ce8;
|
||||
--chart-2: #4fc0b0;
|
||||
--chart-3: #d99b2e;
|
||||
--chart-4: #6f5fc4;
|
||||
--chart-5: #2b8a7e;
|
||||
--chart-6: #a97420;
|
||||
--chart-grid: #28313b;
|
||||
--chart-reference: #8d99a6;
|
||||
--coral: #F08A72;
|
||||
--mustard: #F2C94C;
|
||||
--mustard-ink: #EFC658;
|
||||
--sage: #8FCBB7;
|
||||
--sky: #9CD4E6;
|
||||
--lavender: #C2BBEA;
|
||||
--sand: #1B2730;
|
||||
|
||||
--series-1: #9c8ce8;
|
||||
--series-2: #4fc0b0;
|
||||
--series-3: #d99b2e;
|
||||
--series-4: #e07a9d;
|
||||
--series-5: #6fa8dc;
|
||||
--series-6: #94c46b;
|
||||
--series-7: #c48bc4;
|
||||
--series-8: #e08a6b;
|
||||
--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);
|
||||
|
||||
--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;
|
||||
--chart-1: #5FC7BB;
|
||||
--chart-2: #7FCB8A;
|
||||
--chart-3: #EFC658;
|
||||
--chart-4: #EFA184;
|
||||
--chart-5: #8FB4D9;
|
||||
--chart-6: #B6A9DD;
|
||||
--chart-grid: #26343D;
|
||||
--chart-reference: #8B9AA1;
|
||||
|
||||
--surface-sunken: #0a0e13;
|
||||
--on-sunken: #d8dde2;
|
||||
--on-sunken-muted: #9aa4ae;
|
||||
--on-sunken-faint: #6d777f;
|
||||
--on-sunken-link: #b0a3ee;
|
||||
--series-1: #5FC7BB;
|
||||
--series-2: #7FCB8A;
|
||||
--series-3: #EFC658;
|
||||
--series-4: #EFA184;
|
||||
--series-5: #8FB4D9;
|
||||
--series-6: #B6A9DD;
|
||||
--series-7: #6FD0DC;
|
||||
--series-8: #D99BB8;
|
||||
|
||||
--medal-gold: #d4a72c;
|
||||
--medal-silver: #a8b0bb;
|
||||
--medal-bronze: #c08552;
|
||||
--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;
|
||||
|
||||
--brand-rgb: 156, 140, 232;
|
||||
--status-above-rgb: 79, 192, 176;
|
||||
--status-below-rgb: 217, 155, 46;
|
||||
--ink-rgb: 228, 233, 238;
|
||||
--text-inverse-rgb: 16, 21, 27;
|
||||
--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: 141, 153, 166;
|
||||
--phase-secondary-rgb: 162, 148, 196;
|
||||
--medal-silver-rgb: 168, 176, 187;
|
||||
--medal-bronze-rgb: 192, 133, 82;
|
||||
--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 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);
|
||||
--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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,6 +440,30 @@ table,
|
||||
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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="26" width="6" height="12" rx="1" fill="#16202a" fill-opacity="0.22"/>
|
||||
<rect x="10" y="16" width="6" height="22" rx="1" fill="#16202a" fill-opacity="0.38"/>
|
||||
<rect x="18" y="8" width="6" height="30" rx="1" fill="#16202a" fill-opacity="0.55"/>
|
||||
<rect x="26" y="14" width="6" height="24" rx="1" fill="#584a9b"/>
|
||||
<rect x="34" y="28" width="4" height="10" rx="1" fill="#16202a" fill-opacity="0.22"/>
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 30 C15 35.6 11.6 40.2 7.8 42.8" stroke="#0F766E" stroke-width="8.5" stroke-linecap="round" fill="none"/>
|
||||
<circle cx="26.5" cy="18.5" r="16.5" fill="#0F766E"/>
|
||||
<path d="M26.5 29.5 C26.5 25 26.6 21 27 18" stroke="#FFFFFF" stroke-width="2.6" stroke-linecap="round" fill="none"/>
|
||||
<path d="M27 19.6 C28.2 13.8 32.4 9.8 37.6 9.4 C38 15.6 33.8 20.8 27 19.6 Z" fill="#FFFFFF"/>
|
||||
<path d="M26.4 24.6 C25.2 20 21.2 17 16.4 17.1 C16.2 21.8 19.8 25.8 26.4 24.6 Z" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 521 B After Width: | Height: | Size: 594 B |
+15
-16
@@ -1,5 +1,5 @@
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { Schibsted_Grotesk, Literata } from 'next/font/google';
|
||||
import { Manrope, Inter } from 'next/font/google';
|
||||
import Script from 'next/script';
|
||||
import { Navigation } from '@/components/Navigation';
|
||||
import { Footer } from '@/components/Footer';
|
||||
@@ -7,23 +7,22 @@ import { ComparisonToast } from '@/components/ComparisonToast';
|
||||
import { ComparisonProvider } from '@/context/ComparisonProvider';
|
||||
import './globals.css';
|
||||
|
||||
// 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({
|
||||
// Manrope carries headings and key messaging — the guideline's "friendly,
|
||||
// modern personality". It never sets body copy or a control.
|
||||
const manrope = Manrope({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-schibsted',
|
||||
weight: ['500', '600', '700'],
|
||||
variable: '--font-manrope',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
// Literata is for prose only — the admissions guide, methodology notes, the
|
||||
// editorial sections. It never touches a number or a control.
|
||||
const literata = Literata({
|
||||
// Inter carries body copy, every control and every figure — chosen in the
|
||||
// guideline for clarity and legibility. Its tabular numerals hold a column
|
||||
// properly, which matters on a page that is mostly percentages.
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '600'],
|
||||
style: ['normal', 'italic'],
|
||||
variable: '--font-literata',
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-inter',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
@@ -36,8 +35,8 @@ export const viewport: Viewport = {
|
||||
// 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: '#f2f1ed' },
|
||||
{ media: '(prefers-color-scheme: dark)', color: '#10151b' },
|
||||
{ media: '(prefers-color-scheme: light)', color: '#FAFAF8' },
|
||||
{ media: '(prefers-color-scheme: dark)', color: '#111A20' },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -87,7 +86,7 @@ export default function RootLayout({
|
||||
// at :root, so --font-display computed to the guaranteed-invalid value and
|
||||
// every font-family that referenced it silently fell back — the whole site
|
||||
// rendered in Times.
|
||||
<html lang="en" className={`${schibsted.variable} ${literata.variable}`}>
|
||||
<html lang="en" className={`${manrope.variable} ${inter.variable}`}>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://analytics.schoolcompare.co.uk" />
|
||||
<link rel="preconnect" href="https://api.postcodes.io" />
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { BARS } from '@/components/Logo';
|
||||
import { markDataUri } 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.
|
||||
* The card is the brand's own proposition on the Sand ground, with the mark
|
||||
* and a suggestion of the illustration's rolling landscape along the bottom.
|
||||
* It states the promise rather than decorating it.
|
||||
*/
|
||||
export const alt = 'SchoolCompare — compare every school in England against the national picture';
|
||||
export const alt = 'schoolcompare — find the school where your child will flourish';
|
||||
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;
|
||||
const SAND = '#F5EFE6';
|
||||
const INK = '#1C2731';
|
||||
const MUTED = '#5F6A75';
|
||||
const TEAL = '#0F766E';
|
||||
const SAGE = '#A7D7C5';
|
||||
const SAGE_DEEP = '#7FC3AC';
|
||||
const SAGE_PALE = '#D6EDE2';
|
||||
|
||||
async function font(file: string) {
|
||||
return readFile(join(process.cwd(), 'assets', file));
|
||||
@@ -34,8 +30,8 @@ async function font(file: string) {
|
||||
|
||||
export default async function OpengraphImage() {
|
||||
const [regular, bold] = await Promise.all([
|
||||
font('SchibstedGrotesk-Regular.ttf'),
|
||||
font('SchibstedGrotesk-Bold.ttf'),
|
||||
font('Manrope-Regular.ttf'),
|
||||
font('Manrope-Bold.ttf'),
|
||||
]);
|
||||
|
||||
return new ImageResponse(
|
||||
@@ -47,85 +43,62 @@ export default async function OpengraphImage() {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
background: PAPER,
|
||||
padding: '58px 72px 52px',
|
||||
fontFamily: 'Schibsted',
|
||||
background: SAND,
|
||||
padding: '58px 72px 0',
|
||||
fontFamily: 'Manrope',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Rolling hills along the bottom edge — the illustration style,
|
||||
reduced to the one gesture that survives at thumbnail size. */}
|
||||
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: 210, display: 'flex' }}>
|
||||
<svg width="1200" height="210" viewBox="0 0 1200 210">
|
||||
<path d="M0 92 C 200 56 380 100 560 78 C 760 54 940 92 1200 62 L1200 210 L0 210 Z" fill={SAGE_PALE} />
|
||||
<path d="M0 132 C 230 96 420 140 620 118 C 830 94 1000 130 1200 106 L1200 210 L0 210 Z" fill={SAGE} />
|
||||
<path d="M0 176 C 260 146 470 186 690 166 C 900 146 1040 174 1200 158 L1200 210 L0 210 Z" fill={SAGE_DEEP} />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Lockup */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div style={{ position: 'relative', width: 40, height: 40, display: 'flex' }}>
|
||||
{BARS.map(([x, y, w, h, isSchool], i) => (
|
||||
<div
|
||||
key={x}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: x,
|
||||
top: y,
|
||||
width: w,
|
||||
height: h,
|
||||
borderRadius: 1.5,
|
||||
background: isSchool ? BRAND : INK,
|
||||
opacity: isSchool ? 1 : [0.22, 0.38, 0.55, 1, 0.22][i],
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', fontSize: 29, fontWeight: 700, letterSpacing: '-0.028em' }}>
|
||||
<span style={{ color: INK }}>School</span>
|
||||
<span style={{ color: BRAND }}>Compare</span>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={markDataUri(TEAL, SAND)} width={52} height={52} alt="" />
|
||||
<div style={{ display: 'flex', fontSize: 34, fontWeight: 700, letterSpacing: '-0.04em' }}>
|
||||
<span style={{ color: INK }}>school</span>
|
||||
<span style={{ color: TEAL }}>compare</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Proposition. Sized so the headline holds two lines and the subhead
|
||||
one — an orphaned word reads as an accident at this size. */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 18, paddingBottom: 236 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
fontSize: 66,
|
||||
flexWrap: 'wrap',
|
||||
fontSize: 72,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.036em',
|
||||
lineHeight: 1.03,
|
||||
letterSpacing: '-0.035em',
|
||||
lineHeight: 1.05,
|
||||
color: INK,
|
||||
maxWidth: 1010,
|
||||
maxWidth: 900,
|
||||
}}
|
||||
>
|
||||
A score means nothing until you see the spread.
|
||||
<span style={{ marginRight: 18 }}>Find the school where</span>
|
||||
<span style={{ marginRight: 18 }}>your child will</span>
|
||||
<span style={{ color: TEAL }}>flourish.</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', fontSize: 26, color: MUTED, letterSpacing: '-0.005em' }}>
|
||||
<div style={{ display: 'flex', fontSize: 27, color: MUTED, letterSpacing: '-0.005em' }}>
|
||||
24,000+ schools in England · SATs, GCSEs, Ofsted and admissions
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* The device, at poster scale. Same object as the mark above. */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 7, height: 120 }}>
|
||||
{SPREAD.map((h, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: `${h}%`,
|
||||
borderRadius: 2,
|
||||
background: i === LIT ? BRAND : INK,
|
||||
opacity: i === LIT ? 1 : 0.15,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 18, color: MUTED, letterSpacing: '0.09em' }}>
|
||||
<span>NATIONAL SPREAD</span>
|
||||
<span style={{ letterSpacing: '-0.005em' }}>schoolcompare.co.uk</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
...size,
|
||||
fonts: [
|
||||
{ name: 'Schibsted', data: regular, weight: 400, style: 'normal' },
|
||||
{ name: 'Schibsted', data: bold, weight: 700, style: 'normal' },
|
||||
{ name: 'Manrope', data: regular, weight: 400, style: 'normal' },
|
||||
{ name: 'Manrope', data: bold, weight: 700, style: 'normal' },
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user