PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m5s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 46s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 2m25s
The homepage made four statements that were not true, carried elements that
asked nothing of anyone, and had a hero illustration that broke in both the
places it had to work.
Claims, all verified against the code or the API:
* "24,000+ schools" (three places) against a real 27,230. The fact box meant
to show the live figure rendered its own fallback on every request, because
DataInfoResponse declared a `total_schools` field the API has never sent —
it sends `unique_schools`. The fetch succeeded; only that field was
undefined, so nothing threw and nothing failed. The interface, not the
code, was the thing that was wrong.
* "Up to three schools side by side" against MAX_SCHOOLS = 5, contradicting a
card 400px below it that correctly said five.
* "Class sizes" — data the codebase has never held. That copy line was the
only hit in a full-repo grep.
* Invented results and Ofsted grades attributed to two real, named schools
in the compare preview.
Also one feature, three words: Compare (nav), shortlist (footer), pin (cards).
Settled on Compare everywhere. And <title> was the bare string "Home".
Cut: the trust line (repeated the coverage figure one paragraph after the hero
gave it, behind three decorative dots), the "Start exploring" row (three links
to two destinations already in the nav), the six-row coverage table, and three
of the four countdown cards — which gave the page's largest numeral to dates up
to 245 days away, two of them offer days, which cannot be missed. All four
dates remain, at proportionate weight. Value-prop titles drop from <h2> to <p>;
they were outranking the page's real headings in the document outline.
Rhythm: the gaps between the seven landing bands were 24/32/24/16/48/32/16px,
each band setting its own margin, with four different section-header
treatments between them. The page container now owns one gap, and there is one
header pattern. An e2e test asserts the gaps are identical.
Illustration: it kept a fixed light palette in both themes, which left a pale
sky slab as the brightest object on a near-black page, out-shouting the H1 and
the search box. It now reads from --ill-* tokens with a dark re-grade. And the
hero slot ranges from 1.34:1 to 4.9:1 across breakpoints, which no single
composition survives under `slice` — at 860x176 a 540x520 scene shows only its
bottom 110 units, so the schoolhouse was cropped away entirely on phones,
leaving hills and a pin pointing at nothing. There are now two compositions,
each drawn against the crop window its own breakpoint produces, with CSS
showing one. Both are static server-rendered SVG.
The deadline bar renders on the server rather than on hydrate. The effect-based
version needed a reserved height, and one guessed number cannot cover a block
whose supporting line wraps differently at every width — measured, it was short
at all four, shifting the page up to 108px on a phone.
Verified on the built output through an offline render harness (no local
server): real compiled CSS, real rendered markup, four widths, both themes.
tsc clean, 159/159 unit tests, build green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1476 lines
30 KiB
CSS
1476 lines
30 KiB
CSS
.homeView {
|
|
width: 100%;
|
|
}
|
|
|
|
/* ── Page rhythm ───────────────────────────────────────────────────────────
|
|
The landing page owns the vertical gap between its bands; no band sets its
|
|
own top or bottom margin. Previously each one did, and the gaps came out as
|
|
24 / 32 / 24 / 16 / 48 / 32 / 16px — no scale, which is most of the reason a
|
|
designed page read as a stack of unrelated strips. If you add a band here,
|
|
give it padding and a ground, never a margin. */
|
|
.landing {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--section-gap);
|
|
}
|
|
|
|
/* The hero and the four reasons under it are one thought, so they sit closer
|
|
to each other than to the next band. This is the only place that overrides
|
|
the page gap, and it does it by grouping rather than by re-spacing. */
|
|
.heroGroup {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2rem;
|
|
}
|
|
|
|
/* ── Section header ────────────────────────────────────────────────────────
|
|
One pattern for every band: kicker, heading, optional aside. */
|
|
.sectionHead {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto;
|
|
align-items: baseline;
|
|
column-gap: 1.5rem;
|
|
margin-bottom: var(--section-head-gap);
|
|
}
|
|
|
|
.sectionKicker {
|
|
grid-column: 1 / -1;
|
|
font-size: var(--step--2);
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--brand-strong);
|
|
margin: 0 0 0.35rem;
|
|
}
|
|
|
|
.sectionHeading {
|
|
font-family: var(--font-display);
|
|
font-size: var(--step-3);
|
|
font-weight: 700;
|
|
line-height: 1.2;
|
|
letter-spacing: -0.02em;
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
max-width: 24ch;
|
|
text-wrap: balance;
|
|
}
|
|
|
|
.sectionAside {
|
|
font-size: var(--step--1);
|
|
color: var(--text-muted);
|
|
margin: 0;
|
|
text-align: right;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.sectionHead {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.sectionHeading {
|
|
font-size: var(--step-2);
|
|
max-width: none;
|
|
}
|
|
.sectionAside {
|
|
text-align: left;
|
|
margin-top: 0.4rem;
|
|
}
|
|
}
|
|
|
|
/* ── Hero ──────────────────────────────────────────────────────────────────
|
|
A Sand panel: proposition and search on the left, the brand landscape
|
|
bleeding to the panel's right edge. The illustration is decorative, so on
|
|
phones it drops to a short band under the content rather than competing
|
|
with the search for the fold. */
|
|
.heroPanel {
|
|
display: grid;
|
|
grid-template-columns: 1.12fr 0.88fr;
|
|
align-items: stretch;
|
|
background: var(--bg-secondary);
|
|
border-radius: var(--radius-xl);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.heroContent {
|
|
align-self: center;
|
|
padding: 3rem 1.5rem 3rem 3rem;
|
|
min-width: 0;
|
|
}
|
|
|
|
.heroArt {
|
|
position: relative;
|
|
min-width: 0;
|
|
/* The scene needs height to read; without a floor it collapses to the
|
|
content column's height on short viewports. */
|
|
min-height: 24rem;
|
|
}
|
|
|
|
/* Both compositions fill the slot; the breakpoint below picks one. */
|
|
.heroArtWide,
|
|
.heroArtBand {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
|
|
.heroArt svg {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
}
|
|
|
|
/* The band composition only exists below the one-column breakpoint — above
|
|
it the slot is tall and the wide drawing is the right picture. */
|
|
.heroArtBand {
|
|
display: none;
|
|
}
|
|
|
|
.heroEyebrow {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
font-size: 0.72rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--brand);
|
|
background: rgba(var(--brand-rgb), 0.1);
|
|
padding: 0.3rem 0.7rem;
|
|
border-radius: 999px;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.heroEyebrowDot {
|
|
display: inline-block;
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: var(--brand);
|
|
}
|
|
|
|
.heroTitle {
|
|
font-size: var(--step-4);
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
margin-bottom: 0.75rem;
|
|
line-height: 1.12;
|
|
letter-spacing: -0.03em;
|
|
font-family: var(--font-display);
|
|
max-width: 18ch;
|
|
}
|
|
|
|
/* The em is a colour change, not an emphasis change — Manrope has no italic
|
|
in the loaded weights, so leaving font-style alone would synthesise a slant. */
|
|
.heroEmph {
|
|
color: var(--brand);
|
|
font-style: normal;
|
|
}
|
|
|
|
.heroDescription {
|
|
font-size: var(--step-1);
|
|
color: var(--text-secondary);
|
|
margin: 0 0 1.5rem;
|
|
max-width: 46ch;
|
|
line-height: 1.55;
|
|
}
|
|
|
|
.heroDescription strong {
|
|
color: var(--text-primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.heroContent {
|
|
padding: 2.25rem 1.5rem 2.25rem 2rem;
|
|
}
|
|
.heroArt {
|
|
min-height: 20rem;
|
|
}
|
|
}
|
|
|
|
/* One column: content first, then a short band of landscape. */
|
|
@media (max-width: 860px) {
|
|
.heroPanel {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.heroContent {
|
|
padding: 2rem 1.5rem 1.75rem;
|
|
}
|
|
/* One column: the slot becomes a wide, short band, so swap in the drawing
|
|
composed for that crop. See the note at the top of Illustration.tsx. */
|
|
.heroArt {
|
|
min-height: 11rem;
|
|
order: 2;
|
|
}
|
|
.heroArtWide {
|
|
display: none;
|
|
}
|
|
.heroArtBand {
|
|
display: block;
|
|
}
|
|
.heroTitle {
|
|
font-size: var(--step-3);
|
|
max-width: none;
|
|
}
|
|
.heroDescription {
|
|
font-size: var(--step-0);
|
|
max-width: none;
|
|
}
|
|
}
|
|
|
|
/* Above the fold on phones, every line costs. Drop the eyebrow tag and the
|
|
long coverage sentence, leaving the proposition and the search — the data
|
|
provenance is carried by the "Official & trusted" prop immediately below
|
|
the hero (audit P1.7, feeds the 46% home exit rate). */
|
|
@media (max-width: 640px) {
|
|
.heroPanel {
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
.heroContent {
|
|
padding: 1.25rem 1.1rem 1.5rem;
|
|
}
|
|
.heroEyebrow,
|
|
.heroDescriptionFull {
|
|
display: none;
|
|
}
|
|
.heroDescription {
|
|
font-size: 0.9rem;
|
|
line-height: 1.45;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.heroTitle {
|
|
font-size: 1.75rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
.heroArt {
|
|
min-height: 8.5rem;
|
|
}
|
|
}
|
|
|
|
/* ── Value props ───────────────────────────────────────────────────────────
|
|
Four reasons, straight from the guideline. The fourth is ours rather than
|
|
theirs — see the comment on VALUE_PROPS in HomeView.tsx. */
|
|
.valueProps {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 1.25rem;
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.valueProp {
|
|
display: flex;
|
|
gap: 0.8rem;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.propIcon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 0 0 2.6rem;
|
|
width: 2.6rem;
|
|
height: 2.6rem;
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.propIconTrust {
|
|
background: var(--brand-bg);
|
|
color: var(--brand);
|
|
}
|
|
.propIconCompare {
|
|
background: rgba(var(--sage-rgb), 0.42);
|
|
color: var(--status-above);
|
|
}
|
|
.propIconContext {
|
|
background: rgba(242, 201, 76, 0.26);
|
|
color: var(--mustard-ink);
|
|
}
|
|
.propIconDeadline {
|
|
background: rgba(221, 216, 245, 0.6);
|
|
color: var(--phase-nursery);
|
|
}
|
|
|
|
.propTitle {
|
|
font-family: var(--font-display);
|
|
font-size: var(--step-0);
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
margin: 0 0 0.15rem;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.propBody {
|
|
font-size: var(--step--1);
|
|
color: var(--text-muted);
|
|
line-height: 1.5;
|
|
margin: 0;
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.valueProps {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 1rem 1.25rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 560px) {
|
|
.valueProps {
|
|
grid-template-columns: 1fr;
|
|
gap: 0.85rem;
|
|
}
|
|
}
|
|
|
|
/* View Toggle */
|
|
.viewToggle {
|
|
display: flex;
|
|
gap: 0.2rem;
|
|
background: var(--bg-secondary);
|
|
padding: 0.2rem;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.viewToggleBtn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding: 0.375rem 0.625rem;
|
|
font-size: 0.8125rem;
|
|
font-weight: 500;
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 5px;
|
|
cursor: pointer;
|
|
color: var(--text-secondary);
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.viewToggleBtn:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.viewToggleBtn.active {
|
|
background: var(--bg-card);
|
|
color: var(--brand-strong);
|
|
box-shadow: 0 2px 4px rgba(var(--shadow-rgb), 0.08);
|
|
}
|
|
|
|
.viewToggleBtn svg {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.results {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.mapViewResults {
|
|
margin-top: 0;
|
|
}
|
|
|
|
/* Map View Layout */
|
|
.mapViewContainer {
|
|
display: grid;
|
|
grid-template-columns: 1fr 340px;
|
|
gap: 1rem;
|
|
height: calc(100dvh - 280px);
|
|
min-height: 520px;
|
|
max-height: 800px;
|
|
}
|
|
|
|
.mapContainer {
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
border: 1px solid var(--border);
|
|
height: 100%;
|
|
}
|
|
|
|
.compactList {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
overflow-y: auto;
|
|
height: 100%;
|
|
padding-right: 0.375rem;
|
|
}
|
|
|
|
.compactList::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.compactList::-webkit-scrollbar-track {
|
|
background: var(--bg-secondary);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.compactList::-webkit-scrollbar-thumb {
|
|
background: var(--border);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.compactList::-webkit-scrollbar-thumb:hover {
|
|
background: var(--text-muted);
|
|
}
|
|
|
|
/* Compact School Item */
|
|
.compactItem {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.625rem 0.75rem;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.compactItem:hover {
|
|
border-color: var(--brand);
|
|
box-shadow: 0 2px 6px rgba(var(--shadow-rgb), 0.05);
|
|
}
|
|
|
|
.compactItemContent {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.compactItemHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
margin-bottom: 0.125rem;
|
|
}
|
|
|
|
.compactItemName {
|
|
font-weight: 600;
|
|
font-size: 0.8125rem;
|
|
color: var(--text-primary);
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.compactItemName:hover {
|
|
color: var(--brand-strong);
|
|
}
|
|
|
|
.distanceBadge {
|
|
flex-shrink: 0;
|
|
padding: 0.0625rem 0.375rem;
|
|
font-size: 0.6875rem;
|
|
font-weight: 600;
|
|
background: var(--brand);
|
|
color: var(--brand-on);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.compactItemMeta {
|
|
display: flex;
|
|
gap: 0.375rem;
|
|
font-size: 0.6875rem;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.compactItemMeta span:not(:last-child)::after {
|
|
content: '·';
|
|
margin-left: 0.375rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.compactItemStats {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
font-size: 0.6875rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.compactStat strong {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.compactItemActions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
|
|
.sectionHeader {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.sectionHeader h2 {
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.5rem;
|
|
color: var(--text-primary);
|
|
font-family: var(--font-display);
|
|
}
|
|
|
|
/* Decorative coral bar under section headings */
|
|
.sectionHeader h2::after {
|
|
content: '';
|
|
display: block;
|
|
width: 50px;
|
|
height: 2px;
|
|
background: var(--brand);
|
|
border-radius: 1px;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.sectionDescription {
|
|
font-size: 0.9375rem;
|
|
color: var(--text-secondary);
|
|
margin: 0;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.schoolList {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1.25rem;
|
|
}
|
|
|
|
/* Staggered fade-in for rows */
|
|
.schoolList > *:nth-child(1) { animation-delay: 0ms; }
|
|
.schoolList > *:nth-child(2) { animation-delay: 30ms; }
|
|
.schoolList > *:nth-child(3) { animation-delay: 60ms; }
|
|
.schoolList > *:nth-child(4) { animation-delay: 90ms; }
|
|
.schoolList > *:nth-child(5) { animation-delay: 120ms; }
|
|
.schoolList > *:nth-child(6) { animation-delay: 150ms; }
|
|
.schoolList > *:nth-child(7) { animation-delay: 180ms; }
|
|
.schoolList > *:nth-child(8) { animation-delay: 210ms; }
|
|
.schoolList > *:nth-child(9) { animation-delay: 240ms; }
|
|
.schoolList > *:nth-child(n+10) { animation-delay: 270ms; }
|
|
|
|
.emptyState {
|
|
text-align: center;
|
|
padding: 2.5rem 1.5rem;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.emptyStateTitle {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: 0.375rem;
|
|
font-family: var(--font-display);
|
|
}
|
|
|
|
.emptyStateDescription {
|
|
font-size: 0.9375rem;
|
|
color: var(--text-secondary);
|
|
max-width: 380px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.resultsHeader {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.resultsHeaderActions {
|
|
width: 100%;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.viewToggle {
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* The sort <select> sizes to its widest option ("Highest Reading, Writing
|
|
& Maths %"), which overflows a phone viewport — beside the view toggle it
|
|
ran off the right edge. Let it flex into the remaining space and shrink;
|
|
the selected label truncates instead of pushing past the screen. */
|
|
.sortSelect {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.mapViewContainer {
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 260px auto;
|
|
height: auto;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.mapContainer {
|
|
height: 260px;
|
|
}
|
|
|
|
.compactList {
|
|
height: auto;
|
|
max-height: 350px;
|
|
padding-right: 0;
|
|
}
|
|
|
|
.compactItem {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.compactItemActions {
|
|
flex-direction: row;
|
|
}
|
|
|
|
.compactItemActions > * {
|
|
flex: 1;
|
|
}
|
|
|
|
.emptyState {
|
|
padding: 2rem 1.25rem;
|
|
}
|
|
}
|
|
|
|
/* Highlighted List Item */
|
|
.highlightedItem .compactItem {
|
|
border-color: var(--brand);
|
|
box-shadow: 0 0 0 1px var(--brand);
|
|
background: var(--bg-secondary);
|
|
}
|
|
|
|
/* Mobile Bottom Sheet */
|
|
.bottomSheetWrapper {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.bottomSheetWrapper {
|
|
display: block;
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
z-index: 1000;
|
|
padding: 1rem;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.bottomSheet {
|
|
position: relative;
|
|
background: var(--bg-card);
|
|
border-radius: 12px;
|
|
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;
|
|
}
|
|
|
|
.bottomSheet .compactItem {
|
|
border: none;
|
|
box-shadow: none;
|
|
background: transparent;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.bottomSheet .compactItem:hover {
|
|
box-shadow: none;
|
|
}
|
|
|
|
.closeSheetBtn {
|
|
position: absolute;
|
|
top: -12px;
|
|
right: -12px;
|
|
width: 30px;
|
|
height: 30px;
|
|
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);
|
|
cursor: pointer;
|
|
box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.1);
|
|
z-index: 10;
|
|
}
|
|
|
|
@keyframes slideUpSheet {
|
|
from {
|
|
transform: translateY(120%);
|
|
}
|
|
to {
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* When map view on mobile, expand map and hide list */
|
|
.mapViewContainer {
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr;
|
|
height: calc(100dvh - 280px);
|
|
min-height: 400px;
|
|
}
|
|
|
|
.mapContainer {
|
|
height: 100%;
|
|
}
|
|
|
|
.compactList {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.quickSearches {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.quickSearchLabel {
|
|
font-size: 0.875rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.quickSearchChip {
|
|
padding: 0.375rem 0.875rem;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 999px;
|
|
font-size: 0.875rem;
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
transition: all var(--transition);
|
|
}
|
|
|
|
.quickSearchChip:hover {
|
|
background: var(--brand-strong);
|
|
color: var(--brand-on);
|
|
border-color: var(--brand-strong);
|
|
}
|
|
|
|
/* ── How it works section ─────────────────────────────────────────────────
|
|
A Sand band, borrowing the hero panel's ground and radius so the page has
|
|
two anchored bands rather than one designed element and a long tail of
|
|
unstyled strips. */
|
|
|
|
.howItWorks {
|
|
background: var(--bg-secondary);
|
|
border-radius: var(--radius-xl);
|
|
padding: 2.5rem 2rem;
|
|
}
|
|
|
|
.hiwGrid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 1rem;
|
|
}
|
|
|
|
.hiwCard {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 1.25rem;
|
|
box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.06);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.9rem;
|
|
}
|
|
|
|
.hiwVisual {
|
|
background: var(--bg-secondary);
|
|
border-radius: 10px;
|
|
padding: 0.9rem;
|
|
min-height: 180px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.hiwPhaseBlock {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.hiwPhaseLabel {
|
|
font-size: 0.58rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.hiwPhaseLabel strong {
|
|
color: var(--brand);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.hiwCardBody {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.3rem;
|
|
}
|
|
|
|
.hiwStep {
|
|
font-size: 0.65rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--brand-strong);
|
|
}
|
|
|
|
.hiwTitle {
|
|
font-family: var(--font-display);
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.hiwDesc {
|
|
font-size: 0.86rem;
|
|
color: var(--text-secondary);
|
|
line-height: 1.45;
|
|
margin: 0;
|
|
}
|
|
|
|
/* Mini cascade (performance card) */
|
|
.miniCascade {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 0.4rem;
|
|
}
|
|
|
|
.miniCascadeCol {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.15rem;
|
|
min-width: 0;
|
|
}
|
|
|
|
.miniSubj {
|
|
font-size: 0.5rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.1em;
|
|
text-transform: uppercase;
|
|
color: var(--text-muted);
|
|
margin-bottom: 0.1rem;
|
|
}
|
|
|
|
.miniRowHead {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
gap: 0.2rem;
|
|
font-size: 0.44rem;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.01em;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.miniRowHead strong {
|
|
font-family: var(--font-display);
|
|
font-size: 0.62rem;
|
|
color: var(--text-primary);
|
|
font-weight: 700;
|
|
letter-spacing: 0;
|
|
text-transform: none;
|
|
}
|
|
|
|
.miniTrack {
|
|
height: 5px;
|
|
border-radius: 2px;
|
|
background: rgba(var(--status-above-rgb), 0.08);
|
|
position: relative;
|
|
overflow: visible;
|
|
}
|
|
|
|
.miniBarExp {
|
|
height: 100%;
|
|
border-radius: 2px;
|
|
background: var(--brand);
|
|
}
|
|
|
|
.miniBarExc {
|
|
height: 100%;
|
|
border-radius: 2px;
|
|
background: var(--brand);
|
|
}
|
|
|
|
.miniNatPill {
|
|
position: absolute;
|
|
top: -9px;
|
|
transform: translateX(-50%);
|
|
background: var(--brand);
|
|
color: var(--text-inverse);
|
|
font-size: 0.4rem;
|
|
font-weight: 700;
|
|
padding: 0.05rem 0.2rem;
|
|
border-radius: 3px;
|
|
z-index: 2;
|
|
white-space: nowrap;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Attainment 8 mini bar */
|
|
.att8Row {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto;
|
|
gap: 0.6rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.att8BarWrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.2rem;
|
|
}
|
|
|
|
.att8BarHead {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 0.5rem;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.att8Track {
|
|
height: 7px;
|
|
background: rgba(var(--status-above-rgb), 0.08);
|
|
border-radius: 3px;
|
|
position: relative;
|
|
}
|
|
|
|
.att8Fill {
|
|
height: 100%;
|
|
background: var(--brand);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.att8NatLine {
|
|
position: absolute;
|
|
top: -2px;
|
|
bottom: -2px;
|
|
width: 1.5px;
|
|
background: rgba(var(--brand-rgb), 0.6);
|
|
z-index: 2;
|
|
}
|
|
|
|
.att8Score {
|
|
font-variant-numeric: tabular-nums;
|
|
text-align: right;
|
|
}
|
|
|
|
.att8Value {
|
|
font-variant-numeric: tabular-nums;
|
|
font-family: var(--font-display);
|
|
font-size: 1.5rem;
|
|
font-weight: 700;
|
|
color: var(--status-above);
|
|
line-height: 1;
|
|
}
|
|
|
|
.att8Delta {
|
|
font-variant-numeric: tabular-nums;
|
|
font-size: 0.55rem;
|
|
color: var(--status-above);
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
margin-top: 0.15rem;
|
|
}
|
|
|
|
/* Ofsted preview */
|
|
.ofstedPreview {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
width: 100%;
|
|
padding: 0.4rem 0.2rem;
|
|
}
|
|
|
|
.ofstedHead {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding-bottom: 0.4rem;
|
|
border-bottom: 1.5px solid var(--border);
|
|
}
|
|
|
|
.ofstedBullet {
|
|
display: block;
|
|
width: 3px;
|
|
height: 1em;
|
|
background: var(--brand);
|
|
border-radius: 2px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.ofstedTitle {
|
|
font-family: var(--font-display);
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.ofstedFramework {
|
|
align-self: flex-start;
|
|
padding: 0.15rem 0.5rem;
|
|
border-radius: 4px;
|
|
background: rgba(var(--phase-secondary-rgb), 0.12);
|
|
color: var(--phase-secondary-text);
|
|
font-size: 0.5rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.05em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.rcList {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.3rem;
|
|
}
|
|
|
|
.rcRow {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.rcArea {
|
|
font-size: 0.62rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.rcChip {
|
|
padding: 0.1rem 0.4rem;
|
|
border-radius: 3px;
|
|
font-size: 0.52rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.03em;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.rcChipGood {
|
|
background: rgba(var(--status-above-rgb), 0.12);
|
|
color: var(--status-above);
|
|
}
|
|
|
|
.rcChipNeutral {
|
|
background: var(--status-below-bg);
|
|
color: var(--status-below);
|
|
}
|
|
|
|
/* Safeguarding is a separate binary met/not-met judgement, not a graded area,
|
|
so it sits below the five-point list behind a divider with its own chip. */
|
|
.rcSafeguarding {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.5rem;
|
|
margin-top: 0.05rem;
|
|
padding-top: 0.35rem;
|
|
border-top: 1px dashed var(--border);
|
|
}
|
|
|
|
.rcSep {
|
|
color: var(--text-muted);
|
|
font-style: italic;
|
|
}
|
|
|
|
.rcChipMet {
|
|
background: transparent;
|
|
color: var(--status-above);
|
|
border: 1px solid rgba(var(--status-above-rgb), 0.4);
|
|
}
|
|
|
|
.ofstedMeta {
|
|
font-size: 0.6rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Compare preview */
|
|
.comparePreview {
|
|
width: 100%;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
font-size: 0.6rem;
|
|
}
|
|
|
|
.compareHead {
|
|
display: grid;
|
|
grid-template-columns: 1fr repeat(2, 1fr);
|
|
background: rgba(var(--status-above-rgb), 0.1);
|
|
padding: 0.35rem 0.5rem;
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.compareHeadCell {
|
|
font-family: var(--font-display);
|
|
font-size: 0.62rem;
|
|
font-weight: 700;
|
|
color: var(--status-above);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.compareHeadLabel {
|
|
font-family: inherit;
|
|
font-size: 0.48rem;
|
|
color: var(--text-muted);
|
|
font-weight: 500;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.compareRow {
|
|
display: grid;
|
|
grid-template-columns: 1fr repeat(2, 1fr);
|
|
padding: 0.3rem 0.5rem;
|
|
gap: 0.35rem;
|
|
border-top: 1px solid var(--border);
|
|
align-items: baseline;
|
|
}
|
|
|
|
.compareRowLabel {
|
|
font-size: 0.55rem;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.compareRowVal {
|
|
font-family: var(--font-display);
|
|
font-size: 0.72rem;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.compareRowValHi {
|
|
color: var(--status-above);
|
|
}
|
|
|
|
.compareFoot {
|
|
font-size: 0.52rem;
|
|
color: var(--text-muted);
|
|
padding: 0.35rem 0.5rem;
|
|
background: var(--bg-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
text-align: center;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.hiwGrid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
.howItWorks {
|
|
padding: 1.75rem 1.25rem;
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
}
|
|
|
|
/* On phones, hide the scaled-down preview visuals (text becomes illegible
|
|
at this size) and let the explanatory text carry each card. */
|
|
@media (max-width: 640px) {
|
|
.hiwVisual {
|
|
display: none;
|
|
}
|
|
.hiwCard {
|
|
padding: 1rem 1.1rem;
|
|
gap: 0.45rem;
|
|
}
|
|
.hiwTitle {
|
|
font-size: 1.05rem;
|
|
}
|
|
.hiwDesc {
|
|
font-size: 0.9rem;
|
|
}
|
|
}
|
|
|
|
/* ── Editorial section ───────────────────────────────── */
|
|
|
|
/* Heading beside the prose rather than above it. The fact box used to occupy
|
|
the right-hand column; with it gone, a single column left the card half
|
|
empty, because the prose is capped at a 62ch reading measure and the card
|
|
is not. */
|
|
.editorialCard {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
|
|
column-gap: 3rem;
|
|
padding: 2rem 2.25rem;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
|
|
/* The shared header pattern spaces itself for a header sitting above its
|
|
content; here it sits beside it. */
|
|
.editorialCard .sectionHead {
|
|
margin-bottom: 0;
|
|
display: block;
|
|
}
|
|
|
|
.editorialCard .coverageLine {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.editorialText {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.65rem;
|
|
}
|
|
|
|
/* 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-family: var(--font-prose);
|
|
font-size: 0.97rem;
|
|
color: var(--text-secondary);
|
|
line-height: 1.65;
|
|
margin: 0;
|
|
max-width: 62ch;
|
|
}
|
|
|
|
/* One line of live coverage figures, replacing a six-row table whose headline
|
|
number was its own hardcoded fallback. Quiet by design: it is provenance,
|
|
not a claim. */
|
|
.coverageLine {
|
|
margin: 1.25rem 0 0;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid var(--border);
|
|
font-size: var(--step--1);
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.editorialCard {
|
|
grid-template-columns: 1fr;
|
|
padding: 1.25rem;
|
|
}
|
|
.editorialCard .sectionHead {
|
|
margin-bottom: var(--section-head-gap);
|
|
}
|
|
}
|
|
|
|
.resultsHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
padding: 0 0 1rem;
|
|
}
|
|
|
|
.resultsHeaderActions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sortSelect {
|
|
padding: 0.375rem 0.75rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
background: var(--bg-card);
|
|
color: var(--text-secondary);
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.activeFilters {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.filterChip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
padding: 0.25rem 0.625rem;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border);
|
|
border-radius: 999px;
|
|
font-size: 0.8rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.chipRemove {
|
|
color: var(--text-muted);
|
|
text-decoration: none;
|
|
font-size: 0.9rem;
|
|
line-height: 1;
|
|
transition: color var(--transition, 0.2s ease);
|
|
}
|
|
|
|
.chipRemove:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.loadMoreSection {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 1.5rem 0;
|
|
}
|
|
|
|
.loadMoreCount {
|
|
font-variant-numeric: tabular-nums;
|
|
font-size: 0.875rem;
|
|
color: var(--text-muted);
|
|
margin: 0;
|
|
}
|
|
|
|
.loadMoreButton {
|
|
min-width: 160px;
|
|
}
|
|
/* =========================================================
|
|
Next admissions deadline
|
|
|
|
One bar, replacing four equally-weighted countdown cards. Those gave the
|
|
page's largest numeral to things up to 245 days away, and two of the four
|
|
counted down to offer days — dates you receive something on, which cannot
|
|
be missed. The headline is the next real deadline; the rest stay on the
|
|
page as one supporting line, so no fact was lost, only its weight.
|
|
========================================================= */
|
|
|
|
.admissions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.85rem;
|
|
}
|
|
|
|
.nextDeadline {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1.5rem;
|
|
position: relative;
|
|
overflow: hidden;
|
|
padding: 1.15rem 1.5rem 1.15rem 1.75rem;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-soft);
|
|
color: inherit;
|
|
text-decoration: none;
|
|
transition: border-color var(--transition), box-shadow var(--transition);
|
|
}
|
|
|
|
/* A brand rule down the leading edge — the same device the countdown cards
|
|
used along their top edge, kept so the band still reads as admissions. */
|
|
.nextDeadline::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0 auto 0 0;
|
|
width: 5px;
|
|
background: var(--brand);
|
|
}
|
|
|
|
.nextDeadline:hover {
|
|
border-color: var(--border-strong);
|
|
box-shadow: var(--shadow-medium);
|
|
}
|
|
|
|
.nextDeadlineBody {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.15rem;
|
|
min-width: 0;
|
|
}
|
|
|
|
.nextDeadlineKicker {
|
|
font-size: var(--step--2);
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--brand-strong);
|
|
}
|
|
|
|
.nextDeadlineTitle {
|
|
font-family: var(--font-display);
|
|
font-size: var(--step-1);
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.nextDeadlineDate {
|
|
font-size: var(--step--1);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.nextDeadlineCount {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.35rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.nextDeadlineDays {
|
|
font-family: var(--font-display);
|
|
font-size: var(--step-3);
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
letter-spacing: -0.02em;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--brand-strong);
|
|
}
|
|
|
|
.nextDeadlineUnit {
|
|
font-size: var(--step--1);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Inside a fortnight the count changes hue rather than size, so urgency is
|
|
legible without the bar growing. Terracotta is the status hue, not the
|
|
action hue — this is a state, not a button. */
|
|
.nextDeadlineUrgent .nextDeadlineDays {
|
|
color: var(--status-below);
|
|
}
|
|
|
|
.laterDates {
|
|
font-size: var(--step--1);
|
|
color: var(--text-muted);
|
|
line-height: 1.6;
|
|
margin: 0;
|
|
}
|
|
|
|
.laterDatesLink {
|
|
color: var(--brand);
|
|
font-weight: 600;
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.laterDatesLink:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.nextDeadline {
|
|
align-items: flex-start;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
padding: 1rem 1.1rem 1rem 1.35rem;
|
|
}
|
|
.nextDeadlineCount {
|
|
align-items: baseline;
|
|
}
|
|
}
|