/* Groove v1 stylesheet (ADR-0006): Harmony tokens, plain CSS, no build step.
   The ramp + data-accent region between the GENERATED markers is emitted by
   scripts/generate-ramps.ts and committed — the file you edit is the file served.
   Layers: tokens (ramps + semantic slots) → base → components → pages.
   Component blocks are ordered to match the files in src/html/primitives/,
   then src/html/components/, then the route views. */

@layer tokens, base, components, pages;

@layer tokens {
	/* BEGIN GENERATED RAMPS — do not hand-edit. `pnpm generate:ramps` (scripts/generate-ramps.ts) */

	/* Harmony ramps — canonical OKLCH from scripts/harmony-source.json, 12 curated accent hues. */

	:root {
		--sand-50: oklch(0.988281 0.0104375 75);
		--sand-100: oklch(0.964844 0.012 75);
		--sand-200: oklch(0.917969 0.012 75);
		--sand-300: oklch(0.853516 0.012 75);
		--sand-400: oklch(0.789063 0.012 75);
		--sand-500: oklch(0.726563 0.012 75);
		--sand-600: oklch(0.613281 0.012 75);
		--sand-700: oklch(0.523438 0.012 75);
		--sand-800: oklch(0.412109 0.012 75);
		--sand-900: oklch(0.302734 0.012 75);
		--sand-950: oklch(0.193359 0.012 75);
	}

	:root {
		--red-50: oklch(0.988281 0.0046875 20);
		--red-100: oklch(0.966797 0.0171875 20);
		--red-200: oklch(0.921875 0.0421875 20);
		--red-300: oklch(0.861328 0.078125 20);
		--red-400: oklch(0.800781 0.114063 20);
		--red-500: oklch(0.742188 0.151562 20);
		--red-600: oklch(0.626953 0.135937 20);
		--red-700: oklch(0.535156 0.115625 20);
		--red-800: oklch(0.419922 0.090625 20);
		--red-900: oklch(0.306641 0.065625 20);
		--red-950: oklch(0.193359 0.040625 20);
	}

	:root {
		--orange-50: oklch(0.988281 0.0046875 43.3333);
		--orange-100: oklch(0.966797 0.0171875 43.3333);
		--orange-200: oklch(0.921875 0.0421875 43.3333);
		--orange-300: oklch(0.859375 0.078125 43.3333);
		--orange-400: oklch(0.798828 0.114063 43.3333);
		--orange-500: oklch(0.738281 0.151562 43.3333);
		--orange-600: oklch(0.623047 0.135937 43.3333);
		--orange-700: oklch(0.53125 0.115625 43.3333);
		--orange-800: oklch(0.417969 0.090625 43.3333);
		--orange-900: oklch(0.304688 0.065625 43.3333);
		--orange-950: oklch(0.191406 0.040625 43.3333);
	}

	:root {
		--amber-50: oklch(0.988281 0.0046875 66.6667);
		--amber-100: oklch(0.966797 0.0171875 66.6667);
		--amber-200: oklch(0.919922 0.0421875 66.6667);
		--amber-300: oklch(0.857422 0.078125 66.6667);
		--amber-400: oklch(0.794922 0.114063 66.6667);
		--amber-500: oklch(0.732422 0.151562 66.6667);
		--amber-600: oklch(0.619141 0.135937 66.6667);
		--amber-700: oklch(0.527344 0.115625 66.6667);
		--amber-800: oklch(0.414063 0.090625 66.6667);
		--amber-900: oklch(0.302734 0.065625 66.6667);
		--amber-950: oklch(0.191406 0.040625 66.6667);
	}

	:root {
		--green-50: oklch(0.988281 0.0046875 123.333);
		--green-100: oklch(0.962891 0.0171875 123.333);
		--green-200: oklch(0.914063 0.0421875 123.333);
		--green-300: oklch(0.847656 0.078125 123.333);
		--green-400: oklch(0.78125 0.114063 123.333);
		--green-500: oklch(0.714844 0.151562 123.333);
		--green-600: oklch(0.603516 0.135937 123.333);
		--green-700: oklch(0.515625 0.115625 123.333);
		--green-800: oklch(0.404297 0.090625 123.333);
		--green-900: oklch(0.296875 0.065625 123.333);
		--green-950: oklch(0.1875 0.040625 123.333);
	}

	:root {
		--teal-50: oklch(0.988281 0.0046875 160);
		--teal-100: oklch(0.962891 0.0171875 160);
		--teal-200: oklch(0.912109 0.0421875 160);
		--teal-300: oklch(0.84375 0.078125 160);
		--teal-400: oklch(0.775391 0.114063 160);
		--teal-500: oklch(0.705078 0.151562 160);
		--teal-600: oklch(0.59375 0.135937 160);
		--teal-700: oklch(0.507813 0.115625 160);
		--teal-800: oklch(0.398438 0.090625 160);
		--teal-900: oklch(0.291016 0.065625 160);
		--teal-950: oklch(0.183594 0.040625 160);
	}

	:root {
		--sky-50: oklch(0.988281 0.0046875 210);
		--sky-100: oklch(0.962891 0.0171875 210);
		--sky-200: oklch(0.914063 0.0421875 210);
		--sky-300: oklch(0.845703 0.078125 210);
		--sky-400: oklch(0.777344 0.114063 210);
		--sky-500: oklch(0.708984 0.151562 210);
		--sky-600: oklch(0.595703 0.135937 210);
		--sky-700: oklch(0.507813 0.115625 210);
		--sky-800: oklch(0.398438 0.090625 210);
		--sky-900: oklch(0.291016 0.065625 210);
		--sky-950: oklch(0.181641 0.040625 210);
	}

	:root {
		--blue-50: oklch(0.988281 0.0046875 240);
		--blue-100: oklch(0.964844 0.0171875 240);
		--blue-200: oklch(0.916016 0.0421875 240);
		--blue-300: oklch(0.849609 0.078125 240);
		--blue-400: oklch(0.783203 0.114063 240);
		--blue-500: oklch(0.71875 0.151562 240);
		--blue-600: oklch(0.605469 0.135937 240);
		--blue-700: oklch(0.517578 0.115625 240);
		--blue-800: oklch(0.40625 0.090625 240);
		--blue-900: oklch(0.296875 0.065625 240);
		--blue-950: oklch(0.1875 0.040625 240);
	}

	:root {
		--indigo-50: oklch(0.988281 0.0046875 260);
		--indigo-100: oklch(0.964844 0.0171875 260);
		--indigo-200: oklch(0.917969 0.0421875 260);
		--indigo-300: oklch(0.853516 0.078125 260);
		--indigo-400: oklch(0.789063 0.114063 260);
		--indigo-500: oklch(0.726563 0.151562 260);
		--indigo-600: oklch(0.613281 0.135937 260);
		--indigo-700: oklch(0.523438 0.115625 260);
		--indigo-800: oklch(0.412109 0.090625 260);
		--indigo-900: oklch(0.300781 0.065625 260);
		--indigo-950: oklch(0.191406 0.040625 260);
	}

	:root {
		--violet-50: oklch(0.988281 0.0046875 280);
		--violet-100: oklch(0.964844 0.0171875 280);
		--violet-200: oklch(0.919922 0.0421875 280);
		--violet-300: oklch(0.857422 0.078125 280);
		--violet-400: oklch(0.794922 0.114063 280);
		--violet-500: oklch(0.734375 0.151562 280);
		--violet-600: oklch(0.619141 0.135937 280);
		--violet-700: oklch(0.529297 0.115625 280);
		--violet-800: oklch(0.416016 0.090625 280);
		--violet-900: oklch(0.304688 0.065625 280);
		--violet-950: oklch(0.193359 0.040625 280);
	}

	:root {
		--fuchsia-50: oklch(0.988281 0.0046875 320);
		--fuchsia-100: oklch(0.966797 0.0171875 320);
		--fuchsia-200: oklch(0.921875 0.0421875 320);
		--fuchsia-300: oklch(0.861328 0.078125 320);
		--fuchsia-400: oklch(0.800781 0.114063 320);
		--fuchsia-500: oklch(0.742188 0.151562 320);
		--fuchsia-600: oklch(0.626953 0.135937 320);
		--fuchsia-700: oklch(0.535156 0.115625 320);
		--fuchsia-800: oklch(0.419922 0.090625 320);
		--fuchsia-900: oklch(0.308594 0.065625 320);
		--fuchsia-950: oklch(0.195313 0.040625 320);
	}

	:root {
		--pink-50: oklch(0.988281 0.0046875 340);
		--pink-100: oklch(0.966797 0.0171875 340);
		--pink-200: oklch(0.923828 0.0421875 340);
		--pink-300: oklch(0.863281 0.078125 340);
		--pink-400: oklch(0.802734 0.114063 340);
		--pink-500: oklch(0.744141 0.151562 340);
		--pink-600: oklch(0.628906 0.135937 340);
		--pink-700: oklch(0.537109 0.115625 340);
		--pink-800: oklch(0.419922 0.090625 340);
		--pink-900: oklch(0.308594 0.065625 340);
		--pink-950: oklch(0.195313 0.040625 340);
	}

	/* Hue aliases: --hue-{step} is the page's ramp. No data-accent = sand (the backstage); the Worker renders data-accent="{hue}" on an act's pages, and an element showing a hue on a Sand page (the settings swatch, the top-bar dot, the share card) carries data-hue="{hue}". */

	:root {
		--hue-50: var(--sand-50);
		--hue-100: var(--sand-100);
		--hue-200: var(--sand-200);
		--hue-300: var(--sand-300);
		--hue-400: var(--sand-400);
		--hue-500: var(--sand-500);
		--hue-600: var(--sand-600);
		--hue-700: var(--sand-700);
		--hue-800: var(--sand-800);
		--hue-900: var(--sand-900);
		--hue-950: var(--sand-950);
	}

	:root[data-accent="sand"], [data-hue="sand"] {
		--hue-50: var(--sand-50);
		--hue-100: var(--sand-100);
		--hue-200: var(--sand-200);
		--hue-300: var(--sand-300);
		--hue-400: var(--sand-400);
		--hue-500: var(--sand-500);
		--hue-600: var(--sand-600);
		--hue-700: var(--sand-700);
		--hue-800: var(--sand-800);
		--hue-900: var(--sand-900);
		--hue-950: var(--sand-950);
	}

	:root[data-accent="red"], [data-hue="red"] {
		--hue-50: var(--red-50);
		--hue-100: var(--red-100);
		--hue-200: var(--red-200);
		--hue-300: var(--red-300);
		--hue-400: var(--red-400);
		--hue-500: var(--red-500);
		--hue-600: var(--red-600);
		--hue-700: var(--red-700);
		--hue-800: var(--red-800);
		--hue-900: var(--red-900);
		--hue-950: var(--red-950);
	}

	:root[data-accent="orange"], [data-hue="orange"] {
		--hue-50: var(--orange-50);
		--hue-100: var(--orange-100);
		--hue-200: var(--orange-200);
		--hue-300: var(--orange-300);
		--hue-400: var(--orange-400);
		--hue-500: var(--orange-500);
		--hue-600: var(--orange-600);
		--hue-700: var(--orange-700);
		--hue-800: var(--orange-800);
		--hue-900: var(--orange-900);
		--hue-950: var(--orange-950);
	}

	:root[data-accent="amber"], [data-hue="amber"] {
		--hue-50: var(--amber-50);
		--hue-100: var(--amber-100);
		--hue-200: var(--amber-200);
		--hue-300: var(--amber-300);
		--hue-400: var(--amber-400);
		--hue-500: var(--amber-500);
		--hue-600: var(--amber-600);
		--hue-700: var(--amber-700);
		--hue-800: var(--amber-800);
		--hue-900: var(--amber-900);
		--hue-950: var(--amber-950);
	}

	:root[data-accent="green"], [data-hue="green"] {
		--hue-50: var(--green-50);
		--hue-100: var(--green-100);
		--hue-200: var(--green-200);
		--hue-300: var(--green-300);
		--hue-400: var(--green-400);
		--hue-500: var(--green-500);
		--hue-600: var(--green-600);
		--hue-700: var(--green-700);
		--hue-800: var(--green-800);
		--hue-900: var(--green-900);
		--hue-950: var(--green-950);
	}

	:root[data-accent="teal"], [data-hue="teal"] {
		--hue-50: var(--teal-50);
		--hue-100: var(--teal-100);
		--hue-200: var(--teal-200);
		--hue-300: var(--teal-300);
		--hue-400: var(--teal-400);
		--hue-500: var(--teal-500);
		--hue-600: var(--teal-600);
		--hue-700: var(--teal-700);
		--hue-800: var(--teal-800);
		--hue-900: var(--teal-900);
		--hue-950: var(--teal-950);
	}

	:root[data-accent="sky"], [data-hue="sky"] {
		--hue-50: var(--sky-50);
		--hue-100: var(--sky-100);
		--hue-200: var(--sky-200);
		--hue-300: var(--sky-300);
		--hue-400: var(--sky-400);
		--hue-500: var(--sky-500);
		--hue-600: var(--sky-600);
		--hue-700: var(--sky-700);
		--hue-800: var(--sky-800);
		--hue-900: var(--sky-900);
		--hue-950: var(--sky-950);
	}

	:root[data-accent="blue"], [data-hue="blue"] {
		--hue-50: var(--blue-50);
		--hue-100: var(--blue-100);
		--hue-200: var(--blue-200);
		--hue-300: var(--blue-300);
		--hue-400: var(--blue-400);
		--hue-500: var(--blue-500);
		--hue-600: var(--blue-600);
		--hue-700: var(--blue-700);
		--hue-800: var(--blue-800);
		--hue-900: var(--blue-900);
		--hue-950: var(--blue-950);
	}

	:root[data-accent="indigo"], [data-hue="indigo"] {
		--hue-50: var(--indigo-50);
		--hue-100: var(--indigo-100);
		--hue-200: var(--indigo-200);
		--hue-300: var(--indigo-300);
		--hue-400: var(--indigo-400);
		--hue-500: var(--indigo-500);
		--hue-600: var(--indigo-600);
		--hue-700: var(--indigo-700);
		--hue-800: var(--indigo-800);
		--hue-900: var(--indigo-900);
		--hue-950: var(--indigo-950);
	}

	:root[data-accent="violet"], [data-hue="violet"] {
		--hue-50: var(--violet-50);
		--hue-100: var(--violet-100);
		--hue-200: var(--violet-200);
		--hue-300: var(--violet-300);
		--hue-400: var(--violet-400);
		--hue-500: var(--violet-500);
		--hue-600: var(--violet-600);
		--hue-700: var(--violet-700);
		--hue-800: var(--violet-800);
		--hue-900: var(--violet-900);
		--hue-950: var(--violet-950);
	}

	:root[data-accent="fuchsia"], [data-hue="fuchsia"] {
		--hue-50: var(--fuchsia-50);
		--hue-100: var(--fuchsia-100);
		--hue-200: var(--fuchsia-200);
		--hue-300: var(--fuchsia-300);
		--hue-400: var(--fuchsia-400);
		--hue-500: var(--fuchsia-500);
		--hue-600: var(--fuchsia-600);
		--hue-700: var(--fuchsia-700);
		--hue-800: var(--fuchsia-800);
		--hue-900: var(--fuchsia-900);
		--hue-950: var(--fuchsia-950);
	}

	:root[data-accent="pink"], [data-hue="pink"] {
		--hue-50: var(--pink-50);
		--hue-100: var(--pink-100);
		--hue-200: var(--pink-200);
		--hue-300: var(--pink-300);
		--hue-400: var(--pink-400);
		--hue-500: var(--pink-500);
		--hue-600: var(--pink-600);
		--hue-700: var(--pink-700);
		--hue-800: var(--pink-800);
		--hue-900: var(--pink-900);
		--hue-950: var(--pink-950);
	}

	/* Semantic slots, written once per scheme against the aliases. An act page carries data-theme (the act's choice); the backstage has none and follows the viewer's OS. A [data-hue] element re-derives them from its own ramp, so it renders as a piece of that act's page. */

	:root, [data-hue] {
		color-scheme: light;
		--surface: var(--hue-50);
		--surface-raised: white;
		--surface-sunken: var(--hue-100);
		--surface-strong: var(--hue-200);
		--ink: var(--hue-950);
		--ink-muted: var(--hue-800);
		--line: var(--hue-200);
		--line-strong: var(--hue-600);
		--accent: var(--hue-800);
		--accent-text: var(--hue-700);
		--accent-contrast: var(--hue-100);
		--accent-hover: var(--hue-900);
		--accent-active: var(--hue-950);
		--accent-line: var(--hue-300);
		--accent-vivid: var(--hue-600);
		--shade: var(--hue-950);
	}

	@media (prefers-color-scheme: dark) {
		:root:not([data-theme="light"]), :root:not([data-theme="light"]) [data-hue] {
			color-scheme: dark;
			--surface: var(--hue-950);
			--surface-raised: black;
			--surface-sunken: var(--hue-800);
			--surface-strong: var(--hue-700);
			--ink: var(--hue-50);
			--ink-muted: var(--hue-200);
			--line: var(--hue-800);
			--line-strong: var(--hue-700);
			--accent: var(--hue-200);
			--accent-text: var(--hue-300);
			--accent-contrast: var(--hue-900);
			--accent-hover: var(--hue-100);
			--accent-active: var(--hue-50);
			--accent-line: var(--hue-700);
			--accent-vivid: var(--hue-600);
			--shade: var(--hue-950);
		}
	}

	:root[data-theme="dark"], :root[data-theme="dark"] [data-hue] {
		color-scheme: dark;
		--surface: var(--hue-950);
		--surface-raised: black;
		--surface-sunken: var(--hue-800);
		--surface-strong: var(--hue-700);
		--ink: var(--hue-50);
		--ink-muted: var(--hue-200);
		--line: var(--hue-800);
		--line-strong: var(--hue-700);
		--accent: var(--hue-200);
		--accent-text: var(--hue-300);
		--accent-contrast: var(--hue-900);
		--accent-hover: var(--hue-100);
		--accent-active: var(--hue-50);
		--accent-line: var(--hue-700);
		--accent-vivid: var(--hue-600);
		--shade: var(--hue-950);
	}

	/* END GENERATED RAMPS */

	/* Built from the slots, so declared wherever the slots are: a [data-hue]
	   element re-derives them, and its own ink, rather than inheriting
	   :root's computed values */
	:root,
	[data-hue] {
		/* Shadow only under things that float over content */
		--shadow-1: 0 1px 2px color-mix(in oklab, var(--shade) 8%, transparent);
		--shadow-2: 0 2px 8px color-mix(in oklab, var(--shade) 12%, transparent);
		--focus-ring: 2px solid var(--accent-text);
	}

	[data-hue] {
		color: var(--ink);
	}

	/* Status hue (ADR-0006 amendment, #226): Red never follows the act hue —
	   danger must stay visible on a Red-themed act, and is never carried by hue
	   alone. Danger is a form, not a text colour: a red-600 stripe, border or
	   marker beside copy set in ink. red-600 is the most saturated step that
	   clears 3:1 non-text on both grounds; no --ok slot — success is a
	   checkmark in ink. */
	:root {
		--danger: var(--red-600);
		--danger-tint: var(--red-100);

		/* Rhythm (ADR-0006 #276): leading is the 24px beat, type a major second (1.125) */
		--font-sans: system-ui, -apple-system, 'Segoe UI', sans-serif;
		/* One step below sm, for caps labels only (badges, #280) */
		--text-xs: 0.79rem;
		--text-sm: 0.889rem;
		--text-base: 1rem;
		--text-lg: 1.125rem;
		--text-xl: 1.266rem;
		--text-2xl: 1.424rem;
		/* --text-display: reserved for #228's display face, no value yet */
		--weight-regular: 400;
		--weight-medium: 500;
		--weight-strong: 600;
		--leading-body: 1.5;
		/* Soft scale: 20 stays as the compact row step; 32/40 were off-beat */
		--space-1: 4px;
		--space-2: 8px;
		--space-3: 12px;
		--space-4: 16px;
		--space-5: 20px;
		--space-6: 24px;
		--space-7: 36px;
		--space-8: 48px;
		/* One column for every page, backstage and client: rem, not ch, so the
		   bands' smaller text resolves to the same width as the body. 44rem is
		   the client's 66ch at its 18px desktop base */
		--page-width: 44rem;
		/* Inline padding that puts a full-bleed band's content on that column */
		--page-inset: max(
			var(--space-5),
			calc((100% - var(--page-width)) / 2 + var(--space-5))
		);

		/* Radius follows the size of the box, and inset is never less than it */
		--radius-xs: 4px;
		--radius-sm: 6px;
		--radius-md: 8px;
		--radius-lg: 16px;
		--radius-full: 9999px;
		--border-width: 1px;
		--focus-offset: 2px;

		/* Motion sits under prefers-reduced-motion; --duration-notice is a dwell, not motion */
		--duration-fast: 150ms;
		--duration-base: 300ms;
		--duration-slow: 1000ms;
		--duration-notice: 5s;
		--ease: ease-out;

		/* raised: shop header · sticky: save bar · overlay: menus, popovers, the skip link */
		--z-raised: 1;
		--z-sticky: 2;
		--z-overlay: 3;

		/* The client's touch floor, and 2.5.8's floor for desktop-only controls */
		--target-touch: 44px;
		--target-min: 24px;

		/* Breakpoint 720px: var() can't reach @media, so each query is marked bp-desktop */
	}

	/* Client base: 16px, 18px from the breakpoint; 18 × 4/3 keeps the 24px line */
	@media (min-width: 720px) { /* bp-desktop 720px */
		:root[data-accent] {
			font-size: 18px;
			--leading-body: calc(4 / 3);
		}
	}

	/* Dark mirrors the generated slot blocks: an act page's data-theme wins over the OS. */
	@media (prefers-color-scheme: dark) {
		:root:not([data-theme="light"]) {
			--danger-tint: var(--red-900);
		}
	}

	:root[data-theme="dark"] {
		--danger-tint: var(--red-900);
	}
}

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

	body {
		margin: 0;
		font-family: var(--font-sans);
		font-size: var(--text-base);
		line-height: var(--leading-body);
		color: var(--ink);
		background: var(--surface);
	}

	/* Rhythm comes from the space scale, not UA em-margins; headings take
	   one beat of line (24px) instead of 1.5× their size, so they sit on
	   the grid the body sets */
	h1 {
		font-size: var(--text-xl);
		font-weight: var(--weight-strong);
		line-height: calc(var(--leading-body) * 1rem);
		margin: 0 0 var(--space-6);
	}

	h2 {
		font-size: var(--text-lg);
		font-weight: var(--weight-strong);
		line-height: calc(var(--leading-body) * 1rem);
		margin: 0 0 var(--space-3);
	}

	p {
		margin-top: 0;
	}

	/* Every list here is component layout, never prose: one reset */
	:where(ul, ol) {
		margin: 0;
		padding: 0;
		list-style: none;
	}

	a {
		color: var(--accent-text);
	}

	/* UA controls don't inherit the page's type */
	button,
	input,
	textarea,
	select {
		font: inherit;
	}

	/* iOS Safari zooms the page in whenever a focused control computes under
	   16px — and never zooms back out, so the fields a form inherits
	   --text-sm into (the details forms) end up off the right edge. The floor
	   goes on the control; `maximum-scale` would fix it by failing 1.4.4.
	   max-width keeps a control that sizes itself — a date input — inside its
	   column (1.4.10) */
	input,
	textarea,
	select {
		font-size: max(1rem, 16px);
		max-width: 100%;
	}

	/* A textarea is an input (#276): the same recipe, not the UA border */
	input,
	textarea {
		color: var(--ink);
		background: var(--surface-raised);
		border: var(--border-width) solid var(--line-strong);
		border-radius: var(--radius-sm);
		padding: var(--space-2) var(--space-3);
	}

	/* Typed fields are the touch floor; a checkbox or radio is a glyph in
	   its label, which carries the target */
	input:not([type='checkbox'], [type='radio']),
	textarea {
		min-height: var(--target-touch);
	}

	/* A textarea shows three lines, on the grid as the input is: the input's
	   beat plus its chrome, then two more beats */
	textarea {
		min-height: calc(var(--target-touch) + 2 * (var(--leading-body) * 1rem));
	}

	/* iOS Safari sizes a native date input to itself, overflowing a phone (#307) */
	input[type='date'] {
		min-width: 0;
		-webkit-appearance: none;
		appearance: none;
	}

	/* Without the native look an empty value collapses: it keeps a line */
	input[type='date']::-webkit-date-and-time-value {
		min-height: 1lh;
		text-align: start;
	}

	input::placeholder,
	textarea::placeholder {
		color: var(--ink-muted);
		font-style: italic;
	}

	:focus-visible {
		outline: var(--focus-ring);
		outline-offset: var(--focus-offset);
	}

	/* The block padding makes every button the touch floor tall at any
	   size of text, its label centred, with no min-height to fight */
	.button {
		font-weight: var(--weight-medium);
		border: var(--border-width) solid transparent;
		border-radius: var(--radius-sm);
		padding: calc((var(--target-touch) - 1lh) / 2 - var(--border-width)) var(--space-4);
		cursor: pointer;
	}

	/* A link wearing the button's clothes (the shop front's "Your setlist",
	   the setlist page's browse links): the variant sets its colour */
	a.button {
		display: inline-block;
		text-decoration: none;
	}

	.button--primary {
		background: var(--accent);
		color: var(--accent-contrast);
	}

	.button--primary:hover {
		background: var(--accent-hover);
	}

	.button--primary:active {
		background: var(--accent-active);
	}

	.button--secondary {
		background: var(--accent-contrast);
		color: var(--accent-text);
		border-color: var(--accent-line);
	}

	/* Border means pressable (#276): pressing deepens the edge, the fill stays */
	.button--secondary:hover {
		border-color: var(--accent-text);
	}

	.button--secondary:active {
		border-color: var(--accent);
	}

	/* Sand is neutral, so its accent text is a grey that reads as disabled
	   on a secondary button: there the label takes the ink. A hued piece
	   (the share card's data-hue) keeps its act's colour */
	:root:not([data-accent]) .button--secondary,
	[data-accent='sand'] .button--secondary {
		color: var(--ink);
	}

	[data-hue]:not([data-hue='sand']) .button--secondary {
		color: var(--accent-text);
	}

	/* The quiet voice: per-row actions that shouldn't compete with the
	   content they sit beside (matches the popover summary's weight) */
	.button--quiet {
		background: none;
		color: var(--ink-muted);
		font-size: var(--text-sm);
		padding-inline: var(--space-2);
	}

	.button--quiet:hover {
		color: var(--ink);
	}

	.button--danger {
		background: transparent;
		color: var(--ink);
		border-color: var(--danger);
	}

	.button--danger:hover,
	.button--danger:active {
		background: var(--danger-tint);
	}

	/* Off-screen but announced — live-region text and the like */
	.visually-hidden {
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip-path: inset(50%);
		white-space: nowrap;
	}

	/* Off-screen until focused, then over the sticky chrome it skips (2.4.1) */
	.skip-link {
		position: absolute;
		inset-block-start: var(--space-2);
		inset-inline-start: var(--space-2);
		z-index: var(--z-overlay);
		padding: var(--space-2) var(--space-4);
		background: var(--surface-raised);
		border-radius: var(--radius-sm);
		box-shadow: var(--shadow-2);
		transform: translateY(-200%);
	}

	.skip-link:focus {
		transform: none;
	}

	/* !important outranks the components layer's cursor: pointer (e.g. .move) */
	:disabled,
	[aria-disabled='true'] {
		cursor: not-allowed !important;
		opacity: 0.45;
	}

	/* htmx's own indicator CSS is off (document.tsx): unlayered, it would beat this layer */
	.htmx-request {
		cursor: progress;
	}

	.htmx-indicator {
		visibility: hidden;
	}

	.htmx-request .htmx-indicator,
	.htmx-request.htmx-indicator {
		visibility: visible;
	}

	/* Dim only past a beat, so quick swaps don't flicker */
	@media (prefers-reduced-motion: no-preference) {
		:is(button, .button).htmx-request {
			animation: pending var(--duration-fast) var(--ease) var(--duration-base)
				forwards;
		}
	}

	@keyframes pending {
		to {
			opacity: 0.6;
		}
	}

	/* ADR-0006 settle: ~150ms opacity on the small swaps listed here, a row
	   or count swapped on its own. A swapped list or form never fades */
	@media (prefers-reduced-motion: no-preference) {
		:is(.row, #typed-row, #songs-count, #request-hint).htmx-settling {
			animation: settle var(--duration-fast) var(--ease);
		}
	}

	@keyframes settle {
		from {
			opacity: 0.4;
		}
		to {
			opacity: 1;
		}
	}
}

@layer components {
	/* layout */
	/* Phone: act + View live on row 1, tabs on row 2. Desktop: one row.
	   DOM order is act → tabs → View live; grid areas place them */
	.topbar {
		display: grid;
		grid-template-columns: minmax(0, 1fr) auto;
		grid-template-areas:
			'act live'
			'tabs tabs';
		/* View live is a size smaller: share the words' baseline, not the box's foot */
		align-items: last baseline;
		column-gap: var(--space-4);
		/* More below than above: the current mark needs air off the bar's edge */
		padding-block: var(--space-1) var(--space-3);
		padding-inline: var(--page-inset);
		background: var(--surface-raised);
		border-bottom: var(--border-width) solid var(--line);
	}

	@media (min-width: 720px) { /* bp-desktop 720px */
		.topbar {
			grid-template-columns: auto minmax(0, 1fr) auto;
			grid-template-areas: 'act tabs live';
			column-gap: var(--space-6);
		}
	}

	.tabs {
		grid-area: tabs;
		display: flex;
		gap: var(--space-4);
	}

	/* A 44px target with the text at its foot, so the current mark sits under
	   the word, not at the bottom of the box */
	.tabs a,
	.act-switcher > summary,
	.view-live {
		display: flex;
		/* At the foot, on one baseline: the act's dot keeps sitting on it */
		align-items: last baseline;
		min-height: var(--target-touch);
		padding-bottom: var(--space-1);
		/* Transparent by default so the current border doesn't shift baselines */
		border-bottom: 2px solid transparent;
	}

	.tabs a {
		color: var(--ink-muted);
		text-decoration: none;
	}

	.tabs a:hover {
		color: var(--ink);
	}

	/* On Settings no tab is current: the act menu that holds it carries the mark */
	.tabs a[aria-current='page'],
	.act-switcher:has(a[aria-current='page']) > summary {
		border-bottom-color: var(--accent-vivid);
	}

	.tabs a[aria-current='page'] {
		color: var(--accent-text);
	}

	/* High contrast shows transparent borders, so every tab would read as current */
	@media (forced-colors: active) {
		.tabs a:not([aria-current='page']),
		.act-switcher:not(:has(a[aria-current='page'])) > summary,
		.view-live {
			border-bottom-color: Canvas;
		}
	}

	/* act-switcher: a menu Popover — the act, then the account */
	.act-switcher {
		grid-area: act;
		justify-self: start;
		min-width: 0;
	}

	.act-switcher > summary::after {
		margin-inline-start: var(--space-1);
	}

	.act-switcher li {
		padding: var(--space-2) var(--space-3);
		font-weight: var(--weight-regular);
		/* Non-current acts aren't actionable in v1 — read as such */
		color: var(--ink-muted);
	}

	.act-switcher__current {
		font-weight: var(--weight-strong);
		color: var(--ink);
	}

	.act-switcher__soon {
		margin: 0;
		padding: var(--space-2) var(--space-3);
		color: var(--ink);
		font-size: var(--text-sm);
		font-weight: var(--weight-regular);
	}

	.view-live {
		grid-area: live;
		font-size: var(--text-sm);
		text-decoration: none;
	}

	.view-live:hover {
		text-decoration: underline;
	}

	/* form-errors */
	.form-errors {
		margin: 0 0 var(--space-2);
		padding: var(--space-2) var(--space-3);
		color: var(--ink);
		border-left: 4px solid var(--danger);
		font-size: var(--text-sm);
	}

	/* Only script focuses this (a 422 naming no field); :focus-visible skips that after a click */
	.form-errors-focus:focus {
		outline: var(--focus-ring);
		outline-offset: var(--focus-offset);
	}

	/* no-data */
	.no-data {
		text-align: center;
		color: var(--ink-muted);
	}

	/* popover (#279): pops over instead of reflowing what it sits in.
	   Default is the anchored two-step confirm, end-aligned to its summary */
	.popover {
		position: relative;
	}

	.popover > summary {
		cursor: pointer;
		color: var(--ink-muted);
		font-size: var(--text-sm);
	}

	.popover[open] > summary {
		color: var(--ink);
	}

	.popover__panel {
		position: absolute;
		right: 0;
		top: 100%;
		z-index: var(--z-overlay);
		/* Sized to its content, not the summary it hangs from, and never past
		   the page gutter at 320px (1.4.10): a long label wraps instead */
		width: max-content;
		max-width: calc(100vw - 2 * var(--space-5));
		margin-top: var(--space-1);
	}

	/* Shadow earns its place here: this is functional layering, not decoration */
	.popover__panel > .button {
		box-shadow: var(--shadow-2);
	}

	/* The menu: a dropdown under a full-weight trigger, start-aligned */
	.popover--menu {
		font-weight: var(--weight-strong);
	}

	.popover--menu > summary {
		color: inherit;
		font-size: inherit;
		list-style: none;
	}

	.popover--menu > summary::after {
		content: ' ▾';
		color: var(--ink-muted);
	}

	.popover--menu > .popover__panel {
		right: auto;
		min-width: 220px;
		margin-top: var(--space-2);
		width: auto;
		padding: var(--space-2);
		background: var(--surface-raised);
		border: var(--border-width) solid var(--line);
		border-radius: var(--radius-md);
		box-shadow: var(--shadow-2);
	}

	/* A menu popover's groups (MenuGroup), ruled apart */
	.popover__group {
		margin: var(--space-2) 0 0;
		padding-top: var(--space-2);
		border-top: var(--border-width) solid var(--line);
	}

	/* A menu row, link or button alike: full width, a touch target, no edge */
	.popover__group :is(a, button) {
		display: flex;
		align-items: center;
		width: 100%;
		min-height: var(--target-touch);
		padding: 0 var(--space-3);
		color: var(--ink);
		background: none;
		border: 0;
		border-radius: var(--radius-sm);
		font-weight: var(--weight-regular);
		text-align: start;
		text-decoration: none;
		cursor: pointer;
	}

	.popover__group :is(a, button):is(:hover, :focus-visible) {
		background: var(--surface-sunken);
	}

	.popover__group a[aria-current='page'] {
		color: var(--accent-text);
		font-weight: var(--weight-strong);
	}

	/* badge (#280): state, never pressable — filled, caps one step down so it
	   reads as a label beside sentence-case chips; always a <span> (Badge) */
	.badge {
		font-size: var(--text-xs);
		font-weight: var(--weight-medium);
		text-transform: uppercase;
		color: var(--ink);
		background: var(--surface-strong);
		/* Stands in for the dropped border, so badges keep their height */
		padding: var(--border-width) var(--space-2);
		border-radius: var(--radius-xs);
		/* Its own leading, not the beat a line it sits on sets: a badge on a
		   beat line would otherwise push the line box past the beat */
		line-height: var(--leading-body);
	}

	/* The lock is a flag, not a lifecycle state (ADR-0010): inverted, as it changes what holders can do */
	.badge--locked {
		color: var(--accent-contrast);
		background: var(--accent);
	}

	/* New (ADR-0015): the read view's mark on what this visit added */
	.badge--new {
		color: var(--accent-text);
		background: var(--accent-contrast);
		box-shadow: inset 0 0 0 var(--border-width) var(--accent-line);
	}

	/* High contrast drops the fill, so a border keeps the pill */
	@media (forced-colors: active) {
		.badge {
			padding-block: 0;
			border: var(--border-width) solid CanvasText;
		}
	}

	/* row (#280): the one list row — a flex line on the beat with a bottom
	   rule; slots are the row's children, the text block is two lines so
	   trailing controls never squeeze the title (#191) */
	.row {
		display: flex;
		align-items: center;
		gap: var(--space-3);
		padding: var(--space-3) 0;
		border-bottom: var(--border-width) solid var(--line);
	}

	[data-sortable] .row {
		cursor: grab;
	}

	/* Position gutter leads; the arrows sit trailing with Remove — #191's
	   recorded fallback, taken because the stacked column doubled row height */
	.row__index {
		flex: none;
		min-width: 2ch;
		text-align: right;
		color: var(--ink-muted);
		font-size: var(--text-sm);
	}

	.row__text {
		display: grid;
		flex: 1;
		min-width: 0;
	}

	.row__text .badge,
	.row__badges {
		justify-self: start;
	}

	/* The marks are a line of their own: one beat, the badge centred on it */
	.row__badges {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--space-1);
		min-height: calc(var(--leading-body) * 1rem);
	}

	/* A row's state (Locked, Request) at the top right, on the name's line,
	   as beside the setlist page's h1: a long name wraps before it */
	.row__state {
		align-self: start;
		display: inline-flex;
		align-items: center;
		height: 1lh;
	}

	.row__title {
		font-weight: var(--weight-medium);
	}

	/* Every line under the title takes one beat, as the title does (ADR-0006) */
	.row__meta {
		color: var(--ink-muted);
		font-size: var(--text-sm);
		line-height: calc(var(--leading-body) * 1rem);
	}

	.row__actions {
		display: flex;
		flex: none;
		align-items: center;
		gap: var(--space-2);
		margin-left: auto;
	}

	/* Row controls take the touch floor on both surfaces (ADR-0006: no density switch) */
	/* A 44 target overhangs the row's line, never grows it (ADR-0006): a
	   one-line row with a button stays one beat between its paddings */
	.row .button {
		min-height: var(--target-touch);
		flex: none;
		margin-block: calc((var(--leading-body) * 1rem - var(--target-touch)) / 2);
	}

	/* In the setlist (ADR-0009): the title in accent — no fill, no stripe,
	   no mark (#391), so rows stay flat; the Remove control says it */
	.row--added .row__title {
		color: var(--accent-text);
	}

	/* The row an action just landed, a move (#240) or an add in place: a ~1s
	   outline flash; under reduced motion the outline holds until the next
	   swap of the row drops the class */
	.row--moved {
		outline: 2px solid var(--accent-text);
		outline-offset: -2px;
	}

	@media (prefers-reduced-motion: no-preference) {
		.row--moved {
			outline-color: transparent;
			animation: moved-flash var(--duration-slow) var(--ease);
		}
	}

	@keyframes moved-flash {
		from {
			outline-color: var(--accent-text);
		}
		to {
			outline-color: transparent;
		}
	}

	/* page-header (#280): the title line of a backstage page — one h1 size,
	   its actions and state badges beside it. The client band is ClientHeader,
	   site chrome, not a page header */
	.page-header {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--space-3);
		margin-bottom: var(--space-6);
	}

	/* The base h1's bottom margin is part of the flex item's margin box,
	   so centring floats the title above the badge without this */
	.page-header h1 {
		margin: 0;
	}

	/* Beside a state badge the title takes the rest of the line and wraps
	   within it (a zero basis, or a long name pushes the badge beneath); the
	   two share a baseline so the badge rides the first line either way */
	.page-header:has(.page-header__state) h1 {
		flex: 1 1 0;
		min-width: 0;
		align-self: baseline;
	}

	/* An action beside the title (New setlist) overhangs the h1's line, so
	   every page's h1 sits at the same height, action or not */
	.page-header > form {
		margin-block: calc((1lh - var(--target-touch)) / 2);
	}

	/* The state badge sits at the right of the h1 line, a full touch target,
	   and links to where the state is changed; a long name wraps beneath it.
	   The target overhangs the line instead of growing it, and sits at the
	   top of the row (its overhang centres it on the h1's first line), so
	   the header keeps one rhythm locked or not */
	.page-header__state {
		margin-left: auto;
		align-self: flex-start;
		display: inline-flex;
		align-items: center;
		min-height: var(--target-touch);
		margin-block: calc((1lh - var(--target-touch)) / 2);
		text-decoration: none;
	}

	/* notice (#323): floats above the bottom edge, or the save bar's
	   --bar-clearance; only a persistent card takes clicks and never fades */
	.notice {
		position: fixed;
		inset: auto 0 var(--bar-clearance, calc(var(--space-4) + env(safe-area-inset-bottom)));
		z-index: var(--z-sticky);
		padding: 0 var(--space-4);
		text-align: center;
		pointer-events: none;
	}

	.notice__card {
		display: inline-flex;
		flex-wrap: wrap;
		align-items: center;
		justify-content: center;
		gap: var(--space-1) var(--space-3);
		padding: var(--space-2) var(--space-4);
		color: var(--ink);
		background: var(--surface-raised);
		border: var(--border-width) solid var(--line);
		border-left: 4px solid var(--danger);
		border-radius: var(--radius-md);
	}

	.notice__card:empty,
	.notice__card[hidden] {
		display: none;
	}

	.notice:not(.notice--persistent) .notice__card {
		animation: notice-out var(--duration-notice) forwards;
	}

	.notice--persistent .notice__card {
		pointer-events: auto;
	}

	@keyframes notice-out {
		0%,
		80% {
			opacity: 1;
		}
		100% {
			opacity: 0;
		}
	}

	/* login */
	.login {
		min-height: 100dvh;
		display: grid;
		align-content: center;
		justify-content: center;
	}

	.login h1 {
		font-size: var(--text-2xl);
		line-height: var(--leading-body);
		margin-bottom: var(--space-1);
	}

	.login__subline {
		color: var(--ink-muted);
		font-size: var(--text-sm);
		margin-bottom: var(--space-6);
	}

	.login__form {
		display: grid;
		gap: var(--space-4);
		width: min(360px, 90vw);
		padding: var(--space-5);
		background: var(--surface-raised);
		border: var(--border-width) solid var(--line);
		border-radius: var(--radius-md);
	}

	.login__form label {
		display: grid;
		gap: var(--space-1);
		font-size: var(--text-sm);
		color: var(--ink-muted);
	}

	/* Repertoire rows (#325): every row is one link to the song's page — the
	   title link stretched over the row, a chevron trailing — on the
	   Setlists row's grid: text, a state slot, the chevron. A request row
	   wears its Request badge top right on the title's line */
	.song-row {
		display: grid;
		grid-template-columns: minmax(0, 1fr) auto auto;
		column-gap: var(--space-3);
		/* .row's flex gap would open a row gap under a one-line title */
		row-gap: 0;
		align-items: center;
	}

	.song-row > .row__text {
		grid-column: 1;
		grid-row: 1 / 3;
		/* The title shares the badge's line: never centred on the stack */
		align-self: start;
	}

	.song-row > .row__state {
		grid-column: 2;
		grid-row: 1;
	}

	.song-row__link {
		color: var(--ink);
		text-decoration: none;
	}

	/* The whole row is the link's target (≥ 44px with the row's beat) */
	.song-row--link {
		position: relative;
	}

	.song-row--link .song-row__link::after {
		content: '';
		position: absolute;
		inset: 0;
	}

	/* Hover underlines the title, a link's own cue — a border would read
	   as a button, a fill as state. Focus rings the row the link covers */
	.song-row__link:hover,
	.song-row--link:hover .song-row__link {
		text-decoration: underline;
		text-underline-offset: 3px;
	}

	.song-row--link .song-row__link:focus-visible {
		outline: none;
	}

	.song-row--link:has(:focus-visible) {
		outline: var(--focus-ring);
		outline-offset: var(--focus-offset);
	}

	.song-row__artist,
	.song-row__tags {
		display: block;
	}

	/* Desktop: the tags share the artist's line */
	@media (min-width: 720px) { /* bp-desktop 720px */
		.song-row__artist,
		.song-row__tags {
			display: inline;
		}

		/* Decoration: the alt text after the slash keeps it out of the name */
		.song-row__artist + .song-row__tags::before {
			content: ' · ';
			content: ' · ' / '';
		}
	}

	/* setlists (list) */
	.setlist-list {
		margin: var(--space-5) 0 0;
	}

	/* A row is one link to the setlist's page, like Repertoire's: the name
	   stretched over the row, a chevron trailing. Name, when and for whom,
	   where it stands: three lines. Delete stays on the name's line as the
	   row's one other control; the chevron centres on all three */
	.setlist-row {
		position: relative;
		display: grid;
		grid-template-columns: minmax(0, 1fr) auto auto;
		column-gap: var(--space-3);
		align-items: center;
	}

	.setlist-row__link {
		color: var(--ink);
		text-decoration: none;
	}

	.setlist-row__link::after {
		content: '';
		position: absolute;
		inset: 0;
	}

	/* The overlay is part of the link, so hovering anywhere on the row but
	   Delete underlines the name. Focus rings the row the link covers,
	   never when the focus is on Delete */
	.setlist-row__link:hover {
		text-decoration: underline;
		text-underline-offset: 3px;
	}

	.setlist-row__link:focus-visible {
		outline: none;
	}

	.setlist-row:has(.setlist-row__link:focus-visible) {
		outline: var(--focus-ring);
		outline-offset: var(--focus-offset);
	}

	/* Every line under the name takes one beat, as headings do, so the row
	   is two phrases on the grid: what and when, then where it stands, a
	   half-beat apart */
	.setlist-row__facts,
	.setlist-row__changed {
		line-height: calc(var(--leading-body) * 1rem);
	}

	/* The date pill under the name: 0 put the name's hover underline on the
	   pill, a phrase (12) read as a break, so 8 (Tom, 2026-10-18) — the only
	   sub-beat step in a stack; the facts take the 4 back so the row stays 6 beats */
	.setlist-row .row__meta {
		display: block;
		margin-top: var(--space-2);
	}

	.setlist-row__facts {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: 0 var(--space-2);
		margin-top: calc(var(--space-3) + var(--space-1));
		color: var(--ink-muted);
		font-size: var(--text-sm);
	}

	/* Repeated on every row: its own line, under the facts */
	.setlist-row__changed {
		display: block;
		color: var(--ink-muted);
		font-size: var(--text-sm);
	}

	.song-row__chevron {
		grid-column: 3;
		grid-row: 1 / 3;
	}

	.song-row__chevron,
	.setlist-row__chevron {
		flex: none;
		margin-left: auto;
		color: var(--ink-muted);
		font-size: var(--text-lg);
		line-height: 1;
	}

	.setlists-panel {
		margin-top: var(--space-5);
	}

	/* The same air the shop front's chips take, so the filter never sits on
	   the list or on the empty line under it (#355) */
	.setlists-panel .tag-chips {
		margin-bottom: var(--space-3);
	}

	/* The filter leads into its rows: a phrase, as heading → rows is */
	.setlists-panel .setlist-list {
		margin-top: 0;
	}

	/* setlist-summary: the setlist's details under its name, the list row's
	   shape. The date or "No date yet" with Change details at the line's end
	   (the sections' "fact left, action right"), the count and last change,
	   then the notes quoted */
	/* Three lines on the beat, a half-beat apart: when (and the way to
	   change it), where it stands, the notes; a section break (--space-7,
	   as Share → Add songs) before Share. The h1's --space-6 pulls back to
	   a half-beat */
	.setlist-summary {
		display: grid;
		margin: calc(var(--space-3) - var(--space-6)) 0 var(--space-7);
		line-height: calc(var(--leading-body) * 1rem);
	}

	.setlist-summary__when,
	.setlist-summary__facts,
	.setlist-summary__notes {
		margin: 0;
	}

	.setlist-summary__when {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		justify-content: space-between;
		gap: var(--space-2) var(--space-3);
	}

	.setlist-summary__facts {
		color: var(--ink-muted);
		font-size: var(--text-sm);
	}

	/* A 44px target that overhangs its line instead of growing it, so the
	   pill keeps the --space-3 beat under the h1 (1lh is the line's own) */
	.setlist-summary__change {
		display: inline-flex;
		align-items: center;
		min-height: var(--target-touch);
		margin-block: calc((1lh - var(--target-touch)) / 2);
	}

	.setlist-summary__facts {
		display: flex;
		flex-wrap: wrap;
		gap: 0 var(--space-2);
		margin-top: var(--space-3);
	}

	/* Decoration: a dot between the count and the last change */
	.setlist-summary__facts time::before {
		content: '·';
		content: '·' / '';
		margin-right: var(--space-2);
	}

	/* The notes, quoted and cut to three lines; Change details holds them
	   whole, and AT reads past the clamp */
	.setlist-summary__notes {
		display: -webkit-box;
		-webkit-box-orient: vertical;
		-webkit-line-clamp: 3;
		line-clamp: 3;
		overflow: hidden;
		/* The header's one voice: small, in ink — the client's words, not
		   metadata, but no louder than the date */
		margin-top: var(--space-3);
		font-size: var(--text-sm);
		padding-left: var(--space-3);
		border-left: 2px solid var(--accent);
		white-space: pre-line;
		overflow-wrap: anywhere;
	}

	/* song-count: a count never breaks between number and noun */
	.song-count {
		white-space: nowrap;
	}

	/* event-date: the one fact a setlist is about, as a pill beside the name */
	/* One beat tall, so the line it sits on stays on the grid */
	.event-date {
		display: inline-flex;
		align-items: center;
		gap: var(--space-2);
		block-size: calc(var(--leading-body) * 1rem);
		padding: 0 var(--space-3);
		background: var(--surface-sunken);
		border-radius: var(--radius-full);
		color: var(--ink);
		font-size: var(--text-sm);
		font-weight: var(--weight-medium);
		/* On a text line: sit on its top, not the baseline, so the line stays a beat */
		vertical-align: top;
	}

	.event-date__glyph {
		flex: none;
	}

	/* Undated: the slot stays, said plainly, no fill (a fill means a fact) */
	.event-date--none {
		padding: 0;
		background: none;
		color: var(--ink-muted);
		font-weight: var(--weight-regular);
	}

	/* setlist-name: the rename form on Change details, a labelled field like
	   the details form's own, spaced as one of its rows */
	/* A field of the details form below it: the form's --space-3 apart */
	.setlist-name {
		display: grid;
		margin-bottom: var(--space-3);
	}

	.setlist-name label {
		font-size: var(--text-sm);
		line-height: calc(var(--leading-body) * 1rem);
		color: var(--ink-muted);
	}

	.setlist-name input {
		width: 100%;
		min-width: 0;
	}

	/* The autosave confirmation the Rename/Save buttons used to stand in for */
	.form-saved {
		color: var(--ink-muted);
		font-size: var(--text-sm);
	}

	/* The share bar (#357): the link and the three things an owner does with
	   it, on the setlist page where the setlist is. The page's one raised
	   card: share is the thing handed out, everything else is worked on flat,
	   so it alone wears the act's hue (#370) — a preview of the guest's page.
	   NoShareLink carries no data-hue and stays neutral */
	.share-bar {
		display: flex;
		flex-direction: column;
		gap: var(--space-3);
		margin-bottom: var(--space-7);
		padding: var(--space-4);
		background: var(--surface-raised);
		border: var(--border-width) solid var(--accent-line);
		border-radius: var(--radius-md);
	}

	/* data-hue re-derives every slot from the act's hue inside the card, so
	   it renders as the guest page's header band; only the link needs a
	   step of its own to stand off the band */
	.share-bar[data-hue] {
		background: var(--accent-contrast);
	}

	.share-bar[data-hue] .share__url {
		background: var(--surface-strong);
	}

	.share-bar__url {
		margin: 0;
		display: flex;
	}

	/* The link itself: text, not an input that only looks editable. Full ink —
	   it is the page's one piece of content — and it wraps inside itself
	   rather than pushing the page sideways at 320px (1.4.10) */
	.share__url {
		flex: 1 1 auto;
		min-width: 0;
		padding: var(--space-2) var(--space-3);
		background: var(--surface-sunken);
		border-radius: var(--radius-sm);
		font-size: var(--text-sm);
		color: var(--ink);
		overflow-wrap: anywhere;
	}

	.share-bar__actions {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--space-3);
	}

	/* The system sheet is one device's answer, not every device's (#357): on a
	   desktop its targets are AirDrop and Notes and its copy is buried, so the
	   pointer decides, in CSS rather than a script. View and Copy are always
	   there, and share.js falls back to the clipboard if a coarse-pointer
	   device turns out to have no sheet. */
	.share-bar__sheet {
		display: none;
	}

	@media (pointer: coarse) {
		.share-bar__sheet {
			display: inline-block;
		}
	}

	/* A setlist whose link was revoked: the bar keeps its slot and says so */
	.share-bar__none {
		margin: 0;
		color: var(--ink-muted);
		font-size: var(--text-sm);
	}

	/* share (ADR-0019, #357): what Copy says afterwards — "Link copied ✓".
	   It stays until the next copy, never fading on a timer */
	.share__ack {
		color: var(--ink-muted);
		font-size: var(--text-sm);
	}

	/* Share settings (ADR-0019, regrouped in #357): the lock, then what a
	   holder may ask of the link, then the two ways to kill it. The link
	   itself is on the setlist page. */
	/* Opens with a headed section: a section under the page header, so the
	   header's block gap tops up to the Section rung (ADR-0006); padding,
	   as a margin would collapse into the header's */
	.share {
		display: flex;
		flex-direction: column;
		padding-top: var(--space-3);
	}

	/* The divider between blocks keeps the same air on both sides */
	.share > * + * {
		margin-top: var(--space-6);
		padding-top: var(--space-6);
		border-top: var(--border-width) solid var(--line);
	}

	/* A setting: its control and label on one line, anything about it under */
	.share__setting {
		display: flex;
		flex-direction: row;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--space-3);
	}

	.share__setting + .share__setting {
		margin-top: var(--space-3);
	}

	.share__setting label {
		display: flex;
		align-items: center;
		gap: var(--space-2);
	}

	/* A checkbox setting is a beat-tall line; its label is the 44px target,
	   overhanging the line */
	.share__setting > label {
		min-height: var(--target-touch);
		margin-block: calc((1lh - var(--target-touch)) / 2);
	}

	/* The request cap (ADR-0016 §7): presets, not a number field (#357) — a
	   lone small box beside a label read as an oversized checkbox, and the
	   two ends it needed magic values for are tiles of their own now */
	.share__cap {
		margin: 0;
		padding: 0;
		border: 0;
	}

	/* The tiles' label: one phrase with them, as a field's label is */
	.share__cap legend {
		padding: 0;
		font-size: var(--text-sm);
		line-height: calc(var(--leading-body) * 1rem);
		color: var(--ink-muted);
	}

	/* "Other" opens a number box beside the tiles — revealed by the checked
	   radio alone, no JS. The box borrows the global input recipe and only
	   adds its size: three digits and a spinner, tile-high */
	.share__cap-other {
		display: none;
		align-items: center;
		gap: var(--space-2);
		margin-top: var(--space-3);
		font-size: var(--text-sm);
	}

	.share__cap:has(#request-cap-other:checked) .share__cap-other {
		display: flex;
	}

	.share__cap-other input {
		width: 5ch;
		min-height: var(--target-touch);
		border-radius: var(--radius-md);
	}

	.share__cap-other-unit {
		color: var(--ink-muted);
	}

	.share__setting .form-errors {
		flex-basis: 100%;
		margin-bottom: 0;
	}

	/* Danger zone (#357; Share settings, a song's page): last on the page
	   and set apart by a rule, carrying the same red stripe as the other
	   things that have gone wrong (.form-errors, the notice card) — danger
	   is a form here, never red text on its own. The stripe hangs in the
	   gutter so the heading and text keep the page's left edge like every
	   other section */
	.danger-zone {
		--danger-rule: 4px;
		position: relative;
		margin-top: var(--space-6);
		margin-left: calc(-1 * var(--space-4));
		padding-top: var(--space-6);
		padding-left: var(--space-4);
	}

	/* The divider keeps the column's left edge like every other; only the
	   stripe hangs in the gutter */
	.danger-zone::after {
		content: '';
		position: absolute;
		top: 0;
		right: 0;
		left: var(--space-4);
		border-top: var(--border-width) solid var(--line);
	}

	/* Drawn, not a border: it starts level with the heading, under the
	   divider's air, and ends with the last line, as the heading's text does */
	.danger-zone::before {
		content: '';
		position: absolute;
		top: var(--space-6);
		bottom: 0;
		left: 0;
		width: var(--danger-rule);
		background: var(--danger);
	}

	.danger-zone__actions {
		display: flex;
		flex-direction: column;
		gap: var(--space-6);
	}

	/* The summary names the action and the line under it says what it costs,
	   so neither the heading nor the button has to say it twice */
	.danger-zone__row {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		margin: 0;
	}

	/* The action and its cost are one phrase: a beat each, no gap. The
	   summary is a full target that overhangs its line */
	.danger-zone__row > .popover > summary {
		display: flex;
		align-items: center;
		gap: var(--space-2);
		min-height: var(--target-touch);
		color: var(--ink);
		line-height: calc(var(--leading-body) * 1rem);
		margin-block: calc((1lh - var(--target-touch)) / 2);
	}

	/* The disclosure cue: it opens the confirm, it doesn't act. Decoration,
	   so the alt text keeps it out of the name */
	.danger-zone__row > .popover > summary::before {
		content: '▸';
		content: '▸' / '';
		font-size: var(--text-lg);
		line-height: 1;
	}

	.danger-zone__row > .popover[open] > summary::before {
		content: '▾';
		content: '▾' / '';
	}

	.danger-zone__row .hint {
		margin: 0;
	}

	/* A destructive step confirms in flow, not in a floating panel: the
	   button opens under its summary and pushes the hint down, so nothing is
	   covered and nothing hangs past the viewport at 390px */
	.danger-zone__actions .popover__panel {
		position: static;
		width: auto;
		max-width: none;
		margin: var(--space-3) 0;
	}

	/* Closed, the in-flow panel must take no space at all (Chrome keeps a box
	   for a closed details' content), or the page measures past the save bar */
	.danger-zone__actions .popover:not([open]) > .popover__panel {
		display: none;
	}

	.contact-summary {
		margin-bottom: var(--space-5);
		padding: var(--space-5);
		border: var(--border-width) solid var(--line);
		border-radius: var(--radius-md);
	}

	.contact-summary dl {
		display: grid;
		grid-template-columns: max-content 1fr;
		gap: var(--space-1) var(--space-3);
		margin: var(--space-3) 0 0;
	}

	.contact-summary dt {
		color: var(--ink-muted);
		font-size: var(--text-sm);
	}

	.contact-summary dd {
		margin: 0;
	}

	.move-group {
		display: flex;
		gap: var(--space-1);
	}

	/* 24×24 CSS px is the 2.5.8 floor, verified at tier 3 (#169) */
	.move {
		min-width: var(--target-min);
		min-height: var(--target-min);
		padding: 0;
		border: var(--border-width) solid transparent;
		border-radius: var(--radius-xs);
		background: none;
		color: var(--ink-muted);
		font-size: var(--text-sm);
		line-height: 1;
		cursor: pointer;
	}

	.move:hover:not([aria-disabled]) {
		color: var(--ink);
		border-color: var(--line);
	}

	/* Parked after a mouse move (#200): the island sets the class on the
	   list until the pointer moves, so the arrow that slid under a stationary
	   cursor can't take the next click. Focus and keyboard are untouched. */
	[data-sortable].pointer-idle .move {
		pointer-events: none;
	}

	.tag-chips {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-1);
	}

	/* Bordered, so filters never read as the filled .badge they sit near */
	.chip {
		font-size: var(--text-sm);
		color: var(--ink-muted);
		background: transparent;
		border: var(--border-width) solid var(--line);
		border-radius: var(--radius-sm);
		padding: calc((var(--target-touch) - 1lh) / 2 - var(--border-width)) var(--space-3);
		cursor: pointer;
	}

	.chip:hover {
		border-color: var(--accent-line);
	}

	/* The secondary slot (fill/label swap) — tint carries the state, not
	   colour alone; aria-pressed carries it for assistive tech */
	.chip--active {
		color: var(--accent-text);
		background: var(--accent-contrast);
		border-color: var(--accent-line);
	}

	/* High contrast drops the tint; opted out, or the backplate hides HighlightText */
	@media (forced-colors: active) {
		.chip--active {
			forced-color-adjust: none;
			color: HighlightText;
			background: Highlight;
			border-color: Highlight;
		}
	}

	.empty {
		color: var(--ink-muted);
	}

	/* settings */
	.settings {
		display: grid;
		gap: var(--space-6);
	}

	.settings__name {
		display: grid;
		font-weight: var(--weight-medium);
	}

	.settings__group {
		margin: 0;
		padding: 0;
		border: none;
	}

	/* A group's heading: a phrase above its content */
	.settings__group legend {
		padding: 0;
		margin-bottom: var(--space-3);
		font-weight: var(--weight-medium);
	}

	.swatches {
		display: grid;
		grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
		gap: var(--space-2);
	}

	.theme-options {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-2);
	}

	/* The whole tile is the label, so the target is the tile, not the dot */
	.swatch-option {
		display: flex;
		align-items: center;
		gap: var(--space-2);
		min-height: var(--target-touch);
		padding: var(--space-2) var(--space-3);
		background: var(--surface-raised);
		border: var(--border-width) solid var(--line);
		border-radius: var(--radius-md);
		cursor: pointer;
	}

	.swatch-option:has(:checked) {
		border-color: var(--accent-text);
		box-shadow: inset 0 0 0 1px var(--accent-text);
	}

	.swatch-option input {
		margin: 0;
		padding: 0;
		accent-color: var(--accent);
	}

	/* A hue other than the page's: data-hue re-derives the slots from that
	   hue on this element (generated blocks). The name beside it carries
	   the choice, so the chip needs no contrast floor of its own, and no edge:
	   inside data-hue --line-strong is the fill's own step. The dot is
	   the same chip beside the act's name in the top bar, sized to its text */
	.swatch {
		width: 20px;
		height: 20px;
		flex-shrink: 0;
		border-radius: var(--radius-full);
		background: var(--accent-vivid);
	}

	.swatch--dot {
		display: inline-block;
		width: 0.75em;
		height: 0.75em;
		margin-inline-end: var(--space-2);
		/* Not middle: a 0.75em dot on the baseline centres on the capitals */
		vertical-align: baseline;
	}

	/* client: header band (ADR-0006 — the act's one structural accent) on the
	   read view: the setlist's name with the act as lead (ADR-0014). The
	   change pages wear the plain .change-header instead (ADR-0015). */
	.shop-header {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: var(--space-3) var(--space-6);
		padding-block: var(--space-6);
		padding-inline: var(--page-inset);
		background: var(--accent-contrast);
		border-bottom: var(--border-width) solid var(--accent-line);
	}

	.shop-header__text {
		flex: 1 1 24ch;
		min-width: 0;
	}

	/* The display title takes a beat and a half (ADR-0006: heights in
	   half-beats); the act and the date are its phrase, no gaps */
	.shop-header h1 {
		margin: 0;
		font-size: var(--text-2xl);
		line-height: calc(1.5 * calc(var(--leading-body) * 1rem));
		color: var(--accent);
	}

	.shop-header__lead {
		margin: 0;
		color: var(--ink-muted);
	}

	/* The event date, on every client view (ADR-0010) */
	.shop-header__meta {
		margin: 0;
		font-size: var(--text-sm);
		line-height: calc(var(--leading-body) * 1rem);
		color: var(--ink-muted);
	}

	.shop-header__state {
		margin: var(--space-3) 0 0;
	}

	/* client: the search, outside the swapped panel */
	.songs-controls {
		display: flex;
		gap: var(--space-2);
		margin-bottom: var(--space-3);
	}

	.songs-controls input {
		flex: 1;
		min-width: 0;
	}

	/* client: song list. The line under the search says what else the one
	   field does and what the cap leaves (ADR-0016, #391), in both modes */
	.search-hint {
		margin: 0 0 var(--space-3);
		font-size: var(--text-sm);
		line-height: calc(var(--leading-body) * 1rem);
		color: var(--ink-muted);
	}

	.search-hint__left {
		white-space: nowrap;
	}

	/* A host with nothing to hint keeps the slot for its riders, not the gap */
	.search-hint:empty {
		display: none;
	}

	/* A song row is two beats with or without an artist: the typed row and
	   an artist-less song match their neighbours, a lone title mid-row with
	   its button */
	.song-list .row__text {
		min-height: calc(2 * var(--leading-body) * 1rem);
		align-content: center;
	}

	/* A badged row's title keeps its badge's line (Repertoire, Add songs) */
	.song-list .song-row > .row__text,
	.song-list .row:has(> .row__state) > .row__text {
		align-content: start;
	}

	/* The no-match line speaks in the search hint's voice, in its place */
	.song-list > .empty {
		margin-bottom: 0;
		font-size: var(--text-sm);
	}

	.song-list .tag-chips {
		margin-bottom: var(--space-4);
	}

	/* A quiet label over its rows (#316): an h2, since no section heading
	   sits above it, styled small */
	.results-section__sub {
		margin: 0 0 var(--space-2);
		font-size: var(--text-sm);
		font-weight: var(--weight-regular);
		color: var(--ink-muted);
	}

	/* "As you typed it" only names a group when rows follow it; alone, or
	   while the catalogue is still looking (#382), the typed row is the
	   section, so the button stays in view on a phone */
	.song-list > #typed-row:has(+ #not-ours > #catalogue:empty) .results-section__sub,
	.song-list
		> #typed-row:has(+ #not-ours > #catalogue > .catalogue-looking)
		.results-section__sub {
		display: none;
	}

	/* A search too short for the catalogue leaves the typed row alone too */
	.song-list > #typed-row:has(+ #not-ours:empty) .results-section__sub {
		display: none;
	}

	/* The slot's "Looking…" line (#382), in the sub-heading's voice; a phrase
	   apart from the typed row, replaced whole by the answer so nothing shifts */
	.catalogue-looking {
		margin: var(--space-3) 0 0;
		font-size: var(--text-sm);
		color: var(--ink-muted);
	}

	/* A search answers in one list: what could be added continues the
	   matches (a gap between ruled rows read as a hole), or opens a phrase
	   under the box */
	.song-list > ul + #not-ours {
		margin-top: 0;
	}

	.song-list > .search-hint + #typed-row {
		margin-top: var(--space-3);
	}

	/* The Repertoire's one list leads straight out of its chips, a phrase,
	   as the Setlists list does: the promote queue, then the repertoire (#411) */
	.song-list > .song-requests + ul {
		margin-top: 0;
	}

	.song-list > .tag-chips:has(+ .song-requests + ul),
	.song-list
		> .tag-chips:has(+ .song-requests > .results-section > ul:first-child) {
		margin-bottom: var(--space-3);
	}

	/* A clear group break on the line unit (#276): the tag groups while
	   browsing, and Your requests closing the panel (ADR-0016) */
	.song-group h2,
	.results-section h2 {
		margin: var(--space-7) 0 var(--space-3);
		font-size: var(--text-base);
		color: var(--ink-muted);
	}

	/* client: change-header (ADR-0015 §4) — the plain band on every change
	   page, never the read view's accent band: breadcrumb rooted at the
	   setlist, then the page's h1. The band and the save bar say "editing". */
	.change-header {
		padding-block: var(--space-3);
		padding-inline: var(--page-inset);
		background: var(--surface-raised);
		border-bottom: var(--border-width) solid var(--line);
	}

	.change-header h1 {
		margin: 0;
	}

	.crumbs ol {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-1);
		margin: 0 0 var(--space-3);
		padding: 0;
		list-style: none;
		font-size: var(--text-sm);
		color: var(--ink-muted);
	}

	.crumbs li + li::before {
		content: '›';
		margin-right: var(--space-1);
	}

	.crumbs a {
		display: inline-block;
		min-height: var(--target-min);
	}

	/* A button that reads as a link: the save log's Undo */
	.link {
		min-height: var(--target-min);
		padding: 0;
		font: inherit;
		color: var(--accent-text);
		text-decoration: underline;
		text-underline-offset: 2px;
		background: none;
		border: 0;
		cursor: pointer;
	}

	/* client: save-bar (ADR-0015 §4–5) — the pinned primary slot (ADR-0006)
	   on every change page: the save log on the left, the way back on the
	   right (none on Songs, #349). "Saving…" takes the log's place while a
	   request is in flight (the body's hx-indicator points at the bar). */
	.save-bar {
		position: fixed;
		inset: auto 0 0;
		z-index: var(--z-sticky);
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: var(--space-3);
		/* The band is full-bleed; its content sits on the change header's column */
		padding: var(--space-3) var(--page-inset);
		padding-bottom: calc(var(--space-3) + env(safe-area-inset-bottom));
		background: var(--surface-raised);
		border-top: var(--border-width) solid var(--line);
	}

	/* The log fills what the button leaves: the whole bar on Songs (#349),
	   where a long line wraps and the band grows upward; the band keeps the
	   button's height either way */
	.save-bar__log,
	.save-bar__saving {
		flex: 1;
		margin: 0;
		min-width: 0;
		min-height: var(--target-touch);
		display: flex;
		align-items: center;
		font-size: var(--text-sm);
		color: var(--ink-muted);
	}

	.save-bar__saving,
	.save-bar.htmx-request .save-bar__log {
		display: none;
	}

	.save-bar.htmx-request .save-bar__saving {
		display: flex;
	}

	.save-bar__back {
		display: inline-flex;
		flex: none;
		align-items: center;
		gap: var(--space-2);
		min-height: var(--target-touch);
	}

	/* The count as a pill on the button: label-on-fill swapped once more so
	   it reads as a marker, not a second button */
	.basket__pill {
		min-width: 1.6em;
		padding: 0 var(--space-2);
		font-size: var(--text-sm);
		font-weight: var(--weight-strong);
		line-height: 1.6;
		text-align: center;
		color: var(--accent);
		background: var(--accent-contrast);
		border-radius: var(--radius-xs);
	}

	/* Pure CSS bump on the swapped pill (spec conventions: OOB replacement
	   restarts the animation) */
	@media (prefers-reduced-motion: no-preference) {
		#basket-pill.htmx-settling {
			animation: basket-bump var(--duration-base) var(--ease);
		}
	}

	@keyframes basket-bump {
		from {
			transform: scale(1.15);
		}
		to {
			transform: scale(1);
		}
	}

	/* client: the read view (ADR-0015) — the welcome line, the one entry
	   point (ADR-0016), and a heading row with its change link on the right */
	.welcome {
		margin-bottom: var(--space-6);
		padding: var(--space-4);
		background: var(--accent-contrast);
		border: var(--border-width) solid var(--accent-line);
		border-radius: var(--radius-md);
	}

	.welcome p {
		margin: 0;
	}

	.entry-row {
		display: grid;
		margin-bottom: var(--space-6);
	}

	.entry-row .button {
		min-height: var(--target-touch);
		text-align: center;
	}

	/* segmented: one bordered row of presets, the chosen one filled — a
	   control, so it never borrows the swatch tiles' look. The radio is
	   clipped, not removed: keyboard and AT still land on it, and the ring
	   draws inset on the tile since the row's overflow would cut it */
	.segmented {
		display: flex;
		flex-wrap: wrap;
		/* The dividers are the gap: the row's background shows through 1px
		   between tiles, so a row that wraps at 320px keeps them on both axes */
		gap: var(--border-width);
		max-width: 100%;
		background: var(--line);
		border: var(--border-width) solid var(--line-strong);
		border-radius: var(--radius-md);
		overflow: hidden;
	}

	.segmented__option {
		position: relative;
		display: flex;
		flex: 1 1 auto;
		align-items: center;
		justify-content: center;
		min-height: var(--target-touch);
		padding: 0 var(--space-3);
		background: var(--surface-raised);
		font-size: var(--text-sm);
		font-weight: var(--weight-medium);
		text-align: center;
		white-space: nowrap;
		cursor: pointer;
	}

	.segmented__option:has(:checked) {
		background: var(--accent);
		color: var(--accent-contrast);
	}

	.segmented__option:has(:focus-visible) {
		outline: var(--focus-ring);
		outline-offset: calc(-1 * var(--focus-offset));
	}

	.segmented__option input {
		position: absolute;
		width: 1px;
		height: 1px;
		margin: 0;
		overflow: hidden;
		clip-path: inset(50%);
	}

	/* section (#355): the Section primitive's head — the h2, the one link that
	   acts on what it heads, and the muted line of guidance under them. The
	   h2 keeps its own bottom margin, so the head sets the rhythm either way */
	.section-head {
		display: flex;
		justify-content: space-between;
		align-items: baseline;
		gap: var(--space-3);
	}

	.section-head a {
		display: inline-flex;
		align-items: center;
		min-height: var(--target-min);
	}

	/* A button as the one action (the lock): a full touch target, centred
	   on the heading rather than hung from its baseline */
	.section-head .button {
		align-self: center;
		min-height: var(--target-touch);
	}

	/* state-section: the badge is the state and the line beside it only the
	   consequence (#357), one line of text; the one action sits under both */
	.state-section__state {
		margin: 0;
		color: var(--ink);
		font-size: var(--text-sm);
		line-height: calc(var(--leading-body) * 1rem);
	}

	.state-section__state .badge {
		margin-right: var(--space-2);
		vertical-align: middle;
	}

	.state-section > .button {
		margin-top: var(--space-3);
	}

	/* Guidance, never state: the one muted line under a heading or a field */
	.hint {
		margin: 0 0 var(--space-3);
		color: var(--ink-muted);
		font-size: var(--text-sm);
		line-height: calc(var(--leading-body) * 1rem);
	}

	/* First on the page, above Add songs (#347) */
	.read-notes {
		margin-bottom: var(--space-6);
	}

	.read-notes p {
		white-space: pre-line;
	}

	/* Touch-sized arrows and an icon-only remove on the order page */
	.move--touch,
	.remove {
		min-width: var(--target-touch);
		min-height: var(--target-touch);
	}

	.remove {
		padding: 0;
		font-size: var(--text-lg);
		line-height: 1;
		color: var(--ink-muted);
		background: none;
		border: var(--border-width) solid transparent;
		border-radius: var(--radius-xs);
		cursor: pointer;
	}

	.remove:hover {
		color: var(--ink);
		border-color: var(--line);
	}

	/* Inputs otherwise keep the browser's intrinsic ~20ch width and overflow
	   the page on narrow phones */
	.details-form input {
		width: 100%;
		min-width: 0;
	}

	/* details-form: the client's date + notes, the owner's with name, email and phone (ADR-0010, #343) */
	.details-form {
		display: grid;
		gap: var(--space-3);
		margin-bottom: var(--space-4);
	}

	/* A label and its field are one phrase: the label a beat, no gap (ADR-0006) */
	.details-form label {
		display: grid;
		font-size: var(--text-sm);
		line-height: calc(var(--leading-body) * 1rem);
		color: var(--ink-muted);
	}

	.song-membership {
		margin-top: var(--space-6);
		padding-top: var(--space-6);
		border-top: var(--border-width) solid var(--line);
	}

	/* song-facts: a song's details read-only, in a setlist's context (#398) */
	.song-facts {
		display: grid;
		grid-template-columns: max-content 1fr;
		/* Each fact a phrase: --space-3 between them, one beat per line */
		gap: var(--space-3) var(--space-4);
		margin: 0;
		line-height: calc(var(--leading-body) * 1rem);
	}

	.song-facts dt {
		color: var(--ink-muted);
	}

	.song-facts dd {
		margin: 0;
		min-width: 0;
		overflow-wrap: anywhere;
	}

	/* song-form: a song's details, labelled fields on Change details' grid */
	.song-form {
		display: grid;
		gap: var(--space-3);
	}

	/* An empty error slot takes no grid gap: a form opens a block under
	   its h1 (ADR-0006) */
	.details-form > .form-errors-focus:empty,
	.song-form > .form-errors-focus:empty {
		display: none;
	}

	.song-form label {
		display: grid;
		font-size: var(--text-sm);
		line-height: calc(var(--leading-body) * 1rem);
		color: var(--ink-muted);
	}

	.song-form input {
		width: 100%;
		min-width: 0;
	}

	/* A request's correction, then its facts: two blocks of one section (#406) */
	.song-form + .song-facts {
		margin-block-start: var(--space-6);
	}

	/* The hint belongs to the field above it: the label's beat, not the form's */
	.song-form__field {
		display: grid;
	}

	.song-form__field .hint {
		margin: 0;
	}
}

@layer pages {
	.page {
		max-width: var(--page-width);
		margin: 0 auto;
		padding: var(--space-6) var(--space-5) var(--space-8);
	}

	/* Change pages: the last row and a focused control both clear the fixed save bar (2.4.11, #300) */
	:root:has(.page--bar) {
		--bar-clearance: calc(var(--target-touch) + var(--space-8) + env(safe-area-inset-bottom));
		scroll-padding-bottom: var(--bar-clearance);
	}

	.page--bar {
		padding-bottom: var(--bar-clearance);
	}
}
