/* =============================================================================
   Airside Crew — public website shell
   -----------------------------------------------------------------------------
   One stylesheet for every page. It holds the colour tokens, the type scale,
   the spacing scale, the header/footer and the components the designs reuse,
   so a new page is markup plus these classes and nothing more.

   Values come from apps/site/design/*.dc.html, where they were per-element
   inline styles produced by the design canvas. Nothing here is invented: the
   two colour sets are the `themes.day` and `themes.night` objects in
   Main.dc.html's renderVals(), and the breakpoints are its media queries.

   Not here: the phone and dashboard mockups. Those keep the designs' own
   inline styles so they cannot drift from App*.dc.html — see the note on
   style-src in src/security-headers.ts.
   ========================================================================== */

/* -----------------------------------------------------------------------------
   1. Font — self-hosted, not fonts.googleapis.com
   -----------------------------------------------------------------------------
   The designs link the Google Fonts stylesheet. That is two extra DNS lookups
   and TLS handshakes on the critical path, a dependency on a third party's
   uptime for the site's legibility, every visitor's IP address handed to
   Google, and a CSP that has to allow an outside style host and an outside
   font host. Self-hosting removes all four and costs 53 KB committed once.

   The axis values below reproduce exactly what Google serves for
   `Atkinson+Hyperlegible+Next:wght@400;600;800`: three weights, two unicode
   subsets, and — because the family ships as a variable font — the same two
   files behind all three weights. So the browser downloads one file for the
   Latin text on the page, not three.

   Copyright 2020-2024 The Atkinson Hyperlegible Next Project Authors, under
   the SIL Open Font License 1.1. The licence text is distributed with the
   font files as required: public/fonts/LICENSE.txt.
   -------------------------------------------------------------------------- */
@font-face {
	font-family: "Atkinson Hyperlegible Next";
	font-style: normal;
	font-weight: 400;
	font-display: swap;
	src: url("/fonts/atkinson-hyperlegible-next-latin.woff2") format("woff2");
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
		U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
		U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
	font-family: "Atkinson Hyperlegible Next";
	font-style: normal;
	font-weight: 600;
	font-display: swap;
	src: url("/fonts/atkinson-hyperlegible-next-latin.woff2") format("woff2");
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
		U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
		U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
	font-family: "Atkinson Hyperlegible Next";
	font-style: normal;
	font-weight: 800;
	font-display: swap;
	src: url("/fonts/atkinson-hyperlegible-next-latin.woff2") format("woff2");
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
		U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191,
		U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
	font-family: "Atkinson Hyperlegible Next";
	font-style: normal;
	font-weight: 400;
	font-display: swap;
	src: url("/fonts/atkinson-hyperlegible-next-latin-ext.woff2") format("woff2");
	unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
		U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
		U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
	font-family: "Atkinson Hyperlegible Next";
	font-style: normal;
	font-weight: 600;
	font-display: swap;
	src: url("/fonts/atkinson-hyperlegible-next-latin-ext.woff2") format("woff2");
	unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
		U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
		U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
	font-family: "Atkinson Hyperlegible Next";
	font-style: normal;
	font-weight: 800;
	font-display: swap;
	src: url("/fonts/atkinson-hyperlegible-next-latin-ext.woff2") format("woff2");
	unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
		U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
		U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* -----------------------------------------------------------------------------
   2. Tokens
   -----------------------------------------------------------------------------
   Day is the default. Night arrives two ways and both must work:
     - the device asks for it (prefers-color-scheme), unless the visitor has
       explicitly chosen day;
     - the visitor chose it with the switch (data-theme on <html>).
   scripts/theme.js sets data-theme before first paint, so there is no flash.

   Brand colours are fixed and never themed (root CLAUDE.md).
   -------------------------------------------------------------------------- */
:root {
	/* Brand — fixed in both themes. */
	--navy: #02113a;
	--blue: #0461f8;
	--sky: #04a5f8;

	/* Day. */
	--bg: #f4f6fa;
	--surface: #ffffff;
	--band: #eaeff7;
	--ink: #02113a;
	--muted: #3d4b68;
	--line: #dde3ee;
	--accent: #0452d0;
	--btn-line: #8e9cb8;
	--shadow: rgba(2, 17, 58, 0.12);

	/* The five the Oct 27 redesign adds, named the way the rest of this block
	   already is rather than as a second system. They are the `themes` object
	   in design/Main.dc.html's renderVals(), which every redesigned page
	   shares:
	     chip  — the quiet fill behind an icon tile or a segmented control
	     route — the dashed taxiway line, and an unfilled control's edge
	     tile  — the filled step tile, which is navy by day and blue at night
	     lift / lift-big — the two card shadows, small and large
	   `--lift-big` rather than `--liftBig`: every other token here is
	   hyphenated, and a stylesheet that spells two conventions is one someone
	   has to guess at. */
	--chip: #e3eaf6;
	--route: #b9c6dd;
	--tile: #02113a;
	--lift: 0 1px 2px rgba(2, 17, 58, 0.06), 0 8px 22px rgba(2, 17, 58, 0.08);
	--lift-big: 0 2px 4px rgba(2, 17, 58, 0.05), 0 18px 44px rgba(2, 17, 58, 0.12);

	/* A form field's own fill. The designs give it its own `field` colour
	   rather than reusing `surface`, and the two differ at night. The Screens
	   page's phone-width select is the first field on the site; the signup
	   form is the next. */
	--field: #ffffff;

	/* Two card colours, used by the How it works page: red for the failure
	   card ("Alerts offline. Verify by radio.") and amber for work that is
	   planned and not built. The values are the designs' own `redBg/redInk/
	   redLine` and `amberBg/amberInk/amberLine` (the themes object in
	   How.dc.html's renderVals()). mockups.css keeps its own `--m-red-*` and
	   `--m-amber-*` copies on purpose: those have to track App*.dc.html, this
	   pair tracks the page chrome, and one set must be free to change without
	   dragging the other with it. */
	--red-bg: #fde7e5;
	--red-ink: #8a1a10;
	--red-line: #d9534a;
	--amber-bg: #fff3d6;
	--amber-ink: #5c3b00;
	--amber-line: #e59a00;

	/* The hero and the founder card are navy in both themes. */
	--on-navy: #e6ecf8;
	--on-navy-muted: #c3cee6;
	--on-navy-link: #7fd0ff;
	--hero-grid: #0a2560;
	--hero-grid-dash: #0e3378;

	/* Spacing and measure. */
	--wrap-max: 1180px;
	--wrap-pad: 28px;
	--sec-gap: 96px;

	/* Minimum touch target. Well above the ~10 mm floor in the HCI rules. */
	--tap: 48px;

	color-scheme: light;
}

/* Device preference, unless the visitor explicitly chose day. */
@media (prefers-color-scheme: dark) {
	:root:not([data-theme="day"]) {
		--bg: #030a1c;
		--surface: #0b1633;
		--band: #071027;
		--ink: #e6ecf8;
		--muted: #a7b6d3;
		--line: #1c2c55;
		--accent: #7fd0ff;
		--btn-line: #3a5085;
		--shadow: rgba(0, 0, 0, 0.5);
		--chip: #13244a;
		--route: #2a3b66;
		--tile: #0461f8;
		--lift: 0 1px 2px rgba(0, 0, 0, 0.5), 0 10px 26px rgba(0, 0, 0, 0.45);
		--lift-big: 0 2px 4px rgba(0, 0, 0, 0.5), 0 22px 50px rgba(0, 0, 0, 0.5);
		--field: #0e1b38;
		--red-bg: #3a1210;
		--red-ink: #ffb3a8;
		--red-line: #e0614f;
		--amber-bg: #2a1f08;
		--amber-ink: #f5c56b;
		--amber-line: #e39b22;
		color-scheme: dark;
	}
}

/* Explicit choice wins over the device in both directions. */
:root[data-theme="night"] {
	--bg: #030a1c;
	--surface: #0b1633;
	--band: #071027;
	--ink: #e6ecf8;
	--muted: #a7b6d3;
	--line: #1c2c55;
	--accent: #7fd0ff;
	--btn-line: #3a5085;
	--shadow: rgba(0, 0, 0, 0.5);
	--chip: #13244a;
	--route: #2a3b66;
	--tile: #0461f8;
	--lift: 0 1px 2px rgba(0, 0, 0, 0.5), 0 10px 26px rgba(0, 0, 0, 0.45);
	--lift-big: 0 2px 4px rgba(0, 0, 0, 0.5), 0 22px 50px rgba(0, 0, 0, 0.5);
	--field: #0e1b38;
	--red-bg: #3a1210;
	--red-ink: #ffb3a8;
	--red-line: #e0614f;
	--amber-bg: #2a1f08;
	--amber-ink: #f5c56b;
	--amber-line: #e39b22;
	color-scheme: dark;
}

/* -----------------------------------------------------------------------------
   3. Base
   -----------------------------------------------------------------------------
   `border-box` everywhere, EXCEPT where a design element relies on the
   opposite. The design canvas has no box-sizing reset, so every element it
   draws is `content-box` unless its own style says otherwise: a size it
   gives is the size of the CONTENT, and padding and border are added
   outside it. For an element with padding or a border AND a size — a
   `height`, a `min-height`, a `width` or a flex basis — the two models draw
   different boxes. The header bar is `height: 64px` plus a 1px border, so
   66px drawn; How it works' fact panel is a 380px basis plus 13px of padding
   and border a side, so it takes more of its row than a border-box 380px
   would and the text column beside it less. Those elements carry
   `box-sizing: content-box` in their own rules, so the drawn size is the
   built size and the arithmetic stays the design's. Each one was found by
   measuring the design file in a browser; search this sheet for
   `content-box` to list them.
   -------------------------------------------------------------------------- */
*,
*::before,
*::after {
	box-sizing: border-box;
}

html {
	/* Anchors land below the sticky-free header with a little air. */
	scroll-padding-top: 24px;
}

/* 16px and `normal`, because that is what every design inherits. The design
   canvas's `body` and its page root (`#hr-top`) set neither, so anything in
   a design that declares no font-size or line-height of its own renders at
   the browser's 16px and `normal` — list items, form labels, the onward
   cards' "Next" label and title, the footer's small print. This sheet had
   17px and 1.55 here, from the pre-redesign shell, and every such element
   came out larger than drawn: How it works' onward card measured 114px tall
   against the design's 104px in headless Chrome at 1440px. Anything that wants another size or leading
   declares it, as the designs do. */
body {
	margin: 0;
	background: var(--bg);
	color: var(--ink);
	font-family: "Atkinson Hyperlegible Next", "Segoe UI", system-ui, sans-serif;
	font-weight: 400;
	font-size: 16px;
	line-height: normal;
	-webkit-text-size-adjust: 100%;
}

img {
	max-width: 100%;
}

a {
	color: var(--blue);
}
a:hover {
	color: #0442b0;
}

button,
input,
select,
textarea {
	font-family: inherit;
}
button {
	cursor: pointer;
}

::selection {
	background: var(--sky);
	color: var(--navy);
}

:focus-visible {
	outline: 3px solid var(--sky);
	outline-offset: 3px;
}

/* Skip link — the header has five nav items before the content starts. */
.skip {
	position: absolute;
	left: -9999px;
	top: 0;
	z-index: 20;
	padding: 12px 18px;
	border-radius: 0 0 10px 0;
	background: #ffffff;
	color: var(--navy);
	font-weight: 800;
	text-decoration: none;
}
.skip:focus {
	left: 0;
}

/* -----------------------------------------------------------------------------
   4. Layout
   -------------------------------------------------------------------------- */
.wrap {
	max-width: var(--wrap-max);
	margin: 0 auto;
	padding-left: var(--wrap-pad);
	padding-right: var(--wrap-pad);
}

.sec {
	padding-top: var(--sec-gap);
}

.stack {
	display: flex;
	flex-direction: column;
	gap: 32px;
}
.stack--tight {
	gap: 28px;
}

.headline-block {
	display: flex;
	flex-direction: column;
	gap: 12px;
	max-width: 680px;
}

/* -----------------------------------------------------------------------------
   5. Type scale
   -------------------------------------------------------------------------- */
/* THE SCALE HAS TWO HERO STEPS (docs/ui-rules.md, Dylan, Oct 27, 2026).
   -----------------------------------------------------------------------------
   The Oct 27 redesign draws Home's h1 larger than the five pages behind it,
   and draws those five at an IDENTICAL size to one another. That is deliberate
   hierarchy, a landing page against its inner pages, and not the per-page
   drift the "shared scale wins" rule was written to stop — so the shared scale
   carries two hero steps and every page matches its own design using these
   classes. A third step would need the same conversation this one had; a page
   may still not reinterpret the scale for itself.

   `.t-h1` keeps the value the five unconverted pages render today, so nothing
   moves under them before their own tasks. They take `.t-h1--page` as each
   one converts, and the size they inherit then comes from here rather than
   from each page re-deriving it from its own design file. */
.t-h1 {
	margin: 0;
	font-size: clamp(38px, 6.2vw, 72px);
	line-height: 1.02;
	font-weight: 800;
	letter-spacing: -0.02em;
	text-wrap: balance;
}

/* Step 1: the Home hero. design/Main.dc.html. Its line-height is `.t-h1`'s
   1.02, which is what that design draws. */
.t-h1--hero {
	font-size: clamp(40px, 6.2vw, 74px);
}

/* Step 2: every inner page's hero. How, Screens, Progress, About and Contact
   all specify exactly this, line-height included: they draw 1.04 where Home
   draws 1.02, so the step carries its own rather than inheriting `.t-h1`'s. */
.t-h1--page {
	font-size: clamp(36px, 5.4vw, 64px);
	line-height: 1.04;
}

.t-h2 {
	margin: 0;
	font-size: clamp(30px, 4vw, 48px);
	line-height: 1.06;
	font-weight: 800;
	letter-spacing: -0.02em;
	text-wrap: balance;
}

/* THE FOUR CARD STEPS (docs/ui-rules.md, "Card headings match their
   drawings", Dylan, Oct 28, 2026: "Match the drawings").
   -----------------------------------------------------------------------------
   A heading inside a card takes `.t-h2` AND one of these, never a size of
   its own. The designs draw card headings at exactly four sizes, each one
   repeated wherever the same kind of card appears, so the scale carries four
   steps and each card heading uses the step its own design draws. A fifth
   size needs the same conversation, not a page deciding for itself.

   Each step is the whole drawn style, not only the size: the line height
   and letter spacing below are the designs' own for that step, and none of
   the designs balances a card heading's lines, so `text-wrap` goes back to
   ordinary wrapping (`.t-h2` balances, for the section headings that draw
   it). Where the record disagrees with itself it is said beside the step. */

/* Card step 1: Progress's status card ("In the design phase.") and the Get
   in touch form card. design/Progress.dc.html, design/Contact.dc.html. */
.t-h2--card-s {
	font-size: clamp(24px, 2.8vw, 32px);
	line-height: 1.15;
	letter-spacing: -0.01em;
	text-wrap: wrap;
}

/* Card step 2: the Screens page's captions. design/Screens.dc.html.
   design/Contact.dc.html also draws its "Message sent." state at this size,
   with a 1.15 line height and no letter spacing; the captions' figures are
   the ones used here. */
.t-h2--card-m {
	font-size: clamp(26px, 3vw, 34px);
	line-height: 1.12;
	letter-spacing: -0.01em;
	text-wrap: wrap;
}

/* Card step 3: Progress's signup card and Home's founder band, both on navy.
   design/Progress.dc.html and design/Main.dc.html draw no letter spacing
   for either, where the other three steps draw -0.01em. */
.t-h2--card-l {
	font-size: clamp(26px, 3.2vw, 38px);
	line-height: 1.12;
	letter-spacing: normal;
	text-wrap: wrap;
}

/* Card step 4: About's founder card. design/About.dc.html. */
.t-h2--card-xl {
	font-size: clamp(28px, 3.4vw, 40px);
	line-height: 1.1;
	letter-spacing: -0.01em;
	text-wrap: wrap;
}

.t-h3 {
	margin: 0;
	font-size: 20px;
	line-height: 1.25;
	font-weight: 800;
}

.t-lead {
	margin: 0;
	max-width: 540px;
	font-size: clamp(17px, 2vw, 20px);
	line-height: 1.55;
	font-weight: 400;
}

.t-sub {
	margin: 0;
	max-width: 560px;
	font-size: 17px;
	line-height: 1.55;
	font-weight: 400;
	color: var(--muted);
}

.t-body {
	margin: 0;
	font-size: 16px;
	line-height: 1.55;
	font-weight: 400;
	color: var(--muted);
}

.t-small {
	font-size: 15px;
	font-weight: 400;
	color: var(--muted);
}

.t-note {
	margin: 0;
	font-size: 14px;
	line-height: 1.5;
	font-weight: 400;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   6. Buttons and links
   -------------------------------------------------------------------------- */
.btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-height: 52px;
	padding: 0 22px;
	border-radius: 10px;
	border: 2px solid transparent;
	text-decoration: none;
	font-size: 16px;
	font-weight: 800;
	transition: filter 0.15s ease-out, transform 0.15s ease-out;
}
.btn:hover {
	filter: brightness(1.12);
}
.btn:active {
	transform: translateY(1px);
}

.btn--primary {
	background: var(--blue);
	border-color: var(--blue);
	color: #ffffff;
}
.btn--primary:hover {
	color: #ffffff;
}

.btn--on-navy {
	background: #ffffff;
	border-color: #ffffff;
	color: var(--navy);
}
.btn--on-navy:hover {
	color: var(--navy);
}

.btn--ghost-navy {
	border-color: rgba(230, 236, 248, 0.45);
	color: #ffffff;
}
.btn--ghost-navy:hover {
	color: #ffffff;
}

/* The same outline button on the page background rather than on the hero's
   navy: the designs' `btnLine` border with the body ink. First used by the
   Contact page's "Follow on LinkedIn". */
.btn--ghost {
	border-color: var(--btn-line);
	color: var(--ink);
}
/* `.btn`'s shared hover is `filter: brightness(1.12)`, which does almost
   nothing to an outline button on a light background — there is no fill to
   brighten. So this one answers with its border instead. */
.btn--ghost:hover {
	color: var(--ink);
	border-color: var(--ink);
}

.btn-row {
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
	padding-top: 4px;
}

.lnk {
	text-underline-offset: 4px;
	text-decoration-thickness: 2px;
}
.lnk:hover {
	text-decoration: underline;
}

/* "How it works, in detail →" */
.more-link {
	align-self: flex-start;
	min-height: var(--tap);
	display: inline-flex;
	align-items: center;
	gap: 8px;
	font-size: 16px;
	font-weight: 800;
	color: var(--accent);
	text-decoration: none;
}
.more-link:hover {
	color: var(--accent);
}

/* -----------------------------------------------------------------------------
   7. Header
   -----------------------------------------------------------------------------
   The header sits on the navy hero on every page, so its logo is always the
   light-on-dark one and its colours do not follow the theme.
   -------------------------------------------------------------------------- */
.site-header {
	position: relative;
	z-index: 2;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	padding-top: 22px;
	padding-bottom: 22px;
}

.site-header__logo {
	display: inline-flex;
}
/* The nudge below is an optical correction for the ASSET, not a layout tweak,
   and it is the one place the built site deliberately differs from the design
   mockups (recorded in README.md's "Changes from the designs").

   The logo box centres perfectly: measured in the browser at 1280px, the
   image's box, the nav container's box and each nav link's box all have
   `centerY: 46.625` — identical to 0.000px. What looks low is the artwork
   inside it. The PNG is 900x176 and is a lockup: the signal waves and speech
   bubble on the left reach source row 6, while the WORDMARK's ink spans only
   source rows 88..169. Its centre therefore sits at source y=129 against a
   canvas centre of y=88 — 41 source pixels low, which is 7.920px at the
   header's rendered 34px.

   41 of 176 = 23.295% of the rendered height, so the percentage keeps the
   correction right if the logo is ever rendered at another size.

   It aligns the wordmark to the header's own centre line (the one every box in
   the header already shares) rather than to the nav text's ink. The nav text's
   ink centre measures 44.5 and the CTA's 45.219 — the two nav items disagree
   with each other by 0.719px, because of the links' bottom-only 2px underline
   slot and the font's ascent/descent asymmetry — so there is no single "nav
   text centre" to aim at, and chasing it would tie the logo's position to the
   underline's width.

   A `transform` because it must move nothing else: measured before and after,
   the nav's left edge (393.406) and the header's height (93.25) are unchanged.

   `CLAUDE.md` forbids changing the logo and `brand/` is read-only, so this
   compensates in CSS instead. If the asset is ever recropped, delete this
   declaration rather than adjusting it — src/html/header-logo-nudge.test.ts
   pins the file it was measured from and will say so. */
.site-header__logo img {
	height: 34px;
	width: auto;
	transform: translateY(-23.295%);
}

.nav-links {
	display: flex;
	align-items: center;
	gap: 28px;
	font-size: 15px;
	font-weight: 700;
}
.nav-links a:not(.btn) {
	color: var(--on-navy-muted);
	text-decoration: none;
	padding: 12px 0;
	border-bottom: 2px solid transparent;
}
/* Pointer and keyboard get the same answer: the muted nav ink comes up to
   white. It was `var(--on-navy-muted)` — the colour the link already is —
   because this rule's original job was only to stop the global `a:hover` blue
   reaching a link on the navy hero. That left five of the header's links with
   no hover state at all.

   White and NOT the sky underline: the underline is how the header says which
   page you are on (`[aria-current="page"]` below), and a hover that borrowed
   it would make every link look like the current one on the way past. Colour
   alone is safe here because it is not carrying the meaning — it is feedback
   for a pointer that is already on the link. */
.nav-links a:not(.btn):hover,
.nav-links a:not(.btn):focus-visible {
	color: #ffffff;
}
.nav-links .btn {
	min-height: 44px;
	padding: 0 18px;
}
/* The current page's own nav entry: white text over a sky underline, per the
   designs (e.g. design/About.dc.html draws the About link as
   `color: #FFFFFF; border-bottom: 2px solid #04A5F8`).

   `a:not(.btn)` is load-bearing, not tidiness. On a page whose call-to-action
   points at itself — Contact is the first, and any later page with the same
   shape — the CTA also carries `aria-current="page"`. Unscoped, this rule is a
   class plus an attribute selector, so it outranked `.btn--on-navy` and forced
   `color: #ffffff` onto a white button: white text on a white background, a
   contrast ratio of 1.000, rendering as a blank rectangle. Scoping it to
   non-button links makes the button unreachable from here, so the clash cannot
   come back on the next such page.

   The designs agree that this is the right answer rather than a different
   current-state look for the button: design/Contact.dc.html gives its CTA
   `aria-current="page"` and styles it identically to every other page's CTA
   (`background: #FFFFFF; color: #02113A`, no underline), i.e. no current-page
   treatment at all. So the button gets none here either. */
.nav-links a:not(.btn)[aria-current="page"] {
	color: #ffffff;
	border-bottom-color: var(--sky);
}

/* Day/night switch and burger share the icon-button shape. */
.icon-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	min-height: var(--tap);
	min-width: var(--tap);
	padding: 0 14px;
	border-radius: 12px;
	border: 1px solid rgba(230, 236, 248, 0.28);
	background: rgba(255, 255, 255, 0.06);
	color: #ffffff;
	font-size: 15px;
	font-weight: 700;
}
.icon-btn:hover {
	background: rgba(255, 255, 255, 0.12);
}

.header-controls {
	display: flex;
	align-items: center;
	gap: 10px;
}

.burger {
	display: none;
	padding: 0;
}

/* The DEPICTED phone app's own day/night switch, inside the mockups on Home
   and Screens — not a control of this site.
   -----------------------------------------------------------------------------
   These classes were shared with a switch in the site header until that
   control moved to the footer as a three-option radio group (docs/web-app.md,
   Dylan, Oct 24, 2026 — see `.theme-pick` in the footer section below). What
   is left here serves the mockups alone: public/index.html and
   public/screens.html both carry `<span class="ts-day">Day</span>` inside the
   depicted flagger screen, copied from design/AppFlagger.dc.html so the
   mockups cannot drift from the designs. The grid that positioned these three
   inside the old header pill went with the pill; the mockup's own header is a
   flex row with its spacing in inline attributes, which is why only the
   `visibility` pairing and one margin remain.

   The inactive member of each pair is hidden with `visibility: hidden` rather
   than removed with `display: none`, so it still occupies its space and the
   depicted pill's width does not change when the theme is toggled.

   `visibility: hidden` also takes the inactive label out of the accessibility
   tree, so only the state the switch is in is announced, exactly as
   `display: none` did. That is the CSS `visibility` contract (MDN) and
   ACCNAME-1.2 step 2.1 "Hidden Not Referenced", which lists
   `visibility: hidden` alongside `display: none` as hidden for name-from-
   content.

   The 8px stays: it is the gap between the state word and the suffix beside
   it inside the mockup ("Day · auto"), not header chrome. */
.ts-auto {
	margin-left: 8px;
}

.ts-night,
:root[data-theme="night"] .ts-day {
	visibility: hidden;
}
:root[data-theme="night"] .ts-night {
	visibility: visible;
}
@media (prefers-color-scheme: dark) {
	:root:not([data-theme="day"]) .ts-day {
		visibility: hidden;
	}
	:root:not([data-theme="day"]) .ts-night {
		visibility: visible;
	}
}
/* "· auto" shows only while no choice has been made, so the label says where
   the theme came from as well as what it is. Driven by the same attribute as
   the colours, so it cannot drift out of step and never flashes. Hidden rather
   than removed, so choosing a theme does not resize the pill either. */
:root[data-theme] .ts-auto {
	visibility: hidden;
}

/* Available to a screen reader, invisible on screen. */
.vh {
	position: absolute;
	width: 1px;
	height: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
}

/* -----------------------------------------------------------------------------
   8. Mobile menu
   -------------------------------------------------------------------------- */
.mobile-menu {
	position: absolute;
	z-index: 5;
	top: 84px;
	left: 12px;
	right: 12px;
	padding: 10px;
	border-radius: 16px;
	background: #ffffff;
	color: var(--navy);
	box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
	display: flex;
	flex-direction: column;
	animation: drop 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.mobile-menu[hidden] {
	display: none;
}
/* The burger menu's copy of the nav. `a:not(.btn)` for the same reason, and
   with the same mechanism, as `.nav-links a:not(.btn)` above — this is the
   text-link rule.

   Unscoped it was `.mobile-menu a`, a class plus a type selector (0-1-1),
   which outranks the single-class `.btn--primary` (0-1-0). It therefore won
   `background` and `color` on the menu's "Get in touch" and computed a
   transparent background with navy text, on all five pages, from the day the
   site was built. The designs draw that button as a blue pill with white text
   (`background: #0461F8; color: #FFFFFF`, design/Main.dc.html line 79, and
   line 79 of every `*-Mobile.dc.html`), and give the transparent/navy, 18px,
   `0 14px` treatment below to the TEXT links on the lines above it — so these
   declarations were always the text links'.

   Scoped by not matching rather than by `!important`: a rule that cannot reach
   a button needs no weight to win, and leaves the next person free to restyle
   a button here without out-shouting anything. */
.mobile-menu a:not(.btn) {
	display: flex;
	align-items: center;
	min-height: 52px;
	padding: 0 14px;
	border-radius: 10px;
	background: transparent;
	color: var(--navy);
	text-decoration: none;
	font-size: 18px;
	font-weight: 800;
}
/* Same scoping, same reason, for the burger menu's copy of the nav. The
   designs apply this pale-blue current-page plate to text links only
   (design/About-Mobile.dc.html line 78) and never to the CTA, which stays the
   blue primary button even on its own page (design/Contact.dc.html line 79).
   Unscoped, this rule beat `.btn--primary` on specificity and repainted the
   Contact page's CTA from blue to #e3ecff. */
.mobile-menu a:not(.btn)[aria-current="page"] {
	background: #e3ecff;
}
.mobile-menu .btn {
	margin-top: 6px;
	justify-content: center;
	min-height: 52px;
	font-size: 17px;
}

@keyframes drop {
	from {
		opacity: 0;
		transform: translateY(-8px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

/* -----------------------------------------------------------------------------
   9. Hero
   -------------------------------------------------------------------------- */
.hero {
	position: relative;
	background: var(--navy);
	color: var(--on-navy);
}

/* The clip that keeps the taxiway diagram inside the hero belongs HERE, on the
   diagram, and not on `.hero`.

   It used to be `overflow: hidden` on `.hero`, carrying the comment "The
   taxiway diagram is drawn past the edges on purpose." It did clip the
   diagram. It also clipped `.mobile-menu`, which is `position: absolute`
   inside the same `<section class="hero">`, so on the pages whose hero is
   just a heading the bottom of the open burger menu — its "Get in touch"
   button — was cut off at the hero's edge and could not be tapped. The hero
   needs to contain a decorative background; it has no business containing the
   navigation.

   Moving the declaration does not un-clip the diagram. The diagram is an
   `<svg>` element sized `inset: 0` to exactly the hero's box, and the SVG user
   agent style sheet already overrides `overflow` to hidden for an `svg` that
   is not the root element of a stand-alone document (SVG 2, Rendering Model).
   The declaration below therefore states what the UA sheet already computes:
   it is written out so the clip is owned by the element that needs it, and so
   that redrawing this background some other way — a `<div>` with a background
   image, say — cannot silently lose the clip along with the UA rule.

   `.hero` keeps `position: relative`: that is the containing block both the
   diagram's `inset` and the menu's `top`/`left`/`right` resolve against, and
   it is untouched. Dropping `overflow` does stop `.hero` establishing a block
   formatting context, since every `overflow` value except `visible` and `clip`
   creates one (MDN). Nothing inside the hero floats and no child's margin
   needs containing, so no box moves. Should a BFC ever be wanted here again,
   ask for it with `display: flow-root`, which creates one without clipping —
   bringing `overflow: hidden` back would re-break the menu. */
.hero__diagram {
	position: absolute;
	inset: 0;
	width: 100%;
	height: 100%;
	overflow: hidden;
}
.hero__grid {
	fill: none;
	stroke: var(--hero-grid);
	stroke-width: 2;
}
.hero__grid-dash {
	fill: none;
	stroke: var(--hero-grid-dash);
	stroke-width: 1.5;
	stroke-dasharray: 14 12;
}

.hero__inner {
	position: relative;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 36px 56px;
	padding-top: 24px;
	padding-bottom: 80px;
}

.hero__copy {
	flex: 1 1 480px;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 22px;
}
.hero__copy .t-h1 {
	color: #ffffff;
}
.hero__copy .t-lead {
	color: var(--on-navy-muted);
}

/* The one-column hero the inner pages use: a heading and a standfirst, no
   mockup beside them. The measures are the designs' (How.dc.html): the
   heading is allowed to run wider than the standfirst, so the first line of
   body text still sits at a comfortable reading width under a wide heading. */
.hero__lede {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: 16px;
	padding-top: 32px;
	padding-bottom: 72px;
}
.hero__lede .t-h1 {
	max-width: 820px;
	color: #ffffff;
}
.hero__lede .t-lead {
	max-width: 620px;
	color: var(--on-navy-muted);
}

.hero__aside {
	flex: 0 0 auto;
	margin: 0 auto;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 20px;
}

.hero__wording {
	margin: 0;
	max-width: 300px;
	display: flex;
	flex-direction: column;
	gap: 6px;
	color: #ffffff;
}
.hero__wording-label {
	display: flex;
	align-items: center;
	gap: 8px;
	font-size: 14px;
	font-weight: 700;
	color: var(--on-navy-muted);
}
.hero__wording-quote {
	font-size: 17px;
	line-height: 1.5;
	font-weight: 800;
}

/* The taxiway letter shown as a sign, so "Golf" is never carried by the
   spoken word alone. */
.twy-sign {
	display: inline-block;
	min-width: 1.1em;
	text-align: center;
	padding: 0.05em 0.3em;
	margin: 0 0.25em 0 0.1em;
	border-radius: 4px;
	background: var(--navy);
	color: #ffffff;
	border: 2px solid var(--sky);
	font-size: 0.95em;
	font-weight: 800;
	line-height: 1.25;
}
.nowrap {
	white-space: nowrap;
}

/* -----------------------------------------------------------------------------
   10. Mockup frames
   -----------------------------------------------------------------------------
   A fixed-size mockup scaled down inside a clipped frame, exactly as the
   designs do it. The inner element keeps its real pixel size so the mockup
   markup is identical to App*.dc.html; `transform: scale()` shrinks the
   picture and the frame clips the leftover layout box.
   -------------------------------------------------------------------------- */
.shot {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 12px;
}
.shot__tag {
	font-size: 14px;
	font-weight: 700;
	opacity: 0.8;
}
.shot__bezel {
	padding: 10px;
	border-radius: 46px;
	background: #0b1633;
	box-shadow: 0 24px 60px var(--shadow);
}
.hero .shot__bezel {
	box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55);
}
/* The dashboard bezel is a laptop lid at wide sizes and a phone below 680 px;
   see .dash-frame. */
.shot__bezel--dash {
	border-radius: 18px;
}

.phone-box {
	width: 281px;
	height: 608px;
	border-radius: 36px;
	overflow: hidden;
	/* Decoration: it must not swallow taps or take keyboard focus. */
	pointer-events: none;
}
.phone-screen {
	width: 390px;
	height: 844px;
	transform: scale(0.72);
	transform-origin: top left;
}

/* The dashboard frame.
   The designs render the dashboard twice — once in a wide frame and once in a
   phone frame, swapped at 680 px. Here there is one frame and one copy of the
   markup: the frame changes size, which changes the width of the element
   inside it, and the container query in mockups.css reshapes the dashboard to
   match. One picture to keep correct instead of two. */
.dash-frame {
	width: 640px;
	height: 450px;
	border-radius: 10px;
	overflow: hidden;
	pointer-events: none;
}
.dash-inner {
	width: 1280px;
	height: 900px;
	transform: scale(0.5);
	transform-origin: top left;
	/* The size container for the dashboard mockup. It has to be the wrapper
	   and not the mockup itself: a container query styles the container's
	   descendants, never the container, so the mockup could not reshape
	   itself. Its width is the fixed one above, which is the whole point. */
	container-type: inline-size;
}

/* -----------------------------------------------------------------------------
   11. Principles strip
   -------------------------------------------------------------------------- */
/* NO `border-bottom` HERE, and that is a correction rather than a style
   choice (task R1b). The pre-redesign strip was a full-width band with a rule
   line under it; design/Main.dc.html's strip is a FLOATING CARD pulled up over
   the hero's edge, and it draws its border on the <ul> inside this section
   (`border-radius: 22px; border: 1px solid {{c.line}}`), not on the section.
   The old declaration was unopposed by section 26 and so went on painting a
   0.8px line right across the page under the card. Found by the same sweep as
   the step-card defect; no page but index.html carries a `strip` class. */
.strip {
	background: var(--surface);
}
.strip__list {
	margin: 0 auto;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
	gap: 0 16px;
	padding-top: 0;
	padding-bottom: 0;
}
.strip__item {
	display: flex;
	align-items: center;
	gap: 10px;
	padding: 16px 0;
	font-size: 15px;
	line-height: 1.3;
	font-weight: 800;
}
.strip__item svg {
	flex-shrink: 0;
	stroke: var(--accent);
}

/* -----------------------------------------------------------------------------
   12. Numbered steps — REMOVED, see section 26.8
   -----------------------------------------------------------------------------
   This section held the pre-redesign `.steps`, `.steps__item` and
   `.steps__item svg`: a two-column grid with a 32px icon gutter, a rule line
   under each row, and an accent stroke forced onto the icon. The Oct 27
   redesign makes `.steps__item` a card and the icon a white glyph on a navy
   tile, and those three rules kept winning on the rebuilt Home page:

     * `.steps__item { align-items: start }` was not opposed by anything in
       section 26, so the card's flex column shrink-wrapped its children and
       "Step N" sat hard against the tile with a 0px gap, where the design
       pushes it to the card's right edge.
     * `.steps__item svg { stroke: var(--accent) }` beat the icon's own
       `stroke="currentColor"` presentation attribute — a CSS declaration
       always does — so the glyph painted #0452D0 on the navy tile at 2.73:1
       instead of inheriting the tile's #FFFFFF at 18.36:1.
     * `.steps { border-top: 1px solid var(--line) }` drew a rule line above
       the three cards that the design does not draw.

   Deleted rather than overridden, and that is checkable: no built page except
   `public/index.html` carries any `steps` class at all, so these rules reached
   nothing but the page that wants different ones. Overriding them would have
   left three dead declarations for the next person to trip over — this is the
   fourth old-rule-beats-new-rule defect found on this one page.

   Measurements and the search that found it:
   docs/evidence/2026-10-27-step0.5-R1b-header-icons-and-corrections.md.
   -------------------------------------------------------------------------- */

/* -----------------------------------------------------------------------------
   13. Screens tabs
   -------------------------------------------------------------------------- */
.band {
	background: var(--band);
	border-top: 1px solid var(--line);
	border-bottom: 1px solid var(--line);
}
.band__inner {
	padding-top: 64px;
	padding-bottom: 64px;
	display: flex;
	flex-direction: column;
	gap: 28px;
}
.band__intro {
	max-width: 680px;
	display: flex;
	flex-direction: column;
	gap: 22px;
}

.tabs {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: 4px;
	padding: 4px;
	border-radius: 12px;
	background: var(--surface);
	border: 1px solid var(--line);
	max-width: 440px;
}
.tabs button {
	min-height: var(--tap);
	padding: 0 8px;
	border-radius: 9px;
	border: 0;
	background: transparent;
	color: var(--ink);
	font-size: 15px;
	font-weight: 800;
}
.tabs button[aria-selected="true"] {
	background: var(--blue);
	color: #ffffff;
}
/* An unselected tab takes the band tint under a pointer. `:not()` so the
   selected one keeps its blue fill: a hover that undressed it would read as
   the selection having moved before it has. */
.tabs button:not([aria-selected="true"]):hover {
	background: var(--band);
}

.tabpanel {
	padding: 40px clamp(16px, 4vw, 48px);
	border-radius: 20px;
	background: var(--surface);
	border: 1px solid var(--line);
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 36px;
}
.tabpanel[hidden] {
	display: none;
}
.tabpanel__text {
	width: 100%;
	max-width: 960px;
	display: flex;
	flex-direction: column;
	gap: 18px;
}
.tabpanel__title {
	margin: 0;
	font-size: clamp(22px, 2.6vw, 30px);
	line-height: 1.15;
	font-weight: 800;
}

/* A two-column list that becomes one column when it runs out of room. */
.split-list {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
	gap: 0 32px;
	border-top: 1px solid var(--line);
}
.split-list > li {
	padding: 16px 0;
	border-bottom: 1px solid var(--line);
	font-size: 16px;
	line-height: 1.55;
	font-weight: 400;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   14. Buyer columns
   -------------------------------------------------------------------------- */
.buyer {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
	gap: 0 40px;
	/* NO `border-top`. It was here for the pre-redesign list, which had a rule
	   line above it; the redesign draws "For companies" as a grid of cards with
	   nothing above them, and section 26 never named the property, so the old
	   line kept being painted. Task R1b. */
}
.buyer__col {
	padding: 22px 0;
	border-bottom: 1px solid var(--line);
	display: flex;
	flex-direction: column;
	gap: 10px;
}
.buyer__points {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 8px;
}
.buyer__points li {
	display: flex;
	gap: 12px;
	font-size: 16px;
	line-height: 1.55;
	font-weight: 400;
	color: var(--muted);
}
.buyer__bullet {
	flex: 0 0 8px;
	height: 8px;
	margin-top: 9px;
	border-radius: 2px;
	background: var(--accent);
}

.cta-row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 12px 20px;
}

/* -----------------------------------------------------------------------------
   15. Timeline ("Built in public")
   -----------------------------------------------------------------------------
   Markup for the items comes from src/html/timeline.ts, so these class names
   are a contract with that module.
   -------------------------------------------------------------------------- */
.timeline {
	max-width: 760px;
}
.timeline ol {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
}

.tl-item {
	display: grid;
	grid-template-columns: 28px minmax(0, 1fr);
	gap: 0 16px;
}
.tl-rail {
	display: flex;
	flex-direction: column;
	align-items: center;
}
.tl-dot {
	width: 14px;
	height: 14px;
	margin-top: 5px;
	border-radius: 3px;
	background: var(--blue);
	border: 3px solid var(--blue);
}
/* "Up next" has not happened yet: hollow dot, muted ring, muted meta line.
   Shape and fill both change, so the difference is never carried by colour
   alone. */
.tl-dot--pending {
	background: var(--bg);
	border-color: var(--muted);
}
.tl-line {
	flex: 1;
	width: 2px;
	background: var(--line);
	margin-top: 4px;
}
.tl-item:last-child .tl-line {
	display: none;
}
.tl-body {
	padding-bottom: 26px;
	display: flex;
	flex-direction: column;
	gap: 6px;
}
.tl-title {
	font-size: 20px;
	line-height: 1.25;
	font-weight: 800;
}
.tl-meta {
	font-size: 14px;
	font-weight: 700;
	color: var(--accent);
}
.tl-meta--pending {
	color: var(--muted);
}
.tl-text {
	font-size: 16px;
	line-height: 1.55;
	font-weight: 400;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   16. Founder card — REMOVED, see section 26.11
   -----------------------------------------------------------------------------
   This section held the pre-redesign `.sec--founder`, `.founder`,
   `.founder__photo`, `.founder__title`, `.founder__text` and
   `.founder__link` (with its hover and focus rule). Home is the only page
   that draws the founder band, and it is on the redesign shell, so these
   rules reached that one page and nothing else — and two of their properties
   were not opposed by section 26.11:

     * `.founder { flex-direction: column }`. 26.11 sets `display: flex` and
       `flex-wrap` but no direction, because the design draws the row as
       the default. The old column went on applying, so the photo stacked
       above the text and the card rendered 640px tall against the
       design's ~301px.
     * `.founder__link { align-self: flex-start }`, which lifted "Why I'm
       building this" to the top of a row the design centres.

   Deleted rather than scoped or opposed property by property: the old card
   had no page left to style, and a redesign rule that has to restate every
   property an older rule might set is how this defect happens in the first
   place. src/html/founder-band-rules.test.ts holds the band to that: no rule
   before the redesign boundary may name a founder class.
   -------------------------------------------------------------------------- */

/* -----------------------------------------------------------------------------
   17. How it works page
   -----------------------------------------------------------------------------
   The components that only this page uses. Sizes here are component-local and
   come straight from design/How*.dc.html, the same way .strip__item,
   .tl-title and .buyer__points carry theirs; the shared type scale above is
   used for every heading and standfirst, so there is one scale and not two.
   -------------------------------------------------------------------------- */

/* Each section on this page opens with a hairline above its heading, which is
   what separates one topic from the next on a page that is all prose. */
.headline-block--ruled {
	border-top: 1px solid var(--line);
	padding-top: 40px;
}

/* Short label and value facts about the box, as a definition list so the label
   and the value stay associated. The How it works page draws its own version
   of this as `.hw-facts` (section 28.3). */
.facts {
	margin: 0;
	padding: 22px;
	border-radius: 14px;
	background: var(--surface);
	border: 1px solid var(--line);
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
	gap: 20px 28px;
}
.facts__pair {
	display: flex;
	flex-direction: column;
	gap: 4px;
}
.facts dt {
	font-size: 14px;
	font-weight: 700;
	color: var(--muted);
}
.facts dd {
	margin: 0;
	font-size: 18px;
	font-weight: 800;
}

/* Examples of what a phone says, each with a plain-language note underneath
   saying which situation it is. The speaker icon marks them as spoken. */
.say-list {
	margin: 0;
	padding: 0;
	max-width: 880px;
	list-style: none;
	border-top: 1px solid var(--line);
}
.say-list > li {
	padding: 18px 0;
	border-bottom: 1px solid var(--line);
	display: grid;
	grid-template-columns: 26px minmax(0, 1fr);
	gap: 4px 12px;
}
.say-list svg {
	grid-row: span 2;
	margin-top: 4px;
	stroke: var(--accent);
}
.say-list__quote {
	font-size: 19px;
	line-height: 1.5;
	font-weight: 800;
}
.say-list__case {
	font-size: 15px;
	font-weight: 400;
	color: var(--muted);
}

/* Two cards that have to read differently from the rest of the page: the red
   one is what the phone says when it cannot do its job, the amber one marks
   something that is designed and not built. Both get a border as well as a
   background, so neither depends on colour alone. */
.note-card {
	display: flex;
	flex-direction: column;
}
.note-card--alarm {
	max-width: 880px;
	padding: 22px;
	border-radius: 14px;
	gap: 10px;
	background: var(--red-bg);
	color: var(--red-ink);
	border: 2px solid var(--red-line);
}
.note-card--planned {
	max-width: 680px;
	padding: 20px;
	border-radius: 14px;
	gap: 6px;
	background: var(--amber-bg);
	color: var(--amber-ink);
	border: 1px dashed var(--amber-line);
}
.note-card__shout {
	font-size: clamp(24px, 3vw, 32px);
	line-height: 1.15;
	font-weight: 800;
}
.note-card__body {
	font-size: 16px;
	line-height: 1.55;
	font-weight: 600;
}
.note-card__label {
	font-size: 15px;
	font-weight: 800;
}
.note-card__lead {
	font-size: 17px;
	line-height: 1.55;
	font-weight: 600;
}

/* One shape for both icon lists on this page: an icon in its own column
   spanning the two rows of text beside it. The two modifiers are the designs'
   two sizes — a pair of wide features, and a four-item list of limits. */
.icon-list {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	border-top: 1px solid var(--line);
}
.icon-list > li {
	border-bottom: 1px solid var(--line);
	display: grid;
	align-items: start;
}
.icon-list svg {
	grid-row: span 2;
	stroke: var(--accent);
}
.icon-list__text {
	font-size: 16px;
	line-height: 1.55;
	font-weight: 400;
	color: var(--muted);
}

.icon-list--pair {
	grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
	gap: 0 28px;
}
.icon-list--pair > li {
	padding: 18px 0;
	grid-template-columns: 30px minmax(0, 1fr);
	gap: 4px 12px;
}
.icon-list--pair .icon-list__title {
	margin: 0;
	font-size: 19px;
	font-weight: 800;
}

.icon-list--limits {
	grid-template-columns: repeat(auto-fit, minmax(min(380px, 100%), 1fr));
	gap: 0 40px;
}
.icon-list--limits > li {
	padding: 16px 0;
	grid-template-columns: 26px minmax(0, 1fr);
	gap: 3px 14px;
}
.icon-list--limits svg {
	margin-top: 2px;
}
.icon-list--limits .icon-list__title {
	font-size: 18px;
	font-weight: 800;
}

/* The link on to the next page, with its own closing rule. It sits closer to
   the section above it than the shared --sec-gap would put it, because it
   belongs to the end of the page rather than being another topic. */
.sec--next {
	padding-top: 56px;
	padding-bottom: 72px;
}
.next-row {
	border-top: 1px solid var(--line);
	padding-top: 20px;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 8px 24px;
}
.next-row__label {
	font-size: 16px;
	font-weight: 400;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   18. Screens page
   -----------------------------------------------------------------------------
   The picker above the six screens, and the one line of copy inside a panel
   that the Home page's screens section has no equivalent for. Sizes come
   straight from design/Screens*.dc.html; the panel itself, its heading, the
   mockup frames and the two-column point list are the shared components from
   sections 10 and 13, so Home and this page cannot drift apart.
   -------------------------------------------------------------------------- */

/* Tab list, phone-width select and the day/night pair on one row, wrapping
   when they stop fitting. */
.shots-bar {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 14px 24px;
}

/* Six tabs, each its own bordered button rather than segments inside one
   container (which is what .tabs is): at six across, the segment look runs
   out of room before the labels do. auto-fit lets them wrap to a second row
   instead of shrinking below a readable width. */
.screen-tabs {
	flex: 1 1 520px;
	min-width: 0;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
	gap: 6px;
}
.screen-tabs button {
	min-height: var(--tap);
	padding: 0 10px;
	border-radius: 10px;
	border: 1px solid var(--line);
	background: var(--surface);
	color: var(--ink);
	font-size: 15px;
	font-weight: 800;
}
/* Selected is carried by fill and by aria-selected, never by colour alone:
   the open panel sits directly below and names the same screen. */
.screen-tabs button[aria-selected="true"] {
	border-color: var(--blue);
	background: var(--blue);
	color: #ffffff;
}
/* An unselected tab firms up its border under a pointer, rather than taking
   any blue: blue is this row's selected state, and previewing it on hover
   would say the panel below had already changed. */
.screen-tabs button:not([aria-selected="true"]):hover {
	border-color: var(--btn-line);
}

/* The phone-width alternative to the tabs. A native select so the platform's
   own full-height picker does the work; six 104 px tabs at 375 px would be
   below the size a gloved hand can hit. */
.screen-select {
	width: 100%;
	font-size: 15px;
	font-weight: 800;
}
.screen-select select {
	display: block;
	width: 100%;
	height: 52px;
	margin-top: 8px;
	padding: 0 14px;
	border-radius: 10px;
	border: 1px solid var(--btn-line);
	background: var(--field);
	color: var(--ink);
	font-size: 16px;
	font-weight: 700;
}

/* Day or night, as a radio group. It sets the page's theme, so the mockups
   follow it — see the note in screens.html. */
.mode-switch {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 4px;
	padding: 4px;
	border-radius: 12px;
	background: var(--surface);
	border: 1px solid var(--line);
}
.mode-switch button {
	min-height: 44px;
	padding: 0 18px;
	border-radius: 9px;
	border: 0;
	background: transparent;
	color: var(--ink);
	font-size: 14px;
	font-weight: 800;
}
.mode-switch button[aria-checked="true"] {
	background: var(--blue);
	color: #ffffff;
}
/* Same shape as the tabs above, for the same reason: the option that is not
   in effect tints, the one that is keeps its fill. */
.mode-switch button:not([aria-checked="true"]):hover {
	background: var(--band);
}

/* The panels sit on the page background on this page, not inside a .band
   section as they do on Home, so the panel itself takes the band colour. */
.tabpanel--band {
	background: var(--band);
}

/* "For flaggers", "For leads" — who the screen is for, under its heading. */
.shot__who {
	font-size: 15px;
	font-weight: 700;
	color: var(--accent);
}

/* Which of the two pickers is showing. Swapped at 560 px in section 22. */
.only-sm {
	display: none;
}

/* -----------------------------------------------------------------------------
   19. About page — REMOVED, see section 30
   -----------------------------------------------------------------------------
   This section held the pre-redesign About page's six classes: `.story__photo`
   (a 280px square above the prose), `.story` and `.story__text` (the reading
   column), `.how-list`, `.how-list__pair` with its `dt`/`dd` rules, and
   `.note-card--stand` with `.note-card__title` (the amber "Where things
   stand" card). The Oct 27 redesign replaces every one of them — the prose is
   now three cards, the how list is one card with three rows, and "Where
   things stand" is a card-shaped link to the Progress page, not an amber
   note — so none of these rules has an element left to style.

   Deleted rather than left in place, and that is checkable rather than
   asserted. Checked with a word-exact class match, because a substring one is
   wrong here — `\bstory\b` also matches the NEW page's `ab-story`, since the
   hyphen is a word boundary:
     grep -rhoE 'class="[^"]*"' public/*.html | grep -oE '[A-Za-z0-9_-]+' \
       | sort -u | grep -xE 'story|story__photo|story__text|how-list|how-list__pair|note-card--stand|note-card__title'
   finds nothing now that the page is converted, so these rules reached no
   page at all.

   THIS IS ALSO WHERE THE `68ch` CAP WENT. apps/site/README.md records it as
   superseded on Oct 27, 2026: Dylan asked for it in Oct 2026 for a long line
   of first-person prose, and `.story__text` and `.note-card--stand` were the
   two declarations that carried it. The redesign has neither element, and
   every long-measure block on the new page carries the design's own pixel
   `max-width` instead — so the figure left with the component it was for,
   not because the readability reason stopped mattering. It was the last `ch`
   unit in this stylesheet: `grep -nE '[0-9]+(\.[0-9]+)?ch\b' site.css` now
   finds only prose in comments.

   The `.note-card` base `.note-card--stand` layered on is section 17's and is
   LEFT ALONE — but not for the reason section 21's note gives for the same
   base. That note said About, Screens and Contact were still on the old shell
   and used it; About was the last of the three that did, so measured now,
   `grep -rn 'note-card' public/ src/ | grep -v styles/site.css` finds
   NOTHING. It is dead rather than shared. It stays because section 17 is the
   pre-redesign How it works page's block and retiring it is that shell's
   removal, not this page's: the page whose own section this is is the only one
   a page task may retire. Section 17 comes out with the rest of the
   superseded shell when the last page converts.
   -------------------------------------------------------------------------- */

/* -----------------------------------------------------------------------------
   20. Contact page
   -----------------------------------------------------------------------------
   One column, because there is no form. docs/web-app.md and
   apps/site/README.md both record that "Get in touch" is an email link; the
   reason the design's form is not built is in public/contact.html.

   The measure is this file's, not the design's: the design sized this column
   by what was left beside the form, and with the form gone it would run the
   full 1180px wrap. 560px matches .t-sub's measure, so the page's narrowest
   text column and this one agree.
   -------------------------------------------------------------------------- */
.contact {
	max-width: 560px;
	display: flex;
	flex-direction: column;
	gap: 18px;
	/* This page has no "Next" row, so the column itself has to hold the gap
	   above the footer that .sec--next holds on the other pages. */
	padding-bottom: 72px;
}
.contact__group {
	display: flex;
	flex-direction: column;
	gap: 6px;
}
/* The address is the page's main action, so it is the biggest thing on the
   page after the heading. `overflow-wrap: anywhere` because an email address
   has no spaces to break at and docs/ui-rules.md forbids sideways scrolling
   at any width. */
.contact__email {
	min-height: 44px;
	display: inline-flex;
	align-items: center;
	font-size: clamp(22px, 2.6vw, 28px);
	line-height: 1.2;
	font-weight: 800;
	color: var(--accent);
	text-decoration: none;
	overflow-wrap: anywhere;
}
/* The page's main action had no hover state: this restated its own colour, to
   keep the global `a:hover` blue off it. An underline rather than a colour
   change, because at this size a colour shift is barely readable and because
   it is what the rest of the site's text links do (`.lnk`). */
.contact__email:hover,
.contact__email:focus-visible {
	color: var(--accent);
	text-decoration: underline;
	text-decoration-thickness: 2px;
	text-underline-offset: 4px;
}
.contact__next {
	border-top: 1px solid var(--line);
	padding-top: 18px;
	display: flex;
	flex-direction: column;
	gap: 10px;
}
.contact__next-title {
	font-size: 18px;
	font-weight: 800;
}
.contact__steps {
	margin: 0;
	padding-left: 22px;
	display: flex;
	flex-direction: column;
	gap: 8px;
	font-size: 16px;
	line-height: 1.55;
	font-weight: 400;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   21. Progress page — REMOVED, see section 29
   -----------------------------------------------------------------------------
   This section held the pre-redesign Progress page's two classes:
   `.note-card--upnext`, the "Up next" card as a neutral variant of section
   17's `.note-card`, and `.progress-log`, the 760px column that held the
   timeline and the LinkedIn button under it. The Oct 27 redesign replaces
   both — the "Up next" item is now a row inside the "Where it is now" panel,
   and the log is a filtered list of month groups — so neither rule has an
   element left to style.

   Deleted rather than left in place, and that is checkable rather than
   asserted: `grep -rn 'note-card--upnext\|progress-log' public/ src/` finds
   nothing outside this stylesheet now that the page is converted, so these
   rules reached no page at all. Leaving them would have been two dead
   declarations under a heading saying the filter bar is deferred, which it no
   longer is.

   The `.note-card` base they layered on is section 17's and is LEFT ALONE:
   About, Screens and Contact are still on the old shell and use it. Section
   17 comes out with the rest of the superseded shell when the last page
   converts.
   -------------------------------------------------------------------------- */

/* -----------------------------------------------------------------------------
   22. Footer
   -------------------------------------------------------------------------- */
.site-footer {
	border-top: 1px solid var(--line);
}
.site-footer__inner {
	padding-top: 32px;
	padding-bottom: 36px;
	display: flex;
	flex-direction: column;
	gap: 18px;
}
.site-footer__row {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 8px 24px;
}
.site-footer__logo {
	height: 26px;
	width: auto;
}
/* Two logo files, one per theme, so the wordmark stays legible on both
   backgrounds. Only one is ever displayed; both are below the fold. */
.site-footer__logo--night,
:root[data-theme="night"] .site-footer__logo--day {
	display: none;
}
:root[data-theme="night"] .site-footer__logo--night {
	display: block;
}
@media (prefers-color-scheme: dark) {
	:root:not([data-theme="day"]) .site-footer__logo--day {
		display: none;
	}
	:root:not([data-theme="day"]) .site-footer__logo--night {
		display: block;
	}
}

.site-footer nav {
	display: flex;
	flex-wrap: wrap;
	gap: 0 22px;
}
.site-footer nav a {
	min-height: 44px;
	display: inline-flex;
	align-items: center;
	color: var(--muted);
	text-decoration: none;
	font-size: 15px;
	font-weight: 700;
}
/* Same correction as the header's: this restated the colour the link already
   is, so the footer nav had no hover state. Muted ink comes up to full ink. */
.site-footer nav a:hover,
.site-footer nav a:focus-visible {
	color: var(--ink);
}
.site-footer__legal {
	margin: 0;
	flex: 1 1 360px;
	min-width: 0;
}
.site-footer__meta {
	font-size: 14px;
	font-weight: 700;
	color: var(--muted);
}
.site-footer__meta a {
	color: var(--muted);
}

/* "Theme: Auto · Day · Night" — the site's only theme control.
   -----------------------------------------------------------------------------
   docs/web-app.md (Dylan, Oct 24, 2026) asks for a QUIET LINE in the footer,
   not the pill this replaced in the header. So the options are styled as the
   footer's own text is — same size, same weight, same muted colour, no border
   and no fill — and the only thing marking the current one is that it takes
   the full ink colour and an underline. That is what lets the line read as a
   sentence while still saying which of the three is in effect.

   Two cells, because the label is not one of the choices: "Theme:" sits
   outside the `radiogroup` element, which is named by it through
   `aria-labelledby`, so the accessible name of the group is the text on the
   screen rather than a second copy of it in an attribute.

   Both boxes are flex, which is what lets the markup put each option on its
   own line for legibility: a flex container discards whitespace-only text
   nodes, so the indentation between the buttons contributes nothing and `gap`
   is the only thing setting their spacing. The "·" separators are real
   elements rather than `::before` content so they can be hidden from the
   accessibility tree in the markup, where it is visible to a reader.

   `flex-wrap` on both: at 375px this line shares the footer with the legal
   note, and docs/ui-rules.md forbids horizontal overflow outright. */
.theme-pick {
	margin: 0;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0 10px;
	font-size: 14px;
	font-weight: 700;
	color: var(--muted);
}
.theme-pick__group {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0 8px;
}
.theme-pick__option {
	/* A 44px target without a 44px box. docs/ui-rules.md wants 44px; the
	   decision wants a text line, and a 44px-tall visible button is not one —
	   so the height is spent on transparent padding above and below the text,
	   which the surrounding flex row absorbs. */
	min-height: 44px;
	padding: 0 2px;
	border: 0;
	background: transparent;
	color: var(--muted);
	/* Inherited rather than restated, so this line can never drift away from
	   the footer text it is supposed to read as part of. */
	font: inherit;
	cursor: pointer;
}
/* The checked option. `aria-checked` and not a class: site.js already has to
   set that attribute for the screen reader, so keying the visible state off
   the same attribute means there is no second thing to keep in step — the
   pattern `.mode-switch button[aria-checked="true"]` already uses. */
.theme-pick__option[aria-checked="true"] {
	color: var(--ink);
	text-decoration: underline;
	text-decoration-thickness: 2px;
	text-underline-offset: 5px;
}
/* The two that are not in effect answer a pointer by coming up to full ink —
   the same amount of change the checked one already shows, and no underline,
   so hovering never dresses an option up as the one in effect. Equal
   specificity to the rule above and written after it, which is what lets the
   checked option keep its own colour while it is hovered. */
.theme-pick__option:hover,
.theme-pick__option:focus-visible {
	color: var(--ink);
}
.theme-pick__sep {
	/* Lighter than the options it sits between, so the three choices read as
	   the content and the punctuation does not compete with them. */
	color: var(--line);
}

/* -----------------------------------------------------------------------------
   23. Responsive
   -----------------------------------------------------------------------------
   Breakpoints are the designs': 900 (nav collapses), 680 (dashboard shot
   swaps), 560 (phone widths). Everything is checked at 375 px and 1280 px for
   horizontal overflow, which docs/ui-rules.md forbids outright.
   -------------------------------------------------------------------------- */
@media (max-width: 900px) {
	:root {
		--sec-gap: 72px;
	}
	.nav-links {
		display: none;
	}
	.burger {
		display: inline-flex;
	}
	.dash-frame {
		width: 520px;
		height: 366px;
	}
	.dash-inner {
		transform: scale(0.40625);
	}
}

/* Below this the wide frame no longer fits, so the dashboard is shown in a
   phone frame — which is also how a supervisor would really be reading it on
   a screen this narrow. */
@media (max-width: 680px) {
	.shot__bezel--dash {
		border-radius: 46px;
	}
	.dash-frame {
		width: 281px;
		height: 608px;
		border-radius: 36px;
	}
	.dash-inner {
		width: 390px;
		height: 844px;
		transform: scale(0.72);
	}
}

@media (max-width: 560px) {
	:root {
		--wrap-pad: 18px;
		--sec-gap: 56px;
	}
	.phone-box {
		width: 236px;
		height: 511px;
	}
	.phone-screen {
		transform: scale(0.605);
	}
	.dash-frame {
		width: 236px;
		height: 511px;
	}
	.dash-inner {
		transform: scale(0.605);
	}
	.tabpanel {
		padding-left: 16px;
		padding-right: 16px;
	}
	/* Six tabs stop being hittable at this width; the native select takes
	   over. See section 18. */
	.only-lg {
		display: none;
	}
	.only-sm {
		display: block;
	}
}

/* -----------------------------------------------------------------------------
   24. Motion
   -----------------------------------------------------------------------------
   docs/ui-rules.md, "Motion" (Dylan, Oct 24, 2026): the site may animate, and
   the motion is calm and purposeful — the product working quietly, never a
   sizzle reel. Two things move on this site and nothing else does.

     (a) A section arriving. As the visitor scrolls, each direct section of
         <main> fades up ten pixels and settles. Once: scripts/site.js stops
         watching a section the moment it has been shown, so scrolling back up
         does not replay it.
     (b) A control answering. The colour and border changes that already
         happen on :hover and :focus-visible are eased rather than snapped, so
         a pointer moving across the page feels answered instead of making it
         flicker.

   WHAT DELIBERATELY DOES NOT MOVE, because restraint is the brief:

     * The hero. It carries the page's one sentence, and a visitor who has
       just arrived should read it, not watch it arrive.
     * The header, the footer and the theme line as blocks.
     * Anything in a loop. Nothing on this site repeats for ever — see the
       note on the aircraft marker in mockups.css, and the test that now holds
       that line for both stylesheets.
     * Cards and list items inside a section, which arrive with the section
       rather than one after another. A stagger reads as a thing being
       presented; a section settling reads as a page behaving well.
     * `outline`, which appears in none of the transitions below. A focus ring
       that fades in is a focus ring that is briefly hard to see.

   THREE INDEPENDENT GATES hold the reduced-motion promise. docs/ui-rules.md
   allows that rule no exceptions, and the reason is medical rather than
   aesthetic: motion makes some people ill, which is why they asked.

     1. The hidden state below sits inside `prefers-reduced-motion:
        no-preference`. Under `reduce` it never applies, so there is nothing
        to undo and no animation to sit through. It also covers a browser too
        old to know the feature at all: `reduce` and `no-preference` are the
        only two values, and such a browser matches neither.
     2. scripts/site.js evaluates the same preference before it marks
        anything, so under `reduce` no element is ever given a hidden state.
     3. Section 25 is an `!important` backstop over both, so a future edit
        that moves the hidden state out of gate one still cannot trap content.

   The finished state is this stylesheet's DEFAULT and the hidden state is the
   addition — which is why every failure mode lands on visible content rather
   than on a blank page. src/html/motion.test.ts asserts each gate separately,
   because any one of them is enough to keep the promise and so any one of
   them can be deleted without a symptom on the machine of whoever deletes it.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
	/* Applied by scripts/site.js, and only to sections that had not yet
	   reached its trigger line when the page loaded. Content the visitor can
	   already see is never hidden to be shown again. */
	.reveal {
		opacity: 0;
		/* `transform` and not `margin-top` or `top`: a transform moves no
		   other element and forces no reflow, so a section on its way in
		   cannot shift what is already on screen, and the position it settles
		   at is its natural one rather than one this rule chose. Ten pixels,
		   because the fade is doing the work and the rise is only telling the
		   eye which way the page is going. */
		transform: translateY(10px);
	}
	.reveal--shown {
		opacity: 1;
		transform: none;
		/* Declared on the finished state rather than on `.reveal`: a
		   transition is read from the style the element is moving TO, so
		   adding this class is what starts it and the initial hide is
		   instant. 450ms is long enough to read as deliberate and short
		   enough that scrolling never waits for it. `ease-out` because the
		   section should arrive settling, not accelerating away. */
		transition: opacity 450ms ease-out, transform 450ms ease-out;
	}

	/* Controls answering a pointer or a key. Each list holds only properties
	   that something above actually changes on :hover or :focus-visible, so
	   none of these is a transition waiting for a state that never arrives.

	   `a` rather than the seven link classes it covers, which would have to
	   be kept in step with every page added. It also reaches the links inside
	   the depicted app screens, and harmlessly: those are `inert`, and no rule
	   anywhere changes their colour, so there is never a change for this to
	   ease. It adds no declaration to their markup, which stays byte-identical
	   between pages and pinned against design/App*.dc.html. */
	a,
	.theme-pick__option {
		transition: color 140ms ease-out;
	}
	.nav-links a:not(.btn) {
		transition: color 140ms ease-out, border-color 140ms ease-out;
	}
	.icon-btn,
	.tabs button,
	.screen-tabs button,
	.mode-switch button {
		transition: background-color 140ms ease-out, border-color 140ms ease-out,
			color 140ms ease-out;
	}
	.btn--ghost {
		transition: filter 0.15s ease-out, transform 0.15s ease-out,
			border-color 140ms ease-out;
	}

	/* The arrow in "See the progress →" leans the way the link goes. The
	   nudge lives inside this media query rather than beside the hover rule
	   it belongs to, so that under reduced motion the arrow does not move at
	   all — a 3px jump with the transition cancelled is still movement. */
	.more-link:hover svg,
	.more-link:focus-visible svg {
		transform: translateX(3px);
	}
	.more-link svg {
		transition: transform 140ms ease-out;
	}
}

/* -----------------------------------------------------------------------------
   25. Reduced motion
   -----------------------------------------------------------------------------
   Gate three. Everything here is redundant on the day it is written, which is
   the point: it stands up on the day gate one is broken by someone who cannot
   see the symptom on their own machine.

   Cancelling the animation and the transition is NOT enough on its own, and
   the two declarations on `.reveal` are why: `transition: none` applied to an
   element sitting at opacity 0 leaves it at opacity 0, instantly and for the
   rest of the visit. The finished state has to be asserted, not merely
   un-animated.
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation: none !important;
		transition: none !important;
	}
	.reveal {
		opacity: 1 !important;
		transform: none !important;
	}
	html {
		scroll-behavior: auto;
	}
}

/* =============================================================================
   26. The Oct 27 redesign: the shared shell and the Home page
   =============================================================================
   Everything below builds `apps/site/design/Main.dc.html`, which Dylan
   approved on Oct 27, 2026 along with its five siblings (Home-Mobile,
   Home-Mobile-Night, Home-Night, Home-Tablet, Home-Tablet-Night). Those five
   differ from Main.dc.html only in the canvas preview size and the default
   theme — every rule here is therefore Main.dc.html's, and the responsive
   behaviour is its media queries at 900, 760, 620 and 560 px.

   WHY SO MANY SELECTORS START WITH `.page`
   ----------------------------------------
   The redesign replaces the shell on all six pages, but only the Home page is
   rebuilt in this task; How it works, Screens, Progress, About and Get in
   touch are still on the old shell until their own tasks convert them. Where
   a class name is shared with one of those pages, the new value is written as
   `.page .thing` rather than edited into the existing rule, so a page without
   the `.page` wrapper keeps exactly what it has today. One of those names is
   shared through generated markup rather than through a file: `.tl-*` is
   emitted by src/html/timeline.ts, which the Progress page's log also calls.

   When the last page converts, the scoping comes off and the superseded
   copies above go with it. Until then, an unscoped edit to one of those names
   is a change to a page this task was told not to touch.
   -------------------------------------------------------------------------- */

/* -----------------------------------------------------------------------------
   26.1 The page wrapper
   -------------------------------------------------------------------------- */

/* design/Main.dc.html's `#hr-top`. Two jobs: the page background and ink, and
   the horizontal clip.

   `overflow-x: clip` IS STILL NEEDED, and not for the reason it was added.
   It was written here as the box the sticky header sticks inside; the header
   no longer sticks (docs/web-app.md, "The public website's header does not
   stick"). What it does now is the job it also always did: the hero's blurred
   glow is painted about 190px past the right edge at every width, and without
   a clip the page would scroll sideways, against docs/ui-rules.md.

   Still `clip` and NOT `hidden`, for a second reason that outlived the first:
   `hidden` creates a scrolling mechanism on this box, which makes it a
   block-axis clip as well — so it could cut off the open burger menu, which
   is the defect src/html/hero-clip.test.ts exists for and which this site has
   already shipped once. `clip` creates no scroll container, which is also why
   the other axis may stay `visible`. */
.page {
	width: 100%;
	overflow-x: clip;
	background: var(--bg);
	color: var(--ink);
}

/* Section rhythm. The designs' `.sec` is 112px, 84px below 900 and 64px below
   560, where the shell this replaces uses 96/72/56. */
.page .sec {
	padding-top: 112px;
}

/* -----------------------------------------------------------------------------
   26.2 No visible scrollbars, anywhere
   -----------------------------------------------------------------------------
   Dylan has asked for this repeatedly, and apps/site/README.md records it:
   areas still scroll, only the bar is hidden. Two declarations are needed
   because they are two different features — `scrollbar-width: none` is the
   standard property (MDN: Baseline since December 2024, and the element
   "remains scrollable") and `::-webkit-scrollbar` is the non-standard
   WebKit/Blink pseudo-element that predates it and is what older Chrome and
   Safari actually obey.

   MDN warns that hiding a scrollbar hurts accessibility, and the warning is
   about content that can ONLY be reached by dragging the bar. Nothing here is
   in that position: the page scrolls with the wheel, the keyboard and touch,
   and no element on it is a scroll container whose content is otherwise
   unreachable. The cost is real, but it is a visible-affordance cost rather
   than a reachability one.
   -------------------------------------------------------------------------- */
/* `html:has(.page)` rather than a bare `html`, and this is not fussiness.
   The page scrolls on the root element, so hiding ITS bar needs a rule on
   `html` — which would reach all six pages, and five of them are still on the
   old shell and outside this task. `:has()` makes the root rule conditional on
   the new shell actually being present, so each page gains this as it converts
   and none of them changes before its own task. (Measured: without the
   scoping, `html { scrollbar-width: none }` applies to every page in the
   directory.) */
/* SCOPED TO THE ELEMENTS THAT ACTUALLY SCROLL, which is the root and the
   body. It used to carry `.page *`, `.page *::before` and `.page *::after` as
   well — every descendant of the page — and that was wrong in two ways at
   once, flagged in the security review of task R1:

     * It did nothing. Measured on the built page: seven elements under
       `.page` are overflow containers and ALL SEVEN are `overflow: hidden`,
       so not one of them can ever display a scrollbar. The blanket reached
       several hundred elements and changed the rendering of none.
     * It pre-emptively suppressed the affordance on elements nobody has
       written yet. MDN's accessibility warning about `scrollbar-width: none`
       is about content reachable ONLY by dragging the bar, and a rule that
       covers every future descendant decides that question in advance, for
       every scrollable area anyone adds later, invisibly.

   So: if a new area genuinely needs its bar hidden, give it its own rule and
   check at that point that its content is reachable by wheel, keyboard and
   touch. That is a decision worth making once per area, out loud.
   `apps/site/README.md` records Dylan's instruction — areas still scroll,
   only the bar is hidden — and nothing here weakens it: the page's own
   scrollbar, which is the one he was looking at, is still hidden. */
html:has(.page),
html:has(.page) body {
	scrollbar-width: none;
	-ms-overflow-style: none;
}
html:has(.page)::-webkit-scrollbar,
html:has(.page) body::-webkit-scrollbar {
	display: none;
	width: 0;
	height: 0;
}

/* -----------------------------------------------------------------------------
   26.3 The floating header
   -------------------------------------------------------------------------- */

/* `height: 0` takes the bar out of the flow so the hero starts at the top of
   the page and the glass bar floats over it.

   `position: relative` and NOT `sticky` (Dylan, Oct 27, 2026 — docs/web-app.md,
   "The public website's header does not stick"). The redesign draws it sticky
   and it was built that way; having lived with it in a browser he decided
   against it, and the later decision wins. `relative` with `top: 12px` paints
   the bar 12px down from the top of the page — the same place sticky put it at
   rest — without affecting any layout box and without pinning it to the
   viewport, so the header now scrolls away with everything else. The footer
   carries the full navigation, Get in touch and a back-to-top control, which
   is what makes losing the header on the way down acceptable.

   `top` on a `relative` box is a paint offset, not a sticky threshold: there
   is no clamping, so `.page`'s `overflow-x: clip` no longer has anything to
   do with where this lands. */
.hdr-dock {
	position: relative;
	top: 12px;
	z-index: 50;
	height: 0;
}

.hdr {
	position: relative;
}

.hdr__bar {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	height: 64px;
	padding: 0 10px 0 18px;
	border-radius: 20px;
	background: rgba(2, 17, 58, 0.62);
	border: 1px solid rgba(230, 236, 248, 0.14);
	box-shadow: 0 14px 34px rgba(2, 17, 58, 0.3), 0 3px 8px rgba(2, 17, 58, 0.16),
		inset 0 1px 0 rgba(255, 255, 255, 0.1);
	-webkit-backdrop-filter: blur(20px) saturate(1.6);
	backdrop-filter: blur(20px) saturate(1.6);
}

.hdr__logo {
	display: inline-flex;
}

/* The header logo's optical nudge. The figure is NOT inherited from the old
   header: that one rendered the lockup at 34px and this one renders it at
   30px. The correction is a SHARE of the rendered height, so the percentage
   is the same while the pixels it buys are not — re-derived and re-measured
   against this header in
   docs/evidence/2026-10-27-step0.5-R1-shell-and-home.md.

   What it corrects: the asset is a 900x176 lockup whose WORDMARK ink spans
   source rows 88..169, so the wordmark's centre sits at source y=129 against
   a canvas centre of y=88 — 41 source rows low. 41/176 = 23.295% of the
   height, which is why the same percentage is right at any rendered size.

   A `transform` because it must move no layout box. If the asset is ever
   recropped, DELETE this declaration rather than adjusting it: on a correctly
   centred asset the same shift would push the logo high by the same amount.
   src/html/header-logo-nudge.test.ts pins the exact file it was measured
   from. The footer logo is deliberately not nudged. */
.hdr__logo img {
	height: 30px;
	width: auto;
	transform: translateY(-23.295%);
}

.hdr__nav {
	display: flex;
	align-items: center;
	gap: 6px;
	font-size: 15px;
	font-weight: 700;
}

.hdr__link {
	display: inline-flex;
	align-items: center;
	min-height: 44px;
	padding: 0 12px;
	border-radius: 12px;
	color: #d5def0;
	text-decoration: none;
}
.hdr__link:hover,
.hdr__link:focus-visible {
	color: #ffffff;
	background: rgba(255, 255, 255, 0.08);
}

/* The page you are on, plated. Added with task R3a, because How it works is
   the first converted page that APPEARS in this nav — Home is not in its own
   navigation, so until now there was nothing for this rule to style.

   `:not(.btn)` is required of every `aria-current` selector in this sheet by
   src/html/header-current-page.test.ts, and `:not(.hdr__cta)` is the one that
   actually does the work here: the Get in touch link carries `.hdr__link`
   too, and on the Contact page it will point at itself and so carry
   `aria-current="page"` as well. design/Contact.dc.html draws that button
   exactly like every other page's — blue fill, white text, no current-page
   treatment — and a four-selector rule would otherwise outrank the
   single-class `.hdr__cta` and plate it grey. That is the same defect this
   site already shipped once as white-on-white (contrast ratio 1.000). */
.page .hdr__link:not(.btn):not(.hdr__cta)[aria-current="page"] {
	background: rgba(255, 255, 255, 0.14);
	color: #ffffff;
}

.hdr__cta {
	margin-left: 6px;
	padding: 0 18px;
	border-radius: 14px;
	background: var(--blue);
	color: #ffffff;
	font-weight: 800;
	box-shadow: 0 10px 24px rgba(4, 97, 248, 0.38), 0 2px 6px rgba(2, 17, 58, 0.18);
}
.hdr__cta:hover,
.hdr__cta:focus-visible {
	background: var(--blue);
	color: #ffffff;
	box-shadow: 0 14px 30px rgba(4, 97, 248, 0.45), 0 2px 6px rgba(2, 17, 58, 0.18);
}

.hdr__burger {
	display: none;
	width: 48px;
	height: 48px;
	align-items: center;
	justify-content: center;
	border-radius: 14px;
	border: 1px solid rgba(230, 236, 248, 0.22);
	background: rgba(255, 255, 255, 0.08);
	color: #ffffff;
	padding: 0;
}

/* The designs show bars while the menu is shut and a cross while it is open.
   Both icons ship and the swap is driven off `aria-expanded`, which
   scripts/site.js already maintains — so the behaviour needs no script change
   and the icon cannot fall out of step with the state it depicts. */
.hdr__icon--close {
	display: none;
}
.hdr__burger[aria-expanded="true"] .hdr__icon--open {
	display: none;
}
.hdr__burger[aria-expanded="true"] .hdr__icon--close {
	display: block;
}

/* -----------------------------------------------------------------------------
   26.4 The small-screen menu
   -------------------------------------------------------------------------- */
.page .mobile-menu {
	position: absolute;
	top: 74px;
	left: 18px;
	right: 18px;
	padding: 8px;
	border-radius: 20px;
	background: #ffffff;
	color: var(--navy);
	box-shadow: 0 24px 60px rgba(2, 17, 58, 0.35);
	display: flex;
	flex-direction: column;
	z-index: 50;
}
.page .mobile-menu[hidden] {
	display: none;
}

/* `a:not(.btn)` rather than `a`, and it is load-bearing rather than tidy: a
   class plus a type selector outranks the single-class `.btn--primary`, which
   is how the old shell's copy of this rule painted the blue "Get in touch"
   pill out on all five pages. src/html/header-current-page.test.ts pins the
   `:not(.btn)` so it cannot come back. */
.page .mobile-menu a:not(.btn) {
	display: flex;
	align-items: center;
	justify-content: space-between;
	min-height: 52px;
	padding: 0 14px;
	border-radius: 12px;
	color: var(--navy);
	text-decoration: none;
	font-size: 18px;
	font-weight: 800;
}
.page .mobile-menu a:not(.btn)[aria-current="page"] {
	background: #e3ecff;
	color: var(--navy);
}

.mobile-menu__chevron {
	color: #8e9cb8;
	flex-shrink: 0;
}

/* The menu's own entrance, which is the designs' `hr-swap`. It is declared
   here rather than left to cascade because the shell this replaces animates
   its menu with a different keyframe (`drop`, section 9) and that rule still
   matches this element — measured in the browser as a stray
   `transform: translateY(-8px)` on the open menu. Naming the animation here
   replaces it outright rather than fighting it.

   Caught because the measurement was taken in a pane with no rendering
   lifecycle, where an animation sits frozen on its first frame and so shows
   up in a rect. In a normal browser it would have completed in 0.45s and left
   nothing to see. */
.page .mobile-menu {
	animation: hrSwap 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes hrSwap {
	from {
		opacity: 0;
		transform: translateY(10px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

.page .mobile-menu .btn {
	margin-top: 6px;
	min-height: 52px;
	border-radius: 14px;
	font-size: 17px;
	font-weight: 800;
}

/* -----------------------------------------------------------------------------
   26.5 Shared pieces: headline blocks, cards, calls to action, pills
   -------------------------------------------------------------------------- */
.headline {
	display: flex;
	flex-direction: column;
	gap: 12px;
	max-width: 720px;
}

.eyebrow {
	font-size: 14px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--accent);
}

/* "Built in public." is two words, and balancing a two-word heading can leave
   one of them alone on a second line. The designs draw this one unbalanced. */
.t-h2--plain {
	text-wrap: initial;
}

.card {
	padding: 24px;
	border-radius: 22px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift);
	display: flex;
	flex-direction: column;
	gap: 14px;
}

.card__title {
	margin: 0;
	font-size: 21px;
	line-height: 1.25;
	font-weight: 800;
}
.card__title--buyer {
	font-size: 20px;
}

/* The designs' call-to-action shape: taller and rounder than the shell this
   replaces, with the same fill. Layered on `.btn` rather than replacing it,
   so `.btn--primary` keeps carrying the colour and nothing has two owners. */
.cta {
	gap: 8px;
	min-height: 56px;
	padding: 0 24px;
	border-radius: 16px;
	border-width: 0;
	font-size: 17px;
}
.btn--primary.cta {
	box-shadow: 0 10px 24px rgba(4, 97, 248, 0.38), 0 2px 6px rgba(2, 17, 58, 0.18);
}
.btn--primary.cta:hover {
	box-shadow: 0 14px 30px rgba(4, 97, 248, 0.45), 0 2px 6px rgba(2, 17, 58, 0.18);
}

/* The outline call-to-action on navy: the hero's "See how it works" and the
   founder band's "Follow on LinkedIn". */
.cta--glass {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	background: rgba(255, 255, 255, 0.08);
	border: 1px solid rgba(230, 236, 248, 0.28);
	color: #ffffff;
	-webkit-backdrop-filter: blur(12px);
	backdrop-filter: blur(12px);
}
.cta--glass:hover {
	color: #ffffff;
}

.page .cta-row {
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
	padding-top: 6px;
	align-items: center;
}
.page .cta-row--note {
	gap: 12px 20px;
}
/* The hero's row is the one the designs draw with no alignment, so its
   buttons stretch to the tallest: the outlined "See how it works" is 58px
   (56px plus its border, section 3), and "Get in touch" beside it is drawn
   58px too, not the 56px it would be centred. */
.page .cta-row--hero {
	align-items: stretch;
}

/* "Now in the design phase", in the hero and again in the footer. */
.eyebrow-pill {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	align-self: flex-start;
	display: inline-flex;
	align-items: center;
	gap: 8px;
	min-height: 34px;
	padding: 0 14px;
	border-radius: 17px;
	background: rgba(4, 165, 248, 0.14);
	border: 1px solid rgba(4, 165, 248, 0.35);
	color: #bfe6ff;
	font-size: 14px;
	font-weight: 800;
}
.eyebrow-pill__dot {
	width: 8px;
	height: 8px;
	border-radius: 4px;
	background: var(--sky);
}
.eyebrow-pill--sm {
	min-height: 32px;
	padding: 0 12px;
	border-radius: 16px;
	font-size: 13px;
}
.eyebrow-pill--sm .eyebrow-pill__dot {
	width: 7px;
	height: 7px;
}

/* -----------------------------------------------------------------------------
   26.6 The hero
   -----------------------------------------------------------------------------
   `.page-hero` and not `.hero`. This section clips its own contents, which
   the designs require — the approach-light diagram is drawn 120px below the
   section's bottom edge and the "Spoken alert" pill 96px to the left of its
   column — and `.hero` is forbidden from clipping while five pages still have
   their burger menu inside it (src/html/hero-clip.test.ts, and the untappable
   "Get in touch" that rule came from).
   -------------------------------------------------------------------------- */
.page-hero {
	position: relative;
	background: var(--navy);
	color: var(--on-navy);
	overflow: hidden;

	/* THE HERO'S ENTRANCE TIMING LIVES HERE, in three values, because Dylan
	   retunes it by feel and every delay below is derived from these. The
	   lights and the phone are both inside this section, so this is the
	   nearest element both can inherit from.

	   `--hr-light-step` is the gap between one light coming up and the next:
	   the run has to be slow enough to follow (Dylan, Oct 27, 2026: the
	   lights should "light up ... in progression" and the phone appear after
	   them, and "don't do it dead slow"). `--hr-light-lead` is when the first
	   one comes up, `--hr-light-dur` how long each takes to fade in. Change a
	   number here and the whole sequence — twelve light steps, the phone, the
	   spoken-alert pill, the level bars — moves with it. */
	/* THIRD PASS, Dylan, Oct 27, 2026: "Actually, I just want it to feel
	   snappy." Offered 10%, 25% or 40% faster, he asked for snappy rather than
	   a number, and 40% is how that was read — so all three scaled by 0.6 from
	   the second pass (0.16s / 0.24s / 0.4s) and rounded to the nearest 0.01s:
	   0.096 -> 0.1, 0.144 -> 0.14, 0.24 -> 0.24. The rounding rule is written
	   down so the next pass is arithmetic rather than taste, and a millisecond
	   is not perceptible anyway.

	   Three passes in one day have changed only these numbers. The SHAPE is
	   what docs/ui-rules.md fixes — "a sequence that guides the eye finishes
	   before the thing it points at arrives" — and it has not moved once.

	   The phone's own fly-in is deliberately NOT scaled: see --hr-phone-dur
	   below, which now explains what to do if he asks a fourth time. */
	--hr-light-lead: 0.1s;
	--hr-light-step: 0.14s;
	--hr-light-dur: 0.24s;
	/* The moment the last light has finished. 12 steps after the lead, plus
	   one light's own fade. Nothing in the hero's entrance may land before
	   this except the copy column. */
	--hr-lights-end: calc(
		var(--hr-light-lead) + 12 * var(--hr-light-step) + var(--hr-light-dur)
	);
	/* How long the phone takes to arrive once the run is over.
	   DELIBERATELY NOT SCALED with the three above, through all three passes.
	   Dylan specified the one-second fly-in separately and approved it on
	   screen; every "too slow" since has been about the run that PRECEDES the
	   phone, not about the arrival. The spoken-alert pill and the level bars
	   are derived from this plus --hr-lights-end, so they follow either change
	   by themselves.

	   IF HE ASKS FOR SNAPPIER A FOURTH TIME, SCALE THIS AND NOT THE LIGHTS.
	   The lights have been cut from 0.3s a step to 0.14s and the run now ends
	   at 2.02s, while this second is fixed — so the phone's fly-in is about a
	   third of the time from the first light to the phone landing (1s of
	   3.02s). Taking another 40% off the lights would buy ~0.8s and leave the
	   phone as the slowest thing on screen, which is the opposite of what
	   "snappy" asks for. Shortening this to 0.7s buys 0.3s on its own and
	   changes what he is actually looking at. Dylan's call either way, but
	   that is the honest next move. */
	--hr-phone-dur: 1s;
}

.page-hero__glows {
	position: absolute;
	inset: 0;
	overflow: hidden;
}

.page-hero__blob--a {
	width: 520px;
	height: 520px;
	right: -120px;
	top: -80px;
	background: rgba(4, 97, 248, 0.55);
}
.page-hero__blob--b {
	width: 420px;
	height: 420px;
	left: -140px;
	bottom: -120px;
	background: rgba(4, 165, 248, 0.3);
	animation-duration: 20s;
}

.page-hero__inner {
	position: relative;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 48px 56px;
	padding-top: 140px;
	padding-bottom: 120px;
}

.page-hero__copy {
	flex: 1 1 460px;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 22px;
}

.page-hero__title {
	color: #ffffff;
}

.page-hero__lead {
	margin: 0;
	max-width: 540px;
	font-size: clamp(17px, 2vw, 20px);
	line-height: 1.55;
	font-weight: 400;
	color: var(--on-navy-muted);
}

.page-hero__aside {
	flex: 0 0 auto;
	margin: 0 auto;
	position: relative;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 14px;
}

/* The taxiway running away from the crossing. Masked so it fades up out of
   nothing rather than starting on a hard edge. */
.page-hero__route {
	position: absolute;
	left: 50%;
	bottom: -120px;
	width: 760px;
	height: 560px;
	transform: translateX(-50%);
	-webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 45%);
	mask-image: linear-gradient(to bottom, transparent 0%, #000 45%);
}

.page-hero__halo {
	position: absolute;
	left: 50%;
	top: 46%;
	width: 460px;
	height: 460px;
	margin: -230px 0 0 -230px;
	border-radius: 50%;
	background: radial-gradient(circle, rgba(4, 165, 248, 0.32), rgba(4, 165, 248, 0) 65%);
}

.page-hero__tag {
	position: relative;
	font-size: 13px;
	font-weight: 700;
	color: var(--on-navy-muted);
}

/* The phone in the hero. Its own box and screen classes rather than the
   `.phone-box` / `.phone-screen` the Screens page uses: this one is a
   different size, and the Screens page is not this task's to move. */
.hero-phone {
	position: relative;
	padding: 10px;
	border-radius: 48px;
	background: #0b1633;
	border: 1px solid rgba(230, 236, 248, 0.1);
	box-shadow: 0 40px 80px rgba(0, 0, 0, 0.55), 0 8px 20px rgba(0, 0, 0, 0.35);
}

.hero-phone__box {
	width: 281px;
	height: 608px;
	border-radius: 38px;
	overflow: hidden;
	/* The designs' own declaration: the picture is a picture, not a control.
	   `inert` in the markup does the keyboard half of the same job. */
	pointer-events: none;
}

.hero-phone__screen {
	width: 390px;
	height: 844px;
	transform: scale(0.72);
	transform-origin: top left;
}

/* "Spoken alert — American on taxiway Golf, about 50 seconds." */
.say-pill {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	position: absolute;
	left: -96px;
	bottom: 76px;
	max-width: 250px;
	display: flex;
	flex-direction: column;
	gap: 6px;
	padding: 12px 14px;
	border-radius: 18px;
	background: rgba(255, 255, 255, 0.96);
	color: var(--navy);
	box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35), 0 3px 8px rgba(0, 0, 0, 0.2);
}
.say-pill__label {
	display: flex;
	align-items: center;
	gap: 8px;
	font-size: 12px;
	font-weight: 800;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: #3d4b68;
}
.say-pill__bars {
	display: inline-flex;
	align-items: center;
	gap: 2px;
	height: 16px;
	color: var(--blue);
}
.say-pill__bar--1 {
	height: 8px;
}
.say-pill__bar--2 {
	height: 14px;
}
.say-pill__bar--3 {
	height: 10px;
}
.say-pill__bar--4 {
	height: 16px;
}
.say-pill__bar--5 {
	height: 7px;
}
.say-pill__quote {
	font-size: 16px;
	line-height: 1.4;
	font-weight: 800;
}

/* -----------------------------------------------------------------------------
   26.7 The principles bar, lifted over the hero's bottom edge
   -------------------------------------------------------------------------- */
.page .strip {
	position: relative;
	z-index: 3;
	margin-top: -44px;
	padding-top: 0;
	padding-bottom: 0;
	background: none;
}

.page .strip__list {
	margin: 0;
	padding: 10px;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
	gap: 8px;
	border-radius: 22px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift-big);
}

.page .strip__item {
	display: flex;
	align-items: center;
	gap: 12px;
	padding: 10px 12px;
	font-size: 15px;
	line-height: 1.3;
	font-weight: 800;
	color: var(--ink);
}

.strip__icon {
	flex-shrink: 0;
	width: 40px;
	height: 40px;
	border-radius: 12px;
	background: var(--chip);
	display: flex;
	align-items: center;
	justify-content: center;
	color: var(--accent);
}
.page .strip__item svg {
	width: 22px;
	height: 22px;
	color: inherit;
}

/* -----------------------------------------------------------------------------
   26.8 How it works: three steps on a dashed route
   -------------------------------------------------------------------------- */
.page .sec--how,
.page .sec--buyer,
.page .sec--log {
	display: flex;
	flex-direction: column;
}
.page .sec--how {
	gap: 36px;
}
.page .sec--buyer {
	gap: 32px;
}
.page .sec--log {
	gap: 28px;
}

/* The wrapper is `position: relative` and nothing else, which is all
   design/Main.dc.html gives it: it exists to be the positioning parent for
   the dashed route drawn behind the three cards. It used to also undo a grid
   and a gap inherited from the pre-redesign `.steps`; that rule is gone
   (section 12), so there is nothing left to undo. */
.steps {
	position: relative;
}

.steps__route {
	position: absolute;
	left: 0;
	right: 0;
	top: 50px;
	width: 100%;
	height: 40px;
	stroke: var(--route);
}

.steps__list {
	position: relative;
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
	gap: 20px;
}

.steps__top {
	display: flex;
	align-items: center;
	justify-content: space-between;
}

.steps__tile {
	width: 56px;
	height: 56px;
	border-radius: 18px;
	background: var(--tile);
	color: #ffffff;
	display: flex;
	align-items: center;
	justify-content: center;
	box-shadow: 0 8px 18px rgba(2, 17, 58, 0.25);
}

.steps__n {
	font-size: 14px;
	font-weight: 800;
	color: var(--muted);
}

/* The step card is `.card` and nothing more. design/Main.dc.html draws it as
   24px padding, a 22px radius, the surface fill, a 1px line border, the lift
   shadow and a 14px flex column — which is exactly what `.card` is, so
   `.steps__item` carries no box of its own.

   There used to be a reset block here, overriding margin, padding, border and
   the icon's size against the pre-redesign rules. Those rules are deleted
   (section 12) and the reset went with them: a reset that opposes nothing is
   worse than no reset, because it reads as a live constraint.

   NOTHING styles the icon. It is `stroke="currentColor"` inside
   `.steps__tile`, which sets `color: #ffffff`, so the glyph is white because
   it inherits — the design's own mechanism. Do not add a `stroke` declaration
   for any selector that reaches this icon: a CSS declaration outranks a
   presentation attribute, so one here would silently take the colour away
   from the tile again. */

/* -----------------------------------------------------------------------------
   26.9 For companies
   -------------------------------------------------------------------------- */
.page .buyer {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
	gap: 20px;
}

.page .buyer__col {
	padding: 24px;
	border: 1px solid var(--line);
	background: var(--surface);
}

.buyer__icon-wrap {
	align-self: flex-start;
}

.buyer__icon {
	width: 48px;
	height: 48px;
	border-radius: 15px;
	background: var(--chip);
	color: var(--accent);
	display: flex;
	align-items: center;
	justify-content: center;
}

.page .buyer__points {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 10px;
}

.page .buyer__points li {
	display: flex;
	gap: 12px;
	font-size: 16px;
	line-height: 1.55;
	color: var(--muted);
}

.page .buyer__bullet {
	flex: 0 0 8px;
	height: 8px;
	margin-top: 9px;
	border-radius: 4px;
	background: var(--accent);
}

/* -----------------------------------------------------------------------------
   26.10 Built in public
   -----------------------------------------------------------------------------
   The list items are written by src/html/timeline.ts at request time, and the
   Progress page's log calls the same function — so every rule here is scoped
   to `.page` and the Progress page keeps the styling it has today.
   -------------------------------------------------------------------------- */
.log {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 14px;
	max-width: 820px;
}

.page .tl-item {
	display: grid;
	grid-template-columns: 28px minmax(0, 1fr);
	gap: 0 14px;
}

.page .tl-rail {
	position: relative;
	display: flex;
	justify-content: center;
}

.page .tl-dot {
	position: relative;
	z-index: 1;
	margin-top: 24px;
	width: 16px;
	height: 16px;
	border-radius: 8px;
	box-sizing: border-box;
	background: var(--blue);
	border: 3px solid var(--blue);
	color: var(--blue);
}

/* "Up next" has not happened yet: a hollow dot with a muted ring. */
.page .tl-dot--pending {
	background: var(--bg);
	border-color: var(--muted);
	color: var(--muted);
}

.page .tl-line {
	position: absolute;
	left: 50%;
	margin-left: -1px;
	top: 44px;
	bottom: -38px;
	width: 2px;
	border-radius: 1px;
	background: var(--blue);
	transform-origin: top;
}
.page .tl-item:last-child .tl-line {
	display: none;
}
/* The line leaving the pending item is dashed, because what it connects to
   has not happened yet. */
.page .tl-item:has(.tl-dot--pending) .tl-line {
	background: repeating-linear-gradient(
		to bottom,
		var(--route) 0 6px,
		transparent 6px 12px
	);
}

.page .tl-body {
	display: flex;
	flex-direction: column;
	gap: 4px;
	min-width: 0;
	padding: 18px 20px;
	border-radius: 20px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift);
}
.page .tl-item:has(.tl-dot--pending) .tl-body {
	background: transparent;
	border: 2px dashed var(--route);
	box-shadow: none;
}

/* The designs put the "when · kind" line ABOVE the title. It is moved with
   `order` rather than in the markup because src/html/timeline.ts writes the
   same markup for the Progress page's log, which this task must not change;
   the Progress task moves it into the markup when that page converts. The
   mismatch between reading order and visual order is confined to three spans
   with nothing focusable in them. */
.page .tl-meta {
	order: -1;
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--accent);
}
.page .tl-meta--pending {
	color: var(--muted);
}

.page .tl-title {
	font-size: 19px;
	line-height: 1.3;
	font-weight: 800;
	color: var(--ink);
}

.page .tl-text {
	font-size: 16px;
	line-height: 1.55;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   26.11 The founder band
   -------------------------------------------------------------------------- */
.page .sec--founder {
	padding-bottom: 112px;
}

.page .founder {
	position: relative;
	overflow: hidden;
	border-radius: 28px;
	background: var(--navy);
	color: var(--on-navy);
	padding: clamp(28px, 5vw, 56px);
	box-shadow: 0 30px 60px rgba(2, 17, 58, 0.28), 0 6px 16px rgba(2, 17, 58, 0.18);
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 28px 40px;
}

.founder__blob {
	width: 380px;
	height: 380px;
	right: -100px;
	top: -140px;
	background: rgba(4, 97, 248, 0.5);
}

.page .founder__photo {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	position: relative;
	width: 120px;
	height: 120px;
	object-fit: cover;
	border-radius: 24px;
	border: 3px solid var(--sky);
	box-shadow: 0 12px 28px rgba(0, 0, 0, 0.4);
}

.founder__body {
	position: relative;
	flex: 1 1 380px;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 14px;
}

/* Its type is the shared scale's card step 3, on the element as
   `.t-h2 .t-h2--card-l` (docs/ui-rules.md). Only the colour is the band's. */
.page .founder__title {
	color: #ffffff;
}

.page .founder__text {
	margin: 0;
	max-width: 620px;
	font-size: 17px;
	line-height: 1.6;
	color: var(--on-navy-muted);
}

.page .founder__link {
	min-height: 44px;
	display: inline-flex;
	align-items: center;
	padding: 0 8px;
	font-size: 16px;
	font-weight: 800;
	color: var(--on-navy-link);
	text-decoration: none;
}

/* -----------------------------------------------------------------------------
   26.12 The footer
   -------------------------------------------------------------------------- */
.ftr {
	padding: 0 16px 16px 16px;
	background: none;
	border: 0;
}

.ftr__card {
	position: relative;
	overflow: hidden;
	max-width: 1408px;
	margin: 0 auto;
	border-radius: 32px;
	background: var(--navy);
	color: var(--on-navy-muted);
	box-shadow: 0 30px 60px rgba(2, 17, 58, 0.25), 0 6px 16px rgba(2, 17, 58, 0.16);
}

.ftr__blob {
	width: 420px;
	height: 420px;
	left: -160px;
	bottom: -220px;
	background: rgba(4, 97, 248, 0.45);
}

/* The row of green centreline lights along the top edge of the card. */
.ftr__lights {
	position: absolute;
	left: 50%;
	top: 0;
	transform: translateX(-50%);
	display: flex;
	gap: 18px;
	padding-top: 14px;
}
.ftr__light {
	width: 6px;
	height: 6px;
	border-radius: 3px;
	background: #3be07f;
	opacity: 0.75;
}

.ftr__cols {
	position: relative;
	padding-top: 72px;
	padding-bottom: 40px;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
	gap: 40px 48px;
}

.ftr__brand {
	display: flex;
	flex-direction: column;
	gap: 16px;
	grid-column: span 2;
	min-width: 0;
}

.ftr__logo {
	align-self: flex-start;
	height: 34px;
	width: auto;
}

.ftr__blurb {
	margin: 0;
	max-width: 420px;
	font-size: 16px;
	line-height: 1.55;
}

.ftr__col {
	display: flex;
	flex-direction: column;
	gap: 6px;
}

.ftr__col-head {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--on-navy-link);
}
/* The "Talk to me" column has no gap of its own in the designs — its links
   carry their own 44px height — so its heading spaces itself instead. */
.ftr__col:has(> .ftr__col-head--talk) {
	gap: 0;
}
.ftr__col-head--talk {
	margin-bottom: 6px;
}

.ftr__links {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
}

.ft-link {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	min-height: 44px;
	color: var(--on-navy-muted);
	text-decoration: none;
	font-size: 16px;
	font-weight: 700;
}
.ft-link:hover,
.ft-link:focus-visible {
	color: #ffffff;
}
/* The "Talk to me" column's links, which are the column's own children. The
   "Explore" links sit in a `ul` inside their column and keep the 16px and 8px
   above, as every design draws them; a descendant selector here reached them
   too and built them at 15px. src/html/drawn-sizes.test.ts holds both. */
.ftr__col > .ft-link {
	gap: 10px;
	font-size: 15px;
}
.ft-link--mail {
	overflow-wrap: anywhere;
}
.ftr__icon {
	flex-shrink: 0;
}

/* The arrow arrives on hover AND on keyboard focus, so it is never a
   pointer-only affordance. */
.ft-arrow {
	opacity: 0;
	transform: translateX(-6px);
}
.ft-link:hover .ft-arrow,
.ft-link:focus-visible .ft-arrow {
	opacity: 1;
	transform: none;
}

.ftr__base {
	position: relative;
	padding-bottom: 28px;
}

.ftr__rule {
	padding-top: 22px;
	border-top: 1px solid rgba(230, 236, 248, 0.14);
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 14px 24px;
}

.ftr__legal {
	margin: 0;
	flex: 1 1 420px;
	min-width: 0;
	font-size: 14px;
	line-height: 1.5;
	color: #a7b6d3;
}

.ftr__meta {
	display: flex;
	align-items: center;
	gap: 14px;
}

.ftr__copy {
	font-size: 14px;
	font-weight: 700;
	color: #a7b6d3;
}

.ftr__top {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 6px;
	min-height: 44px;
	padding: 0 14px;
	border-radius: 22px;
	background: rgba(255, 255, 255, 0.08);
	border: 1px solid rgba(230, 236, 248, 0.22);
	color: #ffffff;
	text-decoration: none;
	font-size: 14px;
	font-weight: 800;
}
.ftr__top:hover {
	color: #ffffff;
	background: rgba(255, 255, 255, 0.14);
}

/* The theme line sits on the navy card, so it takes the card's ink rather
   than the page's. */
.page .theme-pick {
	margin-top: 14px;
	color: #a7b6d3;
}
.page .theme-pick__option {
	color: #a7b6d3;
}
.page .theme-pick__option[aria-checked="true"] {
	color: #ffffff;
}
.page .theme-pick__option:hover,
.page .theme-pick__option:focus-visible {
	color: #ffffff;
}
.page .theme-pick__sep {
	color: rgba(230, 236, 248, 0.4);
}

/* -----------------------------------------------------------------------------
   26.13 Motion
   -----------------------------------------------------------------------------
   Every animation below is the designs'. Four of them repeat for ever —
   hrDrift, hrPing, hrBreath and hrRun — which docs/ui-rules.md would
   otherwise forbid; Dylan approved them by name in this rebuild's
   instruction, and src/html/motion.test.ts allows exactly those four and
   fails on a fifth.

   The whole block is cancelled by section 25's reduced-motion backstop,
   which is `animation: none !important` on every element and pseudo-element.
   That is the gate docs/ui-rules.md allows no exceptions to.
   -------------------------------------------------------------------------- */

/* Press feedback, shared by the header links, the burger, the calls to action
   and the "Top" button. */
.hdr__link,
.hdr__burger,
.cta,
.ftr__top {
	transition: transform 0.14s ease-out, box-shadow 0.2s ease-out,
		background-color 0.2s ease-out;
}
.hdr__link:active,
.hdr__burger:active,
.cta:active,
.ftr__top:active {
	transform: scale(0.97);
}

.hr-card {
	transition: transform 0.2s ease-out, box-shadow 0.2s ease-out;
}
.hr-card:hover {
	transform: translateY(-3px);
}
/* The icon tile answers a hover anywhere on its card, which is how the
   designs tie the tile to the card rather than to the pointer's exact
   position. */
.hr-tile {
	transition: background-color 0.25s ease-out, color 0.25s ease-out,
		transform 0.3s cubic-bezier(0.3, 1.4, 0.5, 1);
}
.hr-card:hover .hr-tile {
	background: var(--blue);
	color: #ffffff;
	transform: rotate(-6deg) scale(1.06);
}

.ft-link,
.ft-arrow {
	transition: color 0.2s ease-out, opacity 0.2s ease-out, transform 0.2s ease-out;
}

/* The hero arrives: the copy column's four children rise in turn, then the
   phone, then the spoken-alert pill. One shot each. */
.hr-in > * {
	animation: hrUp 0.7s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.hr-in > *:nth-child(2) {
	animation-delay: 0.08s;
}
.hr-in > *:nth-child(3) {
	animation-delay: 0.16s;
}
.hr-in > *:nth-child(4) {
	animation-delay: 0.24s;
}
@keyframes hrUp {
	from {
		opacity: 0;
		transform: translateY(16px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

/* The phone arrives AFTER the approach lights have finished their run, not
   during it (Dylan, Oct 27, 2026). It used to start at 0.2s and finish at
   1.1s while the last light was still coming up at 1.57s, so the two read as
   one busy moment instead of a sequence. Both numbers are derived from the
   tokens on `.page-hero`; neither is written here. */
.hr-phone {
	animation: hrPhone var(--hr-phone-dur) cubic-bezier(0.2, 0.8, 0.2, 1)
		var(--hr-lights-end) both;
}
@keyframes hrPhone {
	from {
		opacity: 0;
		transform: translateY(28px) scale(0.97);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

/* The spoken-alert pill points at the phone, so it cannot arrive before it.
   Half a second after the phone has settled — the same half-second gap the
   design drew (phone finished 1.1s, pill 1.6s), now measured from wherever
   the phone finishes. */
.hr-say {
	animation: hrSay 0.6s cubic-bezier(0.3, 1.2, 0.5, 1)
		calc(var(--hr-lights-end) + var(--hr-phone-dur) + 0.5s) both;
}
@keyframes hrSay {
	from {
		opacity: 0;
		transform: translateX(-14px) scale(0.96);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

/* The five level bars beside "Spoken alert". Five iterations, not infinite:
   they say the alert is being spoken now, and then they stop, the way the
   alert does. */
.hr-bar {
	display: inline-block;
	width: 4px;
	border-radius: 2px;
	background: currentColor;
	transform-origin: center;
	/* 0.3s after the pill starts, as the design drew it (1.6s → 1.9s), now
	   derived so the bars cannot start speaking before the pill exists. */
	animation: hrBar 0.8s ease-in-out
		calc(var(--hr-lights-end) + var(--hr-phone-dur) + 0.8s) 5 both;
}
.hr-bar:nth-child(2) {
	animation-duration: 0.6s;
}
.hr-bar:nth-child(3) {
	animation-duration: 0.9s;
}
.hr-bar:nth-child(4) {
	animation-duration: 0.7s;
}
.hr-bar:nth-child(5) {
	animation-duration: 0.85s;
}
@keyframes hrBar {
	0%,
	100% {
		transform: scaleY(0.35);
	}
	50% {
		transform: scaleY(1);
	}
}

/* The shine that crosses a call-to-action once, shortly after the page
   settles. One pass, and then it is gone. */
.cta--shine {
	position: relative;
	overflow: hidden;
}
.cta--shine::after {
	content: "";
	position: absolute;
	top: 0;
	bottom: 0;
	width: 40%;
	left: -60%;
	background: linear-gradient(
		100deg,
		transparent,
		rgba(255, 255, 255, 0.38),
		transparent
	);
	/* THE DESIGN'S OWN DECLARATION, VERBATIM. Every `design/*.dc.html` that
	   draws a call to action writes this rule as
	   `.hr-cta::after{…;animation:hrShine 1.1s ease-out 1.1s both}`, and
	   src/html/hero-shine.test.ts reads that value out of the design files
	   rather than repeating it here, so a retune of the drawing moves the
	   guard with it instead of leaving it behind.

	   IT IS NOT DERIVED FROM THE HERO, and that is a decision rather than an
	   omission. The delay was briefly computed as
	   `calc(var(--hr-lights-end) + var(--hr-phone-dur))` — both declared on
	   `.page-hero` — so the sweep would start as Home's phone mockup landed.
	   Dylan reversed it on Oct 28, 2026: "I just don't want it to shine on
	   the fucking phone. I want it to shine no matter what device it's on."

	   The shine belongs to the button, not to one page's entrance sequence.
	   about.html, how-it-works.html, progress.html and screens.html each
	   have a `.page-hero` and no `.hr-phone` at all, so a hero-derived delay
	   makes a button inside those heroes wait for an arrival that never
	   happens; and outside the hero the `var()`s do not resolve at all,
	   which makes the declaration invalid at computed-value time — every
	   longhand then takes its INITIAL value rather than any earlier
	   declaration, so `animation-name` computes to `none` and there is no
	   sweep at all. docs/ui-rules.md records the reversal, and
	   docs/evidence/2026-10-28-step0.5-shine-decoupled.md the measurements. */
	animation: hrShine 1.1s ease-out 1.1s both;
}
@keyframes hrShine {
	to {
		left: 130%;
	}
}

/* APPROVED LOOP 1 of 4 — hrDrift. The soft lights behind the hero, the
   founder card and the footer. They drift and breathe so the navy is not a
   flat slab; nothing depends on them and nothing is read off them. */
.hr-blob {
	position: absolute;
	border-radius: 50%;
	filter: blur(60px);
	opacity: 0.5;
	animation: hrDrift 16s ease-in-out infinite alternate;
}
@keyframes hrDrift {
	from {
		transform: translate(0, 0) scale(1);
	}
	to {
		transform: translate(40px, 30px) scale(1.12);
	}
}

/* The approach lights coming up one after another, once. */
.hr-light {
	opacity: 0;
	animation: hrLight var(--hr-light-dur) ease-out forwards;
}
/* A light starts invisible and is brought up BY the animation, so section
   25's `animation: none !important` backstop would leave all 34 of them at
   zero — a visitor who asked for less motion would lose the hero's diagram
   altogether rather than see it finished, which is the opposite of what
   docs/ui-rules.md promises. `0.85` is design/Main.dc.html's own
   reduced-motion value for these. It has to sit AFTER the rule above, not up
   in section 25's block: both selectors are a single class, so the later one
   wins and an override written earlier would silently lose. */
@media (prefers-reduced-motion: reduce) {
	.hr-light {
		opacity: 0.85;
	}
}
@keyframes hrLight {
	from {
		opacity: 0;
	}
	60% {
		opacity: 0.9;
	}
	to {
		opacity: 0.6;
	}
}
/* The twelve pairs of approach lights, nearest first, each one step behind
   the last. Written as `lead + n * step` rather than as twelve literals so
   the run can be retuned by changing one number on `.page-hero`: the
   literals they replaced (0.3s ... 1.07s, 0.07s apart) were a 0.84s run that
   finished while the phone was still arriving. */
.hr-l1 {
	animation-delay: calc(var(--hr-light-lead) + 1 * var(--hr-light-step));
}
.hr-l2 {
	animation-delay: calc(var(--hr-light-lead) + 2 * var(--hr-light-step));
}
.hr-l3 {
	animation-delay: calc(var(--hr-light-lead) + 3 * var(--hr-light-step));
}
.hr-l4 {
	animation-delay: calc(var(--hr-light-lead) + 4 * var(--hr-light-step));
}
.hr-l5 {
	animation-delay: calc(var(--hr-light-lead) + 5 * var(--hr-light-step));
}
.hr-l6 {
	animation-delay: calc(var(--hr-light-lead) + 6 * var(--hr-light-step));
}
.hr-l7 {
	animation-delay: calc(var(--hr-light-lead) + 7 * var(--hr-light-step));
}
.hr-l8 {
	animation-delay: calc(var(--hr-light-lead) + 8 * var(--hr-light-step));
}
.hr-l9 {
	animation-delay: calc(var(--hr-light-lead) + 9 * var(--hr-light-step));
}
.hr-l10 {
	animation-delay: calc(var(--hr-light-lead) + 10 * var(--hr-light-step));
}
.hr-l11 {
	animation-delay: calc(var(--hr-light-lead) + 11 * var(--hr-light-step));
}
.hr-l12 {
	animation-delay: calc(var(--hr-light-lead) + 12 * var(--hr-light-step));
}
/* The ten green lights along the near edge come up together, at the lead.
   They are the row the run starts from, not part of the run. */
.hr-lg {
	animation-delay: var(--hr-light-lead);
}

/* APPROVED LOOP 2 of 4 — hrRun. The dashes on the hero's taxiway centreline
   travel, which is what makes it read as a taxiway rather than as a dotted
   line. */
.hr-dash {
	animation: hrRun 3s linear infinite;
}
@keyframes hrRun {
	from {
		stroke-dashoffset: 0;
	}
	to {
		stroke-dashoffset: -52;
	}
}

/* APPROVED LOOPS 3 and 4 of 4 — hrPing and hrBreath. The "Up next" dot at the
   live edge of the timeline: a ring expanding outward, and the dot itself
   breathing. It marks the one item that has not happened yet. */
.page .tl-dot--pending::after {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: 16px;
	height: 16px;
	margin: -8px 0 0 -8px;
	border-radius: 50%;
	border: 2px solid currentColor;
	box-sizing: border-box;
	animation: hrPing 2.4s ease-out infinite;
}
.page .tl-dot--pending::before {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: 6px;
	height: 6px;
	margin: -3px 0 0 -3px;
	border-radius: 50%;
	background: currentColor;
	animation: hrBreath 2.4s ease-in-out infinite;
}
@keyframes hrPing {
	0% {
		transform: scale(1);
		opacity: 0.7;
	}
	80%,
	100% {
		transform: scale(2.3);
		opacity: 0;
	}
}
@keyframes hrBreath {
	0%,
	100% {
		opacity: 0.35;
		transform: scale(0.8);
	}
	50% {
		opacity: 1;
		transform: scale(1);
	}
}

@keyframes hrPop {
	from {
		opacity: 0;
		transform: scale(0.55) rotate(-8deg);
	}
	to {
		opacity: 1;
		transform: none;
	}
}
@keyframes hrSide {
	from {
		opacity: 0;
		transform: translateX(-12px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}
@keyframes hrDot {
	from {
		transform: scale(0.3);
	}
	70% {
		transform: scale(1.25);
	}
	to {
		transform: none;
	}
}
@keyframes hrGrow {
	from {
		transform: scaleY(0);
	}
	to {
		transform: none;
	}
}

/* Scroll-driven reveals, where the browser has them.
   -----------------------------------------------------------------------------
   `animation-timeline: view()` ties an animation's progress to how far the
   element has travelled through the scrollport instead of to a clock, and
   `animation-range` picks which part of that travel to use (MDN: with
   `view()`, 0% is where the subject first becomes visible). MDN records the
   feature as Limited availability — "not Baseline because it does not work in
   some of the most widely-used browsers" — which is exactly why it is inside
   `@supports` and why the finished state is the default outside it. A browser
   without it shows the content, in place, immediately; nothing here is the
   only way to see anything.

   This sits alongside, not instead of, the `.reveal` pass in
   scripts/site.js: that one fades each top-level section in once as it
   arrives, and these are the smaller movements inside a section. Both are
   cancelled wholesale by section 25 under reduced motion — and the
   `no-preference` query here is a second gate on the same promise, because
   `@supports` on its own would not carry one. */
@supports (animation-timeline: view()) {
	@media (prefers-reduced-motion: no-preference) {
		/* `hr-rise` and not the designs' `hr-reveal`. `reveal` is the name of
		   the hidden state scripts/site.js applies at runtime, and
		   src/html/motion.test.ts forbids any page from SHIPPING that word in
		   a class attribute — a section that arrived with `class="reveal"` in
		   the file would be invisible to every visitor whose JavaScript is
		   blocked, with nothing able to undo it. The test matches the word,
		   so `hr-reveal` tripped it. Renaming this one costs nothing (a class
		   name is not part of what was approved; the rendering is) and leaves
		   that guard at full strength. */
		.hr-rise {
			animation: hrUp linear both;
			animation-timeline: view();
			animation-range: entry 0% entry 35%;
		}
		.hr-pop {
			animation: hrPop linear both;
			animation-timeline: view();
			animation-range: entry 15% entry 55%;
		}
		.hr-pt {
			animation: hrSide linear both;
			animation-timeline: view();
			animation-range: entry 20% entry 70%;
		}
		.page .tl-dot {
			animation: hrDot linear both;
			animation-timeline: view();
			animation-range: entry 30% entry 70%;
		}
		.page .tl-line {
			animation: hrGrow linear both;
			animation-timeline: view();
			animation-range: entry 40% cover 55%;
		}
	}
}

/* -----------------------------------------------------------------------------
   26.14 Responsive — the designs' breakpoints at 900, 760, 620 and 560
   -------------------------------------------------------------------------- */
@media (max-width: 900px) {
	.page .sec {
		padding-top: 84px;
	}
	.hdr__nav {
		display: none;
	}
	.hdr__burger {
		display: inline-flex;
	}
	/* The taxiway diagrams go once the columns stack: there is no room beside
	   the phone, and behind the step cards the dashes would run under the
	   text. */
	.route {
		display: none;
	}
}

/* The "Spoken alert" pill stops hanging off the side of the phone and tucks
   under it, where there is no longer a margin for it to sit in. */
@media (max-width: 760px) {
	.say-pill {
		position: relative;
		left: auto;
		bottom: auto;
		margin-top: -34px;
	}
}

/* Two columns of footer brand no longer fit. */
@media (max-width: 620px) {
	.ftr__brand {
		grid-column: auto;
	}
}

@media (max-width: 560px) {
	.page .sec {
		padding-top: 64px;
	}
	.page .sec--founder {
		padding-bottom: 64px;
	}
	.hero-phone__box {
		width: 236px;
		height: 511px;
	}
	.hero-phone__screen {
		transform: scale(0.605);
	}
}

/* =============================================================================
   27. "Try the app" — the Home page's interactive section
   =============================================================================
   design/Main.dc.html, the block commented "try the app: interactive
   emulator". It is the Home page's, so it could have been another 26.x; it is
   its own section because it is the first thing on this site with behaviour
   rather than appearance, and because R2b and R2c extend it.

   WHAT IS HERE AND WHAT IS IN mockups.css. Everything on this page's side of
   the glass — the band, the card, the role control, the checklist, the
   spoken-alerts switch — is here and uses the page's own tokens. The depicted
   phone screen inside the frame is a different design file with a different
   palette, and it lives in mockups.css with the other mockups.

   NO RUNTIME STYLE ATTRIBUTES, ANYWHERE. The design drives the sliding
   indicator, the switch knob and the aircraft marker with per-render inline
   `style` values. None of that is reproduced: every moving part here is a
   class or a data attribute the script sets, or a keyframe animation that
   runs on its own. That is partly apps/site/README.md's "no style attributes
   anywhere, mockups included", and partly so that nothing on this page
   depends on how a browser treats CSSOM writes under `style-src-attr` — which
   docs/security.md §7 says is coming out before the first form ships.
   -------------------------------------------------------------------------- */

/* The band runs full width behind the section; the wrap inside it holds the
   content to the page measure. The designs' own nesting. */
.try-band {
	background: var(--band);
}

.try {
	padding-top: 80px;
	padding-bottom: 80px;
	display: flex;
	flex-direction: column;
	gap: 28px;
}

/* The designs give this paragraph 580px where the shared `.t-sub` is 560. */
.t-sub--try {
	max-width: 580px;
}

.try__card {
	box-sizing: border-box;
	padding: clamp(20px, 4vw, 44px);
	border-radius: 28px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift-big);
	display: flex;
	flex-direction: column;
	gap: 28px;
}

/* -----------------------------------------------------------------------------
   27.1 The role control
   -----------------------------------------------------------------------------
   A segmented control drawn as three buttons over one sliding tile. The tile
   is a sibling rather than a background on the checked button, because it
   travels.

   The three positions are driven by `data-active` on the group, which is the
   only thing the script writes here. `calc(N * (100% + 4px))` is the designs'
   own expression: `100%` is the indicator's own width, one third of the track
   less the gaps, and 4px is the gap.
   -------------------------------------------------------------------------- */
.roles {
	position: relative;
	box-sizing: border-box;
	width: min(100%, 480px);
	margin: 0 auto;
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: 4px;
	padding: 4px;
	border-radius: 16px;
	background: var(--chip);
}

.roles__ind {
	position: absolute;
	top: 4px;
	left: 4px;
	width: calc((100% - 16px) / 3);
	height: 52px;
	border-radius: 12px;
	background: var(--surface);
	box-shadow: 0 2px 6px rgba(2, 17, 58, 0.16);
}
.roles[data-active="flagger"] .roles__ind {
	transform: translateX(0);
}
.roles[data-active="escort"] .roles__ind {
	transform: translateX(calc(100% + 4px));
}
.roles[data-active="crew"] .roles__ind {
	transform: translateX(calc(2 * (100% + 4px)));
}

/* 52px, which is already past the 44px floor in docs/ui-rules.md. */
.roles__opt {
	position: relative;
	min-height: 52px;
	padding: 0 6px;
	border-radius: 12px;
	border: 0;
	background: transparent;
	color: var(--ink);
	font-size: 16px;
	font-weight: 800;
}

/* -----------------------------------------------------------------------------
   27.2 The two columns: the phone, and the panel beside it
   -------------------------------------------------------------------------- */
.try__cols {
	display: flex;
	flex-wrap: wrap;
	align-items: flex-start;
	justify-content: center;
	gap: 32px 56px;
}

.try__phone {
	flex: 0 0 auto;
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 12px;
}

/* The phone body. #0B1633 is drawn flat in both themes in the design — it is
   the object's own colour, not a themed surface, the same way the hero
   phone's is. */
.emu-frame {
	position: relative;
	padding: 10px;
	border-radius: 52px;
	background: #0b1633;
	box-shadow: 0 30px 60px var(--shadow), 0 6px 16px var(--shadow);
}

/* The window, and the full-size screen scaled down inside it. 330/390 =
   0.846, which is the designs' own number. `overflow: hidden` is the clip
   that makes the scaled screen fit the window, and it is also why no
   scrollbar can appear in here. */
.emu-box {
	width: 330px;
	height: 714px;
	border-radius: 42px;
	overflow: hidden;
}
.emu-scale {
	width: 390px;
	height: 844px;
	transform: scale(0.846);
	transform-origin: top left;
}

.try__tag {
	font-size: 13px;
	font-weight: 700;
	color: var(--muted);
}

.try__panel {
	flex: 1 1 340px;
	min-width: 0;
	max-width: 520px;
	display: flex;
	flex-direction: column;
	gap: 22px;
}

.try__cap {
	display: flex;
	flex-direction: column;
	gap: 8px;
}
/* `hidden` has to beat the `display` above it, the same way .tabpanel[hidden]
   and .mobile-menu[hidden] do higher up this sheet. Without this the two
   inactive captions and the two inactive lists would all render at once. */
.try__cap[hidden],
.try__list[hidden] {
	display: none;
}
.try__cap-title {
	margin: 0;
	font-size: clamp(24px, 2.8vw, 30px);
	line-height: 1.15;
	font-weight: 800;
}
.try__cap-sub {
	margin: 0;
	font-size: 16px;
	line-height: 1.55;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   27.3 The "Try this" checklist
   -------------------------------------------------------------------------- */
.try__list-block {
	display: flex;
	flex-direction: column;
	gap: 10px;
}

.try__list-head {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--muted);
}

/* The count is the one part of that line that is not a label, so the design
   takes the label's letter-spacing and upper case off it. */
.try__count {
	letter-spacing: 0;
	text-transform: none;
	font-weight: 700;
}

.try__list {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.try__item {
	position: relative;
	display: flex;
	gap: 12px;
	align-items: flex-start;
	padding: 14px 16px;
	border-radius: 16px;
	background: var(--surface);
	border: 1.5px solid var(--line);
}
.try__item--done {
	background: var(--chip);
	border-color: transparent;
}

.try__box {
	flex-shrink: 0;
	width: 26px;
	height: 26px;
	border-radius: 13px;
	box-sizing: border-box;
	border: 2px solid var(--route);
	background: transparent;
	color: #ffffff;
	display: flex;
	align-items: center;
	justify-content: center;
}
.try__item--done .try__box {
	border-color: var(--blue);
	background: var(--blue);
}

/* The tick is in the markup from the start and revealed when the item is
   done, rather than inserted. `visibility` and not `display`, so the box
   never changes size as it fills.

   NOT `opacity: 0` on the base rule: that is the shape of the defect R1b
   found in the hero's lights, where the finished state lived only inside an
   animation and `animation: none` under Reduce Motion left 34 elements
   invisible for ever. Here the finished state is `.try__item--done .try__tick
   { visibility: visible }`, which is a plain declaration that owes nothing to
   the animation; emuTick only adds the pop. */
.try__tick {
	visibility: hidden;
}
.try__item--done .try__tick {
	visibility: visible;
}

.try__text {
	display: flex;
	flex-direction: column;
	gap: 2px;
	min-width: 0;
}
.try__item-title {
	font-size: 16px;
	line-height: 1.35;
	font-weight: 800;
}
.try__item-desc {
	font-size: 14px;
	line-height: 1.45;
	color: var(--muted);
}

/* Each item's state in words, for a screen reader only: the done state is
   otherwise carried by a fill colour and a tick, and neither is text. The
   designs carry this and it is the reason the tick itself is aria-hidden. */
.try__status {
	position: absolute;
	width: 1px;
	height: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* -----------------------------------------------------------------------------
   27.4 Spoken alerts
   -------------------------------------------------------------------------- */
.try__sound {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	padding: 12px 14px 12px 16px;
	border-radius: 16px;
	background: var(--bg);
}

.try__sound-label {
	display: flex;
	flex-direction: column;
	gap: 2px;
	min-width: 0;
	flex: 1 1 200px;
}
.try__sound-title {
	font-size: 15px;
	font-weight: 800;
}
.try__sound-note {
	font-size: 13px;
	line-height: 1.4;
	color: var(--muted);
}

/* THE SWITCH IS 51x31 AND THE TARGET IS 51x44.
   docs/ui-rules.md: anything tappable is at least 44px tall, "with the extra
   spent on transparent padding where a visible box that large would be
   wrong". A 44px-tall switch would be wrong — it is drawn as the iOS control
   it depicts — so the button is the target and the track inside it is the
   control. The track carries every visual property; the button carries none. */
.try__switch {
	flex-shrink: 0;
	width: 51px;
	height: var(--tap);
	padding: 0;
	border: 0;
	background: transparent;
	display: flex;
	align-items: center;
}
.try__switch-track {
	position: relative;
	display: block;
	width: 51px;
	height: 31px;
	border-radius: 16px;
	background: var(--route);
}
.try__switch[aria-checked="true"] .try__switch-track {
	background: var(--blue);
}
.try__switch-knob {
	position: absolute;
	top: 2px;
	left: 2px;
	width: 27px;
	height: 27px;
	border-radius: 14px;
	background: #ffffff;
	box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.try__switch[aria-checked="true"] .try__switch-knob {
	transform: translateX(20px);
}

/* The browser has no speech at all. The switch stops claiming it can do
   something it cannot, and the note under the label says so instead — root
   CLAUDE.md rule 6, never fail silently. The wording is set by the script,
   because a page whose script never runs has no speech control to explain. */
.try__switch[aria-disabled="true"] .try__switch-track {
	background: var(--route);
	opacity: 0.5;
}

/* -----------------------------------------------------------------------------
   27.5 Start over, and the link out
   -------------------------------------------------------------------------- */
.try__actions {
	display: flex;
	flex-wrap: wrap;
	gap: 10px 18px;
	align-items: center;
}

.try__restart {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	min-height: 48px;
	padding: 0 18px;
	border-radius: 14px;
	border: 1.5px solid var(--line);
	background: var(--surface);
	color: var(--ink);
	font-size: 15px;
	font-weight: 800;
}

/* -----------------------------------------------------------------------------
   27.6 Motion
   -----------------------------------------------------------------------------
   Cancelled whole by section 25's backstop. Every finished state below is a
   plain declaration somewhere above, so cancelling an animation here leaves
   the element where it belongs rather than where its first keyframe put it.
   -------------------------------------------------------------------------- */
.try__restart {
	transition: transform 0.14s ease-out, box-shadow 0.2s ease-out,
		background-color 0.2s ease-out;
}
.try__restart:active {
	transform: scale(0.97);
}

.roles__ind {
	transition: transform 0.32s cubic-bezier(0.3, 0.7, 0.3, 1);
}

.try__item {
	transition: background-color 0.3s ease-out, border-color 0.3s ease-out;
}
.try__box {
	transition: background-color 0.25s ease-out, border-color 0.25s ease-out;
}

.try__switch-track {
	transition: background-color 0.2s ease-out;
}
.try__switch-knob {
	transition: transform 0.22s cubic-bezier(0.3, 0.7, 0.3, 1);
}

/* The tick pops in when an item is marked. One shot, and the element's
   resting state is its natural one, so `animation: none` is harmless. */
.try__tick--pop {
	animation: emuTick 0.4s cubic-bezier(0.3, 1.5, 0.5, 1) both;
}
@keyframes emuTick {
	from {
		transform: scale(0);
	}
	to {
		transform: none;
	}
}

/* And the item itself gives one soft ring. Shadow only — nothing moves and
   nothing is hidden by it. */
.try__item--just-done {
	animation: emuDone 0.6s ease-out;
}
@keyframes emuDone {
	0% {
		box-shadow: 0 0 0 0 rgba(4, 97, 248, 0.35);
	}
	100% {
		box-shadow: 0 0 0 10px rgba(4, 97, 248, 0);
	}
}

/* The caption and the screen are replaced when the role changes, and the
   design replays their entrance. Two names doing the same thing, alternated
   by the script: re-adding a class that is already present does not restart a
   CSS animation, and swapping between two does. That is the design's own A/B
   mechanism (`showA`/`showB` keyed off `runN`), reproduced as an effect
   rather than as a remount. */
.try__cap--in-a {
	animation: trySwapA 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.try__cap--in-b {
	animation: trySwapB 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes trySwapA {
	from {
		opacity: 0;
		transform: translateY(10px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}
@keyframes trySwapB {
	from {
		opacity: 0;
		transform: translateY(10px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

/* -----------------------------------------------------------------------------
   27.7 Narrow widths
   -----------------------------------------------------------------------------
   design/Main.dc.html's own breakpoint for the mockup frame is 560px, where
   the phone window becomes 289x625 and the screen inside it scales to 0.741.
   Nothing else in this section needs a breakpoint: the two columns already
   wrap on their own flex basis, and the card's padding is a clamp.
   -------------------------------------------------------------------------- */
@media (max-width: 560px) {
	.emu-box {
		width: 289px;
		height: 625px;
	}
	.emu-scale {
		transform: scale(0.741);
	}
}

/* =============================================================================
   28. How it works — the Oct 27 redesign
   =============================================================================
   Everything below builds `apps/site/design/How.dc.html`, which Dylan
   approved on Oct 27, 2026 along with its five siblings (How-Night,
   How-Tablet, How-Tablet-Night, How-Mobile, How-Mobile-Night). Those five
   differ from How.dc.html only in the canvas preview size and the default
   theme — checked, not assumed: with both files' tags put on separate lines,
   `diff` reports the `$preview` size and, on the three Night variants, the
   theme default, and nothing else. So every rule here is How.dc.html's, and
   the responsive behaviour is the designs' media queries at 900 and 560 px,
   which section 26.14 already carries for `.sec` and the header.

   WHAT IS NOT HERE, AND WHY
   -------------------------
   The page's four "Hear it" buttons are task R3b. They would reuse the speech
   module task R2a built, which is still under re-audit, so the buttons, their
   caption and the `.hw-say` speaking animation are held back together. The
   three alert cards and the "Alerts offline" card are complete apart from
   them. Nothing here is a disabled or dead control: root CLAUDE.md rule 6 is
   about exactly that, and an absent button is honest where a present one that
   does nothing is not.

   SECTION 17 IS THE PRE-REDESIGN VERSION OF THIS PAGE AND IS LEFT ALONE.
   Five of its classes — `.next-row`, `.next-row__label`, `.note-card`,
   `.note-card__lead`, `.sec--next` — are also used by About, Progress and
   Screens, which are still on the old shell, so deleting that section would
   change pages this task was told not to touch. None of the names below is
   one of them: `hw-` was unused in both stylesheets before this section
   (`grep -c 'hw-'` returned 0 in each). Section 17 comes out with the rest of
   the superseded shell when the last page converts.
   -------------------------------------------------------------------------- */

/* -----------------------------------------------------------------------------
   28.1 The hero
   -----------------------------------------------------------------------------
   `.page-hero` carries the shared part (position, navy, ink, its own clip);
   these carry what this hero does differently from Home's. One column instead
   of two, a 96px foot rather than 120px, and the three-step flow list under
   the copy where Home has a phone beside it.
   -------------------------------------------------------------------------- */

/* The two soft lights. `.hr-blob` (section 26.13) already gives them their
   shape, blur, opacity and the approved `hrDrift` loop; only the size,
   position and tint differ from Home's pair, so only those are set here. */
.hw-hero__blob--a {
	width: 520px;
	height: 520px;
	right: -140px;
	top: -120px;
	background: rgba(4, 97, 248, 0.5);
}
.hw-hero__blob--b {
	width: 380px;
	height: 380px;
	left: -140px;
	bottom: -160px;
	background: rgba(4, 165, 248, 0.28);
	animation-duration: 20s;
}

.hw-hero__inner {
	position: relative;
	padding-top: 140px;
	padding-bottom: 96px;
	display: flex;
	flex-direction: column;
	gap: 40px;
}

.hw-hero__copy {
	display: flex;
	flex-direction: column;
	gap: 18px;
	max-width: 860px;
}

/* The design's standfirst runs to 620px here where Home's stops at 540px.
   Written as a descendant of `.hw-hero` rather than as a class of its own, so
   the markup carries one class instead of two and `.page-hero__lead` supplies
   everything else. */
.hw-hero .page-hero__lead {
	max-width: 620px;
}

.hw-flow {
	position: relative;
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
	gap: 16px;
}

.hw-node {
	position: relative;
	display: flex;
	align-items: center;
	gap: 14px;
	padding: 16px 18px;
	border-radius: 20px;
	background: rgba(255, 255, 255, 0.07);
	border: 1px solid rgba(230, 236, 248, 0.16);
	box-shadow: 0 14px 34px rgba(0, 0, 0, 0.28);
	-webkit-backdrop-filter: blur(16px);
	backdrop-filter: blur(16px);
}

.hw-node__tile {
	flex-shrink: 0;
	width: 48px;
	height: 48px;
	border-radius: 15px;
	background: var(--blue);
	color: #ffffff;
	display: flex;
	align-items: center;
	justify-content: center;
	box-shadow: 0 8px 18px rgba(4, 97, 248, 0.45);
}

.hw-node__text {
	display: flex;
	flex-direction: column;
	gap: 2px;
	min-width: 0;
}

.hw-node__step {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--on-navy-link);
}

.hw-node__title {
	font-size: 17px;
	line-height: 1.3;
	font-weight: 800;
	color: #ffffff;
}

/* -----------------------------------------------------------------------------
   28.2 The three two-column sections, and the prose inside them
   -----------------------------------------------------------------------------
   "One box, wherever you need it", "If it fails, it says so" and
   "Route-informed alerts, later" share a shape — copy on one side, a panel on
   the other, both vertically centred — and the design gives each a slightly
   different row gap and a slightly narrower copy column. `.hw-split` is the
   shape; the two modifiers are the differences.
   -------------------------------------------------------------------------- */
.hw-split {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 36px 56px;
}
.hw-split--fail {
	gap: 28px 56px;
}
.hw-split--later {
	gap: 24px 56px;
}

/* `.headline` supplies the column; these three values are this page's.
   Declared after it in the sheet, so an equal-specificity override wins. */
.hw-split__copy {
	flex: 1 1 380px;
	min-width: 0;
	gap: 14px;
	max-width: 560px;
}
.hw-split--fail .hw-split__copy,
.hw-split--later .hw-split__copy {
	flex: 1 1 360px;
	max-width: 520px;
}

/* The body paragraph under a section heading on this page. Larger than
   `.t-body` and with its own line height, which the design sets on every one
   of them; the HEADINGS use the shared scale (docs/ui-rules.md), and this is
   component-local copy rather than part of that scale. */
.hw-prose {
	margin: 0;
	font-size: 18px;
	line-height: 1.6;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   28.3 The box's four facts
   -----------------------------------------------------------------------------
   A definition list, so each label stays associated with its value. The
   `<div>` around each pair is what HTML allows for grouping inside a `<dl>`
   and is what makes each pair one grid cell.
   -------------------------------------------------------------------------- */
.hw-facts {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	flex: 1 1 380px;
	min-width: 0;
	margin: 0;
	padding: 12px;
	border-radius: 24px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift-big);
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 10px;
}

.hw-fact {
	display: flex;
	flex-direction: column;
	gap: 10px;
	padding: 16px;
	border-radius: 18px;
	background: var(--bg);
}

.hw-fact__pop {
	align-self: flex-start;
}

.hw-fact__tile {
	width: 40px;
	height: 40px;
	border-radius: 13px;
	background: var(--chip);
	color: var(--accent);
	display: flex;
	align-items: center;
	justify-content: center;
}

.hw-fact__label {
	font-size: 14px;
	font-weight: 700;
	color: var(--muted);
}

.hw-fact__value {
	margin: 0;
	font-size: 18px;
	line-height: 1.3;
	font-weight: 800;
}

/* -----------------------------------------------------------------------------
   28.4 Plain alerts: the band and the three samples
   -----------------------------------------------------------------------------
   The band is full-bleed and the content inside it is wrapped, which is why
   the `.sec` rhythm sits on the section and the 80px of air sits on the inner
   wrap: the tint has to reach both page edges.
   -------------------------------------------------------------------------- */
.hw-band {
	background: var(--band);
}

.hw-band__inner {
	padding-top: 80px;
	padding-bottom: 80px;
	display: flex;
	flex-direction: column;
	gap: 28px;
}

.hw-band__head {
	gap: 14px;
	max-width: 760px;
}

.hw-says {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
	gap: 16px;
}

.hw-say {
	display: flex;
	flex-direction: column;
	gap: 14px;
	padding: 20px;
	border-radius: 22px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift);
}

/* `space-between` puts the kind label at the start of the row and the "Hear
   it" button at the end, which is the design's own layout.

   THE 44px IS A RESERVE, NOT A DECORATION. The button ships `hidden` and
   public/scripts/hear-it.js removes the attribute, so without this the row
   would be the height of the 16px level bars until the script ran and then
   jump to the button's 44px. apps/site/README.md records the same requirement
   for the Progress page's filter row: a control revealed by script has its
   height reserved in CSS so revealing it shifts nothing. The design's own row
   is 44px tall for the same reason — it contains the same button. */
.hw-say__top {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 10px;
	min-height: 44px;
}

.hw-say__kind {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--muted);
}

.hw-say__bars {
	display: inline-flex;
	align-items: center;
	gap: 2px;
	height: 16px;
	color: var(--blue);
}

/* Five level bars. At rest they sit at half height and do not move: the
   design only animates them while that sample is being spoken. The
   `transform` is a resting state rather than an animation's end frame, so
   section 25's reduced-motion backstop has nothing to cancel here — with
   Reduce Motion on, the bars simply stay at rest while the alert is spoken,
   and the speech is the feedback. */
.hw-bar {
	display: inline-block;
	width: 4px;
	border-radius: 2px;
	background: currentColor;
	transform-origin: center;
	transform: scaleY(0.5);
}
.hw-bar--7 {
	height: 7px;
}
.hw-bar--8 {
	height: 8px;
}
.hw-bar--10 {
	height: 10px;
}
.hw-bar--14 {
	height: 14px;
}
.hw-bar--16 {
	height: 16px;
}

/* While a sample is being spoken these bars move, and that is section 28.9's
   (`.hw-say__bars--live`), with the rest of this page's motion. */

/* The "Hear it" buttons, shared between the three samples and the offline
   card: the design draws the same shape in both places and differs only in
   the fill and the border.

   `[hidden]` needs the override because `display` is set here, which beats the
   user-agent's `[hidden] { display: none }` — the same reason .tabpanel,
   .mobile-menu and .pg-item each carry one. Without it the buttons would ship
   visible and inert, which root CLAUDE.md rule 6 forbids.

   44px is the design's `min-height` and docs/ui-rules.md's floor. These are
   site chrome, not controls inside a depicted app screen, so the scaled-mockup
   exemption does not apply to them and they are measured as rendered. */
.hw-hear {
	flex-shrink: 0;
	display: inline-flex;
	align-items: center;
	gap: 6px;
	min-height: 44px;
	padding: 0 14px;
	border-radius: 22px;
	font-size: 14px;
	font-weight: 800;
}
.hw-hear[hidden] {
	display: none;
}
/* Their press feedback is section 28.9's, with the rest of the page's motion. */

.hw-say__hear {
	border: 0;
	background: var(--chip);
	color: var(--accent);
}

/* The caption under the three samples, which is also where a failed press
   says so (public/scripts/hear-it.js). The design's own two declarations; the
   `[hidden]` rule is not needed for a <p>, whose display this sheet never
   sets, and is written anyway so that adding a `display` here later cannot
   quietly ship a visible caption over hidden buttons. */
.hw-says__note {
	margin: 0;
	font-size: 14px;
	color: var(--muted);
}
.hw-says__note[hidden] {
	display: none;
}

/* The sample alert itself. Every one of these strings is docs/alerts.md's own
   wording, character for character. */
.hw-say__quote {
	margin: 0;
	font-size: 20px;
	line-height: 1.45;
	font-weight: 800;
}

/* The taxiway letter and its phonetic word never split across a line:
   "taxiway G Golf" reading down two lines would be two pieces of one fact. */
.hw-say__nb {
	white-space: nowrap;
}

/* The letter plate. `aria-hidden` in the markup, so a screen reader hears
   "American on taxiway Golf, about 50 seconds." once rather than the letter
   and then the word. Sized in `em` so it tracks the quote's own font size. */
.hw-say__plate {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	display: inline-block;
	min-width: 1.1em;
	text-align: center;
	padding: 0.05em 0.3em;
	margin: 0 0.25em 0 0.1em;
	border-radius: 4px;
	background: var(--navy);
	color: #ffffff;
	border: 2px solid var(--sky);
	font-size: 0.9em;
	font-weight: 800;
	line-height: 1.25;
}

.hw-say__case {
	font-size: 15px;
	line-height: 1.45;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   28.5 "Alerts offline. Verify by radio."
   -----------------------------------------------------------------------------
   Root CLAUDE.md rule 6 in one card. Red in both themes, from the `--red-*`
   tokens section 2 already carries, and a `role="note"` in the markup because
   it is an aside about the product rather than a live alert.
   -------------------------------------------------------------------------- */
.hw-alarm {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	flex: 1 1 420px;
	min-width: 0;
	padding: 24px;
	border-radius: 24px;
	background: var(--red-bg);
	color: var(--red-ink);
	border: 2px solid var(--red-line);
	box-shadow: var(--lift);
	display: flex;
	flex-direction: column;
	gap: 12px;
}

/* The same 44px reserve as .hw-say__top, for the same reason: this row's
   "Hear it" button ships `hidden` and is revealed by script. */
.hw-alarm__top {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	min-height: 44px;
}

/* Outlined rather than filled, which is the one way the design's offline
   button differs from the three on the samples. The ink and the line are the
   card's own red tokens, so it stays inside the card's palette in both
   themes. */
.hw-alarm__hear {
	border: 1.5px solid var(--red-line);
	background: transparent;
	color: var(--red-ink);
}

.hw-alarm__label {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
}

.hw-alarm__shout {
	font-size: clamp(24px, 3vw, 32px);
	line-height: 1.15;
	font-weight: 800;
}

.hw-alarm__body {
	font-size: 16px;
	line-height: 1.55;
	font-weight: 600;
}

/* This card's status line for its "Hear it" button. The design draws no
   caption here, so the element ships EMPTY and inventing copy for it would be
   words nobody approved; `:empty` keeps it out of the card's 12px flex gap
   until the script has something to say. It inherits the card's red ink, so
   its contrast is the card's own. */
.hw-alarm__note {
	margin: 0;
	font-size: 14px;
	line-height: 1.45;
	font-weight: 600;
}
.hw-alarm__note:empty {
	display: none;
}

/* -----------------------------------------------------------------------------
   28.6 The two stacked sections: the tools, and the four nevers
   -------------------------------------------------------------------------- */
.hw-stack {
	display: flex;
	flex-direction: column;
	gap: 28px;
}

.hw-stack__head {
	gap: 14px;
	max-width: 680px;
}

.hw-tools {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
	gap: 20px;
}

.hw-tool {
	padding: 24px;
	border-radius: 22px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift);
	display: flex;
	gap: 16px;
	align-items: flex-start;
}

/* `.hr-tile` (section 26.13) is what makes this answer a hover anywhere on
   its card, which is how the designs tie the tile to the card rather than to
   the pointer's exact position. */
.hw-tool__tile {
	width: 52px;
	height: 52px;
	border-radius: 16px;
	background: var(--chip);
	color: var(--accent);
	display: flex;
	align-items: center;
	justify-content: center;
}

.hw-tool__text {
	display: flex;
	flex-direction: column;
	gap: 6px;
	min-width: 0;
}

.hw-tool__title {
	font-size: 20px;
	line-height: 1.25;
	font-weight: 800;
}

.hw-tool__body {
	font-size: 16px;
	line-height: 1.55;
	color: var(--muted);
}

.hw-nevers {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
	gap: 16px;
}

.hw-never {
	padding: 20px 22px;
	border-radius: 22px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift);
	display: flex;
	gap: 16px;
	align-items: flex-start;
}

/* A crossed-out circle, filled and round. It does not answer a hover: the
   design gives these no `.hr-tile`, and a "never" is not an invitation. */
.hw-never__tile {
	width: 44px;
	height: 44px;
	border-radius: 22px;
	background: var(--tile);
	color: #ffffff;
	display: flex;
	align-items: center;
	justify-content: center;
	box-shadow: 0 6px 14px rgba(2, 17, 58, 0.25);
}

.hw-never__text {
	display: flex;
	flex-direction: column;
	gap: 6px;
	min-width: 0;
}

.hw-never__title {
	font-size: 19px;
	line-height: 1.3;
	font-weight: 800;
}

.hw-never__body {
	font-size: 16px;
	line-height: 1.55;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   28.7 Route-informed alerts, later
   -----------------------------------------------------------------------------
   Drawn as an outline rather than a filled card, which is the design saying
   the thing inside it does not exist yet. Root CLAUDE.md rule 4: route-
   informed alerts stay off for customers until counsel approves.
   -------------------------------------------------------------------------- */
.hw-later {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	flex: 1 1 420px;
	min-width: 0;
	padding: 22px;
	border-radius: 22px;
	background: transparent;
	border: 2px dashed var(--route);
	display: flex;
	gap: 16px;
	align-items: flex-start;
}

/* `hw-next` is the design's own name for this ring. APPROVED LOOP 3 of 4 —
   hrPing (docs/ui-rules.md, Dylan, Oct 27, 2026). It marks what is next and
   depicts nothing: no alert, no aircraft, no decision, and nothing is read
   off it. Unlike Home's `.tl-dot--pending` it has no breathing centre dot —
   the design draws only the ring here. */
.hw-next {
	flex-shrink: 0;
	position: relative;
	width: 16px;
	height: 16px;
	margin-top: 4px;
	border-radius: 8px;
	border: 3px solid var(--muted);
	box-sizing: border-box;
	color: var(--muted);
}
.hw-next::after {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: 16px;
	height: 16px;
	margin: -8px 0 0 -8px;
	border-radius: 50%;
	border: 2px solid currentColor;
	box-sizing: border-box;
	animation: hrPing 2.4s ease-out infinite;
}

.hw-later__text {
	display: flex;
	flex-direction: column;
	gap: 6px;
	min-width: 0;
}

.hw-later__label {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--muted);
}

.hw-later__body {
	font-size: 17px;
	line-height: 1.55;
	font-weight: 600;
}

/* -----------------------------------------------------------------------------
   28.8 On to the next page
   -----------------------------------------------------------------------------
   One large card-shaped link, so the whole card is the target rather than a
   word inside it.
   -------------------------------------------------------------------------- */
.hw-onward-sec {
	padding-top: 72px;
	padding-bottom: 96px;
}

/* `:hover` is listed BESIDE the base state, and that is load-bearing rather
   than tidy: section 3's `a:hover { color: #0442b0 }` is a type selector plus
   a pseudo-class, which outranks a single class, so without the second
   selector here the whole card's text turns brand blue on hover. The designs'
   own card is `color: {{c.ink}}` with no hover colour at all. */
.hw-onward,
.hw-onward:hover {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	padding: 22px 24px;
	border-radius: 22px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift-big);
	color: var(--ink);
	text-decoration: none;
}

.hw-onward__text {
	display: flex;
	flex-direction: column;
	gap: 4px;
}

.hw-onward__label {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--muted);
}

.hw-onward__title {
	font-size: clamp(22px, 2.6vw, 28px);
	font-weight: 800;
}

.hw-onward__arrow {
	flex-shrink: 0;
	width: 48px;
	height: 48px;
	border-radius: 24px;
	background: var(--blue);
	color: #ffffff;
	display: flex;
	align-items: center;
	justify-content: center;
	box-shadow: 0 10px 24px rgba(4, 97, 248, 0.38);
}

/* -----------------------------------------------------------------------------
   28.9 Motion
   -----------------------------------------------------------------------------
   Everything this page moves on its own, all of it the designs'. Everything
   else moves through the shared classes in section 26.13 (`.hr-card`,
   `.hr-tile`, `.hr-blob`) and the scroll-driven reveals (`.hr-rise`,
   `.hr-pop`).

   The whole block is cancelled by section 25's reduced-motion backstop
   (`animation: none !important`, `transition: none !important` on every
   element and pseudo-element). NOTHING BELOW HIDES ANYTHING ON ITS OWN — the
   hidden frame lives inside `hrUp`, not in the rule that uses it, and the
   level bars' resting `scaleY(0.5)` is a declaration in section 28.4 rather
   than an animation's end frame — so cancelling every animation here leaves
   each element at its natural, visible position. That is the distinction the
   hero's 34 approach lights got wrong once: a rule that declares `opacity: 0`
   itself and relies on an animation to undo it needs a finished state in the
   reduce block, and src/html/motion.test.ts scans both stylesheets for
   exactly that shape.
   -------------------------------------------------------------------------- */

/* The three flow cards rise in turn under the hero copy. Delays are the
   design's own (.35s, .5s, .65s), as classes rather than inline styles. */
.hw-node {
	animation: hrUp 0.7s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.hw-node--1 {
	animation-delay: 0.35s;
}
.hw-node--2 {
	animation-delay: 0.5s;
}
.hw-node--3 {
	animation-delay: 0.65s;
}

/* Press feedback on the onward card, the same shape section 26.13 gives the
   header links and the calls to action. Declared here rather than appended to
   that list so this task changes no shared rule. */
.hw-onward {
	transition: transform 0.14s ease-out, box-shadow 0.2s ease-out,
		background-color 0.2s ease-out;
}
.hw-onward:active {
	transform: scale(0.97);
}

/* The same, for the four "Hear it" buttons (task R3b), and declared separately
   for the same reason.

   THE DESIGN'S OWN CLASS NAME IS NOT USED, deliberately. design/How.dc.html
   puts `.hr-press` on these buttons and declares exactly these two rules
   against it. Nothing in this stylesheet has ever carried that class — every
   converted page declares the effect on its own element instead — so shipping
   the name would have put a word in a class attribute that styles nothing,
   and a reader would reasonably assume the press effect was there. It WAS
   written that way first, and the check is
   `grep -cE '^\.hr-press' public/styles/site.css` — no rule here selects it,
   and the only mentions of the name in this file are in this comment. (That
   second clause is why the query is anchored: a plain `grep -c hr-press`
   counts this paragraph, so the obvious command would answer about itself.) */
.hw-hear {
	transition: transform 0.14s ease-out, box-shadow 0.2s ease-out,
		background-color 0.2s ease-out;
}
.hw-hear:active {
	transform: scale(0.97);
}

/* The level bars beside a sample's kind label, while that sample is being
   spoken. Added by public/scripts/hear-it.js only when the speech module
   reports it actually spoke, and taken off when the utterance's time is up or
   when the browser turns out to have said nothing — so the bars can never
   claim speech over silence.

   FOUR ITERATIONS, AS THE DESIGN DRAWS THEM (`hrBar .7s ease-in-out 4`), and
   four 0.7s iterations is what makes 2800ms the right lifetime for the class
   in that file. docs/ui-rules.md's approved-loop list is untouched: this is
   finite. The three per-bar durations are the design's too, and they are what
   stops the five bars moving as one block. */
.hw-say__bars--live .hw-bar {
	animation: hrBar 0.7s ease-in-out 4;
}
.hw-say__bars--live .hw-bar:nth-child(2) {
	animation-duration: 0.55s;
}
.hw-say__bars--live .hw-bar:nth-child(3) {
	animation-duration: 0.8s;
}
.hw-say__bars--live .hw-bar:nth-child(4) {
	animation-duration: 0.6s;
}

/* =============================================================================
   29. Progress — the Oct 27 redesign
   =============================================================================
   Everything below builds `apps/site/design/Progress.dc.html`, which Dylan
   approved on Oct 27, 2026 along with its five siblings (Progress-Night,
   Progress-Tablet, Progress-Tablet-Night, Progress-Mobile,
   Progress-Mobile-Night). Those five differ from Progress.dc.html only in the
   canvas preview size and the default theme — checked, not assumed: with both
   files' tags on separate lines, `diff` reports the preview size and, on the
   three Night variants, the theme default, and nothing else. So every rule
   here is Progress.dc.html's, and the responsive behaviour is its own media
   queries at 900 and 480 px, on top of the 900/760/620/560 steps section
   26.14 already carries for `.sec`, the header and the menu.

   THE NAMESPACE IS FREE. `grep -c 'pg-' public/styles/site.css
   public/styles/mockups.css` returned 0 in each before this section, the same
   check task R3a made for `hw-`.

   WHAT IS NOT HERE, AND WHY
   -------------------------
   The signup card's SUCCESS STATE. The design draws a tick, "You're on the
   list.", "Updates will go to <address>." and a "Change" button, reachable by
   typing a valid address and clicking. It is not built: docs/security.md
   section 2 requires double opt-in, so that sentence would be false at the
   moment it appeared, and docs/open-questions.md records the true wording as
   undecided and waiting on Dylan. The idle state is built exactly as drawn,
   with the design's own "Design mockup · the form isn't live yet" line, and
   nothing validates, submits or speaks to a server. Held back rather than
   stubbed, for the reason root CLAUDE.md rule 6 gives.

   SECTION 21 WAS THE PRE-REDESIGN VERSION OF THIS PAGE AND IS NOW REMOVED
   (see its stub above). Its two classes, `.note-card--upnext` and
   `.progress-log`, were used by this page and by nothing else — checked with
   `grep -rn 'note-card--upnext\|progress-log' public/ src/`, which finds
   nothing outside the stylesheet now that the page is converted. The
   `.note-card` base it layered on is section 17's and is left alone: About,
   Screens and Contact are still on the old shell.

   THE FOUR FILTER BUTTONS ARE 44px TALL. design/Progress.dc.html drew them at
   `min-height: 40px`, below docs/ui-rules.md's 44px floor; Dylan chose the
   floor over the drawing ("build the buttons at 44px", Oct 28, 2026 —
   docs/ui-rules.md's "Sub-44px controls in the redesign are raised to 44px"),
   and the design file was raised to 44px under that authorisation. The row
   they sit in is 52px (4px of padding each side of the 44px button).
   -------------------------------------------------------------------------- */

/* -----------------------------------------------------------------------------
   29.1 The hero
   -----------------------------------------------------------------------------
   `.page-hero` carries the shared part (position, navy, ink, its own clip);
   these carry what this hero does differently from Home's and How it works'.
   One column, an 88px foot, and an 18px rhythm.
   -------------------------------------------------------------------------- */

/* The two soft lights. `.hr-blob` (section 26.13) already gives them their
   shape, blur, opacity and the approved `hrDrift` loop; only the size,
   position and tint differ from the other two heroes' pairs. */
.pg-hero__blob--a {
	width: 520px;
	height: 520px;
	right: -140px;
	top: -140px;
	background: rgba(4, 97, 248, 0.5);
}
.pg-hero__blob--b {
	width: 360px;
	height: 360px;
	left: -120px;
	bottom: -180px;
	background: rgba(4, 165, 248, 0.28);
	animation-duration: 20s;
}

.pg-hero__inner {
	position: relative;
	padding-top: 140px;
	padding-bottom: 88px;
	display: flex;
	flex-direction: column;
	gap: 18px;
}

.pg-hero__title {
	max-width: 820px;
}

/* The design's standfirst runs to 620px here where Home's stops at 540px.
   Written as a descendant so the shared rule is untouched. */
.pg-hero .page-hero__lead {
	max-width: 620px;
}

/* -----------------------------------------------------------------------------
   29.2 "Where it is now" — the stage panel
   -----------------------------------------------------------------------------
   Lifted 40px over the hero's bottom edge, the same move section 26.7 makes
   for Home's principles bar (which uses -44px; this panel's own figure is
   -40px).
   -------------------------------------------------------------------------- */
.pg-stand {
	position: relative;
	z-index: 3;
	margin-top: -40px;
}

.pg-stand__card {
	box-sizing: border-box;
	padding: clamp(20px, 3.4vw, 36px);
	border-radius: 28px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift-big);
	display: flex;
	flex-direction: column;
	gap: 28px;
}

.pg-stand__head {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 10px 20px;
}

.pg-stand__title {
	display: flex;
	flex-direction: column;
	gap: 4px;
}

.pg-stand__eyebrow {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--muted);
}

/* `.pg-stand__h2` IS GONE — "In the design phase." takes the shared `.t-h2`.
   A SETTLED QUESTION, so it is recorded here rather than left to be
   reconstructed from the design file.

   This rule used to hold `clamp(24px, 2.8vw, 32px)` / `1.15` / `-0.01em`,
   which is what design/Progress.dc.html draws, with a comment arguing that a
   heading inside a card is one of the "component-local sizes inside a page's
   own components" that `docs/ui-rules.md` exempts from the shared scale. The
   How it works page read the same sentence the other way and put its card
   heading on the shared class. **Dylan settled it on Oct 28, 2026 —
   "shared scale"** — and `docs/ui-rules.md` now carries "A heading takes the
   shared scale, wherever it sits — including inside a card". The exemption
   still covers component-local sizes that are NOT headings: a chip, a label,
   a badge, a number.

   So the page's two off-scale headings moved onto `.t-h2` and this rule had
   nothing left to declare. The line it falls on here is the heading ELEMENT:
   the four `<h1>`/`<h2>` on this page take the shared scale, and
   `.pg-stage__title`, `.pg-title`, `.pg-next__title` and `.pg-onward__title`
   keep their own sizes because they are spans — card labels and a link's
   text, not headings.

   NARROWED THE SAME DAY, and the heading now matches its drawing. Measured in
   Chrome, `.t-h2` alone rendered "In the design phase." at 48px where it is
   drawn at 32px, and Dylan chose the drawings ("Match the drawings";
   docs/ui-rules.md, "Card headings match their drawings"). The shared scale
   now carries four card steps, and this heading takes step 1,
   `.t-h2 .t-h2--card-s` — the figures this rule used to hold, now in the
   scale where every card heading of this kind can reach them. The earlier
   before-and-after is in docs/evidence/2026-10-27-step0.5-R5-progress.md
   §10b. */

.pg-stand__chip {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	min-height: 34px;
	box-sizing: border-box;
	padding: 6px 14px;
	border-radius: 17px;
	background: var(--chip);
	color: var(--ink);
	font-size: 14px;
	line-height: 1.35;
	font-weight: 800;
}

/* The five stages, across at wide widths and down a rail below 900px. */
.pg-track {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(5, minmax(0, 1fr));
	gap: 24px;
}

.pg-stage {
	position: relative;
	display: flex;
	flex-direction: column;
	gap: 12px;
}

/* The connector between one stage and the next. `--lk` is the design's own
   variable name for its colour; it is set per state below rather than written
   into an inline style, which is what the design does. */
.pg-link {
	--lk: var(--route);
	position: absolute;
	top: 19px;
	left: 48px;
	right: -16px;
	height: 2px;
	border-radius: 1px;
	background: var(--lk);
}
.pg-dash {
	background: repeating-linear-gradient(
		to right,
		var(--lk) 0 6px,
		transparent 6px 12px
	);
}
.pg-stage--done .pg-link {
	--lk: var(--blue);
}

/* The stage dot. One 40px box in every state; the state decides its fill,
   its edge and its shadow. */
.pg-dot {
	position: relative;
	z-index: 1;
	flex-shrink: 0;
	width: 40px;
	height: 40px;
	box-sizing: border-box;
	border-radius: 20px;
	background: var(--surface);
	color: var(--muted);
	display: flex;
	align-items: center;
	justify-content: center;
}
.pg-stage--done .pg-dot {
	background: var(--blue);
	border: 0;
	color: #ffffff;
	box-shadow: 0 6px 16px rgba(4, 97, 248, 0.3);
}
.pg-stage--now .pg-dot {
	border: 3px solid var(--blue);
	box-shadow: 0 6px 16px rgba(4, 97, 248, 0.3);
}
.pg-stage--next .pg-dot {
	border: 2px solid var(--muted);
}
.pg-stage--later .pg-dot {
	border: 2px dashed var(--route);
}

/* The blue pip inside the "Now" dot. */
.pg-dot__pip {
	width: 12px;
	height: 12px;
	border-radius: 6px;
	background: var(--blue);
}

/* APPROVED LOOPS 3 and 4 of 4 — hrPing and hrBreath, on the "Up next" stage
   dot. The design's own `hr-next`. It depicts nothing: no alert, no aircraft
   and no decision, which is why docs/ui-rules.md lets these two repeat. Both
   are cancelled by section 25's reduced-motion backstop, and neither rule
   hides anything on its own — the ring and the pip are pseudo-elements that
   exist at full opacity and only move — so cancelling them leaves the dot
   exactly as drawn. */
.pg-dot--next::after {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: 16px;
	height: 16px;
	margin: -8px 0 0 -8px;
	border-radius: 50%;
	border: 2px solid currentColor;
	box-sizing: border-box;
	animation: hrPing 2.4s ease-out infinite;
}
.pg-dot--next::before {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: 6px;
	height: 6px;
	margin: -3px 0 0 -3px;
	border-radius: 50%;
	background: currentColor;
	animation: hrBreath 2.4s ease-in-out infinite;
}

.pg-stage__text {
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 4px;
}

.pg-stage__tag {
	align-self: flex-start;
	display: inline-flex;
	align-items: center;
	min-height: 24px;
	padding: 0 10px;
	border-radius: 12px;
	background: transparent;
	color: var(--muted);
	font-size: 12px;
	font-weight: 800;
	letter-spacing: 0.05em;
	text-transform: uppercase;
}
.pg-stage--done .pg-stage__tag {
	background: var(--chip);
	color: var(--accent);
}
.pg-stage--now .pg-stage__tag {
	background: var(--blue);
	color: #ffffff;
}

.pg-stage__title {
	font-size: 18px;
	line-height: 1.3;
	font-weight: 800;
	color: var(--muted);
}
.pg-stage--done .pg-stage__title,
.pg-stage--now .pg-stage__title {
	color: var(--ink);
}

.pg-stage__body {
	font-size: 15px;
	line-height: 1.5;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   29.3 The "Up next" row inside the panel
   -----------------------------------------------------------------------------
   Two of its three lines are Worker-injected from content/up-next.md
   (src/index.ts's PROGRESS_SLOTS), so they are empty in the file.
   -------------------------------------------------------------------------- */
.pg-next {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 16px 20px;
	padding: 18px 20px;
	border-radius: 20px;
	background: var(--bg);
}

.pg-next__tile {
	flex-shrink: 0;
	width: 48px;
	height: 48px;
	border-radius: 14px;
	background: var(--tile);
	color: #ffffff;
	display: flex;
	align-items: center;
	justify-content: center;
}

.pg-next__text {
	flex: 1 1 320px;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 2px;
}

.pg-next__eyebrow {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--muted);
}

.pg-next__title {
	font-size: 18px;
	line-height: 1.3;
	font-weight: 800;
}

.pg-next__body {
	font-size: 15px;
	line-height: 1.5;
	color: var(--muted);
}

.pg-next__cta {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	min-height: 48px;
	padding: 0 18px;
	border-radius: 24px;
	background: var(--chip);
	color: var(--accent);
	text-decoration: none;
	font-size: 15px;
	font-weight: 800;
}
.pg-next__cta:hover,
.pg-next__cta:focus-visible {
	color: var(--accent);
}

/* -----------------------------------------------------------------------------
   29.4 The build log
   -------------------------------------------------------------------------- */
.pg-log {
	display: flex;
	flex-direction: column;
	gap: 24px;
}

.pg-log__lead {
	margin: 0;
	max-width: 560px;
	font-size: 17px;
	line-height: 1.55;
	color: var(--muted);
}

.pg-log__body {
	max-width: 820px;
	display: flex;
	flex-direction: column;
	gap: 18px;
}

/* The filter and the count line on one row.
   `min-height: 52px` RESERVES THE FILTER'S HEIGHT. The filter ships `hidden`
   and scripts/progress.js reveals it, so without this the row would grow as
   the script ran — a layout shift on every load. 52px is the control's own
   height (4px of padding either side of a 44px button). */
.pg-bar {
	min-height: 52px;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 10px 16px;
}

.pg-seg {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	position: relative;
	display: grid;
	grid-template-columns: repeat(4, minmax(0, 1fr));
	gap: 4px;
	width: 440px;
	max-width: 100%;
	padding: 4px;
	border-radius: 14px;
	background: var(--chip);
}
/* Needed because the rule above sets a `display`, which outranks the `hidden`
   attribute's UA `display: none`. Same correction section 26.4 makes for the
   small-screen menu. */
.pg-seg[hidden] {
	display: none;
}

/* The sliding indicator. Its position is the group's `data-pg-index` and
   three rules, never an inline transform: the design computes
   `translateX(calc(N * (100% + 4px)))` per render, and apps/site/README.md
   forbids a `style` attribute anywhere on this site. The percentage is of the
   indicator's OWN width, which is one column, so one step is a column plus
   the 4px gap. Index 0 needs no rule: `left: 4px` already puts it there. */
.pg-ind {
	position: absolute;
	top: 4px;
	left: 4px;
	width: calc((100% - 20px) / 4);
	height: 44px;
	border-radius: 11px;
	background: var(--surface);
	box-shadow: 0 2px 6px rgba(2, 17, 58, 0.16);
}
.pg-seg[data-pg-index="1"] .pg-ind {
	transform: translateX(calc(1 * (100% + 4px)));
}
.pg-seg[data-pg-index="2"] .pg-ind {
	transform: translateX(calc(2 * (100% + 4px)));
}
.pg-seg[data-pg-index="3"] .pg-ind {
	transform: translateX(calc(3 * (100% + 4px)));
}

/* THE 44px IS THE DESIGN'S, RAISED FROM 40px UNDER DYLAN'S AUTHORISATION.
   docs/ui-rules.md sets a 44px floor for anything tappable; the design drew
   these at `min-height: 40px` and Dylan chose the floor over the drawing —
   see the note at the top of this section. `.pg-ind` above is raised to the
   same 44px so the sliding highlight still fills the chosen button's box. */
.pg-seg__btn {
	position: relative;
	min-height: 44px;
	padding: 0 6px;
	border: 0;
	border-radius: 11px;
	background: transparent;
	color: var(--ink);
	font-size: 14px;
	font-weight: 800;
	white-space: nowrap;
}

/* A live region, so it ships visible and empty rather than hidden: an empty
   `role="status"` promises nothing, and starting empty is how a live region
   is meant to start (MDN, ARIA status role). `min-height` keeps its line so
   the first announcement moves nothing. */
.pg-count {
	min-height: 21px;
	font-size: 14px;
	font-weight: 700;
	color: var(--muted);
}

/* The log itself: month groups, each a heading and its own list. */
.pg-list {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 14px;
}

.pg-group {
	display: flex;
	flex-direction: column;
	gap: 14px;
}
.pg-group[hidden] {
	display: none;
}

/* 42px clears the 28px rail plus its 14px gutter, so the heading starts where
   the cards do. The design's own figure. */
.pg-month {
	padding-left: 42px;
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--muted);
}

.pg-items {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: 14px;
}

.pg-item {
	display: grid;
	grid-template-columns: 28px minmax(0, 1fr);
	gap: 0 14px;
}
.pg-item[hidden] {
	display: none;
}

.pg-rail {
	position: relative;
	display: flex;
	justify-content: center;
}

.pg-rail__dot {
	position: relative;
	z-index: 1;
	margin-top: 24px;
	width: 16px;
	height: 16px;
	border-radius: 8px;
	background: var(--blue);
	border: 3px solid var(--blue);
	box-sizing: border-box;
}

/* The line to the next entry. The design draws it on every entry but the LAST
   ONE IT IS SHOWING, which depends on the filter — so the structural last
   entry covers the no-JavaScript case and `data-pg-last`, which
   scripts/progress.js moves, covers the filtered one. Both are needed: with
   the script blocked nothing is marked, and with it running the structural
   last entry may be hidden. */
.pg-rail__line {
	position: absolute;
	left: 50%;
	margin-left: -1px;
	top: 44px;
	bottom: -38px;
	width: 2px;
	border-radius: 1px;
	background: var(--line);
}
.pg-group:last-child .pg-items .pg-item:last-child .pg-rail__line,
.pg-item[data-pg-last] .pg-rail__line {
	background: transparent;
}

.pg-card {
	display: flex;
	gap: 14px;
	align-items: flex-start;
	min-width: 0;
	padding: 18px 20px;
	border-radius: 20px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift);
}

.pg-ico {
	flex-shrink: 0;
	width: 40px;
	height: 40px;
	border-radius: 12px;
	background: var(--chip);
	color: var(--accent);
	display: flex;
	align-items: center;
	justify-content: center;
}

.pg-text {
	flex: 1;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 4px;
}

.pg-kind {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.04em;
	text-transform: uppercase;
	color: var(--accent);
}

.pg-title {
	font-size: 19px;
	line-height: 1.3;
	font-weight: 800;
}

.pg-body {
	font-size: 16px;
	line-height: 1.55;
	color: var(--muted);
}

/* Both buttons ship `hidden` and the script reveals whichever applies, so
   this row reserves their height for the same reason `.pg-bar` does. */
.pg-more {
	min-height: 48px;
	display: flex;
	flex-wrap: wrap;
	gap: 12px;
	padding-left: 42px;
}

.pg-more__btn {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	min-height: 48px;
	padding: 0 20px;
	border: 0;
	border-radius: 24px;
	background: var(--chip);
	color: var(--accent);
	font-size: 15px;
	font-weight: 800;
}
.pg-more__btn[hidden] {
	display: none;
}

/* -----------------------------------------------------------------------------
   29.5 Get updates — the signup card
   -----------------------------------------------------------------------------
   Navy in both themes, so every colour here is fixed. The idle state, the
   design's validation line and its success panel; public/scripts/signup.js
   moves between them by the `hidden` attribute and `aria-invalid`, never by a
   style.
   -------------------------------------------------------------------------- */

/* The design's own figure, so the "Get updates" link in the panel above lands
   the card clear of the top of the viewport. */
.pg-sub-sec {
	scroll-margin-top: 110px;
}

.pg-sub {
	position: relative;
	overflow: hidden;
	box-sizing: border-box;
	border-radius: 28px;
	background: var(--navy);
	color: var(--on-navy);
	padding: clamp(28px, 5vw, 56px);
	box-shadow: 0 30px 60px rgba(2, 17, 58, 0.28), 0 6px 16px rgba(2, 17, 58, 0.18);
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 28px 48px;
}

.pg-sub__blob {
	width: 380px;
	height: 380px;
	right: -100px;
	top: -140px;
	background: rgba(4, 97, 248, 0.5);
}

.pg-sub__copy {
	position: relative;
	flex: 1 1 340px;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.pg-sub__eyebrow {
	font-size: 14px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--on-navy-link);
}

/* THE WHITE ONLY. The size, line height, weight and letter spacing come from
   the shared `.t-h2` and its card step 3, `.t-h2--card-l`, which the markup
   carries alongside this class — a
   heading takes the shared scale wherever it sits (docs/ui-rules.md, Dylan,
   Oct 28, 2026; the note on `.pg-stand__h2` in section 29.2 has the whole of
   it). This card is navy in both themes and the design draws the heading
   #FFFFFF, which `.t-h2` does not set and which the card's own
   `var(--on-navy)` ink would otherwise supply at #E6ECF8. */
.pg-sub__h2 {
	color: #ffffff;
}

.pg-sub__lead {
	margin: 0;
	max-width: 520px;
	font-size: 17px;
	line-height: 1.6;
	color: var(--on-navy-muted);
}

.pg-sub__side {
	position: relative;
	flex: 1 1 380px;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 12px;
}

.pg-sub__field {
	display: flex;
	flex-direction: column;
	gap: 10px;
}
/* scripts/signup.js hides the field while the success panel shows, and the
   `display` above would otherwise beat the attribute. */
.pg-sub__field[hidden] {
	display: none;
}

.pg-sub__label {
	font-size: 15px;
	font-weight: 800;
	color: #ffffff;
}

.pg-sub__row {
	display: flex;
	flex-wrap: wrap;
	gap: 10px;
}

.pg-sub__input {
	flex: 1 1 220px;
	min-width: 0;
	min-height: 56px;
	box-sizing: border-box;
	padding: 0 18px;
	border-radius: 16px;
	border: 2px solid rgba(230, 236, 248, 0.28);
	background: rgba(255, 255, 255, 0.08);
	color: #ffffff;
	font-family: inherit;
	font-size: 17px;
	outline-offset: 2px;
}

/* THE PLACEHOLDER'S COLOUR, WHICH THE DESIGN DOES NOT DECLARE AND THE BROWSER
   DOES. Measured in the pane before this rule existed: the user-agent default
   is `rgb(117, 117, 117)`, and over this field — `rgba(255,255,255,0.08)`
   composited on the navy card, which resolves to `rgb(22, 36, 74)` — that is
   **3.29:1**, below WCAG AA's 4.5:1 for text. docs/ui-rules.md (Dylan,
   Oct 27, 2026) requires AA and requires a translucent surface to be measured
   against the backdrop a visitor can really put behind it, which is this one.

   This is not inventing a colour to compensate for the brand palette, which
   that rule forbids: the design declares no placeholder colour at all, so
   there is nothing of its to override, and the value used is the card's OWN
   dimmest text colour — #A7B6D3, which design/Progress.dc.html gives the
   "Design mockup · the form isn't live yet" line in the same card. It stays
   clearly dimmer than the typed value (#FFFFFF, measured at 15.15:1 over the
   same field), so a placeholder still reads as a prompt rather than as an
   entered address.

   OWED BY THE GET IN TOUCH PAGE (task R7): that form's fields sit on a light
   surface rather than this navy one, so they need their own measurement — the
   same default grey passes over white and fails here. This rule is
   deliberately scoped to this one input rather than written as a shared
   `input::placeholder`, so that page has to measure rather than inherit. */
.pg-sub__input::placeholder {
	color: #a7b6d3;
	opacity: 1;
}

.pg-sub__btn {
	flex: 0 0 auto;
	min-height: 56px;
	padding: 0 24px;
	border: 0;
	border-radius: 16px;
	background: var(--blue);
	color: #ffffff;
	font-size: 17px;
	font-weight: 800;
	box-shadow: 0 10px 24px rgba(4, 97, 248, 0.38), 0 2px 6px rgba(2, 17, 58, 0.18);
}
.pg-sub__btn:hover {
	box-shadow: 0 14px 30px rgba(4, 97, 248, 0.45), 0 2px 6px rgba(2, 17, 58, 0.18);
}

/* The design's bad-address state: `fieldLine` turns #FF8A7A when the
   message below says the address is wrong. Keyed to the attribute the script
   already sets for assistive technology, so the colour and what a screen
   reader is told cannot disagree. */
.pg-sub__input[aria-invalid="true"] {
	border-color: #ff8a7a;
}

/* Empty until there is something to say, and it keeps its line so nothing
   moves when a message is written here. */
.pg-sub__msg {
	min-height: 22px;
	font-size: 15px;
	font-weight: 700;
	color: #ffb3a8;
}

/* Turnstile. Interaction-only, so for an ordinary visitor the container is
   empty, but it is still a flex item and the field group's 10px gap would
   open between the label and the field, where the design draws none. The
   negative margin takes that gap back; scripts/signup.js sets
   `data-interactive` while Cloudflare is showing a challenge, and the row
   opens to fit it. The Get in touch form's `.ct-turnstile` does the same. */
.pg-sub__turnstile {
	margin-top: -10px;
}
.pg-sub__turnstile[data-interactive] {
	margin-top: 0;
}

/* The success panel, as drawn (`subDone` in design/Progress.dc.html). The
   two `hidden` overrides are needed because each class sets its own
   `display`, which beats the user agent's `[hidden] { display: none }`
   (src/html/hidden-display.test.ts). The swap and the tick's pop are the
   design's `hr-swap` and `emu-tick`, whose keyframes are declared once in
   this sheet; the reduced-motion backstop in section 25 cancels both. */
.pg-sub__done {
	display: flex;
	align-items: center;
	gap: 14px;
	padding: 18px 20px;
	border-radius: 20px;
	background: rgba(255, 255, 255, 0.08);
	border: 1px solid rgba(230, 236, 248, 0.2);
	animation: hrSwap 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.pg-sub__done[hidden] {
	display: none;
}

.pg-sub__tick {
	flex-shrink: 0;
	width: 44px;
	height: 44px;
	border-radius: 22px;
	background: var(--blue);
	color: #ffffff;
	display: flex;
	align-items: center;
	justify-content: center;
	box-shadow: 0 10px 24px rgba(4, 97, 248, 0.38);
	animation: emuTick 0.4s cubic-bezier(0.3, 1.5, 0.5, 1) both;
}

.pg-sub__done-text {
	flex: 1;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 2px;
}
.pg-sub__done-text[hidden] {
	display: none;
}

.pg-sub__done-title {
	font-size: 18px;
	font-weight: 800;
	color: #ffffff;
}
/* Focus lands on the line that just appeared, so a screen reader starts
   there; it is not a control, so the ring the design does not draw is
   suppressed for that programmatic focus only. */
.pg-sub__done-title:focus {
	outline: none;
}

.pg-sub__done-body {
	font-size: 15px;
	color: var(--on-navy-muted);
	/* An address has no spaces to wrap at. */
	overflow-wrap: anywhere;
}

.pg-sub__change {
	flex-shrink: 0;
	min-height: 44px;
	padding: 0 14px;
	border: 0;
	border-radius: 22px;
	background: transparent;
	color: var(--on-navy-link);
	font-size: 15px;
	font-weight: 800;
}

.pg-sub__foot {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	justify-content: space-between;
	gap: 8px 16px;
}

.pg-sub__linkedin {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-height: 48px;
	padding: 0 20px;
	border-radius: 24px;
	background: rgba(255, 255, 255, 0.08);
	border: 1px solid rgba(230, 236, 248, 0.28);
	color: #ffffff;
	text-decoration: none;
	font-size: 15px;
	font-weight: 800;
}
.pg-sub__linkedin:hover,
.pg-sub__linkedin:focus-visible {
	color: #ffffff;
}
/* The confirm states draw no LinkedIn row (design/Progress-Confirm.dc.html),
   and the `display` above would otherwise beat the attribute. */
.pg-sub__foot[hidden] {
	display: none;
}

/* -----------------------------------------------------------------------------
   29.5b Get updates — the confirm states
   -----------------------------------------------------------------------------
   design/Progress-Confirm.dc.html draws the same navy card when the link in
   the confirmation email opens this page: "Confirm your email" with the
   address, the spam check and a Confirm button; then "Thanks for
   confirming."; or "That link has expired." with the field again.
   public/scripts/signup.js sets `data-cf` on the section for as long as the
   card is in one of those states, and moves between them by `hidden` and
   `aria-disabled`, never by a style.
   -------------------------------------------------------------------------- */

/* That drawing's card differs from Progress.dc.html's in three figures — 24px
   rather than 28px of minimum padding, and columns that begin at 320px and
   360px rather than 340px and 380px — and its lead may break inside a word.
   Taken as drawn, for the states it draws. */
.pg-sub-sec[data-cf] .pg-sub {
	padding: clamp(24px, 5vw, 56px);
}
.pg-sub-sec[data-cf] .pg-sub__copy {
	flex-basis: 320px;
}
.pg-sub-sec[data-cf] .pg-sub__side {
	flex-basis: 360px;
}
.pg-sub-sec[data-cf] .pg-sub__lead {
	overflow-wrap: anywhere;
}
/* The expired state draws the field and Notify me with no message line under
   them. The line keeps its reserved height on the idle card, where the design
   draws it; here it takes no room until it has something to say, and the
   negative margin takes back the field group's 10px gap, as Turnstile's does. */
.pg-sub-sec[data-cf] .pg-sub__msg:empty {
	min-height: 0;
	margin-top: -10px;
}

.pg-cf {
	display: flex;
	flex-direction: column;
	gap: 12px;
}
.pg-cf[hidden] {
	display: none;
}

/* "Confirming" and the link's address. */
.pg-cf__who {
	display: flex;
	flex-direction: column;
	gap: 2px;
	padding: 14px 16px;
	border-radius: 16px;
	background: rgba(255, 255, 255, 0.08);
	border: 1px solid rgba(230, 236, 248, 0.2);
}
.pg-cf__who[hidden] {
	display: none;
}
.pg-cf__who-label {
	font-size: 13px;
	font-weight: 700;
	color: #a7b6d3;
}
.pg-cf__who-email {
	font-size: 18px;
	font-weight: 800;
	color: #ffffff;
	/* An address has no spaces to wrap at. */
	overflow-wrap: anywhere;
}

/* The spam check, as drawn: a white row with a box that fills blue and takes
   a tick once Turnstile has answered (`data-checked`, set by the script). */
.pg-cf__check {
	display: flex;
	align-items: center;
	gap: 12px;
	min-height: 60px;
	padding: 0 14px;
	box-sizing: border-box;
	border-radius: 14px;
	background: #ffffff;
	color: var(--navy);
}
.pg-cf__check[hidden] {
	display: none;
}
.pg-cf__box {
	flex-shrink: 0;
	width: 26px;
	height: 26px;
	box-sizing: border-box;
	border-radius: 6px;
	border: 2px solid #8e9cb8;
	background: #ffffff;
	display: flex;
	align-items: center;
	justify-content: center;
	color: #ffffff;
}
.pg-cf__tick {
	visibility: hidden;
}
.pg-cf__check[data-checked] .pg-cf__box {
	border-color: var(--blue);
	background: var(--blue);
}
.pg-cf__check[data-checked] .pg-cf__tick {
	visibility: visible;
}
.pg-cf__check-text {
	flex: 1;
	font-size: 15px;
	font-weight: 700;
}
.pg-cf__check-label {
	font-size: 11px;
	font-weight: 700;
	color: #3d4b68;
	text-align: right;
	line-height: 1.2;
}

/* Turnstile, interaction-only: empty for an ordinary visitor, so the negative
   margin takes back the panel's 12px gap; the script sets `data-interactive`
   while Cloudflare is showing a challenge, and the row opens to fit it. The
   same arrangement as `.pg-sub__turnstile` above. */
.pg-cf__turnstile {
	margin-top: -12px;
}
.pg-cf__turnstile[data-interactive] {
	margin-top: 0;
}

/* Confirm: Notify me's button (`.pg-sub__btn`) laid out as the design draws
   this one, with its spinner beside the label. Dimmed while it cannot be
   pressed — before the spam check has passed, and while confirming — keyed
   to the attribute assistive technology reads, so the two cannot disagree. */
.pg-cf__btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 10px;
}
.pg-cf__btn[aria-disabled="true"] {
	opacity: 0.6;
}
/* The design draws `pvSpin .8s linear infinite`. A loop that never ends is
   not on docs/ui-rules.md's list of approved loops, so it runs a fixed 25
   turns (20 s), far longer than a confirmation takes; whether it should loop
   for ever is Dylan's to approve by name. The reduced-motion backstop in
   section 25 stops it, which leaves the ring standing still. */
.pg-cf__spin {
	width: 18px;
	height: 18px;
	box-sizing: border-box;
	border-radius: 9px;
	border: 3px solid rgba(255, 255, 255, 0.35);
	border-top-color: #ffffff;
	animation: pvSpin 0.8s linear 25;
}
@keyframes pvSpin {
	to {
		transform: rotate(360deg);
	}
}

/* Empty until something goes wrong, and then the same red as the field's
   message line. The design draws nothing here, so while it is empty the
   negative margin takes back the panel's 12px gap. */
.pg-cf__msg {
	font-size: 15px;
	font-weight: 700;
	color: #ffb3a8;
}
.pg-cf__msg:empty {
	margin-top: -12px;
}

/* -----------------------------------------------------------------------------
   29.6 On to the next page
   -----------------------------------------------------------------------------
   The same CARD section 28.8 gives the How it works page, and not the same
   section: `.pg-onward__text`, `__label`, `__title` and `__arrow` are
   declaration-for-declaration identical to their `hw-` counterparts (compared
   block by block, not assumed), while the section's own top padding differs —
   design/Progress.dc.html draws 56px where design/How.dc.html draws 72px, and
   each page takes its own.

   Duplicated under this page's prefix rather than shared, because making it
   shared means editing a rule the How it works page already uses and this task
   was told not to touch that page. The two merge when the last page converts
   and the superseded shell comes out.
   -------------------------------------------------------------------------- */
.pg-onward-sec {
	padding-top: 56px;
	padding-bottom: 96px;
}

/* `:hover` IS LISTED BESIDE THE BASE STATE, and that is load-bearing rather
   than tidy — section 28.8 carries the same note for the same reason, and this
   card shipped without it for one commit. Section 3's
   `a:hover { color: #0442b0 }` is a type selector plus a pseudo-class, which
   outranks a single class, so with only the base selector here the whole
   card's text — "Next" and "About the builder" — turns brand blue on hover.
   The design draws it as `color: {{c.ink}}` with no hover colour at all.

   Found by resolving a cross-reference rather than by a measurement: the
   claim above USED to say this component was identical to section 28.8's, and
   checking that claim against the file is what showed 28.8 carried a second
   selector this did not. The CSSOM sweep cannot see it — `Element.matches()`
   answers for the element's CURRENT state, so no `:hover` rule matches an
   element nobody is hovering. */
.pg-onward,
.pg-onward:hover {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	padding: 22px 24px;
	border-radius: 22px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift-big);
	color: var(--ink);
	text-decoration: none;
}

.pg-onward__text {
	display: flex;
	flex-direction: column;
	gap: 4px;
}

.pg-onward__label {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--muted);
}

.pg-onward__title {
	font-size: clamp(22px, 2.6vw, 28px);
	font-weight: 800;
}

.pg-onward__arrow {
	flex-shrink: 0;
	width: 48px;
	height: 48px;
	border-radius: 24px;
	background: var(--blue);
	color: #ffffff;
	display: flex;
	align-items: center;
	justify-content: center;
	box-shadow: 0 10px 24px rgba(4, 97, 248, 0.38);
}

/* -----------------------------------------------------------------------------
   29.7 Motion
   -----------------------------------------------------------------------------
   Everything else on this page moves through the shared classes in section
   26.13 (`.hr-card`, `.hr-tile`, `.hr-blob`, `.hr-in`) and the scroll-driven
   reveals (`.hr-rise`).

   The whole block is cancelled by section 25's reduced-motion backstop
   (`animation: none !important`, `transition: none !important` on every
   element and pseudo-element). NO RULE BELOW HIDES ANYTHING ON ITS OWN — the
   hidden first frame lives inside `hrUp`, `hrSwap` and `hrSwapB`, not in the
   rule — so cancelling them leaves every element at its natural, visible
   position. That is the distinction the hero's 34 approach lights got wrong
   once: a rule that declares `opacity: 0` ITSELF and relies on an animation
   to undo it needs a finished state in the reduce block, and
   src/html/motion.test.ts scans both stylesheets for exactly that shape.
   -------------------------------------------------------------------------- */

/* The five stages rise in turn when the panel arrives. The design's own
   delays, `(0.15 + i * 0.07)s`, as classes rather than inline styles. */
.pg-st {
	animation: hrUp 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.pg-st--1 {
	animation-delay: 0.15s;
}
.pg-st--2 {
	animation-delay: 0.22s;
}
.pg-st--3 {
	animation-delay: 0.29s;
}
.pg-st--4 {
	animation-delay: 0.36s;
}
.pg-st--5 {
	animation-delay: 0.43s;
}

/* The indicator slides between filters. A transition and not an animation, so
   under reduced motion it simply jumps to the new position — which is the
   finished state, immediately. */
.pg-ind {
	transition: transform 0.32s cubic-bezier(0.3, 0.7, 0.3, 1);
}

/* The log replays its entrance when the filter changes. Two names doing the
   same thing, alternated by the script: re-adding a class that is already
   present does not restart a CSS animation, and swapping between two does.
   The design's own A/B mechanism (`listCls` keyed off `listN`), and the same
   one section 27.6 reproduces for the "Try the app" caption. */
.pg-list--a {
	animation: hrSwap 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.pg-list--b {
	animation: hrSwapB 0.45s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
@keyframes hrSwapB {
	from {
		opacity: 0;
		transform: translateY(10px);
	}
	to {
		opacity: 1;
		transform: none;
	}
}

/* The log entry's dot pops as it arrives, where the browser supports
   scroll-driven animation. Its own `@supports`/`no-preference` pair rather
   than an edit to section 26.13's, so no rule another page uses changes. The
   design puts `hr-dot` on this element, which is the same `hrDot` keyframe
   Home's timeline dots use. */
@supports (animation-timeline: view()) {
	@media (prefers-reduced-motion: no-preference) {
		.pg-rail__dot {
			animation: hrDot linear both;
			animation-timeline: view();
			animation-range: entry 30% entry 70%;
		}
	}
}

/* Press feedback, the same shape section 26.13 gives the header links and the
   calls to action. Declared here rather than appended to that list so this
   task changes no shared rule. */
.pg-next__cta,
.pg-seg__btn,
.pg-more__btn,
.pg-sub__btn,
.pg-sub__change,
.pg-sub__linkedin,
.pg-onward {
	transition: transform 0.14s ease-out, box-shadow 0.2s ease-out,
		background-color 0.2s ease-out;
}
.pg-next__cta:active,
.pg-seg__btn:active,
.pg-more__btn:active,
.pg-sub__btn:active,
.pg-sub__change:active,
.pg-sub__linkedin:active,
.pg-onward:active {
	transform: scale(0.97);
}

/* -----------------------------------------------------------------------------
   29.8 Narrow widths — the design's own 900 and 480
   -------------------------------------------------------------------------- */
@media (max-width: 900px) {
	/* The five stages stop being columns and become a vertical rail. */
	.pg-track {
		grid-template-columns: 1fr;
		gap: 0;
	}
	.pg-stage {
		flex-direction: row;
		gap: 16px;
		padding-bottom: 24px;
	}
	.pg-stage:last-child {
		padding-bottom: 0;
	}
	.pg-link {
		top: 46px;
		bottom: 6px;
		left: 19px;
		right: auto;
		width: 2px;
		height: auto;
	}
	.pg-dash {
		background: repeating-linear-gradient(
			to bottom,
			var(--lk) 0 6px,
			transparent 6px 12px
		);
	}
}

@media (max-width: 480px) {
	/* Four labels in 440px minus padding is tight once the row is at the
	   viewport's width; the design shrinks the type rather than wrapping. */
	.pg-seg__btn {
		font-size: 13px;
		padding: 0 2px;
	}
}

/* =============================================================================
   30. About — the Oct 27 redesign
   =============================================================================
   design/About.dc.html and its five siblings (task R6). The six differ only in
   the canvas preview size and the default theme, so every value here is
   About.dc.html's and holds for all six.

   What this page shares rather than redeclares: `.page`, `.hdr*` and the
   small-screen menu (26.1-26.4), `.headline`, `.eyebrow`, `.eyebrow-pill` and
   `.cta--shine` (26.5 and 26.13), `.page-hero` with its glows, title and lead
   (26.6), the footer (26.12), the card/tile/blob transitions and the
   scroll-driven reveals (26.13), and the responsive steps at 900, 620 and 560
   (26.14). Section 19 held the pre-redesign version of this page and has been
   retired; its note says what went and why.

   SIZES HERE ARE COMPONENT-LOCAL AND THE DESIGN'S OWN, except the headings.
   Every heading element on the page carries a shared scale class and nothing
   from this section re-declares its type (docs/ui-rules.md, Dylan, Oct 28,
   2026: a heading takes the shared scale wherever it sits, including inside a
   card). The founder card's `<h2>` takes the scale's card step 4,
   `.t-h2 .t-h2--card-xl`, which is the clamp(28px, 3.4vw, 40px) the design
   draws (docs/ui-rules.md, "Card headings match their drawings"). Until that
   step existed it carried `.t-h2` alone and rendered at the 48px section
   step, larger than its drawing. src/html/about.test.ts holds the heading to
   the step and the step to the drawing.

   THE DESIGN DRAWS NO DEPICTED APP SCREEN on this page, so none of the 44px
   exemptions in docs/ui-rules.md apply to anything here: every control is
   measured as rendered. The page's own controls are the two founder calls to
   action at 52px and two card-shaped links far taller than that; the rest are
   the shared shell's.
   -------------------------------------------------------------------------- */

/* -----------------------------------------------------------------------------
   30.1 The hero
   -----------------------------------------------------------------------------
   `.page-hero` carries the shared part (position, navy, ink, its own clip);
   these carry what this hero does differently. One column, an 88px foot where
   How it works has 96px and Home 120px, and nothing beside or beneath the
   copy — the founder card below is what fills that space, lifted over the
   hero's bottom edge.
   -------------------------------------------------------------------------- */

/* The two soft lights. `.hr-blob` (section 26.13) already gives them their
   shape, blur, opacity and the approved `hrDrift` loop; only the size,
   position and tint differ from the other heroes', so only those are here. */
.ab-hero__blob--a {
	width: 520px;
	height: 520px;
	right: -140px;
	top: -140px;
	background: rgba(4, 97, 248, 0.5);
}
.ab-hero__blob--b {
	width: 360px;
	height: 360px;
	left: -120px;
	bottom: -180px;
	background: rgba(4, 165, 248, 0.28);
	animation-duration: 20s;
}

.ab-hero__inner {
	position: relative;
	padding-top: 140px;
	padding-bottom: 88px;
	display: flex;
	flex-direction: column;
	gap: 18px;
}

/* The design caps the heading itself rather than the column, which is why
   this is on the `<h1>` and How it works' equivalent is on its copy div. A
   max-width is not a type property, so it does not touch the shared scale. */
.ab-hero__title {
	max-width: 860px;
}

/* The design's standfirst runs to 620px here where Home's stops at 540px, the
   same figure How it works uses. A descendant of `.ab-hero` rather than a
   class of its own, so the markup carries one class and `.page-hero__lead`
   supplies everything else. */
.ab-hero .page-hero__lead {
	max-width: 620px;
}

/* -----------------------------------------------------------------------------
   30.2 The founder band
   -----------------------------------------------------------------------------
   NOT section 26.11's `.founder`, which is Home's: that one is a navy card
   with a fixed 120px photo and a paragraph of prose. This one is a surface
   card lifted 40px over the hero's bottom edge, with a photo that scales with
   the viewport, three facts as chips and two calls to action. Two components
   that look alike and are not the same, so this page carries its own rather
   than bending Home's with modifiers.

   No `.sec`: the design gives this section a negative top margin instead of
   section padding, so the card overlaps the hero.
   -------------------------------------------------------------------------- */
.ab-founder-sec {
	position: relative;
	z-index: 3;
	margin-top: -40px;
}

.ab-founder {
	box-sizing: border-box;
	padding: clamp(20px, 3.4vw, 36px);
	border-radius: 28px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift-big);
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 24px 40px;
}

/* The design's `.ab-photo`: a positioning context around the photo with no
   size of its own, so the image decides the column's width. */
.ab-founder__frame {
	flex: 0 0 auto;
	position: relative;
}

/* A square that scales between 140px and 200px, cropped to fill rather than
   letterboxed. `width` and `height` are also on the element as attributes, so
   the box is reserved before the file arrives and nothing below it jumps. */
.ab-founder__photo {
	box-sizing: content-box; /* the design canvas's sizing: see section 3 */
	display: block;
	width: clamp(140px, 18vw, 200px);
	height: auto;
	aspect-ratio: 1;
	object-fit: cover;
	border-radius: 28px;
	border: 3px solid var(--sky);
	box-shadow: var(--lift);
}

.ab-founder__body {
	flex: 1 1 380px;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 14px;
}

.ab-founder__head {
	display: flex;
	flex-direction: column;
	gap: 2px;
}

.ab-founder__label {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--muted);
}

/* "Flagger for two years / Airfield construction / Taxiway crossings". */
.ab-facts {
	margin: 0;
	padding: 0;
	list-style: none;
	display: flex;
	flex-wrap: wrap;
	gap: 8px;
}

.ab-fact {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	min-height: 34px;
	padding: 0 12px;
	border-radius: 17px;
	background: var(--chip);
	color: var(--ink);
	font-size: 14px;
	font-weight: 800;
}

/* The design strokes this icon with `{{c.accent}}`. An SVG presentation
   attribute cannot resolve `var()` (apps/site/README.md item 5), so the
   markup strokes `currentColor` and the colour is set here. */
.ab-fact__icon {
	flex-shrink: 0;
	color: var(--accent);
}

/* The design's own row: 4px of top padding, where the shared `.cta-row` has
   6px. Its own class rather than `.cta-row` plus an override, because
   `.page .cta-row` is two classes and would outrank a single-class fix. */
.ab-founder__actions {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 12px;
	padding-top: 4px;
}

/* The two calls to action. Both 52px, which is above the 44px floor
   docs/ui-rules.md sets, and both the design's own shape — taller and rounder
   than the pre-redesign `.btn`, and not the shell's `.cta` either, which is
   56px with 24px of side padding and a 17px label. */
.ab-cta {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-height: 52px;
	text-decoration: none;
	font-size: 16px;
	font-weight: 800;
}

/* `:hover` beside the base state for `color`, and that is load-bearing:
   section 3's `a:hover { color: #0442b0 }` is a type selector plus a
   pseudo-class (0-1-1) and outranks a single class, so without it the white
   label would turn brand blue on the brand-blue fill. The design draws no
   hover colour on either of these. src/html/about.test.ts guards it, because
   the CSSOM sweep cannot see a `:hover` rule at all. */
.ab-cta--primary,
.ab-cta--primary:hover {
	padding: 0 22px;
	border-radius: 16px;
	background: var(--blue);
	color: #ffffff;
}
.ab-cta--primary {
	box-shadow: 0 10px 24px rgba(4, 97, 248, 0.38),
		0 2px 6px rgba(2, 17, 58, 0.18);
}
.ab-cta--primary:hover {
	box-shadow: 0 14px 30px rgba(4, 97, 248, 0.45),
		0 2px 6px rgba(2, 17, 58, 0.18);
}

.ab-cta--quiet,
.ab-cta--quiet:hover {
	padding: 0 20px;
	border-radius: 26px;
	background: var(--chip);
	color: var(--accent);
}

/* -----------------------------------------------------------------------------
   30.3 The two stacked sections, and what is inside them
   -----------------------------------------------------------------------------
   "Why I am building this" and "How I am building it" share a shape — a
   headline block above a group — with the same 28px gap and a slightly
   different headline measure.
   -------------------------------------------------------------------------- */
.ab-stack {
	display: flex;
	flex-direction: column;
	gap: 28px;
}

.ab-stack__head {
	max-width: 760px;
}
/* The design draws the second one narrower, which is also `.headline`'s own
   default — declared anyway so this file carries the design's figure instead
   of inheriting it silently. */
.ab-stack__head--how {
	max-width: 720px;
}

/* The three cards: the noise, the gap, the idea. */
.ab-story {
	margin: 0;
	padding: 0;
	list-style: none;
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
	gap: 16px;
}

.ab-story__item {
	display: flex;
	flex-direction: column;
	gap: 12px;
	padding: 24px;
	border-radius: 22px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift);
}

.ab-story__top {
	display: flex;
	align-items: center;
	justify-content: space-between;
}

.ab-story__tile {
	flex-shrink: 0;
	width: 48px;
	height: 48px;
	border-radius: 14px;
	background: var(--chip);
	color: var(--accent);
	display: flex;
	align-items: center;
	justify-content: center;
}

.ab-story__kind {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--muted);
}

.ab-story__title {
	font-size: 20px;
	line-height: 1.25;
	font-weight: 800;
}

.ab-story__body {
	font-size: 16px;
	line-height: 1.6;
	color: var(--muted);
}

/* One card holding three rows, divided by a hairline. */
.ab-how {
	margin: 0;
	box-sizing: border-box;
	border-radius: 24px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift);
	overflow: hidden;
}

/* A grid rather than the design's flex row, because a `<dl>` row is three
   elements (the icon, the `<dt>` and the `<dd>`) and not two: the icon spans
   both text rows in the first column. A `<span>` wrapping the pair would be
   invalid inside a `<dl>` and the parser would move it. The rendering is the
   design's — icon, 16px gap, then the name with its sentence 4px under it. */
.ab-how__row {
	display: grid;
	grid-template-columns: auto minmax(0, 1fr);
	column-gap: 16px;
	row-gap: 4px;
	align-items: start;
	padding: 20px 24px;
	border-top: 1px solid var(--line);
}
/* The design's `{{h.sep}}` is `0` on the first item and a hairline on the
   other two. Same rendering, with nothing to keep in step per row. */
.ab-how__row:first-child {
	border-top: 0;
}

.ab-how__tile {
	grid-row: span 2;
	width: 40px;
	height: 40px;
	border-radius: 12px;
	background: var(--chip);
	color: var(--accent);
	display: flex;
	align-items: center;
	justify-content: center;
}

.ab-how__title {
	grid-column: 2;
	font-size: 18px;
	font-weight: 800;
}

.ab-how__body {
	grid-column: 2;
	margin: 0;
	font-size: 16px;
	line-height: 1.55;
	color: var(--muted);
}

/* -----------------------------------------------------------------------------
   30.4 Where things stand
   -----------------------------------------------------------------------------
   One card-shaped link to the Progress page, so the whole card is the target
   rather than a word inside it.

   `:hover` is listed BESIDE the base state for the same reason sections 28.8
   and 29.6 do it: section 3's `a:hover { color: #0442b0 }` is a type selector
   plus a pseudo-class, which outranks a single class, so without the second
   selector the card's title and the text inheriting from it turn brand blue
   on hover. design/About.dc.html draws `color: {{c.ink}}` and no hover colour
   at all.
   -------------------------------------------------------------------------- */
.ab-stand,
.ab-stand:hover {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 16px 20px;
	padding: 22px 24px;
	border-radius: 22px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift);
	color: var(--ink);
	text-decoration: none;
}

/* The design's `.hr-next`: a chip-filled disc with a ring expanding out of
   its centre and a dot breathing inside it. Both loops are in section 30.6.
   It depicts nothing — no alert, no aircraft, no decision. */
.ab-stand__dot {
	position: relative;
	flex-shrink: 0;
	width: 48px;
	height: 48px;
	border-radius: 24px;
	background: var(--chip);
	color: var(--muted);
}

.ab-stand__text {
	flex: 1 1 320px;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 4px;
}

.ab-stand__label {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--muted);
}

.ab-stand__title {
	font-size: 18px;
	line-height: 1.35;
	font-weight: 800;
}

.ab-stand__body {
	font-size: 15px;
	line-height: 1.5;
	color: var(--muted);
}

.ab-stand__more {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	font-size: 15px;
	font-weight: 800;
	color: var(--accent);
}

/* -----------------------------------------------------------------------------
   30.5 On to the next page
   -----------------------------------------------------------------------------
   The same card the How it works and Progress pages end on, with this page's
   own section padding: design/About.dc.html draws 56px above and 96px below,
   where design/How.dc.html draws 72px above. `:hover` is beside the base
   state for the reason in 30.4.
   -------------------------------------------------------------------------- */
.ab-onward-sec {
	padding-top: 56px;
	padding-bottom: 96px;
}

.ab-onward,
.ab-onward:hover {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 16px;
	padding: 22px 24px;
	border-radius: 22px;
	background: var(--surface);
	border: 1px solid var(--line);
	box-shadow: var(--lift-big);
	color: var(--ink);
	text-decoration: none;
}

.ab-onward__text {
	display: flex;
	flex-direction: column;
	gap: 4px;
}

.ab-onward__label {
	font-size: 13px;
	font-weight: 800;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--muted);
}

.ab-onward__title {
	font-size: clamp(22px, 2.6vw, 28px);
	font-weight: 800;
}

.ab-onward__arrow {
	flex-shrink: 0;
	width: 48px;
	height: 48px;
	border-radius: 24px;
	background: var(--blue);
	color: #ffffff;
	display: flex;
	align-items: center;
	justify-content: center;
	box-shadow: 0 10px 24px rgba(4, 97, 248, 0.38);
}

/* -----------------------------------------------------------------------------
   30.6 Motion
   -----------------------------------------------------------------------------
   Everything this page moves on its own, all of it the designs'. Everything
   else moves through the shared classes in section 26.13 (`.hr-card`,
   `.hr-tile`, `.hr-blob`, `.hr-in`, `.cta--shine`) and the scroll-driven
   reveals (`.hr-rise`, `.hr-pop`).

   The whole block is cancelled by section 25's reduced-motion backstop, which
   is `animation: none !important` on every element and pseudo-element.
   Nothing here starts from a hidden state that only an animation can undo, so
   the backstop alone leaves the finished page: the two loops below are
   decoration on an element that is already painted, which is why this section
   needs no `prefers-reduced-motion: reduce` opacity rescue of the kind
   section 26.13 gives `.hr-light`.
   -------------------------------------------------------------------------- */

/* APPROVED LOOPS 3 and 4 of 4 — hrPing and hrBreath, numbered as sections
   26.13, 28.7 and 29 already number them. docs/ui-rules.md: Dylan approved
   these four by name, and src/html/motion.test.ts allows exactly four and
   fails on a fifth. The keyframes are section 26.13's; only the element they
   are attached to is this page's.

   The same pair is on Home's "Up next" timeline dot and the Progress page's
   "Up next" stage dot. The How it works page's "Planned, not built yet" ring
   takes hrPing ALONE — its design draws no breathing centre dot — so this
   page's dot is the Home/Progress shape and not that one. */
.ab-stand__dot::after {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: 16px;
	height: 16px;
	margin: -8px 0 0 -8px;
	border-radius: 50%;
	border: 2px solid currentColor;
	box-sizing: border-box;
	animation: hrPing 2.4s ease-out infinite;
}
.ab-stand__dot::before {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: 6px;
	height: 6px;
	margin: -3px 0 0 -3px;
	border-radius: 50%;
	background: currentColor;
	animation: hrBreath 2.4s ease-in-out infinite;
}

/* Press feedback, the same shape section 26.13 gives the header links and the
   calls to action, and the same shape section 29.7 declares for the Progress
   page's controls. Declared here rather than appended to that list so this
   task changes no shared rule. These four are the elements
   design/About.dc.html marks `hr-press` that are not the shared shell's. */
.ab-cta--primary,
.ab-cta--quiet,
.ab-stand,
.ab-onward {
	transition: transform 0.14s ease-out, box-shadow 0.2s ease-out,
		background-color 0.2s ease-out;
}
.ab-cta--primary:active,
.ab-cta--quiet:active,
.ab-stand:active,
.ab-onward:active {
	transform: scale(0.97);
}

/* =============================================================================
   Updates from the database (the /post work, Oct 28, 2026)
   -----------------------------------------------------------------------------
   Since the updates are posted from /post and read from Postgres, three things
   can now appear in the Home and Progress lists that the markdown never had.
   ========================================================================== */

/* An update's text may be one or two short paragraphs (design/Post-Updates:
   "One or two short paragraphs"), so a line break Dylan typed is kept. */
.pg-body,
.tl-text {
	white-space: pre-line;
	overflow-wrap: anywhere;
}

/* An update's photo, as design/Post-Updates.dc.html's "How it will look on
   Progress" preview draws it: under the text, full width, 120px tall,
   cropped to fill, 12px corners, 6px above it. */
.pg-photo {
	display: block;
	width: 100%;
	height: 120px;
	margin-top: 6px;
	object-fit: cover;
	border-radius: 12px;
}

/* When the updates cannot be read: one honest line where the list would be.
   The dashed route border and muted ink are the design's own "No updates
   yet" treatment (design/Post-Updates.dc.html), the nearest state it draws. */
.pg-unavailable,
.tl-item--unavailable {
	list-style: none;
	padding: 18px 20px;
	border-radius: 20px;
	border: 2px dashed var(--route);
	color: var(--muted);
	font-size: 16px;
	line-height: 1.5;
}
.page .tl-item--unavailable,
.tl-item--unavailable {
	display: block;
}
.tl-item--unavailable .tl-body {
	padding-bottom: 0;
}
