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 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 49s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 5m29s
Main's staging E2E has been red since #90. Two of the four failures were the staging container serving a stale image at the moment the gate ran — apple-icon and the header lockup both check out against staging now. The other two were real, and both are the same mistake: a colour pairing verified against one ground while the element sits on another. * .heroEyebrow --brand on its own 10% tint over Sand is 4.19:1. The token clears AA on Sand, but the tint darkens the ground under the label, and that composite was never the thing measured. --brand-strong is 5.85:1. * .hiwVisual The preview panel was grounded in Sand while everything inside it is a translucent status tint — and those tints are specced to clear AA over --bg-card. Over Sand the report-card chips landed at 4.22:1 and 4.29:1. The ratio depended on a background two levels up. Moving the panel to the card surface puts them at 5.69:1 and 5.81:1; a border keeps it reading as an inset frame now that panel and card share a colour. * Footer .sectionTitle --sage flips with the theme; the footer band does not (it is teal in both). So the pairing only held in one of them — the dark sage measured 4.08:1, on every page. Now --on-sunken-muted, which is what the --on-sunken-* family exists for, as Footer.tsx's own header comment already says. * .compareHeadLabel --text-muted on the header row's tint is 4.45:1 in the dark theme. Under by a hair, same cause. Now --text-secondary. The harness gained the footer, because it had no footer and so could not have caught the one failure that appeared on all three pages. Rewriting fragments now uses each CSS module's own hash — the footer rewritten with HomeView's prefix renders unstyled, which would have made any contrast measured on it meaningless while still reporting a number. Verified: 0 AA failures in both themes across the assembled page, measured with the same probe the e2e test uses, against the real compiled CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1493 lines
31 KiB
CSS
1493 lines
31 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;
|
|
/* --brand on its own 10% tint over Sand measures 4.19:1 — the tint darkens
|
|
the ground under the label, and the token was specced against the ground,
|
|
not against the tint. --brand-strong on the same composite is 5.85:1. */
|
|
color: var(--brand-strong);
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* The preview panel sits on the card surface, not on Sand.
|
|
*
|
|
* Everything inside it is a translucent status or brand tint, and those tokens
|
|
* are specced to clear AA over --bg-card. Over Sand the same tints composite
|
|
* darker, which put the report-card chips at 4.22:1 and 4.29:1 against a 4.5
|
|
* floor — the ratio depended on a ground two levels up. On the card surface
|
|
* they are 5.69:1 and 5.81:1. The border keeps it reading as an inset frame
|
|
* now that panel and card share a colour.
|
|
*/
|
|
.hiwVisual {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
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;
|
|
/* --text-muted on the header row's tint is 4.45:1 in the dark theme —
|
|
under the floor by a hair, because the tint darkens the ground beneath
|
|
the label. --text-secondary clears it on both grounds. */
|
|
color: var(--text-secondary);
|
|
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;
|
|
}
|
|
}
|