@layer colours, reset, layout, typography, ranking, scoring, buttons;

/* Cross-document view transitions: on same-origin navigations the browser
   captures the old and new page and animates between them. We name the CRT
   cabinet and its top chrome (below) so they morph in place — the bezel
   resizes from one page's shape to the next while the screen contents
   cross-fade — instead of the whole page hard-cutting. Where unsupported
   (e.g. Firefox today) navigation falls back to an instant load. */
@view-transition {
	navigation: auto;
}

/*
 * Core colour palette — imported from johnpeart.github.io
 * (engine/styles/variables/colours.css). Keep in sync with that source.
 */

@layer colours {
	:root {
		/* This project is dark-only. Forcing `dark` makes every light-dark()
		   resolve to its dark branch as the permanent baseline; the
		   prefers-color-scheme: light override below lightens the dark
		   surface tokens ~10% for a subtly "tinted" variant. */
		color-scheme: dark;

		--white: hsl(0, 0%, 98%);
		--absolute-white: hsl(0, 0%, 100%);

		--black: hsl(216, 11%, 9%);
		--absolute-black: hsl(0, 0%, 0%);

		--absolute: light-dark(var(--absolute-white), var(--absolute-black));
		--inverse-absolute: light-dark(var(--absolute-black), var(--absolute-white));

		--gray-100: hsl(216, 11%, 86%);
		--gray-300: hsl(216, 11%, 68%);
		--gray-500: hsl(216, 11%, 53%);
		--gray-700: hsl(216, 11%, 38%);
		--gray-900: hsl(216, 11%, 18%);

		/* Palette trios. Each colour has three variants:
		   -tint: light, WCAG AA against BLACK text (≥4.5:1) — use as a soft
		          surface in light mode.
		   (default): the saturated brand colour.
		   -tone: dark, WCAG AA against WHITE text (≥4.5:1) — use as a soft
		          surface in dark mode.
		   --*-tint-tone is a light-dark() helper that picks tint in light
		   mode, tone in dark, so a surface can be set once and adapt. */

		--red-tint: hsl(353, 90%, 92%);
		--red: hsl(353, 96%, 56%);
		--red-tone: hsl(353, 80%, 25%);
		--red-tint-tone: light-dark(var(--red-tint), var(--red-tone));

		--orange-tint: hsl(19, 100%, 88%);
		--orange: hsl(19, 100%, 50%);
		--orange-tone: hsl(19, 100%, 24%);
		--orange-tint-tone: light-dark(var(--orange-tint), var(--orange-tone));

		--yellow-tint: hsl(51, 100%, 84%);
		--yellow: hsl(51, 100%, 50%);
		--yellow-tone: hsl(51, 100%, 16%);
		--yellow-tint-tone: light-dark(var(--yellow-tint), var(--yellow-tone));

		--green-tint: hsl(136, 50%, 84%);
		--green: hsl(136, 37%, 46%);
		--green-tone: hsl(136, 60%, 18%);
		--green-tint-tone: light-dark(var(--green-tint), var(--green-tone));

		--cyan-tint: hsl(198, 95%, 88%);
		--cyan: hsl(198, 100%, 45%);
		--cyan-tone: hsl(198, 100%, 22%);
		--cyan-tint-tone: light-dark(var(--cyan-tint), var(--cyan-tone));

		--blue-tint: hsl(211, 100%, 88%);
		--blue: hsl(211, 100%, 50%);
		--blue-tone: hsl(211, 100%, 24%);
		--blue-tint-tone: light-dark(var(--blue-tint), var(--blue-tone));

		--purple-tint: hsl(283, 80%, 90%);
		--purple: hsl(283, 97%, 63%);
		--purple-tone: hsl(283, 70%, 28%);
		--purple-tint-tone: light-dark(var(--purple-tint), var(--purple-tone));

		--magenta-tint: hsl(327, 90%, 90%);
		--magenta: hsl(327, 100%, 45%);
		--magenta-tone: hsl(327, 100%, 22%);
		--magenta-tint-tone: light-dark(var(--magenta-tint), var(--magenta-tone));

		/* Neutral tint-tone — soft surface that adapts to scheme. */
		--gray-tint-tone: light-dark(var(--gray-100), var(--gray-900));

		--color: light-dark(var(--black), var(--white));
		--color--inverse: light-dark(var(--white), var(--black));

		--background: light-dark(var(--white), var(--black));
		--background--inverse: light-dark(var(--black), var(--white));
		--background--translucent: light-dark(hsla(0, 0%, 98%, 0.5), hsla(216, 11%, 9%, 0.5));

		--secondary-background: var(--gray-100);
		--secondary-borderColor: var(--gray-300);
		--secondary-color: var(--gray-700);

		--link-accent: light-dark(var(--orange), var(--orange-tint));

		/* Project-specific semantic tokens (this site is dark-only). Chosen to
		   meet WCAG AA against --background in both the dark baseline and the
		   tinted (prefers light) variant:
		   - text-muted (gray-300): ~7.7:1 dark / ~5.7:1 tinted.
		   - border-color (gray-700): a subtle divider (non-text, no AA floor).
		   - on-accent: stable pure black for text on the vivid button faces
		     (cyan/red), which don't shift between modes — ≥4.5:1 on both. */
		--border-color: var(--gray-700);
		--text-muted: var(--gray-300);
		--on-accent: hsl(0, 0%, 0%);
	}

	/* Project-specific (not from the johnpeart.github.io source): the site is
	   dark-only, so "light mode" is a tinted variant — the dark surface tokens
	   lifted ~10 lightness points. Only the dark branches that light-dark()
	   resolves to are overridden; foreground/accent tints stay as-is. */
	@media (prefers-color-scheme: light) {
		:root {
			--black: hsl(216, 11%, 19%);
			--absolute-black: hsl(0, 0%, 0%);
			--gray-900: hsl(216, 11%, 28%);

			--red-tone: hsl(353, 80%, 35%);
			--orange-tone: hsl(19, 100%, 34%);
			--yellow-tone: hsl(51, 100%, 26%);
			--green-tone: hsl(136, 60%, 28%);
			--cyan-tone: hsl(198, 100%, 32%);
			--blue-tone: hsl(211, 100%, 34%);
			--purple-tone: hsl(283, 70%, 38%);
			--magenta-tone: hsl(327, 100%, 32%);

			--background--translucent: hsla(216, 11%, 19%, 0.5);
		}
	}
}

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

	* {
		margin: 0;
	}

	img,
	svg {
		display: block;
		max-width: 100%;
	}

	/* Pixel-art country flags stay crisp when scaled up from their 15×10 source. */
	.flag {
		image-rendering: pixelated;
	}

	ul,
	ol {
		list-style: none;
		padding: 0;
	}
}

@layer layout {
	:root {
		--pad: 15px;
		--page-max-width: 1200px;
		--gutter: clamp(var(--pad), 4vw, calc(var(--pad) * 3));
	}

	body {
		min-height: 100dvh;
		background-color: var(--background);
		color: var(--color);
	}

	/* Every page is a single column: the CRT hero on top, everything else below.
	 * main is the page-wide width container — it caps how big anything (the CRT
	 * included) can get on wide screens and owns the gutter off the viewport. */
	main {
		display: flex;
		flex-direction: column;
		gap: calc(var(--pad) * 1.5);
		min-height: 100dvh;
		max-width: var(--page-max-width);
		margin-inline: auto;
		padding: var(--gutter);
	}

	/* Everything below the CRT flows in the same column. */
	main > :not(.crt) {
		width: 100%;
		margin-inline: auto;
	}
}

@layer typography {
	/* Self-hosted from /fonts as WOFF2 (brotli-compressed — ~40KB vs ~180KB of
	   static TTFs). One variable file covers every weight (300–800) we use, so
	   the whole UI loads in a single request. font-display: swap keeps text
	   visible during load; the "Parkinsans Fallback" face below cancels the
	   layout shift that swap would otherwise cause. */
	@font-face {
		font-family: "Parkinsans";
		font-weight: 300 800;
		font-style: normal;
		font-display: swap;
		src: url("/fonts/Parkinsans/Parkinsans-VariableFont_wght.woff2") format("woff2");
	}

	/* Metric-matched fallback: a local system font reshaped (size-adjust +
	   ascent/descent overrides) to occupy the exact box Parkinsans will, so the
	   swap from fallback → Parkinsans reflows nothing. Overrides are derived
	   from Parkinsans' metrics relative to Arial (avg advance 0.560 vs 0.522 →
	   size-adjust 107.1%; ascent 1050/descent 350 per 1000 em ÷ size-adjust). */
	@font-face {
		font-family: "Parkinsans Fallback";
		src: local("Arial"), local("Helvetica Neue"), local("Roboto");
		size-adjust: 107.1%;
		ascent-override: 98.0%;
		descent-override: 32.7%;
		line-gap-override: 0%;
	}

	/* Variable font used only inside the CRT (display text, not buttons). */
	@font-face {
		font-family: "Bitcount";
		font-weight: 400 600;
		font-style: normal;
		font-display: swap;
		src: url("/fonts/Bitcount/Single.woff2") format("woff2");
	}

	:root {
		--font-body:
			"Parkinsans", "Parkinsans Fallback", system-ui, -apple-system,
			"Segoe UI", Roboto, Ubuntu, Cantarell, "Noto Sans", "Helvetica Neue",
			Arial, sans-serif;

		--font-crt: "Bitcount", var(--font-body);

		/* The whole UI type scale is anchored to --font-size-base via fixed pixel
		   steps, so changing the base (e.g. the 18px base scoped to .crt-display)
		   lifts every heading and step with it. The +px offsets are the scale:
		   0 / 2 / 4 / 6 / 12 / 20 / 32, chosen to land on the previous desktop
		   sizes at a 16px base (h1 48, h2 36, h3 28, h4 22, h5 18, h6 16). */
		--font-size-h1: calc(var(--font-size-base) + 20px);
		--font-size-h2: calc(var(--font-size-base) + 16px);
		--font-size-h3: calc(var(--font-size-base) + 12px);
		--font-size-h4: calc(var(--font-size-base) + 8px);
		--font-size-h5: calc(var(--font-size-base) + 4px);
		--font-size-h6: var(--font-size-base);

		/* Body / UI type scale. Headings use --font-size-h* above; these
		   cover body copy, controls and secondary text. */
		--font-size-base: 16px;
		--font-size-lg: calc(var(--font-size-base) + 2px);
		--font-size-xl: calc(var(--font-size-base) + 4px);

		/* Fluid display sizes (the CRT screen). */
		--font-size-crt-display: clamp(17.6px, 13.6px + 1.2vw, 25.6px);
		--font-size-song-title: clamp(22px, 4.5vw, 40px);
		--font-size-song-subtitle: clamp(17px, 3.5vw, 32px);

		--line-height-heading: 1.1;
		--line-height-body: 1.5;

		/* Single source of truth for button text. Drives every .btn (links,
		   native buttons and the radio score keys) plus the size modifiers, so
		   they all render at the same size regardless of ambient font-size. */
		--btn-font-size: var(--font-size-base);
	}

	body {
		font-family: var(--font-body);
		line-height: var(--line-height-body);
	}

	h1, h2, h3, h4, h5, h6 {
		text-wrap: balance;
		line-height: var(--line-height-heading);
		font-weight: 600;
	}

	h1 {
		font-size: var(--font-size-h1);
	}

	h2 {
		font-size: var(--font-size-h2);
	}

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

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

	h5 {
		font-size: var(--font-size-h5);
	}

	h6 {
		font-size: var(--font-size-h6);
		font-weight: 600;
	}

	p {
		max-width: 65ch;
		text-wrap: pretty;
	}

	ul {
		list-style: disc;
	}

	ol {
		list-style: decimal;
	}

	li + li {
		margin-block-start: calc(var(--pad) * 0.25);
	}

	a {
		color: var(--link-accent);
		text-underline-offset: 0.15em;
	}
}

@layer ranking {
	/* The magic-link form lives in a settings card on the /sign-in/ glass panel,
	 * stacking its label, recessed email field and submit key as one column. */
	.sign-in-form {
		display: grid;
		width: 100%;
		color: var(--white);
	}

	.sign-in-email {
		display: grid;
		gap: calc(var(--pad) * 0.4);
		min-inline-size: 0;
		font-size: var(--font-size-lg);
	}

	/* Recessed "plastic" text field, ported from johnpe.art: the well frame and
	 * inset shadow tones derive from --plastic-shadow, and the depression deepens
	 * from rest → hover → focus so typing reads as pressing the field in. */
	.sign-in-form input[type="email"] {
		--plastic-color: var(--color);
		--plastic-background: var(--background);

		box-sizing: border-box;
		width: 100%;
		padding: var(--pad);
		font-family: var(--font-body);
		font-size: var(--font-size-base);
		line-height: 1;
		color: var(--plastic-color);
		background-color: var(--plastic-background);
		border: 1px solid var(--plastic-shadow);
		border-radius: var(--radius-base);
		corner-shape: squircle;
		outline-offset: var(--outline-offset);
		transition: border-color 0.2s, box-shadow 0.2s, padding 0.2s;

		box-shadow:
			inset 0 1px var(--plastic-shadow),
			inset 0 2px var(--plastic-shadow),
			inset 0 3px transparent,
			inset 0 4px transparent,
			inset 0 5px transparent,
			inset 0 6px transparent;
	}

	.sign-in-form input[type="email"]:hover {
		border-color: var(--orange);
		padding: calc(var(--pad) + 2px) var(--pad) calc(var(--pad) - 2px);
		box-shadow:
			inset 0 1px var(--plastic-shadow),
			inset 0 2px var(--plastic-shadow),
			inset 0 3px var(--plastic-shadow),
			inset 0 4px var(--plastic-shadow),
			inset 0 5px transparent,
			inset 0 6px transparent;
	}

	.sign-in-form input[type="email"]:focus {
		caret-color: var(--orange);
		outline: none;
		padding: calc(var(--pad) + 4px) var(--pad) calc(var(--pad) - 4px);
		box-shadow:
			inset 0 1px var(--plastic-shadow),
			inset 0 2px var(--plastic-shadow),
			inset 0 3px var(--plastic-shadow),
			inset 0 4px var(--plastic-shadow),
			inset 0 5px var(--plastic-shadow),
			inset 0 6px var(--plastic-shadow);
	}

	.sign-in-form input[type="email"]:invalid {
		caret-color: var(--red);
	}

	.sign-in-form input[type="email"]:invalid:not(:focus) {
		border-color: var(--red);
		color: var(--red);
	}

	.sign-in-error {
		font-family: var(--font-body);
		font-size: var(--font-size-base);
		color: var(--red);
		margin: 0;
	}

	/* Reusable full-row link, shared by the ranking and progress lists. Resets
	 * link chrome, inverts on hover/focus-visible (keyboard parity), and fills
	 * with the accent colour on press. Per-list grid layout stays separate; this
	 * owns only the link reset + interaction states. Muted children should use
	 * opacity (not a fixed grey) so they stay legible when the row inverts. */
	.row-link {
		color: inherit;
		text-decoration: none;
	}

	.row-link:hover,
	.row-link:focus-visible {
		background: var(--white);
		color: var(--absolute-black);
		outline: none;
	}

	.row-link:active {
		background: var(--orange);
		color: var(--on-accent);
	}

	/* Country + year sit together on one line with no stretch between them.
	 * Shared by the ranking and progress rows so locale reads identically. */
	.locale {
		display: flex;
		align-items: baseline;
		gap: 0.5ch;
	}

	.ranking {
		display: grid;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	/* A row is an <li> wrapping a single link. The link is a grid with fixed rank
	 * and flag tracks, a flexible locale track, then an auto total track. */
	.ranking-row {
		--rank-col: 3ch;
		--flag-col: 36px;
		--row-cols: var(--rank-col) var(--flag-col) 1fr auto;
	}

	/* Dividing line between rows. */
	.ranking-row + .ranking-row {
		border-top: 1px solid var(--border-color);
	}

	.ranking-row > a {
		display: grid;
		grid-template-columns: var(--row-cols);
		grid-template-areas:
			"rank flag locale total"
			".    .    song   song"
			".    .    artist artist";
		column-gap: var(--pad);
		row-gap: calc(var(--pad) * 0.25);
		align-items: center;
		padding-block: calc(var(--pad) * 0.75);
	}

	.ranking-row .rank {
		grid-area: rank;
		font-variant-numeric: tabular-nums;
		text-align: right;
	}

	.ranking-row .flag {
		grid-area: flag;
	}

	.ranking-row .locale {
		grid-area: locale;
	}

	.ranking-row .year {
		opacity: 0.7;
		font-variant-numeric: tabular-nums;
	}

	.ranking-row .total {
		grid-area: total;
		font-variant-numeric: tabular-nums;
		/* Scale the score's hue with its value: low totals read red, high totals
		 * green (--score is the 0–60 total, set inline per row). Saturation/lightness
		 * are fixed at a light pastel so every hue clears AA contrast on the dark
		 * glass. */
		color: hsl(calc(var(--score, 0) * 2.2deg) 65% 70%);
	}

	/* When the row inverts on hover/focus (white) or fills on press (orange), the
	 * score drops its scaled hue and takes the link's foreground (black) so it
	 * always contrasts the new background. */
	.row-link:hover .total,
	.row-link:focus-visible .total,
	.row-link:active .total {
		color: inherit;
	}

	.ranking-row .title {
		grid-area: song;
	}

	.ranking-row .artist {
		grid-area: artist;
		opacity: 0.7;
	}
}

@layer scoring {
	.auth-bar {
		display: flex;
		flex-wrap: wrap;
		gap: calc(var(--pad) * 0.5) var(--pad);
		align-items: center;
		padding: calc(var(--pad) * 0.5) 0;
	}

	.song-head {
		display: flex;
		gap: var(--pad);
		align-items: center;
	}

	/* The CRT cabinet: a dark bezel wrapping a label strip (the song-head) above
	 * the glass screen, sized as the page hero by the layout layer. */
	.crt {
		position: relative;
		z-index: 10;
		display: grid;
		/* Menu bar (auto) on top, the lit display filling the rest (1fr). A bottom
		 * controls strip, when present, adds a third auto row (see :has below). */
		grid-template-rows: auto 1fr;
		width: 100%;
		max-width: 100%;
		max-height: calc(100dvh - calc(var(--gutter) * 2));
		/* Let the song-head collapse animate its height between auto and 0 (see
		 * .crt.is-playing .song-head). Where unsupported the collapse simply snaps. */
		interpolate-size: allow-keywords;
		font-family: var(--font-crt);
		/* Bitcount renders small for its em, so scale the display text up — but
		 * fluidly, so it shrinks on small screens instead of dwarfing the body
		 * (Parkinsans) text. Tops out at the original 1.6em on wide viewports. */
		font-size: var(--font-size-crt-display);
		line-height: 1;
		gap: var(--pad);
		padding: clamp(calc(var(--pad) * 0.5), 2.5vw, calc(var(--pad) * 1.25));
		border-radius: clamp(30px, 2vw, 60px);
		background: linear-gradient(150deg, #2a2a2e, #131316 60%, #050506);
		box-shadow:
			inset 0 1px 0 rgb(255 255 255 / 0.08),
			0 1px 2px rgb(0 0 0 / 0.45),
			0 4px 12px rgb(0 0 0 / 0.3);
		/* The cabinet is the one element present on every page. Naming it lifts
		   it into its own transition group: the browser matches the old and new
		   cabinet by name and animates the box (position + size) between them,
		   so the CRT resizes smoothly. Its captured snapshot — the bezel plus
		   the screen contents — cross-fades inside that morphing box, giving the
		   contents the cross-fade. The menu bar is named separately below so it
		   stays pinned rather than scaling with the bezel. */
		view-transition-name: crt;
	}

	/* Buttons opt out of the CRT display font and size, keeping the body face
	 * so the transport/control layout isn't blown up by the 2em display scale. */
	.crt button,
	.crt .btn {
		font-family: var(--font-body);
		font-size: var(--btn-font-size);
		font-weight: 600;
	}

	/* Glass glare: a soft diagonal sheen bound to the cabinet. Absolutely
	 * positioned (so it's out of grid flow and never displaces the rows), it's
	 * inset by the same clamp() as .crt's padding so it tracks the inner content
	 * box, and carries the screen's border-radius. z 4 sits it above the scanline
	 * overlay (z 3); pointer-events: none keeps clicks reaching the controls and
	 * video. Fades to transparent by ~38% from the top-left corner. */
	.crt::before {
		content: "";
		position: absolute;
		inset: 0;
		z-index: 4;
		pointer-events: none;
		border-radius: clamp(30px, 2vw, 60px);
		background: linear-gradient(
			125deg,
			rgb(255 255 255 / 0.16) 0%,
			rgb(255 255 255 / 0.05) 50%,
			transparent 38%,
			transparent 100%
		);
	}

	/* Panel variant: the same cabinet, glare and lit, scanlined glass, but
	 * wrapping arbitrary content (e.g. the ranking list) instead of a 16:9 video
	 * screen. */

	/* A CRT with a bottom controls strip (the progress filter keys, or the video
	 * transport) adds a third auto-height row beneath the display for the strip:
	 * menu bar / display / controls. */
	.crt:has(.crt-controls) {
		grid-template-rows: auto 1fr auto;
	}

	/* In the video cabinet the lit screen fills the display's remaining height
	 * below the song-head (the song-head is the auto row, the screen the 1fr). With
	 * the cabinet capped at the viewport (see .crt max-height) this gives the screen
	 * a definite height when the viewport is the binding constraint, so the picture
	 * can shrink to fit rather than overflow — keeping the whole cabinet (gutter,
	 * menu bar, controls included) within 100dvh. */
	.crt:has(.crt-screen) .crt-display {
		grid-template-rows: auto 1fr;
	}

	/* The control strip across the top of every CRT cabinet: the brand on the
	 * left, the hamburger menu key on the right. It reads as chrome on the dark
	 * bezel, so its text is light like the screen content. */
	.crt-menu-bar {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: var(--pad);
		padding: 0 var(--pad);
		font-family: var(--font-body);
		font-size: var(--font-size-base);
		color: var(--white);
		/* The top chrome (brand + nav + hamburger) is the same on every page.
		   Giving it its own name pulls it out of the cabinet's snapshot so it
		   morphs independently — the brand mark stays sharp and pinned instead
		   of scaling and cross-fading with the resizing bezel. */
		view-transition-name: crt-menu-bar;
	}

	.crt-brand {
		display: inline-flex;
		color: inherit;
		text-decoration: none;
	}

	.crt-brand-mark {
		display: block;
		height: clamp(20px, 2vw, 30px);
		width: auto;
		/* Matches wordmark.svg's viewBox so the width is reserved before the SVG
		   loads — no horizontal shift of the menu bar on first paint. */
		aspect-ratio: 195 / 25;
	}

	/* The hamburger key is a keycap (.btn) carrying a three-line glyph. */
	.crt-menu-bar .menu-toggle {
		gap: 4px;
		min-width: 0;
		min-height: 0;
		padding: calc(var(--pad) * 0.4);
		aspect-ratio: 1;
	}

	.menu-icon,
	.menu-icon::before,
	.menu-icon::after {
		display: block;
		width: 20px;
		height: 2px;
		background: currentColor;
		border-radius: 2px;
	}

	.menu-icon {
		position: relative;
	}

	.menu-icon::before,
	.menu-icon::after {
		content: "";
		position: absolute;
		left: 0;
	}

	.menu-icon::before { top: -6px; }
	.menu-icon::after { top: 6px; }

	/* Persistent primary navigation in the cabinet's top strip. Hidden by default
	 * (the hamburger dialog is the floor on narrow screens); revealed as inline
	 * links at the desktop breakpoint below, where the hamburger is hidden in turn.
	 * Auth visibility (.signed-in / .signed-out) is toggled by _middleware.js, the
	 * same as the dialog links, so the [hidden] rule still hides the wrong set even
	 * while the bar is shown. */
	.crt-nav-inline {
		display: none;
	}

	/* The sign-out control is a <form> wrapping a .btn keycap; display: contents
	 * lifts the button up to be a direct flex item so it sits in line with the
	 * other keys. */
	.crt-nav-inline .nav-signout-inline {
		display: contents;
	}

	@media (width >= 800px) {
		/* margin-inline-start: auto pins the nav to the right edge of the bar even
		 * if the (now-hidden) hamburger is ever re-shown alongside it. */
		.crt-nav-inline {
			display: flex;
			align-items: center;
			gap: calc(var(--pad) * 0.75);
			margin-inline-start: auto;
		}
	}

	/* The on-screen page title (and any header content). It's the first child of
	 * .crt-content and scrolls with it, so the outer inset comes from
	 * .crt-content's padding; this only spaces the title from the content below. */
	.crt-head {
		position: relative;
		margin-bottom: var(--pad);
		color: var(--white);
	}

	.crt-head h1 {
		margin: 0 0 var(--pad);
		line-height: 1.1;
		font-size: var(--font-size-h2);
		/* Bitcount's dotted glyphs merge into a bloom at heavy weights; keep the
		 * CRT title at a normal weight so it stays crisp. */
		font-weight: 400;
	}

	.crt-head .summary,
	.crt-head p,
	.crt-head ul {
		font-size: var(--font-size-xl);
		line-height: 1.2;
		color: var(--gray-300);
		margin-block-end: var(--pad);
	}

	.crt-head ul {
		margin-block-start: 0;
		padding-inline-start: 1.5em;
		list-style-type: "-  ";
	}

	.crt-head li {
		margin-block-end: calc(var(--pad) / 4);
	}

	.crt-head hr {
		border: 0;
		border-block-start: 1px solid var(--gray-700);
		margin-block: var(--pad);
	}

	.crt-head .summary .summary-progress,
	.crt-head .summary .summary-showing {
		display: block;
	}

	.crt-head .summary .summary-showing {
		color: var(--yellow);
	}

	/* Live top-5 preview on the landing page: a small ranking set into the glass
	 * below the intro, so visitors see real content before signing up. */
	.landing-preview {
		margin-block-start: var(--pad);
	}

	.landing-preview h2 {
		margin-block-end: calc(var(--pad) * 0.5);
		font-size: var(--font-size-h4);
		color: var(--white);
	}

	/* The hamburger-driven navigation dialog. With JS it opens modally
	 * (showModal); without JS the :target rule below reveals it inline as the
	 * floor. Either way it's a centred panel of vertical button links. */
	.site-menu {
		margin: auto;
		width: min(100% - var(--pad) * 2, 600px);
		padding: calc(var(--pad) * 1.5);
		border: 1px solid var(--border-color);
		border-radius: var(--radius-comfortable);
		corner-shape: squircle;
		background: var(--background);
		color: var(--color);
	}

	/* No-JS floor: an anchor toggle sets :target, which reveals the dialog inline
	 * (non-modal, no backdrop). The close link clears the target. */
	.site-menu:target,
	.site-menu[open] {
		display: grid;
		align-content: start;
		gap: var(--pad);
	}

	.site-menu::backdrop {
		background: rgb(0 0 0 / 0.6);
	}

	/* Close sits alone on the top row, pushed to the right; the nav's top border
	 * rules it off from the keys below. */
	.menu-close {
		order: -1;
		justify-self: end;
	}

	/* Two equal columns of keys (2×2 when signed in); the footer row spans both. */
	.site-menu-nav {
		display: grid;
		grid-template-columns: 1fr 1fr;
		gap: calc(var(--pad) * 0.75);
		border-top: 1px solid var(--border-color);
		padding-top: var(--pad);
	}

	/* Stretch every key (links, the sign-out button) to fill its cell so they're
	 * all the same size. */
	.site-menu-nav > .btn,
	.site-menu-nav .nav-signout button {
		width: 100%;
	}

	/* Footer: signed-in user and sign-out side by side, ruled off from the keys. */
	.site-menu-footer {
		grid-column: 1 / -1;
		display: grid;
		grid-template-columns: 1fr 1fr;
		align-items: center;
		gap: calc(var(--pad) * 0.75);
		border-top: 1px solid var(--border-color);
		padding-top: var(--pad);
	}

	.site-menu-nav .nav-signout {
		display: grid;
	}

	.site-menu-user {
		color: var(--text-muted);
		font-size: var(--font-size-base);
	}
	
	/* The .crt--panel can be manually set to occupy the available
	 * vertical height of the screen usign this class       		*/
	.crt--panel.crt--panel--full-height {
		min-height: calc(100dvh - calc(var(--gutter) * 2));
	}

	/* The filter strip's text (the "Group by" label) sits on the dark cabinet,
	 * so it reads light like the screen content. The keycaps set their own
	 * colour via --plastic-color, so only the bare label is affected. */
	.crt--panel .crt-controls {
		color: var(--white);
	}

	/* Content set into the glass panel. position: relative keeps it in flow
	 * beneath the scanline + vignette overlay (.crt-display::before, z 3) so it
	 * picks up the CRT treatment; white text reads on the black face. */
	.crt-content {
		position: relative;
		padding: clamp(var(--pad), 2.5vw, calc(var(--pad) * 2));
		color: var(--white);
	}

	/* A panel's display holds a single scrolling .crt-content (the title now lives
	 * inside it), so give that content the whole glass as one fill row. */
	.crt--panel .crt-display {
		grid-template-rows: 1fr;
	}

	/* In a panel the content scrolls — not the glass — so the scanline + vignette
	 * overlay (.crt-display::before) stays pinned over the full visible glass
	 * while the rows (title included) scroll beneath it. Fills the glass when short. */
	.crt--panel .crt-content {
		min-height: 0;
		height: 100%;
		overflow-y: auto;
	}

	/* The song-head reads as an on-screen label on the dark bezel. As a grid item
	 * of .crt-display it must be allowed to shrink below its content width —
	 * otherwise its single-line (nowrap) rows inflate the grid column and push the
	 * video row out past the cabinet. */
	.crt .song-head {
		min-width: 0;
		gap: clamp(calc(var(--pad) * 0.6), 1.5vw, var(--pad));
		padding: var(--pad);
		color: var(--white);
		/* Collapses up into the top of the cabinet while the video plays. overflow
		 * hidden clips it as it retracts; .crt-display's own overflow hidden clips
		 * the upward translate at the cabinet's top edge. */
		overflow: hidden;
		transition:
			height 360ms cubic-bezier(0.4, 0, 0.2, 1),
			padding 360ms cubic-bezier(0.4, 0, 0.2, 1),
			transform 360ms cubic-bezier(0.4, 0, 0.2, 1),
			opacity 220ms ease;
	}

	/* While playing, retract the song-head: zero its height (handing the space to
	 * the screen) and slide it up out of view. It animates back on pause/stop. */
	.crt.is-playing .song-head {
		height: 0;
		padding-block: 0;
		opacity: 0;
		transform: translateY(-100%);
	}

	@media (prefers-reduced-motion: reduce) {
		.crt .song-head {
			transition: none;
		}
	}

	.crt .song-head .flag {
		width: clamp(60px, 7vw, 96px);
		height: auto;
		aspect-ratio: 3 / 2;
	}

	/* The text column must be allowed to shrink below its content width so the
	 * rows can clip (and marquee) instead of pushing the flag around or wrapping. */
	.crt .song-head > div {
		min-width: 0;
		flex: 1;
	}

	/* Each row stays on a single line. The no-JS floor clips long text with an
	 * ellipsis; the marquee enhancement (crt-marquee.js) scrolls overflowing
	 * rows into view instead — see .marquee below. */
	.crt .song-head h1,
	.crt .song-head p {
		overflow: hidden;
		white-space: nowrap;
		text-overflow: ellipsis;
	}

	.crt .song-head h1 {
		margin: 0;
		font-size: var(--font-size-song-title);
		line-height: 1.2;
	}

	.crt .song-head p {
		margin: 0;
		font-size: var(--font-size-song-subtitle);
		color: var(--gray-300);
	}

	/* Marquee track: JS wraps an overflowing row's text in this flex track holding
	 * two identical copies separated by --marquee-gap, then sets --marquee-shift
	 * (one copy + the gap) and --marquee-duration (scaled to that distance). The
	 * track scrolls continuously by exactly one copy and loops seamlessly, since
	 * the trailing copy sits where the leading one started. Only attached to rows
	 * that actually overflow. */
	.crt .song-head .marquee {
		--marquee-gap: 48px;
		display: inline-flex;
		column-gap: var(--marquee-gap);
		white-space: nowrap;
		animation: song-head-marquee var(--marquee-duration, 8s) linear infinite;
	}

	@keyframes song-head-marquee {
		to { transform: translateX(var(--marquee-shift, 0)); }
	}

	/* Wraps the song-head label and the glass face on one continuous black
	 * surface, so they read as a single lit panel on the cabinet. */
	.crt-display {
		/* On-screen body copy reads a step larger than off-screen UI: bump the
		 * base step to 18px, scoped here so it only affects text inside the lit
		 * glass. Everything deriving from --font-size-base (settings/sign-in copy,
		 * form fields, button labels) inherits the larger size. */
		--font-size-base: 18px;

		/* The cabinet (.crt) sets line-height: 1 for the tightly-packed transport
		 * labels; on-screen copy needs room to breathe, so open it to 1.2. Tight
		 * single-line elements (headings, keycaps, clipped song-head rows) reset
		 * their own line-height where they need it. */
		line-height: 1.2;

		position: relative;
		display: grid;
		/* A label row (the crt-head title, or the video's song-head) above the lit
		 * content/screen which fills the rest and scrolls. */
		grid-template-rows: auto auto;
		min-height: 0;
		overflow: hidden;
		background: var(--absolute-black);
		/* Concentric with .crt: outer radius minus the cabinet padding that insets
		 * this panel, so the corner arcs stay parallel at every viewport width. */
		border-radius: calc(
			clamp(30px, 2vw, 60px) -
			clamp(calc(var(--pad) * 0.5), 2.5vw, calc(var(--pad) * 1.25))
		);
	}

	/* Scanlines + corner vignette across the whole panel (song-head + screen).
	 * pointer-events: none lets clicks fall through to .crt-overlay / the iframe;
	 * z 3 keeps it below the sweeping glow band (.crt-display::after, z 4). */
	.crt-display::before {
		content: "";
		position: absolute;
		inset: 0;
		z-index: 3;
		pointer-events: none;
		background:
			repeating-linear-gradient(
				to bottom,
				rgb(0 0 0 / 0.14) 0,
				rgb(0 0 0 / 0.14) 1px,
				transparent 1px,
				transparent 3px
			),
			radial-gradient(
				120% 120% at 50% 50%,
				transparent 60%,
				rgb(0 0 0 / 0.35) 100%
			);
		mix-blend-mode: multiply;
		/* Sink the picture back under a pane of glass: an inset shadow hugging the
		 * panel edges, clipped to the rounded corners by .crt-display's overflow. */
		box-shadow: inset 0 0 18px 6px rgb(0 0 0 / 0.55);
	}

	/* The glass face: rounded, recessed, black. It centres the 16:9 tube and
	 * carries the scanline overlay across its whole surface. */
	.crt-screen {
		position: relative;
		display: grid;
		place-items: center;
		width: 100%;
		/* Fill the display's 1fr row and allow it to shrink below the picture's
		 * natural height, so the screen is a definite box the tube can be contained
		 * within when the viewport is short. */
		height: 100%;
		min-height: 0;
		border-radius: 15px;
		overflow: hidden;
		background: #000;
		box-shadow: inset 0 0 0 1px rgb(0 0 0 / 0.6);
	}

	/* The picture itself: 16:9, never distorted. It's width-driven by default (so
	 * the screen wraps it exactly, no letterbox), but max-height: 100% caps it to
	 * the screen box — when the viewport is short the cabinet's max-height shrinks
	 * the screen and the picture shrinks with it instead of overflowing, keeping the
	 * whole cabinet within 100dvh. */
	.crt-tube {
		position: relative;
		width: 100%;
		height: auto;
		max-height: 100%;
		aspect-ratio: 16 / 9;
	}

	.crt-tube iframe {
		position: absolute;
		inset: 0;
		width: 100%;
		height: 100%;
		border: 0;
		display: block;
	}

	/* Transparent click layer over the video. The scanlines + vignette are painted
	 * by .crt-display::before across the whole panel; this element only exists to
	 * catch play/pause clicks. pointer-events: none lets clicks reach the iframe
	 * until JS flips it to auto (see .crt-screen.is-live below). */
	.crt-overlay {
		position: absolute;
		inset: 0;
		pointer-events: none;
		z-index: 3;
	}

	/* With JS the overlay catches clicks (toggling play/pause) so the pointer
	 * never reaches the iframe and YouTube's hover chrome stays hidden. */
	.crt-screen.is-live .crt-overlay {
		pointer-events: auto;
		cursor: pointer;
	}

	/* Custom poster: the video's own thumbnail, covering YouTube's poster chrome
	 * (title + play button) whenever playback is stopped. Sits above the iframe
	 * but below the scanline overlay, which keeps the CRT look and handles the
	 * click. A centred play glyph signals it's playable. */
	.crt-poster {
		position: absolute;
		inset: 0;
		z-index: 2;
		background: #000;
		transition: opacity 0.3s;
	}

	.crt-poster img {
		width: 100%;
		height: 100%;
		object-fit: cover;
		display: block;
	}

	.crt-screen.is-showing .crt-poster {
		opacity: 0;
		visibility: hidden;
		pointer-events: none;
	}

	/* The vote cabinet is a query container so the transport strip can react to
	 * its own available width (not the viewport): it stays a single row of six
	 * keys while they fit and only folds into a 3×2 grid when they don't. Scoped
	 * with :has(.crt-play) so only the cabinet carrying the video transport opts
	 * in — the progress-page filter strip is unaffected. */
	.crt:has(.crt-play) {
		container-type: inline-size;
	}

	/* The front panel: a control strip on the cabinet below the screen,
	 * revealed by crt-player.js once the IFrame API is ready. The display font
	 * (--font-crt) is reserved for the lit glass (.crt-display); chrome outside
	 * it — like this strip — reads in the body face (Parkinsans). */
	.crt-controls {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		justify-content: center;
		gap: var(--pad);
		font-family: var(--font-body);
	}

	/* NB: the transport keys' size (min-width/width/gap) is set in @layer buttons,
	 * not here — the .btn.is-key rule there is in a later layer and would override
	 * any sizing set in this layer regardless of specificity. This block only
	 * owns layout (the flex row above, and the folded grid below).
	 *
	 * When the cabinet is too narrow to hold all six keys in one row (six 41.6px
	 * keys + five var(--pad) gaps ≈ 325px), the strip would otherwise wrap to an
	 * awkward 5 + 1. Below that width, lay the keys out as a tidy 3×2 grid — top
	 * row play / pause / stop, bottom row rewind / fast-forward / skip. `order`
	 * overrides the DOM order (rewind comes first in source). The threshold is the
	 * cabinet's own inline-size (see the container-type rule above), not the
	 * viewport, so it folds exactly when it has to. */
	@container (max-width: 325px) {
		.crt-controls:has(.crt-play) {
			display: grid;
			grid-template-columns: repeat(3, 45px);
			gap: var(--pad);
			justify-content: center;
		}

		.crt-controls .crt-play { order: 1; }
		.crt-controls .crt-pause { order: 2; }
		.crt-controls .crt-stop { order: 3; }
		.crt-controls .crt-rewind { order: 4; }
		.crt-controls .crt-ff { order: 5; }
		.crt-controls .crt-skip { order: 6; }
	}

	/* The light beneath the glyph lights orange on the active key, mirroring a
	 * selected score key. [aria-current] covers the progress filter/group keys
	 * (the current view / grouping), [aria-pressed] the transport keys. */
	.crt-controls .btn[aria-pressed="true"] .key-light,
	.crt-controls .btn[aria-current] .key-light {
		background-color: var(--orange) !important;
		box-shadow: 0 0 6px var(--orange);
		border-top: 1px solid var(--red);
	}

	/* Filter/group keys carry a text label above their light strip; give the
	 * pair the same small gap the transport keys use. */
	.crt-controls .progress-nav .btn,
	.crt-controls .group-controls .btn {
		gap: 6px;
	}

	/* The progress controls are two fieldsets — the view nav and the group-by
	 * keys. A big gap pushes them to opposite edges of the strip on a wide
	 * cabinet; when the pair no longer fits they wrap to two stacked rows. */
	.crt-controls:has(.progress-nav) {
		flex-wrap: wrap;
		align-items: flex-start;
		justify-content: space-between;
	}

	/* Real <fieldset>s, kept at their default block display so the browser
	 * notches the top border for the <legend> (the keys lay out in the flex
	 * .control-keys row inside — making the fieldset itself flex would drop the
	 * native legend cut-out). The thin white frame matches .key-well/.category. */
	.crt-controls .progress-nav,
	.crt-controls .group-controls {
		min-inline-size: 0;
		inline-size: fit-content;
		max-inline-size: 100%;
		margin: 0;
		padding: calc(var(--pad) * 0.9) calc(var(--pad) * 1.1) calc(var(--pad) * 1.1);
		border: 1px solid #fff;
		border-radius: var(--radius-comfortable);
		corner-shape: squircle;
	}

	.crt-controls .control-keys {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		justify-content: center;
		gap: var(--pad);
	}

	/* The legend straddles the top border like a native fieldset legend; auto
	 * inline margins centre it over the frame. Font matches the /vote category
	 * legends (.category > legend: body face, base size, bold, uppercase). */
	.crt-controls .group-label {
		margin-inline: auto;
		padding-inline: 0.5em;
		font-family: var(--font-body);
		font-size: var(--font-size-base);
		font-weight: 700;
		letter-spacing: 0.05em;
		text-transform: uppercase;
	}

	.crt-icon {
		position: relative;
		display: block;
	}

	/* Play: a right-pointing triangle. */
	.crt-icon-play {
		width: 0;
		height: 0;
		border-style: solid;
		border-width: 6.4px 0 6.4px 10.4px;
		border-color: transparent transparent transparent currentColor;
		margin-left: 1.92px;
	}

	/* Pause: two vertical bars. */
	.crt-icon-pause {
		width: 9.6px;
		height: 11.52px;
		background: linear-gradient(
			to right,
			currentColor 0 35%,
			transparent 35% 65%,
			currentColor 65% 100%
		);
	}

	/* Stop: a solid square. */
	.crt-icon-stop {
		width: 9.92px;
		height: 9.92px;
		background: currentColor;
		border-radius: 1px;
	}

	/* Rewind / fast-forward: a pair of triangles drawn with the pseudo-elements;
	 * the modifier sets their direction. */
	.crt-icon-rewind,
	.crt-icon-ff {
		width: 14.4px;
		height: 11.2px;
	}

	.crt-icon-rewind::before,
	.crt-icon-rewind::after,
	.crt-icon-ff::before,
	.crt-icon-ff::after {
		content: "";
		position: absolute;
		top: 0;
		width: 0;
		height: 0;
		border-style: solid;
		border-width: 5.6px 7.2px;
	}

	.crt-icon-rewind::before,
	.crt-icon-rewind::after {
		border-color: transparent currentColor transparent transparent;
		border-left-width: 0;
	}
	.crt-icon-rewind::before { left: 0; }
	.crt-icon-rewind::after { left: 7.2px; }

	.crt-icon-ff::before,
	.crt-icon-ff::after {
		border-color: transparent transparent transparent currentColor;
		border-right-width: 0;
	}
	.crt-icon-ff::before { left: 0; }
	.crt-icon-ff::after { left: 7.2px; }

	/* Skip (to next song): a right-pointing triangle butted against a bar. */
	.crt-icon-skip {
		width: 13.6px;
		height: 11.2px;
	}
	.crt-icon-skip::before {
		content: "";
		position: absolute;
		left: 0;
		top: 0;
		width: 0;
		height: 0;
		border-style: solid;
		border-width: 5.6px 0 5.6px 8.32px;
		border-color: transparent transparent transparent currentColor;
	}
	.crt-icon-skip::after {
		content: "";
		position: absolute;
		right: 0;
		top: 0;
		width: 2.24px;
		height: 11.2px;
		background: currentColor;
	}

	/* Shrink-wrap the form around its content (the key grid drives the width)
	 * instead of stretching to main's full width. fit-content overrides the
	 * `main > :not(.crt) { width: 100% }` layout rule (this scoring layer comes
	 * later, so it wins); max-inline-size keeps it inside the viewport on narrow
	 * screens, and the inherited margin-inline:auto centres it.
	 *
	 * NB: nothing in this subtree may carry inline-size containment (no
	 * container-type) — size containment makes an element ignore its own
	 * contents, which would collapse this shrink-to-fit chain. */
	.form-scroll {
		inline-size: fit-content;
		max-inline-size: 100%;
	}

	/* The voting form is itself a raised plastic panel — the slab the score
	 * keys are set into. It borrows the keycap recipe's tokens (a cascaded
	 * --plastic-background re-derives the shadow/frame tones via the `*` rule
	 * in the buttons layer) and the same solid side-shadow stack for thickness,
	 * but it never presses. */
	.categories {
		display: grid;
		gap: var(--pad);
		padding: var(--pad);
		margin-bottom: var(--plastic-depth);

		--plastic-background: var(--gray-900);
		background-color: var(--plastic-background);
		border: 1px solid var(--plastic-shadow);
		/* Match the CRT cabinet's radius (.crt) so the form echoes the screen
		 * above it; the fieldsets inset by --pad stay concentric. */
		border-radius: clamp(30px, 2vw, 60px);
		corner-shape: squircle;

		box-shadow:
			0 1px 0 0 var(--plastic-shadow),
			0 2px 0 0 var(--plastic-shadow),
			0 3px 0 0 var(--plastic-shadow),
			0 4px 0 0 var(--plastic-shadow),
			0 5px 0 0 var(--plastic-shadow),
			0 8px 8px 0 rgba(0, 0, 0, 0.22);
	}

	/* The fieldset itself is the key-well (see buttons layer): strip the default
	 * fieldset min-width and let the legend + hint span the full key grid above
	 * the keys. The well stretches to fill the panel width (overriding the
	 * key-well's max-content sizing). (The white outline comes from .key-well
	 * in the buttons layer.) */
	.category {
		min-inline-size: 0;
		inline-size: 100%;
	}

	.category > legend {
		grid-column: 1 / -1;
		/* Anchor for the hint tooltip, which is absolutely positioned to it. */
		position: relative;
		text-align: center;
		font-family: var(--font-body);
		font-weight: 700;
		padding: 0 var(--pad);
		text-transform: uppercase;
	}

	/* The category label doubles as the hint trigger: hovering it (mouse) or
	 * focusing it (keyboard — it's tabbable) reveals the tooltip. The dotted
	 * underline + help cursor signal there's more to read. */
	.legend-label {
		text-decoration: underline dotted;
		text-underline-offset: 3px;
		cursor: help;
		outline-offset: var(--outline-offset);
	}

	/* The hint is kept in the DOM (role="tooltip", linked via aria-describedby on
	 * the label) so screen readers and no-CSS users still get it; CSS turns it
	 * into a floating bubble above the legend, hidden until hover/focus. */
	.hint {
		position: absolute;
		inset-block-end: calc(100% + 8px);
		inset-inline-start: 50%;
		transform: translateX(-50%);
		z-index: 1000;
		inline-size: max-content;
		max-inline-size: min(280px, 70vw);
		padding: calc(var(--pad) * 0.5) calc(var(--pad) * 0.75);
		font-family: var(--font-body);
		font-size: var(--font-size-base);
		font-weight: 400;
		line-height: 1.4;
		text-transform: none;
		text-align: center;
		color: var(--white);
		background: var(--gray-900);
		border: 1px solid var(--white);
		border-radius: var(--radius-base);
		box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
		opacity: 0;
		visibility: hidden;
		pointer-events: none;
		transition: opacity 120ms ease, visibility 0s linear 120ms;
	}

	/* Little caret pointing down at the legend. */
	.hint::after {
		content: "";
		position: absolute;
		inset-block-start: 100%;
		inset-inline-start: 50%;
		transform: translateX(-50%);
		border: 6px solid transparent;
		border-block-start-color: var(--gray-900);
	}

	.legend-label:hover + .hint,
	.legend-label:focus-visible + .hint {
		opacity: 1;
		visibility: visible;
		transition-delay: 0s;
	}

	/* The 10 score keys (1–8, 10, 12) live in their own grid inside the well, so
	 * the legend + hint stack above them and the key row can be re-laid-out on its
	 * own. One row of 10 by default; the media query below folds it to 5×2 when
	 * there's no room. justify-items:center keeps each key its natural cap size
	 * within its track rather than stretching it. */
	.scale {
		display: grid;
		gap: var(--pad);
		inline-size: 100%;
		justify-items: center;
		grid-template-columns: repeat(10, 1fr);
	}

	/* The fieldsets live in their own grid INSIDE the form, separate from the
	 * full-width .vote-actions bar. A single column the cards stack in; the form
	 * shrink-wraps it (see .form-scroll), so the key grid below drives the width. */
	.category-grid {
		display: grid;
		gap: var(--pad);
		grid-template-columns: 1fr;
	}

	/* Below 640px there isn't room for a row of ten 50px keys, so fold the scale
	 * to 5×2. (A plain media query, not a container query — the form is
	 * shrink-to-fit, which is incompatible with inline-size containment.) */
	@media (width < 800px) {
		.scale {
			grid-template-columns: repeat(5, 1fr);
		}

		/* On phones, pull the page gutter and the CRT's side/bottom padding in
		 * tight — small but still visible — so the cabinet uses nearly the full
		 * width. The top padding is left at its base value to keep the menu bar
		 * breathing room. */
		main {
			--gutter: calc(var(--pad) * 0.4);
		}

		.crt {
			padding: var(--pad) calc(var(--pad) * 0.4);
		}

		/* Phones: collapse the progress status to just the lamp — the word is
		 * dropped (kept accessible for screen readers) so the colour carries it. */
		.progress-list .status-label {
			position: absolute;
			width: 1px;
			height: 1px;
			padding: 0;
			margin: -1px;
			overflow: hidden;
			clip: rect(0 0 0 0);
			white-space: nowrap;
			border: 0;
		}
	}

	/* Selected/pressed scale keys get their recessed look from the button
	 * component (.btn:has(.score-input:checked) in the buttons layer). */

	/* The Vote button is the form's primary action, so it gets its own centred
	 * row. grid-column 1 / -1 makes it span the full form width (it's a grid item
	 * of .categories, which can be multi-column on mobile). Clear/status sit on a
	 * second centred row beneath. */
	.vote-actions {
		grid-column: 1 / -1;
		display: flex;
		flex-direction: column;
		gap: calc(var(--pad) * 0.75);
		align-items: center;
		margin: 0;
		padding-block: var(--pad);
	}

	/* Save confirmation overlay (JS enhancement only): a fixed, full-viewport
	 * scrim that centres a card during the brief handoff to the next song. */
	.save-overlay {
		position: fixed;
		inset: 0;
		z-index: 1000;
		display: grid;
		place-items: center;
		padding: var(--pad);
		background: rgb(0 0 0 / 0.6);
	}

	/* The card: a tick, a "Saved" heading and a "Loading next entry…" line,
	 * stacked and centred. */
	.save-card {
		display: grid;
		justify-items: center;
		gap: calc(var(--pad) * 0.5);
		min-width: min(320px, 80vw);
		padding: calc(var(--pad) * 2);
		text-align: center;
		color: var(--color);
		background: var(--background);
		border: 1px solid var(--border-color);
		border-radius: var(--radius-comfortable);
		corner-shape: squircle;
		box-shadow: 0 8px 30px rgb(0 0 0 / 0.4);
	}

	.save-tick {
		font-size: calc(var(--font-size-h1) * 1.6);
		line-height: 1;
		color: var(--green);
	}

	.save-title {
		font-size: var(--font-size-h2);
		font-weight: 600;
		line-height: var(--line-height-heading);
	}

	.save-text {
		font-size: var(--font-size-base);
		color: var(--text-muted);
	}

	/* Progress page list. Rows are separated by a single dividing line (no
	 * per-row box), like the ranking. The whole row is one link:
	 *
	 *   [flag] [country year]   [status]
	 *          [song]
	 *          [artist]
	 *
	 * a fixed flag column, a middle column that stacks locale (country + year)
	 * / song / artist (any of which may be absent), and the status pinned
	 * top-right. Matches the ranking row's locale/song/artist stack. */
	.progress-list {
		display: grid;
		list-style: none;
		margin: 0;
		padding: 0;
	}

	.progress-list li + li {
		border-top: 1px solid var(--border-color);
	}

	.progress-list li > a {
		--flag-col: 36px;
		display: grid;
		grid-template-columns: var(--flag-col) 1fr auto;
		column-gap: var(--pad);
		align-items: start;
		padding: calc(var(--pad) * 0.75) var(--pad);
	}

	/* Middle column: locale (country + year), song, artist stacked. */
	.progress-list .meta {
		display: grid;
		row-gap: calc(var(--pad) * 0.25);
		min-width: 0;
	}

	.progress-list .year,
	.progress-list .artist {
		/* Muted via opacity (not a fixed grey) so it stays legible when the row
		 * inverts on hover or fills with colour on press. */
		opacity: 0.7;
	}

	.progress-list .year {
		font-variant-numeric: tabular-nums;
	}

	/* Indicator lamp: a small round light, off by default. Always sits beside
	   the visible status word — never the sole signal. */
	.lamp {
		display: inline-block;
		flex: none;
		width: 12px;
		height: 12px;
		border-radius: 999px;
		background: var(--lamp-color, var(--gray-700));
		box-shadow: inset 0 1px 1px rgb(255 255 255 / 0.2);
	}

	.progress-list .status {
		display: inline-flex;
		align-items: center;
		gap: calc(var(--pad) * 0.5);
	}

	.progress-list li[data-status="scored"] .lamp {
		--lamp-color: var(--green);
		box-shadow:
			0 0 7px var(--green),
			inset 0 1px 1px rgb(255 255 255 / 0.35);
	}

	.progress-list li[data-status="skipped"] .lamp {
		--lamp-color: var(--yellow-tone);
	}

	.progress-list li[data-status="unscored"] .lamp {
		--lamp-color: var(--red);
		box-shadow:
			0 0 7px var(--red),
			inset 0 1px 1px rgb(255 255 255 / 0.35);
	}

	.empty-state {
		padding: var(--pad) 0;
	}

	/* Progress sub-navigation + grouping controls */
	.progress-nav {
		display: flex;
		flex-wrap: wrap;
		gap: var(--pad);
		padding: calc(var(--pad) * 0.5) 0;
	}

	.group-controls {
		display: flex;
		flex-wrap: wrap;
		gap: var(--pad);
		align-items: center;
		padding: calc(var(--pad) * 0.5) 0;
	}

	.group {
		display: grid;
		gap: var(--pad);
		margin-block: calc(var(--pad) * 1.5) 0;
	}

	/* Settings page — the controls live inside the CRT glass panel. The dotted
	 * display font (var(--font-crt) on .crt) is right for the on-screen labels
	 * but unreadable for form copy, so the controls drop back to the body face. */
	.settings {
		display: grid;
		gap: calc(var(--pad) * 1.5);
		font-family: var(--font-body);
		font-size: var(--font-size-base);
	}

	/* Each control is a card carved out of the glass: a faint inset panel that
	 * lifts it off the scanlined backdrop and groups its label, blurb and key. */
	.setting {
		display: grid;
		gap: var(--pad);
		justify-items: start;
		padding: calc(var(--pad) * 1.25);
		border: 1px solid rgb(255 255 255 / 0.12);
		border-radius: var(--radius-comfortable);
		corner-shape: squircle;
		background: rgb(255 255 255 / 0.04);
	}

	.setting h2,
	.group h2 {
		margin: 0;
		font-size: var(--font-size-h4);
		color: var(--white);
	}

	.group h2 .group-name {
		display: block;
	}

	.group h2 .group-count {
		display: block;
		font-size: var(--font-size-base);
		color: var(--gray-300);
	}

	.setting p {
		margin: 0;
		color: var(--gray-300);
	}

	/* The destructive control reads red — a danger-tinted card and heading ahead
	 * of the red keycap on the button itself. */
	.setting.danger {
		border-color: color-mix(in oklch, var(--red), transparent 55%);
		background: color-mix(in oklch, var(--red), transparent 88%);
	}

	.setting.danger h2 {
		color: var(--red-tint);
	}

	/* Native file picker: stack the label over the input as one block so the
	 * control reads clearly on the dark glass. */
	.import-form {
		display: grid;
		gap: var(--pad);
		justify-items: start;
		width: 100%;
	}

	.import-form label {
		display: grid;
		gap: calc(var(--pad) * 0.5);
		color: var(--gray-300);
	}

	/* The field reads as a recessed slot on the glass; the native picker button
	 * inside it borrows the secondary keycap face. A pseudo-element can't carry
	 * the keycap's ::after well frame, so it's a flat cap rather than the full
	 * 3D key. */
	.import-form input[type="file"] {
		box-sizing: border-box;
		width: 100%;
		padding: calc(var(--pad) * 0.75);
		font-family: var(--font-crt);
		font-size: var(--font-size-base);
		line-height: 1;
		color: var(--white);
		background-color: rgb(0 0 0 / 0.35);
		border: 1px solid rgb(255 255 255 / 0.18);
		border-radius: var(--radius-base);
		corner-shape: squircle;
		cursor: pointer;
	}

	.import-form input[type="file"]::file-selector-button {
		margin-right: var(--pad);
		padding: calc(var(--pad) * 0.5) var(--pad);
		font-family: var(--font-crt);
		font-size: var(--btn-font-size);
		font-weight: 700;
		color: var(--on-accent);
		background-color: var(--secondary-background);
		border: 1px solid var(--secondary-borderColor);
		border-radius: var(--radius-base);
		corner-shape: squircle;
		cursor: pointer;
		transition: background-color 0.2s;
	}

	@media (hover: hover) {
		.import-form input[type="file"]::file-selector-button:hover {
			background-color: var(--gray-100);
		}
	}

	/* The magic-link form reuses the home cabinet's recessed email field, but here
	 * it sits inside a settings card rather than the control strip, so it stacks
	 * its label, key and error message as one left-aligned column. */
	.setting .sign-in-form {
		gap: var(--pad);
		justify-items: start;
		width: 100%;
	}

	.setting .sign-in-email {
		width: 100%;
	}

	/* Every element inside the lit glass uses the Bitcount display face. Nested
	 * content — settings cards, form fields, the sign-in form, paragraphs — drops
	 * back to the body face in its own rules; this re-asserts the CRT font for all
	 * on-screen text. Placed last in the layer so it wins the source-order ties
	 * against the equally specific input rules above. Buttons get the same face
	 * via a companion rule in @layer buttons (that layer's .btn font-family would
	 * otherwise win regardless of specificity here). */
	.crt-display,
	.crt-display * {
		font-family: var(--font-crt);
	}

	[hidden] {
		display: none !important;
	}
}

/*
 * Buttons — ported from the johnpeart.github.io "plastic" design system.
 *
 * The look is a keycap: a solid face that sits proud of the page on a stack
 * of solid side-shadows, with an ::after "well frame" drawn behind it. Pressing
 * (or selecting — a [aria-pressed] toggle button, or a checked score radio
 * surfaced via :has(.score-input:checked)) sinks the cap into its well using 3D
 * translate transforms.
 *
 * Surface + colour derive from a single input — --plastic-background. The
 * border, side-shadow and well-frame all resolve as deeper tones of that
 * colour via color-mix() in oklch, so an intent variant only sets the one
 * token. This file carries the minimal tokens the recipe needs (colours,
 * sizing, elevation shadows). Colour tokens come from the shared `colours`
 * layer (src/css/colours.css).
 */

@layer buttons {
	/* --- Tokens the plastic recipe depends on -------------------------- */
	:root {
		/* Inverse-of-page grey for the default button face: a dark grey on a
		   light page, a light grey on a dark page, with contrasting text. */
		--btn-default-background: light-dark(hsl(216, 11%, 38%), hsl(216, 11%, 86%));
		--btn-default-color: light-dark(var(--white), var(--black));

		--radius-base: 10px;
		--radius-comfortable: calc(var(--radius-base) * 1.5);
		--outline-offset: 3px;
		--outline-active-offset: 3px;

		/* Plastic material inputs. */
		--plastic-color: var(--color);
		--plastic-background: var(--background);
		--plastic-depth: 5px;
	}

	/* Derive the border / side-shadow / frame tones from each element's OWN
	   cascaded --plastic-background. Declared on `*` (not :root) so an intent
	   override re-derives every downstream tone against the new face colour. */
	* {
		--plastic-shadow: color-mix(in oklch, var(--plastic-background), var(--absolute-black) 25%);
		--plastic-frame: color-mix(in oklch, var(--plastic-background), var(--absolute-black) 15%);
	}

	/* --- The keycap ----------------------------------------------------- */
	.btn {
		position: relative;
		box-sizing: border-box;
		display: inline-grid;
		place-items: center;
		min-width: 50px;
		min-height: 50px;
		margin-bottom: var(--plastic-depth);
		padding: calc(var(--pad) * 0.5) var(--pad);

		/* Default cap: inverse-of-page grey (overridden by intent variants). */
		--plastic-color: var(--btn-default-color);
		--plastic-background: var(--btn-default-background);

		color: var(--plastic-color);
		background-color: var(--plastic-background);
		border: 1px solid var(--plastic-shadow);

		font-family: var(--font-body);
		font-weight: 700;
		/* Anchored to a shared var (not inherit) so links, native buttons and
		   radio score keys all render at the same size regardless of context. */
		font-size: var(--btn-font-size);

		/* All corners route through this so the squircle override has one knob. */
		--button-radius: var(--radius-base);
		border-radius: var(--button-radius);
		corner-shape: squircle;

		line-height: 1;
		text-align: center;
		text-decoration: none;
		cursor: pointer;
		outline-offset: var(--outline-offset);

		backface-visibility: hidden;
		transform-style: preserve-3d;
		transform: translate3d(0, 0, var(--plastic-depth));

		/* The solid side-shadow stack (the cap's "thickness") plus a soft
		   contact shadow on the page. Every state below keeps EXACTLY these
		   six layers (5 solid + 1 contact) and only animates each layer's
		   offset/blur — matching layer counts let box-shadow interpolate
		   smoothly instead of discretely flipping mid-transition (the jitter).
		   A single-primitive contact shadow is used rather than a multi-part
		   elevation token so the count stays exact. */
		box-shadow:
			0 1px 0 0 var(--plastic-shadow),
			0 2px 0 0 var(--plastic-shadow),
			0 3px 0 0 var(--plastic-shadow),
			0 4px 0 0 var(--plastic-shadow),
			0 5px 0 0 var(--plastic-shadow),
			0 8px 8px 0 rgba(0, 0, 0, 0.22);

		transition:
			transform 0.2s cubic-bezier(0.2, 1, 0.2, 1),
			box-shadow 0.2s cubic-bezier(0.2, 1, 0.2, 1),
			background-color 0.2s;
	}

	/* The well frame drawn behind the cap. */
	.btn::after {
		content: "";
		display: block;
		position: absolute;
		inset: 0px -5px -10px;
		border: 2px solid var(--plastic-frame);
		border-radius: calc(var(--button-radius) + 3px);
		corner-shape: squircle;
		box-sizing: content-box;
		transform: translate3d(0, 0, calc(var(--plastic-depth) * -1));
		transition: transform 0.2s cubic-bezier(0.2, 1, 0.2, 1);
		pointer-events: none;
	}

	/* Hover: the cap eases down 1px and the stack shortens, so it reads as
	   ready to press. Only on devices that truly hover, and only on buttons
	   that are neither being pressed nor already selected — the :not() guards
	   make the hover lift mutually exclusive with the pressed/selected sink so
	   the two transforms can never both apply (no source-order dependency). */
	@media (hover: hover) {
		.btn:hover:not(:disabled):not([aria-disabled="true"]):not(:active):not([aria-pressed="true"]):not([aria-current]):not(:has(.score-input:checked)) {
			transform: translate3d(0, 1px, var(--plastic-depth));
			/* Same six layers as rest; the stack shortens by 1px (cap moved
			   down 1px) and the contact shadow tightens. */
			box-shadow:
				0 1px 0 0 var(--plastic-shadow),
				0 2px 0 0 var(--plastic-shadow),
				0 3px 0 0 var(--plastic-shadow),
				0 4px 0 0 var(--plastic-shadow),
				0 4px 0 0 var(--plastic-shadow),
				0 6px 8px 0 rgba(0, 0, 0, 0.2);
		}

		.btn:hover:not(:disabled):not([aria-disabled="true"]):not(:active):not([aria-pressed="true"]):not([aria-current]):not(:has(.score-input:checked))::after {
			transform: translate3d(0, -1px, calc(var(--plastic-depth) * -1));
		}
	}

	/* A score key is a <label> wrapping a visually-hidden radio; keyboard focus
	   lands on the radio, so surface the focus ring on the cap (the label). */
	.btn:focus-visible,
	.btn:has(.score-input:focus-visible) {
		outline: 3px solid var(--orange);
		outline-offset: var(--outline-active-offset);
	}

	/* Pressed and selected share one sunk look on purpose: :active is the
	   transient "I'm physically pushing this" state, while [aria-pressed="true"]
	   (toggle buttons), a current progress key (.crt-controls .btn[aria-current])
	   and a checked score radio (:has(.score-input:checked)) are the persistent
	   "this is selected" states — all should read as held down. The hover rule
	   above excludes every one via :not(), so a depressed key keeps this sunk
	   look and never lifts on hover — precedence no longer depends on source
	   order. */
	.btn:active:not(:disabled):not([aria-disabled="true"]),
	.btn[aria-pressed="true"],
	.crt-controls .btn[aria-current],
	.site-menu-nav .btn[aria-current],
	.crt-nav-inline .btn[aria-current],
	.btn:has(.score-input:checked) {
		/* Keep Z equal to the resting depth so only the Y travel animates.
		   Changing Z (with no perspective ancestor) is visually inert but
		   forces the 3D layer to re-composite at the end of the transition,
		   which snaps the cap by a subpixel — the upward jitter on release. */
		transform: translate3d(0, var(--plastic-depth), var(--plastic-depth));
		border-color: transparent;
		/* Same six layers as rest, all collapsed to zero offset so the cap
		   reads as flat in its well. Matching the count keeps the rise on
		   release a smooth interpolation rather than a discrete flip. */
		box-shadow:
			0 0 0 0 var(--plastic-shadow),
			0 0 0 0 var(--plastic-shadow),
			0 0 0 0 var(--plastic-shadow),
			0 0 0 0 var(--plastic-shadow),
			0 0 0 0 var(--plastic-shadow),
			0 0 1px 1px var(--plastic-shadow);
		outline: none;
	}

	.btn:active:not(:disabled):not([aria-disabled="true"])::after,
	.btn[aria-pressed="true"]::after,
	.crt-controls .btn[aria-current]::after,
	.site-menu-nav .btn[aria-current]::after,
	.crt-nav-inline .btn[aria-current]::after,
	.btn:has(.score-input:checked)::after {
		transform: translate3d(0, calc(var(--plastic-depth) * -1), calc(var(--plastic-depth) * -1));
	}

	/* Disabled: the resting raised cap, just made translucent so it reads as
	   inactive. It keeps its default position, shadow stack and ::after border
	   frame (inherited from the base rule); the hover and :active press rules
	   are all guarded with :not(:disabled):not([aria-disabled="true"]) so the
	   cap neither lifts on hover nor sinks on press — it stays fully at rest.
	   Covers both a native :disabled button and one disabled via
	   [aria-disabled="true"] (the latter still receives events, so the guard,
	   not the absence of events, is what keeps it from responding). */
	.btn:disabled,
	.btn:disabled:hover,
	.btn:disabled:active,
	.btn:disabled:focus,
	.btn[aria-disabled="true"],
	.btn[aria-disabled="true"]:hover,
	.btn[aria-disabled="true"]:focus {
		cursor: not-allowed;
		opacity: 0.5;
		outline: none;
	}


	/* --- Intent variants ----------------------------------------------- *
	 * Solid colour: set --plastic-background and the derivation handles the
	 * border / side-shadow / frame. Primary is orange; secondary is a neutral
	 * grey cap distinct from the default --background face. */
	/* Vivid/light button faces don't shift between the dark and tinted variants,
	   so their text uses the stable --on-accent (pure black): ≥4.5:1 on orange,
	   red, and the light secondary-background in both modes. White text on these
	   faces fails AA, so dark text is required. */
	.btn.is-primary {
		--plastic-color: var(--on-accent);
		--plastic-background: var(--orange);
	}

	.btn.is-secondary {
		--plastic-color: var(--on-accent);
		--plastic-background: var(--secondary-background);
	}

	.btn.is-danger {
		--plastic-color: var(--on-accent);
		--plastic-background: var(--red);
	}

	/* --- Size modifiers ------------------------------------------------- *
	 * Adapted from the johnpeart.github.io design system. btn-lg sets its own
	 * --plastic-depth, padding and font-size, then rebuilds all three box-shadow
	 * states (resting / hover / pressed) so every layer count matches the cap
	 * depth — matching counts keep the press a smooth interpolation instead of a
	 * discrete flip (see the base .btn rule). The ::after well frame inset scales
	 * with the depth too. */
	.btn.btn-lg {
		--plastic-depth: 8px;
		padding: calc(var(--pad) * 0.7) calc(var(--pad) * 1.3);
		font-size: calc(var(--btn-font-size) * 1.15);
		box-shadow:
			0 1px 0 0 var(--plastic-shadow),
			0 2px 0 0 var(--plastic-shadow),
			0 3px 0 0 var(--plastic-shadow),
			0 4px 0 0 var(--plastic-shadow),
			0 5px 0 0 var(--plastic-shadow),
			0 6px 0 0 var(--plastic-shadow),
			0 9px 7px 0 rgba(0, 0, 0, 0.22);
	}

	/* The primary "Vote now" key is the page's main action — bump it past the
	   shared btn-lg sizing so it reads as the obvious thing to press. */
	.btn.vote {
		padding: var(--pad) calc(var(--pad) * 1.8);
		font-size: calc(var(--btn-font-size) * 1.4);
	}

	/* Hover lift: each size keeps its own layer count; the cap eases down 1px,
	   so the last solid layer collapses onto the previous and the contact
	   shadow tightens — same shape as the base hover rule, scaled per size. */
	@media (hover: hover) {
		.btn.btn-lg:hover:not(:disabled):not([aria-disabled="true"]):not(:active):not([aria-pressed="true"]) {
			box-shadow:
				0 1px 0 0 var(--plastic-shadow),
				0 2px 0 0 var(--plastic-shadow),
				0 3px 0 0 var(--plastic-shadow),
				0 4px 0 0 var(--plastic-shadow),
				0 5px 0 0 var(--plastic-shadow),
				0 5px 0 0 var(--plastic-shadow),
				0 7px 7px 0 rgba(0, 0, 0, 0.2);
		}
	}

	/* Pressed/selected collapse: zero every solid layer so the cap sits flat
	   in its well. Layer count matches btn-lg's resting state above. */
	.btn.btn-lg:active:not(:disabled):not([aria-disabled="true"]),
	.btn.btn-lg[aria-pressed="true"] {
		box-shadow:
			0 0 0 0 var(--plastic-shadow),
			0 0 0 0 var(--plastic-shadow),
			0 0 0 0 var(--plastic-shadow),
			0 0 0 0 var(--plastic-shadow),
			0 0 0 0 var(--plastic-shadow),
			0 0 0 0 var(--plastic-shadow),
			0 0 1px 1px var(--plastic-shadow);
	}

	.btn.btn-lg::after {
		inset: 0px -6px -12px;
		border-radius: calc(var(--button-radius) + 4px);
	}

	/* --- The score key -------------------------------------------------- *
	 * Square keycap used for the score scale. aspect-ratio + equal min sizes
	 * keep it square at any content size; padding: 0 lets the glyph centre in
	 * the square rather than forcing it wider than tall. */
	.btn.is-key {
		aspect-ratio: 1;
		min-width: 50px;
		min-height: 50px;
		width: 3.4em;
		height: auto;
		padding: 0;
		/* Stack the number over its light strip and centre the pair in the cap. */
		gap: var(--pad);
		align-content: center;
		--button-radius: var(--radius-comfortable);
		border-radius: var(--button-radius);
		font-variant-numeric: tabular-nums;
	}

	.btn.is-key::after {
		border-radius: calc(var(--button-radius) + 3px);
	}

	/* Transport keys (the CRT video controls) are sized down from the default
	 * keycap so six fit one row in the cabinet. This must live in @layer buttons,
	 * after .btn.is-key — its min-width: 50px / width: 3.4em would otherwise win
	 * by layer order and the keys would render at ~54px. Layout (the flex row and
	 * the folded 3×2 grid) lives with the other .crt-controls rules in @layer
	 * scoring; this block only owns the key dimensions. */
	.crt-controls .btn.is-key {
		min-width: 0;
		min-height: 0;
		gap: var(--pad);
	}

	/* Folded layout: when the cabinet can't hold one row, the keys grow to a 45px
	 * touch target (the @container query that drives the 3×2 grid is in @layer
	 * scoring; this just sets the matching size). */
	@container (max-width: 325px) {
		.crt-controls:has(.crt-play) .btn.is-key {
			width: 45px;
		}
	}

	/* On phones the cabinet is too narrow for full-size 50px keys: the folded 5×2
	 * score grid (5 × ~54px keys + gaps) overflows its key-well, and the six
	 * transport keys spill the control strip. Shrink both keycap sets and tighten
	 * their gaps so they fit. Lives in @layer buttons to override the .btn.is-key
	 * sizing above (it would otherwise win by layer order). The 420px threshold is
	 * where the 5-key score row stops fitting the cabinet. */
	@media (width < 420px) {

		.scale .btn.is-key,
		.crt-controls .btn.is-key {
			min-width: 0;
			min-height: 0;
			width: clamp(40px, 12vw, 50px);
		}

		.crt-controls {
			gap: 10px;
		}
	}

	/* On mobile size classes, scale down only the progress-page Filter and
	 * Group-by buttons: a smaller --btn-font-size flows through their text and
	 * the caps drop their 50px tap-target floor to a tighter content-fit box, so
	 * the two fieldsets pack into the narrow cabinet without crowding. Scoped to
	 * those fieldsets so every other button keeps its standard size. */
	@media (width < 800px) {
		.progress-nav .btn,
		.group-controls .btn {
			--btn-font-size: calc(var(--font-size-base) * 0.85);
			min-width: 0;
			min-height: 0;
			padding: calc(var(--pad) * 0.4) calc(var(--pad) * 0.75);
		}
	}

	/* Indicator light below the number: a dark pill that lights orange (with a
	   soft glow) while this key's radio is the checked one. */
	.key-light {
		width: 20px;
		height: 4px;
		border-radius: 999px;
		border-top: 1px solid var(--gray-900);
		background-color: var(--gray-500);
		transition: background-color 0.2s, box-shadow 0.2s;
	}

	.btn.is-key:has(.score-input:checked) .key-light {
		background-color: var(--orange);
		box-shadow: 0 0 6px var(--orange);
		border-top: 1px solid var(--red);
	}

	/* The radio that backs a score key: visually hidden but still focusable and
	   in the accessibility tree (the label text is its accessible name). Its
	   :checked state drives the cap's sunk look via :has() above. */
	.score-input {
		position: absolute;
		width: 1px;
		height: 1px;
		margin: -1px;
		padding: 0;
		border: 0;
		overflow: hidden;
		clip-path: inset(50%);
		white-space: nowrap;
	}

	/* Container that holds a group of keys, outlined with a thin white border.
	   It hugs its contents; the keys inside rise proud of it via their own cap
	   shadows. Lay out a group by setting grid-template-* on the well (e.g.
	   .scale below). */
	.key-well {
		display: grid;
		padding: calc(var(--pad) * 1.25);
		width: max-content;
		max-width: 100%;
		border: 1px solid #fff;
		border-radius: var(--radius-comfortable);
		corner-shape: squircle;
		justify-items: center;
	}

	/* The category fieldsets are concentric with their form (.categories), which
	 * borrows the CRT cabinet's radius. They inset by --pad, so subtract it from
	 * the form's radius to keep the curves parallel. Lives in the buttons layer
	 * to win over .key-well's --radius-comfortable above. */
	.category.key-well {
		border-radius: calc(clamp(30px, 2vw, 60px) - var(--pad));
	}

	/* corner-shape: squircle reads less round than the circular-arc fallback at
	   the same radius, so engines that support it get a doubled radius to match
	   the rounder Safari fallback. */
	@supports (corner-shape: squircle) {
		.btn {
			--button-radius: calc(var(--radius-base) * 2);
		}

		.btn.is-key {
			--button-radius: calc(var(--radius-comfortable) * 2);
		}
	}

	/* Respect reduced-motion: keep the box-shadow state changes (they convey
	   the pressed/selected state) but drop the easing and the physical travel. */
	@media (prefers-reduced-motion: reduce) {
		.btn,
		.btn::after {
			transition: none;
		}

		.btn:hover,
		.btn:active,
		.btn[aria-pressed="true"],
		.btn:has(.score-input:checked) {
			transform: translate3d(0, 0, var(--plastic-depth));
		}

		.btn:hover::after,
		.btn:active::after,
		.btn[aria-pressed="true"]::after,
		.btn:has(.score-input:checked)::after {
			transform: translate3d(0, 0, calc(var(--plastic-depth) * -1));
		}
	}

	/* Buttons inside the lit glass (settings/sign-in action keys, the vote score
	 * keys) take the Bitcount display face like the rest of the on-screen text.
	 * The .btn base rule above sets the body face, and this layer outranks the
	 * .crt-display rule in @layer scoring, so the override has to live here.
	 * Control-strip keys (.crt-controls) sit outside .crt-display and keep the
	 * body face. */
	.crt-display .btn,
	.crt-display button {
		font-family: var(--font-crt);
	}

	/* Hide the hamburger key on desktop, where the inline nav takes over. The
	 * toggle is a .btn, whose `display: inline-grid` lives in this same layer, so
	 * the hide must live here too (a scoring-layer rule would lose the cascade to
	 * the later buttons layer regardless of specificity). */
	@media (width >= 800px) {
		.crt-menu-bar .menu-toggle {
			display: none;
		}
	}
}

/* View-transition pseudo-elements live on the document root, outside the
   cascade layers, so they're tuned here at the top level.

   The "group" pseudo is the morphing box that interpolates the cabinet's
   position and size between pages; the "old"/"new" image pseudos are the
   captured snapshots that cross-fade within it. We slow both a touch past the
   browser default (0.25s) and ease them so the resize reads as a deliberate
   glide rather than a snap. */
::view-transition-group(crt),
::view-transition-group(crt-menu-bar) {
	animation-duration: 0.42s;
	animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}

::view-transition-old(crt),
::view-transition-new(crt) {
	animation-duration: 0.42s;
	animation-timing-function: ease-in-out;
}

/* Honour reduced-motion: skip the captured-image cross-fade and the morph so
   navigation is an instant swap with no movement. */
@media (prefers-reduced-motion: reduce) {
	::view-transition-group(*),
	::view-transition-old(*),
	::view-transition-new(*) {
		animation: none !important;
	}
}