/* Design tokens — Ember palette (single theme, dark default, auto light).
   Fonts imported via Google Fonts link in base.html. */

/* ═══════════════════════════════════════════════════════════════════════════
   EMBER PRIMITIVES (dark)
   Named source of truth. Mirrors ~/.config/theme/palettes/ember-dark.yaml so
   the app stays in sync with the system palette (GTK, Dunst, Qt6, Ghostty).
   Every semantic token below derives from these.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
	/* App-wide because the Steev face wears its coral disc on EVERY page,
	   not only the coach's — these two are read by `.nav-brand-blob` in
	   navigation.css, which is global chrome. Scheme-independent: coral is
	   one value in both schemes, so its ink is too. */
	--radius-blob: 50% 46% 52% 48%;
	--paper-ink-on-coral: #2c241c;
	--ember-base: #1c1b19;
	--ember-surface: #242320;
	--ember-overlay: #2e2d2a;
	--ember-text: #d8d0c0;

	--ember-coral: #e08060;
	/* coach / intense */
	--ember-orange: #c09058;
	/* accent */
	--ember-gold: #c8b468;
	/* caution / moderate */
	--ember-olive: #8a9868;
	/* success */
	--ember-sage: #80a090;
	/* user / recovery */
	--ember-steel: #7890a0;
	/* action */
	--ember-rose: #b07878;
	/* danger */
	--ember-mauve: #988090;
	/* strength */
}

/* ═══════════════════════════════════════════════════════════════════════════
   SEMANTIC TOKENS (dark)
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
	/* Surfaces */
	--surface: var(--ember-base);
	--surface-raised: var(--ember-surface);
	--surface-raised-hover: var(--ember-overlay);
	--surface-sunken: var(--ember-base);
	--surface-sunken-focus: var(--ember-surface);
	--surface-overlay: var(--ember-overlay);

	/* Text */
	--text: var(--ember-text);
	--text-secondary: #b8b0a0;
	--text-muted: #a09888;
	--text-faint: #7a7268;
	--text-bright: #e8e0d0;
	/* ONE value, both schemes: an accent surface that carries text wears an
	   --accent-*-strong fill (below), and every strong fill is picked to hold
	   white. Ink never darkens to rescue a fill — the fill darkens. */
	--text-on-accent: #ffffff;

	/* Borders */
	--border: #3e3c38;
	--border-strong: #585550;

	/* Neutral interactive */
	--neutral: #8a8578;

	/* Shadow */
	--shadow: rgba(0, 0, 0, 0.5);

	/* Accent base — aliased directly to primitives */
	--accent-coach: var(--ember-coral);
	--accent-user: var(--ember-sage);
	--accent-action: var(--ember-steel);
	--accent-success: var(--ember-olive);
	--accent-caution: var(--ember-gold);
	--accent-danger: var(--ember-rose);
	--accent-strength: var(--ember-mauve);
	--accent-accent: var(--ember-orange);

	/* Role aliases that share a primitive with a sibling */
	--accent-intense: var(--ember-coral);
	/* ≡ coach */
	--accent-recovery: var(--ember-sage);
	/* ≡ user  */
	--accent-moderate: var(--ember-gold);
	/* ≡ caution */
	--accent-info: var(--ember-mauve);
	/* coach voice ≡ strength (mauve) — Steev speaking, distinct from coral structure */
	--accent-moment: var(--ember-gold);
	--moment-wash: color-mix(in srgb, var(--ember-gold) 10%, transparent);
	/* celebration (Phase-5 hero cards) — gold, distinct from every card accent; NOT coach voice */

	/* Text in a role's hue. Here it is the alias; the light block swaps in an ink
	   that holds on cream, where the bright fills measure 1.6–3:1. */
	--accent-coach-text: var(--accent-coach);
	--accent-intense-text: var(--accent-intense);
	--accent-user-text: var(--accent-user);
	--accent-recovery-text: var(--accent-recovery);
	--accent-action-text: var(--accent-action);
	--accent-success-text: var(--accent-success);
	--accent-caution-text: var(--accent-caution);
	--accent-moderate-text: var(--accent-moderate);
	--accent-danger-text: var(--accent-danger);
	--accent-strength-text: var(--accent-strength);
	--accent-accent-text: var(--accent-accent);
	--accent-info-text: var(--accent-info);
	--accent-moment-text: var(--accent-moment);

	/* Ink-carrying fills. A bright primitive cannot hold white: coral measures
	   2.83:1, steel 3.34:1, sage 2.86:1, olive 3.03:1, rose 3.62:1. Every
	   accent surface that CARRIES TEXT takes the strong twin (4.7:1 – 6.6:1);
	   the primitives stay bright for tints, rules, rings, marks and the face
	   blob. Scheme-independent on purpose — these ARE the light scheme's
	   primitives, so light-mode buttons render exactly as before. */
	--accent-coach-strong: #b85820;
	--accent-coach-strong-hover: #9a4618;
	/* The other end of the same scale: the face blob's ground, which carries
	   DARK ink, so it lightens where the fills darken. Coral mixed toward the
	   paper cream — 6.9:1 under --paper-ink-on-coral, up from 5.4:1. */
	--accent-coach-soft: #e69c82;
	--accent-action-strong: #3a6080;
	--accent-user-strong: #386858;
	--accent-user-strong-hover: #2c5246;
	--accent-success-strong: #4a6830;
	--accent-success-strong-hover: #3a5426;
	--accent-danger-strong: #905050;
	--accent-danger-strong-hover: #744040;

	/* Hover — hand-tuned deep-of-primitive (not pure black-mix: preserves warmth) */
	--accent-coach-hover: #c06840;
	--accent-user-hover: #648070;

	/* Faint tint (20% on transparent) — derived from primitive */
	--accent-coach-faint: color-mix(in srgb, var(--ember-coral) 20%, transparent);
	--accent-user-faint: color-mix(in srgb, var(--ember-sage) 20%, transparent);
	--accent-action-faint: color-mix(in srgb, var(--ember-steel) 20%, transparent);
	--accent-success-faint: color-mix(in srgb, var(--ember-olive) 20%, transparent);
	--accent-caution-faint: color-mix(in srgb, var(--ember-gold) 20%, transparent);
	--accent-danger-faint: color-mix(in srgb, var(--ember-rose) 20%, transparent);
	--accent-strength-faint: color-mix(in srgb, var(--ember-mauve) 20%, transparent);
	--accent-intense-faint: color-mix(in srgb, var(--ember-coral) 20%, transparent);
	--accent-recovery-faint: color-mix(in srgb, var(--ember-sage) 20%, transparent);
	--accent-moderate-faint: color-mix(in srgb, var(--ember-gold) 20%, transparent);
	--accent-info-faint: color-mix(in srgb, var(--ember-mauve) 20%, transparent);

	/* Tag-text — hand-tuned for contrast on same-hue faint tint */
	--tag-text-coach: #f0a080;
	--tag-text-user: #a0c0b0;
	--tag-text-action: #a0b0c0;
	--tag-text-success: #a8b888;
	--tag-text-caution: #d8c488;
	--tag-text-danger: #c89898;
	--tag-text-strength: #b8a0b0;
	--tag-text-recovery: #a0c0b0;
	--tag-text-moderate: #d8c488;
	--tag-text-info: #b8a0b0;

	color-scheme: dark;

	/* ── Font tokens ── */
	--font-wordmark: "Bricolage Grotesque", system-ui, sans-serif;
	--font-display: "Bricolage Grotesque", system-ui, sans-serif;
	--font-heading: "Bricolage Grotesque", system-ui, sans-serif;
	--font-body: "Albert Sans", system-ui, sans-serif;
	--font-coach: "Albert Sans", system-ui, sans-serif;
	--font-mono: "IBM Plex Mono", ui-monospace, monospace;
	--font-input: "Albert Sans", system-ui, sans-serif;
	--font-annotation: "Caveat", cursive;

	/* Typography adjustments */
	--body-letter-spacing: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   LAYOUT / TYPOGRAPHY / MOTION TOKENS
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
	/* Layout */
	--content-max-width: 800px;

	/* Stacking: the sticky nav, and the one-column node sheet over it. */
	--z-nav: 100;
	--z-sheet: 110;

	--space-xs: 0.25rem;
	--space-sm: 0.5rem;
	--space-md: 1rem;
	--space-lg: 1.5rem;
	--space-xl: 2rem;
	--space-2xl: 3rem;

	/* ── Fluid horizontal insets (viewport-based, 320px→800px) ──
     Use for ALL horizontal padding and horizontal gaps.
     --space-* remain for vertical rhythm and margin-bottom.
     Slope unchanged from the original 360→800 design; floors lowered
     so iPhone-SE-class widths (320px) still resolve via the formula
     instead of clamping above the available gutter. */
	--inset-sm: clamp(0.25rem, 0.2rem + 0.8vw, 0.5rem);
	--inset: clamp(0.375rem, 0.1rem + 1.8vw, 1rem);
	--inset-lg: clamp(0.5rem, 0.1rem + 2.9vw, 1.5rem);

	--text-2xs: 0.7rem;
	--text-xs: 0.8rem;
	--text-sm: 0.9rem;
	--text-base: 1.0rem;
	--text-lg: 1.2rem;
	--text-xl: 1.6rem;
	--text-2xl: 2.0rem;
	--text-3xl: 2.5rem;

	--radius-xs: 0.125rem;
	--radius-sm: 0.375rem;
	--radius-md: 0.5rem;
	--radius-lg: 0.75rem;
	--radius-full: 9999px;

	/* Focus / active-state rule thickness — used by .form-input baseline wipe,
	   .nav-item.active underline (Stage 3), bottom-tab active rail (Stage 6). */
	--rule-focus: 2px;

	--tracking-tight: -0.01em;
	--tracking-wide: 0.04em;
	--tracking-wider: 0.08em;

	--transition-fast: 150ms ease;
	--transition-normal: 300ms ease;
	--transition-elegant: 400ms cubic-bezier(0.16, 1, 0.3, 1);
	--transition-spring: 500ms cubic-bezier(0.34, 1.56, 0.64, 1);

	--reveal-step: 200ms;

	--motion-arrive: 300ms cubic-bezier(0.16, 1, 0.3, 1);

	--shadow-sm: 0 1px 2px var(--shadow);
	--shadow-md: 0 4px 6px var(--shadow);
	--shadow-lg: 0 10px 15px var(--shadow);

	--weight-normal: 400;
	--weight-medium: 500;
	--weight-semibold: 600;
	--weight-bold: 700;

	--overlay-bg: rgba(0, 0, 0, 0.75);

	/* ── Signature primitives (Phase 1) ── */

	/* P02 — Breath Line */
	--breath-gradient: linear-gradient(to right,
			transparent 0%,
			color-mix(in srgb, var(--ember-coral) 12%, transparent) 10%,
			color-mix(in srgb, var(--ember-coral) 60%, transparent) 50%,
			color-mix(in srgb, var(--ember-coral) 12%, transparent) 90%,
			transparent 100%);

	/* P07 — Editorial Rhythm */
	--space-section: clamp(2rem, 8vh, 3rem);

	/* Shared P04 paper grain (used by .poster variants) */
	--poster-grain: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}

/* ═══════════════════════════════════════════════════════════════════════════
   TRAINING-LOG THEME — type, geometry and the paper signature tokens.
   ═══════════════════════════════════════════════════════════════════════════ */
:root {
	/* ── Scheme-independent: layout + geometry ── */
	/* Landing value; body.coach-ledger below narrows it with the window. The
	   rail's own box (ground + dashed left rule) is the seam, so the gap is 0. */
	--coach-rail-w: 372px;
	--coach-rail-gap: 0;
	/* Chat prose stops at a readable measure however wide the column gets —
	   cards and stat grids still use the full width. */
	--coach-prose-measure: 72ch;
	--coach-header-h: 63px;
	--radius-uneven-primary: 14px 17px 13px 16px;
	--radius-uneven-chip: 15px 12px 16px 13px;
	--radius-node: 50% 44% 52% 48%;
	/* Ledger (SPEC §1/§2): the rail ground, the poster's sunken stat column
	   and the day-stamp gutter. --paper-sunken is a shade off --paper-panel on
	   purpose — sharing the rail's ground made the poster's stat band read at
	   the same weight as the rail behind it (SPEC §4.3). */
	--paper-panel: var(--ember-overlay);
	--paper-sunken: var(--ember-surface);
	--ledger-gutter: 92px;
	--ledger-gap: 22px;
	--tap-target: 44px;
	/* The coach surface's own prose size, off the --text-* scale on purpose:
	   re-tuning it after the device pass must be ONE edit. iOS twin:
	   EmberText.chat. */
	--chat-prose-size: 16px;

	/* ── Signature tokens, DARK values (ember substrate) ── */
	--paper-rule: var(--border);
	--paper-rule-soft: color-mix(in srgb, var(--border) 80%, transparent);
	--paper-rule-faint: color-mix(in srgb, var(--border) 55%, transparent);
	--paper-track: var(--surface-raised);
	--paper-link: var(--accent-coach);
	--paper-ink-faint: rgba(0, 0, 0, 0.28);
	--paper-coral-deep: var(--accent-coach-hover);
	/* Marker stops are lighter on the dark substrate: at the paper weights the
	   text under them measures 3.5:1, and the mockup's tint carries emphasis
	   alone (no bold), so the tint is the only signal there is. */
	--paper-marker-from: rgba(224, 128, 96, 0.25);
	--paper-marker-to: rgba(224, 128, 96, 0.18);
	/* Pure black here where the iOS twin goes warm, on purpose: the web paints
	   no ember glow in either scheme, so there is no gold hotspot for a black
	   shadow to desaturate. Do not "converge" the hue. */
	--shadow-hard-sm: 2px 4px 0 rgba(0, 0, 0, 0.35);
	--shadow-hard-md: 3px 4px 0 rgba(0, 0, 0, 0.35);
}

/* The grain is the LEDGER's (SPEC §1): a feTurbulence layer at 3.5% multiply,
   so the paper reads textured, not lit. */
body.coach-ledger::after {
	content: "";
	position: fixed;
	inset: 0;
	pointer-events: none;
	z-index: -1;
	background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
	background-size: 200px 200px;
	opacity: var(--ledger-grain-opacity);
	mix-blend-mode: multiply;
}

body.coach-ledger,
body.coach-landing {
	/* The mockup drew 1100px, which is the CHAT's comfortable measure, not the
	   page's: at 1100 the rail is squeezed to 300px (labels like
	   "Milestone: 10.0km 5:40/km" truncate) and a wide monitor is mostly
	   gutter. The shell takes the room when it exists; the prose keeps its own
	   measure below, so the extra width goes to the rail and the cards. */
	--content-max-width: 1400px;
}

/* Ledger-only page chrome (SPEC §2.1): a 7px shorter header than the landing's.
   The grain strength is a token because an opaque patch that must pass for
   paper (the sticky day-stamp cell) paints its own grain at the same value; iOS
   twin: `GrainOverlay.ledger`. */
body.coach-ledger {
	--coach-header-h: 56px;
	--ledger-grain-opacity: 0.035;
	/* The rail shrinks with the window (SPEC §2, amended 2026-10-02): 440px from
	   ~1470px wide down to 300px at 1000px; below 1000px the one-column shell
	   takes over (coach-page.css). */
	--coach-rail-w: clamp(300px, 30vw, 440px);
}

/* The 92px date gutter pays for itself only beside a wide column: below 1100px
   (two-pane with a 300px rail, or one column) the 44px cell; the container
   query in chat-marginalia.css collapses both tokens to 0 under 34rem. */
@media (max-width: 1100px) {
	body.coach-ledger {
		--ledger-gutter: 44px;
	}
}

@media (prefers-color-scheme: light) {
	:root {
		color-scheme: light;
		--moment-wash: color-mix(in srgb, var(--accent-moment) 8%, transparent);

		/* Ember primitives on paper — dark ink values on cream */
		--ember-base: #f7f1e6;
		--ember-surface: #fffdf6;
		--ember-overlay: #fffdf6;
		--ember-text: #3a3128;
		--ember-coral: #e08060;
		--ember-orange: #c09058;
		--ember-gold: #c8b468;
		--ember-olive: #8a9868;
		--ember-sage: #80a090;
		--ember-steel: #7890a0;
		--ember-rose: #b07878;
		--ember-mauve: #988090;

		/* Surfaces */
		--surface: #f7f1e6;
		--surface-raised: #fffdf6;
		--surface-raised-hover: #f3ead8;
		--surface-sunken: #f7f1e6;
		--surface-sunken-focus: #fffdf6;
		--surface-overlay: #fffdf6;

		/* Text */
		--text: #3a3128;
		--text-secondary: #6b5c48;
		--text-muted: #8a7a64;
		--text-faint: #937f63;
		--text-bright: #2c241c;

		/* Borders */
		--border: #e0d2b8;
		--border-strong: #b8a688;

		/* Neutral */
		--neutral: #8a7a64;

		/* Shadow */
		--shadow: rgba(120, 90, 50, 0.15);

		/* Coach voice on paper: coral structure, ink prose */
		--accent-info: var(--ember-coral);
		--tag-text-info: var(--text);

		--accent-coach-text: var(--tag-text-coach);
		--accent-intense-text: var(--tag-text-coach);
		--accent-user-text: var(--tag-text-user);
		--accent-recovery-text: var(--tag-text-recovery);
		--accent-action-text: var(--tag-text-action);
		--accent-success-text: var(--tag-text-success);
		--accent-caution-text: var(--tag-text-caution);
		--accent-moderate-text: var(--tag-text-moderate);
		--accent-danger-text: var(--tag-text-danger);
		--accent-strength-text: var(--tag-text-strength);
		--accent-accent-text: #946030;
		--accent-info-text: var(--tag-text-coach);
		--accent-moment-text: var(--tag-text-caution);

		/* Hovers (paper-tuned deep-of-primitive) */
		--accent-coach-hover: #c9603d;
		--accent-user-hover: #6f8a7c;

		/* Tag text on cream */
		--tag-text-coach: #c05a34;
		--tag-text-user: #4a5a50;
		--tag-text-recovery: #4a5a50;
		--tag-text-success: #6c7a4e;
		--tag-text-caution: #8a6a10;
		--tag-text-moderate: #8a7a3a;
		--tag-text-action: #3a6080;
		--tag-text-danger: #905050;
		--tag-text-strength: #5a4c5a;

		/* Paper signature tokens */
		--paper-rule: #dcccae;
		--paper-rule-soft: #e5d5b8;
		--paper-rule-faint: #e8dcc4;
		--paper-track: #efe3cc;
		--paper-panel: #f3ead8;
		--paper-sunken: #faf5ea;
		--paper-link: #c05a34;
		--paper-ink-faint: rgba(120, 90, 50, 0.2);
		--paper-coral-deep: #d9744f;
		--paper-marker-from: rgba(224, 128, 96, 0.4);
		--paper-marker-to: rgba(224, 128, 96, 0.3);
		/* 0.08, NOT the 0.1 iOS light uses: the owner's ruling fenced this pair's
		   consumers (.chat-card, .empty-state-card, .cite-pop) as cards that must
		   not move, so the light gap with iOS stays open on purpose. */
		--shadow-hard-sm: 2px 4px 0 rgba(120, 90, 50, 0.08);
		--shadow-hard-md: 3px 4px 0 rgba(120, 90, 50, 0.08);
	}
}

*,
*::before,
*::after {
	box-sizing: border-box;
}

/* `!important` so the hidden attribute beats author `display` rules
   (e.g. a `display: grid` on the same element would otherwise win). */
[hidden] {
	display: none !important;
}

html {
	/* Root font-size scales every rem-based token in tokens.css uniformly.
     14px (~87% of browser default) lands body text at the readability
     floor — fits more content per viewport on a 13" MacBook Air at
     100% zoom. Users who want larger can browser-zoom on top. */
	font-size: 15px;
	-webkit-font-smoothing: antialiased;
	-moz-osx-font-smoothing: grayscale;
}

body {
	margin: 0;
	padding: 0;
	font-family: var(--font-body);
	background-color: var(--surface);
	color: var(--text);
	line-height: var(--body-line-height, 1.6);
	letter-spacing: var(--body-letter-spacing, 0);
	font-weight: var(--body-font-weight, 400);
	min-height: 100vh;
	transition:
		background-color 0.3s ease,
		color 0.3s ease;
	font-feature-settings:
		"liga" 1,
		"calt" 1;
	font-variant-numeric: tabular-nums;
}

.container {
	width: 100%;
	max-width: var(--content-max-width);
	margin: 0 auto;
	padding-inline: var(--inset-lg);
}

h1,
h2,
h3,
h4,
h5,
h6,
/* Title/heading classes that render visually as headings but use <div>/<span> tags.
   Consolidated here so swapping --font-heading (typography combo) cascades app-wide. */
.section-title,
.card-title,
.modal-title,
.onboarding-title,
.message-board-title,
.pricing-title,
.race-card-name,
.race-result-name,
.selected-race-name {
	font-family: var(--font-heading);
	font-weight: var(--weight-semibold);
	line-height: 1.2;
	letter-spacing: var(--tracking-tight);
}

h1,
h2,
h3,
h4 {
	margin: 0 0 var(--space-md) 0;
	color: var(--text-bright);
	line-height: 1.3;
}

h1 {
	font-size: var(--text-3xl);
}

h2 {
	font-size: var(--text-2xl);
}

h3 {
	font-size: var(--text-xl);
}

h4 {
	font-size: var(--text-lg);
}

/* Heading sizes — all derive from the --text-* token scale in tokens.css
   so the app has a single typographic source of truth. */
.pricing-title {
	font-size: var(--text-xl);
}

.card-title,
.section-title {
	font-size: var(--text-lg);
}

.modal-title,
.message-board-title {
	font-size: var(--text-base);
}

.onboarding-title {
	font-size: var(--text-sm);
}

.race-card-name,
.race-result-name,
.selected-race-name {
	font-size: var(--text-base);
}

.empty-state-title {
	font-size: var(--text-xl);
}


p {
	margin: 0 0 var(--space-md) 0;
	color: var(--text-secondary);
}

.text-muted {
	color: var(--text-muted);
}

.text-success {
	color: var(--accent-success-text);
}

.text-danger {
	color: var(--accent-danger-text);
}

a {
	font-family: inherit;
	color: var(--accent-action-text);
	text-decoration: none;
	transition: color var(--transition-fast);
}

.section-title {
	font-size: var(--text-lg);
	color: var(--text-secondary);
	margin: 0;
}

.site-footer {
	margin-top: var(--space-lg);
	padding: var(--space-md) var(--inset-lg);
	border-top: 1px solid var(--surface-overlay);
}

.footer-content {
	max-width: var(--content-max-width);
	margin: 0 auto;
	text-align: center;
	display: flex;
	flex-direction: column;
	gap: var(--space-md);
}

.footer-quote {
	font-size: var(--text-xs);
	font-style: italic;
	color: var(--text-secondary);
	line-height: 1.6;
	max-width: var(--content-max-width);
	margin: 0 auto;
}

.footer-links {
	display: flex;
	justify-content: center;
	align-items: center;
	gap: var(--space-sm);
	flex-wrap: wrap;
}

.footer-links a {
	font-size: var(--text-sm);
	color: var(--text-muted);
	text-decoration: none;
	opacity: 0.5;
	transition: opacity var(--transition-fast);
}

.footer-links a:hover {
	opacity: 0.8;
	color: var(--text-secondary);
}

.footer-sep {
	color: var(--text-muted);
	opacity: 0.3;
	font-size: var(--text-sm);
}

.footer-copy {
	font-size: var(--text-sm);
	color: var(--text-muted);
	opacity: 0.4;
}

.footer-brand {
	display: inline;
}

/* =============================================================================
   BRAND WORDMARK — "Steev" in Bricolage Grotesque 800
   Used in nav, footer, auth headers, anywhere the brand appears inline.
   Sized by the parent context via .X .brand-wordmark { font-size: … }.
   ============================================================================= */
.brand-wordmark {
	display: inline-block;
	font-family: var(--font-wordmark);
	font-weight: 800;
	font-size: var(--text-lg);
	line-height: 1;
	letter-spacing: -0.02em;
	color: var(--accent-coach-text);
	vertical-align: middle;
}

/* Context-specific sizing */
.nav-brand .brand-wordmark {
	font-size: var(--text-xl);
}

.footer-brand .brand-wordmark {
	font-size: 1.1em;
}

/* relative to parent footer text */
.login-header .brand-wordmark {
	font-size: var(--text-3xl);
	margin-bottom: var(--space-sm);
}

.footer-alpha {
	text-transform: uppercase;
	font-size: var(--text-2xs);
	font-weight: var(--weight-medium);
	letter-spacing: var(--tracking-wider);
}

.footer-alpha-link {
	color: var(--accent-action-text);
	opacity: 0.6;
	transition: opacity var(--transition-fast);
}

.footer-alpha-link:hover {
	opacity: 1;
}

/* =============================================================================
   CARDS - Card components
   ============================================================================= */

.card {
	background: var(--surface-raised);
	border-radius: var(--radius-lg);
	padding: var(--space-lg) var(--inset-lg);
	margin-bottom: var(--space-lg);
	border: 1px solid var(--border);
	font-family: var(--font-body);
	transition:
		background-color 0.3s ease,
		border-color 0.3s ease;
}

.card-title {
	margin: 0;
	font-size: var(--text-xl);
}

/* =============================================================================
   LAYER A — Global atmosphere (grain + warm vignette)
   Applied on html::before/::after.
   ============================================================================= */

html::before {
	content: "";
	position: fixed;
	inset: 0;
	pointer-events: none;
	z-index: -2;
	background: radial-gradient(ellipse 90% 50% at 50% -10%,
			var(--accent-coach-faint),
			transparent 70%);
	opacity: 0.10;
}

html::after {
	content: "";
	position: fixed;
	inset: 0;
	pointer-events: none;
	z-index: -2;
	background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='g'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.55 0'/></filter><rect width='100%' height='100%' filter='url(%23g)'/></svg>");
	background-size: 220px 220px;
	opacity: 0.05;
	mix-blend-mode: overlay;
}

/* ========================================================================
   P05 — The Stutter Tic

   Reusable three-beat diminishing dash rule (14-8-4 px).
   Use with: <span class="stutter-rule"><span></span></span>
   ======================================================================== */
.stutter-rule {
	display: inline-flex;
	gap: 4px;
	align-items: center;
	line-height: 0;
}

.stutter-rule::before,
.stutter-rule::after,
.stutter-rule>span {
	content: "";
	display: inline-block;
	height: 2px;
	background: var(--accent-coach);
}

.stutter-rule::before {
	width: 14px;
}

.stutter-rule>span {
	width: 8px;
}

.stutter-rule::after {
	width: 4px;
}

/* =============================================================================
   ACTION-PILL.CSS - Card-level inline action buttons

   Used on node detail panels and chat cards. Pill-shaped with optional
   primary (action-accent) and muted variants. Distinct from
   .chat-chip (the composer's state chips) — these are literal actions
   attached to a card.
   ============================================================================= */

.action-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.4em 0.9em;
  border-radius: var(--radius-full);
  border: 1px solid color-mix(in srgb, var(--text-muted) 25%, transparent);
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  font-family: inherit;
  cursor: pointer;
  transition:
    color var(--transition-fast),
    background var(--transition-fast),
    border-color var(--transition-fast);
}

.action-pill:hover {
  background: color-mix(in srgb, var(--text-muted) 10%, transparent);
  color: var(--text);
  border-color: var(--text-secondary);
}

.action-pill:active {
  transform: scale(0.97);
}

.action-pill-icon {
  font-size: 1.05em;
  line-height: 1;
}

.action-pill--primary {
  border-color: color-mix(in srgb, var(--accent-action) 40%, transparent);
  color: var(--accent-action-text);
}

.action-pill--primary:hover {
  background: color-mix(in srgb, var(--accent-action) 8%, transparent);
  border-color: var(--accent-action);
}

.action-pill--muted {
  color: var(--text-muted);
}

.action-pill--muted:hover {
  color: var(--text-secondary);
}

/* =============================================================================
   ALERTS - Alert, toast, and status banner components
   ============================================================================= */

.alert {
  padding: var(--space-md);
  border-radius: var(--radius-md);
  font-size: var(--text-base);
}

.alert-caution {
  background: var(--accent-caution-faint);
  border: 1px solid var(--accent-caution);
  color: var(--accent-caution-text);
}

.alert-danger {
  background: var(--accent-danger-faint);
  border: 1px solid var(--accent-danger);
  color: var(--accent-danger-text);
}

/* =============================================================================
   TOASTS - Notification popups
   ============================================================================= */

.toast-container {
  position: fixed;
  bottom: var(--space-lg);
  right: var(--space-md);
  left: var(--space-md);
  z-index: 1000;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  pointer-events: none;
}

.toast {
  padding: var(--space-md);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  box-shadow: var(--shadow-lg);
  animation: slideIn var(--transition-normal) ease-out;
  pointer-events: auto;
}

/* Auto-dismiss toasts fade out after 4 seconds */
.toast[data-auto-dismiss] {
  animation: slideIn var(--transition-normal) ease-out, toastFadeOut 0.3s ease-out 4s forwards;
}

@keyframes toastFadeOut {
  to { opacity: 0; transform: translateY(-10px); pointer-events: none; }
}

.toast-error {
  border-left: 4px solid var(--accent-danger);
}

.toast-caution {
  border-left: 4px solid var(--accent-caution);
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateX(100%);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}


/* =============================================================================
   AVATAR.CSS - Initial-letter circle avatars

   Three sizes: default (sized per-context, no fixed width), sm (32px),
   md (48px). Background is accent-coach-strong with text-on-accent — the
   filled-primary pair of .btn-primary, readable on both light and dark themes.
   ============================================================================= */

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--accent-coach-strong);
  color: var(--text-on-accent);
  font-family: var(--font-heading);
  font-weight: var(--weight-bold);
  letter-spacing: -0.02em;
  text-transform: uppercase;
  flex-shrink: 0;
}

.avatar-sm {
  width: 32px;
  height: 32px;
  min-width: 32px;
  min-height: 32px;
  font-size: var(--text-sm);
}

.avatar-md {
  width: 48px;
  height: 48px;
  min-width: 48px;
  min-height: 48px;
  font-size: var(--text-lg);
}

/* =============================================================================
   BADGES & PROGRESS - Badge and progress bar components
   ============================================================================= */

.progress {
  height: 8px;
  background: var(--surface-raised-hover);
  border-radius: var(--radius-full);
  overflow: hidden;
}

.progress-bar {
  height: 100%;
  background: var(--accent-action);
  border-radius: var(--radius-full);
  transition: width var(--transition-normal);
}

.progress-sm {
  height: 3px;
}

/* =============================================================================
   BADGES
   ============================================================================= */

.badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 2px var(--space-xs);  /* Smaller padding */
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);  /* Lighter weight */
  border-radius: var(--radius-full);
  text-transform: none;  /* Removed uppercase */
  letter-spacing: 0.02em;  /* Reduced spacing */
}

.badge-success {
  background: color-mix(in srgb, var(--accent-success) 12%, transparent);
  color: var(--tag-text-success, var(--accent-success));
  border: 1px solid color-mix(in srgb, var(--accent-success) 25%, transparent);
}

.badge-recovery {
  background: var(--accent-recovery-faint);
  color: var(--tag-text-recovery, var(--accent-recovery));
}

.badge-tint {
  background: var(--surface-sunken);
  color: var(--text-secondary);
}

.badge-sm {
  padding: 1px 6px;
  font-size: var(--text-xs);
}

/* Ghost badge - transparent with border */
.badge-ghost {
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
}

/* Muted badge - disabled appearance */
.badge-muted {
  background: var(--surface-sunken);
  color: var(--text-muted);
  opacity: 0.7;
}

/* Credits badge - clickable for free users in nav */
.badge-credits {
  background: var(--accent-strength-faint);
  color: var(--tag-text-strength);
  cursor: pointer;
  text-decoration: none;
  transition: opacity 0.15s;
}
.badge-credits:hover {
  opacity: 0.8;
}
.badge-credits.badge-exhausted {
  background: var(--accent-danger-faint);
  color: var(--accent-danger-text);
}

/* =============================================================================
   GEAR WEAR — locker pair condition pill (own namespace, not .badge-<key>:
   that namespace is shared by gear catalog categories + guides taxonomy and
   is grepped by every_taxonomy_key_has_css_classes)
   ============================================================================= */

.gear-wear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 2px var(--space-xs);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  border-radius: var(--radius-full);
  letter-spacing: 0.02em;
}

/* Ghost variant of the success tint — a filled fresh pill would twin the
   filled Default badge next to it on the same row. */
.gear-wear--fresh {
  background: transparent;
  color: var(--tag-text-success, var(--accent-success));
  border: 1px solid color-mix(in srgb, var(--accent-success) 30%, transparent);
}

.gear-wear--broken-in {
  background: var(--surface-sunken);
  color: var(--text-secondary);
}

.gear-wear--worn-out {
  background: color-mix(in srgb, var(--accent-caution) 12%, transparent);
  color: var(--tag-text-caution, var(--accent-caution));
  border: 1px solid color-mix(in srgb, var(--accent-caution) 25%, transparent);
}


/* ========================================================================
   P02 — The Breath Line

   1px coral hairline with a gentle breath animation. Placed at the baseline
   of a Steev opener sentence. Synced (by cadence) with the face halo pulse.

   Usage:
     <div class="breath-line" aria-hidden="true"></div>

   Respects prefers-reduced-motion.
   ======================================================================== */

@keyframes steev-breath {
  0%, 100% {
    transform: scaleX(0.995);
    opacity: 0.55;
  }
  50% {
    transform: scaleX(1);
    opacity: 0.80;
  }
}

.breath-line {
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--breath-gradient);
  transform-origin: 50% 50%;
  animation: steev-breath 4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .breath-line {
    animation: none;
    opacity: 0.7;
    transform: scaleX(1);
  }
}

/* =============================================================================
   BUTTONS - Button components including FAB
   ============================================================================= */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-lg);
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  border: none;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: color var(--transition-fast), background var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast), opacity var(--transition-fast);
  text-decoration: none;
  white-space: nowrap;
  -webkit-appearance: none;
  appearance: none;
  background: var(--surface-raised);
  color: var(--text);
}

.btn:active:not(:disabled) {
  transform: scale(0.97);
}

.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Filled buttons take the -strong fills, never the bright primitives: these
   carry text, and the ink is white in both schemes (tokens.css). */
.btn-success {
  background: var(--accent-success-strong);
  color: var(--text-on-accent);
}

.btn-success:hover:not(:disabled) {
  background: var(--accent-success-strong-hover);
  color: var(--text-on-accent);
}

.btn-danger {
  background: var(--accent-danger-strong);
  color: var(--text-on-accent);
}

.btn-danger:hover:not(:disabled) {
  background: var(--accent-danger-strong-hover);
  color: var(--text-on-accent);
}

.btn-ghost {
  background: transparent;
  color: var(--text-secondary);
}

.btn-ghost:hover:not(:disabled) {
  background: var(--surface-raised);
  color: var(--text);
}

.btn-block {
  width: 100%;
}

.btn-sm {
  padding: var(--space-xs) var(--space-sm);
  font-size: var(--text-sm);
}

.btn-lg {
  padding: var(--space-md) var(--space-xl);
  font-size: var(--text-lg);
}

/* Icon-only button variant */
.btn-icon {
  padding: var(--space-sm);
  min-width: 2.5rem;
  min-height: 2.5rem;
  font-size: var(--text-lg);
  line-height: 1;
}

.btn-icon.btn-sm {
  min-width: 2rem;
  min-height: 2rem;
}

/* =============================================================================
   ACTION PILLS — these also style the empty-state CTA. The card's cta_row is no
   longer a pill (card-sections.css .card-cta).
   ============================================================================= */

.btn-primary {
  padding: 10px 24px;
  border-radius: var(--radius-uneven-primary);
  background: var(--accent-coach-strong);
  color: var(--text-on-accent);
  font-size: 14.5px;
}

.btn-primary:hover:not(:disabled) {
  background: var(--accent-coach-strong-hover);
}

.btn-outline {
  padding: 9px 20px;
  border: 1.5px solid var(--paper-rule);
  border-radius: 16px 13px 17px 14px;
  background: transparent;
  color: var(--text-muted);
  font-size: 14px;
  font-weight: 600;
}

.btn-outline:hover:not(:disabled) {
  border-color: var(--border-strong);
  color: var(--text);
}

@media (max-width: 760px) {
  .btn,
  .btn-icon.btn-sm {
    min-height: 44px;
  }
}

/* =============================================================================
   CARD-SECTIONS.CSS — the card vocabulary's section blocks

   Chrome (border, radius, padding) comes from .chat-card; this file styles
   only what lives INSIDE a card. One block per section kind, plus the
   fallback core every card degrades to. Sections stack vertically —
   horizontal layout never escapes the kind that owns it. Phase 0 is
   motion-free: motion is defined per surface later.
   ============================================================================= */

/* `card` marks a CARD BODY: the element whose direct children are sections.
   render_card emits it, and the two wrappers that render a bare section list
   (journey node detail, standalone chat card) declare it too, so
   section rhythm has ONE owner instead of each wrapper's incidental gap.
   base.css owns a legacy `.card` block (own surface + bottom margin) — inside
   the vocabulary the wrapper's own chrome is authoritative. */
.chat-card.card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 0;
}

/* chat-cards.css tightens the journey detail to --space-xs — a value tuned when
   the card held a poster plus its pills. The body's own rhythm is the one
   owner, at every surface it renders on. */
.timeline-detail > .chat-card.card {
  gap: 10px;
}

/* A labelled prose/list after the first section OPENS A GROUP: a dashed rule
   with 18 above (the 10 gap plus 8) and 16 below. A segments label is a
   column, not a boundary — hence the :is() restriction, which is the same
   prose/list split iOS draws (CardSectionListView's groupLabel). The label is
   emitted only when it is non-empty, so `:has(> .card-section__label)` already
   reads "has a real label". A TONED prose is excluded: `_prose.html` puts
   `card-prose--info/--caution/--success` on the section element itself, and
   those carry their own border and padding — a callout is already a boundary,
   and the group rule would replace the box it draws. */
.chat-card.card
  > .card-section
  + :is(.card-section--prose, .card-section--list):not(
      .card-prose--info,
      .card-prose--caution,
      .card-prose--success
    ):has(> .card-section__label) {
  margin-top: 8px;
  padding-top: 16px;
  border-top: 1.5px dashed var(--paper-rule);
}

/* .card-actions predates the card body and spaces itself; inside one the gap
   already separates it, so its own top margin would double it. */
.card > .card-actions {
  margin-top: 0;
}

/* Server-composed copy is unbounded: long tokens wrap instead of widening
   the card. */
.card-section,
.card-fallback {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* ─── Fallback core ─── */

.card-fallback {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

/* The heading is the control when the fallback carries an action. */
.card-fallback__action {
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.card-fallback__action:hover {
  color: var(--card-accent-text);
}

/* ─── header ─── */

.card-section--header {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.card-eyebrow {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--space-sm);
  margin: 0;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--card-accent-text);
}

/* `kind` is an OPEN styling hint (http/api/card.rs): a segment whose kind has
   no rule here simply renders as plain eyebrow text — the documented
   degradation. `was` is the one kind that styles itself: the title this
   session replaced, struck out and stepped back. */
.card-eyebrow__seg--was {
  text-decoration: line-through;
  color: var(--text-faint);
}

/* `tag` closes the stamped line with the session's own word ("tempo"), so it
   steps back from the accent the dated segments carry. */
.card-eyebrow__seg--tag {
  color: var(--text-muted);
}

/* Flex, so the mark can centre against the title's line box instead of hanging
   off a baseline. A mark is a line drawing whose ink sits mid-box, so pinning
   its box to the text baseline leaves the ink floating above the letterforms;
   `align-self: center` resolves against the font's real metrics at render time,
   which a tuned `em` offset cannot (the first attempt assumed a 24px heading
   that is actually 30px). It also makes an overlong title indent beside the
   mark rather than wrap under it — matching the iOS HStack. */
.card-heading {
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 28px;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--text-bright);
}

.card-heading__icon {
  width: 24px;
  height: 24px;
  color: var(--card-accent-text);
  align-self: center;
}

/* A subtitle is a date range — data, not voice. The coach register belongs to
   .card-prose; size and colour carry the distinction here. */
.card-subtitle {
  margin: 0;
  font-family: var(--font-body);
  font-size: 14px;
  color: var(--text-muted);
}

/* ─── stat_row ─── */

/* Both rules live on the CELLS, not here: at a narrow column the row wraps, and
   a section-level top rule leaves line 2 unruled while a right-hand divider on
   the last cell of line 1 runs into empty space. `overflow: hidden` is what
   clips the divider each row's first cell hangs at -1px. */
.card-section--stat_row {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: 0;
  margin: 0;
  overflow: hidden;
}

/* column-reverse: the value reads above its label, dt still precedes dd.
   A real basis, not `auto`: flex wraps on the CONTENT width before it shrinks
   anything, so at max-content the stats page's three labels break the row onto
   two lines. 5.5rem is the narrowest legible cell — below it the row wraps,
   which is what flex-wrap is for.

   NO `min-width: 0` — the flex incantation is wrong here. The value is
   `white-space: nowrap`, so a cell floored at zero is a cell that overflows:
   a pace RANGE ("4:50–5:05 /km") is twice any other reading, and on a phone it
   either ran over its neighbour or was eaten by the row's `overflow: hidden`
   (live defect, 2026-09-19). The automatic minimum IS the content, which is
   what makes the wrap above fire instead. */
.card-stat {
  display: flex;
  flex-direction: column-reverse;
  align-items: flex-start;
  gap: 2px;
  flex: 1 1 5.5rem;
  padding: 10px 6px 4px;
}

/* A lone stat is a reading, not a column: without this it grows to the whole
   row. */
.card-section--stat_row .card-stat:only-child {
  flex: 0 0 auto;
}

.card-stat__value {
  margin: 0;
  white-space: nowrap;
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: var(--text-xl);
  line-height: 1;
  letter-spacing: var(--tracking-tight);
  color: var(--text);
}

.card-stat__label {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* A second run after the value, never part of it — the wire splits the unit
   once, server-side, and no renderer re-parses the value to find it. */
.card-stat__unit {
  font-size: 12px;
  font-weight: var(--weight-medium);
  color: var(--text-muted);
  letter-spacing: 0;
}

/* Emphasis tints the whole reading, unit included (RULE C). */
.card-stat--emphasis .card-stat__unit,
.card-feel__target--emphasis .card-stat__unit {
  color: inherit;
}

/* The ten-dot effort scale. Geometry only: the poster's stat column is the one
   place it shows, and that scope flips this display. */
.card-stat__dots {
  display: none;
  gap: 3px;
  align-items: center;
  margin-left: 8px;
}

.card-stat__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  border: 1px solid var(--border);
}

.card-stat__dot--on {
  border-color: transparent;
  background: var(--ember-sage);
}

.card-stat--emphasis .card-stat__value {
  font-size: var(--text-2xl);
  color: var(--card-accent-text);
}

/* The tint goes on the arrow, never on the value (DESIGN §1.2) — a trend with
   no tone stays neutral, same muted ink the stat label already uses. */
.card-stat__trend {
  margin-left: 2px;
  font-size: 0.75em;
  color: var(--text-muted);
}

.card-stat__trend--good {
  color: var(--accent-success-text);
}

.card-stat__trend--bad {
  color: var(--accent-danger-text);
}

.card-stat__trend--neutral {
  color: var(--text-faint);
}

/* ─── meter (pure CSS: no JS, no chart library) ─── */

.card-section--meter {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.card-meter--bar {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.card-meter__track {
  height: var(--space-xs);
  border-radius: var(--radius-full);
  background: color-mix(in srgb, var(--border) 70%, transparent);
  overflow: hidden;
}

/* width comes from the normalized point, inline. */
.card-meter__bar {
  height: 100%;
  border-radius: inherit;
  background: var(--card-accent);
}

.card-meter--spark {
  display: flex;
  align-items: flex-end;
  gap: var(--space-xs);
  height: var(--space-xl);
}

.card-meter__spark {
  flex: 1 1 0;
  min-height: var(--radius-xs);
  border-radius: var(--radius-xs) var(--radius-xs) 0 0;
  background: color-mix(in srgb, var(--card-accent) 70%, transparent);
}

/* --card-meter-fill is the inline sweep angle; the mask punches the centre so
   the ring reads on any card tint. */
.card-meter--ring {
  --card-meter-fill: 0deg;
  width: var(--space-2xl);
  height: var(--space-2xl);
  border-radius: var(--radius-full);
  background: conic-gradient(
    var(--card-accent) var(--card-meter-fill),
    color-mix(in srgb, var(--border) 70%, transparent) 0
  );
  -webkit-mask: radial-gradient(closest-side, transparent 64%, black 65%);
  mask: radial-gradient(closest-side, transparent 64%, black 65%);
}

/* The formatted value ("48 / 60 km") sits in normal flow after the shape, at
   full size, for every style. It used to overlay the ring — a 48px hole is
   not a text box, and the value wrapped onto its own stroke. */
.card-meter__display {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--text);
}

.card-meter__caption {
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
}

/* Up to three segment labels under the bars (SPEC §4.2), spread across the
   full width — `justify-content` alone does the spacing so two labels sit at
   the ends and three land with one centred, no gap needed. */
.card-meter__labels {
  display: flex;
  justify-content: space-between;
  margin: 0;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
}

/* ─── prose ─── */

/* Prose is the coach talking — same register as .scribble-text and iOS's
   EmberFont.coach. Italic stays reserved for *em* inside the markdown. */
/* The coach register: same family and size as the chat scribble
   (`chat-marginalia.css` `.scribble-text`) — a card is the same voice as the
   stream, not a denser one. The subtitle above is deliberately NOT this: a
   date range is data. */
.card-prose {
  font-family: var(--font-coach);
  /* The scribble's own size, so a card in the stream cannot end up a size apart
     from the reply above it. iOS says the same thing structurally — its card
     prose renders through the very `coachAttributed` facility the scribble
     uses, so it cannot drift at all. */
  font-size: var(--chat-prose-size);
  /* Tighter than the stream: a card's prose sits between ruled groups rather
     than running on. */
  line-height: 1.5;
  color: var(--text);
}

.card-prose > :first-child {
  margin-top: 0;
}

.card-prose > :last-child {
  margin-bottom: 0;
}

.card-prose p {
  margin: 0 0 var(--space-sm);
}

.card-prose ul,
.card-prose ol {
  margin: 0 0 var(--space-sm);
  padding-left: var(--space-lg);
}

.card-prose strong {
  font-weight: var(--weight-semibold);
  color: var(--text-bright);
}

.card-prose code {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

/* A coach callout: a tinted hairline + inset, never a left bar (repo rule,
   memory no-left-accent-bars). --accent-info is the coach-voice colour and
   `info` is a coach aside, so this is its legitimate use. */
.card-prose--info,
.card-prose--caution,
.card-prose--success {
  border: 1px solid;
  border-radius: var(--radius-sm);
  padding: var(--space-sm);
}

.card-prose--info {
  border-color: var(--accent-info-faint);
}

.card-prose--caution {
  border-color: var(--accent-caution-faint);
}

.card-prose--success {
  border-color: var(--accent-success-faint);
}

/* Optional micro-label above prose/list/segments blocks (detail surfaces).
   Mirrors .poster__subsection-label; an <li> in list/segments contexts. */
.card-section__label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-muted);
  list-style: none;
}

/* Anything carrying an entity taps through to the journey node detail — a
   whole card, or a single list row inside one. Mirrors streaming.js's
   delegate selector exactly, so the cursor never disagrees with what is
   actually clickable. Interactive children keep their own cursors via their
   own rules. */
[data-open-node] {
  cursor: pointer;
}

/* A stub card taps too (its fill, not navigation) and reads incomplete —
   the web twin of iOS's 0.6-dimmed stub card and the 0.72 stub row below.
   Keyed on the attribute autoFillChatStub removes the moment a fill starts,
   so the dim and the cursor heal themselves. */
.chat-card[data-detail-pending="true"] {
  cursor: pointer;
  opacity: 0.72;
}

/* role="button" tabindex="0" (card.html): a keyboard runner landing here is
   told nothing without a ring — same statement as .convo-card's. */
.chat-card[data-detail-pending="true"]:focus-visible {
  outline: 2px solid var(--accent-user);
  outline-offset: 2px;
}

/* ─── list ─── */

.card-section--list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-sm);
}

.card-list__row {
  display: flex;
  color: var(--text);
}

/* The row stays a listitem; its tap target is this child, stretched to the
   whole row so the cursor and the focus ring cover it. */
.card-list__hook {
  display: flex;
  flex: 1;
}

/* Numbered steps: the count steps down in size only — its ink is the row's,
   --card-accent or whatever status the row carries. */
.card-list__glyph {
  flex: none;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--card-accent-text);
}

/* Reserved per LIST (`ListData::any_kicker`), like the glyph beside it: the day
   stamp is a column, so a row without one still holds its cell. */
.card-list__kicker {
  flex: 0 0 58px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--text-muted);
}

/* `align-self: center`, not baseline: an <svg> has no baseline, so a
   flex item wrapping one contributes its BOTTOM edge instead — parking a 20px
   box's bottom on the text baseline and leaving the mark's mid-box ink floating
   ~6px above the label. `vertical-align` cannot fix it; it does not apply to
   flex items. */
.card-list__icon {
  width: 20px;
  height: 20px;
  align-self: center;
  color: var(--card-accent-text);
}

.card-list__value {
  flex: none;
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* Shared column for label + its optional second line, so .card-list__value
   keeps reading from the right and the glyph/icon columns stay untouched. A
   row with no detail renders byte-identically to before this wrapper. */
.card-list__text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}

.card-list__detail {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* Done is not struck out: the session HAPPENED, and a strike reads as
   cancelled. Full ink (the row's own) plus a check, the way a log is ticked
   off. The check is presentation, alt text "" — the class carries the state. */
.card-list__row--done .card-list__label::after {
  content: "✓" / "";
  margin-left: 6px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--accent-success-text);
}

.card-list__row--done .card-list__glyph,
.card-list__row--done .card-list__icon {
  color: var(--accent-success-text);
}

.card-list__row--active .card-list__label {
  font-weight: var(--weight-medium);
  color: var(--text-bright);
}

.card-list__row--active .card-list__glyph,
.card-list__row--active .card-list__icon {
  color: var(--accent-coach-text);
}

/* Announced, unlike the "·" and "✓": nothing else on the row says which
   session is next. iOS reads its own word from copy — PLAN open question 4
   holds the trade-off. */
.card-list__row--active .card-list__label::after {
  content: "next";
  display: inline-block;
  margin-left: 6px;
  padding: 1px 4px 0;
  border: 1px solid currentColor;
  border-radius: 2px;
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent-coach-text);
}

.card-list__row--pending .card-list__label,
.card-list__row--pending .card-list__kicker,
.card-list__row--pending .card-list__value,
.card-list__row--pending .card-list__glyph,
.card-list__row--pending .card-list__icon {
  color: var(--text-faint);
}

/* A session that came and went. */
.card-list__row--missed .card-list__label {
  color: var(--text-muted);
  text-decoration: line-through;
  text-decoration-color: var(--border);
}

.card-list__row--missed .card-list__glyph,
.card-list__row--missed .card-list__icon {
  color: var(--text-muted);
}

/* A placeholder session: the coach has claimed the day but not written it
   yet, so every metric on the row is an estimate. */
.card-list__row--stub {
  opacity: 0.72;
}

/* ─── chip_row (chips are .chat-chip: quoted utterances, never pills) ─── */

.card-section--chip_row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs) var(--space-md);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* Navigate/open_url chips are anchors; they must read exactly like the button
   chips beside them. */
.card-section--chip_row a.chat-chip {
  text-decoration: none;
}

/* On the detail poster the chip is METADATA with an edit affordance (the shoe
   line), not an utterance — it joins the poster's mono metadata family instead
   of wearing the composer follow-up pill `.chat-chip` would give it. The
   dotted underline is the whole affordance. Three classes: it outranks
   `.chat-chip` and its :hover (chat-marginalia.css, later in the bundle). */
.timeline-detail .card-section--chip_row .chat-chip {
  position: relative;
  padding: 0;
  border: 0;
  border-radius: 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-style: normal;
  font-weight: var(--weight-medium);
  color: var(--text-muted);
  text-decoration: underline dotted;
  text-decoration-color: color-mix(in srgb, var(--text-muted) 55%, transparent);
  text-underline-offset: 4px;
}

/* With the pill's padding gone, ::after is the whole 44px hit area (the same
   invisible overlay `.chat-chip` arms on mobile, here at every width because
   the bare line box is ~15px tall everywhere). */
.timeline-detail .card-section--chip_row .chat-chip::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: var(--tap-target);
  transform: translateY(-50%);
}

.timeline-detail .card-section--chip_row .chat-chip:hover {
  color: var(--paper-link);
  /* The dotted underline turns solid, in the link ink. */
  text-decoration-line: underline;
  text-decoration-style: solid;
  text-decoration-color: currentColor;
}

/* ─── cta_row (rows of quiet text, never buttons — this is not an app about
   buttons; the CTAs answer in the paper's own register, like the chips above
   them, and the only chrome is a hairline BETWEEN two answers) ─── */

.card-section--cta_row {
  display: flex;
  flex-direction: column;
}

/* The whole row is the target, so the 44px floor and the full width live on
   the macro's element — there is no wrapper to hang them on. */
.card-cta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  min-height: var(--tap-target);
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  transform: none;
  font-family: inherit;
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

/* The cue that this line answers. Presentational (alt text ""), and it never
   moves on hover — the underline is the whole of that feedback. */
.card-cta::after {
  content: "→" / "";
  font-family: var(--font-mono);
  font-size: 14px;
}

/* Between rows only: a rule under the last answer would read as a box side. */
.card-cta + .card-cta {
  border-top: 1px solid var(--border);
}

/* The tint IS the hierarchy, and the primary answer takes the paper's own link
   ink — `--paper-link`, not `--accent-coach` directly, and never
   `--accent-info`, which is the coach's speaking voice and never a control. */
.card-cta--primary {
  color: var(--paper-link);
}

.card-cta--secondary {
  color: var(--text-secondary);
}

/* The quieter answer's cue is quieter still; the label carries the weight. */
.card-cta--secondary::after {
  color: var(--text-faint);
}

/* The cue inherits: a danger answer's arrow is the same red as its label. */
.card-cta--danger {
  color: var(--accent-danger-text);
}

/* Underline, never a fill — a fill is the pill coming back. The tint survives
   the hover, so a danger answer stays red while it is being pressed. */
.card-cta:hover,
.card-cta:focus-visible,
.card-cta:active {
  text-decoration: underline;
  text-underline-offset: 4px;
}

.card-cta:focus-visible {
  outline: 2px solid var(--accent-action);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

/* ─── segments ─── */

/* A 76px label column and the rows beside it. `grid-column: -2 / -1` puts every
   row in the LAST column, so the same rule serves both the two-column grid and
   the one-column one a label-less section collapses to (the metrics line). */
.card-section--segments {
  display: grid;
  grid-template-columns: 76px 1fr;
  column-gap: 10px;
  row-gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
}

.card-segment {
  grid-column: -2 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 10px;
}

.card-segment__type {
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* Pace and heart rate carry their own classes so the data row can put one at
   each end; all three trailing runs are a reading that never shrinks. */
.card-segment__pace,
.card-segment__hr,
.card-segment__meta {
  flex: none;
}

/* ─── feel ─── */

.card-section--feel {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs) var(--space-lg);
  margin: 0;
}

.card-feel__target {
  display: flex;
  align-items: baseline;
  gap: var(--space-xs);
  min-width: 0;
}

/* Whole words: a sentence-long value beside it wraps instead of breaking
   "TARGET HR" at any character. */
.card-feel__label {
  flex: none;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
}

.card-feel__value {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text);
}

.card-feel__target--emphasis .card-feel__value {
  font-weight: var(--weight-medium);
  color: var(--card-accent-text);
}

/* ─── delta ─── */

/* One mono line per metric, read left to right: LABEL planned → actual, and
   the difference falls out at the right margin. */
.card-section--delta {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  margin: 0;
  font-family: var(--font-mono);
  font-size: 12px;
}

.card-delta__row {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
}

/* A fixed column, not the slack: the planned figures have to stack. */
.card-delta__label {
  flex: 0 0 76px;
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* Shrinkable: the label is a fixed column, so in a narrow poster column the
   figures are the only thing left that can give way. */
.card-delta__cell {
  flex: 0 1 auto;
  min-width: 0;
  margin: 0;
  font-size: 12px;
}

.card-delta__cell--planned {
  color: var(--text-faint);
}

.card-delta__arrow {
  color: var(--text-faint);
}

.card-delta__cell--actual {
  color: var(--text-bright);
}

/* Pushed to the right margin (the label is a fixed column now, so nothing else
   takes the slack). Direction is not wire-encoded, so the delta is set apart by
   weight, never by a tint that would read as a verdict (iOS matches). */
.card-delta__cell--delta {
  margin-left: auto;
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
}

/* ─── chart (server-rendered SVG: no JS, no chart library, no motion) ─────────

   The plot is a 0..100 × 0..100 viewBox stretched into this box, so nothing
   inside it may be a circle or a scaling stroke: bars are fills, the line
   carries vector-effect. Labels live OUTSIDE the SVG in grid rows that share
   the plot's slots, which is why they keep the page's type scale.
   ------------------------------------------------------------------------- */

.card-section--chart {
  --card-chart-slots: 1;
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0;
}

.card-chart__values,
.card-chart__labels {
  display: grid;
  grid-template-columns: repeat(var(--card-chart-slots), minmax(0, 1fr));
  align-items: end;
}

/* Held open whether or not the slots kept a display: the series is thinned
   upstream, and a row that collapses would move the plot under it. */
.card-chart__values {
  height: 16px;
}

/* One line each, overflowing its cell rather than wrapping: the row is 16px
   tall and a second line lands in the plot. */
.card-chart__value,
.card-chart__label {
  min-width: 0;
  font-family: var(--font-mono);
  text-align: center;
  white-space: nowrap;
}

/* Text never wears the series color — the mark under it carries identity.
   `now` is the exception: it is the reading the chart exists for. */
.card-chart__value {
  font-size: 11px;
  color: var(--text-secondary);
}

.card-chart__value--last {
  font-weight: var(--weight-medium);
  color: var(--accent-coach-text);
}

.card-chart__label {
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* `now` again — the one axis label that is read rather than scanned. Keyed on
   the mark's flag, not :last-child: a status series has no `now`. */
.card-chart__label--last {
  color: var(--text-muted);
}

/* The box the viewBox stretches into; the border IS the baseline rule. */
.card-chart {
  display: block;
  width: 100%;
  height: 72px;
  border-bottom: 1.5px solid var(--border);
}

/* ONE series, one ink: the coach's coral, weighted by how much the slot has to
   say — a thinned bucket .55, the peak .75, `now` solid. Steel is gone from the
   plot; the target keeps caution (warm) as the counter-hue, which is the pair
   in the palette that clears CVD separation against the card surface. The ramp
   is the HISTORY series only: a bar carrying a status states that status at
   full strength (below). */
.card-chart__bar {
  fill: var(--accent-coach);
  fill-opacity: 0.55;
}

.card-chart__bar--emphasis {
  fill-opacity: 0.75;
}

.card-chart__bar--last {
  fill-opacity: 1;
}

.card-chart__bar--target {
  fill: var(--accent-caution);
  fill-opacity: 1;
}

/* plan_week speaks the status vocabulary the list rows already speak, at a
   FIXED weight — the history ramp above never reaches these. Pending is faint
   as well as neutral, so state never rests on hue alone. */
.card-chart__bar--done {
  fill: var(--accent-success);
  fill-opacity: 1;
}

.card-chart__bar--active {
  fill: var(--accent-coach);
  fill-opacity: 1;
}

.card-chart__bar--pending {
  fill: var(--neutral);
  fill-opacity: 0.45;
}

.card-chart__bar--missed {
  fill: var(--accent-danger);
  fill-opacity: 1;
}

/* An empty bucket: a tick on the baseline, quieter than any drawn session —
   including the faintest of them, the pending bar above. Last, so it wins the
   cascade on a tick that also carries a status. */
.card-chart__bar--tick {
  fill: var(--text-faint);
  fill-opacity: 0.6;
}

/* The line keeps its 2px whatever the box does to the viewBox — the element
   carries vector-effect as an attribute, so this is only its colour + weight. */
.card-chart__line {
  fill: none;
  stroke: var(--accent-coach);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Dashed: the target stays legible as a reference under any color vision. */
.card-chart__line--target {
  stroke: var(--accent-caution);
  stroke-dasharray: 4 3;
}

/* A sentence about the plot, not a stamp over it. */
.card-chart__caption {
  margin: 0;
  font-family: var(--font-body);
  font-size: 13px;
  color: var(--text-muted);
}

/* The stats page is a COLUMN OF PLOTS, read top to bottom in one pass — the
   chat stream's card is read one at a time. Both numbers are the page's, not
   the chart's: a looser body rhythm and a taller plot than a card in the
   stream. `#stats-cards` is that page's card container (pages/stats.html). */
#stats-cards .chat-card.card {
  gap: 16px;
}

#stats-cards .card-chart {
  height: 84px;
}

/* ─── hero (celebration; the card's `moment` accent carries the chrome half of
   the treatment — chat-cards.css) ─── */

.card-section--hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 0;
  text-align: center;
}

/* A disc of the card's own accent, sized to the glyph — no image, no confetti,
   nothing that moves. --card-accent, not --accent-moment: the same read iOS's
   HeroSectionView takes, so the disc is gold on a moment card and coral on the
   coach-accented bare hero. */
.card-hero__glyph {
  margin: 0;
  padding: 0;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 24px;
  line-height: 1;
  background: color-mix(in srgb, var(--card-accent) 22%, transparent);
}

.card-hero__headline {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--weight-bold);
  font-size: 20px;
  line-height: 1.15;
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
  color: var(--text);
}

.card-hero__stat {
  margin: 0;
}

/* Value over label, centred: the base column-reverse grammar, tightened — not
   the ruled cell `.card-stat` draws (flex-start, 2px, its own padding); a
   celebration number is not a cell. */
.card-section--hero .card-hero__stat .card-stat {
  align-items: center;
  gap: 4px;
  padding: 0;
}

/* THE number of the celebration — it leads, the headline names it. Later in
   the file than `.card-stat--emphasis .card-stat__value` and at the same
   specificity, so it wins on both the size and the accent an emphasized hero
   stat would otherwise take at --text-2xl. */
.card-hero__stat .card-stat__value {
  font-size: 48px;
  font-weight: 800;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  color: var(--card-accent-text);
}

/* Not the cell's 10px faint stamp (`.card-stat__label`). */
.card-section--hero .card-hero__stat .card-stat__label {
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--text-muted);
}

.card-hero__stat .card-stat__unit {
  font-size: 16px;
}

/* ─── choices (option rows with a sentence each — the shape chips cannot
   carry, DESIGN §1.1) ─── */

.card-section--choices {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* Grid, not a flex overlay: the macro's element (the real, focusable tap
   target) and the detail text are explicitly placed, so an absent glyph or
   detail never depends on DOM order. The glyph spans both rows via -1, which
   collapses to one row when there is no detail. */
.card-choices__row {
  display: grid;
  grid-template-columns: minmax(0, auto) 1fr;
  align-items: center;
  column-gap: var(--space-sm);
  row-gap: 2px;
  min-height: var(--tap-target);
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  transition: border-color var(--transition-fast);
}

.card-choices__row:hover {
  border-color: var(--accent-action);
}

.card-choices__glyph {
  grid-column: 1;
  grid-row: 1 / -1;
  align-self: center;
  font-family: var(--font-mono);
  font-size: var(--text-lg);
  color: var(--card-accent-text);
}

.card-choices__option {
  grid-column: 2;
  grid-row: 1;
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text-bright);
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}

.card-choices__option:focus-visible {
  outline: 2px solid var(--accent-action);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}

.card-choices__detail {
  grid-column: 2;
  grid-row: 2;
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* ─── table (flat strings, ≤4 columns — scrolls inside the card via the
   .scroll-row utility, never the page) ─── */

.card-section--table {
  margin: 0;
}

.card-table {
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.card-table__caption {
  caption-side: bottom;
  padding-bottom: var(--space-xs);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-muted);
  text-align: left;
}

.card-table__head {
  padding: 0 var(--space-md) var(--space-xs) 0;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-faint);
  text-align: left;
  white-space: nowrap;
  overflow-wrap: normal;
  border-bottom: 1px solid var(--border);
}

/* Values are numbers: the same mono face .card-list__value and
   .card-delta__cell already use for data, so a table cell reads like every
   other value in this card vocabulary. */
.card-table__cell {
  padding: var(--space-xs) var(--space-md) var(--space-xs) 0;
  font-family: var(--font-mono);
  color: var(--text);
  white-space: nowrap;
  overflow-wrap: normal;
  border-bottom: 1px solid var(--border);
}

.card-table__row:last-child .card-table__cell {
  border-bottom: 0;
}

/* Same status vocabulary as the list rows above (card-sections.css:400-431) —
   a done row here and a done row in a list read the same tokens. */
.card-table__row--done .card-table__cell {
  color: var(--accent-success-text);
}

.card-table__row--active .card-table__cell {
  font-weight: var(--weight-medium);
  color: var(--text-bright);
}

.card-table__row--pending .card-table__cell {
  color: var(--text-faint);
}

.card-table__row--missed .card-table__cell {
  color: var(--text-muted);
}

/* The row that matters: brightness + weight, not a background slab. AFTER the
   status tints on purpose — same specificity, so source order decides, and
   "this is the row" must beat "this row is pending", which would otherwise
   render the one emphasised row the faintest of all. */
.card-table__row--emphasis .card-table__cell {
  font-weight: var(--weight-medium);
  color: var(--text-bright);
}

.card-table__row--missed .card-table__cell:first-child {
  text-decoration: line-through;
  text-decoration-color: var(--border);
}

/* =============================================================================
   Card foot — byline + actions row BELOW the section stack. A sibling of the
   sections, not one of them, so each wrapper (chat stream, journey detail,
   timeline) can swap the action set without disturbing the card body.
   ============================================================================= */

.chat-card__foot {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid var(--border);
}

.chat-card__byline {
  font-family: var(--font-coach);
  font-style: italic;
  color: var(--text-faint);
  font-size: var(--text-sm);
}

.chat-card__acts {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-muted);
  display: flex;
  align-items: baseline;
}

.chat-card__acts .sep {
  color: var(--text-faint);
  margin: 0 0.4em;
}

.chat-card__acts a,
.chat-card__acts button {
  background: none;
  border: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
  padding: 0;
}

.chat-card__acts a:hover,
.chat-card__acts button:hover {
  color: var(--text);
}

/* =============================================================================
   Live stub fill — streaming.js owns every class below: it builds the header
   (buildStreamingHeader), marks the card `.card--streaming`, and adds
   --enter/--in per streamed section (openDetailFillStream).
   ============================================================================= */

/* A fill's progress sits where its finished card lands something — an action
   pill's slot on a detail card, the coach's note on a poster — so nothing
   moves when it arrives. */
.card-fill-indicator {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  color: var(--text-muted);
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
}

.card-fill-indicator .typing-indicator {
  padding: 0;
}

/* A pill like its neighbours, only not pressable yet. */
.action-pill--pending {
  pointer-events: none;
}

.action-pill--pending .card-fill-indicator {
  color: inherit;
  font-size: inherit;
  font-weight: inherit;
}

/* Each section fades + rises on its FIRST appearance only; enrichment (a
   section re-rendering as more fields arrive) swaps silently. */
.card--streaming .card-section--enter {
  opacity: 0;
  transform: translateY(var(--space-xs));
}

.card--streaming .card-section--in {
  opacity: 1;
  transform: none;
  transition: opacity var(--transition-normal), transform var(--transition-normal);
}

@media (prefers-reduced-motion: reduce) {
  .card--streaming .card-section--enter,
  .card--streaming .card-section--in {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* =============================================================================
   RULED DETAIL — the stat row's cells, the list and segment rows, and the
   eyebrow's separators.
   ============================================================================= */

/* ─── stat_row: ruled cells under a solid top rule ─── */

/* The cell's own rules — scoped to the row, so the hero's single stat (which
   reuses this vocabulary) stays unruled. The -1px overlaps each divider onto
   its neighbour's edge and hangs the first cell of every row outside the
   section, where the clip above eats it. */
.card-section--stat_row .card-stat {
  margin-left: -1px;
  border-top: 1.5px solid var(--border);
  border-left: 1px dashed var(--paper-rule-faint);
}

/* Scoped to the row: a hero's stat is the celebration's number and keeps its
   display type (.card-hero__stat .card-stat__value). Emphasis inside the row
   needs no rule of its own — it inherits this size and the base rule's accent,
   and a taller cell would break the rule grid anyway. */
.card-section--stat_row .card-stat__value {
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
}

/* ─── list: one line per session, tight enough to read as a week ─── */

/* `center`, not baseline: the row carries a kicker stamp, a 20px mark and a
   label at three different sizes. */
.card-list__row,
.card-list__hook {
  align-items: center;
  gap: 10px;
  min-height: 22px;
}

.card-list__label {
  font-size: 14px;
}

/* The label reads a half-step up from the weekly summary's, scoped by the glyph
   COLUMN, so only a list that reserves one (ListData::any_glyph) takes the
   larger size. */
.card-section--list:has(> li > .card-list__hook > .card-list__glyph)
  .card-list__label {
  font-size: 14.5px;
}

/* ─── segments: the session's structure, as data rows ─── */

.card-section--segments:not(:has(> .card-section__label)) {
  grid-template-columns: 1fr;
}

.card-section--segments > .card-section__label {
  grid-column: 1;
  grid-row: 1;
  font-size: 10px;
  letter-spacing: 0.1em;
  color: var(--text-faint);
}

.card-segment__distance {
  color: var(--text);
}

/* The descriptor takes the row's voice — a data row, not a sentence.
   `flex: 0 1 auto` so it yields the slack to the heart rate's right edge
   instead of eating it. */
.card-segment__descriptor {
  flex: 0 1 auto;
}

/* Heart rate closes the row at the right edge — the column the eye scans. */
.card-segment__hr {
  margin-left: auto;
}

/* ─── eyebrow: ·-separated segments ─── */

/* The eyebrow reads as one stamped line, so the segments need a separator of
   their own; the flex gap alone reads as two unrelated stamps. It hangs off the
   PRECEDING segment so a phone-width wrap leaves the dot trailing line 1 rather
   than opening line 2 with an orphan. One source of spacing per side of the
   dot: this margin on its left, the eyebrow's column gap on its right. */
.card-eyebrow__seg:not(:last-child)::after {
  content: "·" / "";
  margin-left: var(--space-sm);
}

/* =============================================================================
   THE POSTER — a proposed session on paper. The card shell (2-column grid, no
   tape, no tilt) is chat-cards.css; this is what lives inside it. The scope is
   `data-variant="poster"`, stated on the wire by `workout_card`.
   ============================================================================= */

/* The shell zeroes its own padding and row gap, so every section pays for its
   own rhythm. Left column by default; the stat row claims column 2 below. */
.chat-card[data-variant="poster"] > .card-section {
  grid-column: 1;
  padding: 0 26px 14px;
}

.chat-card[data-variant="poster"] > .card-section:first-child {
  padding-top: 22px;
}

.chat-card[data-variant="poster"] > .card-section:last-child {
  padding-bottom: 22px;
}

/* The numbers, sunk into a ruled panel down the right edge: one reading per
   row, label over value, hairline-ruled between — not the paper card's ruled
   cells. `span 20` is "however many sections this poster has" — the shell's
   `gap: 0` makes the unused rows free, and the rows rule themselves, so the
   section carries no gap of its own. */
.chat-card[data-variant="poster"] > .card-section--stat_row {
  grid-column: 2;
  grid-row: 1 / span 20;
  align-self: stretch;
  flex-direction: column;
  flex-wrap: nowrap;
  /* Stated, not inherited: each row has to span the column's full width or its
     rule stops short of the panel's edges. */
  align-items: stretch;
  gap: 0;
  margin: 0;
  padding: 22px 20px;
  border-left: 1.5px dashed var(--paper-rule);
  background: var(--paper-sunken);
}

/* `column`, not the base grammar's column-reverse: in the panel the label is
   the heading of its reading, not its caption. The dashed rule sits on top of
   each row, so the first row and the last edge stay flush with the padding.
   `flex: 0 0 auto` because the axis turned: the base `1 1 5.5rem` is a width
   basis in a wrapping row, and a HEIGHT basis that grows in this column — 88px
   rows in a panel whose rows measure 61. */
.chat-card[data-variant="poster"] .card-stat {
  flex: 0 0 auto;
  flex-direction: column;
  align-items: flex-start;
  gap: 3px;
  padding: 12px 0;
  margin-left: 0;
  border: 0;
  border-top: 1px dashed var(--paper-rule-faint);
}

.chat-card[data-variant="poster"] .card-stat:first-child {
  border-top: 0;
  padding-top: 0;
}

.chat-card[data-variant="poster"] .card-stat:last-child {
  padding-bottom: 0;
}

/* `overflow-wrap: anywhere` (top of this file) otherwise breaks `HR` into
   `H` / `R` when the panel narrows — the compact card carries the same fix
   below. */
.chat-card[data-variant="poster"] .card-stat__label {
  overflow-wrap: normal;
  font-size: 10px;
  letter-spacing: 0.1em;
  color: var(--text-faint);
}

/* Body type, not display: the panel is a reading list. A baseline row, so the
   unit and the effort dots ride the value's own baseline. */
.chat-card[data-variant="poster"] .card-stat__value {
  font-family: var(--font-body);
  font-size: 17px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: left;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px;
}

.chat-card[data-variant="poster"] .card-stat__unit {
  font-size: 13px;
}

/* The one surface that shows the effort scale (the geometry is at the top of
   this file); everywhere else the number alone carries it. */
.chat-card[data-variant="poster"] .card-stat__dots {
  display: flex;
}

/* The title is the poster: display type, tight, one line's worth of leading.
   `flex-start`, not the base rule's baseline: a title that wraps must keep its
   mark beside its FIRST line, not floating at the middle of the block. */
.chat-card[data-variant="poster"] .card-heading {
  align-items: flex-start;
  font-size: 40px;
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1;
}

/* `align-self: center` (the base rule, written for a one-line heading) beats
   the `align-items` above, so the mark has to state its own. The margin re-
   centres it INSIDE the first line box, which `line-height: 1` makes exactly
   1em tall — so it follows the title down to the phone size below. */
.chat-card[data-variant="poster"] .card-heading__icon {
  align-self: flex-start;
  margin-top: calc((1em - 24px) / 2);
}

/* The one-line summary belongs to the title, not to the section under it: the
   header's own gap is tuned for a 40px display face and leaves the sentence
   floating a line away from it. */
.chat-card[data-variant="poster"] .card-subtitle {
  margin-top: -6px;
}

/* ─── the effort profile ─── */

.chat-card[data-variant="poster"] .card-meter--spark {
  height: 44px;
  gap: 2px;
  border-bottom: 1.5px solid var(--border);
}

.chat-card[data-variant="poster"] .card-meter__spark {
  border-radius: 2px 2px 0 0;
  background: color-mix(in srgb, var(--ember-sage) 85%, transparent);
  transform-origin: bottom;
  animation-name: meter-bar-rise;
  animation-duration: 500ms;
  animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
  animation-fill-mode: backwards;
}

.chat-card[data-variant="poster"] .card-meter__labels {
  font-size: 10px;
  letter-spacing: 0.1em;
}

/* The plan draws itself left to right, 30ms apart — SPEC §7. Written out
   because CSS has no index to compute a delay from, and left UNSCOPED for the
   same reason: twelve copies of the poster selector to carry one number each.
   A delay with no animation-name is inert, so every other spark meter ignores
   these. */
@keyframes meter-bar-rise {
  from {
    transform: scaleY(0);
  }
  to {
    transform: scaleY(1);
  }
}

.card-meter__spark:nth-child(1) { animation-delay: 0ms; }
.card-meter__spark:nth-child(2) { animation-delay: 30ms; }
.card-meter__spark:nth-child(3) { animation-delay: 60ms; }
.card-meter__spark:nth-child(4) { animation-delay: 90ms; }
.card-meter__spark:nth-child(5) { animation-delay: 120ms; }
.card-meter__spark:nth-child(6) { animation-delay: 150ms; }
.card-meter__spark:nth-child(7) { animation-delay: 180ms; }
.card-meter__spark:nth-child(8) { animation-delay: 210ms; }
.card-meter__spark:nth-child(9) { animation-delay: 240ms; }
.card-meter__spark:nth-child(10) { animation-delay: 270ms; }
.card-meter__spark:nth-child(11) { animation-delay: 300ms; }
.card-meter__spark:nth-child(12) { animation-delay: 330ms; }

/* =============================================================================
   THE COMPACT CARD — one completed session, one line in the log. Shell (flex
   row, tape, tilt) is chat-cards.css.
   ============================================================================= */

/* WHOLE WORDS. `.card-section`'s `min-width: 0; overflow-wrap: anywhere` (top
   of this file) was written for a COLUMN stack, where a long token would widen
   the card; laid out in a ROW it lets a sibling squeeze the title to zero and
   break it at any character — "Sh / ort / Ea / sy / Ru / n". The floor is the
   longest word, and the break stays at the spaces. */
.chat-card[data-variant="compact"] > .card-section {
  flex: 0 1 auto;
  min-width: min-content;
  overflow-wrap: normal;
}

.chat-card[data-variant="compact"] .card-section--header {
  flex-direction: row;
  align-items: center;
  gap: 10px;
}

/* 15/700 — SPEC §2.2. A literal like the 12.5px meta and 22px mark below it:
   the compact line is set in its own sizes, not the body scale's. */
.chat-card[data-variant="compact"] .card-heading {
  font-family: var(--font-display);
  font-size: 15px;
  font-weight: var(--weight-bold);
}

/* The mark carries --card-accent, which is --accent-success on a completed
   card; only the size is the compact card's business. */
.chat-card[data-variant="compact"] .card-heading__icon {
  width: 22px;
  height: 22px;
}

/* Done is a badge ON THE MARK, not a tick after the title — the title reads as
   its own words again, and the state sits where the eye already is. BOTTOM-LEFT
   because bottom-right is taken: `.steev-icon__badge` (icons.css) hangs the
   mark's own role badge there on a goal, milestone, PB or imported run.
   Presentation, like the eyebrow's brackets: alt text "" keeps it out of the
   accessibility tree (the onboarding checklist's precedent). `.steev-icon-wrap`
   already supplies the positioning context (icons.css). */
.chat-card[data-variant="compact"] .card-heading__icon::after {
  content: "✓" / "";
  position: absolute;
  left: -5px;
  bottom: -4px;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--accent-success);
  color: var(--surface-raised);
  font-family: var(--font-mono);
  font-size: 9px;
  line-height: 12px;
  text-align: center;
}

/* The day stays; type and tags go. The gutter stamp is the MESSAGE's day, not
   the session's, and a week of easy runs needs each line to say which one it
   is (owner, 2026-09-17). The type is the log line's noise. */
.chat-card[data-variant="compact"] .card-eyebrow__seg:not(.card-eyebrow__seg--day) {
  display: none;
}

.chat-card[data-variant="compact"] .card-eyebrow__seg--day::after {
  content: none;
}

.chat-card[data-variant="compact"] .card-section--stat_row {
  flex-wrap: nowrap;
  align-items: baseline;
  gap: 0 6px;
}

.chat-card[data-variant="compact"] .card-stat {
  flex: 0 0 auto;
  flex-direction: row;
  align-items: baseline;
  gap: 4px;
  padding: 0;
  margin-left: 0;
  border: none;
}

/* One line, VALUES only. The server composes the compact stat row so every
   value speaks for itself — a scale rides along as the unit run ("7 /10") — so
   this file needs neither a position rule (a session with no distance leads
   with Time) nor the label text (that is copy). */
.chat-card[data-variant="compact"] .card-stat__label,
.chat-card[data-variant="compact"] .card-feel__label {
  display: none;
}

.chat-card[data-variant="compact"] .card-section--stat_row .card-stat__value {
  font-family: var(--font-body);
  font-size: 12.5px;
  font-weight: var(--weight-normal);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  color: var(--text-muted);
}

.chat-card[data-variant="compact"] .card-stat__unit {
  font-size: 11px;
  font-weight: inherit;
  color: inherit;
}

.chat-card[data-variant="compact"] .card-stat + .card-stat::before {
  content: "· " / "";
  color: var(--text-faint);
}

/* The feel word closes the line, in the runner's own hand — set down at the
   angle a hand writes at, off the bottom-left corner where the word starts. */
.chat-card[data-variant="compact"] .card-section--feel {
  flex: 0 0 auto;
  margin-left: auto;
  font-family: var(--font-annotation);
  font-size: 17px;
  color: var(--accent-success-text);
  transform: rotate(-2deg);
  transform-origin: bottom left;
}

.chat-card[data-variant="compact"] .card-feel__value {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
}

@media (max-width: 760px) {
  /* One column: the stat panel becomes a bottom band. It keeps the stack —
     one reading per ruled row is the grammar at every width, not a layout the
     desktop column happens to have room for. */
  .chat-card[data-variant="poster"] > .card-section--stat_row {
    grid-column: 1;
    grid-row: auto;
    border-left: none;
    border-top: 1.5px dashed var(--paper-rule);
  }

  /* The iOS scale (SPEC §4.2): 40px runs a three-word title to three lines in
     a phone column, and the mark then has three lines to hang beside. */
  .chat-card[data-variant="poster"] .card-heading {
    font-size: 30px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .chat-card[data-variant="poster"] .card-meter__spark {
    animation: none;
  }
}

/* =============================================================================
   CHAT-CARDS.CSS — Unified card system for chat-area structured content

   Cards are structured chat content (workout posters, goal cards, summaries),
   drawn as index cards. Each card type sets --card-accent, which colours
   eyebrows, glyphs, meters and marks; the surface and the border stay neutral.
   Voice positioning is handled by [data-voice] in chat-marginalia.css.
   ============================================================================= */

/* ─── Base ─── */

.chat-card {
  --card-accent: var(--accent-coach);
  --card-accent-text: var(--accent-coach-text);
  position: relative;
  padding: 20px 22px 16px;
  border: 1.5px solid var(--border);
  border-radius: 4px;
  background: var(--surface-raised);
  box-shadow: var(--shadow-hard-sm);
  align-self: flex-start;
  max-width: 88%;
  font-size: var(--text-sm);
}

/* ─── Accent variants ─── */

/* Column-flex stacks the body text and workout poster as direct children. */
.chat-card--coach {
  --card-accent: var(--accent-coach);
  --card-accent-text: var(--accent-coach-text);
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}
.chat-card--action {
  --card-accent: var(--accent-action);
  --card-accent-text: var(--accent-action-text);
}
.chat-card--success {
  --card-accent: var(--accent-success);
  --card-accent-text: var(--accent-success-text);
}
.chat-card--danger {
  --card-accent: var(--accent-danger);
  --card-accent-text: var(--accent-danger-text);
}

/* Timeline node type aliases */
.chat-card--completed {
  --card-accent: var(--text-muted);
  --card-accent-text: var(--text-muted);
}
.chat-card--proposed {
  --card-accent: var(--accent-coach);
  --card-accent-text: var(--accent-coach-text);
}
.chat-card--milestone {
  --card-accent: var(--accent-strength);
  --card-accent-text: var(--accent-strength-text);
}
.chat-card--goal {
  --card-accent: var(--accent-danger);
  --card-accent-text: var(--accent-danger-text);
}

/* When a chat-card is hosted inside the global dialog (e.g. leaderboard
   "Log workout" button reusing the same card as the coach page), let it
   span the dialog width and center on the page instead of pinning left
   or right like it would in the chat stream. The form is wrapped in
   .exchange-group > .user-workout-card to put it on the user side of the
   chat — those wrappers exist inside the dialog too, so we override
   their alignment + width. */
dialog#modal-container .exchange-group,
dialog#modal-container .user-workout-card {
  align-self: center;
  max-width: 100%;
  width: 100%;
}

dialog#modal-container .chat-card {
  align-self: center;
  width: 100%;
  max-width: var(--content-max-width);
  max-height: 90vh;
  overflow-y: auto;
}

/* ─── Entrance animation ─── */

.chat-card--animated {
  animation: card-enter 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes card-enter {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* Fill reconcile: the streamed preview already showed this content, so the
   canonical card must not replay its entrance animation when it swaps in —
   that re-pop is what made the end of streaming jump. */
.detail--reconciling .chat-card--animated {
  animation: none;
}

/* ─── Card actions ─── */

.card-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  margin-top: var(--space-md);
  padding-top: var(--space-sm);
  border-top: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
}

/* ─── Timeline detail context ─── */

.timeline-detail > .chat-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.timeline-detail > .chat-card > .detail-close {
  position: absolute;
  top: var(--space-xs);
  right: var(--space-xs);
  z-index: 1;
}

.timeline-detail > .chat-card--goal {
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

/* Compact card hidden while its in-place poster (#chat-node-poster) stands in
   for it; streaming.js applies and removes it. Two classes to beat
   `.chat-card.card` (card-sections.css, same weight, imported earlier); the
   variant-bearing form, three, to beat the `.chat-card[data-variant="…"]`
   display rules later in this file — or a compact card keeps its row and the
   poster lands UNDER it. */
.chat-card.chat-card--poster-hidden,
.chat-card[data-variant].chat-card--poster-hidden {
  display: none;
}

/* ─── Size variants ─── */

/* After `.chat-card` at the same weight, so a summary pill stays a pill and a
   form card stays a form card. */
.chat-card--compact {
  padding: var(--space-xs) var(--inset-sm);
}

.chat-card--expanded {
  padding: var(--space-md) var(--inset-lg);
  width: 88%;
}

/* The celebration card, deliberately bigger than its siblings. No glow: a lit
   3px top edge and a gold wash carry it, so a hero-bearing card reads as an
   event in the stream instead of another update, and reads as one at any
   elevation the surface around it happens to have. The wash is --moment-wash,
   which is where the scheme's own 10/8 split lives (tokens.css); a gradient of
   one colour is how a translucent wash layers over an opaque surface, since a
   colour is only legal as the LAST background layer. --accent-info is coach
   voice and is never borrowed here.

   NOT an accent variant: written `.chat-card.chat-card--moment` so it outranks
   `.chat-card`'s padding, background and top border. */
.chat-card.chat-card--moment {
  --card-accent: var(--accent-moment);
  --card-accent-text: var(--accent-moment-text);
  padding: 26px 30px 22px;
  border-top: 3px solid var(--accent-moment);
  background: linear-gradient(var(--moment-wash), var(--moment-wash)),
    var(--surface-raised);
}

/* Tape + tilt: entity-carrying cards only — the index-card metaphor is for
   workouts, goals and milestones, not for form or summary cards. `::after`,
   never `::before`: journey-stage.css owns the card's ::before type label.
   `chat-workout-` is the same entity card re-rendered after an edit.
   The poster is excluded at the source rather than un-tilted afterwards: it
   never wore tape, so it has nothing to straighten and no counter-rule to keep
   ahead of the reduced-motion twin at the bottom of this file. */
.chat-card:is([id^="workout-"], [id^="chat-workout-"]):not([data-variant="poster"]) {
  --card-rest-tilt: rotate(-0.4deg);
  margin-top: 12px;
  transform: var(--card-rest-tilt);
  transition: transform 0.2s;
}

.chat-card:is([id^="workout-"], [id^="chat-workout-"]):not([data-variant="poster"]):hover {
  transform: rotate(0deg);
}

.chat-card:is([id^="workout-"], [id^="chat-workout-"]):not([data-variant="poster"])::after {
  content: "";
  position: absolute;
  top: -11px;
  left: 50%;
  width: 88px;
  height: 22px;
  transform: translateX(-50%) rotate(1.5deg);
  background: color-mix(in srgb, var(--ember-gold) 35%, transparent);
  border-left: 1px dashed var(--paper-ink-faint);
  border-right: 1px dashed var(--paper-ink-faint);
  pointer-events: none;
}

/* ─── The proposed-workout POSTER ─── */

/* A proposal is the ledger's headline, not another index card: two columns —
   the session on the left, its numbers sunk into a ruled panel on the right —
   edge to edge, and (SPEC §7) NO tape and NO tilt. It keeps `card-enter` and
   nothing else. Border, radius and shadow come from the base `.chat-card`.

   `data-variant="poster"` is stated on the wire (`http/api/card.rs`): the
   composition-shaped selector this replaced also matched the standalone
   edit/complete card, which never renders through card.html and so could not
   opt out. */
.chat-card[data-variant="poster"] {
  display: grid;
  grid-template-columns: 1fr 216px;
  grid-auto-rows: max-content;
  /* 0, not the card's gap: the stat column spans a fixed 20 rows so it can
     stretch beside however many sections a poster has, and a row gap would
     charge for every empty one. Sections carry their own padding instead
     (card-sections.css). */
  gap: 0;
  padding: 0;
  max-width: 100%;
  overflow: hidden;
}

/* No numbers, no panel: a stub poster has no stat row, and the reserved 216px
   column would otherwise draw as an empty sunken band down the right edge. */
.chat-card[data-variant="poster"]:not(:has(> .card-section--stat_row)) {
  grid-template-columns: 1fr;
}

/* ─── The ledger's completed card ─── */

/* One line in the log: mark, title, the numbers, the feel word. It KEEPS the
   tape and the tilt — it is still an index card, just a small one. */
.chat-card[data-variant="compact"] {
  display: flex;
  flex-direction: row;
  align-items: center;
  /* Wrap, not overflow: the sections refuse to shrink below their content
     (card-sections.css re-arms the min-content floor the row layout removes),
     so a long title beside a six-stat line drops the stats to a second full-
     width line instead of pushing them past the card's edge. */
  flex-wrap: wrap;
  gap: 14px;
  padding: 12px 16px;
  max-width: 520px;
}

@media (max-width: 760px) {
  /* The stat column becomes a bottom band (card-sections.css does the rest). */
  .chat-card[data-variant="poster"] {
    grid-template-columns: 1fr;
  }

  .chat-card[data-variant="compact"] {
    max-width: 100%;
  }
}

/* ─── Reduced motion ─── */

@media (prefers-reduced-motion: reduce) {
  .chat-card--animated {
    animation: none;
  }

  /* Policy in coach-page.css: the hover un-tilt goes, the resting tilt stays —
     dropping it left the tape askew at +1.5° on its own. The poster carries
     neither, so it is excluded here for the same reason it is above. */
  .chat-card:is([id^="workout-"], [id^="chat-workout-"]):not([data-variant="poster"]):hover {
    transform: var(--card-rest-tilt);
  }
}

/* ─── Arrival streak line ─── */

.streak-line {
  color: var(--accent-info-text);
  font-size: var(--text-sm);
  margin-bottom: var(--space-xs);
}

/* ─── Live card (render_card streamed while the turn writes) ─── */

/* Open but not yet filled: the frame arrives before its first section, and an
   empty bordered box under the bubble reads as a broken card. */
.chat-card.card--streaming:empty {
  display: none;
}

/* On the form-submit flow the live card lands inside #chat-loading-stub, a
   plain block — neither the exchange-group's gap nor .chat-card's align-self
   reaches it there. Flex WITHOUT a gap keeps the scribble/tool-status stack
   exactly as it was; the two sibling rules supply the group's own rhythm on
   whichever side the card sits. Gated on `:has` so a turn without a live card
   keeps the block layout. */
#chat-loading-stub:has(> .card--streaming) {
  display: flex;
  flex-direction: column;
}

#chat-loading-stub > .card--streaming:not(:last-child) {
  margin-bottom: var(--space-sm);
}

#chat-loading-stub > .card--streaming:not(:first-child) {
  margin-top: var(--space-sm);
}

/* =============================================================================
   CHAT-ERROR.CSS - Error bubble + expandable diagnostic details

   Typographic only — no heavy background/border pill. The user-visible
   message sits inline with a details button (⚠︎); diagnostic keys/values
   expand via native <details> beneath a hairline separator.

   The bubble is partials/chat_error.html, rendered by
   `llm_pipeline::render_web_error`.
   ============================================================================= */

.scribble-text.chat-error {
  color: var(--accent-danger-text);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-xs);
}

.chat-error-message {
  flex: 1 1 auto;
  min-width: 0;
}

.chat-error-details {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.chat-error-details > summary {
  list-style: none;
}

.chat-error-details > summary::-webkit-details-marker,
.chat-error-details > summary::marker {
  display: none;
}

.chat-error-info-btn {
  cursor: pointer;
  padding: 0 var(--space-xs);
  font-size: var(--text-base);
  line-height: 1;
  color: var(--text-muted);
  opacity: 0.7;
  user-select: none;
  transition: opacity var(--transition-fast) ease;
}

.chat-error-info-btn:hover,
.chat-error-details[open] .chat-error-info-btn {
  opacity: 1;
  color: var(--accent-danger-text);
}

.chat-error-body {
  flex: 1 1 100%;
  margin-top: var(--space-xs);
  padding-top: var(--space-xs);
  border-top: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.chat-error-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.chat-error-key {
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.chat-error-val {
  color: var(--text);
  word-break: break-word;
}

/* =============================================================================
   CHAT-MARGINALIA.CSS

   Training-notebook layout for the chat surface. The runner's notes sit in
   the right margin; the coach writes against a pen stroke on the left.
   Page-level items (date separators, journey strip, empty state) span the
   full width.

   ONE LAYOUT, ALL SIZES — no device breakpoint and no grid. The chat zone is
   an inline-size container; the day stamp's gutter reads it (2026-09-06) and
   collapses where the conversation has no room to spare, so the two states
   are one layout answering its own width rather than a phone design beside a
   desktop one.

   Layout lives on [data-voice], not on .exchange-group. .exchange-group
   is a thin shell carrying mood class + entrance animation; one group can
   contain BOTH a user entry AND a coach scribble, so it does not itself
   carry a voice.
   ============================================================================= */

.coach-inline {
  container-type: inline-size;
  container-name: chat;
  --chat-bracket-size: 5px;
  --chat-bracket-color: var(--accent-info);
}

.exchange-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

/* A resumed onboarding card is OOB-deleted out of the wrapper the server sent
   it in; the widow group must not go on eating a stream gap. */
.exchange-group:empty {
  display: none;
}

/* ─── Entry (runner's note) ────────────────────────────────────────── */

.entry-body {
  margin: 0;
  color: var(--tag-text-user);
}

/* ─── Scribble (coach's margin-note) ───────────────────────────────── */

.scribble {
  position: relative;
  padding-left: calc(var(--chat-bracket-size) + 16px);
}

.scribble-bracket {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: var(--chat-bracket-size);
  /* The streaming stub's scribble is empty until the first token lands; without
     a floor the stroke would have no height to draw beside the typing dots. */
  min-height: 18px;
  border-radius: 4px;
  background: linear-gradient(
    var(--chat-bracket-color),
    color-mix(in srgb, var(--chat-bracket-color) 15%, transparent)
  );
  /* The completion pulse and outburst gestures scale/rotate this box — a
     full-height stroke must grow from the pen's start, not its middle. */
  transform-origin: top left;
}

/* Prose stops at a readable measure; the entry around it keeps the 88% so
   cards and stat grids still spend the whole column. Without this, widening
   the shell just makes the coach's lines longer, which is the opposite of
   readable. */
.scribble-text {
  max-width: var(--coach-prose-measure);
  margin: 0;
  font-family: var(--font-coach);
  font-size: var(--chat-prose-size);
  line-height: 1.6;
  color: var(--tag-text-info);
  letter-spacing: 0.005em;
}

.scribble--fallback .scribble-text {
  color: var(--text-muted);
  cursor: help;
}

.scribble--celebration .scribble-text {
  color: var(--accent-info-text);
}

/* ─── Receipt rows ──────────────────────────────────────────────────
   The server's own account of what a turn changed where the reply and its
   card show nothing (a moved/updated/removed/added calendar item). This is
   NOT the coach speaking, so it never takes --accent-info — muted ink only.
   Quiet rows, no buttons: the same restraint `cta_row` renders answers with
   (card-sections.css), a hairline between two lines and nothing else. Sits
   beside `.scribble`, not inside it, so it renders even when the reply text
   is empty and the scribble itself is absent. */

.receipt-rows {
  display: flex;
  flex-direction: column;
}

.receipt-row {
  margin: 0;
  padding: var(--space-xs) 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
  line-height: 1.4;
}

.receipt-row + .receipt-row {
  border-top: 1px solid var(--border);
}

/* ─── Mood tint (A1) ────────────────────────────────────────────────
   The persisted mood-* class on .exchange-group (server: chat.rs
   build_exchanges; live: streaming.js applyMood) retints the EXISTING
   bracket — no new chrome, no accent bars. Neutral stays on
   the .coach-inline default (--accent-info). Light mode re-resolves via
   the tokens.css media query for free. */

.mood-proud     { --chat-bracket-color: var(--accent-coach); }
.mood-concerned { --chat-bracket-color: var(--accent-danger); }
.mood-curious   { --chat-bracket-color: var(--accent-action); }
.mood-amused    { --chat-bracket-color: var(--accent-accent); }
.mood-tired     { --chat-bracket-color: var(--accent-recovery); }

/* ─── Coach reply markdown ──────────────────────────────────────────
   The reply is rendered from the model's markdown by the `md` filter, so the
   bubble (a <div class="scribble-text">) holds block children — paragraphs, the
   odd list — plus inline strong/em. Collapse the outer margins to keep the
   bubble tight. The coach voice is upright now, so the model's markdown reads
   cleanly: **bold** is the marker, *italic* its natural slant — two distinct
   signals it can reach for. */
.scribble-text > :first-child {
  margin-top: 0;
}
.scribble-text > :last-child {
  margin-bottom: 0;
}
.scribble-text p {
  margin: 0 0 var(--space-sm) 0;
}
/* Markdown children must keep the coach voice colour. Global element rules
   (base.css `p` → --text-secondary, headings → --text-bright) would otherwise
   override the inherited --tag-text-info, leaving paragraphs dark while list
   items / emphasis stayed mauve — a split-colour bubble. */
.scribble-text :is(p, li, h1, h2, h3, h4, h5, h6) {
  color: inherit;
}
.scribble-text ul,
.scribble-text ol {
  margin: var(--space-sm) 0;
  padding-left: var(--inset-sm);
}
/* Defensive: the coach shouldn't reach for headings, but if one slips through
   keep it at bubble scale rather than blowing up the layout. */
.scribble-text :is(h1, h2, h3, h4, h5, h6) {
  font-size: inherit;
  font-weight: 600;
  margin: 0 0 var(--space-sm) 0;
}

/* ─── Completion bracket pulse ─────────────────────────────────────── */

.scribble--completed .scribble-bracket {
  animation: scribble-bracket-pulse 600ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

@keyframes scribble-bracket-pulse {
  0%   { transform: scale(1);    opacity: 1; }
  35%  { transform: scale(1.4);  opacity: 1; }
  100% { transform: scale(1);    opacity: 0.7; }
}

@media (prefers-reduced-motion: reduce) {
  .scribble--completed .scribble-bracket { animation: none; }
}

/* ─── Outburst arrival + settle mark (A2) ──────────────────────────
   outburst-laugh / outburst-gasp land on .exchange-group when the SSE
   mood event carries one (streaming.js applyMood) and stay for the
   session; outburst-settled follows ~1.8s later and freezes the
   animation. Not persisted — a reload shows no mark (outbursts are
   transient by design, mood.rs). The mark mirrors the face's outburst
   mouth: laugh = wide grin-slit, gasp = a small O.

   Source order is load-bearing: these rules sit AFTER the completion
   pulse above (equal specificity, later wins — the outburst gesture
   replaces the generic pulse), and .outburst-settled comes last so it
   beats both. */

.outburst-laugh .scribble-bracket {
  animation: outburst-jiggle 700ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

.outburst-gasp .scribble-bracket {
  animation: outburst-pop 500ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

@keyframes outburst-jiggle {
  0%, 100% { transform: rotate(0) scale(1); }
  20%      { transform: rotate(-8deg) scale(1.25); }
  45%      { transform: rotate(6deg) scale(1.15); }
  70%      { transform: rotate(-3deg) scale(1.05); }
}

@keyframes outburst-pop {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.5); }
  100% { transform: scale(1); }
}

/* The settle mark — hangs just below the bracket corner on the margin
   line. Base state IS the settled state (opacity 0.7); the entrance
   keyframes only decorate the arrival. */
.outburst-laugh .scribble-bracket::after,
.outburst-gasp .scribble-bracket::after {
  content: "";
  position: absolute;
  left: 1px;
  top: calc(100% + 6px);
  opacity: 0.7;
  animation: outburst-mark-in 600ms 150ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

.outburst-laugh .scribble-bracket::after {
  width: 7px;
  height: 3px;
  border-radius: var(--radius-full);
  background: var(--chat-bracket-color);
}

.outburst-gasp .scribble-bracket::after {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  border: 1.5px solid var(--chat-bracket-color);
  background: transparent;
}

@keyframes outburst-mark-in {
  from { opacity: 0; transform: scale(0.2); }
  60%  { opacity: 1; transform: scale(1.3); }
  to   { opacity: 0.7; transform: scale(1); }
}

/* Settled: freeze everything; the base styles ARE the record. Last in
   source order so it beats both the outburst entrances above and the
   completion pulse. */
.outburst-settled .scribble-bracket,
.outburst-settled .scribble-bracket::after {
  animation: none;
}

@media (prefers-reduced-motion: reduce) {
  .outburst-laugh .scribble-bracket,
  .outburst-gasp .scribble-bracket,
  .outburst-laugh .scribble-bracket::after,
  .outburst-gasp .scribble-bracket::after {
    animation: none;
  }
}

/* ─── Big-moment face (A7) — transient theatrical entrance ─────────
   Cloned from #face-moment-template by streaming.js onto the landed
   coach bubble when a celebration or outburst turn completes (one per
   turn); streaming.js removes the node on animationend. Never persisted —
   history keeps only the celebration/mood styling. It hangs in the margin the
   coach note leaves free — the 12% to its RIGHT — so it shifts no layout and
   is never clipped. */

.face-moment {
  position: absolute;
  left: 100%;
  top: 0;
  width: min(13%, 56px);
  aspect-ratio: 1;
  pointer-events: none;
  transform-origin: 50% 100%;
  animation: face-moment-play 2600ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

.face-moment .steev-face {
  width: 100%;
  height: 100%;
}

@keyframes face-moment-play {
  0%   { opacity: 0; transform: translateY(8px) scale(0.4) rotate(-8deg); }
  12%  { opacity: 1; transform: translateY(0) scale(1.06) rotate(2deg); }
  20%  { transform: none; }
  80%  { opacity: 1; transform: none; }
  100% { opacity: 0; transform: translateY(3px) scale(0.85); }
}

/* Reduced motion: a pure opacity fade — the moment still lands, without
   spring or travel. animationend still fires, so JS cleanup is shared. */
@keyframes face-moment-fade {
  0%   { opacity: 0; }
  15%  { opacity: 1; }
  80%  { opacity: 1; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .face-moment {
    animation: face-moment-fade 2600ms ease both;
  }
}

/* ─── Exchange entrance (page load + HTMX-appended) ────────────────── */

@keyframes entry-arrive {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

.exchange-group {
  animation: entry-arrive var(--motion-arrive) both;
}

.exchange-group:nth-child(1) { animation-delay:   0ms; }
.exchange-group:nth-child(2) { animation-delay:  50ms; }
.exchange-group:nth-child(3) { animation-delay: 100ms; }
.exchange-group:nth-child(4) { animation-delay: 150ms; }
.exchange-group:nth-child(5) { animation-delay: 200ms; }
.exchange-group:nth-child(6) { animation-delay: 250ms; }
.exchange-group:nth-child(7) { animation-delay: 300ms; }
.exchange-group:nth-child(8) { animation-delay: 350ms; }
.exchange-group:nth-child(n+9) { animation-delay: 400ms; }

.exchange-group.exchange-arriving {
  animation: entry-arrive var(--motion-arrive) both;
  animation-delay: 0ms;
}

/* The coach's pen draws its own stroke as the reply starts (SPEC §7). Live
   turns only: history is already written.

   `:where()` on purpose — the group keeps `exchange-arriving` for its whole
   life, so this must LOSE to the gestures that fire later in the same turn
   (`.scribble--completed`, `.outburst-*`, both 0-2-0). At 0-1-0 it does, at any
   order in this file. `transform-origin: top left` is on the bracket. */
@keyframes pen-stroke-in {
  from { transform: scaleY(0); }
  to   { transform: scaleY(1); }
}

:where(.exchange-group.exchange-arriving) .scribble-bracket {
  animation: pen-stroke-in 500ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

@media (prefers-reduced-motion: reduce) {
  .exchange-group,
  .exchange-group.exchange-arriving { animation: none; }

  :where(.exchange-group.exchange-arriving) .scribble-bracket { animation: none; }
}

/* ─── Typing indicator (streaming) ─────────────────────────────────── */

.typing-indicator {
  display: flex;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--inset-sm);
}

.typing-indicator span {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--text-faint);
  animation: typing-bounce 1.8s infinite ease-in-out;
}

.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }

@keyframes typing-bounce {
  0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
  40%           { transform: scale(1);   opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .typing-indicator span { animation: none; opacity: 0.5; }
}

/* ─── Tool status (under streaming scribble) ───────────────────────── */

.tool-status {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--inset-sm);
  font-size: var(--text-xs);
  color: var(--text-muted);
  animation: fadeIn 0.2s ease-in;
}

/* ─── Date separator: [ TODAY · THURSDAY ] ─────────────────────────────
   The ledger stamp replaced this on /coach; the landing demo conversation still
   renders the class. Delete these with that demo, not before. */

.chat-date-separator {
  padding: var(--space-xs) 0;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-faint);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.chat-date-separator::before {
  content: "[ ";
}

.chat-date-separator::after {
  content: " ]";
}

/* ─── System line (budget notes, wall notice) ──────────────────────
   System voice, not coach voice: muted mono, full width — the same
   register as the date separator, left-aligned. */
.system-line {
  padding: var(--space-xs) 0;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--text-muted);
  letter-spacing: var(--tracking-wide);
}

.system-line-link {
  color: var(--text-secondary);
  text-decoration: underline;
}

/* ─── Composer (.entry-line) ───────────────────────────────────────── */

.entry-line {
  padding: var(--space-xs) 0 0;
  transition: opacity var(--motion-arrive), translate var(--motion-arrive);
}

/* Onboarding's finalize drops [hidden] from the composer, which otherwise
   snaps into existence. @starting-style is the before-change style for the
   display:none → rendered transition, so the band fades and lifts instead —
   and on a cold load it arrives on the same curve as .exchange-group. */
@starting-style {
  .entry-line {
    opacity: 0;
    translate: 0 var(--space-sm);
  }
}

@media (prefers-reduced-motion: reduce) {
  .entry-line {
    transition: none;
  }
}

/* A ruled line in the log, not a plate: one sage rule under the field, nothing
   else. */
.entry-line-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0 8px 2px;
  border-bottom: 2px solid var(--ember-sage);
}

/* 16px is the floor below which mobile Safari zooms a focused <input>, and this
   is a real one (chat_area.html, composer_walled.html) — NOT the prose size that
   happens to share the number today. Never point it at --chat-prose-size: the
   next chat re-tune would silently re-arm the zoom. */
/* The composer is the runner speaking, so what they type wears the runner's
   slant — the same italic their note carries once it lands in the timeline.
   Everything the COACH says stays upright; the slant is whose voice it is, not
   decoration. */
.entry-line-input {
  flex: 1;
  background: transparent;
  border: 0;
  color: var(--text);
  font-family: inherit;
  font-size: 16px;
  font-style: italic;
  outline: none;
}

/* The prompt speaks in the runner's voice, so it wears the runner's slant — the
   same italic their sent notes carry in the timeline, and the same one iOS sets
   on this one field. */
.entry-line-input::placeholder {
  color: var(--text-faint);
  font-style: italic;
}

/* Ink only, both states — no fill, no shadow, no pill. */
.entry-line-send {
  background: transparent;
  border: 0;
  color: var(--accent-coach-strong);
  font-family: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
  flex-shrink: 0;
  transition: color 0.15s;
}

.entry-line-send:disabled {
  color: var(--text-faint);
  cursor: not-allowed;
}

/* The fill glyph in the Send slot: a lone mark has no word's width, so the tap target is its own. */
.entry-line-fill {
  min-width: var(--tap-target);
  text-align: right;
}

.entry-line--disabled .entry-line-input {
  cursor: not-allowed;
}

/* ─── Seam action group ─────────────────────────────────────────────── */

.coach-actions-group {
  display: contents;
}

/* `display: contents` above outranks the UA's `[hidden] { display: none }`, so
   the attribute the onboarding guard writes (chat_area.html) has to be said
   here or the actions render right through the conversation. The separator
   that follows the group goes with it: the standing methodology link is the
   group's sibling, and through onboarding it is the seam's ONLY item — a
   leading dot would be all that survived. */
.coach-actions-group[hidden],
.coach-actions-group[hidden] + .entry-seam__sep {
  display: none;
}

/* The groups generate no box, so the pills are the flex items — without this
   they shrink and break their labels over two lines instead of scrolling. */
.coach-actions-group > * {
  flex: none;
}

/* Follow-up chips: outlined pills (the handoff's call, over quotes). The Log /
   Goal / Plan action items share the pill so the row is one height. */
.chat-chip,
.coach-action-item {
  padding: 8px 16px;
  border: 1.5px solid var(--paper-rule);
  border-radius: var(--radius-uneven-chip);
  background: transparent;
  font-family: inherit;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--text-secondary);
  cursor: pointer;
  transition: all 0.15s;
}

/* The get-the-app link + its dismiss, riding together inside their seam slot —
   the web twin of ComposerView's HStack. The slot outside them is what
   app-promo.js un-hides. */
.app-promo {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
}

/* An <a>, unlike its <button> siblings, arrives underlined. */
.app-promo .coach-action-item {
  text-decoration: none;
}

.app-promo-dismiss {
  background: transparent;
  border: 0;
  padding: var(--space-xs);
  color: var(--text-muted);
  font-family: inherit;
  font-size: var(--text-xs);
  line-height: 1;
  cursor: pointer;
}

.app-promo-dismiss:hover {
  color: var(--text);
}

/* ═══════════════════════════════════════════════════════════════════════════
   PAPER — runner notes in the right margin, coach paragraphs against a pen
   stroke.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ─── Runner's entry: a note in the right margin, no bubble ─── */

/* Only the note itself and the runner's logged-workout card take the margin
   placement. The FORM cards that also carry data-voice="user" (onboarding
   step/recap, goal modal, log-workout capture) would be squeezed to 72% of a
   phone by the same selector — they keep full width. */
.entry[data-voice="user"],
.user-workout-card {
  align-self: flex-end;
  max-width: 72%;
}

.entry[data-voice="user"] .entry-body {
  padding-right: 14px;
  border-right: 3px solid var(--ember-sage);
  font-size: var(--chat-prose-size);
  font-style: italic;
  line-height: 1.55;
  text-align: right;
}

/* ─── Coach's entry: paragraphs against a pen stroke ─── */

[data-voice="coach"] {
  max-width: 88%;
}

/* A card carries data-voice="coach" AND is wrapped in a group that does —
   88% of 88% is not the intent. */
[data-voice="coach"] [data-voice="coach"] {
  max-width: 100%;
}

/* The model's markdown bold IS the marker: the highlight carries the emphasis,
   not the weight. The stops are scheme-aware tokens — over the ember substrate
   the paper weights only measured 3.5:1. */
.scribble-text strong {
  padding: 1px 4px;
  border-radius: 3px;
  background: linear-gradient(
    104deg,
    transparent 2%,
    var(--paper-marker-from) 4%,
    var(--paper-marker-to) 96%,
    transparent 98%
  );
  font-weight: inherit;
}

/* ─── Day stamp: the ledger's gutter (SPEC §2.2) ─────────────────────────
   A zero-height anchor in the stream, NOT a row wrapper: #chat-messages is a
   flat list of .exchange-group that live turns append to and history batches
   prepend to, so a per-day box would have to be assembled client-side. The
   cell hangs left into the gutter the stream reserves as padding; the stream's
   own 22px gap carries the rhythm.

   The negative bottom margin cancels that gap under the stamp, so the cell's
   top lines up with the day's first entry and the day-to-day rest reads as
   22px above the divider + 22px below it. */

/* Every stamp sticks, and stays stuck to the end of the stream: the stamps are
   siblings in one flat list, so nothing releases one when the next day
   arrives — the later cell simply paints over it (opaque ground, below; equal
   z-index keeps source order, and today is always last). iOS pins the same way
   (`DayMarkerRow`, `visualEffect` on the scroll view's minY) and the owner
   asked for the web to match on 2026-09-06.

   `--stamp-pin` is where the top of the scrollport effectively is. In the
   two-pane shell that is the stream's own box, so 0 — the default. On one
   column the stream is not the scroller, the PAGE is, and the pinned chrome
   above it covers the first stretch of that scrollport; `viewport.js` measures
   the nav into `--chrome-h`.

   Sticky needs a positioned box, which the cell's absolute placement already
   required — one rule owns both jobs. */
.ledger-stamp {
  position: sticky;
  top: var(--stamp-pin, 0px);
  z-index: 1;
  height: 0;
  margin-top: 22px;
  margin-bottom: -22px;
}

/* Between days only — the first stamp in the stream opens the log rather than
   closing a day. A prepended history batch re-evaluates the selector for free.
   Spans the gutter too, so the rule reads as the day's own edge — in two runs:
   this one covers the gap and the content column, the cell's own ::before
   (below) covers the gutter, so the gutter run belongs to the cell's box and a
   later day's cell wipes a stuck one behind its rule instead of dragging a bare
   dashed line across it. */
.ledger-stamp ~ .ledger-stamp::before {
  content: "";
  position: absolute;
  top: calc(-1 * var(--ledger-gap));
  right: 0;
  left: calc(-1 * var(--ledger-gap));
  border-top: 1.5px dashed var(--paper-rule-faint);
}

.ledger-stamp__cell {
  position: absolute;
  /* The box starts at the divider's line, one gap above the stamp, and the
     padding puts the stamp back where the day's first entry opens. */
  top: calc(-1 * var(--ledger-gap));
  padding-top: var(--ledger-gap);
  left: calc(-1 * (var(--ledger-gutter) + var(--ledger-gap)));
  width: var(--ledger-gutter);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: right;
  /* The cell is the gutter's exact width and the stamp is one token per line:
     wrapping it would push the numeral and the blob below every other day's. */
  white-space: nowrap;
  background: var(--surface);
}

/* The cover needs an opaque cell — at every width, since the stamp pins at
   every width now. Page ground, so a cell at rest reads as paper — except the
   ledger's grain is body::after at z-index -1, UNDER the content, so a plain
   ground hides it and the cell showed as a cleaner box in light. The cell wears
   the shared paper grain at the ledger's strength, drawn over its own text: at
   that strength ink is unchanged. */
.ledger-stamp__cell::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--poster-grain);
  opacity: var(--ledger-grain-opacity);
  mix-blend-mode: multiply;
}

.ledger-stamp ~ .ledger-stamp .ledger-stamp__cell::before {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  border-top: 1.5px dashed var(--paper-rule-faint);
}

.ledger-stamp__weekday {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.ledger-stamp__day {
  font-family: var(--font-display);
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--text-faint);
  opacity: 0.7;
}

.ledger-stamp--today .ledger-stamp__weekday {
  color: var(--tag-text-coach);
}

/* "TUE · TODAY" needs ~87px of mono at this tracking — it fits the 92px cell
   and nothing like the 44px one tokens.css steps to under 1100px, where the
   coral blob is already what marks today (ios-rethink-2c). The weekday code
   alone survives there. */
@media (max-width: 1100px) {
  .coach-ledger .ledger-stamp__sep,
  .coach-ledger .ledger-stamp__today {
    display: none;
  }
}

.ledger-stamp--today .ledger-stamp__day {
  color: var(--text);
  opacity: 1;
}

/* Static halo, no pulse (SPEC §7) — the blob marks today, it does not nag. */
.ledger-stamp__blob {
  width: 12px;
  height: 12px;
  margin: 8px 0 0 auto;
  border-radius: var(--radius-node);
  background: var(--ember-coral);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--ember-coral) 25%, transparent);
}

/* The stamp itself is aria-hidden decoration; this is the date a screen reader
   actually gets. The one visually-hidden recipe on the page — reuse it rather
   than re-deriving the clip. */
.ledger-stamp__time,
.landing-demo input[name="landing-node"] {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ─── No room for a gutter: the stamp becomes a LINE above its day ───
   The same shape iOS already ships (`EmberKit/DayMarkerRow`: rule, then a
   left-set `SAT 06` line above the day's rows), reached here by collapsing the
   ONE pair of tokens the whole gutter is built from — the stream's and the
   dock's left padding, the cell's negative offset, its width, the divider's
   overhang. Nothing below re-states a width.

   A CONTAINER query, not a media query (owner, 2026-09-06: "do not design a
   separate layout for mobile devices, it needs to dynamically adapt"): what
   runs out of room is the conversation, so it is the conversation's box that
   decides, and nothing here names a device — a desktop window narrowed into
   one column collapses exactly as a phone does. `.coach-inline` already
   declares `container-type: inline-size` (top of this file); the container
   cannot style itself, so the tokens are re-declared on the descendants that
   consume them.

   Honest bound: in the TWO-PANE shell `.coach-inline` is the grid holding the
   conversation AND the 372px rail, so the width read here is the pair's and
   this never fires there. That is fine — the gutter is affordable whenever the
   rail fits beside it — but it does mean the query decides in the one-column
   shell only. The genuinely starved case is the narrowest two-pane width,
   where the stream keeps a 92px gutter over a ~340px column; that is HEAD's
   behaviour, untouched here, and fixing it needs the container moved onto a
   wrapper the stream's own padding does not sit on.

   34rem = 510px at the 15px root. That is where the gutter stops paying for
   itself: the 44px cell plus its 22px gap take an eighth of the line, and it
   clears every phone in portrait with room to spare — a 430px device leaves a
   402px container, so the widest phone still collapses rather than landing in
   a band the threshold only just misses. */
@container chat (width < 34rem) {
  .coach-ledger .message-stream,
  .coach-ledger .entry-dock {
    --ledger-gutter: 0px;
    --ledger-gap: 0px;
  }

  /* A row in the flow now, not a zero-height anchor hanging into a margin. The
     top padding is the divider's room: that rule draws at the stamp's own top
     edge (the gap it used to hang in is 0 here), so without it the cell's
     opaque ground would paint straight over it. The stamp carries the ground
     ITSELF here rather than leaving it to the cell — the stamp pins, so that
     10px would otherwise be a transparent band with the day's text sliding
     through it. */
  .coach-ledger .ledger-stamp {
    height: auto;
    margin-bottom: 0;
    padding-top: 10px;
    background: var(--surface);
  }

  /* The cell reads left to right on its own line: `SAT 06 ●`. In flow, so it
     takes the row's height; the numeral keeps the display face it wears in the
     gutter, one step down so the line stays a caption and not a heading. The
     short skirt is iOS's — text sliding under a pinned stamp clears the
     numeral instead of butting against its baseline. */
  .coach-ledger .ledger-stamp__cell {
    position: relative;
    display: flex;
    flex-direction: row;
    align-items: baseline;
    gap: 8px;
    width: auto;
    padding-bottom: 4px;
    text-align: left;
  }

  .coach-ledger .ledger-stamp__day {
    font-size: 20px;
  }

  /* In the gutter the blob sits under the numeral; on a line it sits beside it,
     so the auto left margin and the top offset both go. */
  .coach-ledger .ledger-stamp__blob {
    width: 9px;
    height: 9px;
    margin: 0;
    align-self: center;
  }

  /* The gutter run has no gutter to cover — the cell's own rule would double
     the divider on the line the stamp already draws. */
  .coach-ledger .ledger-stamp ~ .ledger-stamp .ledger-stamp__cell::before {
    content: none;
  }

  /* `TUE · TODAY` fits a full line even on the narrowest phone, so the suffix
     the 44px cell could not hold comes back. */
  .coach-ledger .ledger-stamp__sep,
  .coach-ledger .ledger-stamp__today {
    display: inline;
  }
}

/* One column: the stream is not the scroller, the page is, so the stamp has to
   clear the nav pinned over it. The nav stays pinned with the keyboard up, so
   the offset holds while the runner writes — which is the whole point of a day
   stamp, and what iOS does with the keyboard up.

   NO FALLBACK on `--chrome-h`, deliberately. It looks like the careless
   version and it is the opposite: without viewport.js there is no honest
   number here, and any guess pins the stamp INSIDE the nav's own band, which
   is opaque and outranks it — an invisible stamp, worse than none. An
   unresolved var() makes `top` invalid at computed-value time, so it falls
   back to `auto` and the stamp simply does not pin, which is exactly the
   behaviour that shipped before this round. `--stamp-pin` keeps its own 0
   default for the two-pane shell, where the stream IS the scroller.

   The 1.5px lifts the stamp's dashed rule ONTO the nav's own bottom rule
   instead of parking it one hairline below — two parallel dashed lines is the
   artifact iOS documents avoiding with its skirt. */
@media (max-width: 1000px) {
  .coach-ledger .message-stream {
    --stamp-pin: calc(var(--chrome-h) - 1.5px);
  }
}

@media (min-width: 1000.02px) {
  /* The rail is always on screen in two panes — nothing to summon. The slot
     selector has to outrank `.entry-seam .seam-item`, which sets
     `display: inline-flex` on every ladder slot. */
  body.coach-ledger .entry-seam .seam-item.journey-summon,
  body.coach-ledger .journey-summon {
    display: none;
  }
}

/* ─── Composer ─── */

/* THE DOCK owns sticky, not the composer. The seam line is the dock's last
   child, so it rides along instead of sitting in normal flow below a sticky
   composer — where it was reachable only at the very bottom of the scroll, and
   where the composer visibly shifted by its height as it settled out of sticky.
   One band, one position, both fixed.

   The left inset is the ledger gutter + gap, so the ruled line starts where the
   day's entries start rather than under their stamps.

   The ground fades in over a fixed 12px: the conversation still passes under a
   soft edge, and the fade never reaches the field's text whatever the dock's
   height. */
/* The bottom pad carries three things at once, which is why it is one
   expression and not three rules. `env(safe-area-inset-bottom)` is 0 wherever
   there is no inset, so `max()` is already the value at every width — the
   narrow duplicate this replaced was never a different number.
   `--keyboard-inset` is the part behind the keys: sticky pins the dock's bottom
   edge to the LAYOUT viewport's, which the keyboard never moves, so without it
   the field sits behind them. Padding does two jobs there — it lifts the field
   onto the keyboard, and it grows the document by the same amount so the newest
   message can still scroll clear. It is 0 with no keyboard and 0 without
   viewport.js, which is exactly the resting pad. */
.entry-dock {
  position: sticky;
  bottom: 0;
  z-index: 1;
  padding: 0 28px
    calc(max(22px, env(safe-area-inset-bottom)) + var(--keyboard-inset, 0px))
    calc(var(--ledger-gutter) + var(--ledger-gap));
  background: linear-gradient(transparent, var(--surface) 12px);
}

/* The runner's pen, in the runner's hand. */
.entry-line-glyph {
  font-family: var(--font-annotation);
  font-size: 18px;
  line-height: 1;
  color: var(--ember-sage);
}

.coach-ledger .entry-line-send {
  padding: 0 0 0 16px;
}

.entry-line-send:hover:not(:disabled) {
  color: var(--accent-coach-strong-hover);
  text-decoration: none;
}

/* ─── Follow-up chips ─── */

/* A chip sends AS the runner, so it wears the runner's slant — the same italic
   their typed message and their landed note carry. `.coach-action-item` shares
   the pill so the row stays one height, but it opens a form rather than
   speaking, so it stays upright. Voice, not decoration. */
.chat-chip {
  font-style: italic;
}

.chat-chip:hover,
.coach-action-item:hover {
  border-color: var(--ember-coral);
  color: var(--paper-link);
  text-decoration: none;
}

/* 44 is the TAP target, not the pill. `min-height` PAINTS it — the pill's own
   background and border fill the taller box — which inflates every chip by ~9px
   and, stacked, is what made the native strip unusable. An invisible overlay
   carries the hit area instead, so the pill keeps its designed height. iOS
   corrects the same mistake on these same three controls with a frame outside
   the painted background; this is the CSS twin. */
@media (max-width: 760px) {
  .chat-chip,
  .coach-action-item,
  .entry-line-send {
    position: relative;
  }

  .chat-chip::after,
  .coach-action-item::after,
  .entry-line-send::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
    height: var(--tap-target);
    transform: translateY(-50%);
  }
}

/* ─── Seam line (SPEC §2.3) ───────────────────────────────────────────────
   The stitch under the ruled line: the runner's way into the log, then the
   standing route to the sources. Mono label register (same recipe as
   .cite-pop__label) so it reads as chrome, not as a chip row. No underline at
   rest — that weight was what made the old single line read as a footer.

   The ink is --text-secondary and must NOT be lightened: at this size,
   --text-muted measures 3.70:1 on the light coach ground (#8a7a64 on #f7f1e6),
   under AA's 4.5:1, and the large-text exemption needs 18.66px bold — so type
   size cannot buy it back. The methodology half is the standing route now the
   per-message marker is model-gated, and App Store 1.4.1 rejected this app twice
   on citations being hard to find. The underline returns on hover and on
   keyboard focus, so the affordance still confirms itself. */

/* ONE tight line that scrolls, never a wrapping block (owner ruling
   2026-09-03): the markup carries `.scroll-row .edge-fade--trailing`, the same
   two utilities the landing demo strip uses, so the overflow, the hidden
   scrollbar and the drag-to-scroll handler are all shared rather than restated.
   The gap is the strip's own `--space-md`, with the `·` centred in it, so the
   row reads at the strip's density instead of the old wrapped pitch. */
.entry-seam {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: 14px 0 0;
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* Items and separators are the flex items (the action group is
   `display: contents`), and a scroller must never squeeze or break one.
   `.coach-actions-group > *` already says this for the group's own children. */
.entry-seam > * {
  flex: none;
  white-space: nowrap;
}

/* A <button> and an <a> reading as one row: the button sheds its chrome and
   both take the row's own type and ink. The deterministic action items ride in
   the same row (they are `.coach-action-item`, an outlined pill everywhere
   else), so they shed the pill HERE rather than carrying a second class — the
   markup is shared with the walled composer, where the pill is still right. */
.entry-seam__link,
.entry-seam .coach-action-item {
  position: relative;
  z-index: 2;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: inherit;
  cursor: pointer;
  text-decoration: none;
}

.entry-seam__link:hover,
.entry-seam__link:focus-visible,
.entry-seam .coach-action-item:hover,
.entry-seam .coach-action-item:focus-visible {
  color: var(--paper-link);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.entry-seam__sep {
  color: var(--text-faint);
}

/* One slot per ladder item (chat_action_buttons.html), each with its own
   leading dot. The first slot present takes the ink and drops its dot —
   sibling CSS only, so app-promo.js un-hiding the get-app slot moves the ink
   without a script. The ink sits on the SLOT and the control inherits it:
   the item's own `color: inherit` and `:hover` rules keep winning on the item.

   `--paper-link`, not `--accent-action`: steel measures 2.97:1 against the
   light coach ground at this 10.5px size, which AA's large-text exemption
   cannot buy back, and the row's HOVER ink is already `--paper-link` — one
   family at rest and on hover instead of cool steel warming to rust. */
.entry-seam .seam-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-md);
  color: var(--paper-link);
}

.entry-seam .seam-item[hidden] {
  display: none;
}

.entry-seam .seam-item > .entry-seam__sep {
  display: none;
}

.entry-seam .seam-item:not([hidden]) ~ .seam-item {
  color: inherit;
}

.entry-seam .seam-item:not([hidden]) ~ .seam-item > .entry-seam__sep {
  display: inline;
}

/* 44 is the TAP target, not the line — SPEC §8: "seam links get 44pt overlays,
   not padding". The same invisible overlay the paper chips and the citation
   marker use, because painting a 44px box here spends back the vertical the
   composer strip fought for. The overlay's upper half falls in the composer
   band's inert bottom padding, and that band carries `z-index: 1`; without a
   higher layer here it would swallow those taps and leave the target at ~27px.
   One scrolling line never stacks overlays, so the wrap-era hazard is gone.

   Unscoped by width on purpose (SPEC §8 states the rule without a breakpoint):
   the row is a 10.5px line at EVERY width, and it hangs the deterministic
   action items too now — the ≤760px overlay on `.coach-action-item` stops at
   the breakpoint, this one must not. */
.entry-seam__link::after,
.entry-seam .coach-action-item::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: var(--tap-target);
  transform: translateY(-50%);
}

/* ─── Citation footnote ───────────────────────────────────────────────────
   A raised marker on the end of the reply's last line, the way a printed page
   references a source — only on the replies the coach actually cited. */

/* Two block siblings can never share a line box, so when a marker follows the
   reply the reply's box goes inline — and so does a trailing PARAGRAPH, but only
   a paragraph. A trailing list or heading stays the block it is: inlining a <ul>
   leaves its display: list-item children block-level, which breaks the inline box
   around them and strands the marker anyway. The honest consequence is that a
   reply ending in a list puts its marker on its own line, which is correct — a
   list item's last word is not the sentence's last word. Earlier paragraphs keep
   their own boxes: this bubble spaces paragraphs with BOTTOM margins only
   (.scribble-text p), so nothing collapses. The prose measure moves up to the
   body for the inline case — an inline box ignores max-width. */
.scribble-body:has(> .scribble-cite) {
  max-width: var(--coach-prose-measure);
}

.scribble-body:has(> .scribble-cite) > .scribble-text,
.scribble-body:has(> .scribble-cite) > .scribble-text > p:last-child {
  display: inline;
}

/* Raised the way `sup` is normalized — zero line-height plus a relative
   offset — and NOT with vertical-align: super, which grows the line box of
   the very line the marker joins and would re-space the reply's last line.
   The face is the coach's own: the marker sits inside his sentence now. */
.scribble-cite {
  position: relative;
  top: -0.5em;
  padding: 0;
  border: none;
  background: none;
  /* One step up from the --text-faint the word-sized label wore: the glyph is
     a fifth of that ink, and faint × tiny is invisible in the light scheme. */
  color: var(--text-muted);
  font-family: var(--font-coach);
  font-size: var(--text-2xs);
  line-height: 0;
  vertical-align: baseline;
  text-decoration: none;
  cursor: pointer;
}

.scribble-cite:hover,
.scribble-cite:focus-visible {
  color: var(--paper-link);
}

/* An unstyled ::backdrop is transparent, which is what a footnote popup wants:
   the conversation stays visible behind it. */
.cite-pop {
  max-width: 320px;
  padding: 16px 18px;
  border: 1.5px solid var(--paper-rule);
  border-radius: 6px;
  background: var(--surface-raised);
  box-shadow: var(--shadow-hard-md);
  color: var(--text);
}

.cite-pop__rule {
  font-size: 14px;
  line-height: 1.55;
}

/* Same "SOURCE" label convention the methodology page uses. Deliberately one
   step BELOW --text-2xs (11.2px): this label sits inside the popup, under the
   footnote that opened it, and must not read as loud as its own trigger. */
.cite-pop__label {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.cite-pop__source {
  font-size: 13px;
  color: var(--text-secondary);
}

.cite-pop__link,
.cite-pop__all {
  display: block;
  color: var(--paper-link);
  font-size: 12.5px;
  overflow-wrap: anywhere;
}

/* The marker is a few px in BOTH axes, so the tap target is an invisible
   overlay — the same idiom the paper chips use above, squared. Painting it
   (min-height) would push the marker out of the line it belongs to, which is
   the whole point of the footnote. The marker is already `position: relative`
   for its raise, so the overlay has its anchor. The popup never exceeds the
   viewport minus the page gutters. */
@media (max-width: 760px) {
  .scribble-cite::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--tap-target);
    height: var(--tap-target);
    transform: translate(-50%, -50%);
  }

  .cite-pop {
    max-width: min(320px, calc(100vw - 32px));
  }
}

/* ========================================================================
   P03 — Voice Breaks Chrome + P08 — Emphasis

   P03: Steev's significant sentences render without bubble chrome.
        --font-coach italic, poster-scale, 3-line drop-cap in --font-display.
        Applied to: landing-page opener message, design-kit specimen, poster
        reasoning paragraphs. Chat celebrations stay in flow as a regular
        scribble with `.scribble--celebration` (chat-marginalia.css).

   P08: Inline emphasis. Mauve underline; preserves readability. Coach bubbles
        get this on markdown <strong> (see chat-marginalia.css); .coach-emph is
        the standalone class for static specimens / openers.
   ======================================================================== */

.coach-voice-opener {
  font-family: var(--font-coach);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.75rem, 3.5vw, 3.375rem);
  line-height: 1.12;
  color: var(--tag-text-info);
  letter-spacing: -0.012em;
  max-width: 22ch;
  margin: 0;
}

.coach-voice-opener .drop-cap {
  font-family: var(--font-display);
  font-style: normal;
  font-weight: 900;
  font-size: 1.8em;
  line-height: 0.78;
  float: left;
  margin: 0.12em 0.18em -0.16em 0;
  color: var(--accent-info-text);
}

.coach-voice-opener__signature {
  margin-top: var(--space-md);
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* P08 — Inline emphasis within static coach text (voice-openers, specimens).
   Coach bubbles render markdown <strong> with the same look (chat-marginalia.css). */
.coach-emph {
  border-bottom: 2px solid var(--accent-info);
  padding-bottom: 1px;
  /* Intentionally no text color shift — underline alone preserves readability
     and avoids a confetti-sprinkle effect when multiple phrases are marked. */
}

/* ========================================================================
   P06 — Embodied Steev state animations

   States are set as data-state on .steev-face. CSS drives the visuals.
   Respects prefers-reduced-motion (except sleeping/waking poses, which
   are static).
   ======================================================================== */

.steev-face .sv-el,
.steev-face .sv-er { transition: transform 140ms ease; transform-origin: center; transform-box: fill-box; }
.steev-face[data-state="blinking"] .sv-el,
.steev-face[data-state="blinking"] .sv-er { transform: scaleY(0.1); }

.steev-face .sv-m { transform-origin: 50% 50%; transform-box: fill-box; }
.steev-face[data-state="writing"] .sv-m {
  animation: steev-wiggle 200ms ease-in-out infinite alternate;
}
@keyframes steev-wiggle {
  from { transform: rotate(-0.5deg); }
  to   { transform: rotate(0.5deg); }
}

.steev-face[data-state="sleeping"] .sv-el,
.steev-face[data-state="sleeping"] .sv-er { transform: scaleY(0.3); }
.steev-face[data-state="sleeping"] .sv-m { filter: blur(0.3px); opacity: 0.85; }

@media (prefers-reduced-motion: reduce) {
  .steev-face[data-state="writing"] .sv-m { animation: none !important; }
  .steev-face[data-state="blinking"] .sv-el,
  .steev-face[data-state="blinking"] .sv-er { transform: none; }
}

/* =============================================================================
   GENERIC DETAILS - Base styles for <details>/<summary> elements
   ============================================================================= */

details summary {
  cursor: pointer;
  list-style: none;
}

details summary::-webkit-details-marker {
  display: none;
}

details summary::marker {
  display: none;
}

details[open] summary {
  margin-bottom: var(--space-md);
}

details summary:hover {
  color: var(--text);
}

/* ========================================================================
   P07 — Editorial Rhythm (page composition)

   Long-form pages are vertical compositions, not continuous feeds.
   Each section has:
     - Hairline top rule (1px, var(--border))  -- EXCEPT .composition-section--no-rule
     - Mono micro-label above the content
     - --space-section vertical padding (clamp 5rem..8rem)
     - No shared container background — each section breathes on --surface

   Usage:
     <section class="composition-section">
       <div class="composition-section__label">NEXT UP</div>
       <!-- section content -->
     </section>

   For the first section (no top rule needed) OR the Journey zone
   (memory rule: never a top rule above the Arc):
     <section class="composition-section composition-section--no-rule">...</section>
   ======================================================================== */

.composition-section {
  padding-block: var(--space-section);
}

.composition-section--no-rule {
  border-top: none;
}

.composition-section__label {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-sm);
}

/* =============================================================================
   CREDITS - Credit code display and list styles
   ============================================================================= */

.credit-code-display {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-md) var(--inset);
  background: var(--surface-sunken);
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
}

.credit-code-value {
  font-family: var(--font-mono, monospace);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  letter-spacing: 0.05em;
  color: var(--accent-action-text);
  flex: 1;
}

.credit-code-success {
  padding: var(--space-md) var(--inset);
}

.credit-code-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.credit-code-item {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--inset);
  background: var(--surface-sunken);
  border-radius: var(--radius-md);
  border: 1px solid var(--border);
}

.credit-code-item.redeemed {
  opacity: 0.6;
}

.credit-code-item-main {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
}

.credit-code-item-main .credit-code-value {
  font-size: var(--text-base);
}

.credit-code-item-meta {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.link-subtle {
  color: var(--text-muted);
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 2px;
}

.link-subtle:hover {
  color: var(--accent-action-text);
}

/* =============================================================================
   STORE PLAN SECTION - Tier and model info
   ============================================================================= */

.store-plan-section {
  padding-bottom: var(--space-md);
  margin-bottom: var(--space-md);
  border-bottom: 1px solid var(--border);
}

/* Tier badges */
.badge-tier-plan-0 {
  background: var(--surface-sunken);
  color: var(--text-muted);
}

.badge-tier-plan-1 {
  background: var(--accent-action-faint);
  color: var(--accent-action-text);
}

.badge-tier-plan-2 {
  background: var(--accent-success-faint);
  color: var(--accent-success-text);
}

/* =============================================================================
   CREDIT BAR - Visual progress bar for daily/reserve credits
   ============================================================================= */

/* Full-size credit bar for store page */
.credit-bar {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.credit-bar-track {
  height: 12px;
  background: var(--surface-sunken);
  border-radius: var(--radius-full);
  overflow: hidden;
  display: flex;
}

.credit-bar-daily {
  height: 100%;
  background: var(--accent-success);
  border-radius: var(--radius-full) 0 0 var(--radius-full);
  transition: width var(--transition-normal);
}

/* Round both ends when daily is the only visible segment */
.credit-bar-daily:only-child {
  border-radius: var(--radius-full);
}

.credit-bar-reserve {
  height: 100%;
  background: var(--accent-caution);
  border-radius: 0 var(--radius-full) var(--radius-full) 0;
  transition: width var(--transition-normal);
}

/* State-based colors */
.credit-bar.low .credit-bar-daily {
  background: var(--accent-caution);
}

.credit-bar.exhausted .credit-bar-track {
  background: var(--accent-danger-faint);
}

/* Legend under the bar */
.credit-bar-legend {
  display: flex;
  gap: var(--space-lg);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.credit-bar-legend-item {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.credit-bar-dot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-full);
}

.credit-bar-dot.daily {
  background: var(--accent-success);
}

.credit-bar.low .credit-bar-dot.daily {
  background: var(--accent-caution);
}

.credit-bar-dot.reserve {
  background: var(--accent-caution);
}

/* Status message */
.credit-bar-status {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.credit-bar.low .credit-bar-status {
  color: var(--accent-caution-text);
}

.credit-bar.exhausted .credit-bar-status {
  color: var(--accent-danger-text);
}

/* =============================================================================
   EMPTY STATES

   Rendered when a zone has no content yet (chat with no exchanges, leaderboard
   with no entries, etc.). Brutalist-editorial treatment: tracked mono label,
   condensed display heading, direct subtitle, optional CTA.
   ============================================================================= */

.chat-empty-state {
  display: flex;
  justify-content: center;
  padding: var(--space-xl) var(--inset);
}

.empty-state-card {
  max-width: 440px;
  width: 100%;
  padding: var(--space-xl) var(--inset-lg);
  background: var(--surface-raised);
  border: 1.5px solid var(--border);
  border-radius: 4px;
  box-shadow: var(--shadow-hard-sm);
  text-align: center;
}

.empty-state-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: 0.14em;
  color: var(--text-faint);
  margin-bottom: var(--space-md);
}

.empty-state-title {
  font-family: var(--font-heading);
  font-size: var(--text-2xl);
  line-height: 1.1;
  letter-spacing: -0.015em;
  margin: 0 0 var(--space-sm);
  color: var(--text-bright);
}

.empty-state-subtitle {
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: 1.5;
  color: var(--text-secondary);
  margin: 0 auto var(--space-lg);
  max-width: 36ch;
}


/* =============================================================================
   FORMS - Form components and inputs
   ============================================================================= */

/* =============================================================================
   FORM RHYTHM — unified two-gap system carried on bare <form> and on
   .form--labels-below, so non-form containers (a div of form-groups driven by
   hx-include buttons) opt into the identical rhythm with the grammar class.
   ----------------------------------------------------------------------------
   Tight default 8px gap between adjacent children; section-break classes
   get an additional 16px margin-top, producing a loose 24px gap effective
   (parent gap + child margin-top stack in flex). Inline forms opt out via
   .form-inline (defined at the end of this file).

   Labels-below containers widen the default gap to 16px: the flipped caption
   sits 4px under its own field, and the next group must start clearly farther
   away than that for the label to read upward, never downward.
   ============================================================================= */

form {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.form--labels-below {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

form > * + .form-actions,
.form--labels-below > * + .form-actions,
form > * + .poster__ticker,
form > * + .poster__effort,
form > * + .poster__more-section,
form > * + .poster__foot,
form > * + button[type="submit"]:not(.poster__submit) {
  margin-top: var(--space-md);
}

.form-label {
  display: block;
  margin-bottom: var(--space-sm);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
}

/* Date inputs - neutral placeholder color (avoids theme-specific muted colors) */
input[type="date"]:not(:valid) {
  color: var(--text-secondary);
}
input[type="date"]::-webkit-datetime-edit-text,
input[type="date"]::-webkit-datetime-edit-month-field,
input[type="date"]::-webkit-datetime-edit-day-field,
input[type="date"]::-webkit-datetime-edit-year-field {
  color: inherit;
}

.form-error {
  color: var(--accent-danger-text);
  font-size: var(--text-sm);
  margin-top: var(--space-xs);
}

.form-success {
  color: var(--accent-success-text);
  font-size: var(--text-sm);
}

.form-caution {
  color: var(--accent-caution-text);
  font-size: var(--text-sm);
}

.form-hint {
  display: block;
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-top: var(--space-xs);
}

.form-section {
  background: var(--surface-overlay);
  border-radius: var(--radius-lg);
  padding: var(--space-md) var(--inset);
}


.form-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-md);
}

/* =============================================================================
   PASSWORD INPUT
   ============================================================================= */

.password-input-wrapper {
  position: relative;
  display: flex;
  align-items: center;
}

.password-input-wrapper .input--filled {
  padding-right: 44px;
}

.password-toggle {
  position: absolute;
  right: var(--space-sm);
  background: transparent;
  border: none;
  padding: var(--space-xs);
  cursor: pointer;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  justify-content: center;
}

.password-toggle:hover {
  color: var(--text);
}

.eye-icon {
  width: 20px;
  height: 20px;
}

/* =============================================================================
   TIME INPUT
   ============================================================================= */

.time-input-group {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

/* `.input` (below) also sets `width: 100%` at equal specificity and wins on
   order, so this has to name both classes — a bare `.time-input` here is dead
   and every mm/ss field stretches container-wide. Width is intrinsic: two
   digits plus the padding it carries. */
.input.time-input {
  width: calc(3ch + 2 * var(--space-sm));
  text-align: center;
  padding: var(--space-sm);
}

.input.time-input::-webkit-inner-spin-button,
.input.time-input::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.time-input-sep {
  font-size: var(--text-xl);
  color: var(--text-muted);
  font-weight: var(--weight-semibold);
}

/* =============================================================================
   DISTANCE INPUT
   ============================================================================= */

.distance-input-group {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.distance-select {
  flex: 1;
}

.custom-distance-input {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

/* Sized to its content — a distance is at most a few digits and a decimal —
   so the field never stretches to fill whatever container it lands in. */
.custom-distance-input .input--filled {
  width: calc(6ch + 2 * var(--inset));
}

.input-suffix {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* =============================================================================
   FORM ACTIONS
   ============================================================================= */

.form-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-md);
}

/* ─── Toggle Switch ─────────────────────────────────────────────────── */

.toggle-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm);
  cursor: pointer;
}

.toggle-label {
  flex: 1;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text);
}

.toggle-hint {
  width: 100%;
  font-size: var(--text-xs);
  color: var(--text-muted);
  order: 3;
}

.toggle-input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.toggle-slider {
  position: relative;
  width: 40px;
  height: 22px;
  background: var(--border);
  border-radius: 11px;
  transition: background 0.2s;
  flex-shrink: 0;
}

.toggle-slider::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  background: var(--surface);
  border-radius: 50%;
  transition: transform 0.2s;
}

.toggle-input:checked + .toggle-slider {
  background: var(--accent-action);
}

.toggle-input:checked + .toggle-slider::after {
  transform: translateX(18px);
}

.push-time-select {
  max-width: 160px;
}

/* ─── Send-workout triggers (quiet typographic actions) ─────────────── */

.send-workout-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md);
}

.send-workout-btn {
  background: transparent;
  border: none;
  padding: 0;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
  cursor: pointer;
  transition: color var(--transition-fast);
}

.send-workout-btn::after {
  content: " →";
  color: var(--text-muted);
  transition: color var(--transition-fast);
}

.send-workout-btn:hover:not(:disabled),
.send-workout-btn:hover:not(:disabled)::after {
  color: var(--accent-action-text);
}

.send-workout-btn:disabled {
  opacity: 0.5;
  cursor: progress;
}

.send-workout-status {
  flex: 1;
  font-size: var(--text-sm);
}

/* =============================================================================
   SEGMENTS - Interval entry inside the unified workout-capture form
   ============================================================================= */

.extras-container {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

/* Both modes share the same row anatomy: a flex container with a number
   gutter on the left (auto-numbered via CSS counter) and a data box on
   the right. The mode-specific selectors mostly exist for the rows
   *containers* and inner row-line variants; the row + cell rules below
   apply uniformly. */

.segment-rows,
.segment-splits-rows {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  counter-reset: segment;
}

.segment-row,
.segment-splits-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-xs);
  counter-increment: segment;
}

/* Numeric badge ("1.", "2.") rendered in the gutter beside each row.
   Uses a CSS counter so add/remove/reorder works without JS bookkeeping. */
.segment-row-number {
  /* `3ch` covers "99." comfortably; sized by character count, not magic em. */
  flex: 0 0 3ch;
  text-align: end;
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  /* Align with the first input's text baseline: data box's top padding
     (--space-xs) plus the input's own bottom padding (--space-sm). */
  padding-block-start: calc(var(--space-xs) + var(--space-sm));
}

.segment-row-number::before {
  content: counter(segment) ".";
}

.segment-row-data {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-xs) var(--space-sm);
  /* Baseline border-bottom per row — same input-style baseline rule;
     replaces the previous surface-sunken card-around-row that visually
     fragmented rows from their parent form. */
  border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}

/* Last row in the rows container drops the baseline rule — the row count
   is variable and the trailing rule reads as a "more rows expected" hint
   that we don't want at the visual end of the section. */
.segment-row:last-child .segment-row-data,
.segment-splits-row:last-child .segment-row-data {
  border-bottom: none;
}

.segment-row-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
}

.segment-row-line--elev {
  padding-inline-start: var(--inset-sm);
}

/* Inputs intrinsic-size from their `size` attribute. Override
   the default width so each input takes only the width it needs. */
.segment-row-line input,
.segment-splits-row input {
  width: auto;
  flex: 0 0 auto;
}

.segment-input-mins,
.segment-input-secs,
.segment-input-hr,
.segment-input-dist,
.segment-input-up,
.segment-input-down {
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.segment-input-type {
  font-size: var(--text-sm);
  /* Flat-line select: strip chevron background, no extra right padding. */
  appearance: none;
  background-image: none;
  padding-right: 0;
}

.segment-colon {
  color: var(--text-muted);
}

/* One "mins : secs" reading — the row wraps around it, never through it. */
.segment-time {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  flex: 0 0 auto;
}

/* The row's last column, level with its first line whatever the row wraps
   to: the data box's top padding plus a line box the inputs' size, pinned to
   the top so the mobile touch-target height only extends it downward. */
.segment-remove-btn {
  flex: 0 0 auto;
  align-items: flex-start;
  padding: var(--space-xs) var(--space-xs) 0;
  font-size: var(--text-lg);
  color: var(--text-muted);
}

.segment-add-btn {
  align-self: flex-start;
  margin-top: var(--space-xs);
}

/* The form's wrapper chain (.exchange-group > .user-workout-card > .chat-card)
   normally lets each child auto-size to content, which means opening the
   <details> for extras makes the whole card widen and closing it makes it
   shrink — jarring. Force the user-workout-card to full width when it hosts
   a capture form, so the chat-card's `width: 88%` resolves against a stable
   base instead of a content-sized one. Make the wrapper a flex row that
   end-justifies its child — the parent's `align-self: flex-end` no longer
   has spare space to do that work once we go to 100% width. Scoped via
   :has() so logged-workout cards keep their natural shrink-wrap. */
.user-workout-card:has([data-capture-intervals-section]) {
  width: 100%;
  max-width: 100%;
  display: flex;
  justify-content: flex-end;
}

/* ----- Mode toggle (Splits | Custom) ------------------------------------- */

.segment-section-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
  margin-bottom: var(--space-xs);
}

.segment-mode-toggle {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-xs);
}

.segment-mode-btn {
  font-size: var(--text-xs);
  color: var(--text-muted);
  cursor: pointer;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.segment-mode-btn:has(:checked) {
  color: var(--text-bright);
  font-weight: 600;
}

.segment-mode-btn:has(:focus-visible) {
  outline: 2px solid var(--accent-coach);
  outline-offset: 2px;
}

[data-capture-intervals-section]:has([name="segments_mode"][value="custom"]:checked) [data-segment-pane="splits"],
[data-capture-intervals-section]:has([name="segments_mode"][value="splits"]:checked) [data-segment-pane="custom"] {
  display: none;
}

.segment-mode-btn:not(:last-child)::after {
  content: '·';
  margin-inline-start: var(--space-xs);
  color: var(--text-muted);
}

/* ----- Splits mode --------------------------------------------------------
   Splits rows share most styling with custom rows (see merged rules above);
   only the residual-partial label is splits-only. */

/* Only shown for the residual partial split (e.g. "+0.3km") — full-stride
   rows omit this label entirely since the row number conveys position. */
.segment-splits-label {
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* Splits rows are flat: all inputs (mins/sec/bpm/↑m/↓m) sit as direct
   children of the data box on a single line, wrapping individually when
   horizontal space runs out. Custom rows keep the stacked column layout
   from .segment-row-data because they carry more fields per line. */
.segment-splits-row .segment-row-data {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
}

.segment-extras-toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-bottom: var(--space-xs);
  cursor: pointer;
}

/* HR + elev are off by default in splits mode; the toggle reveals them.
   Nothing to hide in custom mode — both stay visible. */
[data-capture-intervals-section]:not(:has([data-splits-extras-toggle]:checked)) .segment-splits-row :is(.segment-input-hr, .segment-input-up, .segment-input-down) {
  display: none;
}

/* =============================================================================
   FORM RHYTHM — opt-out for inline forms (search, comment, micro-action)
   ============================================================================= */

.form-inline {
  display: inline-flex;
  flex-direction: row;
  align-items: baseline;
  gap: var(--inset-sm);
}


/* =============================================================================
   INPUT PRIMITIVE — one canonical .input + opt-in variants
   ----------------------------------------------------------------------------
   Base carries shared mechanical concerns (width, transitions, browser reset).
   Variants opt in to the visual contract:
     .input--filled  — page form: surface-sunken bg + breath-gradient wipe
     .input--voice   — chat composer (defined in coach-input.css)
     .input--card    — surface-raised bordered card aesthetic
     .input--sm      — density modifier; combine with a variant
                        (e.g. .input--card.input--sm) — each variant
                        defines what --sm means in its context
   Poster typographic-underline inputs are styled by .poster--form scope
   in poster.css and are not part of this primitive.
   ============================================================================= */

.input {
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
  transition:
    border-color var(--transition-fast),
    background-color var(--transition-fast),
    background-size var(--transition-elegant);
}

.input:focus {
  outline: none;
}

select.input {
  appearance: none;
}

input[type="number"].input {
  appearance: textfield;
  -webkit-appearance: textfield;
}

/* ─── .input--filled — page form aesthetic ─────────────────────────────── */
.input--filled {
  font-family: var(--font-input);
  background-color: var(--surface-sunken);
  background-image: var(--breath-gradient);
  background-size: 0% var(--rule-focus);
  background-position: 0 100%;
  background-repeat: no-repeat;
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius-md) var(--radius-md) 0 0;
  padding: 0.85em var(--inset);
  font-size: var(--text-base);
  color: var(--text-bright);
}

.input--filled:focus {
  background-color: var(--surface-sunken-focus);
  background-size: 100% var(--rule-focus);
  border-bottom-color: transparent;
}

.input--filled::placeholder {
  color: var(--text-muted);
  font-family: var(--font-input);
}

select.input--filled {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23a09888' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E"),
    var(--breath-gradient);
  background-position: right var(--space-md) center, 0 100%;
  background-size: 12px 12px, 0% var(--rule-focus);
  background-repeat: no-repeat, no-repeat;
  padding-right: var(--space-2xl);
}

select.input--filled:focus {
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23a09888' viewBox='0 0 16 16'%3E%3Cpath d='M8 11L3 6h10l-5 5z'/%3E%3C/svg%3E"),
    var(--breath-gradient);
  background-position: right var(--space-md) center, 0 100%;
  background-size: 12px 12px, 100% var(--rule-focus);
  background-repeat: no-repeat, no-repeat;
}

textarea.input--filled {
  min-height: 100px;
  resize: vertical;
  padding: 0.75em var(--inset);
}

/* ─── .input--card — surface-raised card aesthetic ─────────────────────── */
.input--card {
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-sm);
  font-size: var(--text-base);
  color: var(--text);
  font-family: inherit;
}

.input--card:focus {
  border-color: var(--accent-action);
  background: var(--surface-sunken-focus);
}

.input--card::placeholder {
  color: var(--text-muted);
}

textarea.input--card {
  min-height: 60px;
  resize: vertical;
}

/* ─── .input--sm — density modifier; per-variant rule (see header) ─────── */
.input--card.input--sm {
  border-radius: var(--radius-sm);
  padding: var(--space-xs) var(--inset-sm);
  font-size: var(--text-sm);
}

/* ─── Goal intent toggle (Race / General fitness) ──────────────────────
   Typographic tabs, no pill backgrounds: the active tab reads via weight +
   bright text + a hairline coral underline. */
.goal-intent-toggle {
  display: flex;
  gap: var(--space-md);
  border-bottom: 1px solid var(--border);
}

.intent-tab {
  display: flex;
  align-items: center;
  text-align: center;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: 0 0 var(--space-sm);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  cursor: pointer;
}

.intent-tab:hover {
  color: var(--text-bright);
}

.toggle-input:checked + .intent-tab {
  color: var(--text-bright);
  font-weight: var(--weight-semibold);
  border-bottom-color: var(--accent-coach);
}

.toggle-input:focus-visible + .intent-tab {
  outline: 2px solid var(--accent-coach);
  outline-offset: 2px;
}

.goal-intent-toggle:has(#goal-intent-fitness:checked) ~ #goal-race-section,
.goal-intent-toggle:has(#goal-intent-race:checked) ~ #goal-fitness-section {
  display: none;
}

.manual-race-entry > summary,
.manual-race-entry > summary:hover {
  color: var(--accent-action-text);
}

.manual-race-entry[open] > summary {
  display: none;
}

/* =============================================================================
   FIELD ERROR SLOT

   Present in the markup at all times so a failure response can rewrite every
   slot of a form out-of-band; collapses to nothing while empty.
   ============================================================================= */

.field-error {
  color: var(--accent-danger-text);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  margin-top: var(--space-xs);
}

.field-error:empty {
  display: none;
}

.form-group:has(> .field-error:not(:empty)) .input--filled {
  border-bottom-color: var(--accent-danger);
}

/* =============================================================================
   LABELS-BELOW GRAMMAR

   Value / rule / label: the field reads first, its label sits under it as a
   caption. `<label for>` stays first in the DOM (a11y, click target) and CSS
   does the flipping. Rank order inside a group:
     -2  the field itself (and any picker chrome that follows it)
     -1  the field's error slot
      0  the label, then hints and secondary actions in DOM order

   Composed by `.form--labels-below` (page forms) and `.poster--form`
   (broadside forms in poster.css) — one owner, two entry points.

   Proximity: the label sits one 4px step under its own field — far tighter
   than the 16px rhythm gap before the next group — so it reads as a caption
   of the field above it, never of the field below.
   ============================================================================= */

:is(.form--labels-below, .poster--form) .form-group:has(> .form-label) {
  display: flex;
  flex-direction: column;
}

:is(.form--labels-below, .poster--form) .form-group:has(> .form-label) > .form-label {
  margin: var(--space-xs) 0 0;
}

:is(.form--labels-below, .poster--form) .form-group:has(> .form-label) > .form-label + * {
  order: -2;
}

:is(.form--labels-below, .poster--form) .form-group:has(> .form-label) > .field-error {
  order: -1;
}

/* Activity marks — composed base + role badge. Geometry per icons/ICONS.md:
   24x24 grid, fixed 2px stroke, badge at 60% bottom-right knocked out of the
   surface. Size comes from the caller's class; colour from currentColor. */

.steev-icon-wrap {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
}

.steev-icon {
  width: 100%;
  height: 100%;
  display: block;
}

.steev-icon__badge {
  position: absolute;
  right: -10%;
  bottom: -10%;
  width: 60%;
  height: 60%;
  background: var(--icon-knockout, var(--surface-raised));
  border-radius: 50%;
}

/* =============================================================================
   INTEGRATIONS - Settings cards for the run-sync paths (Apple Health, the
   Garmin data field). One card shape, stacked inside the Integrations zone.
   ============================================================================= */

/* Sunken inside a raised .form-panel — the same inset-card grammar the credit
   rows use, so a card reads as a panel of its own rather than as panel chrome. */
.integration-card {
  display: grid;
  gap: var(--space-sm);
  padding: var(--space-md) var(--inset);
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.integration-card + .integration-card {
  margin-top: var(--space-md);
}

.integration-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
}

/* The pairing code: the wrist shows it mono, tracked and upper-case, so the
   field the runner copies it into has to look like what they are reading. */
.input--code {
  font-family: var(--font-mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* =============================================================================
   JOURNEY-STAGE.CSS - Rich journey rail visualization (shared)

   The shared canonical rendering of the journey rail. Both the coach page
   (chat_area.html wraps in <div class="journey-stage rail-lit">) and the
   landing page (<div class="landing-journey-stage journey-stage">) consume
   this file — so the rail's status treatments on the activity mark (stub
   ghost, missed fade), the ellipsis older-count, and
   the triangle-pointer detail card with typed small-caps label render
   identically everywhere the journey is shown.

   Each home styles the band: the rail drops it (below), the one-column
   stream bleeds it (progress-track.css).

   Not to be confused with progress-track.css, which owns the simpler
   horizontal scrollable strip (marks + connectors) used outside a
   journey-stage wrapper.
   ============================================================================= */

/* The sunken band — the one-column strip's ground. */
.journey-stage {
  background: var(--surface-sunken);
  position: relative;
}

/* ─── Status treatments on the mark ─── */

/* The rail band is --surface-sunken, so the role badge's knockout disc has to
   be too — icons.css defaults it to --surface-raised. */
.journey-stage .track-mark {
  --icon-knockout: var(--surface-sunken);
}

/* Stub — a proposed workout whose detail isn't generated yet. Faint mark +
   italic label (distinct from the coral of a detailed workout) so the runner
   knows tapping it generates the session. Driven by the same
   data-detail-pending attribute as the tap-fill routing; higher specificity
   than progress-track.css's proposed tint so it wins. */
.journey-stage .track-node[data-detail-pending="true"] .track-mark {
  color: var(--text-faint);
}
.journey-stage .track-node[data-detail-pending="true"]:hover .track-mark {
  color: var(--accent-coach-text);
}
.journey-stage .track-node[data-detail-pending="true"] .track-label {
  color: var(--text-faint);
  font-style: italic;
}

/* Older — ellipsis glyph centred in the shared mark-sized slot */
.journey-stage .track-node--older .track-dot::before {
  content: "• • •";
  color: var(--text-muted);
  letter-spacing: 2px;
  font-size: var(--text-2xs);
  /* Wider than the slot — it overflows centred instead of wrapping to 3 lines. */
  white-space: nowrap;
}

/* ─── Scroll-reveal cascade (driven by JS adding .rail-lit) ─── */

/* New nodes: JS sets --reveal-i on each non-stable node; delay staggers
   left-to-right. Easing is a decelerate curve for a calm arrival feel. */
.journey-stage .track-node {
  opacity: 0;
  transform: translateY(10px) scale(0.94);
  transition: opacity 0.46s cubic-bezier(0.22, 0.61, 0.36, 1),
              transform 0.46s cubic-bezier(0.22, 0.61, 0.36, 1);
}
.journey-stage.rail-lit .track-node {
  opacity: 1;
  transform: none;
  transition-delay: calc(var(--reveal-i, 0) * var(--reveal-step));
}

/* Missed keeps its fade AFTER the reveal: the guard above (and the --stable one
   below) would otherwise silently defeat progress-track.css's node opacity —
   that is exactly how the first missed fade shipped dead. Keyed on data-status
   so a missed milestone fades too. */
.journey-stage.rail-lit .track-node[data-status="missed"] {
  opacity: 0.55;
}
.journey-stage.rail-lit .track-node[data-status="completed"] {
  opacity: 0.7;
}

/* A stub is a placeholder, not a session — ghost it below the reveal guard. */
.journey-stage.rail-lit .track-node[data-detail-pending="true"] {
  opacity: 0.5;
}

/* Pre-existing nodes skip the reveal cascade on strip refresh */
.journey-stage.rail-lit .track-node--stable {
  opacity: 1;
  transform: none;
  transition: none;
}

/* ─── Detail card ─── */

/* Triangle pointer above the card + typographic type label in the
   node-type color (no coloured left border; the dot + small-caps label
   carries the type signal in a way that fits the editorial voice). */
.journey-stage .timeline-detail {
  margin-top: var(--space-sm);
  position: relative;
}

/* Triangle pointer above the card. The border-left/right here are
   CSS-triangle geometry only — not a colored accent. */
.journey-stage .timeline-detail:not(:empty)::before {
  content: "";
  position: absolute;
  top: -8px;
  left: var(--arrow-left, 2rem);
  width: 0;
  height: 0;
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-bottom: 8px solid var(--surface-raised);
}

/* Type label: small mono, uppercase, node-type-colored, glyph + word.
   Rendered as the first line of padding inside the card via ::before. */
.journey-stage .chat-card {
  padding-top: calc(var(--space-md) + var(--text-2xs) + var(--space-xs));
}
.journey-stage .chat-card::before {
  position: absolute;
  top: var(--space-sm);
  left: var(--space-lg);
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  font-weight: var(--weight-medium);
}
.journey-stage .chat-card--completed::before {
  content: "● Completed";
  color: var(--accent-success-text);
}
.journey-stage .chat-card--proposed::before {
  content: "● Proposed";
  color: var(--accent-coach-text);
}
.journey-stage .chat-card--milestone::before {
  content: "◆ Milestone";
  color: var(--accent-strength-text);
}
.journey-stage .chat-card--goal::before {
  content: "▣ Goal";
  color: var(--tag-text-caution);
}

@media (prefers-reduced-motion: reduce) {
  .journey-stage .track-node {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* A bare rail — only the synthetic today marker — hides its whole zone, at
   every width and in both homes: a strip with nothing on it is not chrome the
   runner needs, and `+ Add goal` is the seam's (DESIGN §4.5). The strip's swap
   wiring stays in the DOM, so the next refreshJourney can un-bare it and the
   zone reappears. `data-bare` rides the strip wrapper, whichever element that
   is (nodes row, or the .track-strip around it). */
.chat-journey-zone:has([data-bare]) {
  display: none;
}

/* ─── Permanent coach rail ───────────────────────────────────────────────
   The zone lives inside <aside class="coach-rail">, so the sunken band is
   dropped and the stage is just content in the rail column. */
.coach-rail .journey-stage {
  background: transparent;
}

/* The ledger rail's ground is --paper-panel, not the sunken band the shared
   .journey-stage rule above assumes — so the status badge's knockout disc
   follows it, or the ✓/✗ stamp sits on a lighter puck than the rail. */
body.coach-ledger .coach-rail--ledger .track-mark {
  --icon-knockout: var(--paper-panel);
}

@media (min-width: 760.02px) {
  /* Vertical rail: the detail card sits under the node column, not beside a
     horizontal strip, so its pointer triangle has nothing to point at.
     positionArrow still sets --arrow-left; this override outranks it. The
     landing demo's; the ledger's twin is below. */
  .coach-landing .landing-rail .timeline-detail:not(:empty)::before {
    display: none;
  }
}

@media (min-width: 1000.02px) {
  body.coach-ledger .coach-rail--ledger .timeline-detail:not(:empty)::before {
    display: none;
  }

  /* The bare-zone hide above is the STREAM's: the rail's own header carries the
     face and `+ Add goal`, so hiding it leaves a runner with no plan an empty
     pane and no face on the page. */
  body.coach-ledger .coach-rail--ledger .chat-journey-zone:has([data-bare]) {
    display: block;
  }
}

/* =============================================================================
   LEADERBOARD-CONTROLS.CSS - Period + metric + tab selectors

   Compact control rows that sit above the list/podium: period toggle,
   metric toggle, subtle icon buttons.
   ============================================================================= */

.leaderboard-controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-md);
  align-items: center;
  margin-bottom: var(--space-lg);
}

.btn-icon-subtle {
  background: transparent;
  border: none;
  padding: var(--space-xs);
  cursor: pointer;
  opacity: 0.4;
  transition: opacity 0.15s;
  color: var(--text-muted);
  display: flex;
  align-items: center;
}
.btn-icon-subtle:hover {
  opacity: 1;
}

/* ─── Period toggle (above tabs) ─── */
.leaderboard-period-toggle {
  display: flex;
  gap: var(--space-xs);
  margin-bottom: var(--space-sm);
}
.period-btn {
  padding: var(--space-xs) var(--inset);
  background: var(--surface-sunken);
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  color: var(--text-secondary);
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
  transition: all var(--transition-fast);
}
.period-btn:hover {
  background: var(--surface-sunken-focus);
}
.period-btn.active {
  border-color: var(--accent-action);
  background: var(--accent-action-faint);
  color: var(--accent-action-text);
  font-weight: var(--weight-semibold);
}

/* ─── Metric buttons ─── */
.leaderboard-metric-selector {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-bottom: var(--space-md);
}
.metric-btn {
  padding: var(--space-xs) var(--inset-sm);
  background: var(--surface-sunken);
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  color: var(--text-secondary);
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  font-size: var(--text-sm);
  text-decoration: none;
}
.metric-btn:hover {
  background: var(--surface-sunken-focus);
}
.metric-btn.active {
  border-color: var(--accent-action);
  background: var(--accent-action-faint);
  color: var(--accent-action-text);
}

/* =============================================================================
   LEADERBOARD-LIST.CSS - Ranked entry rows (read-only mini-board)

   The full leaderboard page renders through the card vocabulary; what is left
   here is the base list + entry grid, rank colors and name/value columns used
   by partials/public_leaderboard.html on the race detail page — plus the page
   board's width: the page column, not the chat stream's 88%.
   ============================================================================= */

.leaderboard-page .chat-card {
  max-width: 100%;
}

.leaderboard-list {
  display: flex;
  flex-direction: column;
  gap: 0;
}
.leaderboard-entry {
  display: grid;
  grid-template-columns:
    clamp(1.75rem, 1rem + 2vw, 2.5rem)
    clamp(24px, 20px + 1vw, 32px)
    1fr
    auto;
  align-items: center;
  gap: var(--inset-sm);
  background: transparent;
  border-radius: 0;
  border-bottom: 1px solid var(--border);
}
.leaderboard-entry:last-child {
  border-bottom: none;
}

/* ─── Rank styling — semantic colors that work across all themes ─── */
.leaderboard-rank {
  font-family: var(--font-heading);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  font-size: 1.15em;
  min-width: clamp(1.75rem, 1rem + 2vw, 2.5rem);
  text-align: center;
  color: var(--text-muted);
}
.leaderboard-rank.rank-1 {
  color: var(--accent-success-text);
}
.leaderboard-rank.rank-2 {
  color: var(--accent-action-text);
}
.leaderboard-rank.rank-3 {
  color: var(--accent-moderate-text);
}

.leaderboard-name {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.leaderboard-value {
  font-family: var(--font-heading);
  font-weight: var(--weight-bold);
  color: var(--text-secondary);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  font-size: 1.1em;
}
.leaderboard-value.trend-positive {
  color: var(--accent-success-text);
}
.leaderboard-value.trend-negative {
  color: var(--accent-danger-text);
}

/* =============================================================================
   MESSAGE-BOARD.CSS - Race chat (leaderboard-scoped message board)

   Panel + header + guidelines callout + input form + empty/error notices
   + scrollable list of messages with hover-revealed delete. Rate-limited
   via config/app.toml (max messages/day/user). Visibility gated on
   leaderboard.visible = true server-side.
   ============================================================================= */

.message-board-panel {
  margin-top: var(--space-md);
  padding: var(--space-md) 0;
  border-top: 1px solid var(--border);
}

.message-board-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: var(--space-md);
  padding-bottom: var(--space-md);
  border-bottom: 1px solid var(--surface-sunken);
}

.message-board-title {
  margin: 0;
  font-size: var(--text-lg);
}

.message-board-race {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.message-board-guidelines {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  padding: var(--space-sm) var(--inset);
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
  border-left: 2px solid var(--text-muted);
}

.message-board-form {
  margin-bottom: var(--space-lg);
  padding-bottom: var(--space-md);
  border-bottom: 1px solid var(--surface-sunken);
}

.message-board-input-row {
  display: flex;
  gap: var(--space-sm);
  align-items: flex-end;
}


.message-board-error {
  background: var(--accent-danger-faint);
  color: var(--accent-danger-text);
  padding: var(--space-sm) var(--inset);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-md);
  font-size: var(--text-sm);
}

.message-board-notice {
  text-align: center;
  padding: var(--space-md) var(--inset);
  margin-bottom: var(--space-md);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
}

.message-board-notice p {
  margin-bottom: var(--space-sm);
  color: var(--text-muted);
}

.message-board-empty {
  text-align: center;
  padding: var(--space-xl) var(--inset-lg);
  color: var(--text-secondary);
}

.message-board-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  max-height: 400px;
  overflow-y: auto;
}

.message-board-message {
  display: flex;
  gap: var(--space-sm);
  padding: var(--space-sm);
  background: transparent;
  border-bottom: 1px solid var(--border);
  position: relative;
}

.message-board-message:last-child {
  border-bottom: none;
}

.message-board-message:hover {
  background: var(--surface-sunken);
}

.message-board-message .avatar {
  flex-shrink: 0;
}

.message-content {
  flex: 1;
  min-width: 0;
}

.message-header {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  margin-bottom: var(--space-xs);
}

.message-author {
  font-weight: var(--weight-semibold);
  font-size: var(--text-sm);
}

.message-time {
  font-size: var(--text-xs);
  color: var(--text-muted);
}

.message-text {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  word-wrap: break-word;
  white-space: pre-wrap;
}

.message-delete-btn {
  position: absolute;
  top: var(--space-sm);
  right: var(--space-sm);
  background: transparent;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  padding: var(--space-xs);
  border-radius: var(--radius-sm);
  opacity: 0;
  transition:
    opacity var(--transition-fast),
    color var(--transition-fast);
}

.message-board-message:hover .message-delete-btn {
  opacity: 1;
}

.message-delete-btn:hover {
  color: var(--accent-danger-text);
  background: var(--accent-danger-faint);
}

/* =============================================================================
   MODALS - Modal overlay and dialog components
   ============================================================================= */

/* Native dialog modal — replaces JS-driven .modal-overlay */
dialog#modal-container {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  background: transparent;
  border: none;
  padding: var(--space-lg) var(--inset-lg);
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
}

dialog#modal-container:not([open]) {
  display: none;
}

dialog#modal-container::backdrop {
  background: var(--overlay-bg);
  animation: fadeIn 0.15s ease-out;
}

.modal {
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  padding: var(--space-xl) var(--inset-lg);
  max-width: var(--content-max-width);
  width: 100%;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: var(--shadow-lg);
  animation: slideUp 0.2s ease-out;
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.modal-title {
  margin: 0 0 var(--space-md) 0;
  font-size: var(--text-lg);
  color: var(--text-bright);
}

.modal-subtitle {
  margin: 0 0 var(--space-lg) 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.modal-actions {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  justify-content: flex-end;
  margin-top: var(--space-lg);
}

/* =============================================================================
   RACE SELECTOR - Goal modal race search
   ============================================================================= */

.race-search-group {
  position: relative;
}

.race-results {
  /* Inline positioning avoids clipping from modal's overflow-y: auto */
  margin-top: var(--space-xs);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  max-height: 200px;
  overflow-y: auto;
}

.race-results:empty {
  display: none;
}

.race-results-empty {
  padding: var(--space-md);
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  color: var(--text-secondary);
  background: var(--surface-sunken);
  border-radius: var(--radius-md);
}

.race-results-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.race-result-item {
  padding: var(--space-sm) var(--space-md);
  cursor: pointer;
  border-bottom: 1px solid var(--border);
  transition: background-color var(--transition-fast);
}

.race-result-item:last-child {
  border-bottom: none;
}

.race-result-item:hover {
  background: var(--surface-sunken-focus);
}

.race-result-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.race-result-name {
  font-weight: var(--weight-medium);
  color: var(--text);
}

.race-result-location {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.race-result-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: var(--space-xs);
}

.race-result-date {
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.selected-race {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-md);
  background: var(--accent-action-faint);
  border: 1px solid var(--accent-action);
  border-radius: var(--radius-md);
  margin-top: var(--space-sm);
}

.selected-race-name {
  font-weight: var(--weight-medium);
  color: var(--text);
}

.selected-race-meta {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  flex: 1;
}

/* =============================================================================
   NAVIGATION - Inline priority nav + avatar-dropdown fallback
   ============================================================================= */

/* Frosted bar over the page's own surface (--surface is the body background in
   both schemes), under a dashed rule. */
.nav-minimal {
	position: sticky;
	top: 0;
	z-index: var(--z-nav);
	background: color-mix(in srgb, var(--surface) 94%, transparent);
	backdrop-filter: blur(8px);
	-webkit-backdrop-filter: blur(8px);
	border-bottom: 1.5px dashed var(--paper-rule);
}

/* Height CONTRACT: total rendered header = --coach-header-h. The rail's sticky
   offsets read that token, so the inner box is the token minus the 1.5px rule. */
.nav-inner {
	display: flex;
	align-items: center;
	gap: var(--space-md);
	max-width: var(--content-max-width);
	margin: 0 auto;
	padding: var(--space-sm) var(--inset-lg);
	height: calc(var(--coach-header-h) - 1.5px);
	container-type: inline-size;
	container-name: navbar;
}

/* Brand/Logo */
.nav-brand {
	flex-shrink: 0;
	display: flex;
	align-items: center;
	gap: 11px;
	text-decoration: none;
	color: var(--text-bright);
	transition: color var(--transition-elegant);
}

.nav-brand:hover {
	color: var(--accent-action-text);
}

/* The face's wrapper span: the coral disc the face sits in, on EVERY page. On
   the coach page it rides BESIDE the ledger label (nav.html) and is hidden again
   in two panes, where the rail's header carries the face. */
.nav-brand-blob {
	display: grid;
	place-items: center;
	width: 38px;
	height: 38px;
	border-radius: var(--radius-blob);
	background: var(--accent-coach-soft);
	transform: rotate(-3deg);
}

/* One size, one ink, every state — the disc carries the brand signal, so the
   face neither grows on /coach nor shifts hue on hover. The face derives its
   mouth shade from --accent-info; re-derive it from currentColor (= the ink
   above) or an outburst paints a mauve mouth on coral. */
.nav-brand--face .steev-face {
	width: 29px;
	height: 29px;
	color: var(--paper-ink-on-coral);
	--mouth-shade: color-mix(in oklab, currentColor 55%, black 45%);
}

/* Inline nav items cluster against .nav-right (margin-left: auto pushes
   the group to the right edge). Avoids the dead gap that appeared when
   .nav-items sat flush after the brand and .nav-right floated alone to
   the far edge. */
.nav-items {
	display: flex;
	align-items: center;
	gap: var(--space-sm);
	flex: 0 1 auto;
	min-width: 0;
	margin-left: auto;
}

.nav-item {
	flex-shrink: 0;
	padding: 8px 13px;
	border-bottom: 2.5px solid transparent;
	font-family: var(--font-mono);
	font-size: 12.5px;
	font-weight: 600;
	letter-spacing: 0.04em;
	text-transform: lowercase;
	color: var(--text-muted);
	text-decoration: none;
	white-space: nowrap;
	transition: color var(--transition-elegant);
}

/* Active: coach-coral with its own underline. */
.nav-item.active {
	color: var(--accent-coach-text);
	border-bottom-color: currentColor;
	display: flex !important;
}

/* Priority-based inline hiding driven by the nav's own width (not viewport).
   Items hidden here appear in the avatar dropdown's overflow section.
   Thresholds are set so items only move once they actually no longer fit. */
@container navbar (max-width: 600px) {
	.nav-minimal .nav-item[data-priority="4"] {
		display: none;
	}
}

@container navbar (max-width: 480px) {

	.nav-minimal .nav-item[data-priority="2"],
	.nav-minimal .nav-item[data-priority="3"] {
		display: none;
	}
}

/* Below the last breakpoint only the active item remains. Let it ellipsize
   and trim nav padding so the avatar can never be pushed off-screen. */
@container navbar (max-width: 420px) {
	.nav-minimal .nav-item.active,
	.nav-ledger-label {
		flex-shrink: 1;
		min-width: 0;
		overflow: hidden;
		text-overflow: ellipsis;
	}
}

@media (max-width: 420px) {
	.nav-inner {
		padding-left: var(--space-sm);
		padding-right: var(--space-sm);
	}
}

/* Right side container — sits adjacent to .nav-items (which carries the
   margin-left: auto that pushes the whole group right). */
.nav-right {
	display: flex;
	align-items: center;
	gap: var(--space-sm);
	position: relative;
	flex-shrink: 0;
}

/* Credit badge - only shown when low/exhausted.
   A hidden placeholder reserves the same slot when absent, so nav breakpoints
   don't shift as credit state changes. */
.nav-credit-badge {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-width: 22px;
	height: 22px;
	padding: 0 var(--space-xs);
	border-radius: var(--radius-full);
	font-size: var(--text-2xs);
	font-weight: var(--weight-semibold);
	text-decoration: none;
	transition: transform var(--transition-spring);
}

.nav-credit-badge:hover {
	transform: scale(1.08);
}

.nav-credit-badge--placeholder {
	visibility: hidden;
	pointer-events: none;
}

.nav-credit-badge--placeholder:hover {
	transform: none;
}

.nav-credit-badge.low {
	background: var(--accent-caution-faint);
	color: var(--accent-caution-text);
}

.nav-credit-badge.exhausted {
	background: var(--accent-danger-faint);
	color: var(--accent-danger-text);
}

.nav-credit-badge.admin {
	background: var(--accent-info-faint);
	color: var(--accent-info-text);
	font-weight: var(--weight-bold);
}

/* Details-based dropdown */
.nav-dropdown {
	position: relative;
}

.nav-dropdown::details-content {
	display: block;
	overflow: visible;
}

/* Avatar trigger button (now a <summary>) */
.nav-user-trigger {
	display: flex;
	padding: 0;
	background: transparent;
	border: 2px solid transparent;
	border-radius: var(--radius-full);
	cursor: pointer;
	transition: border-color var(--transition-elegant);
}

.nav-user-trigger:hover {
	border-color: color-mix(in srgb, var(--accent-action) 50%, transparent);
}

.nav-dropdown[open]>.nav-user-trigger {
	margin-bottom: 0;
}

/* Vertical 3-dot kebab — the user-menu trigger. Reads as "secondary
   options" so it yields visual hierarchy to the nav-brand face on the
   left. Three explicit child spans (rather than an SVG or Unicode glyph)
   make hover/open color shifts easy to express. */
.nav-menu-dots {
	width: 28px;
	height: 28px;
	border-radius: var(--radius-full);
	display: inline-flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: 3px;
}

.nav-menu-dots>span {
	width: 4px;
	height: 4px;
	border-radius: 50%;
	background: var(--text-muted);
	transition: background var(--transition-elegant);
}

.nav-user-trigger:hover .nav-menu-dots>span,
.nav-dropdown[open]>.nav-user-trigger .nav-menu-dots>span {
	background: var(--accent-action);
}

/* Dropdown menu */
.nav-user-menu {
	position: absolute;
	top: calc(100% + var(--space-xs));
	right: 0;
	min-width: 200px;
	background: var(--surface-raised);
	border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
	border-radius: var(--radius-lg);
	box-shadow: var(--shadow-lg);
	opacity: 0;
	visibility: hidden;
	pointer-events: none;
	transform: translateY(-8px);
	transition: opacity var(--transition-elegant), transform var(--transition-elegant);
	z-index: 2;
}

.nav-dropdown[open] .nav-user-menu {
	opacity: 1;
	visibility: visible;
	pointer-events: auto;
	transform: translateY(0);
}

/* User header in dropdown */
.nav-user-header {
	padding: var(--space-md);
}

.nav-user-name {
	font-weight: var(--weight-semibold);
	color: var(--text-bright);
	display: block;
}

.nav-user-tier {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--text-muted);
}

.nav-user-tier.tier-pro {
	color: var(--accent-action-text);
}

.nav-user-tier.tier-basic {
	color: var(--accent-accent-text);
}

/* Menu items */
.nav-menu-item {
	display: block;
	padding: var(--space-sm) var(--space-md);
	color: var(--text-secondary);
	text-decoration: none;
	font-size: var(--text-sm);
	background: none;
	border: none;
	width: 100%;
	text-align: left;
	cursor: pointer;
	font-family: inherit;
	transition:
		color var(--transition-elegant),
		background var(--transition-elegant);
}

.nav-menu-item:hover {
	color: var(--text);
	background: var(--surface-sunken);
}

.nav-menu-item.active {
	color: var(--accent-action-text);
	background: var(--surface-sunken);
}

/* Divider */
.nav-menu-divider {
	height: 1px;
	background: var(--border);
	margin: var(--space-xs) 0;
}

/* Logout styling */
.nav-logout:hover {
	color: var(--accent-danger-text);
	background: var(--accent-danger-faint);
}

/* ── Public nav auth CTAs ── */
.nav-auth-ctas {
	display: flex;
	align-items: center;
	gap: var(--space-sm);
}

.nav-auth-btn {
	display: inline-flex;
	align-items: center;
	min-height: var(--tap-target);
	padding: 0 var(--space-sm);
	font-family: var(--font-body);
	font-size: var(--text-sm);
	font-weight: var(--weight-semibold);
	color: var(--accent-coach-text);
	white-space: nowrap;
}

.nav-auth-btn:hover,
.nav-auth-btn:focus-visible {
	text-decoration: underline;
	text-underline-offset: 3px;
}

.nav-auth-btn:focus-visible {
	outline: 2px solid var(--accent-action);
	outline-offset: 2px;
	border-radius: var(--radius-xs);
}

/* ── Dropdown overflow section (mirrors inline nav items) ──
   Every nav item is rendered here but hidden by default. Container queries
   reveal a dropdown twin only when its inline counterpart is hidden, so each
   item is reachable in exactly one place. */
.nav-overflow-section {
	display: none;
}

.nav-overflow-item {
	display: none;
}

@container navbar (max-width: 600px) {
	.nav-overflow-section {
		display: block;
	}

	.nav-overflow-item[data-priority="4"] {
		display: block;
	}
}

@container navbar (max-width: 480px) {

	.nav-overflow-item[data-priority="2"],
	.nav-overflow-item[data-priority="3"] {
		display: block;
	}
}

/* Active page is always inline; never duplicate it in the dropdown. */
.nav-overflow-section .nav-overflow-item.active {
	display: none !important;
}

/* The ledger header label (SPEC §2.1) — replaces the brand blob + wordmark +
   tagline stack on the coach page only; `nav.ledger_label` is `None`
   everywhere else. */
.nav-ledger-label {
	font-family: var(--font-mono);
	font-size: 11px;
	font-weight: 600;
	letter-spacing: 0.14em;
	color: var(--text-secondary);
	white-space: nowrap;
}

/* Quieter beside the ledger label than beside the landing's wordmark: the
   header is the log's masthead, not a nav bar. */
.coach-ledger .nav-item {
	font-size: 12px;
}

.nav-item:not(.active):hover {
	color: var(--text);
	border-bottom-color: var(--paper-rule);
}

@media (max-width: 760px) {
	.nav-item {
		padding: 8px 10px;
	}
}

@media (min-width: 1000.02px) {
	/* Two panes: the rail's header carries the face (rail-steev). One face per
	   width — steev-face.js drives every .steev-face[data-state], so whichever is
	   visible wears the landed mood. */
	body.coach-ledger .nav-brand--face {
		display: none;
	}
}

/* =============================================================================
   ONBOARDING.CSS - The onboarding conversation card

   The affordance strip the runner answers, mounted inside the coach timeline.
   Prefixed `convo-` on purpose: `onboarding-step` (no dot — a class name, not
   a selector) already belongs to onboarding-checklist.css. Tokens only.
   ============================================================================= */

/* Full width on a phone — a nine-chip row needs every pixel — and capped on a
   wide screen. */
.convo-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding: var(--space-md) var(--inset);
  max-width: min(100%, 40rem);
  background: color-mix(in srgb, var(--accent-user) 10%, var(--surface-raised));
  border: 1px solid color-mix(in srgb, var(--accent-user) 35%, transparent);
  border-radius: var(--radius-lg) var(--radius-xs) var(--radius-lg)
    var(--radius-lg);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  color: var(--text);
}

/* streaming.js moves focus to the card itself (tabindex="-1") after each swap;
   without a ring a keyboard runner is told nothing about where they landed. */
.convo-card:focus-visible {
  outline: 2px solid var(--accent-user);
  outline-offset: 2px;
}

.convo-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

/* Labels-below grammar, forms.css:736-751 in miniature. The DOM is
   affordance → label → error (a11y: the caption is authored next to the control
   it names), so CSS does the flipping: the error lands against the control that
   failed, and hints and the manual-race escape hatch read after the caption.
   The link's tab order now precedes a non-focusable caption — harmless. */
.convo-field > .field-error {
  order: 1;
}

.convo-field > .convo-label {
  order: 2;
}

/* The manual-race box rides with the link that reveals it, or it would surface
   above the control that opens it. */
.convo-field > .form-hint[data-date-label],
.convo-field > .form-link,
.convo-field > #convo-manual-race {
  order: 3;
}

/* Mark the control, not just the space under it (forms.css:714). The recap
   body is the same group with no caption. */
:is(.convo-field, .convo-recap-body):has(> .field-error:not(:empty))
  .input--filled {
  border-bottom-color: var(--accent-danger);
}

/* One caption voice for the card: the field label, the chip-group header and
   the recap row's label are the same annotation at the same rank (the iOS
   EmberFieldLabel twin renders all three with one treatment). In the step card
   the label sits BELOW its control — the control is the subject, the caption
   the annotation; in the recap it leads the row. */
.convo-label,
.convo-chip-group,
.convo-recap-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  color: var(--text-muted);
  margin: 0;
}

/* ── Chips ── */

.convo-chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

.convo-chip-group {
  flex-basis: 100%;
  margin: var(--space-xs) 0 0;
}

.convo-chip {
  display: inline-flex;
  align-items: center;
  padding: var(--space-sm) var(--space-md);
  border: 1px solid var(--border);
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  cursor: pointer;
  transition: border-color var(--transition-fast), color var(--transition-fast), background var(--transition-fast);
}

.convo-chip input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

/* Shape alone encodes select-mode, so it survives without colour: a
   single-select row is a run of soft-rectangle TILES (pick exactly one), a
   multi-select row stays round pills that grow a check glyph. Distance presets
   are radios, so they read as tiles too. Heights are deliberately equal — the
   old pill was ~30px, under every touch guideline. */
.convo-chip:has(input[type="radio"]) {
  border-radius: var(--radius-md);
}

/* `content: "✓" / ""` — the empty alternative text keeps the glyph out of the
   accessible name, which otherwise reads "✓ Cycling". */
.convo-chip:has(input[type="checkbox"]:checked)::before {
  content: "✓" / "";
  margin-right: var(--space-xs);
  font-size: var(--text-xs);
}

/* A SELECTION IS THE RUNNER SPEAKING, so it wears the user accent solid —
   never `--accent-info`, which is reserved for the coach's own voice. */
.convo-chip:hover {
  background: var(--accent-user-faint);
  color: var(--text);
}

.convo-chip:has(input:checked) {
  background: var(--accent-user-strong);
  border-color: transparent;
  color: var(--text-on-accent);
}

/* Without this a selected chip goes dead to the pointer: `:has(input:checked)`
   follows `:hover` at equal specificity and wins the tie. */
.convo-chip:has(input:checked):hover {
  background: var(--accent-user-strong-hover);
}

.convo-chip:has(input:focus-visible) {
  outline: 2px solid var(--accent-user);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: no-preference) {
  .convo-chip:active {
    transform: scale(0.98);
  }
}

/* ── Typed entry ── */

/* The custom-distance and mm:ss controls are forms.css's shipped
   `.custom-distance-input` / `.time-input-group` components — the card adds
   nothing of its own. A year is the one control with no shipped home: four
   digits, sized to them. */

.input.year-input {
  width: calc(5ch + 2 * var(--inset));
}

.convo-sink {
  display: none;
}

/* ── Foot ── */

.convo-foot {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
}

/* Progress as a ROUTE: the dots sit on a thin connecting track, the ones
   behind the runner are filled and the one they are standing on wears a ring.
   Filled reads as the runner's own progress, so it takes the user accent. The
   track is drawn first in tree order, so every dot paints over it. */
.convo-progress {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  margin-right: auto;
}

.convo-progress::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--border);
  transform: translateY(-50%);
}

.convo-dot {
  position: relative;
  width: 0.375rem;
  height: 0.375rem;
  border-radius: var(--radius-full);
  background: var(--border);
  transition: background var(--transition-fast), box-shadow var(--transition-fast);
}

.convo-dot.is-on {
  background: var(--accent-user);
}

.convo-dot.is-current {
  width: 0.5rem;
  height: 0.5rem;
  box-shadow: 0 0 0 2px var(--accent-user-faint);
}

/* A real control, still subordinate to Next: btn-sm's box (so the foot row
   reads as one height) without the `.btn` chrome — no border, muted ink, a
   surface only on hover. The extra right margin buys a beat of separation so
   Skip is never mistaken for part of the primary button. */
.convo-skip {
  background: none;
  border: none;
  padding: var(--space-xs) var(--space-sm);
  margin-right: var(--space-xs);
  border-radius: var(--radius-md);
  font-family: inherit;
  font-size: var(--text-sm);
  color: var(--text-muted);
  cursor: pointer;
}

.convo-skip:hover {
  background: var(--surface-overlay);
  color: var(--text-secondary);
}

.convo-skip:focus-visible {
  outline: 2px solid var(--accent-user);
  outline-offset: 2px;
}

/* ── Recap ── */

.convo-recap-title {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--text-secondary);
}

/* A divider BETWEEN rows: the first row already has the title's baseline above
   it, and a rule there would fence the title off from its own list. Adjacent
   siblings rather than `:first-of-type` — the rows are <form>s among other
   <form>s, so element type is the wrong thing to count. */
.convo-recap-line + .convo-recap-line {
  border-top: 1px solid var(--border);
  padding-top: var(--space-xs);
}

/* `cursor`, `list-style` and the webkit marker reset come from collapsibles.css
   (`details summary`) — this only adds the row's own layout. Padding plus a
   matching negative margin lets the hover band breathe past the text without
   shifting the row out of the card's column. */
.convo-recap-summary {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  padding: var(--space-xs);
  margin-inline: calc(-1 * var(--space-xs));
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
}

/* The whole row is the control, so the whole row has to answer the pointer. */
.convo-recap-summary:hover {
  background: var(--surface-overlay);
}

.convo-recap-summary:focus-visible {
  outline: 2px solid var(--accent-user);
  outline-offset: 2px;
}

/* Caption left, value right — the iOS row (OnboardingRecapCard) is the shape.
   The label yields its width FIRST (`flex-shrink` outweighs the value's 1) and
   only down to the floor, which is `layoutPriority(1)` plus `minWidth` over
   there: without the weight a two-word value wrapped mid-row next to a long
   caption, without the floor a long note left the caption one word per line. */
.convo-recap-label {
  flex-shrink: 999;
  min-width: 8em;
}

.convo-recap-value {
  margin-left: auto;
  text-align: right;
  color: var(--text);
}

/* Three lines, then ellipsis — the iOS `lineLimit(3)` twin. A recap line is a
   summary; open the row to read and edit the rest. The legacy `-webkit-box` is
   still the only JS-free multi-line clamp, and it stays off the invitation
   capsule, which is one short phrase that must not become a scroll box. */
.convo-recap-value:not(.convo-recap-missing) {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ANY unanswered field: an invitation, not an error — no left border bar, and
   never the danger accent. Same faint-pill idiom as .nav-credit-badge.admin,
   in the coach-voice (--accent-info) family so it reads as Steev gently
   prompting, not a validation failure. The template gives such a row no Change
   slot, so this pill is what the runner taps — the <summary> is the control. */
.convo-recap-missing {
  padding: var(--space-xs) var(--space-sm);
  background: var(--accent-info-faint);
  color: var(--accent-info-text);
  border-radius: var(--radius-full);
}

/* The runner's own affordance — tapping it is them speaking, so it belongs to
   the user accent, not to `--accent-info` (the coach's voice). While the row
   is open the SAME slot reads Save: one control, two states, no body button. */
.convo-recap-edit {
  font-size: var(--text-xs);
  color: var(--accent-user-text);
}

.convo-recap-save {
  display: none;
  background: none;
  border: none;
  padding: 0;
  font-family: inherit;
  font-size: var(--text-xs);
  color: var(--accent-user-text);
  cursor: pointer;
}

/* The quiet-text-control hover the app already uses (.entry-line-send,
   .chat-chip): the underline is the affordance, so it reads the same in both
   themes without a second accent. */
.convo-recap-summary:hover .convo-recap-edit,
.convo-recap-save:hover {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

details[open] .convo-recap-edit {
  display: none;
}

details[open] .convo-recap-save {
  display: inline;
}

/* Horizontal inset too: the tint is a panel, and a reopened control flush
   against its edge reads as an overflow, not a nesting.
   NOT --surface-overlay: in light mode it resolves to the card's own surface,
   so the panel had no tint; --surface-raised-hover matches it in dark and is
   a real step in light. */
.convo-recap-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-xs) var(--space-sm) var(--space-sm);
  background: var(--surface-raised-hover);
  border-radius: var(--radius-sm);
}

/* =============================================================================
   ONBOARDING-CHECKLIST.CSS - First-run setup progress card

   Unified small card that surfaces the setup steps (profile,
   connection, first goal, etc.) with dismissable rows and a dismissable
   outer panel. Plays with the existing `.progress` primitive from
   base.css for the completion bar.
   ============================================================================= */

.onboarding-checklist {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-sm) var(--inset-lg) var(--space-sm) var(--inset);
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  margin-bottom: var(--space-sm);
}

.onboarding-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-sm);
  padding-right: var(--inset-lg);
}

.onboarding-title {
  font-weight: var(--weight-medium);
  color: var(--text);
}

.onboarding-checklist .progress {
  margin-bottom: var(--space-xs);
}

.onboarding-step {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-xs) 0;
}

.onboarding-step + .onboarding-step {
  border-top: 1px solid var(--border);
}

.onboarding-dismiss {
  position: absolute;
  top: var(--space-xs);
  right: var(--space-xs);
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: var(--text-lg);
  cursor: pointer;
  padding: 0 var(--space-xs);
  line-height: 1;
}

.onboarding-dismiss:hover {
  color: var(--text);
}

.onboarding-step-dismiss {
  background: none;
  border: none;
  color: var(--text-muted);
  font-size: var(--text-base);
  cursor: pointer;
  padding: 0 var(--space-xs);
  line-height: 1;
}

.onboarding-step-dismiss:hover {
  color: var(--text);
}

.onboarding-complete-title {
  color: var(--accent-info-text);
  font-weight: var(--weight-semibold);
}

.onboarding-complete-body {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* =============================================================================
   PAGE-ZONES.CSS - Shared patterns extracted from dashboard design language
   Staggered entrance animations, zone labels, form panels, pill buttons.
   ============================================================================= */

/* ─── A. Staggered entrance animations ─── */

@keyframes coach-enter {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  /* `none`, not `translateY(0)`: fill-mode `both` leaves the end state on the
     element for good, and a transformed ancestor is a containing block for
     `position: fixed` — which would trap the one-column node sheet inside the
     chat column (progress-track.css). Visually identical. */
  to {
    opacity: 1;
    transform: none;
  }
}

[data-zone] {
  animation: coach-enter 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
}

[data-zone="1"] {
  animation-delay: 0ms;
}
[data-zone="2"] {
  animation-delay: 80ms;
}
[data-zone="3"] {
  animation-delay: 160ms;
}
[data-zone="4"] {
  animation-delay: 240ms;
}
[data-zone="5"] {
  animation-delay: 320ms;
}
[data-zone="6"] {
  animation-delay: 400ms;
}
[data-zone="7"] {
  animation-delay: 480ms;
}
[data-zone="8"] {
  animation-delay: 560ms;
}
[data-zone="9"] {
  animation-delay: 640ms;
}
[data-zone="10"] {
  animation-delay: 720ms;
}

@media (prefers-reduced-motion: reduce) {
  [data-zone] {
    animation: none;
  }
}

/* ─── B. Page zone label ─── */
/* Matches .timeline-section-header / .momentum-label style */

.page-zone-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  font-weight: var(--weight-medium);
  color: color-mix(in srgb, var(--accent-coach) 70%, var(--text-muted));
  margin-bottom: var(--space-md);
}

/* ─── C. Form panel ─── */
/* Lighter alternative to .section > .card nesting */

.form-panel {
  background: var(--surface-raised);
  border-radius: var(--radius-lg);
  padding: var(--space-lg) var(--inset-lg);
  border: 1px solid var(--border);
  margin-bottom: var(--space-md);
  margin-top: var(--space-md);
}

/* ─── D. Pill button modifier ─── */

.btn-pill {
  border-radius: var(--radius-full);
}

/* =============================================================================
   PROGRESS-TRACK.CSS - Horizontal journey timeline with activity marks

   Shared component: consumed by both the coach page and landing. Page-
   specific chrome (e.g. the coach's sunken band, the landing stage
   wrapper) lives in coach-page.css / landing.css; this file owns the
   track itself, nodes, marks, connectors, labels, and their state
   variants (completed / proposed / milestone / goal / older / add /
   active).
   ============================================================================= */

/* ─── Journey zone framing (coach page reuse) ─── */

.chat-timeline-msg {
  padding: var(--space-md) 0;
}

/* Center the journey within the full-bleed band; `safe` falls back to start so
   journeys wider than the viewport stay scrollable instead of clipping TODAY. */
.chat-journey-zone .track-nodes {
  justify-content: safe center;
}

/* Detail card lives inside .progress-track now (sibling of the nodes row) so
   it scrolls with the strip. Anchor it under its node via --detail-left (set
   by streaming.js) and cap width so the scroll range stays sane. Coach only —
   landing keeps its own full-width demo card. */
.chat-journey-zone .timeline-detail {
  margin-left: var(--detail-left, 0);
  width: min(34rem, calc(100vw - 2 * var(--inset)));
}

/* ─── Track container — the scroller and its fade are the `.scroll-row` /
   `.edge-fade` markup utilities. A site that is not a horizontal scroller nulls
   the mask itself: the desktop coach rail below, and landing.css. ─── */
.progress-track {
  /* The mark's box, and the radius the connector halves stop at. One home —
     three sites used to restate the old dot's 6px radius. */
  --track-mark-size: 20px;
  --track-mark-radius: calc(var(--track-mark-size) / 2);
  position: relative;
  overflow-y: visible;
  padding: var(--space-sm) 0 var(--space-xs);
}

/* Nodes container */
.track-nodes {
  display: flex;
  align-items: flex-start;
  gap: var(--space-md);
  min-width: max-content;
  position: relative;
  padding: 0 var(--inset);
}

/* Base node — clickable dot + label */
.track-node {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-family: inherit;
  transition: transform var(--transition-fast);
  position: relative;
  isolation: isolate;
}

/* Per-node connector: two half-segments (::before = left, ::after = right).
   Each extends half the flex gap beyond the node edge, meeting adjacent
   nodes' segments at the gap midpoint. */
.track-node::before,
.track-node::after {
  content: "";
  position: absolute;
  top: calc(0.7rem + 3px + var(--track-mark-radius));
  height: 2px;
  z-index: -1;
  pointer-events: none;
  background: repeating-linear-gradient(
    90deg,
    color-mix(in srgb, var(--text-muted) 16%, transparent) 0 4px,
    transparent 4px 8px
  );
}

/* Left half: gap midpoint → mark left edge */
.track-node::before {
  left: calc(var(--space-md) / -2);
  right: calc(50% + var(--track-mark-radius));
}

/* Right half: mark right edge → gap midpoint */
.track-node::after {
  left: calc(50% + var(--track-mark-radius));
  right: calc(var(--space-md) / -2);
}

/* Completed/older: both halves solid */
.track-node--completed::before,
.track-node--completed::after,
.track-node--older::before,
.track-node--older::after {
  background: color-mix(in srgb, var(--text-muted) 28%, transparent);
}

/* Last solid node's right half reverts to dashed (transition point).
   :has(+) checks if the next sibling is NOT completed/older. */
.track-node--completed:has(
    + :not(.track-node--completed):not(.track-node--older)
  )::after,
.track-node--older:has(
    + :not(.track-node--completed):not(.track-node--older)
  )::after {
  background: repeating-linear-gradient(
    90deg,
    color-mix(in srgb, var(--text-muted) 16%, transparent) 0 4px,
    transparent 4px 8px
  );
}

/* First node: clip left connector to node edge (no extension into empty space) */
.track-nodes > :first-child::before {
  left: 0;
}

/* Last node: no right connector */
.track-nodes > :last-child::after {
  display: none;
}

.track-node:hover {
  transform: translateY(-1px);
}

.track-node:active {
  transform: scale(0.96);
}

/* ─── Mark (the activity icon on the track line) ─── */
.track-mark {
  width: var(--track-mark-size);
  height: var(--track-mark-size);
  color: var(--node-accent, var(--text-muted));
  transition:
    color var(--transition-elegant),
    transform var(--transition-elegant);
}

.track-node:hover .track-mark {
  transform: scale(1.15);
}

/* The three nodes with no workout behind them — the today anchor, "+N older"
   and "+ add" — keep a plain span instead. It carries no shape of its own: it
   is the mark-sized slot the connector anchors to, and each type draws its own
   content inside it. */
.track-dot {
  width: var(--track-mark-size);
  height: var(--track-mark-size);
  flex-shrink: 0;
  display: grid;
  place-items: center;
}

/* ─── Labels ─── */

.track-date {
  font-size: var(--text-2xs);
  line-height: 1;
  color: var(--text-muted);
  white-space: nowrap;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.track-node--proposed .track-date {
  color: var(--tag-text-coach);
}

.track-label {
  font-size: var(--text-xs);
  color: var(--text-muted);
  white-space: normal;
  text-align: center;
  letter-spacing: 0.01em;
}

/* ─── Node type variants ─── */

/* Accent color per node type (used by --active state) */
.track-node--completed {
  --node-accent: var(--text-muted);
}
.track-node--proposed {
  --node-accent: var(--accent-coach);
}
/* Stale falls through to proposed. Unreachable today — journey.rs filters every
   stale workout out of the strip — but stated so the two clients cannot disagree
   if that filter ever changes. */
.track-node--stale {
  --node-accent: var(--accent-coach);
}
.track-node--milestone {
  --node-accent: var(--accent-strength);
}
.track-node--goal {
  --node-accent: var(--accent-caution);
}
.track-node--older {
  --node-accent: var(--text-muted);
}
.track-node--add {
  --node-accent: var(--accent-caution);
}

/* Status treatments key on data-status so they reach milestone and goal nodes
   too — the type class is kind-first and hides their status. Quiet history,
   loud plan: done recedes to muted with a ✓ on the label; the plan keeps the
   kind colours; missed is rose + strike + fade. */
.track-node[data-status="completed"] .track-mark {
  color: var(--text-muted);
}
.track-node[data-status="completed"] .track-date,
.track-node[data-status="completed"] .track-label {
  color: var(--text-muted);
}
/* Muted ink alone does not recede on paper-light (--text-muted and
   --tag-text-caution share a lightness there), so done also fades whole —
   less than missed, which strikes as well. */
.track-node[data-status="completed"] {
  opacity: 0.7;
}
.track-node[data-status="missed"] .track-mark {
  color: var(--accent-danger-text);
}
/* Status badge — the ✓/✗ stamp on the mark's free corner. Role badges own
   bottom-right (icons.css); status takes top-right with the same knocked-out
   disc, so a done milestone wears both. */
.track-node[data-status="completed"] .track-mark::after,
.track-node[data-status="missed"] .track-mark::after {
  content: "\2713";
  position: absolute;
  top: -10%;
  right: -10%;
  width: 60%;
  height: 60%;
  border-radius: 50%;
  background: var(--icon-knockout);
  color: var(--accent-success-text);
  font-size: calc(var(--track-mark-size) * 0.45);
  font-weight: var(--weight-bold);
  display: grid;
  place-items: center;
}
.track-node[data-status="missed"] .track-mark::after {
  content: "\2715";
  color: var(--accent-danger-text);
}
.track-node--proposed .track-mark {
  color: var(--accent-coach-text);
}

.track-node--proposed .track-label {
  color: var(--tag-text-coach);
  font-weight: var(--weight-medium);
}

.track-node--milestone .track-label {
  color: var(--accent-strength-text);
}

.track-node--goal .track-label {
  color: var(--tag-text-caution);
  font-weight: var(--weight-medium);
  max-width: 10rem;
}

/* Older/Add nodes lack .track-date — pad top to align the slot with the line */
.track-node--older,
.track-node--add {
  padding-top: calc(0.7rem + 3px);
}

/* Add goal — an empty dashed slot: the affordance has no workout, so there is
   no modality to draw. */
.track-node--add .track-dot {
  border: 2px dashed color-mix(in srgb, var(--node-accent) 30%, transparent);
  border-radius: 50%;
}
.track-node--add:hover .track-dot {
  border-color: color-mix(in srgb, var(--node-accent) 50%, transparent);
}
.track-node--add:hover .track-label {
  color: var(--node-accent);
}

/* Active node (detail open) — uses type's own accent color */
.track-node--active .track-label {
  color: var(--node-accent);
  font-weight: var(--weight-bold);
}

/* Timeline group — keeps strip + detail card as one unit inside .message-stream */
.timeline-group {
  display: flex;
  flex-direction: column;
}

.timeline-detail:empty {
  display: none;
}

/* HTMX loading pulse on the clicked node — marks on workout nodes, the plain
   slot on the "+N older" / "+ add" buttons, which are htmx too. */
.track-node.htmx-request .track-mark,
.track-node.htmx-request .track-dot {
  animation: track-loading-pulse 0.8s ease-in-out infinite alternate;
}

@keyframes track-loading-pulse {
  from {
    opacity: 1;
  }
  to {
    opacity: 0.4;
  }
}

/* ─── Missed / Stale node types ─── */

.track-node[data-status="missed"] {
  --node-accent: var(--accent-danger);
  opacity: 0.55;
}

/* Missed recedes whole: struck label, node at 0.55 (rail-lit exemption in
   journey-stage.css keeps the fade live on the coach rail). */
.track-node[data-status="missed"] .track-label {
  color: var(--accent-danger-text);
  text-decoration: line-through;
}

/* ─── Today marker — non-interactive position anchor ─── */

.track-node--today-only {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  pointer-events: none;
  opacity: 0.7;
}

.track-node--today-only .track-date {
  color: var(--accent-coach-text);
  font-size: var(--text-2xs);
  line-height: 1;
  white-space: nowrap;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

/* A soft coral blob with a halo, centred in the shared slot, so the today
   anchor's connector meets the line at the same height as every mark. */
.track-node--today-only .track-dot::before {
  content: "";
  width: 12px;
  height: 12px;
  border-radius: var(--radius-node);
  background: var(--accent-coach);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--ember-coral) 25%, transparent);
}

@media (prefers-reduced-motion: reduce) {
  .track-node {
    transition: none;
  }

  .track-mark {
    transition: none;
  }

  .track-node.htmx-request .track-mark,
  .track-node.htmx-request .track-dot {
    animation: none;
    opacity: 0.5;
  }
}

/* =============================================================================
   PERMANENT COACH RAIL — a vertical column beside the conversation on desktop,
   a sticky horizontal strip above it on mobile. The aside's own box (placement,
   stickiness) belongs to coach-page.css; everything here is the INTERNALS.
   ============================================================================= */

.coach-rail .chat-timeline-msg {
  padding: 0;
}

.coach-rail .progress-track {
  padding: 0;
}

/* ─── Rail chrome — the heading (hidden on mobile, below) ─── */

.rail-label {
  font-family: var(--font-mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.14em;
  color: var(--text-faint);
  margin-bottom: 16px;
}

/* ─── Node colours ─── */

/* "+ Set milestones" is the one milestone node that is also an affordance:
   --paper-link is the paper's hover ink, and the dashed slot's border reads
   the same token, so the whole node lights up on hover. */
.track-node--milestone.track-node--add {
  --node-accent: var(--paper-link);
}

/* The guard keeps the hover state reachable: "+ Set milestones" carries BOTH
   --milestone and --add, and this rule ties with .track-node--add:hover
   .track-label while sitting later in the file. :where() so the guard adds no
   specificity — at (0,5,0) it would start outranking the --active rule below. */
.track-node.track-node--milestone:not(:where(.track-node--add:hover)) .track-label {
  color: var(--text-muted);
}

@media (prefers-color-scheme: light) {
  /* Light pins the accent primitives bright (tokens.css), so mark ink
     takes the tag-text twins — the palette's ink-on-tint family. Dark keeps
     the primitives' chroma. */
  .track-node[data-status="completed"] .track-mark::after {
    color: var(--tag-text-success);
  }
  .track-node[data-status="missed"] .track-mark::after {
    color: var(--tag-text-danger);
  }
  .track-node--proposed .track-mark,
  .track-node--stale .track-mark {
    color: var(--tag-text-coach);
  }
  .journey-stage .track-node[data-status="missed"] .track-mark {
    color: var(--tag-text-danger);
  }
  .journey-stage .track-node[data-status="missed"] .track-label {
    color: var(--tag-text-danger);
  }
  .track-node--milestone .track-mark {
    color: var(--tag-text-strength);
  }
  .track-node--goal .track-mark {
    color: var(--tag-text-caution);
  }
  .track-node--add {
    --node-accent: var(--tag-text-caution);
  }
  /* The open-node emphasis reads --node-accent (.track-node--active .track-label),
     which would resolve to the bright primitives light pins — re-point the
     var itself so the emphasis keeps the ink family. Marks are unaffected (the
     explicit rules above win); milestone+add keeps --paper-link and a missed
     node its status accent (both outrank these). */
  .track-node--completed {
    --node-accent: var(--tag-text-success);
  }
  .track-node--proposed,
  .track-node--stale {
    --node-accent: var(--tag-text-coach);
  }
  .track-node--milestone {
    --node-accent: var(--tag-text-strength);
  }
  .track-node--goal {
    --node-accent: var(--tag-text-caution);
  }
}

/* Restated because the label colours above, and the stub's in
   journey-stage.css, outrank the shared .track-node--active pair — without
   this the open node loses its lit state. */
.coach-rail .track-node.track-node--active .track-label {
  color: var(--node-accent);
  font-weight: var(--weight-bold);
}

/* The bar is a link to /stats: an unreset <a> is inline (so its border and
   padding would not lay out) and accent-coloured by base.css. */
.rail-week {
  display: block;
  color: inherit;
  text-decoration: none;
  border-top: 1.5px dashed var(--paper-rule);
  padding-top: 14px;
  margin-top: 4px;
}

.rail-week:hover .rail-week__val,
.rail-week:focus-visible .rail-week__val {
  color: var(--paper-link);
}

.rail-week:focus-visible {
  outline: 2px solid var(--paper-link);
  outline-offset: 3px;
}

/* ─── Desktop: the LANDING demo rail is a vertical column (SPEC §9 freezes
   that page). The ledger's column is its own block further down. ─── */

@media (min-width: 760.02px) {
  /* A column has no horizontal overflow to cue, and the markup's `.edge-fade`
     starts its ramp at x=0 — where the mark column itself sits, with the rail's
     padding zeroed. The mobile strip below keeps the fade. */
  .coach-landing .landing-rail .progress-track {
    mask-image: none;
    -webkit-mask-image: none;
  }

  /* The row carries `.scroll-row .edge-fade` for the PHONE layout (where the
     nodes are the only thing that scrolls). A column has neither overflow to
     cue nor a scroller to be — null both, exactly as coach-page.css nulls the
     phantom scroller on `.progress-track`. */
  .coach-landing .landing-rail .track-nodes {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    min-width: 0;
    padding: 0;
    justify-content: flex-start;
    overflow: visible;
    mask-image: none;
    -webkit-mask-image: none;
    /* …and `.scroll-row`'s grab cursor with them: a column drags nowhere. */
    cursor: auto;
  }

  /* Node becomes a row. The partial stacks date, mark, label — explicit grid
     placement puts the mark first visually without touching the markup: it
     holds column 1 across both text rows, date over label in column 2. */
  .coach-landing .landing-rail .track-node {
    display: grid;
    grid-template-columns: var(--track-mark-size) 1fr;
    column-gap: 14px;
    align-items: start;
    text-align: left;
    padding-bottom: 16px;
  }

  /* The dateless nodes' top pad aligned their slot with the horizontal line. */
  .coach-landing .landing-rail .track-node--older,
  .coach-landing .landing-rail .track-node--add {
    padding-top: 0;
  }

  .coach-landing .landing-rail .track-mark,
  .coach-landing .landing-rail .track-dot {
    grid-column: 1;
    grid-row: 1 / span 2;
  }

  .coach-landing .landing-rail .track-date {
    grid-column: 2;
    grid-row: 1;
    font-size: 10.5px;
    letter-spacing: 0.08em;
  }

  .coach-landing .landing-rail .track-label {
    grid-column: 2;
    grid-row: 2;
    text-align: left;
    font-size: 14.5px;
    font-weight: 600;
  }

  /* "+N older" and "+ add" carry no date — their label takes both rows so it
     sits against the mark instead of dropping below an empty first row. */
  .coach-landing .landing-rail .track-node:not(:has(.track-date)) .track-label {
    grid-row: 1 / span 2;
  }

  /* The shared connector is two horizontal background-gradient halves. Kill
     both, then redraw ::before as a dashed spine down the mark column —
     resetting every geometry property it inherits, background included. */
  .coach-landing .landing-rail .track-node::before,
  .coach-landing .landing-rail .track-node::after {
    display: none;
  }

  .coach-landing .landing-rail .track-node:not(:last-child)::before {
    display: block;
    left: 9px;
    right: auto;
    top: 24px;
    bottom: 2px;
    width: 0;
    height: auto;
    border-left: 1.5px dashed var(--paper-rule);
    background: none;
  }

  /* A column has nothing to anchor sideways to, so the card spans it and
     positionArrow's --detail-left is overridden rather than consumed. */
  .coach-landing .landing-rail .timeline-detail {
    margin-left: 0;
    width: auto;
  }

  /* Opened IN PLACE: streaming.js moves the card to sit directly after its own
     node, so it is a child of the nodes column and takes a node's rhythm rather
     than a trailing card's. */
  .coach-landing .landing-rail .track-nodes > .timeline-detail {
    margin-block: var(--space-sm);
    width: auto;
  }

}

/* ─── Week bar (markup rides inside the strip wrapper) ───
   Outside the desktop block on purpose: /coach hides the whole bar in its
   one-column shell anyway, and the landing demo rail re-shows it there — the
   paint has one home either way. */

.rail-week__head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}

.rail-week__label {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  color: var(--text-muted);
}

.rail-week__val {
  color: var(--text);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
}

.rail-week__bar {
  height: 9px;
  border-radius: 3px;
  background: var(--paper-track);
  margin-top: 8px;
  overflow: hidden;
  transform: rotate(-0.4deg);
}

/* ONE home for the coral hatch: the landing demo's week bar (below) and the
   ledger's today column both paint it. */
:root {
  --rail-hatch: repeating-linear-gradient(
    -55deg,
    var(--ember-coral) 0 5px,
    var(--paper-coral-deep) 5px 10px
  );
}

.rail-week__fill {
  height: 100%;
  background: var(--rail-hatch);
}

/* ─── Mobile: the LANDING demo strip stays horizontal, stripped to the plan
   itself. `.track-node` below is the ledger strip's too. ─── */

@media (max-width: 760px) {
  .coach-landing .landing-rail .rail-label,
  .coach-landing .landing-rail .rail-week,
  .coach-landing .landing-rail .site-footer {
    display: none;
  }

  /* The strip's closing rule belongs to the STRIP, not to the nodes row: the
     detail card is a later sibling inside .progress-track, so a rule on the row
     draws between the nodes and the card an open node produces — reading as a
     card dumped below the strip rather than opened inside it. */
  .coach-landing .landing-rail .track-nodes {
    padding-bottom: var(--space-sm);
  }

  .coach-landing .landing-rail {
    border-bottom: 1.5px dashed var(--paper-rule);
  }

  /* Only when a node is open: closed, the nodes row's own padding already
     spaces the rule, and an unconditional inset would loosen the resting band. */
  .coach-landing .landing-rail:has(.timeline-detail:not(:empty)) {
    padding-bottom: var(--space-sm);
  }

  /* Three stacked rows (when · mark · label) in a strip pinned directly under
     the header rule: at the base 3px gap the label crowds the mark and the
     whole band reads squeezed. */
  .track-node {
    min-height: 44px;
    gap: 6px;
  }

  .coach-landing .landing-rail .track-label {
    line-height: 1.35;
  }

  /* The card opens inside the sticky strip — cap it or it swallows the page. */
  .coach-landing .landing-rail .timeline-detail {
    max-height: 50vh;
    overflow-y: auto;
  }
}

/* =============================================================================
   LEDGER RAIL (SPEC §2.4, §3) — the /coach rail's internals: Steev header,
   [ THE PLAN ] list, the 7-day grid, the goal forecast. The site
   footer that closes the column is chat_area.html's, styled in the desktop
   block below.

   Layout keys on `body.coach-ledger .coach-rail--ledger`, which also outranks
   the shared rail rules above without touching them. Rules for elements only
   the ledger renders (.rail-steev*, .rail-forecast*, .rail-meter*) are
   unscoped — adding one of those classes to the landing demo would import
   its box.
   ============================================================================= */

/* ─── Steev header — THE face on this page (nav.html drops its brand blob) ─── */

.rail-steev {
  display: flex;
  align-items: center;
  border-bottom: 1.5px dashed var(--paper-rule);
}

.rail-steev__text {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

/* The disc: the blob radius and coral ground the nav brand wears, one size up.
   The face's own ink + mouth shade are steev-face.css's, beside the nav's. */
.rail-steev__blob {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  border-radius: var(--radius-blob);
  background: var(--accent-coach-soft);
  box-shadow: var(--shadow-hard-md);
  animation: rail-breathe 5s ease-in-out infinite;
}

@keyframes rail-breathe {
  0%,
  100% {
    transform: scale(1);
  }
  50% {
    transform: scale(1.03);
  }
}

.rail-steev__name {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: lowercase;
  color: var(--text);
  line-height: 1.1;
}

/* Rail chrome, not coach voice: sage, never --accent-info. */
.rail-steev__mood {
  font-family: var(--font-annotation);
  color: var(--accent-user-hover);
  display: inline-block;
  transform: rotate(-1.5deg);
  transform-origin: left center;
}

.rail-steev__countdown {
  font-family: var(--font-mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* ─── The plan head: the label and its week annotation on one baseline ─── */

body.coach-ledger .coach-rail--ledger .rail-plan-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  padding: 22px 26px 14px;
}

/* Ink, not faint: on the ledger the plan is the star, not a caption. */
body.coach-ledger .coach-rail--ledger .rail-label {
  color: var(--text);
  margin-bottom: 0;
}

body.coach-ledger .coach-rail--ledger .rail-plan-head__week {
  font-family: var(--font-annotation);
  font-size: 16px;
  color: var(--text-muted);
}

/* ─── Desktop: the plan list and the week grid ─── */

@media (min-width: 1000.02px) {
  body.coach-ledger .coach-rail--ledger .rail-steev {
    gap: 16px;
    padding: 26px 26px 20px;
  }

  body.coach-ledger .coach-rail--ledger .rail-steev__blob {
    width: 64px;
    height: 64px;
  }

  body.coach-ledger .coach-rail--ledger .rail-steev__name {
    font-size: 22px;
  }

  body.coach-ledger .coach-rail--ledger .rail-steev__mood {
    font-size: 17px;
  }

  body.coach-ledger .coach-rail--ledger .rail-steev__countdown {
    font-size: 10.5px;
  }

  /* ─── The column itself: the ledger's own copy of the shape the landing's
     block above draws for the demo rail, with the ledger's measures. A column
     has no horizontal overflow to cue and no scroller to be, so the row's
     `.scroll-row .edge-fade` (the one-column strip's) is nulled here. ─── */
  body.coach-ledger .coach-rail--ledger .progress-track {
    /* One knob for the mark, the dot and the status badge alike. */
    --track-mark-size: 22px;
    mask-image: none;
    -webkit-mask-image: none;
  }

  /* Each block owns the rail's 26px inset, so the nodes row does too. */
  body.coach-ledger .coach-rail--ledger .track-nodes {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    min-width: 0;
    padding: 0 26px;
    justify-content: flex-start;
    overflow: visible;
    mask-image: none;
    -webkit-mask-image: none;
    cursor: auto;
  }

  /* Node becomes a row: mark · (date over title) · meta. Explicit grid
     placement puts the mark first without touching the markup; the meta rides
     the title's row so a two-line title carries it down. */
  body.coach-ledger .coach-rail--ledger .track-node {
    display: grid;
    grid-template-columns: 24px 1fr auto;
    column-gap: 14px;
    align-items: start;
    text-align: left;
    padding-bottom: 18px;
    /* The hover nudge below is 150ms — SPEC §7. Without this it rides the
       reveal cascade's 0.46s transform (journey-stage.css), slow enough that
       the pointer leaves before it settles; opacity keeps the cascade's timing.
       Longhands, not the shorthand: the shorthand would also reset the
       cascade's `transition-delay` and kill the reveal stagger. */
    transition-property: opacity, transform;
    transition-duration: 0.46s, 150ms;
    transition-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  /* The dateless nodes' top pad aligned their slot with the horizontal line. */
  body.coach-ledger .coach-rail--ledger .track-node--older,
  body.coach-ledger .coach-rail--ledger .track-node--add {
    padding-top: 0;
  }

  body.coach-ledger .coach-rail--ledger .track-mark,
  body.coach-ledger .coach-rail--ledger .track-dot {
    grid-column: 1;
    grid-row: 1 / span 2;
  }

  body.coach-ledger .coach-rail--ledger .track-date {
    grid-column: 2;
    grid-row: 1;
    font-size: 10.5px;
    letter-spacing: 0.1em;
  }

  body.coach-ledger .coach-rail--ledger .track-label {
    grid-column: 2;
    grid-row: 2;
    text-align: left;
    font-family: var(--font-display);
    font-size: 17px;
    font-weight: 700;
    letter-spacing: -0.01em;
    line-height: 1.15;
  }

  /* "+N older" and "+ add" carry no date — their label takes both rows so it
     sits against the mark instead of dropping below an empty first row. */
  body.coach-ledger .coach-rail--ledger .track-node:not(:has(.track-date)) .track-label {
    grid-row: 1 / span 2;
  }

  /* The shared connector is two horizontal background-gradient halves. Kill
     both, then redraw ::before as a dashed spine down the mark column —
     resetting every geometry property it inherits, background included. */
  body.coach-ledger .coach-rail--ledger .track-node::before,
  body.coach-ledger .coach-rail--ledger .track-node::after {
    display: none;
  }

  body.coach-ledger .coach-rail--ledger .track-node:not(:last-child)::before {
    display: block;
    left: 11px;
    right: auto;
    top: 26px;
    bottom: 2px;
    width: 0;
    height: auto;
    border-left: 1.5px dashed var(--paper-rule);
    background: none;
  }

  /* A column has nothing to anchor sideways to, so the card spans it and
     positionArrow's --detail-left is overridden rather than consumed. Opened
     IN PLACE: streaming.js moves the card to sit directly after its own node,
     so it takes a node's rhythm rather than a trailing card's. */
  body.coach-ledger .coach-rail--ledger .timeline-detail {
    margin-left: 0;
    width: auto;
  }

  /* The stream's 88% (chat-cards.css) is voice positioning; here it only
     strands the card short of the column's right edge. */
  body.coach-ledger .coach-rail--ledger .timeline-detail > .chat-card {
    max-width: none;
  }

  body.coach-ledger .coach-rail--ledger .track-nodes > .timeline-detail {
    margin-block: var(--space-sm);
    width: auto;
  }

  body.coach-ledger .coach-rail--ledger .track-meta {
    grid-column: 3;
    grid-row: 2;
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--text-faint);
    font-variant-numeric: tabular-nums;
    text-align: right;
  }

  /* The anchor grows with the mark column it shares. */
  body.coach-ledger
    .coach-rail--ledger
    .track-node--today-only
    .track-dot::before {
    width: 14px;
    height: 14px;
    box-shadow: 0 0 0 5px color-mix(in srgb, var(--ember-coral) 25%, transparent);
  }

  /* Sideways, not up: a column of rows reads as a list nudged, not lifted. */
  body.coach-ledger .coach-rail--ledger .track-node:hover {
    transform: translateX(2px);
  }

  /* ─── This week — seven cells, today hatched ─── */

  body.coach-ledger .coach-rail--ledger .rail-week {
    border-top: 1.5px dashed var(--paper-rule);
    padding: 18px 26px 0;
    margin: 0;
  }

  body.coach-ledger .coach-rail--ledger .rail-week__label {
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-faint);
  }

  body.coach-ledger .coach-rail--ledger .rail-week__grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 6px;
    margin-top: 10px;
  }

  body.coach-ledger .coach-rail--ledger .rail-week__cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
  }

  body.coach-ledger .coach-rail--ledger .rail-week__letter {
    font-family: var(--font-mono);
    font-size: 10px;
    color: var(--text-faint);
  }

  body.coach-ledger .coach-rail--ledger .rail-week__box {
    position: relative;
    width: 100%;
    height: 54px;
    border-radius: 3px;
    background: var(--paper-track);
    padding: 3px;
    display: flex;
    align-items: flex-end;
  }

  /* Same class, different job: on the landing .rail-week__bar IS the track;
     here it is the column inside a cell and its height is the datum (inline).
     Everything the landing rule paints has to be undone, hence the resets. */
  body.coach-ledger .coach-rail--ledger .rail-week__bar {
    display: block;
    width: 100%;
    margin: 0;
    overflow: visible;
    background: var(--ember-olive);
    border-radius: 2px;
    transform: none;
    transform-origin: bottom;
    animation: rail-bar-in 500ms cubic-bezier(0.16, 1, 0.3, 1) both;
  }

  /* The week draws itself left to right, one day behind the next. */
  body.coach-ledger
    .coach-rail--ledger
    .rail-week__cell:nth-child(2)
    .rail-week__bar {
    animation-delay: 30ms;
  }
  body.coach-ledger
    .coach-rail--ledger
    .rail-week__cell:nth-child(3)
    .rail-week__bar {
    animation-delay: 60ms;
  }
  body.coach-ledger
    .coach-rail--ledger
    .rail-week__cell:nth-child(4)
    .rail-week__bar {
    animation-delay: 90ms;
  }
  body.coach-ledger
    .coach-rail--ledger
    .rail-week__cell:nth-child(5)
    .rail-week__bar {
    animation-delay: 120ms;
  }
  body.coach-ledger
    .coach-rail--ledger
    .rail-week__cell:nth-child(6)
    .rail-week__bar {
    animation-delay: 150ms;
  }
  body.coach-ledger
    .coach-rail--ledger
    .rail-week__cell:nth-child(7)
    .rail-week__bar {
    animation-delay: 180ms;
  }

  /* An inset shadow, never a border: `box-sizing: border-box` is global, so a
     1.5px border would shrink this cell's content box and draw today's bar
     ~6% short of an identical day's beside it. */
  body.coach-ledger .coach-rail--ledger .rail-week__cell--today .rail-week__box {
    background: var(--surface-raised);
    box-shadow: inset 0 0 0 1.5px var(--ember-coral);
  }

  body.coach-ledger .coach-rail--ledger .rail-week__cell--today .rail-week__bar {
    background: var(--rail-hatch);
  }

  body.coach-ledger .coach-rail--ledger .rail-week__num {
    font-family: var(--font-mono);
    font-size: 10.5px;
    font-weight: 600;
    color: var(--text-secondary);
  }
}

@keyframes rail-bar-in {
  from {
    transform: scaleY(0);
  }
}

/* ─── Rail meter — the forecast's track, and the ONE thin track the rail has
   (owner, 2026-09-05: the week block keeps its grid alone, no meter beside it).
   The iOS plan panel's track (SPEC §6.1) on web: a full-width capsule, the
   groove in `--text-faint`, the coral fill running to where `--rail-meter`
   (0–1) puts the runner. Nothing marks the start and nothing sits on the
   axis — the labels around it say what each end means, and their colour says
   which part of the bar each one names. */

.rail-meter,
.coach-band__track {
  height: 4px;
  border-radius: var(--radius-full);
  background: var(--text-faint);
}

.rail-meter__fill,
.coach-band__fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: var(--accent-coach);
}

/* One shape, two 0-1 sources: the rail's is `--rail-meter`, the header band's
   is the seam's own `--coach-band-progress` (journey_strip.html). */
.rail-meter__fill {
  width: calc(var(--rail-meter) * 100%);
}

.coach-band__fill {
  width: calc(var(--coach-band-progress) * 100%);
}

/* ─── Goal forecast — the rail's ONE stat (SPEC §3) ───
   A forecast, not a timeline (Amine, 2026-09-05): current against target, and
   nothing on the axis between them. The goal over the track's end in the
   groove's tone; `CURRENT FORECAST 2:02` under its start in the fill's coral —
   the word makes it a caption, so it never reads as the start's value. */

.rail-forecast {
  display: flex;
  flex-direction: column;
  gap: 4px;
  border-top: 1.5px dashed var(--paper-rule);
}

.rail-forecast__goal,
.rail-forecast__current,
.coach-band__target,
.coach-band__current {
  font-family: var(--font-mono);
  font-size: 10px;
  text-transform: uppercase;
  white-space: nowrap;
}

.rail-forecast__goal,
.coach-band__target {
  letter-spacing: 0.1em;
  color: var(--text-faint);
}

/* The rail stacks its three lines; only there does the target hang off the
   track's end. The band's row seats all three on one baseline. */
.rail-forecast__goal {
  align-self: flex-end;
}

.rail-forecast__current,
.coach-band__current {
  font-weight: 600;
  letter-spacing: 0.05em;
  color: var(--accent-coach-text);
}

@media (min-width: 1000.02px) {
  body.coach-ledger .coach-rail--ledger .rail-forecast {
    padding: 22px 26px 0;
    margin-top: 18px;
  }

  /* ─── The site footer, in the quote's place (owner, 2026-09-05) ───
     The same full pause after the last stat the quote kept; left-set like
     every other block in the column, on the rail's own dashed rule rather
     than the page footer's solid one. Paint only: the footer's own rules in
     base.css still size and dim its lines. */
  body.coach-ledger .coach-rail--ledger .site-footer {
    margin-top: 40px;
    padding: 22px 26px 26px;
    border-top: 1.5px dashed var(--paper-rule);
  }

  body.coach-ledger .coach-rail--ledger .footer-content {
    text-align: left;
    gap: 14px;
  }

  body.coach-ledger .coach-rail--ledger .footer-quote {
    margin: 0;
    font-size: 13px;
    line-height: 1.55;
    color: var(--text-faint);
  }

  body.coach-ledger .coach-rail--ledger .footer-links {
    justify-content: flex-start;
  }

  /* A column, not a page foot: one step down so the links and the notice sit
     in the rail's caption register rather than beside the node labels. */
  body.coach-ledger .coach-rail--ledger .footer-links a,
  body.coach-ledger .coach-rail--ledger .footer-sep,
  body.coach-ledger .coach-rail--ledger .footer-copy {
    font-size: var(--text-xs);
  }
}

/* ─── Wide but short (a laptop window with DevTools docked, 1280x540): still
   two panes — the rail's header takes the one-column band's measures so the
   plan starts inside the first screen instead of under a 110px masthead. ─── */
@media (min-width: 1000.02px) and (max-height: 560px) {
  body.coach-ledger .coach-rail--ledger .rail-steev {
    gap: 12px;
    padding: 10px 26px;
  }

  body.coach-ledger .coach-rail--ledger .rail-steev__blob {
    width: 34px;
    height: 34px;
  }

  body.coach-ledger .coach-rail--ledger .rail-steev__name {
    font-size: 16px;
  }

  body.coach-ledger .coach-rail--ledger .rail-steev__mood {
    font-size: 14px;
  }

  body.coach-ledger .coach-rail--ledger .rail-plan-head {
    padding: 12px 26px 8px;
  }

  body.coach-ledger .coach-rail--ledger .track-node {
    padding-bottom: 12px;
  }
}

/* ─── One column: the zone rides in the CONVERSATION, and it is nodes only ───
   Steev's line and the goal track moved to the header band (coach-page.css) —
   what is left is the horizontal scroller of TILES, the finger-friendly card
   the iOS plan panel wore, and a node's card opens as a full-screen sheet over
   it. ─── */

@media (max-width: 1000px) {
  /* Nodes only in the stream (owner): the face, mood, countdown, plan head, week
     grid and forecast are the rail's. Same markup, hidden here. */
  body.coach-ledger .chat-journey-zone .rail-steev,
  body.coach-ledger .chat-journey-zone .rail-plan-head,
  body.coach-ledger .chat-journey-zone .rail-week,
  body.coach-ledger .chat-journey-zone .rail-forecast {
    display: none;
  }

  /* The page's own bleed (the band the sticky rail used). */
  body.coach-ledger .chat-journey-zone .journey-stage {
    padding-block: var(--space-sm);
    margin-inline: calc(-1 * (var(--inset-lg) + var(--ledger-gutter) + var(--ledger-gap)))
                   calc(-1 * var(--inset-lg));
    padding-inline: var(--inset-lg);
  }

  /* The fade is the node row's. Left on the whole box it washed 20px off the
     strip's own edges.

     `overflow: visible` nulls the PHANTOM scroller. `.scroll-row` rides BOTH
     this box and the nodes row inside it, and `overflow-y: visible` beside its
     `overflow-x: auto` computes to auto on both axes — two nested horizontal
     scrollers, one of which the wheel could reach and could not move. WebKit
     happened to latch the other one; Gecko did not, and the strip would not
     scroll at all (owner, on Zen). The row below is the one that scrolls now,
     which is the box `stripScroller()` and `centreOnToday()` already address.
     The same null is what the two-pane rail and the landing do (coach-page.css). */
  body.coach-ledger .chat-journey-zone .progress-track {
    --track-mark-size: 16px;
    overflow: visible;
    mask-image: none;
    -webkit-mask-image: none;
  }

  /* The base row is `min-width: max-content` — it grows to its content and so
     has no overflow of its own to scroll. Zeroing the min is what makes it THE
     scroller instead of the box around it. */
  body.coach-ledger .chat-journey-zone .track-nodes {
    min-width: 0;
  }

  body.coach-ledger .chat-journey-zone .track-nodes > * {
    flex: 0 0 auto;
  }

  /* ─── Tiles, not dots (owner, 2026-09-19) ───
     The finger-friendly card the iOS plan panel wore. The node markup is shared
     with the frozen landing demo (SPEC §9), so the whole shape is CSS scoped to
     this zone: icon beside the date caption, the title under them, the distance
     at the foot. Tiles do not sit on a track, so the connectors go. */
  body.coach-ledger .chat-journey-zone .track-node {
    display: grid;
    grid-template-columns: auto 1fr;
    align-content: start;
    gap: 3px 6px;
    width: 104px;
    min-height: 44px;
    padding: 7px 12px;
    text-align: left;
    border: 1.5px dashed var(--paper-rule);
    border-radius: 4px;
  }

  body.coach-ledger .chat-journey-zone .track-node::before,
  body.coach-ledger .chat-journey-zone .track-node::after {
    display: none;
  }

  body.coach-ledger .chat-journey-zone .track-mark,
  body.coach-ledger .chat-journey-zone .track-dot {
    grid-column: 1;
    grid-row: 1;
  }

  /* The widest caption ("Yesterday") only just clears the column left beside
     the icon, so it clips rather than spilling over the tile's own rule. */
  body.coach-ledger .chat-journey-zone .track-date {
    grid-column: 2;
    grid-row: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 9.5px;
    letter-spacing: 0.1em;
  }

  body.coach-ledger .chat-journey-zone .track-label {
    grid-column: 1 / -1;
    text-align: left;
    font-family: var(--font-display);
    font-size: 13px;
    font-weight: 700;
    line-height: 1.2;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  /* Today is the one you are standing on: wide, raised, coral-ruled, and sitting
     a touch off-square. The tilt is placement, not motion — it stays under
     Reduce Motion, the policy coach-page.css states once. */
  body.coach-ledger .chat-journey-zone .track-node--today {
    width: 170px;
    background: var(--surface-raised);
    border: 1.5px solid var(--accent-coach);
    box-shadow: var(--shadow-sm);
    transform: rotate(-0.4deg);
  }

  body.coach-ledger .chat-journey-zone .track-node--today .track-date {
    color: var(--accent-coach-text);
  }

  body.coach-ledger .chat-journey-zone .track-node--today .track-label {
    font-size: 15px;
    -webkit-line-clamp: 3;
  }

  /* The distance the tile wants — the flat dot strip had no room for it. */
  body.coach-ledger .chat-journey-zone .track-meta {
    grid-column: 1 / -1;
    font-family: var(--font-mono);
    font-size: 9.5px;
    color: var(--text-faint);
    font-variant-numeric: tabular-nums;
  }

  /* "+N older" and "+ add" carry no date, so their label takes the first row
     beside the slot instead of dropping below an empty one. */
  body.coach-ledger .chat-journey-zone .track-node:not(:has(.track-date)) {
    align-content: center;
  }

  body.coach-ledger .chat-journey-zone .track-node:not(:has(.track-date)) .track-label {
    grid-column: 2;
    grid-row: 1;
  }

  /* The collapse has no workout behind it — the count IS the tile, and the
     empty slot would only indent it. */
  body.coach-ledger .chat-journey-zone .track-node--older .track-dot {
    display: none;
  }

  /* The today anchor is the one node with nothing behind it (journey.rs inserts
     it only where the runner has nothing today), so it stays the date over its
     blob rather than an empty tile — iOS draws the same exception. */
  body.coach-ledger .chat-journey-zone .track-node--today-only {
    display: flex;
    width: auto;
    padding: 7px var(--space-sm);
    border: none;
  }

  body.coach-ledger .chat-journey-zone .track-node--today-only .track-dot::before {
    width: 8px;
    height: 8px;
    box-shadow: 0 0 0 4px color-mix(in srgb, var(--ember-coral) 25%, transparent);
  }

  /* One column: a node's card is a full-screen sheet, never a card parked under
     the strip (owner, 2026-09-19). The card's own close button is the way out;
     Escape too (streaming.js). Body scroll is not locked (Gecko body-overflow trap,
     docs/coach-small-screens/): the sheet covers the viewport and contains its own
     overscroll, which is enough. */
  body.coach-ledger .chat-journey-zone .timeline-detail:not(:empty) {
    position: fixed;
    inset: 0;
    z-index: var(--z-sheet);
    margin: 0;
    width: auto;
    background: var(--paper-panel);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: calc(var(--space-lg) + env(safe-area-inset-top))
             calc(var(--inset-lg) + env(safe-area-inset-right))
             calc(var(--space-lg) + env(safe-area-inset-bottom))
             calc(var(--inset-lg) + env(safe-area-inset-left));
  }

  body.coach-ledger .chat-journey-zone .timeline-detail:not(:empty)::before {
    display: none;                                   /* the pointer has no node to point at */
  }

  body.coach-ledger .chat-journey-zone .timeline-detail > .chat-card {
    max-height: none;                                /* the sheet scrolls, the card does not */
    overflow: visible;
  }

  body.coach-ledger .chat-journey-zone .timeline-detail > .chat-card > .detail-close {
    position: fixed;                                 /* pinned to the sheet's top-right */
    top: calc(var(--space-md) + env(safe-area-inset-top));
    right: calc(var(--inset-lg) + env(safe-area-inset-right));
  }
}

/* One paper policy (coach-page.css): an interaction transform reverts to the
   element's resting one, and idle/entrance animation stops. */
@media (prefers-reduced-motion: reduce) {
  .rail-steev__blob,
  body.coach-ledger .coach-rail--ledger .rail-week__bar {
    animation: none;
  }

  body.coach-ledger .coach-rail--ledger .track-node:hover {
    transform: none;
  }
}


/* =============================================================================
   STEEV-FACE.CSS - The canonical face system

   This file owns the complete visual state machine for the Steev face:
   - Base box (size, color, SVG element transform-box setup)
   - Idle choreography (breathe + 20s brow/eye/mustache cycle)
   - Sleeping pose (data-state="sleeping") + waking transition (data-state="waking")
   - Writing / Celebrating transitions (data-state="writing|celebrating")
   - Mood system (neutral, proud, concerned, curious, amused, tired)
   - Outbursts (laugh, gasp) opening the mouth
   - Face-scoped reduced-motion overrides

   Identity envelope: mustache + brows never disappear. Amplitudes bounded
   (brow rotate ≤9°, scale-y ≥0.5, mustache rotate ≤1°, whole-face gaze turn ≤2°).
   ============================================================================= */

/* ─── Base box ─── */

.steev-face {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  color: var(--accent-info-text);

  /* Mouth shade var — the OPEN mouth: deeper ember, reads as "aperture" not
     "ink mark". The resting mouth is face ink; only outbursts use this.
     Locally scoped so the face remains portable. */
  --mouth-shade: color-mix(in oklab, var(--accent-info) 55%, black 45%);

  /* JS-driven gaze turn: standalone rotate composes with the transform-based
     state keyframes (see the mood-system header) — zero keyframe edits. */
  rotate: var(--face-turn, 0deg);
  transition: rotate 80ms ease-out;
}

.steev-face svg {
  width: 100%;
  height: 100%;
  display: block;
  /* Celebrate/think animations translate brows up to 2px past the viewBox.
     Visible spill is desirable; clipping would lose identity (brows). */
  overflow: visible;
}

/* SVG sub-elements: fill-box so transforms are relative to each shape */
.steev-face svg * {
  transform-box: fill-box;
  transform-origin: center;
}


/* Gaze tracking — eyes follow cursor.
   The `translate` property is separate from `transform`, so it composes
   with the `scaleY` blink keyframes below without overriding them. Older
   (sleeping) faces never receive `--eye-x/y`, so the 0 fallback keeps
   them still. */
.steev-face .sv-el,
.steev-face .sv-er {
  translate: var(--eye-x, 0) var(--eye-y, 0);
  transition: translate 80ms ease-out, transform 80ms ease-out;
}

/* Mouth base state: a resting feature, drawn in the face's own ink — so it
   follows the face onto any ground (coach mauve, ink-on-coral) for free.
   Mood reshapes it through the standalone `scale`/`translate`/`rotate`
   properties (untransitioned, like every other mood pose on this face);
   `transform` and `fill` are left free for the state/outburst layers, which
   is what this transition covers. */
.steev-face .sv-mouth {
  fill: currentColor;
  transition: fill 180ms ease-out, transform 180ms ease-out;
}

/* Nose: inherit stroke color and subtle by default. */
.steev-face .sv-n {
  transition: opacity 180ms ease-out;
}

/* ─── Breathing: ambient halo pulse + 20s idle cycle ───
   Driven by data-state="breathing" set by the P06 state machine in
   steev-face.js. There is exactly one face on the page (nav-brand),
   and it spends most of its time in this state when the user is not
   actively interacting. */

.steev-face[data-state="breathing"] {
  animation: sv-breathe 4s ease-in-out infinite;
}

@keyframes sv-breathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.04); }
}

.steev-face[data-state="breathing"] .sv-bl { animation: sv-idle-bl 20s ease-in-out infinite; }
.steev-face[data-state="breathing"] .sv-br { animation: sv-idle-br 20s ease-in-out infinite; }
.steev-face[data-state="breathing"] .sv-el { animation: sv-idle-eye 20s ease-in-out infinite; }
.steev-face[data-state="breathing"] .sv-er { animation: sv-idle-eye 20s ease-in-out 0.04s infinite; }
.steev-face[data-state="breathing"] .sv-m  { animation: sv-idle-m 20s ease-in-out infinite; }

@keyframes sv-idle-bl {
  0%, 62%, 78%, 100% { transform: translateY(0) rotate(0); }
  /* Amused: left brow raises */
  66% { transform: translateY(-1.5px) rotate(-3deg); }
  74% { transform: translateY(-0.5px) rotate(-1deg); }
}

@keyframes sv-idle-br {
  0%, 26%, 40%, 100% { transform: translateY(0) rotate(0); }
  /* Curious: right brow raises */
  30% { transform: translateY(-1.5px) rotate(4deg); }
  37% { transform: translateY(-0.5px) rotate(1deg); }
}

@keyframes sv-idle-eye {
  0%, 11%, 17%, 49%, 55%, 83%, 89%, 100% { transform: scaleY(1); }
  13%, 15% { transform: scaleY(0.1); }
  51%, 53% { transform: scaleY(0.1); }
  85%, 87% { transform: scaleY(0.1); }
}

@keyframes sv-idle-m {
  0%, 62%, 78%, 100% { transform: translateY(0); }
  /* Amused: mustache shifts up slightly with left brow */
  67% { transform: translateY(-0.4px); }
  74% { transform: translateY(-0.2px); }
}

/* ─── Sleeping: 60s idle or tab hidden ───
   Driven by data-state="sleeping" set by the P06 state machine. */
.steev-face[data-state="sleeping"] {
  animation: sv-sleep 7s ease-in-out infinite;
}

/* Eyes shut while asleep (held blink) — the sleep signal, no dim. */
.steev-face[data-state="sleeping"] .sv-el,
.steev-face[data-state="sleeping"] .sv-er {
  transform: scaleY(0.1);
  transition: transform 300ms ease-out;
}

/* Mouth shuts with the eyes — a sleeping face is closed, not agape. `transform`
   rather than `scale` so it multiplies whatever mood shape is set instead of
   replacing it (the eyes above do the same). */
.steev-face[data-state="sleeping"] .sv-mouth {
  transform: scale(0.8, 0.25);
  transition: transform 300ms ease-out;
}

@keyframes sv-sleep {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.02); }
}

/* ─── Waking: transient state back to breathing (P06 wake handler) ─── */
.steev-face[data-state="waking"] {
  animation: sv-breathe 4s ease-in-out infinite;
}

/* ─── Blinking: momentary eye-close (P06 schedules every 6-10s) ─── */
.steev-face[data-state="blinking"] .sv-el,
.steev-face[data-state="blinking"] .sv-er {
  transform: scaleY(0.1);
  transition: transform 80ms ease-out;
}

/* ─── Writing: LLM is producing output (thinking + streaming combined) ───
   Driven by data-state="writing" set by the P06 state machine in response
   to steev:writing:start events from streaming.js. Folds the previous
   per-bubble "thinking" (typing-indicator visible) and "speaking" (chunks
   streaming) into one state — the runner sees "Steev is working" without
   needing substate fidelity. */
.steev-face[data-state="writing"] svg {
  animation: sv-think 2.5s ease-in-out infinite;
}
.steev-face[data-state="writing"] .sv-bl {
  animation: sv-think-brow 2.5s ease-in-out infinite;
}
.steev-face[data-state="writing"] .sv-br {
  animation: sv-think-brow 2.5s ease-in-out infinite reverse;
}
.steev-face[data-state="writing"] .sv-m {
  animation: sv-think-m 2.5s ease-in-out infinite;
}

@keyframes sv-think {
  0%, 100% { transform: rotate(0) translateY(0); }
  30%      { transform: rotate(-5deg) translateY(-1px); }
  70%      { transform: rotate(5deg) translateY(-1px); }
}

@keyframes sv-think-brow {
  0%, 100% { transform: translateY(0) rotate(0); }
  50%      { transform: translateY(0.8px) rotate(5deg); }
}

@keyframes sv-think-m {
  0%, 100% { transform: scaleY(1); }
  50%      { transform: scaleY(0.94); }
}


/* ─── Celebrating: milestone hit / goal complete ───
   Triggered by data-state="celebrating" set by the P06 state machine in
   response to steev:celebrate events from streaming.js. Auto-reverts to
   data-state="breathing" after the celebration animation completes
   (~2.4s — sv-celebrate iteration: 3). */
.steev-face[data-state="celebrating"] svg {
  animation: sv-celebrate 0.8s ease-in-out 3;
}
.steev-face[data-state="celebrating"] .sv-bl {
  animation: sv-celeb-brow 0.6s ease-in-out 4;
}
.steev-face[data-state="celebrating"] .sv-br {
  animation: sv-celeb-brow 0.6s ease-in-out 0.05s 4;
}
.steev-face[data-state="celebrating"] .sv-m {
  animation: sv-celeb-m 0.8s ease-in-out 3;
}

@keyframes sv-celebrate {
  0%, 100% { transform: scale(1) rotate(0); }
  20%      { transform: scale(1.15) rotate(-6deg); }
  50%      { transform: scale(0.97) rotate(3deg); }
  80%      { transform: scale(1.08) rotate(-1deg); }
}

@keyframes sv-celeb-brow {
  0%, 100% { transform: translateY(0) rotate(0); }
  40%      { transform: translateY(-2px) rotate(-6deg); }
}

@keyframes sv-celeb-m {
  0%, 100% { transform: translateY(0) scaleY(1); }
  35%      { transform: translateY(0.8px) scaleY(1.1); }
  70%      { transform: translateY(-0.3px) scaleY(0.95); }
}

/* =============================================================================
   MOOD SYSTEM — face mood (data-mood) + transient outbursts (data-outburst)

   How this layers with the idle system:
   - `translate:`, `rotate:`, `scale:` (separate CSS properties) compose
     with the `transform:`-based idle/sleep/think keyframes rather than
     fighting them.
   - Mood is driven by data-mood on the nav-face, set by streaming.js
     when each new exchange-group lands. The mood class on the wrapper
     drives the chat mood tint (chat-marginalia.css) and, via
     streaming.js, the nav-face data-mood.
   ============================================================================= */

/* ─── Mood baselines ───
   Defaults to neutral (mischievous asymmetric brow). Applied to the
   nav-face directly. The mood class on .exchange-group drives the chat
   mood tint (chat-marginalia.css) and, via streaming.js, the nav-face
   data-mood — which is what animates the face. */

.steev-face .sv-bl {
  translate: 0 -0.6px;
  rotate: -6deg;
}
.steev-face .sv-br {
  translate: 0 0.3px;
  rotate: 2deg;
}

/* ─── The mood mouth ───
   An ellipse's honest vocabulary is APERTURE — width, weight, tilt, height on
   the face. Steev's smile is the mustache (which already carries mood); this
   mark sits below it and says how open he is. At the 24px shipping size it is
   ~3.8×1.4 device px, so WIDTH and INK WEIGHT are the only channels that
   survive rasterization — tilt and offset are refinements that resolve at
   40pt+. Four legible levels: open (proud, amused), neutral (neutral,
   curious), thin (concerned, tired), shut (sleeping). `curious` is deliberately
   left at the base shape — its head tilt already swings the mouth with the
   whole face, and any small round mouth it could take collides with `gasp`. */

/* ─── mood-proud: brows lift, eyes widen, mustache tilts ≤1° ─── */
.steev-face[data-mood="proud"] .sv-bl { translate: 0 -1.4px; rotate: -6deg; }
.steev-face[data-mood="proud"] .sv-br { translate: 0 -0.8px; rotate: 2deg; }
.steev-face[data-mood="proud"] .sv-el,
.steev-face[data-mood="proud"] .sv-er { scale: 1.08; }
.steev-face[data-mood="proud"] .sv-m  { rotate: -0.8deg; }
/* broad and level, riding up with the brows */
.steev-face[data-mood="proud"] .sv-mouth { translate: 0 -0.15px; scale: 1.2 1; }

/* ─── mood-concerned: inner brows lift, eyes narrow, nose scrunch ─── */
.steev-face[data-mood="concerned"] .sv-bl { translate: 0 -0.2px; rotate: 4deg; }
.steev-face[data-mood="concerned"] .sv-br { translate: 0 -0.2px; rotate: -2deg; }
.steev-face[data-mood="concerned"] .sv-el,
.steev-face[data-mood="concerned"] .sv-er { scale: 0.92; }
.steev-face[data-mood="concerned"] .sv-m  { translate: 0 0.2px; }
.steev-face[data-mood="concerned"] .sv-n  { opacity: 0.85; }
/* small and pursed — the narrowest mouth in the set */
.steev-face[data-mood="concerned"] .sv-mouth { scale: 0.75 0.7; }

/* ─── mood-curious: amplified asymmetric brow + head-tilt on the face itself ─── */
.steev-face[data-mood="curious"] .sv-bl { translate: 0 -1.5px; rotate: -9deg; }
.steev-face[data-mood="curious"] .sv-br { translate: 0 0.2px; rotate: 2deg; }
.steev-face[data-mood="curious"] { rotate: calc(-1.8deg + var(--face-turn, 0deg)); }

/* ─── mood-amused: brow lift, eyes squint ─── */
.steev-face[data-mood="amused"] .sv-bl { translate: 0 -0.8px; rotate: -8deg; }
.steev-face[data-mood="amused"] .sv-br { translate: 0 0.3px; rotate: 2deg; }
.steev-face[data-mood="amused"] .sv-el,
.steev-face[data-mood="amused"] .sv-er { scale: 1 0.68; }
.steev-face[data-mood="amused"] .sv-m  { rotate: -0.5deg; }
/* widest and lopsided — the smirk. The tilt is the one thing only the mouth
   can do here; the mustache is capped at 1° by the identity envelope. */
.steev-face[data-mood="amused"] .sv-mouth { translate: 0 0.1px; rotate: -8deg; scale: 1.35 0.95; }

/* ─── mood-tired: brows settle, eyes half-closed ─── */
.steev-face[data-mood="tired"] .sv-bl { translate: 0 0.2px; rotate: -3deg; }
.steev-face[data-mood="tired"] .sv-br { translate: 0 0.4px; rotate: 1deg; }
.steev-face[data-mood="tired"] .sv-el { scale: 1 0.5; }
.steev-face[data-mood="tired"] .sv-er { scale: 1 0.55; }
/* slack: a long, thin, dropped line */
.steev-face[data-mood="tired"] .sv-mouth { translate: 0 0.2px; scale: 1.15 0.45; }

/* ─── Outbursts: the mouth OPENS ───
   data-outburst is set by streaming.js when the LLM emits one and deleted
   after 1.8s, letting the mouth ease back to its mood shape.

   An outburst OVERRIDES the mood mouth rather than multiplying it: a gasp is an
   O whatever the mood, and a `tired`-flattened O is not one. iOS's composer
   overwrites the same three channels for the same reason. `--mouth-shade` is
   what makes the reveal read now that the mouth is always present — an open
   mouth is an aperture, deeper than the ink of the lip that was there. */

.steev-face[data-outburst="laugh"] .sv-mouth,
.steev-face[data-outburst="gasp"] .sv-mouth {
  fill: var(--mouth-shade);
  translate: 0;
  rotate: 0;
  scale: 1;
}

.steev-face[data-outburst="laugh"] .sv-mouth {
  transform: scale(1.35) translateY(0.1px);
}

.steev-face[data-outburst="gasp"] .sv-mouth {
  transform: scale(0.9);
  ry: 0.9;  /* taller, smaller — O-shape */
}

/* ─── Ledger rail seat (SPEC §2.4) — THE face on /coach ───
   The disc itself is progress-track.css's; only the face's own box and ink live
   here, beside the nav disc's twin. 64 × 22/30, the disc-to-face ratio
   navigation.css uses on paper (38/29). Sized here rather than there so every
   face size in the app is readable in one file. */
.rail-steev__blob .steev-face {
  width: 47px;
  height: 47px;
  color: var(--paper-ink-on-coral);
  --mouth-shade: color-mix(in oklab, currentColor 55%, black 45%);
}

@media (min-width: 1000.02px) and (max-height: 560px) {
  .rail-steev__blob .steev-face {
    width: 25px;
    height: 25px;
  }
}

/* ─── Reduced motion: static poses, no transitions ─── */
@media (prefers-reduced-motion: reduce) {
  .steev-face,
  .steev-face svg,
  .steev-face svg * {
    animation: none !important;
  }

  .steev-face,
  .steev-face .sv-el,
  .steev-face .sv-er,
  .steev-face .sv-mouth,
  .steev-face .sv-n {
    transition: none;
  }

  /* The sleeping rules carry their own 300ms transition at a HIGHER
     specificity than the block above, so they escaped this gate. The poses
     themselves are placement and stay; only their easing is motion. */
  .steev-face[data-state="sleeping"] .sv-el,
  .steev-face[data-state="sleeping"] .sv-er,
  .steev-face[data-state="sleeping"] .sv-mouth {
    transition: none;
  }
}

/* ========================================================================
   .poster — typographic broadside, FORM vocabulary

   What remains is the input side: the workout capture form, the goal
   modal, and any modal that composes a .poster__foot action row
   (leaderboard settings, credit-code gift/redeem). The read-only workout
   broadside that used to live here is gone — cards render through the
   card vocabulary (components/card-sections.css).

     .poster--form             workout-input form rendered as a broadside

   Anatomy:
     - .poster__kicker         mono uppercase kicker rail (date / type)
     - .poster__display        Bricolage display title with .accent dot
     - .poster__ticker         metric ticker (key/val stats)
     - .poster__foot           Cancel / Submit action row
   ======================================================================== */

.poster {
	position: relative;
	gap: var(--space-md) var(--space-xl);
	padding-top: var(--space-xl);
	padding-bottom: var(--space-xl);
	color: var(--text);
	font-family: var(--font-body);
	overflow: hidden;
}

/* The surviving poster__ rules serve the form/modal vocabulary only. */
.poster__kicker {
	display: flex;
	flex-wrap: wrap;
	/* allow intensity and focus to stack on narrow screens */
	justify-content: space-between;
	align-items: baseline;
	gap: var(--space-xs) var(--space-md);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: var(--tracking-wider);
	text-transform: uppercase;
	color: var(--text-muted);
	border-bottom: 1px solid var(--border);
	padding-bottom: var(--space-sm);
}


.poster__display {
	font-family: var(--font-display);
	font-weight: 900;
	text-transform: uppercase;
	letter-spacing: -0.045em;
	/* 0.82 was tight for single-line display — compact period sits close
     to the cap baseline. On multi-line wrap, cap-height of line 2
     overlapped the cap-height of line 1. 0.95 keeps the display tone
     without collision. */
	line-height: 0.95;
	color: var(--text-bright);
	margin: 0;
}

.poster__display .accent {
	color: var(--accent-coach-text);
}

.poster__subsection-label {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: var(--tracking-wider);
	text-transform: uppercase;
	color: var(--text-muted);
}

.poster__ticker {
	display: flex;
	flex-wrap: wrap;
	/* allow stats to wrap to a new row when narrow */
	align-items: baseline;
	gap: var(--space-sm) var(--space-lg);
	padding-top: var(--space-md);
	border-top: 1px solid var(--border);
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: var(--tracking-wider);
	text-transform: uppercase;
	color: var(--text-muted);
}

/* ========================================================================
   .poster__foot — typographic action footer (Cancel / Submit)

   Base layout: hairline rule top, justify-between, baseline-aligned.
   Used by .poster--form and any modal that composes .poster__foot
   without a .poster--form ancestor (e.g. leaderboard_settings_modal,
   credit_code_gift_modal, credit_code_redeem_modal).
   ======================================================================== */

.poster__foot {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	padding-top: var(--space-sm);
	border-top: 1px solid var(--border);
	margin-top: var(--space-xs);
}

.poster__foot .poster__cancel {
	background: none;
	border: none;
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: var(--tracking-wider);
	text-transform: uppercase;
	color: var(--text-faint);
	cursor: pointer;
	padding: var(--space-xs) 0;
}

.poster__foot .poster__cancel:hover {
	color: var(--text-muted);
}

.poster__foot .poster__submit {
	background: none;
	border: none;
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	font-weight: var(--weight-semibold);
	letter-spacing: var(--tracking-wider);
	text-transform: uppercase;
	color: var(--accent-coach-text);
	cursor: pointer;
	padding: var(--space-xs) 0;
}

.poster__foot .poster__submit:hover {
	color: var(--accent-coach-hover);
}

.poster__foot .poster__submit:disabled {
	color: var(--text-faint);
	cursor: not-allowed;
}

/* ========================================================================
   .poster--form — workout-input form rendered as a broadside

   Composes the canonical kicker / display / lede / ticker rhythm.
   Replaces the form-group stack with typographic primitives. Effort
   becomes typographic (no pills); More-metrics is a kicker disclosure;
   footer is typographic Cancel/Submit (replaces X+✓ icons).
   ======================================================================== */

.poster--form {
	display: flex;
	flex-direction: column;
	gap: var(--space-sm);
	padding-top: 0;
	padding-bottom: 0;
}

.poster--form .poster__kicker {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: var(--tracking-wider);
	text-transform: uppercase;
	color: var(--accent-coach-text);
	border-bottom: none;
	padding-bottom: 0;
	flex-wrap: nowrap;
	align-items: center;
	column-gap: var(--space-xs);
}

.capture-type-mark {
	width: 20px;
	height: 20px;
	margin-left: auto;
	color: var(--text-muted);
}

.poster--form .poster__kicker-type {
	min-width: 0;
	text-overflow: ellipsis;
}

/* The checked type's `data-hide` flags are `CaptureVisibility` fields,
   stamped server-side; each section hides on its own flag. */
.poster--form:has([name="workout_type"] option:checked[data-hide~="custom_type"]) [data-capture-custom-type],
.poster--form:has([name="workout_type"] option:checked[data-hide~="metrics"]) :is([data-capture-metrics-section], [data-capture-extras-section]),
.poster--form:has([name="workout_type"] option:checked[data-hide~="distance"]) [data-capture-distance-fields],
.poster--form:has([name="workout_type"] option:checked[data-hide~="cadence_elevation"]) :is([data-capture-cadence-field], [data-capture-elevation-row]),
.poster--form:has([name="workout_type"] option:checked[data-hide~="gear"]) :is([data-capture-gear-field], [data-extras-summary="gear"]),
.poster--form:has([name="workout_type"] option:checked[data-hide~="intervals"]) :is([data-capture-intervals-section], [data-extras-summary="intervals"]),
.poster--form:has([name="workout_type"] option:checked:not([data-hide~="gear"])) :is([data-extras-summary="intervals"], [data-extras-summary="metrics"]),
.poster--form:has([name="workout_type"] option:checked:not([data-hide~="intervals"])) [data-extras-summary="metrics"] {
	display: none;
}

.poster--form .poster__display {
	font-family: var(--font-display);
	font-weight: 800;
	font-size: var(--text-3xl);
	line-height: 1;
	text-transform: none;
	letter-spacing: -0.02em;
	margin: 0;
}

.poster--form .poster__lede {
	font-family: var(--font-coach);
	font-style: italic;
	color: var(--text-muted);
	font-size: var(--text-base);
	margin: 0;
}

.poster--form .poster__ticker {
	display: flex;
	flex-wrap: wrap;
	gap: var(--space-sm) var(--inset);
	align-items: baseline;
	border-top: none;
	padding-top: 0;
}

.poster--form .poster__stat {
	display: flex;
	/* column-reverse: labels-below grammar — the key reads under the val's rule line */
	flex-direction: column-reverse;
	gap: 0;
	min-width: 0;
}

/* Race picker chrome shares the field's rank so it keeps reading directly
   under the search input. Labels-below grammar itself lives in forms.css. */
.poster--form .race-search-group > .race-results,
.poster--form .race-search-group > .selected-race {
	order: -2;
}

.poster--form .poster__stat .key {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: var(--tracking-wider);
	text-transform: uppercase;
	color: var(--text-muted);
}

.poster--form .poster__stat .val {
	font-family: var(--font-display);
	font-weight: 700;
	font-size: var(--text-2xl);
	letter-spacing: -0.01em;
	line-height: 1;
}

.poster--form .poster__stat input,
.poster--form .segment-splits-rows input,
.poster--form .segment-rows input {
	font-family: var(--font-body);
	font-weight: var(--weight-medium);
	font-size: var(--text-lg);
	letter-spacing: 0;
	color: inherit;
	background: transparent;
	border: none;
	border-bottom: 1px solid var(--text-faint);
	border-radius: 0;
	padding: 0 0 var(--space-xs);
	width: 100%;
	min-width: 0;
	-webkit-appearance: textfield;
	appearance: textfield;
	box-shadow: none;
}

.poster--form .poster__stat input:focus,
.poster--form .segment-splits-rows input:focus,
.poster--form .segment-rows input:focus {
	outline: none;
	border-bottom-color: var(--accent-coach);
}

.poster--form .poster__stat input::placeholder,
.poster--form .segment-splits-rows input::placeholder,
.poster--form .segment-rows input::placeholder {
	font-family: var(--font-mono);
	font-style: italic;
	font-size: var(--text-base);
	font-weight: 400;
	color: var(--text-faint);
	opacity: 0.6;
	letter-spacing: 0.05em;
}

.poster--form .segment-rows select {
	font-family: var(--font-body);
	font-weight: var(--weight-medium);
	font-size: var(--text-lg);
	letter-spacing: 0;
	color: inherit;
	background: transparent;
	border: none;
	border-bottom: 1px solid var(--text-faint);
	padding: 0 0 var(--space-xs);
	cursor: pointer;
	min-width: 0;
}

.poster--form .segment-rows select:focus {
	outline: none;
	border-bottom-color: var(--accent-coach);
}

/* Select showing its disabled placeholder option (e.g. "How often?"):
   match the input placeholder vocabulary — italic mono, muted, smaller. */
.poster--form .segment-rows select:has(option[value=""]:checked) {
	font-family: var(--font-mono);
	font-style: italic;
	font-size: var(--text-base);
	font-weight: 400;
	color: var(--text-faint);
	opacity: 0.6;
	letter-spacing: 0.05em;
}

.poster--form .poster__ticker--hero .poster__stat input {
	font-family: var(--font-display);
	font-weight: 700;
	font-size: var(--text-2xl);
	letter-spacing: -0.01em;
}

.poster--form .segment-splits-rows input,
.poster--form .segment-rows input {
	width: auto;
}

.poster--form .poster__ticker--more {
	row-gap: var(--space-sm);
	align-items: flex-start;
}

.poster--form .poster__ticker--more .poster__stat {
	flex: 1 1 7em;
	min-width: 6em;
}

.poster--form .poster__elev-pair {
	display: flex;
	gap: var(--inset);
	flex: 2 1 14em;
	min-width: 12em;
}

.poster--form .poster__elev-pair .poster__stat {
	flex: 1 1 0;
	min-width: 0;
}

.poster--form .poster__effort {
	display: flex;
	gap: var(--space-sm);
	align-items: baseline;
	flex-wrap: wrap;
}

.poster--form .poster__effort > .key {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: var(--tracking-wider);
	text-transform: uppercase;
	color: var(--text-muted);
}

.poster--form .poster__effort label {
	font-family: var(--font-body);
	font-size: var(--text-base);
	color: var(--text-muted);
	cursor: pointer;
}

.poster--form .poster__effort label:hover {
	color: var(--text);
}

.poster--form .poster__effort input[type="radio"],
.poster--form .segment-mode-btn input {
	position: absolute;
	opacity: 0;
	pointer-events: none;
}

.poster--form .poster__effort label span {
	border-bottom: 1px solid transparent;
	padding-bottom: 1px;
	transition: color var(--transition-fast), border-color var(--transition-fast);
}

.poster--form .poster__effort input[type="radio"]:checked + span {
	color: var(--text);
	border-bottom-color: var(--accent-coach);
}

.poster--form .poster__byline {
	font-family: var(--font-coach);
	font-style: italic;
	color: var(--text-muted);
	font-size: var(--text-base);
	margin: 0;
}

.poster--form .poster__byline input {
	font: inherit;
	color: inherit;
	background: transparent;
	border: none;
	border-bottom: 1px solid var(--text-faint);
	padding: 0 0 var(--space-xs);
	width: 100%;
}

.poster--form .poster__byline input:focus {
	outline: none;
	border-bottom-color: var(--accent-coach);
}

.poster--form .poster__more-section > .poster__more {
	font-family: var(--font-mono);
	font-size: var(--text-xs);
	letter-spacing: var(--tracking-wider);
	text-transform: uppercase;
	color: var(--text-muted);
	cursor: pointer;
	list-style: none;
}

.poster--form .poster__more::-webkit-details-marker {
	display: none;
}

.poster--form .poster__more::before {
	content: "+ ";
	color: var(--accent-coach-text);
}

.poster--form details[open] > .poster__more::before {
	content: "− ";
}

.poster--form .extras-container .poster__ticker {
	margin-top: var(--space-sm);
}

.poster--form .poster__foot {
	border-top: none;
	padding-top: 0;
}

.poster--form .poster__kicker input[type="date"],
.poster--form .poster__kicker select {
	font-family: inherit;
	font-size: inherit;
	letter-spacing: inherit;
	text-transform: inherit;
	color: inherit;
	background: transparent;
	border: none;
	padding: 0;
	cursor: pointer;
}

.poster--form .poster__kicker input[type="date"]:focus,
.poster--form .poster__kicker select:focus {
	outline: none;
	color: var(--text);
}

/* =============================================================================
   ACCOUNT CLOSE - /account/close confirmation page + settings danger zone
   ============================================================================= */

.account-close-page {
    max-width: var(--content-max-width);
    margin: 0 auto;
    padding: var(--space-xl) var(--inset-lg);
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

.account-close-title {
    font-family: var(--font-heading);
    font-size: var(--text-2xl);
    margin-top: var(--space-md);
}

.account-close-intro {
    color: var(--text-muted);
    font-size: var(--text-base);
}

.account-close-options {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(28rem, 100%), 1fr));
    gap: var(--inset-lg);
    margin-top: var(--space-lg);
}

.account-close-card {
    border-top: 1px solid var(--border);
    padding-top: var(--space-md);
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
}

.account-close-card-heading {
    font-family: var(--font-heading);
    font-size: var(--text-lg);
}

.account-close-card-body {
    color: var(--text-muted);
}

.account-close-back {
    margin-top: var(--space-lg);
    color: var(--text-muted);
    text-decoration: none;
}

.account-close-back:hover {
    color: var(--text-bright);
}

/* ─── Settings danger zone ─── */

.settings-danger {
    margin-top: var(--space-xl);
}

.settings-danger-link {
    display: inline-block;
    color: var(--text-muted);
    margin-top: var(--space-sm);
    text-decoration: none;
}

.settings-danger-link:hover {
    color: var(--accent-danger-text);
}

/* ─── Login flash (from /login?flash=deactivated|deleted) ─── */

.login-flash {
    color: var(--text-muted);
    font-size: var(--text-sm);
    padding: var(--space-sm) 0;
    text-align: center;
}

.signup-container {
  width: 100%;
  max-width: var(--content-max-width);
  margin: 0 auto;
}

.login-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-lg) var(--inset-lg);
}

.login-page:has(.signup-container) {
  align-items: flex-start;
  padding-top: var(--space-2xl);
}

.login-card {
  width: 100%;
  max-width: 400px;
  padding: var(--space-lg) var(--inset-lg);
  border-top: 3px solid var(--accent-action);
}

.login-header {
  text-align: center;
  margin-bottom: var(--space-xl);
}

.login-heading {
  font-size: var(--text-xl);
  margin: var(--space-sm) 0 var(--space-xs) 0;
}

.login-subtitle {
  color: var(--text-muted);
}

.login-footer {
  margin-top: var(--space-xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--border);
}

.login-link {
  color: var(--text);
  text-decoration: none;
  font-weight: var(--weight-semibold);
}

.login-link:hover {
  color: var(--text-bright);
  text-decoration: underline;
}

.login-create-account {
  margin-top: var(--space-lg);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--border);
  text-align: center;
}

.login-create-account-label {
  margin-bottom: var(--space-sm);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* Consent checkbox is shown only when the email field has a value.
   Gates a UX moment without JS — see 2026-05-01-email-opt-out-design.md §2. */
.email-consent {
  display: none;
  margin-top: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--text-muted);
  align-items: flex-start;
  gap: var(--space-xs);
  cursor: pointer;
}

.email-consent input[type="checkbox"] {
  margin-top: 0.2em;
  flex-shrink: 0;
}

.form-group:has(input[name="email"]:not(:placeholder-shown)) .email-consent {
  display: flex;
}

/* ToS takeaways card at signup — always visible, unlike .email-consent. */
.signup-tos-bullets {
  margin: 0 0 var(--space-sm);
  padding-left: 1.2em;
  display: grid;
  gap: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.signup-tos-links {
  display: flex;
  gap: var(--space-md);
  margin-bottom: var(--space-sm);
  font-size: var(--text-sm);
}

.signup-tos-agree {
  display: flex;
  align-items: flex-start;
  gap: var(--space-xs);
  font-size: var(--text-sm);
  color: var(--text-muted);
  cursor: pointer;
}

.signup-tos-agree input[type="checkbox"] {
  margin-top: 0.2em; /* same optical alignment as .email-consent */
  flex-shrink: 0;
}

.coach-page-content {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.pb-add-section {
  border-top: 1px solid var(--border);
  padding-top: var(--space-lg);
}

.unsubscribed-card {
  max-width: 32rem;
  margin: var(--space-xl) auto;
  text-align: center;
  padding: var(--space-xl) var(--inset-lg);
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  color: var(--text);
}

.unsubscribed-card h1 {
  margin-bottom: var(--space-sm);
}

.unsub-feedback {
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: 1px solid var(--border);
  text-align: left;
}

/* Used as a <legend> inside the .unsub-feedback-options fieldset for a11y —
   the prompt is programmatically associated with the radio group. */
.unsub-feedback-prompt {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin-bottom: var(--space-sm);
  padding: 0;
  display: block;
  float: none;
}

.unsub-feedback-options {
  border: 0;
  padding: 0;
  margin: 0 0 var(--space-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  font-size: var(--text-sm);
}

.unsub-feedback-submit {
  font-size: var(--text-sm);
  padding: var(--space-xs) var(--space-md);
  background: transparent;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.unsub-feedback-thanks {
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: 1px solid var(--border);
  font-size: var(--text-sm);
  color: var(--text-muted);
}


.store-status-card {
  position: relative;
}

.store-balance {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.store-status-details {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding-left: var(--inset-lg);
  border-left: 1px solid var(--border);
}

.store-status-row {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  font-size: var(--text-sm);
}

.store-status-row + .store-status-row {
  margin-top: var(--space-sm);
}

.store-status-label {
  color: var(--text-muted);
  min-width: 4rem;
}

.store-status-value {
  color: var(--text);
}

.store-actions {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: 1px solid var(--border);
}

.pricing-controls {
  display: flex;
  justify-content: center;
  gap: var(--space-lg);
  margin-bottom: var(--space-xl);
  flex-wrap: wrap;
}

.toggle-group {
  display: flex;
  background: var(--surface-sunken);
  border-radius: var(--radius-md);
  padding: 4px;
  gap: 4px;
}

.toggle-group .btn {
  background: transparent;
  border: none;
  padding: var(--space-xs) var(--inset);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  border-radius: var(--radius-sm);
  transition: color var(--transition-fast), background var(--transition-fast);
}

.toggle-group .btn:hover {
  color: var(--text);
}

.toggle-group .btn.active {
  background: var(--accent-action-strong);
  color: var(--text-on-accent);
}

.pricing-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 340px));
  justify-content: center;
  gap: var(--space-lg);
  margin-top: var(--space-lg);
}

.pricing-card {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--space-xl) var(--inset-lg);
  transition: border-color var(--transition-fast), transform var(--transition-fast);
  border-top: 3px solid var(--accent-action);
}

.pricing-card:hover {
  border-color: var(--accent-action);
  transform: translateY(-2px);
}

.pricing-card-featured {
  border: 2px solid var(--accent-action);
}

.pricing-badge {
  position: absolute;
  top: -12px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--accent-action-strong);
  color: var(--text-on-accent);
  padding: var(--space-xs) var(--inset);
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  white-space: nowrap;
}

.pricing-title {
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  color: var(--text-bright);
  margin: 0 0 var(--space-sm);
}

.pricing-description {
  color: var(--text-muted);
  font-size: var(--text-sm);
  margin-bottom: var(--space-md);
  line-height: 1.5;
}

.pricing-price {
  margin-bottom: var(--space-sm);
}

.price-amount {
  font-family: var(--font-heading);
  font-size: var(--text-3xl);
  font-weight: var(--weight-bold);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--text-bright);
}

.price-period {
  font-family: var(--font-mono);
  font-size: var(--text-base);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
}

.pricing-savings {
  color: var(--accent-success-text);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  margin-bottom: var(--space-md);
}

.pricing-features {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-xl);
  flex-grow: 1;
}

.pricing-features li {
  padding: var(--space-sm) 0;
  color: var(--text-secondary);
  position: relative;
  padding-left: 1.5rem;
}

.pricing-features li::before {
  content: "\2713";
  position: absolute;
  left: 0;
  color: var(--accent-success-text);
  font-weight: bold;
}

.pricing-loading {
  text-align: center;
  padding: var(--space-2xl);
  color: var(--text-muted);
}

.pricing-loading .spinner {
  margin-bottom: var(--space-md);
}

.pricing-error {
  text-align: center;
  padding: var(--space-xl) var(--inset-lg);
  color: var(--text-secondary);
}

.pricing-error p {
  margin-bottom: var(--space-md);
}

.usage-stats-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: var(--space-sm);
  text-align: center;
}

.usage-stat-card {
  background: var(--surface-sunken);
  border-radius: var(--radius-lg);
  padding: var(--space-lg) var(--inset);
  border-top: 3px solid var(--accent-action);
  transition: transform var(--transition-spring);
}

.usage-stat-card:hover {
  transform: translateY(-3px);
}

.usage-stat-value {
  display: block;
  font-family: var(--font-heading);
  font-size: var(--text-xl);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.015em;
  line-height: 1;
  color: var(--text);
}

.usage-stat-label {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  font-weight: var(--weight-medium);
  margin-top: var(--space-xs);
}

.usage-breakdown {
  border-top: 1px solid var(--border);
  padding-top: var(--space-md);
}

.usage-breakdown-row {
  display: flex;
  justify-content: space-between;
  padding: var(--space-xs) 0;
  font-size: var(--text-sm);
}

/* =============================================================================
   Section Dividers
   ============================================================================= */

.section-divider {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin-bottom: var(--space-xl);
}

.section-divider::before,
.section-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

.divider-text {
  color: var(--text-muted);
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  white-space: nowrap;
}



/* =============================================================================
   Public Pages (Races, Guides)
   ============================================================================= */

.public-page {
  max-width: var(--content-max-width);
  margin: 0 auto;
  padding: var(--space-xl) var(--inset);
}

.public-page-header {
  text-align: center;
  margin-bottom: var(--space-xl);
}

.public-page-header h1 {
  font-size: var(--text-3xl);
  margin-bottom: var(--space-sm);
}

.public-page-subtitle {
  color: var(--text-muted);
  font-size: var(--text-lg);
}

/* Filter Bar */
.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  justify-content: center;
  margin-bottom: var(--space-xl);
}

.filter-chip {
  padding: var(--space-xs) var(--inset);
  background: var(--surface-sunken);
  border-radius: var(--radius-full);
  color: var(--text-secondary);
  text-decoration: none;
  font-size: var(--text-sm);
  transition: color var(--transition-fast), background var(--transition-fast);
}

a.filter-chip:hover {
  background: var(--neutral);
  color: var(--text);
}

.filter-chip.active {
  background: var(--accent-action-strong);
  color: var(--text-on-accent);
}

/* forms.css stacks every <form> as a column; this one is a single inline row. */
.filter-bar--country {
  flex-direction: row;
  align-items: center;
  margin-top: calc(-1 * var(--space-md));
}

.filter-select-label {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.filter-select {
  width: auto;
  min-width: 12rem;
}

/* Breadcrumbs */
.breadcrumbs {
  margin-bottom: var(--space-lg);
}

.breadcrumbs a {
  color: var(--text-muted);
  text-decoration: none;
  font-size: var(--text-sm);
  transition: color var(--transition-fast);
}

.breadcrumbs a:hover {
  color: var(--text);
}

/* Empty State */
.empty-state {
  text-align: center;
  padding: var(--space-2xl);
  color: var(--text-muted);
}

/* Race Grid */
.race-grid {
  display: grid;
  /* min(280px, 100%) lets the column collapse to the container's width on
     viewports narrower than 280px instead of forcing horizontal scroll. */
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  gap: var(--space-lg);
}

.race-card {
  display: flex;
  flex-direction: column;
  padding: var(--space-lg) var(--inset-lg);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  text-decoration: none;
  transition: border-color var(--transition-fast), transform var(--transition-fast);
}

.race-card:hover {
  border-color: var(--accent-action);
  transform: translateY(-2px);
}

.race-card-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--space-sm);
}

.race-badge {
  display: inline-block;
  padding: 2px var(--inset-sm);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.badge-upcoming {
  background: var(--accent-success-faint);
  color: var(--accent-success-text);
}

.badge-past {
  background: var(--surface-sunken);
  color: var(--text-muted);
}

.race-distance-tag {
  font-size: var(--text-xs);
  color: var(--text-muted);
  background: var(--surface-sunken);
  padding: 2px var(--inset-sm);
  border-radius: var(--radius-full);
}

.race-card-name {
  font-size: var(--text-lg);
  color: var(--text-bright);
  margin-bottom: var(--space-sm);
}

.race-card-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

/* Race Detail */
.race-detail-header {
  margin-bottom: var(--space-xl);
}

.race-detail-header h1 {
  font-size: var(--text-3xl);
  margin-top: var(--space-sm);
}

.race-detail-info {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding: var(--space-lg) var(--inset-lg);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  margin-bottom: var(--space-xl);
}

.race-info-label {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  color: var(--text-muted);
}

.race-facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
  gap: var(--space-lg) var(--inset-lg);
}

.race-fact {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  min-width: 0;
}

.race-fact-value {
  font-family: var(--font-heading);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--tracking-tight);
  color: var(--text-bright);
  overflow-wrap: anywhere;
}

/* Flag glyphs resolve to the system emoji face, which sets its own optical size
   inside the display font. */
.race-fact-flag {
  margin-right: var(--space-xs);
  font-size: 0.9em;
  line-height: 1;
}

.race-fact-note {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wide);
  color: var(--text-muted);
}

/* The default track sits a shade above --surface-raised and vanishes on this
   panel, which matters most at 0% — the flattest race would read as a missing
   bar rather than an empty one. */
.race-fact-bar {
  background: var(--surface-sunken);
}

.race-facts-secondary {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--inset-lg);
  padding-top: var(--space-md);
  border-top: 1px solid var(--border);
}

.race-fact-line {
  display: flex;
  align-items: baseline;
  gap: var(--space-sm);
  font-size: var(--text-sm);
  color: var(--text-secondary);
}

.race-fact-line a {
  color: var(--accent-action-text);
  text-decoration: none;
}

.race-fact-line a:hover {
  text-decoration: underline;
}

.race-description {
  color: var(--text-secondary);
  font-size: var(--text-base);
  line-height: 1.6;
  margin-top: var(--space-lg);
}

.race-highlights {
  margin-top: var(--space-xl);
}

.race-highlights h2 {
  font-size: var(--text-xl);
  color: var(--text-bright);
  margin-bottom: var(--space-md);
}

.race-highlights-list {
  list-style: none;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.race-highlights-list li {
  position: relative;
  padding-left: var(--inset-lg);
  color: var(--text-secondary);
  line-height: 1.6;
}

.race-highlights-list li::before {
  content: "\2022";
  position: absolute;
  left: 0;
  color: var(--accent-action-text);
  font-weight: var(--weight-bold);
}

.race-detail-cta {
  text-align: center;
  padding: var(--space-xl) var(--inset-lg);
}

/* Guide Grid */
.guide-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(280px, 100%), 1fr));
  gap: var(--space-lg);
}

.guide-card {
  display: flex;
  flex-direction: column;
  padding: var(--space-lg) var(--inset-lg);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  text-decoration: none;
  transition: border-color var(--transition-fast), transform var(--transition-fast);
}

.guide-card:hover {
  border-color: var(--accent-action);
  transform: translateY(-2px);
}

.guide-category-badge {
  display: inline-block;
  width: fit-content;
  padding: 2px var(--inset-sm);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: var(--space-sm);
  background: var(--surface-sunken);
  color: var(--text-muted);
}

.badge-training {
  background: var(--accent-action-faint);
  color: var(--accent-action-text);
}

.badge-beginner {
  background: var(--accent-success-faint);
  color: var(--accent-success-text);
}

.badge-nutrition {
  background: var(--accent-caution-faint);
  color: var(--accent-caution-text);
}

.badge-recovery {
  background: var(--accent-recovery-faint);
  color: var(--accent-recovery-text);
}

.badge-racing {
  background: var(--accent-strength-faint);
  color: var(--accent-strength-text);
}

.badge-daily {
  background: var(--accent-action-faint);
  color: var(--accent-action-text);
}

.badge-tempo {
  background: var(--accent-moderate-faint);
  color: var(--accent-moderate-text);
}

.badge-race {
  background: var(--accent-intense-faint);
  color: var(--accent-intense-text);
}

.badge-trail {
  background: var(--accent-success-faint);
  color: var(--accent-success-text);
}

.badge-stability {
  background: var(--accent-strength-faint);
  color: var(--accent-strength-text);
}

.badge-max-cushion {
  background: var(--accent-recovery-faint);
  color: var(--accent-recovery-text);
}

.gear-specs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: var(--space-sm);
}

.gear-spec {
  background: var(--surface-sunken);
  border-radius: var(--radius-md);
  padding: var(--inset-sm);
}

.gear-spec dt {
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--text-muted);
}

.gear-spec dd {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  color: var(--text-bright);
}

.gear-spec-qualifier {
  display: block;
  font-size: var(--text-xs);
  font-weight: var(--weight-normal);
  color: var(--text-muted);
}

/* A stack height taken from another brand page cites that page from the value. */
.gear-spec dd a {
  color: inherit;
  text-decoration: underline;
  text-decoration-color: var(--accent-action);
  text-underline-offset: 2px;
}

.gear-editorial-note {
  margin-top: var(--space-sm);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* "Best for" — a row of chips, not a stack of full-width lines. */
.gear-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
}

/* The source is rendered as its raw URL: one unbreakable token that would push
   the page sideways at 320 px. Same fix as `.guide-source-list li`. */
.gear-source {
  overflow-wrap: anywhere;
}

.guide-card-title {
  font-family: var(--font-body);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.005em;
  line-height: 1.3;
  color: var(--text-bright);
  margin-bottom: var(--space-sm);
}

.guide-card-desc {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.5;
  flex-grow: 1;
}

.guide-read-more {
  margin-top: var(--space-md);
  font-size: var(--text-sm);
  color: var(--accent-action-text);
  font-weight: var(--weight-semibold);
}

/* Guide Detail */
.guide-article-header {
  margin-bottom: var(--space-xl);
}

.guide-article-header h1 {
  font-size: var(--text-3xl);
  margin-top: var(--space-sm);
  margin-bottom: var(--space-md);
}

.guide-article-desc {
  font-size: var(--text-lg);
  color: var(--text-secondary);
  line-height: 1.6;
}

.guide-article-body {
  font-size: var(--text-base);
  color: var(--text-secondary);
  line-height: 1.8;
  margin-bottom: var(--space-2xl);
}

/* The body is one blob: the gap between stretches rides on the heading that
   opens each one. */
.guide-article-body h2 {
  font-size: var(--text-xl);
  margin-top: var(--space-xl);
  margin-bottom: var(--space-md);
  color: var(--text-bright);
}

/* Guide at-a-glance strip (flagship guides only) */
.guide-glance {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  margin-bottom: var(--space-xl);
}

.guide-glance-caption {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  color: var(--text-muted);
}

.guide-glance-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(112px, 1fr));
  gap: var(--space-xs);
  list-style: none;
  margin: 0;
  padding: 0;
}

.guide-glance-item {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-sm) var(--inset-sm);
  border-radius: var(--radius-sm);
  min-width: 0;
}

/* No opacity on either mono line: --tag-text-* is contrast-tuned against its own
   faint tint, and fading it undoes that tuning. */
.guide-glance-lead {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
}

.guide-glance-name {
  font-family: var(--font-heading);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-tight);
}

.guide-glance-detail {
  font-family: var(--font-mono);
  font-size: var(--text-2xs);
  font-variant-numeric: tabular-nums;
}

/* Tint pairs are the same ones .intensity-* uses on workout tags, so an effort
   level looks the same here as it does in the app. Coral extends the ramp to a
   fifth step; mauve is skipped — it is the reserved coach voice. */
.guide-glance-item--recovery {
  background: var(--accent-success-faint);
  color: var(--tag-text-success, var(--accent-success));
}

.guide-glance-item--easy {
  background: var(--accent-action-faint);
  color: var(--tag-text-action, var(--accent-action));
}

.guide-glance-item--moderate {
  background: var(--accent-moderate-faint);
  color: var(--tag-text-moderate, var(--accent-moderate));
}

.guide-glance-item--hard {
  background: var(--accent-danger-faint);
  color: var(--tag-text-danger, var(--accent-danger));
}

.guide-glance-item--max {
  background: var(--accent-coach-faint);
  color: var(--tag-text-coach, var(--accent-coach));
}

.guide-detail-cta {
  text-align: center;
  padding: var(--space-xl) var(--inset-lg);
  border-top: 1px solid var(--border);
}

/* Blog List */
.blog-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-lg);
}

.blog-card {
  display: flex;
  flex-direction: column;
  padding: var(--space-lg) var(--inset-lg);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  text-decoration: none;
  transition: border-color var(--transition-fast), transform var(--transition-fast);
}

.blog-card:hover {
  border-color: var(--accent-action);
  transform: translateY(-2px);
}

.blog-card-date {
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: var(--space-xs);
}

.blog-card-title {
  font-family: var(--font-body);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.005em;
  line-height: 1.3;
  color: var(--text-bright);
  margin-bottom: var(--space-sm);
}

.blog-card-desc {
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.5;
  flex-grow: 1;
}

.blog-read-more {
  margin-top: var(--space-md);
  font-size: var(--text-sm);
  color: var(--accent-action-text);
  font-weight: var(--weight-semibold);
}

/* Blog Detail */
.blog-article-header {
  margin-bottom: var(--space-xl);
}

.blog-article-date {
  display: block;
  font-size: var(--text-sm);
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-bottom: var(--space-sm);
}

.blog-article-header h1 {
  font-size: var(--text-3xl);
  margin-bottom: var(--space-md);
}

.blog-article-desc {
  font-size: var(--text-lg);
  color: var(--text-secondary);
  line-height: 1.6;
}

.blog-article-body {
  font-size: var(--text-base);
  color: var(--text-secondary);
  line-height: 1.8;
  margin-bottom: var(--space-2xl);
}

.blog-article-body h2 {
  font-size: var(--text-xl);
  margin-top: var(--space-xl);
  margin-bottom: var(--space-md);
  color: var(--text-bright);
}

/* Guide + blog markdown bodies */
.guide-article-body > :first-child,
.blog-article-body > :first-child {
  margin-top: 0;
}

.guide-article-body > :last-child,
.blog-article-body > :last-child {
  margin-bottom: 0;
}

/* --space-lg, not --space-md: at line-height 1.8 the tighter step reads cramped
   in long-form prose. */
.guide-article-body p,
.blog-article-body p {
  margin: 0 0 var(--space-lg) 0;
}

/* base.css styles `p` and `h1..h6` globally, which would otherwise win over the
   container colour and split the body into two shades. h2 is excluded: it is the
   article's own heading level and owns its colour above. */
.guide-article-body :is(p, li, h1, h3, h4, h5, h6),
.blog-article-body :is(p, li, h1, h3, h4, h5, h6) {
  color: inherit;
}

.guide-article-body strong,
.blog-article-body strong {
  font-weight: var(--weight-semibold);
  color: var(--text-bright);
}

.guide-article-body ul,
.guide-article-body ol,
.blog-article-body ul,
.blog-article-body ol {
  margin: var(--space-sm) 0;
  padding-left: var(--inset-lg);
}

.guide-article-body li,
.blog-article-body li {
  margin-bottom: var(--space-xs);
}

/* No corpus carries a sub-heading today; this keeps a future one at body scale. */
.guide-article-body :is(h1, h3, h4, h5, h6),
.blog-article-body :is(h1, h3, h4, h5, h6) {
  font-size: inherit;
  font-weight: var(--weight-semibold);
  margin: 0 0 var(--space-sm) 0;
}

.blog-article-body code {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

.blog-detail-cta {
  text-align: center;
  padding: var(--space-xl) var(--inset-lg);
  border-top: 1px solid var(--border);
}

/* Related Sections (shared between race + guide detail pages) */
.related-section {
  margin-top: var(--space-xl);
  padding-top: var(--space-lg);
  border-top: 1px solid var(--border);
}

.related-section h2 {
  font-size: var(--text-lg);
  color: var(--text-bright);
  margin-bottom: var(--space-md);
}

.related-links {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.related-link-card {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: var(--space-md) var(--inset);
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  text-decoration: none;
  transition: border-color var(--transition-fast), transform var(--transition-fast);
}

.related-link-card:hover {
  border-color: var(--accent-action);
  transform: translateY(-1px);
}

.related-link-badge {
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  color: var(--accent-action-text);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.related-link-title {
  font-family: var(--font-body);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  letter-spacing: -0.005em;
  line-height: 1.3;
  color: var(--text-bright);
}

/* Guide sources — inside a .related-section, which owns the rule above and the
   heading. Numbered, quieter than the body, and long citations wrap. */
.guide-source-list {
  margin: 0;
  padding-left: var(--inset-lg);
  font-size: var(--text-sm);
  color: var(--text-secondary);
  line-height: 1.6;
}

.guide-source-list li {
  overflow-wrap: anywhere;
}

.guide-source-list li + li {
  margin-top: var(--space-sm);
}

.guide-source-list a {
  color: var(--accent-action-text);
  text-decoration: none;
}

.guide-source-list a:hover {
  text-decoration: underline;
}

/* Editorial article headings — long sentence-case titles read better in body-sans
   than in a condensed display font. Scoped to article wrappers so other h1/h2
   uses (landing, section headers) keep their display treatment. */
.guide-article h1,
.blog-article h1 {
  font-family: var(--font-body);
  font-weight: var(--weight-bold);
  letter-spacing: -0.01em;
  line-height: 1.2;
}
.guide-article h1,
.blog-article h1 {
  font-size: var(--text-2xl);
}

.related-link-meta {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

/* =============================================================================
   LANDING.CSS — the v5 landing layer

   The landing renders with `body.coach-landing`: the training-log paper theme
   paints the ground and every shared piece (rail, cards, scribbles) arrives
   with its production look. This file only LAYS OUT those pieces and adds
   what is landing-only — the keyframes, the hero blob and its wash, the demo's
   two-column grid and its CSS-timed scene, the eyebrow labels, the manifesto
   display type, the closing band.

   Load order matters here: main.css imports this file BEFORE coach-page.css,
   so a tie with a coach-page.css rule is lost, not won. The two places that
   bite (`.coach-inline`, `.message-stream`) carry a note at the rule.
   ============================================================================= */

/* ═══════════════════════════════════════════════════════════════════════════
   KEYFRAMES — the landing-only animations (spec §9). Everything else
   (typing indicator, breath line, face breathing, stutter rules, edge fades,
   card tilt) is production CSS.
   ═══════════════════════════════════════════════════════════════════════════ */

@keyframes hero-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

/* The blob bobs AND holds the brand's −3° tilt: the rotation lives in the
   keyframe, so the resting transform must not be set on the element too. */
@keyframes face-bob {
  0%, 100% { transform: rotate(-3deg) translateY(0); }
  50%      { transform: rotate(-2deg) translateY(-8px); }
}

@keyframes msg-in {
  from { opacity: 0; transform: translateY(12px); }
  to   { opacity: 1; transform: none; }
}

@keyframes swap-in {
  from { opacity: 0; transform: translateY(-5px); }
  to   { opacity: 1; transform: none; }
}

@keyframes swap-pulse {
  0%   { box-shadow: 0 0 0 0 var(--accent-coach-soft); }
  70%  { box-shadow: 0 0 0 14px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}

/* The scene's two windows, run with no fill: a finished-hidden node shows only
   while `demo-window` runs; the struck-out label reads at full ink only while
   `swap-unstruck` runs. */
@keyframes demo-window {
  from, to { visibility: visible; }
}

@keyframes swap-unstruck {
  from, to { opacity: 1; }
}

@keyframes strike-in {
  from { transform: scaleX(0); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   SHARED — band rhythm + the editorial eyebrow every section label uses
   ═══════════════════════════════════════════════════════════════════════════ */

/* One measure for every v5 band. The side gutter is `.container`'s
   `--inset-lg` — adding a second one here would double it. */
.landing-section,
.landing-demo-section {
  max-width: 1160px;
  margin-inline: auto;
  padding-block: var(--space-2xl);
}

.landing-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  color: var(--text-muted);
  margin: 0 0 var(--space-md);
}

/* Brackets are decoration, not copy — they stay out of copy.toml. */
.landing-eyebrow::before { content: "[ "; }
.landing-eyebrow::after  { content: " ]"; }

/* ═══════════════════════════════════════════════════════════════════════════
   HERO (§3) — copy left, bobbing face blob right, two washes behind both
   ═══════════════════════════════════════════════════════════════════════════ */

.landing-hero {
  position: relative;
  overflow: hidden;
  padding-block: clamp(48px, 8vh, 88px) 56px;
}

.landing-hero-wash {
  position: absolute;
  inset: -10%;
  pointer-events: none;
  background:
    radial-gradient(ellipse 70% 60% at 20% 20%, var(--accent-coach-faint), transparent 60%),
    radial-gradient(ellipse 55% 50% at 85% 85%, var(--accent-caution-faint), transparent 65%);
}

/* z-index lifts the stage over the absolutely-positioned wash, which would
   otherwise paint on top of its static siblings. */
.landing-hero-stage {
  position: relative;
  z-index: 1;
  max-width: 1160px;
  margin-inline: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 48px;
  animation: hero-in 500ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

.landing-hero-copy {
  flex: 1 1 480px;
  min-width: 300px;
}

/* Hero opener — extends .coach-voice-opener with a larger display scale
   since this is THE entrance sentence, not an inline prescription.
   Overrides the parent's --tag-text-info base color: on the landing
   hero, only the wordmark deserves coach-coral. The connective greeting
   text ("Hey I'm…") drops to neutral --text so coral stays reserved for
   the brand-essential elements. The .brand-wordmark inside still renders
   coral via its own rule in base.css; the drop-cap is overridden below
   to match the surrounding text. */
.coach-voice-opener--hero {
  font-size: clamp(2.5rem, 5.5vw, 4.5rem);
  line-height: 1.08;
  max-width: 24ch;
  color: var(--text);
}

.coach-voice-opener--hero .drop-cap {
  color: var(--text);
}

.landing-hero-greeting {
  margin-bottom: var(--space-sm);
}

/* Wordmark sits inline with the italic flow. Bricolage Grotesque 800
   has a slightly taller x-height than the coach face; 0.95em equalizes
   visual weight without making the wordmark vanish into the surrounding
   text. */
.landing-hero-greeting .brand-wordmark {
  font-size: 0.95em;
  vertical-align: baseline;
  font-style: normal;
}

/* Breath-line mounted beneath the greeting. Absolute positioning from
   .breath-line is overridden here — hero's breath sits in normal flow
   so the pitch paragraph follows without layering. */
.landing-hero-breath {
  position: relative;
  margin: var(--space-md) 0 var(--space-lg);
  max-width: 44ch;
}

.landing-hero-pitch {
  font-size: var(--text-lg);
  line-height: 1.55;
  color: var(--text);
  max-width: 52ch;
  margin: 0 0 var(--space-xl);
}

/* A <p> carrying the button and the reassurance line side by side. */
.landing-hero-cta-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 20px;
  margin: 0;
}

.landing-hero-reassure {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.landing-hero-blob {
  width: min(300px, 56vw);
  min-width: 220px;
  aspect-ratio: 1;
  margin-inline: auto;
  border-radius: var(--radius-blob);
  background: var(--accent-coach-soft);
  box-shadow: var(--shadow-hard-md);
  display: grid;
  place-items: center;
  animation: face-bob 6s ease-in-out infinite;
}

/* Same treatment as the nav brand blob: dark ink on coral, and the mouth
   shade re-derived from currentColor — left alone it comes from
   --accent-info (coach voice) and paints a mauve mouth on the coral. */
.landing-hero-blob .steev-face,
.landing-closing-blob .steev-face {
  color: var(--paper-ink-on-coral);
  --mouth-shade: color-mix(in oklab, currentColor 55%, black 45%);
}

.landing-hero-blob .steev-face {
  width: 72%;
  height: 72%;
}

/* ═══════════════════════════════════════════════════════════════════════════
   DEMO (§4, §5) — chat column + plan rail, side by side above ~1100px
   ═══════════════════════════════════════════════════════════════════════════ */

.landing-demo-section > .landing-eyebrow {
  margin-bottom: var(--space-lg);
}

.landing-demo {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 40px;
}

/* The second selector is the SAME box at a higher specificity, not a second
   style: `.coach-inline` is the coach page's two-pane GRID at ≥760.02px
   (coach-page.css, which loads after this file), and the landing chat is one
   column inside its own two-column demo. A selector list takes the specificity
   of whichever selector matched, so this block outranks that grid wherever the
   markup carries `.coach-inline` and still applies where it doesn't. */
.landing-chat,
.landing-demo .landing-chat.coach-inline {
  flex: 1 1 520px;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* Gap (30px) and bottom padding come from `.message-stream`. Four classes
   deep on purpose: `.coach-inline > .message-stream` (coach-page.css, 0-2-0,
   and it loads after this file) hands the stream its own scroller for the
   coach's two-pane shell; the landing's stream is not a pane and must not
   become one. */
body.coach-landing .landing-demo .landing-chat > .message-stream {
  overflow: visible;
}

.landing-rail {
  flex: 1 1 300px;
  max-width: 400px;
  min-width: 280px;
}

.landing-rail-note {
  margin: var(--space-lg) 0 0;
  padding: 0;
  font-family: var(--font-coach);
  font-style: italic;
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text-muted);
}

.landing-rail-note p {
  margin: 0;
}

.landing-rail-note footer {
  font-family: var(--font-mono);
  font-style: normal;
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  margin-top: 4px;
}

/* The swapped node's "· swapped" / "· moved" tail. */
.track-date-suffix {
  color: var(--accent-coach-text);
}

/* The pre/post mark wrappers exist only to carry the swap hooks the icon
   macro cannot — they must add no box, or the mark stops being a grid item of
   the node and the desktop rail's explicit placement misses it. */
.landing-demo [data-swap-pre-mark],
.landing-demo [data-swap-post-mark] {
  display: contents;
}

/* Both states of the swap node ship, stacked in ONE slot so the finished
   node's box is the box it would have without its past: the two labels share
   a grid cell (grid items are blockified, so swap-in's drop applies), and the
   two marks already share the desktop rail's explicit mark cell — the phone
   strip's flex column gets the same overlap in the mobile block below. */
.landing-demo [data-demo-swap] .track-label {
  display: grid;
  justify-items: start;
}

.landing-demo :is([data-swap-pre], [data-swap-post]) {
  grid-area: 1 / 1;
}

.landing-demo :is([data-swap-pre], [data-swap-pre-mark] .track-mark) {
  visibility: hidden;
}

/* The struck-out past: only ever seen inside the scene. The strike is a drawn
   line, not text-decoration — Safari does not animate a decoration's colour. */
.landing-demo [data-swap-pre] {
  position: relative;
  opacity: 0.45;
}

.landing-demo [data-swap-pre]::after {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: 50%;
  height: 1.5px;
  background: currentColor;
  transform-origin: left;
}

/* Each node is a radio (clipped by the recipe in chat-marginalia.css) + its
   label. A <label> inherits the page's line-height where the <button> it
   replaced reset it to `normal`. */
.landing-demo label.track-node {
  line-height: normal;
}

.landing-demo input[name="landing-node"]:focus-visible + .track-node {
  outline: 2px solid var(--paper-link);
  outline-offset: 3px;
}

/* Production's `.track-node--active` look (progress-track.css), on the
   checked node. */
.landing-demo input[name="landing-node"]:checked + .track-node .track-label {
  color: var(--node-accent);
  font-weight: var(--weight-bold);
}

.landing-demo .timeline-detail {
  display: none;
}

.landing-demo input[name="landing-node"]:checked + .track-node + .timeline-detail {
  display: block;
}

/* Production solidifies the older node's connector when its next sibling is a
   completed node; here that sibling is the completed node's radio. */
.landing-demo .track-node--older:has(+ input + .track-node--completed)::after {
  background: color-mix(in srgb, var(--text-muted) 28%, transparent);
}

/* The nodes row interleaves a hidden `.timeline-detail` after EVERY node, so
   the last element in `.track-nodes` is a detail card, not a node — and
   production's two "I am last" guards (`.track-nodes > :last-child::after` and
   `.track-node:not(:last-child)::before`) both stop landing on the final node,
   which then draws a connector into empty space. Ask the real question
   instead: is there another node after me? */
.coach-landing .landing-demo .track-nodes .track-node:not(:has(~ .track-node))::after {
  display: none;
}

@media (min-width: 760.02px) {
  /* Vertical rail: ::before is redrawn as the dashed spine running DOWN from
     the node, so the last node's has to go too. Below this width ::before is
     the incoming left half of the horizontal connector and must stay. */
  .coach-landing .landing-demo .track-nodes .track-node:not(:has(~ .track-node))::before {
    display: none;
  }
}

/* Below the production strip's own breakpoint (progress-track.css switches the
   rail from column to horizontal scroller at 760.02px) the in-place detail
   card is a flex item in the scroll row — give it a card's width so it reads
   as one, instead of stretching the row to the poster's full measure. */
@media (max-width: 760px) {
  .landing-demo .track-nodes .timeline-detail {
    flex: 0 0 min(320px, 78vw);
  }

  /* The landing rail is a section of a scrolling marketing page, not the coach
     page's sticky dock: there is no coach header to offset from, nothing
     scrolls under it, and the ± inset bleed and opaque ground read as a
     stranded toolbar. `padding-block` is set outright rather than zeroed —
     it also settles `.coach-rail:has(.timeline-detail:not(:empty))`, whose
     at-rest inset applies PERMANENTLY here because every landing detail card
     is server-rendered (non-empty) and merely hidden. */
  body.coach-landing .landing-demo .coach-rail.landing-rail {
    position: static;
    background: transparent;
    margin-inline: 0;
    padding-inline: 0;
    padding-block: 0 var(--space-sm);
  }

  /* Production hides the week bar on phones (the mobile /coach rail is
     compressed sticky chrome); the landing rail is a full page section, so
     the bar is content here. Re-show it — its paint lives at
     progress-track.css top level (one home), so the full bar renders. */
  body.coach-landing .landing-demo .landing-rail .rail-week {
    display: block;
  }

  /* The swap node's two marks would stack in the phone strip's flex column;
     the same column as a grid puts both in the mark's row. */
  .landing-demo [data-demo-swap],
  .landing-demo [data-demo-swap] .track-label {
    display: grid;
    justify-items: center;
  }

  .landing-demo [data-demo-swap] .track-mark {
    grid-area: 2 / 1;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE SCENE — the demo replays as a CSS timeline. Everything above is the
   FINISHED state (the no-JS and reduced-motion page); each hook's rewound
   state lives only in its keyframes, and its `--at` is its moment. Visible
   when finished = `backwards` fill (the `from` frame holds through the delay);
   hidden when finished = no fill, shown only while its window runs. The
   landing.html script holds the scene paused until the demo is on screen.
   ═══════════════════════════════════════════════════════════════════════════ */

/* `!important`: every hook's `animation` shorthand resets play-state, and the
   strike is a pseudo-element `*` does not reach. */
.landing-demo--waiting,
.landing-demo--waiting *,
.landing-demo--waiting *::after {
  animation-play-state: paused !important;
}

.landing-demo [data-demo="u1"]                          { --at: 0.4s; }
.landing-demo [data-demo="u1"] [data-demo-typing]       { --at: 1s; }
.landing-demo [data-demo="c1"] .scribble                { --at: 1.9s; }
.landing-demo [data-swap-pre]                           { --strike-at: 2.5s; }
.landing-demo :is([data-demo-swap], [data-swap-enter])  { --at: 3s; }
.landing-demo [data-demo="c1"] .chat-card               { --at: 3.6s; }
.landing-demo [data-demo="u2"]                          { --at: 5.1s; }
.landing-demo [data-demo="u2"] [data-demo-typing]       { --at: 5.7s; }
.landing-demo [data-demo="c2"]                          { --at: 6.6s; }
.landing-demo [data-demo="x3"] .entry                   { --at: 8.3s; }
.landing-demo [data-demo="x3"] [data-demo-typing]       { --at: 8.8s; }
.landing-demo [data-demo="x3"] .scribble                { --at: 9.7s; }
.landing-demo [data-demo="x4"] .entry                   { --at: 11.4s; }
.landing-demo [data-demo="x4"] [data-demo-typing]       { --at: 11.9s; }
.landing-demo [data-demo="x4"] .scribble                { --at: 12.8s; }

/* Typing dots: no footprint when finished — the box is zero-tall and its
   margin cancels the `.exchange-group` gap before it — so they overlay the
   reply's slot for their 0.9s window. */
.landing-demo [data-demo-typing] {
  height: 0;
  padding-block: 0;
  align-items: flex-start;
  margin-bottom: calc(-1 * var(--space-sm));
  visibility: hidden;
}

@media (prefers-reduced-motion: no-preference) {
  /* Bubbles: each holds its slot, invisible, until its moment. */
  .landing-demo :is(
    [data-demo="u1"], [data-demo="u2"], [data-demo="c2"],
    [data-demo="c1"] .scribble, [data-demo="c1"] .chat-card,
    [data-demo="x3"] .entry, [data-demo="x3"] .scribble,
    [data-demo="x4"] .entry, [data-demo="x4"] .scribble
  ) {
    animation: msg-in var(--motion-arrive) var(--at) backwards;
  }

  .landing-demo [data-demo-typing] {
    animation: demo-window 0.9s var(--at);
  }

  /* The swap: the past label and mark show until the swap moment (the label
     struck from its own moment on); the new state lands and the mark rings. */
  .landing-demo [data-swap-pre-mark] .track-mark {
    animation: demo-window var(--at);
  }

  .landing-demo [data-swap-pre] {
    animation: demo-window var(--at), swap-unstruck var(--strike-at);
  }

  .landing-demo [data-swap-pre]::after {
    animation: strike-in var(--motion-arrive) var(--strike-at) backwards;
  }

  .landing-demo :is([data-swap-post], [data-swap-suffix], [data-swap-enter]) {
    animation: swap-in 320ms ease-out var(--at) backwards;
  }

  /* On the `.track-mark`: its `display: contents` wrapper has no box to ring. */
  .landing-demo [data-swap-post-mark] .track-mark {
    animation:
      swap-in 320ms ease-out var(--at) backwards,
      swap-pulse 900ms ease-out var(--at);
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   MANIFESTO (§7) — numbered claims, alternating L/R anchor, stutter dividers
   ═══════════════════════════════════════════════════════════════════════════ */

.landing-section--editorial {
  padding-block: var(--space-section);
}

.landing-stack {
  list-style: none;
  padding: 0;
  margin: 0;
}

.landing-stack-row {
  padding-block: var(--space-xl);
}

/* Stutter-rule divider between editorial items — three dashes of
   decreasing length (14-8-4 px), from the shared `.stutter-rule`. */
.landing-stack-stutter {
  display: flex;
  justify-content: center;
  padding: var(--space-sm) 0;
}

.landing-stack-row-inner {
  max-width: 52ch;
}

.landing-stack-row--l .landing-stack-row-inner { margin-left: 0; margin-right: auto; }
.landing-stack-row--r .landing-stack-row-inner { margin-left: auto; margin-right: 0; }

.landing-stack-num {
  display: block;
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  text-transform: uppercase;
  letter-spacing: var(--tracking-wider);
  color: var(--accent-action-text);
  margin-bottom: var(--space-sm);
}

/* Big Shoulders Display is named ahead of the token on purpose: --font-display
   resolves to Bricolage, and these four headings are the one place on the site
   that wants the poster face at 900. */
.landing-stack-title {
  font-family: "Big Shoulders Display", var(--font-display);
  font-weight: 900;
  font-size: clamp(2rem, 4.5vw, 3.5rem);
  line-height: 1.02;
  letter-spacing: -0.02em;
  color: var(--text-bright);
  margin: 0 0 var(--space-md);
}

.landing-stack-desc {
  font-size: var(--text-base);
  line-height: 1.6;
  color: var(--text-secondary);
  margin: 0;
}

.landing-stack-marginalia {
  margin: var(--space-xl) 0 0;
  padding: var(--space-md) 0 0;
  font-size: var(--text-sm);
  color: var(--text-muted);
  border-top: 1px dashed var(--border);
}

/* ═══════════════════════════════════════════════════════════════════════════
   POKE AROUND (§8) — the content routes as one card-list
   ═══════════════════════════════════════════════════════════════════════════ */

/* The card is the section's whole content here, not a chat bubble beside a
   face, so it takes the full measure. */
.landing-poke .chat-card {
  max-width: 100%;
}

/* Production `.card-list__hook` is a <span>; on the landing every row is a
   link, and an unreset <a> arrives underlined and accent-coloured. */
.landing-poke .card-list__hook {
  text-decoration: none;
  color: inherit;
}

/* ═══════════════════════════════════════════════════════════════════════════
   CLOSING (§8) — face blob, one coach-voice line, the primary door
   ═══════════════════════════════════════════════════════════════════════════ */

.landing-closing {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--space-lg);
}

/* Same lift as the hero stage, without a wrapper element to hang it on. */
.landing-closing > :not(.landing-closing-wash) {
  position: relative;
}

.landing-closing-wash {
  position: absolute;
  inset: -10%;
  pointer-events: none;
  background: radial-gradient(
    ellipse 60% 70% at 50% 100%,
    var(--accent-coach-faint),
    transparent 65%
  );
}

.landing-closing-blob {
  width: 64px;
  height: 64px;
  border-radius: var(--radius-blob);
  background: var(--accent-coach-soft);
  display: grid;
  place-items: center;
  transform: rotate(-3deg);
}

.landing-closing-blob .steev-face {
  width: 44px;
  height: 44px;
}

.landing-closing-line {
  font-size: clamp(1.6rem, 3vw, 2.5rem);
  max-width: 26ch;
}

.landing-closing-reassure {
  font-size: var(--text-sm);
  color: var(--text-muted);
  margin: 0;
}

.landing-check {
  color: var(--accent-success-text);
  font-weight: var(--weight-bold);
}

/* ═══════════════════════════════════════════════════════════════════════════
   REDUCED MOTION — the hero's animations off (the scene's only exist under
   `no-preference`, so the demo renders its base, the FINISHED scene).
   `animation: none`, not a 0s duration, so no fill can strand an element in
   its first frame. Card tilt persists per app policy.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  .landing-hero-stage,
  .landing-hero-blob {
    animation: none;
  }

  /* face-bob CARRIES the brand's −3° tilt (it is in the keyframe, so the
     resting element has no transform). Stopping the bob must not straighten
     the blob — every other Steev disc on the site is tilted. */
  .landing-hero-blob {
    transform: rotate(-3deg);
  }
}

/* =============================================================================
   COACH-PAGE.CSS - Coach page layout only

   This file owns the page-level scroll flow (sticky composer, full-height
   chat column) and the coach opener composition. Everything else — the
   chat bubbles, the composer chrome, the journey track, the face system,
   the stats panel — lives in the components/ directory and is composed
   via main.css @imports.
   ============================================================================= */

/* ─── Coach page layout: full-page scroll with sticky input (no footer) ─── */
body:has(.coach-page-content) {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
  /* clip, not hidden: the body never becomes a scroll container, so the
     sticky composer keeps its viewport. */
  overflow-x: clip;
}

body:has(.coach-page-content) > .container {
  flex: 1;
  display: flex;
  flex-direction: column;
}

/* ─── Staggered entrance animation (keyframe in page-zones.css) ─── */

[data-coach-zone] {
  animation: coach-enter 0.45s cubic-bezier(0.16, 1, 0.3, 1) both;
}

[data-coach-zone="timeline"] {
  animation-delay: 160ms;
}

/* ═══════════════════════════════════════════════════════════════════════════
   COACH PAGE LAYOUT — single-column chat flow
   ═══════════════════════════════════════════════════════════════════════════ */

.coach-page-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

.coach-page-chat {
  display: flex;
  flex-direction: column;
  flex: 1;
}

#chat-area {
  display: flex;
  flex-direction: column;
  flex: 1;
}

/* Loading state: typing dots need visible height before HTMX loads */
#chat-area > .chat-loading-stub {
  min-height: 4rem;
  display: flex;
  align-items: center;
  background: none;
  padding: 0;
  border-radius: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   INLINE COACH — full-page conversational area (no card boundary)
   ═══════════════════════════════════════════════════════════════════════════ */

.coach-inline {
  display: flex;
  flex-direction: column;
  flex: 1;
}

/* Stream of bubbles — the shared semantic for any page that renders a
   sequence of exchange-groups (chat page #chat-messages, landing
   conversation demo). Past-face hiding (steev-face.css) and awake-
   tagging (steev-face.js) work per-parent inside this stream. */
.message-stream {
  display: flex;
  flex-direction: column;
  gap: 30px;
  padding-bottom: 20px;
  min-width: 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   TRAINING-LOG SHELL — the chat keeps the base flex column at every width;
   only the rail changes box.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Entry rhythm — the same at every width; the ledger's horizontal padding
   (gutter left, 28px right) is set per width below. */
body.coach-ledger .message-stream {
  gap: 22px;
}

/* The strip's slot (chat_area.html / streaming.js): the stream's gap would open
   a blank row for every empty one — the tail slot in two panes, superseded ones. */
.journey-slot:empty,
.journey-slot:has([data-bare]) {
  display: none;
}

/* The container has no top padding, so the banner carries its own top
   breathing room, or it hugs the header's dashed rule. */
.onboarding-checklist {
  margin-top: var(--space-md);
}

@media (min-width: 760.02px) {
  /* The chat grid and its scrolling pane are the ledger's two-pane (the
     one-column block below undoes them up to 1000px); the rail and the spine
     are the landing demo's (SPEC §9). */
  .coach-inline {
    display: grid;
    grid-template-columns: minmax(0, 1fr) var(--coach-rail-w);
    grid-template-rows: minmax(0, 1fr) auto;
    column-gap: var(--coach-rail-gap);
    min-height: 0;
  }

  .coach-inline > .message-stream {
    grid-column: 1;
    grid-row: 1;
    overflow-y: auto;
    overscroll-behavior: contain;
    min-height: 0;
  }

  body.coach-landing .landing-rail {
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* Chat spine: the pen rule every entry hangs off. */
  body.coach-landing .message-stream {
    border-left: 1.5px dashed var(--paper-rule-soft);
    padding-left: 26px;
  }

  /* Null the PHANTOM scroller. `.progress-track` carries `.scroll-row`'s
     `overflow-x: auto`, and beside `overflow-y: visible` that computes to auto
     on BOTH axes — a scroll container with nothing to scroll in a vertical
     column, and a wheel gesture landing on it was captured by a scroller that
     could not move. */
  body.coach-landing .landing-rail .progress-track {
    overflow: visible;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   TWO INDEPENDENT PANES — the ledger's two-pane, from 1000px wide

   The chat and the plan each own a scroller and neither can move the other —
   `overscroll-behavior: contain` stops a bottomed-out pane handing the gesture
   on. The page itself does not scroll, which is what makes that true at every
   position rather than everywhere except the last screenful. Nothing measures
   page height (streaming.js scrolls whichever element the CSS made the
   scroller), and the composer is a grid ROW rather than a sticky box, so it
   cannot degrade to static the way a sticky grid item did.

   Width alone forks the shell; height picks a density inside it — a short
   window (a landscape phone, a laptop with DevTools docked) keeps two panes
   with the rail's header compacted (progress-track.css), owner's call
   2026-09-17. Below 1000px the one-column shell is page-scrolled: the bounded
   shell at every width was reverted on device (docs/ledger-ui/PROGRESS.md,
   2026-09-06).
   ═══════════════════════════════════════════════════════════════════════════ */
@media (min-width: 1000.02px) {
  /* The BOUND lives on the container, never on <body>. A body's overflow
     propagates to the viewport, and Gecko's async scrolling caches that
     viewport state: set `overflow: clip` + a fixed height on body here and
     remove it on the one-column side, and a window dragged from wide to
     narrow keeps a viewport the wheel cannot move while script still can
     (Zen/Firefox, owner 2026-09-17; Bugzilla 1266833 is the family). So the
     body keeps its one `overflow-x: clip` at every width, and the container
     is what stands exactly one viewport tall and clips what the panes
     overflow. `clip`, not `hidden` — it never becomes a scroll container. */
  body:has(.coach-page-content) > .container {
    /* max-height, not height: the container is a `flex: 1` item, and a
       flex-basis of 0 overrides a set height but is still clamped by max. */
    max-height: calc(100dvh - var(--coach-header-h));
    overflow: clip;
  }

  /* The whole chain is flex-column with `flex: 1`; each link needs a zero min
     so it may shrink below its content, or the panes inherit a content-height
     parent and never bound. */
  body:has(.coach-page-content) > .container,
  body:has(.coach-page-content) .coach-page-content,
  body:has(.coach-page-content) .coach-page-chat,
  body:has(.coach-page-content) #chat-area {
    min-height: 0;
  }

  /* A grid row, not a sticky box — it sits under a scrolling pane rather than
     floating over one, so it needs no stickiness and cannot lose it. */
  body.coach-ledger .coach-inline > .entry-dock {
    grid-column: 1;
    grid-row: 2;
    position: static;
  }

  /* The rail's own box (SPEC §2/§2.4): ground + dashed left rule are the seam. */
  body.coach-ledger .coach-rail--ledger {
    grid-column: 2;
    grid-row: 1 / -1;
    overflow-y: auto;
    overscroll-behavior: contain;
    min-height: 0;
    background: var(--paper-panel);
    border-left: 1.5px dashed var(--paper-rule);
  }

  /* Ledger column (SPEC §2.2): right padding reserves the composer/stamp
     margin; no left padding — the gutter IS the left margin, so 1.4's rows
     supply their own indent via --ledger-gutter/--ledger-gap. */
  body.coach-ledger .message-stream {
    padding-left: calc(var(--ledger-gutter) + var(--ledger-gap));
    padding-right: 28px;
  }

  /* Plain content column — the scroller is the rail itself, one level up.
     Top-anchored on purpose: the footer follows the plan as its sign-off
     instead of pinning to the rail's bottom edge beside the composer. */
  body.coach-ledger .coach-rail__sticky {
    display: flex;
    flex-direction: column;
  }

  body.coach-ledger .coach-rail--ledger .progress-track {
    overflow: visible;
  }

  /* The rail carries the face, Steev's line and the goal track at this width,
     so the band — their one-column twin, filled by the same response out of
     band — is not shown at all. The SHELL is what hides: its face renders from
     first paint, so hiding only the filled body would leave a stray disc. */
  .coach-band {
    display: none;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   ONE COLUMN — up to 1000px wide

   Page-scrolled, and the ledger's aside is hidden: the journey strip rides in
   the conversation instead, in the slot the stream carries (streaming.js moves
   the zone by that display value). The nav keeps its ordinary band — face
   beside the ledger label — so --chrome-h is the nav's height.

   THE CHROME YIELDS TO THE WRITER. With the soft keyboard up, the header band
   and the four labels between the field and the keys are chrome the writer is
   not reading; iOS sheds exactly this line. The signal is `data-kb` on <html>,
   set by viewport.js from the runner's TAP (confirmed by a measured shrink
   where the browser reports one) — never focus: `enableInput()` in streaming.js
   re-focuses the field after EVERY turn, and a programmatic focus raises no
   keyboard on iOS. The nav itself stays pinned, so the day stamp keeps a
   height to clear.

   `body.coach-ledger` keeps all of it off the frozen landing.
   ═══════════════════════════════════════════════════════════════════════════ */
@media (max-width: 1000px) {
  /* Page-scrolled: the ledger is a flex column at every one-column size, not
     the ≥760.02px grid and pane scroller above. */
  body.coach-ledger .coach-inline {
    display: flex;
  }

  body.coach-ledger .coach-inline > .message-stream {
    overflow-y: visible;
  }

  /* One place per width (owner, 2026-09-19): the aside is gone here and the
     zone lives in the stream's slot — streaming.js reads THIS display value. */
  body.coach-ledger .coach-rail--ledger {
    display: none;
  }

  /* Same gutter calc as two-pane — the container query in chat-marginalia.css
     collapses both tokens where the conversation has no room for them. */
  body.coach-ledger .message-stream {
    padding-left: calc(var(--ledger-gutter) + var(--ledger-gap));
  }

  /* ─── The header band — the title toolbar (owner, 2026-09-19) ───
     Steev's line and the goal track belong to the masthead, not to the
     calendar strip. It rides under the nav and pins with it, so the goal bar
     is in view wherever the conversation is. The ink of the track and its two
     labels is the rail forecast's, shared in progress-track.css.

     coach.html renders the shell EMPTY and the strip's response fills it out of
     band, so the resting height is what keeps the conversation from jumping
     when it lands — and an empty band rules off nothing. The gutters and the
     frosted ground are the nav's own, so the two read as one masthead. */
  body.coach-ledger .coach-band {
    position: sticky;
    top: 0;
    z-index: var(--z-nav);
    display: flex;
    align-items: flex-start;
    gap: var(--space-md);
    min-height: var(--coach-header-h);
    margin-inline: calc(-1 * var(--inset-lg));
    /* The kebab and the credit badge float over this band's own top-right, so
       the text column stops short of them. */
    padding: 8px calc(var(--inset-lg) + var(--tap-target)) 8px var(--inset-lg);
    background: color-mix(in srgb, var(--surface) 94%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-bottom: 1.5px dashed var(--paper-rule);
  }

  /* The face seat, the band's own — and the page's only route to Profile now
     that the nav carries no brand. Its disc is the nav blob's, one size up. */
  body.coach-ledger .coach-band__face {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: var(--tap-target);
    min-height: var(--tap-target);
    margin-block: -4px;
  }

  /* The disc the rail's own seat wears, at the band's size — one face rule, not
     a third copy. */
  body.coach-ledger .coach-band__blob {
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    border-radius: var(--radius-blob);
    background: var(--accent-coach-soft);
    box-shadow: var(--shadow-hard-md);
    color: var(--paper-ink-on-coral);
  }

  body.coach-ledger .coach-band__blob .steev-face {
    width: 29px;
    height: 29px;
    /* On the face itself: `.steev-face` sets its own `color`, so ink inherited
       from the disc never reaches it (steev-face.css). The nav's seat states it
       the same way. */
    color: var(--paper-ink-on-coral);
    --mouth-shade: color-mix(in oklab, currentColor 55%, black 45%);
  }

  body.coach-ledger .coach-band__body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
    justify-content: center;
  }

  /* Two columns over the track, bottom-aligned: the aside on the left, the week
     over the goal on the right, so the goal sits above the track's end. */
  body.coach-ledger .coach-band__top {
    display: flex;
    align-items: flex-end;
    gap: var(--space-md);
    min-width: 0;
  }

  body.coach-ledger .coach-band__meta {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    max-width: calc(var(--tap-target) * 3);
  }

  body.coach-ledger .coach-band__week,
  body.coach-ledger .coach-band__target {
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
  }

  body.coach-ledger .coach-band__week {
    color: var(--text-muted);
  }

  body.coach-ledger .coach-band__target {
    color: var(--text-faint);
  }

  /* The nav yields the masthead to the band: a zero-height overlay carrying
     only its right cluster, which floats over the band's reserved top-right.
     A 0px sticky box contributes 0 to --chrome-h (viewport.js counts sticky
     boxes by height), so the band alone is the measured chrome. */
  body.coach-ledger .nav-minimal {
    height: 0;
    overflow: visible;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border-bottom: 0;
    pointer-events: none;
    z-index: calc(var(--z-nav) + 1);
  }

  body.coach-ledger .nav-inner > * {
    pointer-events: auto;
  }

  body.coach-ledger .nav-brand,
  body.coach-ledger .nav-ledger-label,
  body.coach-ledger .nav-items {
    display: none;
  }

  body.coach-ledger .nav-overflow-section,
  body.coach-ledger .nav-overflow-item {
    display: block;
  }

  body.coach-ledger .nav-right {
    margin-left: auto;
  }

  /* Band chrome, not coach voice: sage, like the rail's own mantra. Two lines
     hold an [mantra.ask]-length mantra; the third lets the longest one the server
     keeps ([mantra.limit]) land whole on a 360px phone instead of in an ellipsis. */
  body.coach-ledger .coach-band__mood {
    flex: 1;
    min-width: 0;
    margin: 0;
    font-family: var(--font-annotation);
    font-size: 15px;
    line-height: 1.15;
    color: var(--accent-user-hover);
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  /* The rail stacks its forecast; the band has one line to spend, so the two
     labels take the ends and the track spans what is left. A long target on a
     small phone gives way first — the runner's own forecast is the number that
     must survive. */
  body.coach-ledger .coach-band__goal {
    display: flex;
    flex-direction: column;
    gap: 2px;
  }

  body.coach-ledger .coach-band__track {
    width: 100%;
  }

  /* A revealed entry or poster lands under the pinned nav, not behind it:
     scrollIntoView honours scroll-margin, and --chrome-h is the nav's measured
     height (viewport.js). No fallback on purpose — unresolved, the margin is
     invalid at computed-value time and falls back to 0, which is the honest
     number without the script. */
  body.coach-ledger .message-stream > *,
  body.coach-ledger .message-stream .chat-card {
    scroll-margin-top: calc(var(--chrome-h) + 16px);
  }

  [data-kb] body.coach-ledger .entry-seam,
  [data-kb] body.coach-ledger .coach-band {
    display: none;
  }
}

@media (max-width: 760px) {
  body.coach-landing .landing-rail {
    z-index: 9;
  }
}

/* ═══════════════════════════════════════════════════════════════════════════
   REDUCED MOTION — page-level only; component reduced-motion lives in
   each component file (chat-bubbles, exchange-group, steev-face, etc).

   ONE paper policy, stated here and nowhere else: entrance animations are
   disabled and every interaction-triggered transform REVERTS to the element's
   resting one — `none` where there is no resting tilt, the tilt itself where
   there is. The resting tilts (card, tape, blob, tagline, week bar) STAY: they
   are placement, not motion, and dropping one leaves the rest
   reading more askew than they do with motion on. No `transition` reset is
   needed under that policy — with the transform unchanged there is no
   transform transition left to run, and a colour fade is not motion.
   ═══════════════════════════════════════════════════════════════════════════ */

@media (prefers-reduced-motion: reduce) {
  [data-coach-zone] {
    animation: none;
  }
}

/* =============================================================================
   LEADERBOARD.CSS - Page-level chrome for the leaderboard route

   The board itself is now the card vocabulary (components/card-sections.css);
   what is left here is the bespoke page furniture around it — user summary
   strip, empty / opt-in islands, the stutter divider above race chat — plus
   the public race-detail leaderboard outer frame.
   ============================================================================= */

/* ═══════════════════════════════════════════════════════════════════════════
   EMPTY / OPT-IN STATES
   ═══════════════════════════════════════════════════════════════════════════ */

.leaderboard-opt-in,
.leaderboard-empty-state {
  text-align: center;
  padding: var(--space-xl) var(--inset);
  color: var(--text-secondary);
  border-radius: var(--radius-md);
}
.leaderboard-page [data-zone] + .leaderboard-empty-state,
.public-leaderboard-cta {
  margin-top: var(--space-md);
}
.leaderboard-opt-in p {
  margin-bottom: var(--space-md);
  max-width: 400px;
  margin-left: auto;
  margin-right: auto;
}
.leaderboard-meta {
  margin-top: var(--space-md);
  text-align: center;
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.leaderboard-footer {
  margin-top: var(--space-md);
  padding-top: var(--space-md);
  border-top: 1px solid var(--surface-sunken);
  text-align: center;
}

/* ═══════════════════════════════════════════════════════════════════════════
   LEADERBOARD PAGE STYLES (full-page view only)
   ═══════════════════════════════════════════════════════════════════════════ */

/* User summary — top-of-page identity row */
.leaderboard-user-summary {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  padding: var(--space-sm) 0;
  margin-bottom: var(--space-md);
  border-bottom: 1px solid var(--border);
}
.leaderboard-user-info {
  flex: 1;
  min-width: 0;
}
.leaderboard-user-name {
  font-weight: var(--weight-semibold);
  display: block;
}
.leaderboard-user-best {
  font-size: var(--text-sm);
}

/* Inline metrics layout (page only) */
.leaderboard-metrics-inline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-xs);
  margin-bottom: var(--space-md);
  margin-top: var(--space-md);
}
.metric-divider {
  width: 1px;
  height: 1rem;
  background: var(--border);
  flex-shrink: 0;
}

/* Stutter-rule divider between composition sections */
.leaderboard-section-divider {
  display: flex;
  justify-content: flex-start;
  padding: var(--space-xl) 0;
}

/* ═══════════════════════════════════════════════════════════════════════════
   PUBLIC LEADERBOARD (race detail page outer frame)
   ═══════════════════════════════════════════════════════════════════════════ */

.public-leaderboard {
  margin: var(--space-md) 0;
}

.flex {
  display: flex;
}
.items-center {
  align-items: center;
}
.gap-sm {
  gap: var(--space-sm);
}

.mt-sm {
  margin-top: var(--space-sm);
}
.mt-md {
  margin-top: var(--space-md);
}
.mt-lg {
  margin-top: var(--space-lg);
}
.mb-sm {
  margin-bottom: var(--space-sm);
}
.mb-md {
  margin-bottom: var(--space-md);
}
.mb-lg {
  margin-bottom: var(--space-lg);
}

.text-center {
  text-align: center;
}
.text-sm {
  font-size: var(--text-sm);
}
.text-xs {
  font-size: var(--text-xs);
}

.htmx-indicator {
  display: none;
}
.htmx-request .htmx-indicator {
  display: flex;
}
.htmx-request.htmx-indicator {
  display: flex;
}

.spinner {
  width: 2rem;
  height: 2rem;
  border: 3px solid var(--surface-sunken);
  border-top-color: var(--accent-action);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
.spinner-sm {
  width: 1rem;
  height: 1rem;
  border-width: 2px;
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* =============================================================================
   SCROLL ROW + EDGE FADE — a horizontal scroller and its overflow cue.

   The fade is unconditional, and that is exactly why it needs no JS: a mask over
   an edge that has no content at it is invisible, so it appears when the row
   overflows and vanishes when it does not. A surface that is not a horizontal
   scroller nulls the mask itself.
   ============================================================================= */

.scroll-row {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}

.scroll-row::-webkit-scrollbar {
  display: none;
}

/* A mouse has no swipe: streaming.js drags these rows by scrollLeft, and the
   cursor is the only thing announcing it. Fine pointers only — a finger keeps
   native momentum scrolling and needs no cursor. The clickable children carry
   their own `cursor: pointer`, so `grab` reads on the row's own ground; while a
   drag is live the row overrides them, because nothing in it is clickable then. */
@media (pointer: fine) {
  .scroll-row {
    cursor: grab;
  }
  .scroll-row--dragging,
  .scroll-row--dragging * {
    cursor: grabbing;
    user-select: none;
  }
}

.edge-fade,
.edge-fade--trailing {
  --edge-fade-size: 20px;
  mask-image: linear-gradient(
    to right,
    transparent,
    black var(--edge-fade-size),
    black calc(100% - var(--edge-fade-size)),
    transparent
  );
  -webkit-mask-image: linear-gradient(
    to right,
    transparent,
    black var(--edge-fade-size),
    black calc(100% - var(--edge-fade-size)),
    transparent
  );
}

/* Trailing edge only — for a scroller that always opens at offset 0, where a
   leading fade would permanently dim the first item and read as a bug. */
.edge-fade--trailing {
  mask-image: linear-gradient(
    to right,
    black calc(100% - var(--edge-fade-size)),
    transparent
  );
  -webkit-mask-image: linear-gradient(
    to right,
    black calc(100% - var(--edge-fade-size)),
    transparent
  );
}

/* =============================================================================
   MAIN.CSS - CSS Entry Point

   This file imports all modular CSS files. The order matters:
   1. Foundation - Design tokens, typography, layout primitives
   2. Components - Reusable UI components (alphabetical within block)
   3. Page-specific - Auth, pages, store, public, landing
   4. Feature-specific - Timeline, coach page, leaderboard
   5. Utilities - Utility classes (last for override capability)
   ============================================================================= */

/* ─── Foundation ─── */

/* ─── Components (alphabetical) ─── */

/* ─── Page-specific ─── */

/* ─── Feature-specific ─── */

/* ─── Utilities (last so they can override) ─── */
