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.
287 lines
6.5 KiB
CSS
287 lines
6.5 KiB
CSS
/*
|
||
* Header and mobile tab bar, on the schoolcompare brand.
|
||
*
|
||
* The header is a white card band closed by a single hairline rule — no
|
||
* shadow. The guideline's chrome is quiet; the colour on this surface is the
|
||
* wordmark and the one active nav item, and nothing else.
|
||
*/
|
||
|
||
.header {
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1000;
|
||
background: var(--bg-card);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
/* Add the safe-area inset to horizontal padding so the header content
|
||
clears the notch in landscape on iPhones. */
|
||
padding-inline: max(1.5rem, env(safe-area-inset-left)) max(1.5rem, env(safe-area-inset-right));
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 2rem;
|
||
/* 64px desktop / 56px mobile is load-bearing: the school detail page's
|
||
sticky section nav docks against it with a hardcoded `top` in
|
||
SchoolDetailShell.module.css and schoolSections.module.css, and the
|
||
scroll-spy observers use it as a rootMargin. Change it in all four
|
||
places or not at all. */
|
||
height: 64px;
|
||
}
|
||
|
||
.logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.625rem;
|
||
/* Padded hit area so the logo link is ≥44×44 on touch */
|
||
margin: -0.375rem -0.5rem;
|
||
padding: 0.375rem 0.5rem;
|
||
text-decoration: none;
|
||
color: var(--text-primary);
|
||
transition: color var(--transition);
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
.logo:hover {
|
||
color: var(--brand-stronger);
|
||
}
|
||
|
||
/*
|
||
* The mark colours itself — pin in var(--brand), leaf knocked out in
|
||
* var(--bg-card), which is this header's own ground. It takes no colour from
|
||
* currentColor, so there is deliberately no `color` here.
|
||
*/
|
||
.logoIcon {
|
||
display: inline-flex;
|
||
flex: 0 0 auto;
|
||
width: 36px;
|
||
height: 36px;
|
||
}
|
||
|
||
.logoIcon svg {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
/*
|
||
* "schoolcompare", one word, lower case. Manrope 700 pulled tight — the
|
||
* negative tracking is what makes the two halves read as a single word rather
|
||
* than as two.
|
||
*/
|
||
.logoText {
|
||
font-family: var(--font-display);
|
||
font-size: var(--step-2);
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
letter-spacing: -0.035em;
|
||
}
|
||
|
||
.logoTextAccent {
|
||
color: var(--brand);
|
||
}
|
||
|
||
.logo:hover .logoTextAccent {
|
||
color: var(--brand-stronger);
|
||
}
|
||
|
||
.nav {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.125rem;
|
||
}
|
||
|
||
.navLink {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4375rem;
|
||
padding: 0.5rem 0.875rem;
|
||
font-family: var(--font-ui);
|
||
font-size: var(--step--1);
|
||
font-weight: 500;
|
||
color: var(--text-secondary);
|
||
text-decoration: none;
|
||
border-radius: var(--radius-md);
|
||
transition: color var(--transition), background-color var(--transition);
|
||
}
|
||
|
||
.navLink:hover {
|
||
color: var(--text-primary);
|
||
background: var(--bg-secondary);
|
||
}
|
||
|
||
/* The current page is marked twice — brand hue and a heavier weight — so the
|
||
state never rests on colour alone. */
|
||
.navLink.active {
|
||
color: var(--brand);
|
||
background: var(--brand-bg);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 1.125rem;
|
||
height: 1.125rem;
|
||
padding: 0 0.375rem;
|
||
font-family: var(--font-ui);
|
||
font-size: var(--step--2);
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
color: var(--brand-on);
|
||
background: var(--brand);
|
||
border-radius: 9999px;
|
||
animation: badgePop 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes badgePop {
|
||
0% {
|
||
transform: scale(0.6);
|
||
opacity: 0;
|
||
}
|
||
50% {
|
||
transform: scale(1.2);
|
||
}
|
||
100% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* ─── Bottom tab bar (mobile only) ──────────────────────────────── */
|
||
|
||
.bottomBar {
|
||
display: none;
|
||
}
|
||
|
||
.tab {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 0.25rem;
|
||
flex: 1;
|
||
min-height: 56px;
|
||
padding: 0.375rem 0.25rem;
|
||
font-family: var(--font-ui);
|
||
color: var(--text-secondary);
|
||
text-decoration: none;
|
||
font-size: 0.6875rem;
|
||
font-weight: 500;
|
||
letter-spacing: 0.01em;
|
||
transition: color var(--transition);
|
||
-webkit-tap-highlight-color: transparent;
|
||
}
|
||
|
||
.tabActive {
|
||
color: var(--brand);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.tabIconWrap {
|
||
position: relative;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 24px;
|
||
height: 24px;
|
||
}
|
||
|
||
/* The active tab's icon sits in a brand-tinted stadium — the same rounded
|
||
geometry as the rest of the system, and a second cue beyond hue. Drawn as a
|
||
pseudo-element so it can bleed wider than the 24px icon box without moving
|
||
the badge's anchor. */
|
||
.tabIconWrap::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: -2px -9px;
|
||
border-radius: var(--radius-xl);
|
||
background: transparent;
|
||
transition: background-color var(--transition);
|
||
}
|
||
|
||
.tabActive .tabIconWrap::before {
|
||
background: var(--brand-bg);
|
||
}
|
||
|
||
.tab:active .tabIconWrap::before {
|
||
background: var(--brand-bg);
|
||
}
|
||
|
||
.tabIcon {
|
||
position: relative;
|
||
width: 22px;
|
||
height: 22px;
|
||
}
|
||
|
||
.tabLabel {
|
||
line-height: 1;
|
||
}
|
||
|
||
.tabBadge {
|
||
position: absolute;
|
||
top: -7px;
|
||
right: -7px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 18px;
|
||
height: 18px;
|
||
padding: 0 5px;
|
||
font-family: var(--font-ui);
|
||
font-size: 0.6875rem;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
color: var(--brand-on);
|
||
background: var(--brand);
|
||
border: 2px solid var(--bg-card);
|
||
border-radius: 9999px;
|
||
animation: badgePop 0.3s ease-out;
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.container {
|
||
padding: 0 1rem;
|
||
height: 56px;
|
||
}
|
||
|
||
/* Hide the top text nav; the bottom bar takes over */
|
||
.nav {
|
||
display: none;
|
||
}
|
||
|
||
.logoIcon {
|
||
width: 34px;
|
||
height: 34px;
|
||
}
|
||
|
||
.logoText {
|
||
font-size: var(--step-1);
|
||
}
|
||
|
||
.bottomBar {
|
||
display: flex;
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 1000;
|
||
background: var(--bg-card);
|
||
border-top: 1px solid var(--border);
|
||
box-shadow: 0 -2px 12px rgba(var(--shadow-rgb), 0.06);
|
||
/* Respect iPhone home-indicator (bottom) and notch (left/right in
|
||
landscape) insets so the tab content never sits under system UI. */
|
||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||
padding-inline: env(safe-area-inset-left, 0) env(safe-area-inset-right, 0);
|
||
/* Compensate for iOS Chrome's auto-hiding URL bar — Navigation.tsx
|
||
writes the offset based on the Visual Viewport API. translate3d
|
||
(instead of translateY) forces hardware compositing so the bar
|
||
doesn't lag/flicker during the toolbar animation. */
|
||
transform: translate3d(0, var(--mobile-bar-offset, 0px), 0);
|
||
}
|
||
}
|