:root {
	/* palette: dual-mode brand (identical in light & dark) */
	--cornflower:  #667eea;
	--indigo:      #495dc6;
	--ultramarine: #0091ba;
	--tangerine:   #d06900;
	--salmon:      #e25569;
	--mercury:     #efefef;

	/* neutrals: dark defaults (flipped in the light media query below) */
	--bg:      #0f1117;
	--surface: #161925;
	--text:    #e6e8ee;
	--muted:   #8a94a6;
	--border:  rgba(255, 255, 255, 0.1);
	--land:    #1b2333;
	--coast:   #2b3a52;

	/* semantic: accent as surface/line is constant; accent as text is mode-aware */
	--accent:      var(--cornflower);
	--link:        var(--cornflower);
	--menu:        var(--muted);
	--link-hover:  var(--bg);
	--on-accent:   var(--mercury);
	--dot:         var(--ultramarine);
	--dot-hi:      var(--tangerine);
	--warn:        var(--salmon);
	--font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
	--font-mono:   ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
	/* cctv: a pixel-perfect bitmap UI font (see @font-face below) to finish the retro look. */
	--cctv-font:   "Pixelated MS Sans Serif", "Microsoft Sans Serif", "MS Sans Serif", sans-serif;

	/* metrics */
	--gap:    clamp(1rem, 2vw, 1.5rem);

	/* timings */
	--transition-duration: 250ms;
	--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);

	/* z-index layers: the site otherwise uses none, so keep these low and legible. The
	   cctv CRT glass sits at --crt-z; live playback goes one above it (calc below). */
	--crt-z: 100;

	/* typography: fluid scale */
	--font-min:  13px;
	--font-max:  17px;
	--font-base: clamp(var(--font-min), 0.72rem + 0.3vw, var(--font-max));
	--text-xs:   0.8rem;   /* fine print (e.g. bd-total) */
	--text-sm:   0.9rem;   /* sub-labels (e.g. bd-sub, filter btn) */
	--h-sm:      clamp(1.05rem, 2.5vw, 1.2rem);
	--h-md:      clamp(1.1rem,  3vw,   1.5rem);
	--h-lg:      clamp(1.1rem,  3vw,   1.6rem);
	--h-xl:      clamp(1.5rem,  4vw,   2rem);

	/* layout widths: content sizing */
	--content-max: 160rem;              /* overall cap on <main>, */
	--measure:     var(--content-max);  /* prose (.tips); set e.g. 74ch for readable line length */
	--table-max:   var(--content-max);  /* data tables (.bd-table, .meta) */
	--card-min:    22rem;               /* gallery grid column min */
}

/* Pixelated MS Sans Serif — the classic bitmap UI font, used only by the cctv theme. The
   files are copied verbatim from assets/ to out/ root, so reference them absolutely. */
@font-face {
	font-family: "Pixelated MS Sans Serif";
	src:
		url("/ms_sans_serif.woff2") format("woff2"),
		url("/ms_sans_serif.woff") format("woff");
	font-weight: 400;
	font-style: normal;
	font-display: swap;
}
@font-face {
	font-family: "Pixelated MS Sans Serif";
	src:
		url("/ms_sans_serif_bold.woff2") format("woff2"),
		url("/ms_sans_serif_bold.woff") format("woff");
	font-weight: 700;
	font-style: normal;
	font-display: swap;
}

@media (prefers-color-scheme: light) {
	:root {
		--bg:      #efefef;
		--surface: #f4f6f8;
		--text:    #14171f;
		--muted:   #5c6270;
		--border:  rgba(0, 0, 0, 0.09);
		--land:    #dfe6ee;
		--coast:   #b9c4d0;
		--link:    var(--indigo);
	}
}

/* manual theme override (opt-in, JS only): a class on <html> — set by assets/theme.js
   and restored before paint by the shell's inline head script — forces a theme past the
   OS preference. :root.<class> is specificity (0,2,0), so it beats both :root and the
   light media query in any OS mode. Values mirror the dark :root neutrals and the light
   media query above; the global `transition: all` animates the swap for free. */
:root.dark {
	--bg:      #0f1117;
	--surface: #161925;
	--text:    #e6e8ee;
	--muted:   #8a94a6;
	--border:  rgba(255, 255, 255, 0.1);
	--land:    #1b2333;
	--coast:   #2b3a52;
	--link:    var(--cornflower);
}
:root.light {
	--bg:      #efefef;
	--surface: #f4f6f8;
	--text:    #14171f;
	--muted:   #5c6270;
	--border:  rgba(0, 0, 0, 0.09);
	--land:    #dfe6ee;
	--coast:   #b9c4d0;
	--link:    var(--indigo);
}

/* cctv: green-phosphor monochrome on near-black, matched to the CRT sweep/scanline tint
   rgb(120,220,80). assets/theme.js layers the scanline/sweep/curvature/noise/vignette
   overlay (crt.css) on top; this block supplies the base colours. --accent is overridden
   too so hover states go green instead of the default cornflower blue. */
:root.cctv {
	--bg:      #050a06;
	--surface: #0b140d;
	--text:    #78dc50;
	--muted:   #4c8a3a;
	--border:  rgba(120, 220, 80, 0.22);
	--land:    #0e1f12;
	--coast:   #17351d;
	--link:    #a6f06a;
	--accent:  #78dc50;
	--on-accent: #050a06; /* near-black text on the lime badges/buttons (white was unreadable) */
	--dot:     #57d977; /* map markers: green, not the off-theme blue */
	--dot-hi:  #c8ff9c; /* highlighted markers: brighter green (was orange) */
	--font-family: var(--cctv-font); /* whole UI switches to the pixel font in cctv */

	/* Pixel look: kill font anti-aliasing and scale images nearest-neighbour. Both are
	   inherited, so they reach the whole cctv page (live playback opts back out below). */
	font-smooth: never;
	-webkit-font-smoothing: none;
	-moz-osx-font-smoothing: unset;
	font-synthesis-weight: none;
	image-rendering: -webkit-optimize-contrast;
	image-rendering: -moz-crisp-edges;
	image-rendering: crisp-edges;
	image-rendering: pixelated;
	-ms-interpolation-mode: nearest-neighbor;
}

/* CRT glass: a fixed, click-through overlay over the whole viewport, built by
   assets/theme.js for the cctv theme (its effect knobs are applied inline as custom
   properties; the layout lives here). */
#crt-overlay {
	position: fixed;
	inset: 0;
	overflow: hidden;
	pointer-events: none;
	z-index: var(--crt-z);
}

/* In cctv, real playback renders ABOVE the glass so the scanlines/vignette don't sit on a
   live stream. Only genuinely-playing media qualifies — refreshing/MJPEG stills, the
   mp4/HLS <video>, and the YouTube embed. Previews (plain stills, the click-to-load
   .facade) stay under the glass on purpose. */
:root.cctv :is(img[data-refresh], img[data-mjpeg], .live-video, .yt-embed) {
	position: relative;
	z-index: calc(var(--crt-z) + 1);
	image-rendering: auto; /* live playback stays crisp; only previews/thumbnails pixelate */
}

/* cctv readability: the vignette dims the viewport corners, so brighten + embolden the
   chrome that lives there — the footer stats and the theme picker — and lift the wordmark
   to a brighter green. Scoping --link inside .brand brightens only the wordmark and keeps
   its dark-on-accent hover intact (no specificity override needed). */
:root.cctv .count {
	color: var(--text);
	font-weight: 600;
}
:root.cctv #theme select {
	color: var(--link);
	font-weight: 600;
}
:root.cctv .brand {
	--link: #b6ff7a;
}
/* The pixel font ships only 400/700, so the footer credit's weight 500 maps down to regular
   and loses its look. Force bold — the 3xi.club / xero links inherit it (they keep their
   larger size). */
:root.cctv footer cite {
	font-weight: 700;
}
/* Pixel-bold every button face (pagination, view live, watch on youtube, filter, …). */
:root.cctv .btn .front {
	font-weight: 700;
}

/* CRT retune: a channel-change / power-on flourish. A fixed, click-through shade (dark with
   a bright center scanline baked into the gradient) collapses to a line and snaps back —
   the viewport-centered adaptation of vault66's monitor "is-retuning" picture-collapse.
   assets/theme.js builds #crt-retune with the CRT overlay and toggles .is-active on
   switch-to-cctv, cctv page load, and htmx navigations. Timing/easing match vault66. */
#crt-retune {
	position: fixed;
	inset: 0;
	z-index: calc(var(--crt-z) + 5);
	pointer-events: none;
	opacity: 0;
	transform-origin: center center;
	background: linear-gradient(
		to bottom,
		rgba(20, 28, 20, 0.82) 0%,
		rgba(20, 28, 20, 0.82) 42%,
		rgba(198, 255, 150, 0.5) 46%,
		rgba(220, 255, 180, 0.95) 50%,
		rgba(198, 255, 150, 0.5) 54%,
		rgba(20, 28, 20, 0.82) 58%,
		rgba(20, 28, 20, 0.82) 100%
	);
}

/* The "Connection lost" message: a separate, non-scaled layer above the shade, so the text
   never squishes with the scaleY collapse. Left-centered (vertically centred, left-aligned)
   with a little left padding. */
/* Heavy solid-black message, left-centred over the bright centre scanline (no plate). */
#crt-retune-msg {
	position: fixed;
	inset: 0;
	z-index: calc(var(--crt-z) + 6);
	pointer-events: none;
	display: flex;
	align-items: center;
	padding-left: 2.5rem;
	opacity: 0;
	color: #000;
	/* Bold pixel font (inherits the theme's crisp no-AA rendering), kept big for a legible
	   sub-second flash; a faint green glow helps it read on the beam and the dark alike. */
	font-family: var(--cctv-font);
	font-weight: 700;
	font-size: clamp(1.4rem, 3vw, 2.1rem);
	letter-spacing: 0.02em;
	text-shadow: 0 0 6px rgba(190, 255, 150, 0.85);
}

/* is-retuning on <html> drives the shade collapse (#crt-retune), the message fade
   (#crt-retune-msg), and the <main> reveal together, so htmx-swapped content fades in WITH
   the shade reopening instead of popping in first. theme.js adds the class in the
   htmx:after:swap handler — before the browser paints — so the new page never flashes. */
:root.cctv.is-retuning #crt-retune {
	animation: crt-retune 0.72s cubic-bezier(0.77, 0, 0.175, 1) both;
}
:root.cctv.is-retuning #crt-retune-msg {
	animation: crt-retune-msg 0.72s cubic-bezier(0.77, 0, 0.175, 1) both;
}
:root.cctv.is-retuning main {
	animation: crt-content-retune 0.72s cubic-bezier(0.77, 0, 0.175, 1) both;
}

@keyframes crt-retune {
	0%   { opacity: 0;    transform: scaleY(1); }
	8%   { opacity: 0.95; transform: scaleY(1); }
	34%  { opacity: 1;    transform: scaleY(1); }    /* hold full-dark: the message window */
	54%  { opacity: 1;    transform: scaleY(0.04); } /* collapse */
	62%  { opacity: 1;    transform: scaleY(0.01); } /* pinched to a scanline */
	74%  { opacity: 0.9;  transform: scaleY(0.6); }  /* reopen */
	90%  { opacity: 0.4;  transform: scaleY(0.98); }
	100% { opacity: 0;    transform: scaleY(1); }
}

/* The message reads clearly over the full-dark hold, then fades out before the collapse
   line so it never washes out against the bright scanline. */
@keyframes crt-retune-msg {
	0%   { opacity: 0; }
	10%  { opacity: 1; }
	34%  { opacity: 1; }
	46%  { opacity: 0; }
	100% { opacity: 0; }
}

/* Content stays hidden through the collapse, then fades in as the shade reopens. */
@keyframes crt-content-retune {
	0%   { opacity: 0; }
	62%  { opacity: 0; }
	80%  { opacity: 0.45; }
	100% { opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
	:root.cctv.is-retuning #crt-retune,
	:root.cctv.is-retuning #crt-retune-msg,
	:root.cctv.is-retuning main { animation: none !important; }
}
html, body, * {
	transition-property: all;
	transition-duration: var(--transition-duration);
	transition-timing-function: var(--ease-in-out);
}

html,
body {
	height: 100%;
	background: var(--bg);
	color: var(--text);
	font-family: var(--font-family);
	font-size: var(--font-base);
	line-height: 1.6;

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

a {
	color: var(--link);
	text-decoration: none;

	&:hover,
	&:focus-visible {
		background: var(--accent);
		color: var(--link-hover);
		text-decoration: none;
		outline: none;
	}
}

body > header {
	display: flex;
	flex-flow: row wrap;
	align-items: baseline;
	gap: 0.75rem 1.5rem;
	padding: var(--gap);

	nav {
		display: flex;
		flex-flow: row wrap;
		gap: 1rem;
		align-items: center;

		/* Below ~800px the brand + nav + picker no longer fit on one row (the captioned nav is
		   wider, and a single row only fits from ~780px up), so drop the nav to its own
		   full-width row and let the #theme picker ride the title row. (Icon/title size
		   reductions stay at their own 480px breakpoints.) */
		@media (max-width: 800px) {
			flex-basis: 100%;
			justify-content: space-between;
			gap: unset;
			/* Sort after the order-0 items (brand + the appended #theme aside) so the
			   picker rides the title row and the nav wraps full-width beneath it. */
			order: 1;
		}

		a,
		a:visited {
			display: flex;
			flex-flow: column nowrap;
			align-items: center;
			gap: 0.3rem;
			color: var(--menu); /* the caption; the icon uses var(--menu) via svg fill */

			& span {
				font-size: 0.72rem;
				line-height: 1;
				letter-spacing: 0.02em;
			}

			&:focus,
			&:hover {
				background: transparent !important;
				color: var(--accent); /* caption + (below) icon light up together on hover */
				* {
					fill: var(--accent);
				}
			}
		}
	}
	svg {
		display: block;
		width: 50px;
		height: 50px;
		* {
			fill: var(--menu);
		}

		@media (max-width: 480px) {
			width: 40px;
			height: 40px;
		}
	}
	aside {
		display: flex;
		flex-grow: 1;
		justify-content: end;
		align-self: center;

		select {
			background: var(--surface);
			color: var(--text);
			border: 1px solid var(--border);
			border-radius: 0.35rem;
			padding: 0.3rem 0.5rem;
			font-family: var(--font-family);
			font-size: var(--text-sm);
			cursor: pointer;

			&:hover,
			&:focus-visible {
				border-color: var(--accent);
				outline: none;
			}
		}
	}
}

button {
	background-color: transparent;
	border: 0;
}

h1 {
	font-size: 2rem;
	font-weight: 600;
	letter-spacing: 0.02em;
	line-height: 1em;

	@media (max-width: 480px) {
		font-size: 1.9rem;
	}
}

h1 > em {
	line-height: 1em;
}

.brand {
	display: flex;
	flex-flow: column nowrap;
	align-self: center;
}

.count {
	display: flex;
	flex-flow: column nowrap;
	color: var(--muted);
	font-variant-numeric: tabular-nums;
	align-self: flex-start;
	align-items: self-end;
	flex-grow: 1;
	font-size: 11px;
	& strong {
		color: var(--link);
		font-weight: 600;
		font-size: 10px;
	}

	@media (max-width: 735px) {
		header & {
			display: none;
		}
	}
}

main {
	max-width: var(--content-max);
	padding: var(--gap);
	padding-top: 0px;
}

.empty {
	color: var(--muted);
	font-size: 1rem;
}

.gallery {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
	gap: var(--gap);

	@media (min-width: 80rem) {
		grid-template-columns: repeat(4, minmax(0, 1fr));
	}
}

.card {
	display: flex;
	flex-flow: column nowrap;
	gap: 0.5rem;
	min-width: 0;
	padding: 1rem;
	background: var(--surface);
	border: 1px solid var(--border);
	color: inherit;
	text-decoration: none;
	transition: border-color 0.15s ease, transform 0.15s ease;

	&:hover,
	&:focus-visible {
		border-color: var(--accent);
		background: var(--surface);
		transform: translateY(-2px);
		outline: none;
	}

	& figure {
		position: relative;
		overflow: hidden;
		background: #000;
		aspect-ratio: 4 / 3;
		background-size: cover;
		background-position: center;
	}

	& h2 {
		font-size: 1.05rem;
		font-weight: 600;
		overflow-wrap: anywhere;
	}

	& .dn-line,
	& .dn-sub {
		display: block;
		white-space: nowrap;
		overflow: hidden;
		text-overflow: ellipsis;
		color: var(--text);
	}

	& .dn-sub {
		min-height: 1lh;
	}

	& .loc {
		color: var(--muted);
	}

	& .badge {
		position: absolute;
		right: 0.5rem;
		bottom: 0.5rem;
		padding: 0.1rem 0.5rem;
		background: var(--accent);
		color: var(--on-accent);
		font-size: 0.85em;
	}
}

.pager {
	display: flex;
	flex-flow: row wrap;
	justify-content: center;
	align-items: center;
	gap: 0.5rem;
	padding-top: var(--gap);
	font-variant-numeric: tabular-nums;

	& .btn {
		align-self: center;
	}

	& .btn .front {
		min-width: 2.2rem;
		padding: 0.3rem 0.6rem;
		font-size: 1rem;
	}

	& .gap {
		padding: 0 0.25rem;
		color: var(--muted);
	}

	@media (max-width: 480px) {
		& .pager-ends {
			display: none;
		}
	}
}

.host {
	border-top: 1px solid var(--accent);
	padding-top: 10px;
	display: flex;
	flex-flow: column nowrap;
	gap: var(--gap);

	& > h2 {
		font-size: var(--h-lg);
		font-weight: 600;
		overflow-wrap: anywhere;
	}
}

.dn-name  { color: var(--text); }
.dn-ip    { color: var(--muted); }
.dn-ports { color: var(--link); }

.shots {
	display: flex;
	flex-flow: row wrap;
	gap: var(--gap);
}

.shot {
	display: flex;
	flex-flow: column nowrap;
	gap: 0.5rem;

	& img {
		display: block;
		width: auto;
		height: auto;
		max-width: 100%;
		background: #000;
	}

	& figcaption {
		color: var(--muted);
		font-variant-numeric: tabular-nums;
	}
}

/* A live feed that failed to load: the JS has fallen back to the baked still (the
   "View live" link is always the escape hatch). Desaturate and dim it, with a
   warn-colored ring, so a dead feed reads as intentional rather than broken. */
.feed-error {
	filter: grayscale(1) brightness(0.8);
	outline: 2px solid var(--warn);
	outline-offset: -2px;
}

.btn {
	--btn-face: var(--accent);
	--btn-edge: color-mix(in srgb, var(--btn-face) 70%, #000);
	--btn-edge-dark: color-mix(in srgb, var(--btn-face) 45%, #000);
	align-self: flex-start;
	position: relative;
	border: none;
	background: transparent;
	padding: 0;
	text-decoration: none;
	cursor: pointer;
	outline-offset: 4px;
	transition: filter 250ms;
	font-family: inherit;

	& .shadow {
		display: block;
		position: absolute;
		inset: 0;
		background: hsl(0deg 0% 0% / 0.25);
		will-change: transform;
		transform: translateY(2px);
		transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1);
	}

	& .edge {
		display: block;
		position: absolute;
		inset: 0;
		background: linear-gradient(
			to left,
			var(--btn-edge-dark) 0%,
			var(--btn-edge) 8%,
			var(--btn-edge) 92%,
			var(--btn-edge-dark) 100%
		);
	}

	& .front {
		display: block;
		position: relative;
		padding: 12px 24px;
		font-size: 1.25rem;
		line-height: 1.6;
		text-align: center;
		color: var(--on-accent);
		background: var(--btn-face);
		will-change: transform;
		transform: translateY(-4px);
		transition: transform 600ms cubic-bezier(0.3, 0.7, 0.4, 1);
	}

	&:not(:disabled):hover {
		filter: brightness(110%);
	}

	&:not(:disabled):hover .front {
		transform: translateY(-6px);
		transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5);
	}

	&:not(:disabled):active .front {
		transform: translateY(-2px);
		transition: transform 34ms;
	}

	&:not(:disabled):hover .shadow {
		transform: translateY(4px);
		transition: transform 250ms cubic-bezier(0.3, 0.7, 0.4, 1.5);
	}

	&:not(:disabled):active .shadow {
		transform: translateY(1px);
		transition: transform 34ms;
	}

	&:focus:not(:focus-visible) {
		outline: none;
	}
}

.btn:disabled {
	--btn-face: var(--muted);
	cursor: not-allowed;
	transform: translateY(1px);

	& .front {
		color: #000;
	}
}

.meta {
	width: 100%;
	max-width: var(--table-max);
	border-collapse: collapse;

	& th,
	& td {
		text-align: left;
		vertical-align: top;
		padding: 0.25rem 0;
		overflow-wrap: anywhere;
	}

	& th {
		width: 1%;
		white-space: nowrap;
		padding-right: 1.5rem;
		color: var(--link);
		font-weight: 500;
	}
}

.tips {
	max-width: var(--measure);

	& h2,
	& h3,
	& h4,
	& h5 {
		line-height: 1.25;
		scroll-margin-top: 1rem;
		border-bottom: 1px solid var(--border);
	}

	& h2 {
		margin-bottom: 1rem;
		padding-bottom: 0.35rem;
		font-size: var(--h-xl);
		color: var(--link);
	}

	& h3 {
		margin-top: 2.5rem;
		padding-bottom: 0.3rem;
		font-size: var(--h-md);
		color: var(--link);
	}

	& h4 {
		margin-top: 1.75rem;
		font-size: var(--h-sm);
		color: var(--link);
	}

	& h5 {
		margin-top: 1.25rem;
		font-size: 1rem;
		color: var(--link);
	}

	& p {
		margin: 0.75rem 0;
	}

	& ul,
	& ol {
		margin: 0.75rem 0;
		padding-left: 1.5rem;

		& ul {
			margin: 0.25rem 0;
		}
	}

	& li {
		margin: 0.3rem 0;
	}

	& code {
		padding: 0.1em 0.35em;
		font-family: var(--font-mono);
		font-size: 0.9em;
		color: var(--muted);
		background: var(--surface);
		border-radius: 4px;
		overflow-wrap: anywhere;
	}

	& hr {
		margin: 2.5rem 0;
		border: none;
		border-top: 1px solid var(--border);
	}

	blockquote {
		margin: 1.25rem 0;
		padding: 0.1rem 1rem;
		color: var(--muted);
		border-left: 3px solid var(--border);

		& > :first-child {
			margin-top: 0;
		}
		&.toc {
			background: var(--surface);
			li a {
				font-weight: bold;
			}
			li li a {
				font-weight: normal;
			}
		}
	}

	.admonition {
		color: var(--text);
		border-left-width: 4px;

		& .admonition-label {
			margin: 0 0 0.35rem;
			font-size: 1.1em;
			font-weight: 700;
			letter-spacing: 0.05em;
			text-transform: uppercase;
		}

		&.caution {
			border-left-color: var(--warn);

			& .admonition-label {
				color: var(--warn);
			}
		}

		&.tip {
			border-left-color: var(--dot);

			& .admonition-label {
				color: var(--dot);
			}
		}
	}

	.table-wrap {
		margin: 1.25rem 0;
		overflow-x: auto;
	}

	table {
		width: 100%;
		border-collapse: collapse;

		& th,
		& td {
			padding: 0.4rem 0.7rem;
			text-align: left;
			vertical-align: top;
			border: 1px solid var(--border);
		}

		& thead th {
			color: var(--link);
			font-weight: 600;
			white-space: nowrap;
			background: var(--surface);
		}
	}

	kbd {
		background: var(--surface);
		color: var(--muted);
	}

	.shiki {
		margin: 1.25rem 0;
		padding: 1rem 0;
		font-size: 13px;
		line-height: 1.7;
		tab-size: 4;
		overflow-x: auto;
		border-radius: 6px;

		& span {
			font-style: var(--shiki-light-font-style, normal);
		}

		& code {
			counter-reset: step;
			counter-increment: step 0;
			display: block;
			padding: 0;
			color: inherit;
			background: none;
			border-radius: 0;
			font-size: inherit;
			font-family: var(--font-mono);
			overflow-wrap: normal;

			& .line {
				display: block;
				padding-inline: 1rem;
			}

			& .line::before {
				display: none;
				user-select: none;
				margin: 0;
				width: 0;
				padding: 0;
			}

			& .line:hover {
				background: var(--bg);
			}
		}
	}
}

@media (prefers-color-scheme: dark) {
	.tips .shiki,
	.tips .shiki span {
		color: var(--shiki-dark) !important;
		background-color: transparent !important;
		font-style: var(--shiki-dark-font-style) !important;
		font-weight: var(--shiki-dark-font-weight) !important;
		text-decoration: var(--shiki-dark-text-decoration) !important;
	}

	.tips .shiki {
		background-color: var(--surface) !important;
	}
}

.back {
	align-self: flex-start;
	color: var(--link);
	text-decoration: none;

	&:hover,
	&:focus-visible {
		text-decoration: underline;
	}
}

body > footer {
	display: flex;
	flex-flow: row wrap;
	gap: 0.75rem 1.5rem;
	padding: clamp(1rem, 1vw, 1rem);
	border-top: 1px solid var(--border);
	align-items: center;
	justify-content: space-between;

	cite {
		display: flex;
		font-style: normal;
		font-weight: 500;
		line-height: 1em;
		@media (max-width: 480px) {
			display: none;
		}
		a {
			font-size: 1.5em;
			padding-inline: 4px;
		}
	}
	#syndication {
		a {
			&:hover {
				background: transparent;
				svg * {
					fill: var(--accent);
				}
			}
			svg {
				width: 40px;
				height: 40px;
				* {
					fill: var(--muted);
				}
			}
		}
	}
	.count {
		line-height: 1.23em;
		flex-grow: 0;
	}
}

.shot video {
	display: block;
	width: auto;
	height: auto;
	max-width: 100%;
	background: #000;
}

.live-img {
	display: block;
	max-width: 100%;
	background: #000 center / contain no-repeat;
}

/* Click-to-load facade: a thumbnail with a play overlay, shared by YouTube streams and
   every auto-playing feed (hls/mp4/mjpeg/jpg). Inert until clicked — no third-party or
   cross-origin request fires on page load. See feeds.js for the click handling. */
.facade {
	position: relative;
	display: block;
	width: 40rem;
	max-width: 100%;
	aspect-ratio: 16 / 9;
	background-color: #000;
	background-position: center;
	background-size: cover;
	background-repeat: no-repeat;
	cursor: pointer;
}

.facade .play {
	position: absolute;
	inset: 0;
	margin: auto;
	width: 4rem;
	height: 4rem;
	border: 2px solid #fff;
	border-radius: 50%;
	background: rgb(0 0 0 / 0.55);
}

.facade .play::before {
	content: "";
	position: absolute;
	inset: 0;
	margin: auto;
	width: 0;
	height: 0;
	border-style: solid;
	border-width: 0.7rem 0 0.7rem 1.15rem;
	border-color: transparent transparent transparent #fff;
	transform: translateX(0.15rem);
}

.facade:hover .play,
.facade:focus-visible .play {
	background: var(--accent);
}

.yt-embed {
	display: block;
	width: 40rem;
	max-width: 100%;
	aspect-ratio: 16 / 9;
	border: 0;
	background: #000;
}

.siblings {
	display: flex;
	flex-flow: column nowrap;
	gap: var(--gap);

	& > h3 {
		font-size: 1.1rem;
		font-weight: 600;
		color: var(--link);
	}
}

.tagcloud {
	border-top: 1px solid var(--accent);
	padding-top: 10px;
}

.tags {
	display: flex;
	flex-flow: row wrap;
	align-items: baseline;
	gap: 0.4rem 1.1rem;
	list-style: none;
	line-height: 1.3;

	& a {
		color: var(--muted);
		font-variant-numeric: tabular-nums;
		transition: background 0.5s;
		padding: 8px;
	}

	& a:hover,
	& a:focus-visible {
		color: var(--on-accent);
	}
	li {
		align-self: center;
	}
}

.breakdown {
	border-top: 1px solid var(--accent);
	padding-top: 10px;

	& h2 {
		margin-bottom: 0.2rem;
		font-size: var(--h-md);
		font-weight: 600;
		color: var(--link);
	}

	& .bd-sub {
		margin-bottom: 1.1rem;
		color: var(--muted);
		font-size: var(--text-sm);
	}
}

.bd-table {
	width: 100%;
	max-width: var(--table-max);
	border-collapse: collapse;
	font-variant-numeric: tabular-nums;

	& th,
	& td {
		text-align: left;
		vertical-align: top;
		padding: 0.3rem 0.9rem 0.3rem 0;
		border-bottom: 1px solid var(--border);
	}

	& thead th {
		color: var(--link);
		font-weight: 600;
		white-space: nowrap;
	}

	& .bd-count {
		text-align: right;
		padding-right: 0;
		white-space: nowrap;
	}

	& .bd-make {
		white-space: nowrap;
		color: var(--text);
		font-weight: 600;
		border-bottom: 2px solid var(--accent);
	}

	& .bd-total {
		display: block;
		color: var(--muted);
		font-weight: 400;
		font-size: var(--text-xs);
	}

	& .bd-model {
		color: var(--muted);
		overflow-wrap: anywhere;
	}

	& .bd-none {
		color: var(--border);
	}

	& .bd-filter {
		/* Top-aligned so the button sits beside the make label, not centered down a
		   many-model rowspan; left padding separates it from the right-aligned count. */
		vertical-align: top;
		white-space: nowrap;
		padding-left: 1.5rem;
	}

	& .bd-filter .btn {
		align-self: flex-start;
	}

	& .bd-filter .btn .front {
		padding: 0.3rem 0.7rem;
		font-size: var(--text-sm);
	}
}

/* Heading atop a per-vendor fingerprint gallery (reuses the section-title look, but not
   scoped to .home, and with its own spacing above the card grid that follows). */
.vendor-title {
	font-size: var(--h-md);
	font-weight: 600;
	color: var(--link);
	border-bottom: 1px solid var(--border);
	padding-bottom: 0.35rem;
	margin-bottom: var(--gap);
}

/* Super-feature: a one-off event promoted above every homepage section. A full-width
   clickable banner (the primary feed's frame beside the title) linking to the combined
   /event page. Reuses the card hover feel but reads bigger and leads with an accent edge.
   Flex row wrap stacks the frame above the text on narrow screens. */
.super-feature {
	display: flex;
	flex-flow: row wrap;
	align-items: stretch;
	gap: var(--gap);
	margin-bottom: calc(var(--gap) * 1.5);
	padding: 1rem;
	background: var(--surface);
	border: 1px solid var(--border);
	border-left: 4px solid var(--accent);
	color: inherit;
	text-decoration: none;
	transition: border-color 0.15s ease, transform 0.15s ease;

	&:hover,
	&:focus-visible {
		border-color: var(--accent);
		background: var(--coast);
		transform: translateY(-2px);
		outline: none;
	}

	& .sf-thumb {
		flex: 2 1 22rem;
		min-width: min(100%, 18rem);
		max-width: 500px;
		aspect-ratio: 16 / 9;
		background: #000 center / cover no-repeat;
		border: 1px solid var(--border);
	}

	& .sf-body {
		flex: 1 1 14rem;
		display: flex;
		flex-flow: column nowrap;
		justify-content: center;
		gap: 0.6rem;
	}

	& .sf-tag {
		align-self: flex-start;
		font-size: var(--text-sm);
		font-weight: 600;
		letter-spacing: 0.08em;
		text-transform: uppercase;
		color: var(--on-accent);
		background: var(--accent);
		padding: 0.15rem 0.6rem;
		border-radius: 999px;
	}

	& .sf-title {
		font-size: var(--h-xl);
		font-weight: 700;
		color: var(--link);
		overflow-wrap: anywhere;
	}

	& .sf-more {
		color: var(--muted);
		font-weight: 600;
	}
}

.home {
	display: flex;
	flex-flow: column nowrap;
	gap: var(--gap);
	margin-bottom: calc(var(--gap) * 1.5);

	& .section-title {
		font-size: var(--h-md);
		font-weight: 600;
		color: var(--link);
		border-bottom: 1px solid var(--border);
		padding-bottom: 0.35rem;
	}

}
.more {
	align-self: flex-start;
}

/* Two "top N" columns below the homepage card sections: side by side on desktop, stacked
   on mobile. Nested under nothing, but each column reuses the .home .section-title look via
   its own rule below so both headers keep an independent underline. */
.home-cols {
	display: grid;
	grid-template-columns: 1fr 1fr;
	gap: var(--gap);
	margin-bottom: calc(var(--gap) * 1.5);

	@media (max-width: 735px) {
		grid-template-columns: 1fr;
	}
}

.home-col {
	display: flex;
	flex-flow: column nowrap;
	gap: 0.6rem;

	& .section-title {
		font-size: var(--h-md);
		font-weight: 600;
		color: var(--link);
		border-bottom: 1px solid var(--border);
		padding-bottom: 0.35rem;
	}
}

.top-list {
	list-style: none;
	display: flex;
	flex-flow: column nowrap;
	gap: 0.2rem;
	font-variant-numeric: tabular-nums;

	& .cnt {
		color: var(--muted);
		font-size: var(--text-sm);
	}
}

.mapwrap {
	border-top: 1px solid var(--accent);
	padding-top: 10px;
	display: flex;
	flex-flow: column nowrap;
	gap: 0.75rem;
	position: relative; /* positioning context for the JS canvas overlay + its tooltip */
}

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

.worldmap {
	display: block;
	width: 100%;
	height: auto;
	max-height: 82vh;
	background: var(--bg);
	touch-action: none;
	cursor: grab;

	&:active {
		cursor: grabbing;
	}

	& .land path {
		fill: var(--land);
		stroke: var(--coast);
		stroke-width: 0.5;
		vector-effect: non-scaling-stroke;
	}

	& .dots circle {
		fill: var(--dot);
		fill-opacity: 0.6;
		transition: fill 0.1s ease;
	}

	& .dots a:hover circle,
	& .dots a:focus-visible circle {
		fill: var(--dot-hi);
		fill-opacity: 1;
	}
}

/* JS upgrade: assets/geomap.js swaps the inert SVG for this canvas (clustering, constant-
   size dots, zoom-in state borders). touch-action:none lets d3-zoom own pinch on mobile. */
.worldmap-canvas {
	display: block;
	width: 100%;
	max-width: 100%;
	margin: 0 auto;
	background: var(--bg);
	touch-action: none;
	cursor: grab;

	&:active {
		cursor: grabbing;
	}
}

.map-tip {
	position: absolute;
	z-index: 2;
	transform: translate(-50%, -140%);
	max-width: 60vw;
	overflow: hidden;
	padding: 2px 8px;
	background: var(--bg);
	color: var(--text);
	border: 1px solid var(--accent);
	border-radius: 4px;
	font-size: var(--text-sm);
	white-space: nowrap;
	text-overflow: ellipsis;
	pointer-events: none;

	&[hidden] {
		display: none;
	}
}

/* Cluster popover: cameras at co-located coords that zoom can't separate are listed here. */
.map-list {
	position: absolute;
	z-index: 3;
	top: 2.5rem;
	right: 0.5rem;
	display: flex;
	flex-flow: column nowrap;
	width: min(320px, 80%);
	max-height: 70svh;
	overflow: hidden;
	background: var(--bg);
	border: 1px solid var(--accent);
	border-radius: 6px;
	box-shadow: 0 4px 16px rgb(0 0 0 / 0.4);

	& header {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 0.5rem;
		padding: 6px 10px;
		border-bottom: 1px solid var(--accent);
		font-weight: bold;
		color: var(--text);
	}

	& .map-list-x {
		background: none;
		border: 0;
		color: var(--muted);
		font-size: 1.2rem;
		line-height: 1;
		cursor: pointer;

		&:hover {
			color: var(--accent);
		}
	}

	& ul {
		flex: 1 1 auto;
		min-height: 0;
		overflow-y: auto;
		list-style: none;
	}

	& li a {
		display: block;
		padding: 5px 10px;
		color: var(--accent);
		text-decoration: none;
		white-space: nowrap;
		overflow: hidden;
		text-overflow: ellipsis;

		&:hover {
			background: var(--accent);
			color: var(--bg);
		}
	}

	& .map-list-more {
		padding: 5px 10px;
		color: var(--muted);
		font-size: var(--text-xs);
	}
}
