Files
school_compare/nextjs-app/app/globals.css
T
TudorandClaude Opus 5 dc22fd2853
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
fix(home): correct what the landing page claims, and give it one rhythm
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>
2026-08-14 17:29:47 +01:00

652 lines
23 KiB
CSS

/*
* schoolcompare — "Find the right school. For their future."
*
* Implements the schoolcompare brand guideline. Colour has four jobs and they
* never borrow each other's hues:
* brand (deep teal) identity, navigation, headings, links, primary buttons
* action (coral) the one decisive action on a page — search, submit
* status (green / valence against the England average. Never a CTA.
* terracotta)
* phase (desaturated) category chips, deliberately subordinate.
*
* Two deviations from the supplied swatches, both forced by contrast, both
* confined to text and fills. Coral #F97360 carrying a white label measures
* 2.75:1 where 4.5 is the floor, so --action is a darkened #BE3C27; the
* original coral survives as --coral for tints, borders and illustration.
* Mustard #F2C94C is 1.5:1 as text, so --mustard-ink carries any label while
* --mustard stays exact for fills, rings and badges.
*
* Every value below clears WCAG AA on all three light grounds — Warm White,
* white cards and Sand. Sand is the binding one; several values are a step
* darker than they would need to be on white alone.
*/
:root {
color-scheme: light;
/* ── Ground ─────────────────────────────────────────────────────── */
--bg-primary: #FAFAF8; /* Warm White */
--bg-secondary: #F5EFE6; /* Sand — hero panels, sunken rows */
--bg-card: #FFFFFF;
--surface-inverse: #0F766E;
/* ── Ink ────────────────────────────────────────────────────────── */
--text-primary: #1C2731; /* Ink */
--text-secondary: #4A5560;
--text-muted: #5F6A75; /* 4.8:1 on Sand, the worst ground */
--text-inverse: #FFFFFF;
/* ── Line ───────────────────────────────────────────────────────── */
--border: #E5E7EB; /* Light Grey */
--border-strong: #D3D7DD;
/* ── Brand: deep teal ───────────────────────────────────────────── */
--brand: #0F766E; /* exact brand value; 4.8:1 on Sand */
--brand-strong: #0C5F58;
--brand-stronger: #0A4D47;
--brand-bg: rgba(15, 118, 110, 0.10);
--brand-on: #FFFFFF;
/* ── Action: coral. One per screen, and only for the decisive one. ── */
--action: #BE3C27; /* darkened coral — 5.4:1 under a white label */
--action-strong: #A33320;
--action-stronger: #8C2B1B;
/* Tint of --action, not of --coral, so a coral ghost button's label and
ground share a hue. Unused today; the dark value was already correct. */
--action-bg: rgba(190, 60, 39, 0.14);
--action-on: #FFFFFF;
/* ── Palette accents: exact brand values, for fills and tints only ── */
--coral: #F97360;
--mustard: #F2C94C;
--mustard-ink: #806200; /* any label that has to sit on or beside mustard */
--sage: #A7D7C5;
--sky: #C7EBF5;
--lavender: #DDD8F5;
--sand: #F5EFE6;
/* ── Status: above / at / below the England average ─────────────── */
--status-above: #36743F;
--status-above-bg: rgba(54, 116, 63, 0.12);
--status-below: #A9481F;
/* The tint is the solid hue at low alpha, exactly as every other pair in
this file is. It briefly carried the raw --coral tuple instead, which
put a coral-pink wash behind terracotta text — the one mismatched pair
in the palette. Alpha matches --status-above-bg so the two chips read
as equal weight; at 0.15 the darker terracotta tint drops the label to
4.45:1 on Warm White. */
--status-below-bg: rgba(169, 72, 31, 0.12);
--status-at: #5F6A75;
--status-at-bg: rgba(95, 106, 117, 0.10);
/* ── Charts ─────────────────────────────────────────────────────── */
--chart-1: #0F766E;
--chart-2: #36743F;
--chart-3: #806200;
--chart-4: #A9481F;
--chart-5: #2F6F8F;
--chart-6: #5F4FA8;
--chart-grid: #E5E7EB;
--chart-reference: #5F6A75;
/* ── Series: up to 8 schools compared at once ───────────────────────
Categorical, so wider than the four-hue rule — but every step is held
to the same mid-dark tone and clears AA on the card, so they read as
one family and work as legend text as well as chart lines. */
--series-1: #0F766E;
--series-2: #36743F;
--series-3: #806200;
--series-4: #A9481F;
--series-5: #2F6F8F;
--series-6: #5F4FA8;
--series-7: #0E7A86;
--series-8: #8A4A6B;
/* ── Phase: category, desaturated so it stays under the status hues ── */
--phase-primary: #0F766E;
--phase-primary-bg: rgba(167, 215, 197, 0.40);
--phase-primary-text: #0C5F58;
--phase-secondary: #2F6F8F;
--phase-secondary-bg: rgba(199, 235, 245, 0.55);
--phase-secondary-text: #2A6180;
--phase-all-through: #36743F;
--phase-all-through-bg: rgba(54, 116, 63, 0.12);
--phase-all-through-text: #2F6738;
--phase-post16: #806200;
--phase-post16-bg: rgba(242, 201, 76, 0.26);
--phase-post16-text: #6E5500;
--phase-nursery: #5F4FA8;
--phase-nursery-bg: rgba(221, 216, 245, 0.55);
--phase-nursery-text: #544396;
/* ── Sunken surface: the footer, and anything meant to read as a deep
teal anchor band. Stays teal in both themes on purpose — it is the
"brand in action" card from the guideline. ─────────────────── */
--surface-sunken: #0C5F58;
--on-sunken: #FFFFFF;
--on-sunken-muted: #C9E4DB;
--on-sunken-faint: #A9D6CA;
--on-sunken-link: #C7EBF5;
/* ── Medals: rankings podium ────────────────────────────────────── */
--medal-gold: #806200;
--medal-silver: #6B7580;
--medal-bronze: #8A5A2B;
/* ── Channels ───────────────────────────────────────────────────────
CSS can't interpolate a hex token into rgba(), so any tint that needs
an arbitrary alpha reads the raw channels from here. Keep these in
sync with the hex tokens above — they are the same colours. */
--brand-rgb: 15, 118, 110;
--action-rgb: 190, 60, 39;
--coral-rgb: 249, 115, 96;
--sage-rgb: 167, 215, 197;
--status-above-rgb: 54, 116, 63;
--status-below-rgb: 169, 72, 31;
--ink-rgb: 28, 39, 49;
--text-inverse-rgb: 255, 255, 255;
--shadow-rgb: 28, 39, 49;
--muted-rgb: 95, 106, 117;
--phase-secondary-rgb: 47, 111, 143;
--medal-silver-rgb: 107, 117, 128;
--medal-bronze-rgb: 138, 90, 43;
/* ── Elevation ──────────────────────────────────────────────────── */
--shadow-soft: 0 1px 2px rgba(28, 39, 49, 0.04), 0 2px 8px rgba(28, 39, 49, 0.05);
--shadow-medium: 0 4px 18px rgba(28, 39, 49, 0.09);
--shadow-strong: 0 10px 34px rgba(28, 39, 49, 0.13);
--scrim: rgba(28, 39, 49, 0.5);
/* ── Type ───────────────────────────────────────────────────────────
Manrope for headings and key messaging; Inter for body copy, every
control and every figure. next/font expands each variable to the
family plus its metric-matched fallback, and the classes sit on
<html> so :root can see them — see app/layout.tsx. */
--font-display: var(--font-manrope), -apple-system, BlinkMacSystemFont, sans-serif;
--font-ui: var(--font-inter), -apple-system, BlinkMacSystemFont, sans-serif;
--font-data: var(--font-inter), -apple-system, BlinkMacSystemFont, sans-serif;
--font-prose: var(--font-inter), -apple-system, BlinkMacSystemFont, sans-serif;
/* Type scale, straight from the guideline: H1 40/48, H2 32/40, H3 24/32,
body large 16/24, body 14/20, small 12/16. */
--step--2: 0.75rem;
--step--1: 0.875rem;
--step-0: 1rem;
--step-1: 1.125rem;
--step-2: 1.5rem;
--step-3: 2rem;
--step-4: 2.5rem;
--step-5: 3rem;
/* ── Section rhythm ─────────────────────────────────────────────────
The vertical gap between the bands of a page, and the gap between a
band's header and its content. Two values, not seven: the landing page
previously set its own margin on every band (24 / 32 / 16 / 48px, no
scale), which is what made a designed page read as a stack of unrelated
strips. Bands must not set their own vertical margins — the page
container owns the gap. */
--section-gap: 4rem;
--section-head-gap: 1.5rem;
/* ── Illustration ───────────────────────────────────────────────────
The hero landscape's palette. These are tokens rather than literals in
the SVG for one reason: the drawing previously kept a fixed light
palette in both themes, which left a pale sky slab as the brightest
object on a near-black page — it out-shouted the H1 and the search box.
Artwork still shouldn't invert (that breaks the picture), so the dark
values below are a re-grade, not an inversion: the same scene, lit at
dusk, sitting a step above the panel it lives on. */
--ill-sky-high: #DAEDF8;
--ill-sky-low: #EFF8FB;
--ill-cloud: #FFFFFF;
--ill-hill-far: #D6EDE2;
--ill-hill-mid: #A7D7C5;
--ill-hill-near: #7FC3AC;
--ill-hill-front: #5BA88F;
--ill-path: #FAF6EE;
--ill-path-edge: #E6DAC2;
--ill-wall: #FCE8C3;
--ill-roof: #F0A868;
--ill-door: #0F766E;
--ill-window: #C7EBF5;
--ill-tree-dark: #2E7D6B;
--ill-tree-mid: #3E8C74;
--ill-tree-light: #4A9E85;
--ill-tree-pale: #7FC3AC;
--ill-pin: #F97360;
--ill-pin-eye: #FFFFFF;
/* ── Geometry & motion ──────────────────────────────────────────────
"Soft shapes, rounded corners" — the guideline's geometry is markedly
rounder than the old system's 3/6/10/16. */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 22px;
--transition: 0.2s ease;
--transition-slow: 0.4s ease;
}
/*
* Dark theme. Not in the guideline, which is light-only — but the site already
* ships one, so removing it would be a visible regression. Built from the same
* tokens: teal and coral are lifted independently so each still clears AA on
* the dark ground, and the ground itself keeps the Ink hue rather than going
* to black.
*
* Tokens only. Never style a component from inside this block, or the two
* themes drift apart the first time someone edits one of them.
*/
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--bg-primary: #111A20;
--bg-secondary: #16222A;
--bg-card: #18242C;
--surface-inverse: #E9EEF0;
--text-primary: #E9EEF0;
--text-secondary: #B4C2C7;
--text-muted: #8B9AA1; /* 5.5:1 on the card */
--text-inverse: #111A20;
--border: #26343D;
--border-strong: #35454F;
--brand: #5FC7BB;
--brand-strong: #7BD6CC;
--brand-stronger: #9BE3DB;
--brand-bg: rgba(95, 199, 187, 0.14);
--brand-on: #0A1418;
--action: #F08A72;
--action-strong: #F5A492;
--action-stronger: #F8BFB1;
--action-bg: rgba(240, 138, 114, 0.16);
--action-on: #241009;
--coral: #F08A72;
--mustard: #F2C94C;
--mustard-ink: #EFC658;
--sage: #8FCBB7;
--sky: #9CD4E6;
--lavender: #C2BBEA;
--sand: #1B2730;
--status-above: #7FCB8A;
--status-above-bg: rgba(127, 203, 138, 0.14);
--status-below: #EFA184;
--status-below-bg: rgba(239, 161, 132, 0.14);
--status-at: #8B9AA1;
--status-at-bg: rgba(139, 154, 161, 0.14);
--chart-1: #5FC7BB;
--chart-2: #7FCB8A;
--chart-3: #EFC658;
--chart-4: #EFA184;
--chart-5: #8FB4D9;
--chart-6: #B6A9DD;
--chart-grid: #26343D;
--chart-reference: #8B9AA1;
--series-1: #5FC7BB;
--series-2: #7FCB8A;
--series-3: #EFC658;
--series-4: #EFA184;
--series-5: #8FB4D9;
--series-6: #B6A9DD;
--series-7: #6FD0DC;
--series-8: #D99BB8;
--phase-primary: #5FC7BB;
--phase-primary-bg: rgba(95, 199, 187, 0.16);
--phase-primary-text: #8ADACF;
--phase-secondary: #8FB4D9;
--phase-secondary-bg: rgba(143, 180, 217, 0.16);
--phase-secondary-text: #AAC8E4;
--phase-all-through: #7FCB8A;
--phase-all-through-bg: rgba(127, 203, 138, 0.16);
--phase-all-through-text: #9AD8A3;
--phase-post16: #EFC658;
--phase-post16-bg: rgba(242, 201, 76, 0.16);
--phase-post16-text: #F2D179;
--phase-nursery: #B6A9DD;
--phase-nursery-bg: rgba(182, 169, 221, 0.16);
--phase-nursery-text: #C8BEE6;
--surface-sunken: #0C5F58;
--on-sunken: #FFFFFF;
--on-sunken-muted: #C9E4DB;
--on-sunken-faint: #A9D6CA;
--on-sunken-link: #C7EBF5;
--medal-gold: #EFC658;
--medal-silver: #A9B6BC;
--medal-bronze: #C99070;
--brand-rgb: 95, 199, 187;
--action-rgb: 240, 138, 114;
--coral-rgb: 240, 138, 114;
--sage-rgb: 143, 203, 183;
--status-above-rgb: 127, 203, 138;
--status-below-rgb: 239, 161, 132;
--ink-rgb: 233, 238, 240;
--text-inverse-rgb: 17, 26, 32;
--shadow-rgb: 0, 0, 0;
--muted-rgb: 139, 154, 161;
--phase-secondary-rgb: 143, 180, 217;
--medal-silver-rgb: 169, 182, 188;
--medal-bronze-rgb: 201, 144, 112;
/* Dusk re-grade of the hero landscape. The sky sits one step above the
Sand panel (#16222A) rather than 60 points above it, so the picture
reads as a framed vignette instead of a light leak. The pin keeps
the theme's coral and is the only bright thing left in the scene —
which is the point, since it marks "you are here". */
--ill-sky-high: #1B2C36;
--ill-sky-low: #16242C;
--ill-cloud: #2A3F4A;
--ill-hill-far: #1E3A38;
--ill-hill-mid: #234944;
--ill-hill-near: #2A5A51;
--ill-hill-front: #326B5E;
--ill-path: #4A5A52;
--ill-path-edge: #3A4842;
--ill-wall: #45423A;
--ill-roof: #8A5A3E;
--ill-door: #5FC7BB;
--ill-window: #2F5966;
--ill-tree-dark: #16302C;
--ill-tree-mid: #1B3A34;
--ill-tree-light: #21453D;
--ill-tree-pale: #2A5A51;
--ill-pin: #F08A72;
--ill-pin-eye: #16242C;
--shadow-soft: 0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.3);
--shadow-medium: 0 4px 18px rgba(0, 0, 0, 0.45);
--shadow-strong: 0 10px 34px rgba(0, 0, 0, 0.55);
--scrim: rgba(4, 8, 10, 0.65);
}
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-ui);
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
/* dvh (dynamic viewport) accounts for iOS Safari's collapsing toolbar;
fall back to vh on older engines that don't recognise dvh. */
min-height: 100vh;
min-height: 100dvh;
/* Suppress the iOS Safari grey tap flash — explicit :active states
below carry the press feedback instead. */
-webkit-tap-highlight-color: transparent;
}
/* Form controls don't inherit font or colour from their parent — the UA
supplies its own. Without this they render in the system font at pure
black, which is nearly invisible against --text-primary in light mode and
completely invisible on the dark ground. */
input,
select,
textarea,
button {
font-family: inherit;
color: inherit;
}
/* Every digit that could line up in a column does. This is a data product;
proportional numerals in a results table read as amateur. */
/* The interface is mostly figures, so tabular is the default here rather
than something each component has to remember. Running prose opts back out
below — proportional figures read better in a sentence. */
.main,
table,
[data-numeric] {
font-variant-numeric: tabular-nums;
}
/* Prose gets the serif; the interface never does. */
.prose {
font-family: var(--font-prose);
font-variant-numeric: normal;
font-size: var(--step-0);
line-height: 1.65;
}
/* Provide a baseline press feedback for the most common interactive
elements — replaces the suppressed default tap highlight. Buttons and
.btn-* classes carry their own :active states already; this handles
plain anchors used as inline links and bare button elements. */
@media (hover: none) and (pointer: coarse) {
a:active,
button:active {
opacity: 0.7;
}
}
/* Reserve space for the fixed mobile bottom tab bar (56px + safe-area inset). */
@media (max-width: 640px) {
body {
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
}
}
/* Skip link — visible only on focus for keyboard users */
.skip-link {
position: absolute;
top: -100px;
left: 1rem;
z-index: 10000;
padding: 0.5rem 1rem;
background: var(--surface-inverse);
color: var(--text-inverse);
font-size: 0.875rem;
font-weight: 600;
border-radius: var(--radius-md);
text-decoration: none;
transition: top 0.15s ease;
}
.skip-link:focus {
top: 0.5rem;
}
/* Focus indicators — brand hue, visible on both grounds */
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
/* ================================================================
Shared button classes — use these across all components
================================================================ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
padding: 0.5rem 1rem;
font-family: var(--font-ui);
font-size: 0.875rem;
font-weight: 600;
line-height: 1;
border-radius: var(--radius-md);
border: 1px solid transparent;
cursor: pointer;
transition: background var(--transition), border-color var(--transition), color var(--transition);
text-decoration: none;
white-space: nowrap;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Primary: brand fill — main CTAs (Search, Compare now) */
.btn-primary {
background: var(--brand);
color: var(--brand-on);
border-color: var(--brand);
}
.btn-primary:hover:not(:disabled) {
background: var(--brand-strong);
border-color: var(--brand-strong);
}
.btn-primary:active:not(:disabled) {
background: var(--brand-stronger);
border-color: var(--brand-stronger);
}
/*
* Action: coral fill — the one decisive action on a page, and only that.
*
* The guideline gives coral to "calls to action, highlights, important
* buttons" while its own UI-components sheet draws the primary button in
* teal. Both are honoured by making this a scarce, separate class: teal is
* the default primary, coral marks the single action a page exists for —
* the search submit. Use it more than once per screen and it stops meaning
* anything.
*/
.btn-action {
background: var(--action);
color: var(--action-on);
border-color: var(--action);
}
.btn-action:hover:not(:disabled) {
background: var(--action-strong);
border-color: var(--action-strong);
}
.btn-action:active:not(:disabled) {
background: var(--action-stronger);
border-color: var(--action-stronger);
}
/* Secondary: brand outline — supporting actions (Add to compare) */
.btn-secondary {
background: transparent;
color: var(--brand);
border: 1px solid var(--brand);
}
.btn-secondary:hover:not(:disabled) {
background: var(--brand-bg);
}
/* Tertiary: quiet — low-emphasis (View, Clear) */
.btn-tertiary {
background: var(--bg-secondary);
color: var(--text-secondary);
border: 1px solid var(--border);
}
.btn-tertiary:hover:not(:disabled) {
background: var(--border);
color: var(--text-primary);
}
/* Active toggle state — the compare button once a school is on the list */
.btn-active {
background: var(--brand-bg);
color: var(--brand);
border: 1px solid var(--brand);
}
.btn-active:hover:not(:disabled) {
background: transparent;
color: var(--text-secondary);
border-color: var(--border-strong);
}
/* Small variant */
.btn-sm {
padding: 0.3rem 0.625rem;
font-size: 0.8125rem;
}
/*
* Leaflet ships its own palette — a #ddd tile backdrop, #333 attribution text
* and a #0078A8 link blue that is the most saturated colour anywhere on the
* site and belongs to no part of this system. Pull the map chrome onto the
* tokens. This matters most in dark mode, where Leaflet's white attribution
* bar would otherwise sit on a near-black page.
*
* The tiles themselves stay as OSM renders them; only the chrome is ours.
*
* Every selector here is prefixed with `html` on purpose. leaflet.css is
* imported from a client component, so its chunk loads AFTER globals.css; at
* equal specificity the later sheet wins and these overrides lose silently.
* The `html` prefix takes them to 0,1,1 so load order stops mattering.
*/
html .leaflet-container {
background: var(--bg-secondary);
font-family: var(--font-ui);
}
html .leaflet-control-attribution {
background: rgba(var(--text-inverse-rgb), 0.82);
color: var(--text-muted);
}
html .leaflet-control-attribution a {
color: var(--brand);
}
html .leaflet-bar a {
background: var(--bg-card);
color: var(--text-primary);
border-bottom-color: var(--border);
}
html .leaflet-bar a:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
/* Main content column */
.main {
max-width: 1400px;
margin: 0 auto;
padding: 1.25rem 1.5rem;
min-width: 0;
overflow-x: clip;
}
@media (max-width: 768px) {
.main {
padding: 1rem;
}
/* Tighten the rhythm rather than abandon it — the ratio between the
section gap and the header gap stays the same. */
:root {
--section-gap: 2.75rem;
--section-head-gap: 1.15rem;
}
}
/* Honour the OS setting. Transitions collapse to near-instant rather than
zero so state changes still register as changes. */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}