/* Site styles using Stellar CSS tokens */

* {
	margin: 0;
	padding: 0;
	box-sizing: border-box;
}

/* clip (not hidden — hidden makes a scroll container) so a stray wide child
   never produces horizontal scroll; overscroll-behavior kills bounce. */
html {
	overflow-x: clip;
}

body {
	font-family: var(--font-sans);
	/* 17px: 16px Plex read small at length. Font tokens are rem, so this
	   moves running text only, not the type scale. */
	font-size: 1.0625rem;
	background: var(--neutral-1);
	color: var(--neutral-12);
	/* Unitless, so it follows the 17px; the token is a fixed 1.6rem. */
	line-height: 1.6;
	min-height: 100dvh;
	display: flex;
	overscroll-behavior: none;
}

a {
	color: var(--tertiary-6);
	text-decoration: none;
}

a:hover {
	text-decoration: underline;
}

/* In running prose, links need a non-color cue (WCAG 1.4.1) — color alone
   doesn't distinguish them for color-blind readers. Nav/card links stay
   underline-on-hover. */
.post-body a {
	text-decoration: underline;
	text-decoration-color: color-mix(in oklch, currentColor 45%, transparent);
	text-underline-offset: 0.15em;
}

/* Screen-reader-only content */
.visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/* Keyboard users skip the fixed sidebar nav (~8 tab stops) straight to main */
.skip-link {
	position: fixed;
	top: var(--size-1);
	left: var(--size-1);
	z-index: 100;
	padding: var(--size-0) var(--size-1);
	background: var(--neutral-2);
	border: var(--border-width-0) solid var(--primary-7);
	border-radius: var(--border-radius-1);
	transform: translateY(-200%);
}

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

@media (prefers-reduced-motion: reduce) {
	*,
	::before,
	::after {
		transition-duration: 0.01ms !important;
		animation-duration: 0.01ms !important;
		animation-iteration-count: 1 !important;
		scroll-behavior: auto !important;
	}

	.slide-figures figure img:hover {
		transform: none;
	}
}

/* Sidebar — RIA Pulse pattern: fixed nav + margin-based layout */

.sidebar {
	/* Hide on mobile until Datastar initializes (prevents flash) */
	&:not([data-sidebar-initialized]) {
		@media (width < 48rem) {
			display: none;
		}
	}

	/* Mobile overlay backdrop */
	&[aria-hidden="false"] {
		@media (width < 48rem) {
			position: fixed;
			inset: 0;
			z-index: 40;
			background: rgba(0, 0, 0, 0.4);
		}
	}
}

.sidebar-nav {
	position: fixed;
	inset-block: 0;
	left: 0;
	width: 14rem;
	z-index: 50;
	background: var(--neutral-2);
	border-right: var(--border-width-0) solid var(--neutral-4);
	display: flex;
	flex-direction: column;
	transition: transform 0.3s ease-in-out;
}

/* Closed: slide nav off-screen */
.sidebar[aria-hidden="true"] .sidebar-nav {
	transform: translateX(-100%);
}

/* Main content: margin tracks sidebar on desktop */
.sidebar + .main-wrapper {
	transition: margin-left 0.3s ease-in-out;
	@media (width >= 48rem) {
		margin-left: 14rem;
	}
}

.sidebar[aria-hidden="true"] + .main-wrapper {
	@media (width >= 48rem) {
		margin-left: 0;
	}
}

.sidebar-header {
	padding: var(--size-2) var(--size-2);
}

.site-title {
	font-family: var(--font-mono);
	font-size: var(--font-size-3);
	font-weight: var(--font-weight-bold);
	color: var(--neutral-12);
}

.site-title:hover {
	text-decoration: none;
	color: var(--primary-9);
}

.sidebar-links {
	list-style: none;
	flex: 1;
	padding: var(--size-1) 0;
}

.sidebar-links li a,
.sidebar-links li .sidebar-link-btn {
	display: flex;
	align-items: center;
	gap: var(--size-1);
	padding: var(--size-1) var(--size-2);
	color: var(--neutral-11);
	font-size: var(--font-size-1);
	text-decoration: none;
	border: none;
	background: none;
	width: 100%;
	cursor: pointer;
	font-family: var(--font-sans);
}

.sidebar-links li a:hover,
.sidebar-links li .sidebar-link-btn:hover {
	background: var(--neutral-3);
	color: var(--primary-9);
	text-decoration: none;
}

.sidebar-links svg {
	width: 1.1em;
	height: 1.1em;
	flex-shrink: 0;
}

/* The cookbook's sections, under the cookbook.
   Indented to line up under the parent's label rather than under its icon, so
   the column of words stays a column: one icon width plus the gap the flex row
   already puts between icon and text. */
.sidebar-sub {
	list-style: none;
}

.sidebar-sub li a {
	padding-left: calc(var(--size-2) + 1.1em + var(--size-1));
	/* Tighter than a top-level row: at the parent's spacing the two of them
	   drift apart and stop reading as one group. */
	padding-block: calc(var(--size-1) / 2);
	font-size: var(--font-size-0);
	color: var(--neutral-10);
}

.sidebar-footer {
	padding: var(--size-2);
	border-top: var(--border-width-0) solid var(--neutral-4);
}


/* Content header with sidebar toggle */
.content-header {
	padding: var(--size-1) var(--size-2);
	position: sticky;
	top: 0;
	z-index: var(--zindex-header);
	background: linear-gradient(
		to bottom,
		color-mix(in oklch, var(--neutral-1) 70%, transparent) 0%,
		color-mix(in oklch, var(--neutral-1) 55%, transparent) 60%,
		color-mix(in oklch, var(--neutral-1) 30%, transparent) 100%
	);
	backdrop-filter: blur(14px) saturate(1.4);
	-webkit-backdrop-filter: blur(14px) saturate(1.4);
}

.sidebar-toggle {
	background: none;
	border: none;
	cursor: pointer;
	color: var(--neutral-8);
	padding: var(--size-0);
	display: flex;
	align-items: center;
}

.sidebar-toggle:hover {
	color: var(--neutral-12);
}

.sidebar-toggle svg {
	width: 1.25em;
	height: 1.25em;
}

/* Main wrapper */

/* #page wraps sidebar + main-wrapper so SSE morphs can't clobber the search
   overlay (see base.templ). body is a flex row; #page must fill it and
   re-establish the row for its children, or it shrinks to min-content and
   the whole page collapses into a narrow left column. */
#page {
	flex: 1;
	display: flex;
	min-width: 0;
}

.main-wrapper {
	flex: 1;
	display: flex;
	flex-direction: column;
	min-height: 100dvh;
	min-width: 0;
}

main.content {
	max-width: 48rem;
	margin: 0 auto;
	/* A 1rem floor: on a phone the fluid step alone leaves text at the edge. */
	padding: var(--size-4) max(1rem, var(--size-3));
	width: 100%;
	flex: 1;
}

/* A post reads at about 70 characters a line; at 48rem it ran near 100, and
   the eye loses the next line's start. Title and body share the column;
   sidenotes still float into the margin beside it. */
main.content:has(article .post-body) {
	max-width: calc(32em + 2 * max(1rem, var(--size-3)));
}

/* Skip-link target: focusable programmatically, but a page-sized focus ring
   helps no one. */
main.content:focus {
	outline: none;
}

@media (width < 48rem) {
	main.content {
		padding-top: var(--size-2);
	}
}

/* Theme toggle */

.theme-toggle {
	background: none;
	border: var(--border-width-0) solid var(--neutral-4);
	border-radius: var(--border-radius-1);
	padding: var(--size-0) var(--size-1);
	cursor: pointer;
	color: var(--neutral-11);
	font-size: var(--font-size-0);
	display: flex;
	align-items: center;
	gap: var(--size-0);
}

.theme-toggle:hover {
	border-color: var(--primary-7);
	color: var(--primary-9);
}

.theme-toggle svg {
	width: 1em;
	height: 1em;
	display: none;
}

[data-theme="light"] .theme-icon-light,
[data-theme="dark"] .theme-icon-dark,
[data-theme="auto"] .theme-icon-auto {
	display: block;
}

/* Post cards */

.post-card {
	padding: var(--size-2) 0;
	border-bottom: var(--border-width-0) solid var(--neutral-3);
}

.post-card:last-child {
	border-bottom: none;
}

.post-card h2 {
	font-size: var(--font-size-2);
	margin-bottom: var(--size-0);
}

.post-card .meta {
	color: var(--neutral-7);
	font-size: var(--font-size-0);
	margin-bottom: var(--size-0);
}

.post-card .excerpt {
	color: var(--neutral-10);
}

.tag {
	display: inline-block;
	color: var(--secondary-7); /* secondary-5 measured ~2:1 contrast — invisible */
	padding: 0.1em 0.5em;
	font-size: var(--font-size-0);
}

/* Landing page */

.landing {
	margin-top: var(--size-4);
}

.landing p {
	font-size: var(--font-size-2);
	margin-bottom: var(--size-2);
}

/* Network-error toast — shown when a fetch fails and gives up (data-on:datastar-fetch) */

.net-error-toast {
	position: fixed;
	left: 50%;
	bottom: 1.5rem;
	transform: translateX(-50%);
	z-index: var(--zindex-toast);
	max-width: min(90vw, 32rem);
	padding: 0.75rem 1rem;
	background: var(--neutral-1);
	color: var(--neutral-10);
	border: var(--border-width-0) solid var(--error-6);
	border-left: 4px solid var(--error-8);
	border-radius: var(--border-radius-1);
	box-shadow: var(--shadow-5);
	font-size: var(--font-size-0);
	cursor: pointer;
}

/* Search overlay */

.search-overlay {
	position: fixed;
	inset: 0;
	z-index: var(--zindex-overlay);
}

.search-backdrop {
	position: absolute;
	inset: 0;
	background: rgba(0, 0, 0, 0.4);
}

.search-dialog {
	position: relative;
	max-width: 36rem;
	margin: 10vh auto 0;
	background: var(--neutral-1);
	border: var(--border-width-0) solid var(--neutral-4);
	border-radius: var(--border-radius-1);
	box-shadow: var(--shadow-5);
	display: flex;
	flex-direction: column;
	max-height: 70vh;
	overflow: hidden;
}

@media (width < 48rem) {
	.search-dialog {
		margin: var(--size-2);
		max-width: none;
		max-height: calc(100dvh - var(--size-4));
	}
}

.search-input-row {
	display: flex;
	align-items: center;
	border-bottom: var(--border-width-0) solid var(--neutral-4);
}

/* Visible focus cue for the search input (its own outline is suppressed) */
.search-input-row:focus-within {
	border-bottom-color: var(--primary-7);
}

.search-spinner {
	padding: 0 var(--size-1);
	opacity: 0;
	transition: opacity 0.15s;
}

.search-spinner.active {
	opacity: 1;
}

.search-spinner svg {
	width: 1.2em;
	height: 1.2em;
	color: var(--neutral-8);
}

.search-dialog input[type="search"] {
	width: 100%;
	flex: 1;
	padding: var(--size-2);
	border: none;
	border-radius: 0;
	background: transparent;
	color: var(--neutral-12);
	font-size: var(--font-size-2);
	font-family: var(--font-sans);
}

.search-dialog input[type="search"]:focus {
	outline: none;
	box-shadow: none;
}

.search-dialog input[type="search"]::placeholder {
	color: var(--neutral-8);
}

#search-results {
	overflow-y: auto;
	flex: 1;
}

.search-hint {
	padding: var(--size-2);
	color: var(--neutral-8);
	text-align: center;
}

.search-result {
	display: block;
	padding: var(--size-1) var(--size-2);
	border-bottom: var(--border-width-0) solid var(--neutral-3);
	color: var(--neutral-12);
	text-decoration: none;
}

.search-result:hover {
	background: var(--neutral-2);
	text-decoration: none;
}

.search-result:last-child {
	border-bottom: none;
}

.search-result-header {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: var(--size-1);
}

.search-result-title {
	font-weight: var(--font-weight-bold);
}

.search-result-kind {
	font-size: var(--font-size-0);
	color: var(--neutral-8);
	text-transform: uppercase;
	letter-spacing: 0.05em;
	flex-shrink: 0;
}

.search-result-snippet {
	display: block;
	font-size: var(--font-size-0);
	color: var(--neutral-10);
	margin-top: 0.2em;
}

.search-result-title mark,
.search-result-snippet mark,
.search-result-meta .tag mark {
	/* primary-5, not primary-3. The Stellar scale inverts between themes, so a
	   step sits the same distance from the page background either way: primary-3
	   is ~6 points of lightness from it, which is invisible in dark mode and
	   nearly so in light. primary-5 is ~22 points in both, and neutral-12 (always
	   the text colour) contrasts strongly against it in both. */
	background: var(--primary-5);
	color: var(--neutral-12);
	border-radius: var(--border-radius-0);
	/* Vertical padding only: a fragment match lands mid-word ("Casaub|on"), and
	   horizontal padding there reads as a space that is not in the text. */
	padding: 0.05em 0;
}

.search-result-meta {
	margin-top: 0.3em;
	font-size: var(--font-size-0);
	color: var(--neutral-8);
	display: flex;
	align-items: center;
	gap: var(--size-0);
}

.search-footer {
	padding: var(--size-0) var(--size-2);
	border-top: var(--border-width-0) solid var(--neutral-4);
	font-size: var(--font-size-0);
	color: var(--neutral-8);
	display: flex;
	gap: var(--size-2);
}

.search-footer kbd {
	background: var(--neutral-3);
	border: var(--border-width-0) solid var(--neutral-5);
	border-radius: var(--border-radius-0);
	padding: 0 0.4em;
	font-family: var(--font-mono);
	font-size: 0.85em;
}

/* Post content */

/* A step below the page title each, so sections don't rival it. */
.post-body h1 { font-size: var(--font-size-4); margin: var(--size-3) 0 var(--size-1); }
.post-body h2 { font-size: var(--font-size-3); margin: var(--size-3) 0 var(--size-1); }
.post-body h3 { font-size: var(--font-size-2); margin: var(--size-2) 0 var(--size-0); }
/* Near-white on near-black glares at length; ease running text off. */
.dark .post-body { color: color-mix(in oklch, var(--neutral-12) 88%, var(--neutral-1)); }
.post-body p { margin-bottom: var(--size-2); }
.post-body ul, .post-body ol { margin-bottom: var(--size-2); padding-left: var(--size-3); }
.post-body li { margin-bottom: var(--size-2); }
.post-body li > ul, .post-body li > ol { margin-bottom: 0; margin-top: var(--size-1); }

/* Figures center their content (img / svg) within the column. */
.post-body figure { margin-left: auto; margin-right: auto; text-align: center; }
.post-body figure img { max-width: 100%; height: auto; display: block; margin: 0 auto; }
.post-body figcaption { text-align: center; font-size: var(--font-size--1); color: var(--neutral-11); margin-top: var(--size-1); }

/* Inline diagrams (hand-coded SVG) */
.post-body figure svg.diagram { max-width: 100%; height: auto; display: block; margin: 0 auto; }
.post-body .diagram text { font-family: var(--font-mono); font-size: 14px; fill: currentColor; }
.post-body .diagram .dlabel { font-style: italic; opacity: 0.7; font-size: 13px; }
.post-body .diagram .dhd { font-weight: 600; opacity: 0.7; font-size: 11px; letter-spacing: 0.05em; text-transform: uppercase; }
.post-body .diagram .dnote { font-style: italic; opacity: 0.6; font-size: 11px; }
.post-body .diagram .dbox { stroke: currentColor; stroke-width: 1.5; fill: none; }
.post-body .diagram .dgrid { stroke: currentColor; stroke-opacity: 0.35; stroke-width: 1; fill: none; }
.post-body .diagram .dhi { fill: var(--tertiary-6); fill-opacity: 0.28; }
.post-body .diagram .darrow { stroke: currentColor; stroke-opacity: 0.55; stroke-width: 1.5; fill: none; }

/* Tufte-style sidenotes — pulled into the right margin on wide viewports.
   On narrower viewports they hide, and the footnotes section at the bottom
   takes over. */
.post-body .sidenote { display: none; }
.post-body sup a { text-decoration: none; }

@media (width >= 88rem) {
	.post-body .sidenote {
		display: block;
		float: right;
		clear: right;
		width: 12rem;
		margin-right: -13rem;
		margin-left: 1rem;
		margin-top: 0.3rem;
		margin-bottom: 0.6rem;
		padding-left: 0.6rem;
		border-left: 2px solid var(--tertiary-6);
		font-size: 0.82em;
		line-height: 1.4;
		color: var(--neutral-10);
	}
	.post-body .sidenote-num {
		color: var(--tertiary-7);
		font-weight: 700;
		margin-right: 0.25em;
	}
	.post-body .footnotes-section { display: none; }
}

.post-body ol.footnotes {
	counter-reset: footnote;
	padding-left: 0;
	list-style: none;
}
.post-body ol.footnotes > li {
	counter-increment: footnote;
	position: relative;
	padding-left: 2rem;
}
.post-body ol.footnotes > li::before {
	content: counter(footnote) ".";
	position: absolute;
	left: 0;
	color: var(--tertiary-7);
	font-weight: 700;
}

/* Wider viewports: roomier sidenote */
@media (width >= 100rem) {
	.post-body .sidenote {
		width: 15rem;
		margin-right: -16rem;
	}
}
.post-body pre {
	background: var(--neutral-2);
	padding: var(--size-2);
	border-radius: var(--border-radius-1);
	overflow-x: auto;
	margin-bottom: var(--size-2);
	font-family: var(--font-mono);
	font-size: var(--font-size-0);
}
.post-body code {
	font-family: var(--font-mono);
	font-size: 0.9em;
}
.post-body blockquote {
	border-left: 3px solid var(--primary-7);
	padding-left: var(--size-2);
	color: var(--neutral-10);
	margin-bottom: var(--size-2);
}

/* 404 page — tongue-in-cheek geocities-counter vibe */

.notfound {
	text-align: center;
	padding-block: var(--size-4);
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: var(--size-2);
}

.notfound-code {
	font-family: var(--font-mono);
	font-size: clamp(6rem, 22vw, 14rem);
	font-weight: var(--font-weight-bold);
	line-height: 1;
	letter-spacing: -0.05em;
	color: var(--primary-9);
	margin-block-end: var(--size-1);
}

.notfound h1 {
	font-size: var(--font-size-3);
	color: var(--neutral-12);
}

.notfound-path {
	color: var(--neutral-10);
	font-size: var(--font-size-1);
}

.notfound-path code {
	font-family: var(--font-mono);
	background: var(--neutral-3);
	padding: 0.15em 0.4em;
	border-radius: var(--border-radius-0);
	color: var(--neutral-12);
}

.notfound-blurb {
	color: var(--neutral-10);
	max-width: 32rem;
}

.notfound-counter {
	margin-block-start: var(--size-3);
}

.hit-counter {
	display: inline-flex;
	flex-direction: column;
	align-items: center;
	gap: var(--size-1);
	padding: var(--size-2) var(--size-3);
	border: var(--border-width-0) solid var(--neutral-4);
	border-radius: var(--border-radius-1);
	background: var(--neutral-2);
}

.hit-counter-label {
	font-size: var(--font-size-0);
	color: var(--neutral-8);
	text-transform: uppercase;
	letter-spacing: 0.12em;
}

.hit-counter-digits {
	font-family: var(--font-mono);
	font-size: var(--font-size-5);
	font-weight: var(--font-weight-bold);
	letter-spacing: 0.1em;
	color: var(--primary-9);
	background: var(--neutral-3);
	padding: var(--size-1) var(--size-2);
	border-radius: var(--border-radius-0);
	box-shadow: inset 0 1px 3px color-mix(in oklch, var(--neutral-12) 20%, transparent);
}

.notfound-back {
	margin-block-start: var(--size-2);
	font-size: var(--font-size-1);
}

/* Stats page */

.stats-loading {
	color: var(--neutral-8);
	padding: var(--size-3) 0;
	text-align: center;
}

.stats-header {
	margin-block-end: var(--size-3);
}

.stats-header h1 {
	font-size: var(--font-size-4);
	margin-block-end: var(--size-0);
}

.stats-since {
	color: var(--neutral-8);
	font-size: var(--font-size-0);
	font-family: var(--font-mono);
}

.stats-since-rel {
	color: var(--neutral-7);
	margin-inline-start: var(--size-0);
}

/* Six tiles now, not four, and two of them hold a duration rather than a
   count: "14m 00s" is twice the width of "172" and must not wrap into two
   lines mid-number. Wider tracks and a smaller figure, so the row still wraps
   rather than running off the side of the page. */
.stats-tiles {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
	gap: var(--size-2);
	margin-block-end: var(--size-4);
}

.stat-tile {
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--size-0);
	padding: var(--size-2);
	border: var(--border-width-0) solid var(--neutral-4);
	border-radius: var(--border-radius-1);
	background: var(--neutral-2);
}

.stat-tile--live {
	border-color: var(--primary-7);
	background: color-mix(in oklch, var(--primary-3) 30%, var(--neutral-2));
}

.stat-tile-num {
	font-family: var(--font-mono);
	font-size: var(--font-size-4);
	white-space: nowrap;
	font-weight: var(--font-weight-bold);
	color: var(--neutral-12);
	line-height: 1;
}

.stat-tile--live .stat-tile-num {
	color: var(--primary-9);
}

.stat-tile-label {
	font-size: var(--font-size-0);
	color: var(--neutral-9);
	text-transform: uppercase;
	letter-spacing: 0.08em;
}

.stats-tops {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
	gap: var(--size-3);
}

.stats-list {
	list-style: none;
	padding: 0;
	margin: 0;
}

.stats-list li {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: var(--size-1);
	padding: var(--size-0) 0;
	border-bottom: var(--border-width-0) solid var(--neutral-3);
	font-size: var(--font-size-1);
}

.stats-list li:last-child {
	border-bottom: none;
}

.stats-list-name {
	font-family: var(--font-mono);
	color: var(--neutral-11);
	overflow-wrap: anywhere;
	min-width: 0;
}

.stats-list-count {
	font-family: var(--font-mono);
	color: var(--primary-9);
	font-weight: var(--font-weight-bold);
	flex-shrink: 0;
}

.stats-empty {
	color: var(--neutral-7);
	font-style: italic;
	padding: var(--size-1) 0;
}

/* The page grew a five-column table; the 48rem measure that suits prose puts
   the last column off the edge. Widened from the child, the same way the drops
   console does it, rather than plumbing a flag through PageConfig. */
main.content:has(.stats) {
	max-width: 72rem;
}

.stat-tile-note {
	font-size: var(--font-size--1);
	color: var(--neutral-8);
	line-height: 1.3;
}

.stats-h2 {
	font-size: var(--font-size-1);
	color: var(--neutral-10);
	margin-block-end: var(--size-1);
	font-weight: var(--font-weight-bold);
	text-transform: uppercase;
	letter-spacing: 0.08em;
}

/* Every section here reports a number that means something narrower than its
   heading says. The note is where that caveat goes, so the table can stay a
   table. */
.stats-note {
	font-size: var(--font-size--1);
	color: var(--neutral-9);
	max-width: 60ch;
	margin-block-end: var(--size-2);
}

.stats-reading,
.stats-tops-section,
.stats-who,
.stats-traps {
	margin-block-start: var(--size-5);
}

/* Tables scroll inside their own box on a phone rather than widening the page,
   which would put the site's own layout off screen. */
.stats-scroll {
	overflow-x: auto;
}

.stats-table {
	width: 100%;
	border-collapse: collapse;
	font-size: var(--font-size--1);
}

.stats-table th {
	text-align: left;
	font-weight: var(--font-weight-bold);
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--neutral-9);
	padding: var(--size-0) var(--size-1);
	border-block-end: var(--border-width-0) solid var(--neutral-4);
	white-space: nowrap;
}

.stats-table td {
	padding: var(--size-1);
	border-block-end: var(--border-width-0) solid var(--neutral-3);
	vertical-align: baseline;
	white-space: nowrap;
}

.stats-table tr:last-child td {
	border-block-end: none;
}

.stats-num {
	text-align: right;
	font-family: var(--font-mono);
	font-variant-numeric: tabular-nums;
}

.stats-path {
	width: 55%;
	font-family: var(--font-mono);
	color: var(--neutral-11);
	max-width: 28rem;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* A user agent is unbounded attacker-controlled text. It gets a column, a cap
   and an ellipsis; the full string is in the log for anyone who needs it. */
.stats-agent {
	font-family: var(--font-mono);
	color: var(--neutral-9);
	max-width: 24rem;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* The longest read is the most interesting number in the table — the whole
   reason nothing is clamped — so it is the one that is not grey. */
.stats-longest {
	color: var(--neutral-12);
}

.stats-faded {
	color: var(--neutral-8);
}

.stats-bars {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
	gap: var(--size-3);
}

.stats-breakdown h3 {
	font-size: var(--font-size-0);
	color: var(--neutral-9);
	font-weight: var(--font-weight-bold);
	margin-block-end: var(--size-1);
	text-transform: lowercase;
	letter-spacing: 0.04em;
}

.stats-shares {
	list-style: none;
	padding: 0;
	margin: 0;
}

/* The bar is a background, not an element: --share is set per row and the row
   still lays out as text, so a long value wraps over its own bar instead of
   pushing a drawn box out of shape. */
.stats-shares li {
	position: relative;
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: var(--size-1);
	padding: var(--size-0) var(--size-1);
	font-size: var(--font-size--1);
	background: linear-gradient(
		to right,
		color-mix(in oklch, var(--primary-4) 45%, transparent) var(--share, 0%),
		transparent var(--share, 0%)
	);
	border-radius: var(--border-radius-0);
}

.stats-share-name {
	font-family: var(--font-mono);
	color: var(--neutral-11);
	overflow-wrap: anywhere;
	min-width: 0;
}

.stats-share-count {
	font-family: var(--font-mono);
	color: var(--neutral-12);
	flex-shrink: 0;
	font-variant-numeric: tabular-nums;
}

.stats-share-pct {
	color: var(--neutral-8);
	margin-inline-start: var(--size-0);
}

/* Talks */

/* Talk grid */

.talk-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
	gap: var(--size-3);
	margin-top: var(--size-2);
}

.talk-card {
	display: flex;
	flex-direction: column;
	border: var(--border-width-0) solid var(--neutral-4);
	border-radius: var(--border-radius-1);
	overflow: hidden;
	color: var(--neutral-12);
	text-decoration: none;
	transition: border-color 0.15s, box-shadow 0.15s;
}

.talk-card:hover {
	border-color: var(--primary-7);
	box-shadow: var(--shadow-2);
	text-decoration: none;
}

.talk-card-logo {
	aspect-ratio: 3 / 2;
	display: flex;
	align-items: center;
	justify-content: center;
	background: var(--neutral-2);
	padding: var(--size-2);
}

.talk-card-logo img {
	max-width: 100%;
	max-height: 100%;
	object-fit: contain;

	.dark & {
		filter: invert(1);
	}
}

.talk-card-logo--empty {
	background: var(--neutral-3);
}

.talk-card-body {
	padding: var(--size-2);
}

.talk-card-body h2 {
	font-size: var(--font-size-1);
	margin-bottom: var(--size-0);
	line-height: 1.3;
}

.talk-card-body time {
	font-size: var(--font-size-0);
	color: var(--neutral-7);
}

/* When talk is active, go full viewport */
.talk-shell {
	background: var(--neutral-1);
	color: var(--neutral-12);
	flex: 1;
	display: flex;
	flex-direction: column;
}

main.content:has(.talk-shell) {
	max-width: none;
	padding: 0;
	display: flex;
	flex-direction: column;
}

main.content:has(.talk-grid) {
	max-width: none;
}


.talk-viewport {
	width: 100%;
	flex: 1;
	position: relative;
	overflow: hidden;
	/* Letterbox: center the fixed-aspect stage; size container so the stage
	   can size itself against the viewport via cq units. */
	container-type: size;
	display: flex;
	align-items: center;
	justify-content: center;
}

/* Slides
 *
 * Slides render on a fixed 16:9 stage letterboxed into whatever window shape
 * is available, like a real presentation. All type/spacing inside the slide is
 * sized in container-query units of the stage, so a slide looks IDENTICAL on a
 * 4K monitor, an old 1024x768 projector, and the print/PDF view (print.templ's
 * 1280x720 .print-page picks up the same container) — only bigger or smaller.
 * Portrait screens (phones) drop the stage for a scrollable reading mode; see
 * the orientation media query at the end of this section. */

.talk-viewport > .slide {
	width: min(100cqw, calc(100cqh * 16 / 9));
	aspect-ratio: 16 / 9;
}

.slide {
	container: stage / size;
	padding: 4% 6%;
	display: flex;
	flex-direction: column;

	/* Stage-relative type scale. 1cqw = 1% of stage width (= 16/9 cqh, since
	   the aspect is locked). Values chosen to match the old rem/vw sizes at a
	   1280px-wide stage. */
	--slide-fs-h1: 10cqw;
	--slide-fs-h2: 7cqw;
	--slide-fs-h3: 2.6cqw;
	--slide-fs-text: 2.5cqw;
	--slide-fs-quote: 2.4cqw;
	--slide-fs-small: 1.25cqw;
	--slide-fs-fine: 1.4cqw;
	--slide-img-h: 55cqh;
	--slide-measure: 65cqw;
}

.slide > .slide-row,
.slide > .slide-body {
	margin-block: auto;
}

.slide > .slide-body {
	max-width: var(--slide-measure);
}

.slide > p, .slide > ul:not(.bibliography), .slide > ol:not(.bibliography) {
	max-width: var(--slide-measure);
}

/* h1 is reserved for slide-cover so the raw talk html pages are still valid-ish html w/o h* clobbers */

.slide h2 {
	font-size: var(--slide-fs-h2);
	margin-bottom: 0.1em;
}

.slide h3 {
	font-size: var(--slide-fs-h3);
	margin-bottom: 0.5em;
	color: var(--neutral-10);
	margin-top: 0.4em;
}

.slide p, .slide li {
	font-size: var(--slide-fs-text);
	line-height: var(--font-line-height-7);
}

.slide ul, .slide ol {
	padding-left: 1em;
	margin-bottom: 0.5em;
}

.slide li {
	margin-bottom: 0.7em;
}


.slide a {
	color: var(--tertiary-6);
}

/* Cover slide */
.slide-cover {
	text-align: center;
	justify-content: center;
	align-items: center;
}

.slide-cover h1 {
	font-size: var(--slide-fs-h1);
}

/* Center layout */
.slide-center {
	text-align: center;
	align-items: center;
}

/* A .slide-row inside a center slide is just a figure wrapper: give it the
   slide's remaining height (flex:1) and stretch the figure to fill it, so the
   image's max-height:100% resolves against a real box (same mechanism as a
   figure placed directly under .slide). */
.slide-center > .slide-row {
	flex: 1;
	min-height: 0;
	width: 100%;
	display: flex;
	justify-content: center;
}

/* Quote-driven slides: text-only, centered, larger type */
.slide-quote {
	text-align: center;
	align-items: center;
}

.slide-quote blockquote,
.slide-quote p,
.slide-quote li {
	font-size: var(--slide-fs-quote);
	line-height: 1.5;
}

.slide-quote cite {
	display: block;
	font-style: italic;
	font-size: 0.7em;
	color: var(--neutral-7);
	margin-top: 0.3em;
	font-weight: normal;
}

/* Next-token prediction tables */
.token-pred {
	border-collapse: collapse;
	margin: 0 auto;
	font-family: var(--font-mono, monospace);
	font-size: var(--slide-fs-text);
}

.token-pred td {
	padding: 0.4em 0.9em;
	border: var(--border-width-0) solid var(--neutral-7);
	text-align: center;
}

.token-pred .subject {
	color: var(--error-6);
}

.token-pred td:empty,
.token-pred .empty {
	border: 0;
	padding: 0;
}

.token-pred .pred {
	text-align: left;
	color: var(--tertiary-6);
}

.token-pred .prob {
	text-align: right;
	color: var(--neutral-5);
	font-variant-numeric: tabular-nums;
}

.token-pred tr:first-child .pred,
.token-pred tr:first-child .prob {
	font-weight: bold;
}

/* Side-by-side: heading top, body left, figure right */
.slide-side {
	flex-direction: column;
}

.slide-side > .slide-row {
	flex: 1;
	display: flex;
	gap: 4cqw;
	align-items: center;
}

.slide-side > .slide-row > .slide-body {
	flex: 1;
}

.slide-side > .slide-row > figure {
	flex: 1;
}

/* Figures in slides */
.slide figure {
	flex: 1;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	min-height: 0;
}

.slide figure img, .slide img {
	max-width: 100%;
	max-height: var(--slide-img-h);
	object-fit: contain;
	border-radius: var(--border-radius-1);
}

/* Center-layout figures are the full-width sole content of the slide, so their
   image should fill the flex box the layout grants — not a fixed viewport
   fraction. A hard 55vh cap can exceed that box on wide-but-short screens (old
   projectors, landscape phones): the image then overflowed and spilled over the
   heading above and footnote below. max-height:100% ties it to the real box.
   (slide-side/slide-figures images stay capped at 55vh/45vh — their figures are
   sized by content, not a definite flex box, so a % height has nothing to
   resolve against.) */
.slide-center figure img {
	max-height: 100%;
}

.slide figure.molecule img {
	border-radius: 0;

	.dark & {
		filter: invert(1) hue-rotate(180deg);
	}
}

.slide figcaption {
	text-align: center;
	font-size: var(--slide-fs-small);
	color: var(--neutral-7); /* neutral-6 fails WCAG AA (~3.2:1) at this size */
	margin-top: 0.5em;
}

/* Side-by-side figure pairs */
.slide-figures {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: var(--size-3);
	align-items: center;
	margin-block: auto;
	width: 100%;
}

.slide-figures figure img {
	max-height: 45cqh;
	transition: transform 0.2s ease;
	cursor: zoom-in;
}

.slide-figures figure img:hover {
	transform: scale(1.4);
	position: relative;
	z-index: 10;
}

/* Definition grid — for cards-of-definitions slide layouts */
.slide-defs {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: var(--size-3);
	margin-block: auto;
	width: 100%;
}

.slide-defs > div {
	padding: var(--size-2);
}

.slide-defs dt {
	font-weight: 600;
	margin-bottom: var(--size-1);
}

.slide-defs dd {
	margin: 0;
	font-size: var(--slide-fs-fine);
}

/* Tables in slides — covers stray TableGenerator tg-* classes */
.slide table {
	border-collapse: collapse;
	margin: 0.8em auto;
	font-size: var(--slide-fs-fine);
}

.slide table th,
.slide table td {
	padding: var(--size-1) var(--size-2);
	border: var(--border-width-0) solid var(--neutral-7);
	text-align: left;
	vertical-align: top;
}

.slide table th,
.slide table .tg-head,
.slide table .tg-row-head {
	background: var(--neutral-3);
	font-weight: 600;
}

/* List items in table cells. Two quirks to normalize: (a) Stellar's reset
   strips markers from every <ul>, so nested <ul><li> cells lost their bullets
   while (b) bare <li> dropped straight into a <td> (a TableGenerator export
   quirk, no <ul>) escaped the reset and showed bullets — hanging left onto the
   gridline. Re-assert a disc marker on both, positioned inside so it can't
   collide with the cell border. Scoped to tables; body lists elsewhere stay
   deliberately marker-less. */
.slide table li {
	list-style-type: disc;
	list-style-position: inside;
	margin-bottom: 0.2em;
	/* Match the cell's table font size — .slide li otherwise forces body size,
	   making bulleted cells tower over plain ones in the same table. */
	font-size: inherit;
	line-height: 1.35;
}

/* Slide controls — visible on hover */
.slide-controls {
	position: absolute;
	bottom: 0;
	left: 0;
	right: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	gap: var(--size-2);
	padding: var(--size-2);
	opacity: 0;
	transition: opacity 0.2s;
}

.talk-viewport:hover .slide-controls,
.talk-viewport:focus-within .slide-controls {
	opacity: 1;
}

/* No hover on touch devices: keep the controls visible (landscape; portrait
   has its own sticky treatment below), on a translucent strip for legibility
   over slide content. */
@media (hover: none) {
	.slide-controls {
		opacity: 1;
		background: color-mix(in oklch, var(--neutral-1) 70%, transparent);
		backdrop-filter: blur(8px);
	}
}

.slide-control {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.5rem;
	height: 2.5rem;
	border-radius: var(--border-radius-1);
	color: var(--neutral-8);
	text-decoration: none;
	transition: color 0.15s, background 0.15s;
}

.slide-control:hover {
	color: var(--neutral-12);
	background: var(--neutral-3);
	text-decoration: none;
}

.slide-control.disabled {
	visibility: hidden;
}

.slide-control svg {
	width: 1.25em;
	height: 1.25em;
}

.slide-counter {
	font-size: var(--font-size-0);
	color: var(--neutral-7);
	font-family: var(--font-mono);
}

/* Overview grid */
.talk-overview {
	padding: var(--size-4) var(--size-3);
}

.talk-overview h1 {
	font-size: var(--font-size-4);
	margin-bottom: var(--size-1);
}

.talk-overview-meta {
	font-size: var(--font-size-0);
	color: var(--neutral-7);
	margin-bottom: var(--size-3);
}

.slide-grid {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
	gap: var(--size-3);
}

.slide-thumb {
	display: block;
	border: var(--border-width-0) solid var(--neutral-4);
	border-radius: var(--border-radius-1);
	overflow: hidden;
	text-decoration: none;
	color: var(--neutral-12);
	transition: border-color 0.15s, box-shadow 0.15s;
}

.slide-thumb:hover {
	border-color: var(--primary-7);
	box-shadow: var(--shadow-2);
	text-decoration: none;
}

.slide-thumb-content {
	aspect-ratio: 16 / 9;
	padding: var(--size-2);
	overflow: hidden;
	pointer-events: none;
	font-size: 0.45rem;
	position: relative;
}

/* Thumbnail overrides — no .slide class on thumbs, just tame the content */
.slide-thumb-content .slide-row {
	display: flex;
	gap: 0.3rem;
}

.slide-thumb-content .slide-body,
.slide-thumb-content .slide-row,
.slide-thumb-content figure {
	margin-block: 0;
	flex: initial;
	min-height: 0;
}

.slide-thumb-content .slide-footnotes {
	display: none;
}

.slide-thumb-content h1 { font-size: clamp(0.6rem, 1.5vw, 0.9rem); margin-bottom: 0.2em; }
.slide-thumb-content h2 { font-size: clamp(0.5rem, 1.2vw, 0.7rem); margin-bottom: 0.1em; }
.slide-thumb-content h3 { font-size: clamp(0.45rem, 1vw, 0.6rem); margin-bottom: 0.1em; }
.slide-thumb-content p, .slide-thumb-content li { font-size: 0.4rem; line-height: 1.3; }
.slide-thumb-content ul, .slide-thumb-content ol { padding-left: 0.5rem; margin-bottom: 0.1em; }
.slide-thumb-content img { max-height: 4rem; max-width: 100%; object-fit: contain; }

.slide-thumb-number {
	display: block;
	padding: var(--size-0) var(--size-1);
	font-size: var(--font-size-0);
	color: var(--neutral-7);
	font-family: var(--font-mono);
	border-top: var(--border-width-0) solid var(--neutral-3);
}

/* Click-reveal lists */
.clicks {
	list-style: none;
	padding-left: 0;
}

/* Safety valve: a slide whose content genuinely exceeds the stage (dense old
   decks, bibliographies, image galleries) scrolls instead of clipping. Slides
   that fit are unaffected — their centered layout has no overflow. */
.slide {
	overflow-y: auto;
	position: relative;
}

/* Scroll affordance: an over-full slide otherwise looks complete, giving no
   hint there's more below the fold. A sticky bottom fade signals "keep
   scrolling" and disappears at the end via a scroll-driven animation. Behind
   @supports so browsers without scroll-timelines simply get no fade (rather
   than one stuck on permanently). The pseudo-element uses negative margin so it
   overlays content instead of adding height, and a high order so it sits after
   the footnotes (which carry order:99). */
@supports (animation-timeline: scroll()) {
	.slide::after {
		content: "";
		order: 100;
		position: sticky;
		bottom: 0;
		align-self: stretch;
		height: 3em;
		margin-top: -3em;
		pointer-events: none;
		background: linear-gradient(transparent, var(--neutral-1));
		/* Base = invisible, so slides that fit (inactive scroll timeline) show
		   nothing. When the slide actually scrolls the timeline drives opacity
		   from 1 at the top to 0 at the bottom — present until you reach the
		   end. */
		opacity: 0;
		animation: slide-scroll-fade linear;
		animation-timeline: scroll(self block);
	}

	@keyframes slide-scroll-fade {
		from { opacity: 1; }
		to { opacity: 0; }
	}
}

.bibliography {
	columns: 3;
	column-gap: var(--size-3);
	font-size: var(--slide-fs-small);
	line-height: 1.4;
	padding-left: var(--size-3);
	width: 100%;
}

.bibliography li {
	margin-bottom: 0.5em;
	break-inside: avoid;
	font-size: var(--slide-fs-small);
}

.slide-footnotes {
	margin-top: auto;
	padding-top: var(--size-1);
	border-top: var(--border-width-0) solid var(--neutral-8);
	order: 99;
	text-align: right;
}

/* Everything in a footnote block is footnote-sized, not just <small>. Some
   older slides put a <ul> of source links here, which otherwise rendered at
   body size and pushed the slide off the stage. neutral-7 not neutral-6: the
   lighter gray fails WCAG AA (~3.2:1) at footnote size. */
.slide-footnotes,
.slide-footnotes small,
.slide-footnotes p,
.slide-footnotes li {
	color: var(--neutral-7);
	font-size: var(--slide-fs-small);
}

.slide-footnotes li {
	margin-bottom: 0.2em;
}

/* Portrait (phones): the 16:9 stage would render slides as an unreadable
   thumbnail, so drop the stage entirely and become a document — normal flow,
   rem-based type, side-by-side rows stacked, the whole slide scrollable. */
@media (orientation: portrait) {
	.talk-viewport {
		display: block;
		overflow-y: auto;
	}

	.talk-viewport > .slide {
		width: 100%;
		aspect-ratio: auto;
		min-height: 100%;
		container-type: normal;
		/* extra bottom clearance for the sticky slide-controls bar */
		padding: var(--size-2) var(--size-2) 5rem;

		--slide-fs-h1: var(--font-size-8);
		--slide-fs-h2: var(--font-size-7);
		--slide-fs-h3: var(--font-size-3);
		--slide-fs-text: var(--font-size-1);
		--slide-fs-quote: var(--font-size-2);
		--slide-fs-small: var(--font-size-0);
		--slide-fs-fine: var(--font-size-0);
		--slide-img-h: 45vh;
		--slide-measure: 100%;
	}

	.talk-viewport .slide-side > .slide-row {
		flex-direction: column;
		align-items: stretch;
	}

	.talk-viewport .slide-figures,
	.talk-viewport .slide-defs {
		grid-template-columns: 1fr;
	}

	.talk-viewport .slide-figures figure img {
		max-height: 40vh;
	}

	.talk-viewport .bibliography {
		columns: 1;
	}

	/* No hover on touch: keep controls visible, riding the bottom of the
	   scrollable viewport. */
	.talk-viewport .slide-controls {
		position: sticky;
		bottom: 0;
		opacity: 1;
		background: color-mix(in oklch, var(--neutral-1) 80%, transparent);
		backdrop-filter: blur(8px);
	}
}

/* Recipes */

.recipes-intro {
	color: var(--neutral-7);
	font-size: var(--font-size-0);
	margin-bottom: var(--size-3);
}

.recipe-group {
	margin-bottom: var(--size-3);
}

.recipe-group h2 {
	font-size: var(--font-size-2);
	margin-bottom: var(--size-1);
}

.recipe-links {
	list-style: none;
	padding: 0;
}

.recipe-links li {
	padding: var(--size-0) 0;
	border-bottom: var(--border-width-0) solid var(--neutral-3);
	display: flex;
	justify-content: space-between;
	gap: var(--size-1);
	flex-wrap: wrap;
}

.recipe-links li:last-child {
	border-bottom: none;
}

.recipe-chef {
	color: var(--neutral-7);
	font-size: var(--font-size-0);
}

.breadcrumb {
	font-size: var(--font-size-0);
	margin-bottom: var(--size-1);
}

/* Menus */

.menu-index {
	list-style: none;
	padding: 0;
}

.menu-index li {
	padding: var(--size-1) 0;
	border-bottom: var(--border-width-0) solid var(--neutral-3);
	display: flex;
	justify-content: space-between;
	gap: var(--size-1);
}

.menu-index li:last-child {
	border-bottom: none;
}

.menu-index .meta {
	color: var(--neutral-7);
	font-size: var(--font-size-0);
	display: flex;
	gap: var(--size-1);
}

.menu-checklist {
	list-style: none;
	padding: 0;
	margin-top: var(--size-2);
}

.menu-item {
	display: flex;
	align-items: baseline;
	gap: var(--size-1);
	padding: var(--size-1) 0;
	border-bottom: var(--border-width-0) solid var(--neutral-3);
	font-size: var(--font-size-2);
}

.menu-item:last-child {
	border-bottom: none;
}

.menu-item input[type="checkbox"] {
	/* ≥24px touch target (WCAG 2.5.8) at the checklist's font size */
	width: 1.4em;
	height: 1.4em;
	accent-color: var(--secondary-5);
	cursor: pointer;
	flex-shrink: 0;
}

.menu-item .menu-label-wrap {
	display: flex;
	align-items: baseline;
	gap: var(--size-1);
	cursor: pointer;
}

.menu-item.checked .menu-label {
	text-decoration: line-through;
	color: var(--neutral-7);
}

/* Cooking sessions */

.cook-actions {
	display: flex;
	align-items: center;
	gap: var(--size-2);
	flex-wrap: wrap;
	margin-top: var(--size-1);
}

.cook-start-btn,
.cook-done-btn,
.template-save,
.template-revert {
	font: inherit;
	font-size: var(--font-size-0);
	padding: var(--size-0) var(--size-2);
	border: var(--border-width-0) solid var(--neutral-6);
	border-radius: var(--border-radius-1);
	background: var(--neutral-2);
	color: var(--neutral-12);
	cursor: pointer;
}

.cook-start-btn:hover,
.cook-done-btn:hover,
.template-save:hover,
.template-revert:hover {
	background: var(--neutral-3);
	border-color: var(--secondary-5);
}

.cook-done-btn {
	margin-top: var(--size-3);
}

.cook-open-link,
.cook-provenance {
	font-size: var(--font-size-0);
	color: var(--neutral-7);
}

.cooking-done-badge {
	font-size: var(--font-size-0);
	color: var(--secondary-5);
}

.cooking-progress {
	font-size: var(--font-size-2);
	font-weight: 600;
	color: var(--secondary-5);
	margin-top: var(--size-0);
}

.cook-group {
	margin-top: var(--size-2);
}

.cook-group h2 {
	font-size: var(--font-size-2);
	margin-bottom: var(--size-0);
}

.cook-step-num {
	color: var(--neutral-7);
	margin-right: var(--size-0);
}

/* Cross-document view transitions: same-origin navigations crossfade instead
   of hard-swapping. Progressive enhancement — unsupported browsers (Firefox)
   ignore the at-rule; reduced-motion users keep instant swaps. */
@media (prefers-reduced-motion: no-preference) {
	@view-transition {
		navigation: auto;
	}

	/* Without a name, the fixed sidebar is part of the root snapshot and
	   crossfades against itself on every navigation — a visible flicker on a
	   chrome element that never actually moved. Naming it gives it its own
	   transition group, so the browser holds it still and only the content
	   crossfades. Anything persistent and fixed wants this. */
	.sidebar-nav {
		view-transition-name: sidebar;
	}

	/* The default 250ms root crossfade reads as lag on a same-origin navigation
	   that is otherwise instant. 180ms still registers as a transition. */
	::view-transition-old(root),
	::view-transition-new(root) {
		animation-duration: 180ms;
	}
}

/* --- editing ------------------------------------------------------------
   The reading view and the editing view are meant to look like the same page.
   A reader who can edit sees only a faint affordance on hover; the editor,
   when it morphs in, inherits the article's own typography so the words do not
   move when it appears. */
.post-body.editable {
  cursor: text;
  border-radius: var(--radius-2, 4px);
  outline: 1px solid transparent;
  transition: outline-color 120ms ease;
}
.post-body.editable:hover {
  outline-color: var(--border, rgba(0, 0, 0, 0.12));
}
.post-body.editing overtype-editor {
  display: block;
  width: 100%;
  font: inherit;
}
.edit-hint {
  margin-top: 1rem;
  font-size: 0.85em;
  opacity: 0.7;
}
.edit-hint button {
  font: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
}

/* --- editor -------------------------------------------------------------- */
/* Addressed by UUID, so it shows the public URL rather than being it. */

.edit-identity code {
	font-size: var(--font-size--1);
	color: var(--neutral-9);
}

.edit-unrouted {
	color: var(--neutral-9);
	font-style: italic;
}

.edit-error {
	background: var(--danger-3, var(--primary-3));
	color: var(--neutral-12);
	border-radius: var(--border-radius-1);
	padding: 0.5em 0.75em;
	margin: 1em 0;
}

.edit-facts {
	display: grid;
	gap: 0.6em;
	margin: 1.5em 0;
}

.edit-fact {
	display: grid;
	grid-template-columns: minmax(6em, 10em) 1fr;
	align-items: center;
	gap: 0.75em;
}

.edit-fact-name {
	font-family: var(--font-mono, monospace);
	font-size: var(--font-size--1);
	color: var(--neutral-10);
}

.edit-facts input[type="text"] {
	padding: 0.35em 0.5em;
	border: 1px solid var(--neutral-6);
	border-radius: var(--border-radius-0);
	background: var(--neutral-0, transparent);
	color: inherit;
	font: inherit;
	min-width: 0;
}

.edit-new-fact {
	display: flex;
	gap: 0.5em;
	border: 1px dashed var(--neutral-6);
	border-radius: var(--border-radius-0);
	padding: 0.5em 0.75em;
}

.edit-new-fact legend {
	font-size: var(--font-size--1);
	color: var(--neutral-10);
	padding: 0 0.4em;
}

.edit-actions {
	display: flex;
	align-items: center;
	gap: 0.75em;
}

.login-form {
	display: grid;
	gap: 0.75em;
	max-width: 22em;
	margin: 1.5em 0;
}

.login-form label {
	display: grid;
	gap: 0.35em;
	font-size: var(--font-size--1);
	color: var(--neutral-10);
}

.login-form input[type="password"] {
	padding: 0.35em 0.5em;
	border: 1px solid var(--neutral-6);
	border-radius: var(--border-radius-0);
	background: var(--neutral-0, transparent);
	color: inherit;
	font: inherit;
}

.login-form button,
.login button {
	justify-self: start;
	background: none;
	border: var(--border-width-0) solid var(--neutral-4);
	border-radius: var(--border-radius-1);
	padding: var(--size-0) var(--size-1);
	cursor: pointer;
	color: var(--neutral-11);
	font-size: var(--font-size-0);
}

.login-form button:hover,
.login button:hover {
	border-color: var(--primary-7);
	color: var(--primary-9);
}

.edit-derived {
	margin: 1.5em 0;
	font-size: var(--font-size--1);
	color: var(--neutral-9);
}

.edit-derived dl {
	display: grid;
	grid-template-columns: minmax(6em, 10em) 1fr;
	gap: 0.3em 0.75em;
	margin: 0.5em 0 0;
}

.edit-derived dt {
	font-family: var(--font-mono, monospace);
}

.edit-derived dd {
	margin: 0;
	overflow-wrap: anywhere;
}

.edit-link {
	text-align: right;
	font-size: var(--font-size--1);
	margin: 0;
}

/* --- tag pages and cross-type listings ----------------------------------- */

.tag-cloud {
	list-style: none;
	padding: 0;
	display: flex;
	flex-wrap: wrap;
	gap: 0.5em 0.75em;
}

.tag-cloud li {
	display: flex;
	align-items: baseline;
	gap: 0.3em;
}

.tag-count {
	font-size: var(--font-size--1);
	color: var(--neutral-9);
}

.listing {
	list-style: none;
	padding: 0;
}

.listing li {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	justify-content: space-between;
	gap: 0.5em;
	padding: 0.5em 0;
	border-bottom: 1px solid var(--neutral-4);
}

.listing .meta {
	display: flex;
	align-items: baseline;
	gap: 0.75em;
	font-size: var(--font-size--1);
	color: var(--neutral-9);
}

.edit-danger {
	display: flex;
	align-items: center;
	gap: 0.75em;
	margin: 2em 0 0;
}

.edit-danger .delete-btn {
	color: var(--danger-9, var(--neutral-11));
	border-color: var(--danger-6, var(--neutral-6));
}

/* --- template editor ----------------------------------------------------- */
/* Source beside what it draws, so this page opts out of the reading measure the
   way the talk grid does: 48rem is right for prose and wrong for two panes. */

main.content:has(.template-panes) {
	max-width: none;
}

/* auto-fit is the whole responsive story — two panes while there is room for
   two, one below that, with no breakpoint to keep in sync with the layout. */
.template-panes {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(30rem, 1fr));
	gap: var(--size-4);
	align-items: stretch;
	margin: var(--size-4) 0;
}

.template-source {
	display: flex;
	flex-direction: column;
	gap: var(--size-2);
	min-width: 0;
}

#template-src {
	flex: 1;
	/* Tall enough to hold a real template without scrolling for it, bounded so
	   it cannot outgrow the window on a large display. */
	min-height: clamp(20rem, 62vh, 52rem);
	width: 100%;
	font-family: var(--font-mono, monospace);
	font-size: var(--font-size--1);
	line-height: 1.55;
	tab-size: 2;
	padding: var(--size-2);
	border: var(--border-width-0) solid var(--neutral-6);
	border-radius: var(--border-radius-1);
	background: var(--neutral-1);
	color: var(--neutral-12);
	resize: vertical;
}

.template-preview {
	min-width: 0;
	min-height: clamp(20rem, 62vh, 52rem);
	border: var(--border-width-0) solid var(--neutral-6);
	border-radius: var(--border-radius-1);
	background: var(--neutral-1);
	overflow: hidden;
}

.template-preview iframe {
	display: block;
	width: 100%;
	height: 100%;
	min-height: inherit;
	border: 0;
}

.template-preview .edit-error {
	margin: var(--size-2);
}

.template-saved {
	color: var(--neutral-10);
	font-size: var(--font-size--1);
	margin: var(--size-3) 0;
}

/* Drops console: the storage line, then one row per drop. */

.drops-storage {
	margin-block-end: var(--size-3);
}

.drops-meter {
	height: var(--size-1);
	border-radius: var(--border-radius-6);
	background: var(--neutral-3);
	overflow: hidden;
}

.drops-meter-fill {
	height: 100%;
	background: var(--primary-9);
}

.drops-storage-line {
	margin-block: var(--size-1) 0;
	font-size: var(--font-size-0);
	color: var(--neutral-11);
}

.drops-storage-line strong {
	color: var(--neutral-12);
	font-family: var(--font-mono);
}

.drops-sep {
	color: var(--neutral-8);
	margin-inline: var(--size-0);
}

.drops-storage-note {
	margin-block: var(--size-0) 0;
	font-size: var(--font-size--1);
	color: var(--neutral-9);
}

.drops-empty {
	color: var(--neutral-10);
}

/* A table is the honest shape here, so it gets its own scroller rather than
   pushing the page sideways on a phone. */
.drops-scroll {
	overflow-x: auto;
}

.drops-table {
	width: 100%;
	border-collapse: collapse;
	font-size: var(--font-size--1);
}

/* The filename is the one thing that can be arbitrarily long, so it is the one
   column allowed to wrap — otherwise it pushes the row's buttons past the edge
   of the page, which is where they were. */
.drops-table td:first-child {
	white-space: normal;
	min-width: 10rem;
}

/* The console is a table with six columns and a row of buttons; the 48rem
   measure that suits prose puts the delete button off the edge of the page.
   Widening from the child rather than plumbing a flag through PageConfig,
   because the only thing that wants this is the one page that has a table. */
main.content:has(.drops) {
	max-width: 78rem;
}

.drops-table th {
	text-align: left;
	font-size: var(--font-size--1);
	font-weight: var(--font-weight-bold);
	text-transform: uppercase;
	letter-spacing: 0.08em;
	color: var(--neutral-9);
	padding: var(--size-0) var(--size-1);
	border-block-end: var(--border-width-0) solid var(--neutral-4);
	white-space: nowrap;
}

.drops-table td {
	padding: var(--size-1);
	border-block-end: var(--border-width-0) solid var(--neutral-3);
	vertical-align: baseline;
	white-space: nowrap;
}

.drops-num {
	text-align: right;
	font-family: var(--font-mono);
}

.drops-name {
	color: var(--neutral-12);
}

.drops-note {
	display: block;
	font-size: var(--font-size--1);
	color: var(--neutral-9);
	white-space: normal;
}

/* An expired drop stays in the list until its bytes are swept, greyed rather
   than gone: it is still using the space the line above is counting. */
.drops-dead td {
	color: var(--neutral-8);
}

.drops-dead .drops-name {
	color: var(--neutral-9);
	text-decoration: line-through;
}

/* The upload form. A real multipart form, so the controls are the browser's
   own; what is styled here is only how they sit together. */

.drop-upload {
	margin-block-end: var(--size-3);
	padding: var(--size-2);
	border: var(--border-width-0) solid var(--neutral-4);
	border-radius: var(--border-radius-1);
	background: var(--neutral-2);
}

.drop-upload form {
	display: flex;
	align-items: center;
	gap: var(--size-2);
	flex-wrap: wrap;
}

.drop-field {
	display: inline-flex;
	align-items: center;
	gap: var(--size-0);
	font-size: var(--font-size-0);
	color: var(--neutral-10);
}

.drop-field-note {
	flex: 1 1 12rem;
}

.drop-upload input[type="text"] {
	font: inherit;
	font-size: var(--font-size-0);
	padding: var(--size-0) var(--size-0);
	border: var(--border-width-0) solid var(--neutral-6);
	border-radius: var(--border-radius-0);
	background: var(--neutral-1);
	color: var(--neutral-12);
	min-width: 4rem;
}

/* The quick buttons write into the field beside them, so they have to read as
   shortcuts rather than as five more fields — which is exactly what they looked
   like when they shared the input's box. */
.drop-ttl-quick {
	display: inline-flex;
	gap: var(--size-0);
}

.drop-field-note input[type="text"] {
	flex: 1;
	min-width: 0;
}

.drop-upload input[type="file"] {
	font-size: var(--font-size--1);
	color: var(--neutral-11);
	max-width: 100%;
}

.drop-quick,
.drop-submit {
	font: inherit;
	font-size: var(--font-size--1);
	padding: 0 var(--size-0);
	border: var(--border-width-0) solid var(--neutral-6);
	border-radius: var(--border-radius-0);
	background: var(--neutral-1);
	color: var(--neutral-11);
	cursor: pointer;
}

.drop-submit {
	font-size: var(--font-size-0);
	padding: var(--size-0) var(--size-2);
	background: var(--neutral-3);
	color: var(--neutral-12);
	margin-inline-start: auto;
}

.drop-quick {
	background: transparent;
	color: var(--neutral-10);
	border-color: var(--neutral-4);
}

.drop-quick:hover,
.drop-submit:hover {
	border-color: var(--secondary-5);
	background: var(--neutral-3);
}

.drop-submit:disabled {
	cursor: progress;
	color: var(--neutral-9);
}

.drop-error {
	margin-block: var(--size-1) 0;
	font-size: var(--font-size-0);
	color: var(--error-9);
}

/* The link exists exactly once, in the response to the upload that made it —
   it is not stored and cannot be rendered again. Hence the copy button. */
.drop-new {
	display: flex;
	align-items: center;
	gap: var(--size-1);
	margin-block-start: var(--size-1);
	flex-wrap: wrap;
}

.drop-new-link {
	font-family: var(--font-mono);
	font-size: var(--font-size--1);
	word-break: break-all;
}

.drops-actions {
	text-align: right;
	white-space: nowrap;
}

.drops-actions .drop-quick + .drop-quick {
	margin-inline-start: var(--size-0);
}

.drop-danger:hover {
	border-color: var(--error-9);
	color: var(--error-9);
}

/* Last-downloaded sits under the count, quieter than it. */
.drops-last {
	display: block;
	font-family: var(--font-sans);
	font-size: var(--font-size--1);
	color: var(--neutral-9);
}

/* The landing page. Seen by people who are not me, on a link from a friend:
   what it is, how big, how long it lasts, and one button. */
.drop-offer {
	max-width: 28rem;
	margin-block-start: var(--size-4);
}

.drop-offer h1 {
	font-size: var(--font-size-3);
	word-break: break-word;
}

.drop-offer-meta {
	color: var(--neutral-10);
	margin-block: var(--size-1) var(--size-3);
}

.drop-offer-get {
	display: inline-block;
	font-size: var(--font-size-1);
	padding: var(--size-1) var(--size-3);
	border: var(--border-width-0) solid var(--neutral-6);
	border-radius: var(--border-radius-1);
	background: var(--neutral-2);
	color: var(--neutral-12);
}

.drop-offer-get:hover {
	border-color: var(--secondary-5);
	background: var(--neutral-3);
	text-decoration: none;
}

/* The limits, editable where they are reported. */
.drops-limits {
	display: flex;
	align-items: center;
	gap: var(--size-1);
	flex-wrap: wrap;
	margin-block-start: var(--size-1);
	font-size: var(--font-size--1);
	color: var(--neutral-9);
}

.drops-limits input[type="text"] {
	font: inherit;
	font-size: var(--font-size--1);
	padding: 0 var(--size-0);
	border: var(--border-width-0) solid var(--neutral-4);
	border-radius: var(--border-radius-0);
	background: var(--neutral-1);
	color: var(--neutral-12);
	width: 6rem;
}

.drops-locked {
	font-size: var(--font-size--1);
	margin-inline-start: var(--size-0);
}

.drop-unlock {
	display: flex;
	gap: var(--size-1);
	align-items: center;
	flex-wrap: wrap;
}

.drop-unlock input[type="password"] {
	font: inherit;
	font-size: var(--font-size-1);
	padding: var(--size-0) var(--size-1);
	border: var(--border-width-0) solid var(--neutral-6);
	border-radius: var(--border-radius-1);
	background: var(--neutral-1);
	color: var(--neutral-12);
}

/* The downloads cell opens the row: a count you can press, because the number
   is never the interesting part on its own. */
.drops-count {
	font: inherit;
	font-family: var(--font-mono);
	background: none;
	border: none;
	border-bottom: var(--border-width-0) dotted var(--neutral-7);
	color: var(--neutral-12);
	cursor: pointer;
	padding: 0;
}

.drops-count:hover {
	color: var(--primary-9);
	border-color: var(--primary-9);
}

.drops-fetches td {
	padding-block: var(--size-1);
	background: var(--neutral-2);
}

.drops-fetch-list {
	list-style: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	gap: var(--size-0);
	font-size: var(--font-size--1);
}

.drops-fetch-list li {
	display: flex;
	gap: var(--size-1);
	align-items: baseline;
	flex-wrap: wrap;
}

.drops-fetch-when {
	font-family: var(--font-mono);
	color: var(--neutral-12);
}

.drops-fetch-ago {
	color: var(--neutral-9);
}

.drops-fetch-who {
	color: var(--neutral-11);
}

/* Four short controls, and everything rarer one click further in. */
.drops-actions {
	text-align: right;
	white-space: nowrap;
}

.drops-actions .drop-quick {
	display: inline-block;
	margin-inline-start: var(--size-0);
	margin-block-start: var(--size-0);
	text-decoration: none;
}

a.drop-quick:hover {
	text-decoration: none;
}

.drops-fetch-ip {
	font-family: var(--font-mono);
	color: var(--neutral-10);
}

/* The full agent string, kept readable rather than kept short: it is the thing
   that distinguishes one fetch of a private link from another. */
.drops-fetch-agent {
	flex-basis: 100%;
	font-family: var(--font-mono);
	font-size: var(--font-size--2, 0.72rem);
	color: var(--neutral-8);
	word-break: break-all;
}

/* The link, shown in full so it can be read as well as copied. */
.drops-link {
	margin: 0 0 var(--size-1);
	font-size: var(--font-size--1);
	word-break: break-all;
}

.drops-link code {
	background: var(--neutral-3);
	padding: var(--size-0);
	border-radius: var(--border-radius-0);
	color: var(--neutral-12);
}

/* The fetch command. Same treatment as the link, plus room for its button:
   the line is long and wraps, and the button should not be dragged along the
   last line of it. */
.drops-fetch-cmd {
	display: flex;
	align-items: baseline;
	gap: var(--size-1);
	flex-wrap: wrap;
	margin: 0 0 var(--size-1);
	font-size: var(--font-size--1);
}

.drops-fetch-cmd code {
	background: var(--neutral-3);
	padding: var(--size-0);
	border-radius: var(--border-radius-0);
	color: var(--neutral-12);
	word-break: break-all;
	min-width: 0;
}

.drops-more-actions {
	display: flex;
	gap: var(--size-0);
	flex-wrap: wrap;
	margin-block-end: var(--size-1);
}

.drops-of {
	color: var(--neutral-9);
}

.drops-settings {
	display: flex;
	align-items: center;
	gap: var(--size-1);
	flex-wrap: wrap;
	margin-block-end: var(--size-1);
	font-size: var(--font-size--1);
}

.drops-settings input[type="text"] {
	font: inherit;
	font-size: var(--font-size--1);
	padding: 0 var(--size-0);
	border: var(--border-width-0) solid var(--neutral-4);
	border-radius: var(--border-radius-0);
	background: var(--neutral-1);
	color: var(--neutral-12);
}

/* The upload bar. Present only while an upload is in flight — it is the one
   thing on this page that reports something happening rather than something
   stored. */
.drop-progress {
	display: flex;
	align-items: center;
	gap: var(--size-1);
	margin-block-start: var(--size-1);
}

.drop-progress-track {
	flex: 1;
	height: var(--size-0);
	border-radius: var(--border-radius-6);
	background: var(--neutral-3);
	overflow: hidden;
}

.drop-progress-fill {
	height: 100%;
	width: 0;
	background: var(--primary-9);
	transition: width 0.2s linear;
}

.drop-progress-text {
	font-family: var(--font-mono);
	font-size: var(--font-size--1);
	color: var(--neutral-10);
	white-space: nowrap;
}

/* Working, length unknown: a moving stripe rather than a full bar, which would
   read as finished. */
.drop-progress-fill.drop-progress-unknown {
	background-image: repeating-linear-gradient(
		45deg,
		color-mix(in oklch, var(--primary-9) 45%, transparent) 0 8px,
		transparent 8px 16px
	);
	background-color: var(--neutral-4);
	animation: drop-progress-slide 0.8s linear infinite;
}

@keyframes drop-progress-slide {
	from { background-position: 0 0; }
	to { background-position: 22px 0; }
}

/* ---- the person ------------------------------------------------------ */

/* /about and /now are one entity drawn through two templates. What is styled
   here is the person's own header and their likes; the prose reuses .post-body
   like every other piece of content. */

.person header .meta {
	display: flex;
	flex-wrap: wrap;
	gap: 0.25em 0.6em;
	align-items: baseline;
}

.person-role,
.person-where {
	color: var(--neutral-7);
	font-size: var(--font-size-0);
}

.like-category {
	font-size: var(--font-size-2);
	color: var(--neutral-9);
	margin-top: 1.6em;
	margin-bottom: 0.4em;
}

.like-list {
	list-style: none;
	padding: 0;
	margin: 0;
}

.like-list li {
	padding: 0.15em 0;
}

/* A note is an aside, not a second title: it sits after the thing it is about
   and must not compete with it. */
.like-note {
	color: var(--neutral-7);
	font-size: var(--font-size-0);
}

.like-note::before {
	content: " — ";
}

.person-now-link,
.now-footer {
	margin-top: 2em;
	font-size: var(--font-size-0);
	color: var(--neutral-7);
}

/* ---- the timeline ---------------------------------------------------- */

/* One row per thing that happened, in one column. The lane is a label rather
   than a colour: lanes are open-ended — a content type invented in the browser
   gets a row here with no CSS written for it — so nothing may depend on
   knowing their names in advance. */

.timeline-scope {
	font-size: var(--font-size-0);
	color: var(--neutral-7);
}

.timeline-list {
	list-style: none;
	padding: 0;
	margin: 1.5em 0 0;
}

.timeline-list .event {
	display: grid;
	grid-template-columns: 6.5rem auto 1fr auto;
	gap: 0 0.75em;
	align-items: baseline;
	padding: 0.45em 0;
	border-bottom: 1px solid var(--neutral-3);
}

.event-at {
	color: var(--neutral-7);
	font-size: var(--font-size-0);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

.event-verb {
	color: var(--neutral-8);
	font-size: var(--font-size-0);
	white-space: nowrap;
}

.event-title {
	min-width: 0;
	overflow-wrap: anywhere;
}

.event-lane {
	color: var(--neutral-7);
	font-size: var(--font-size-0);
	justify-self: end;
	white-space: nowrap;
}

.event-private {
	color: var(--secondary-7);
	font-size: var(--font-size-0);
	white-space: nowrap;
}

/* Narrow screens: the date leads, everything else wraps under it rather than
   squeezing the title to nothing. */
@media (max-width: 34rem) {
	.timeline-list .event {
		grid-template-columns: auto 1fr;
	}
	.event-title {
		grid-column: 1 / -1;
	}
	.event-lane {
		justify-self: start;
	}
}

.person-pronouns {
	color: var(--neutral-7);
	font-size: var(--font-size-0);
}

/* /about, /now, /uses and /wants are one entity through four templates, so each
   carries links to the others rather than relying on the sidebar to hold all
   four. */
.person-links {
	margin-top: 2.2em;
	display: flex;
	gap: 1em;
	font-size: var(--font-size-0);
}

.lens-note {
	color: var(--neutral-7);
	font-size: var(--font-size-0);
}

/* The header meta is a run of short facts; the prose under it needs air or it
   reads as a fourth item on that line. */
.person header + .post-body,
.person header + .person-motto {
	margin-top: 1.4em;
}

/* Where a recipe came from, when that is not simply who wrote it. Quieter than
   the byline: the person is the credit, the source is provenance. */
.recipe-source {
	color: var(--neutral-7);
	font-size: var(--font-size-0);
}

/* The drops console on a phone, which is where it is mostly used.
 *
 * The table is the honest shape for six columns and it is what the desktop
 * keeps. On a 390px screen it measured 631px wide inside a 386px scroller, so
 * 245px of it — the whole actions column — sat off the right edge, and every
 * per-drop button needed a sideways scroll to reach. A scroller was the right
 * fix for "the table must not push the page sideways" and the wrong fix for
 * "the buttons must be reachable".
 *
 * So below the width where it stops fitting, each row stops being a row. The
 * cells are already in reading order — name, size, added, expires, downloads,
 * actions — which is why this is a media query and not a second markup path:
 * a card is the same facts with the table taken away.
 *
 * Breakpoint at the measurement rather than at a device: 40rem is about where
 * six columns stop fitting, and a filename long enough to push past it wraps
 * instead, because td:first-child is the one column allowed to.
 */
@media (max-width: 40rem) {
	/* Nothing to scroll once nothing overflows. */
	.drops-scroll {
		overflow-x: visible;
	}

	.drops-table,
	.drops-table tbody,
	.drops-table tr,
	.drops-table td {
		display: block;
		width: auto;
	}

	/* The header row has nowhere to point once the columns are gone. What it
	   labelled is either self-evident (a filesize) or given a word below. */
	.drops-table thead {
		display: none;
	}

	.drops-table tbody tr:not(.drops-fetches) {
		border: var(--border-width-0) solid var(--neutral-4);
		border-radius: var(--border-radius-1);
		padding: var(--size-2);
		margin-block-end: var(--size-2);
	}

	.drops-table td {
		border: 0;
		padding: 0;
		white-space: normal;
	}

	.drops-table td:first-child {
		min-width: 0;
		margin-block-end: var(--size-1);
	}

	/* Size, added and expires read as one quiet line: "878.9 KiB · 1m ago · in
	   6d". Inline rather than a flex row so a long value wraps mid-line like
	   prose instead of forcing a column. */
	.drops-table td:nth-child(2),
	.drops-table td:nth-child(3),
	.drops-table td:nth-child(4) {
		display: inline;
		font-size: var(--font-size--1);
		color: var(--neutral-9);
	}

	.drops-table td:nth-child(2)::after,
	.drops-table td:nth-child(3)::after {
		content: " · ";
	}

	/* A bare number under a card needs its noun back, since the column header
	   that carried it is gone. */
	.drops-table td.drops-num {
		text-align: start;
		margin-block-start: var(--size-1);
	}

	.drops-table td.drops-num::after {
		content: " downloads";
		font-family: var(--font-sans);
		font-size: var(--font-size--1);
		color: var(--neutral-9);
	}

	.drops-table td.drops-last {
		display: inline;
	}

	/* The reason for all of this: the buttons wrap onto as many lines as they
	   need instead of running past the edge of the screen. */
	.drops-table td.drops-actions {
		display: flex;
		flex-wrap: wrap;
		gap: var(--size-0);
		text-align: start;
		white-space: normal;
		margin-block-start: var(--size-2);
	}

	.drops-table td.drops-actions .drop-quick + .drop-quick {
		margin-inline-start: 0;
	}

	/* The expand panel is a sibling row, so it sits under its card rather than
	   inside it. A rule down the side says which card it belongs to.
	   Its display is toggled by data-show, which writes an inline style and so
	   still wins over the display:block above. */
	.drops-fetches {
		border-inline-start: var(--border-width-0) solid var(--neutral-4);
		padding-inline-start: var(--size-2);
		margin-block: calc(var(--size-2) * -1) var(--size-2);
	}

	.drops-more-actions {
		display: flex;
		flex-wrap: wrap;
		gap: var(--size-0);
	}
}

/* The no-JS note. Server-rendered prose works without JavaScript; the live
   half does not, and this says so once, quietly, to the few who will see it. */
.noscript-note {
  margin: 0;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border, #ccc);
  background: var(--surface, #f5f5f5);
  color: var(--text, #222);
  font-size: 0.9rem;
  text-align: center;
}
