fix(home): correct what the landing page claims, and give it one rhythm
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
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>
This commit is contained in:
1 parent
6b117dd26c
commit
dc22fd2853
11 files changed
+849
-569
No files matched your search
@@ -2,6 +2,80 @@
|
||||
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
|
||||
@@ -14,7 +88,6 @@
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-xl);
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.heroContent {
|
||||
@@ -31,6 +104,13 @@
|
||||
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;
|
||||
@@ -39,31 +119,12 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.heroTrust {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
margin-top: 1.4rem;
|
||||
font-size: var(--step--1);
|
||||
color: var(--text-secondary);
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.heroTrustDots {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.heroTrustDot {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--bg-secondary);
|
||||
margin-right: -0.55rem;
|
||||
}
|
||||
.heroTrustDot:nth-child(1) { background: var(--sage); }
|
||||
.heroTrustDot:nth-child(2) { background: var(--mustard); }
|
||||
.heroTrustDot:nth-child(3) { background: var(--sky); }
|
||||
|
||||
.heroEyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -135,10 +196,18 @@
|
||||
.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;
|
||||
@@ -150,13 +219,11 @@
|
||||
}
|
||||
|
||||
/* Above the fold on phones, every line costs. Drop the eyebrow tag and the
|
||||
long coverage sentence, leaving the proposition and the search — a
|
||||
first-time visitor still gets the trust line beneath the box, which carries
|
||||
the data provenance the coverage sentence used to (audit P1.7, feeds the
|
||||
46% home exit rate). */
|
||||
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 {
|
||||
margin-bottom: 1rem;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
.heroContent {
|
||||
@@ -175,10 +242,6 @@
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.heroTrust {
|
||||
margin-top: 1rem;
|
||||
font-size: var(--step--2);
|
||||
}
|
||||
.heroArt {
|
||||
min-height: 8.5rem;
|
||||
}
|
||||
@@ -193,7 +256,7 @@
|
||||
gap: 1.25rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 2rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.valueProp {
|
||||
@@ -256,7 +319,6 @@
|
||||
.valueProps {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.85rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,86 +751,15 @@
|
||||
border-color: var(--brand-strong);
|
||||
}
|
||||
|
||||
.exploringRow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.exploringLabel {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.exploringChips {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.exploringChip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.5rem 0.95rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.exploringChip:hover {
|
||||
border-color: var(--brand);
|
||||
color: var(--brand-strong);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.chipDot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.55;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── How it works section ─────────────────────────────── */
|
||||
/* ── 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 {
|
||||
padding: 3rem 0 1rem;
|
||||
}
|
||||
|
||||
.hiwHeader {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hiwHeading {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hiwSub {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
|
||||
.hiwGrid {
|
||||
@@ -1183,12 +1174,9 @@
|
||||
.hiwGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.hiwHeader {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.hiwHeading {
|
||||
font-size: 1.4rem;
|
||||
.howItWorks {
|
||||
padding: 1.75rem 1.25rem;
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1212,18 +1200,29 @@
|
||||
|
||||
/* ── Editorial section ───────────────────────────────── */
|
||||
|
||||
.editorial {
|
||||
padding: 2rem 0 3rem;
|
||||
}
|
||||
|
||||
.editorialGrid {
|
||||
/* 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: 1.4fr 1fr;
|
||||
gap: 2rem;
|
||||
padding: 1.75rem;
|
||||
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: 14px;
|
||||
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 {
|
||||
@@ -1232,23 +1231,6 @@
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.editorialKicker {
|
||||
font-size: 0.68rem;
|
||||
color: var(--brand-strong);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.editorialHeading {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
@@ -1260,53 +1242,25 @@
|
||||
max-width: 62ch;
|
||||
}
|
||||
|
||||
.factbox {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 10px;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.factboxHeading {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 0.85rem;
|
||||
}
|
||||
|
||||
.factRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.85rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.factRow:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.factKey {
|
||||
/* 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);
|
||||
}
|
||||
|
||||
.factVal {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.editorialGrid {
|
||||
.editorialCard {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 1.25rem;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
.editorialCard .sectionHead {
|
||||
margin-bottom: var(--section-head-gap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1385,189 +1339,137 @@
|
||||
.loadMoreButton {
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
Admissions Countdown Strip
|
||||
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.
|
||||
========================================================= */
|
||||
|
||||
.admissionsStrip {
|
||||
padding: 1.5rem 0 2rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.stripHeader {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stripLabel {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stripCta {
|
||||
font-size: 0.82rem;
|
||||
color: var(--brand);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.stripCta:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.countdownRail {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.countdownChip {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.1rem 0.9rem;
|
||||
box-shadow: 0 2px 8px rgba(var(--shadow-rgb), 0.06);
|
||||
.admissions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
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);
|
||||
}
|
||||
|
||||
.countdownChip::before {
|
||||
/* 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;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
|
||||
.countdownChipDeadline::before {
|
||||
inset: 0 auto 0 0;
|
||||
width: 5px;
|
||||
background: var(--brand);
|
||||
}
|
||||
|
||||
.countdownChipOffer::before {
|
||||
background: var(--status-above);
|
||||
.nextDeadline:hover {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: var(--shadow-medium);
|
||||
}
|
||||
|
||||
.countdownChipUrgent {
|
||||
border-color: rgba(var(--status-below-rgb), 0.4);
|
||||
background: rgba(var(--status-below-rgb), 0.04);
|
||||
}
|
||||
|
||||
.chipTrack {
|
||||
.nextDeadlineBody {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.15rem;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chipTrackDeadline {
|
||||
.nextDeadlineKicker {
|
||||
font-size: var(--step--2);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--brand-strong);
|
||||
}
|
||||
|
||||
.chipTrackOffer {
|
||||
color: var(--status-above);
|
||||
.nextDeadlineTitle {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--step-1);
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.chipTrackDot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
.nextDeadlineDate {
|
||||
font-size: var(--step--1);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.nextDeadlineCount {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.35rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chipDays {
|
||||
font-variant-numeric: tabular-nums;
|
||||
.nextDeadlineDays {
|
||||
font-family: var(--font-display);
|
||||
font-size: 2.6rem;
|
||||
font-size: var(--step-3);
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.02em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--brand-strong);
|
||||
}
|
||||
|
||||
.countdownChipDeadline .chipDays,
|
||||
.countdownChipUrgent .chipDays {
|
||||
.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);
|
||||
}
|
||||
|
||||
.countdownChipOffer .chipDays {
|
||||
color: var(--status-above);
|
||||
}
|
||||
|
||||
.chipDaysUnit {
|
||||
font-family: var(--font-ui);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
.laterDates {
|
||||
font-size: var(--step--1);
|
||||
color: var(--text-muted);
|
||||
margin-left: 0.2rem;
|
||||
vertical-align: bottom;
|
||||
line-height: 2;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chipMilestone {
|
||||
font-size: 0.85rem;
|
||||
.laterDatesLink {
|
||||
color: var(--brand);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.25;
|
||||
margin-top: 0.1rem;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chipDate {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.05rem;
|
||||
.laterDatesLink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.countdownRail {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* On phones the 2×2 grid cramped each chip so badly the "Secondary ·
|
||||
Deadline" track label dropped to 9.6px. Switch to a horizontal
|
||||
snap-scroller — each card is full-width-ish and stays readable,
|
||||
and the rightmost card peeks past the edge to signal there's more. */
|
||||
@media (max-width: 640px) {
|
||||
.countdownRail {
|
||||
display: flex;
|
||||
grid-template-columns: none;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
scrollbar-width: none;
|
||||
.nextDeadline {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding-right: 1.25rem;
|
||||
margin-inline: -1rem;
|
||||
padding-inline: 1rem;
|
||||
-webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
|
||||
mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent);
|
||||
padding: 1rem 1.1rem 1rem 1.35rem;
|
||||
}
|
||||
.countdownRail::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.countdownChip {
|
||||
flex: 0 0 auto;
|
||||
width: 78%;
|
||||
min-width: 220px;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
.chipTrack {
|
||||
font-size: 0.7rem;
|
||||
.nextDeadlineCount {
|
||||
align-items: baseline;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user