/* ===========================================================================
   PATCH NOTES

   Patch list, expandable entries, change categories, filter tabs, pagination
   and the media grid rendered inside patch details.
   Loaded by: the-empty-halls.html   Behaviour: JS/patch-notes-backend.js

   ---------------------------------------------------------------------------
   NOTES ON THE ANIMATION WORK

   · Entries used to open by transitioning max-height from 0 to a hard-coded
     1600px. Two problems with that: anything taller than 1600px was silently
     cut off (the long release notes were), and because the transition covers
     the full 1600px whatever the content, a three-line hotfix opened at a
     crawl while a huge patch snapped. Transitioning grid-template-rows from
     0fr to 1fr animates to the real height, so every entry opens at the same
     apparent speed and nothing is ever clipped.

   · The stagger on load was a JS loop that awaited a 25ms sleep between adding
     a class to each row. That put the sequence on the main thread and left the
     rows mid-animation if a filter changed halfway through. It is now an
     animation-delay derived from --i, which the renderer sets per row.

   · Everything below is turned off under prefers-reduced-motion at the bottom
     of this file.
   =========================================================================== */

.patch-notes {
	width: 100%;
	/* Not cosmetic — this stops a second scrollbar appearing on the page.
	   #patchAnnouncer is .visually-hidden, which is position:absolute, and
	   .scroll_container is position:static (its rule says `display: relative`,
	   which is not a real declaration). So without a positioned ancestor here
	   the announcer resolves its containing block all the way up to #main_area
	   and reports its static position — around 2700px down — as scrollable
	   overflow there, giving #main_area a scrollbar of its own on top of the
	   one .scroll_container already has. Anything absolutely positioned inside
	   the patch notes needs to be contained here. */
	position: relative;
	/* Tints behind a hovered and an open entry. Declared here so the light theme
	   can restate them once rather than in every rule that uses them.

	   Hover used to be --color-selected-glow-1, which is #70BAFA at 50% alpha —
	   a solid blue slab across the whole row that swallowed the version number
	   and the date. A hint of the accent is enough to show which row is under
	   the cursor, and it leaves the open state as the stronger of the two. */
	--patch-hover-tint: rgba(112, 186, 250, 0.06);
	--patch-open-tint: rgba(112, 186, 250, 0.11);
	--patch-ease: cubic-bezier(0.22, 1, 0.36, 1);

	/* Category heading colours. These used to be literals, which meant the
	   yellow "Improvements" heading sat at 1.13:1 on the light theme's panels —
	   effectively invisible — and "New Features" at 2.95:1. Both are now
	   restated below for the light theme at readable contrast. */
	--patch-cat-features: var(--color-visited-1);
	--patch-cat-improvements: #fcff57;
	--patch-cat-fixes: var(--color-warning-1);

	/* Muted metadata — dates, counts, captions, inactive tabs. The palette's
	   --color-primary-a30 lands at 3.2:1 on these panels, under AA for text
	   this size, so this is the same grey family two steps brighter. */
	--patch-muted: #9aa0a8;

	/* Accent for version numbers, counts and bullets. --color-selected-3 is
	   tuned for the dark theme; on the light theme's panels it drops to
	   1.95:1 and the version number all but disappears, hence the override. */
	--patch-accent: var(--color-selected-3);

	/* One dark ink for text that sits on the light accent fills (type chips,
	   the filter pill, the New badge). White on those was around 2:1. */
	--patch-ink: #1d2229;
}

:root[data-theme="light"] .patch-notes {
	--patch-hover-tint: rgba(91, 148, 198, 0.09);
	--patch-open-tint: rgba(91, 148, 198, 0.16);
	--patch-cat-features: #8a2f9c;
	--patch-cat-improvements: #6d5500;
	--patch-cat-fixes: #a3353b;
	--patch-muted: #5c626d;
	--patch-accent: #1c66a3;
}

.patch-list {
	background: var(--color-primary-a10);
	border: 1px solid var(--color-primary-a20);
	border-radius: 16px;
	overflow: hidden;
	backdrop-filter: blur(10px);
	max-width: 100%;
	box-sizing: border-box;
	/* The renderer pins the height while the contents are swapped and then
	   animates it to the new size — see the view-settling section in the JS.

	   Deliberately not --patch-ease. That curve is a hard ease-out, and since a
	   shrinking list drags the scroll position down with it, its fast opening
	   frames came through as a shove of about 138px in a single frame. An
	   even-paced curve spreads the same distance across the whole animation. */
	transition: height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
	/* Chrome's scroll anchoring tries to compensate for the same height change
	   at the same time, which fights the animation and lands the panel a few
	   pixels off. The JS is the one holding the view steady here. */
	overflow-anchor: none;
}

.patch-list:focus {
	outline: none;
}

.patch-list:focus-visible {
	outline: 2px solid var(--color-selected-3);
	outline-offset: 2px;
}

/* The list dims slightly while a new page is on its way, so a slow request
   reads as "working" rather than as a dead control. */
.patch-list.is-loading {
	opacity: 0.55;
	transition: opacity 0.2s ease-in-out;
}

.patch-item {
	border-bottom: 1px solid var(--color-primary-a20);
	max-width: 100%;
	box-sizing: border-box;
	transition: background 0.25s ease-in-out;
}

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

.patch-item:hover {
	background: var(--patch-hover-tint);
}

.patch-item.expanded {
	background: var(--patch-open-tint);
}

/* Staggered entry. --i is the row index, set by the renderer. */
.patch-list .patch-item {
	opacity: 0;
	animation: patch-enter 0.4s var(--patch-ease) forwards;
	animation-delay: calc(var(--i, 0) * 45ms);
}

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

/* Brief ring on the entry a deep link just jumped to, so it is obvious which
   of ten near-identical rows the link meant. */
.patch-item.patch-targeted {
	animation: patch-target-flash 2.2s ease-in-out;
}

@keyframes patch-target-flash {
	0%, 100% { box-shadow: inset 0 0 0 0 transparent; }
	15%, 60% { box-shadow: inset 0 0 0 2px var(--color-selected-3); }
}

/* ---------------------------------------------------------------------------
   Header row
   --------------------------------------------------------------------------- */

.patch-header-row {
	display: flex;
	align-items: stretch;
}

/* A real <button> now, for keyboard and screen reader support, so it needs the
   usual reset before it looks like a row again. */
.patch-header {
	flex: 1;
	min-width: 0;
	appearance: none;
	background: none;
	border: 0;
	margin: 0;
	color: inherit;
	font: inherit;
	text-align: left;
	padding: 1.5rem;
	display: flex;
	justify-content: space-between;
	align-items: center;
	gap: 1rem;
	cursor: pointer;
}

.patch-header:focus-visible {
	outline: 2px solid var(--color-selected-3);
	outline-offset: -4px;
	border-radius: 12px;
}

.patch-info {
	display: flex;
	align-items: center;
	gap: 1rem;
	flex: 1;
	min-width: 0;
}

/* The inner block stacks whether or not there is a thumbnail — one layout is
   easier to reason about than two, and it reads the same either way. */
.patch-info .patch-info {
	flex-direction: column;
	align-items: flex-start;
	gap: 0.35rem;
}

.patch-titles {
	display: flex;
	align-items: center;
	gap: 0.6rem;
	flex-wrap: wrap;
}

.version {
	font-size: 1.25rem;
	font-weight: 1000;
	font-family: "Circular Spotify", sans-serif;
	color: var(--patch-accent);
}

.patch-type {
	font-size: 0.75rem;
	font-family: "Circular Spotify", sans-serif;
	font-weight: 600;
	padding: 0.25rem 0.6rem;
	border-radius: 12px;
	text-transform: uppercase;
	letter-spacing: 0.5px;
}

.patch-type.major {
	background: var(--color-warning-1);
	color: var(--patch-ink);
}

.patch-type.minor {
	background: var(--color-selected-3);
	color: var(--patch-ink);
}

.patch-type.hotfix {
	background: #feca57;
	color: var(--patch-ink);
}

.patch-type.update {
	background: var(--color-primary-a20);
	color: var(--color-text-1);
}

/* Only on the newest patch, and only when it is newer than the one the reader
   last saw. See SEEN_VERSION_KEY in the JS. */
.patch-new {
	font-size: 0.7rem;
	font-family: "Circular Spotify", sans-serif;
	font-weight: 700;
	text-transform: uppercase;
	letter-spacing: 0.6px;
	padding: 0.2rem 0.5rem;
	border-radius: 12px;
	color: var(--patch-ink);
	background: var(--color-selected-3);
	animation: patch-new-pulse 2.4s ease-in-out infinite;
}

@keyframes patch-new-pulse {
	0%, 100% { box-shadow: 0 0 0 0 var(--color-selected-glow-1); }
	50% { box-shadow: 0 0 0 5px transparent; }
}

.patch-summary {
	color: var(--color-text-2);
	font-family: "Circular Spotify", sans-serif;
	font-weight: 400;
	font-size: 0.9rem;
	min-width: 0;
	/* Two lines rather than one hard ellipsis: most summaries fit, and the ones
	   that do not were being cut mid-word on anything narrower than a desktop. */
	display: -webkit-box;
	-webkit-line-clamp: 2;
	line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
}

.spoiler-warning {
	font-family: "Circular Spotify", sans-serif;
	font-weight: 600;
	font-size: 0.72rem;
	text-transform: uppercase;
	letter-spacing: 0.5px;
	color: var(--color-warning-1);
}

.patch-meta {
	display: flex;
	align-items: center;
	gap: 1rem;
	flex-shrink: 0;
	color: var(--patch-muted);
	font-family: "Circular Spotify", sans-serif;
	font-weight: 200;
	font-size: 0.85rem;
}

.date {
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: 0.15rem;
	white-space: nowrap;
}

.date-relative {
	font-size: 0.72rem;
	color: var(--patch-muted);
	opacity: 0.8;
}

/* Drawn rather than a ▼ glyph: it rotates cleanly at any size and picks up the
   text colour, where the character shifted a pixel or two mid-rotation. */
.expand-icon {
	width: 9px;
	height: 9px;
	flex-shrink: 0;
	border-right: 2px solid var(--patch-accent);
	border-bottom: 2px solid var(--patch-accent);
	transform: rotate(45deg);
	transform-origin: 70% 70%;
	transition: transform 0.35s var(--patch-ease);
}

.patch-item.expanded .expand-icon {
	transform: rotate(-135deg);
}

/* ---------------------------------------------------------------------------
   Copy link
   --------------------------------------------------------------------------- */

.patch-copy {
	flex-shrink: 0;
	appearance: none;
	background: none;
	border: 0;
	box-sizing: border-box;
	padding: 0 1.25rem 0 0;
	color: var(--patch-muted);
	cursor: pointer;
	opacity: 0;
	transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
}

.patch-item:hover .patch-copy,
.patch-copy:focus-visible {
	opacity: 1;
}

.patch-copy:hover {
	color: var(--color-selected-3);
}

.patch-copy.copied {
	opacity: 1;
	color: var(--color-selected-3);
}

.patch-copy.copy-failed {
	opacity: 1;
	color: var(--color-warning-1);
}

.patch-copy-icon {
	display: block;
	position: relative;
	width: 15px;
	height: 15px;
}

.patch-copy-icon::before,
.patch-copy-icon::after {
	content: "";
	position: absolute;
	width: 9px;
	height: 9px;
	border: 1.5px solid currentColor;
	border-radius: 3px;
	transition: all 0.2s ease-in-out;
}

.patch-copy-icon::before {
	left: 0;
	bottom: 0;
}

.patch-copy-icon::after {
	right: 0;
	top: 0;
}

/* The two squares fold into a tick once the link is on the clipboard. */
.patch-copy.copied .patch-copy-icon::before {
	opacity: 0;
}

.patch-copy.copied .patch-copy-icon::after {
	width: 6px;
	height: 11px;
	right: 4px;
	top: 1px;
	border-width: 0 2px 2px 0;
	border-radius: 0;
	transform: rotate(45deg);
}

/* ---------------------------------------------------------------------------
   Expanding

   0fr → 1fr animates to whatever the content actually measures. The inner
   wrapper is the thing that clips, and it needs min-height:0 or the grid row
   refuses to go below the content's height.
   --------------------------------------------------------------------------- */

.patch-details {
	display: grid;
	grid-template-rows: 0fr;
	background: var(--color-primary-a0);
	transition: grid-template-rows 0.42s var(--patch-ease);
	max-width: 100%;
	box-sizing: border-box;
}

.patch-item.expanded .patch-details {
	grid-template-rows: 1fr;
}

.patch-details-inner {
	overflow: hidden;
	min-height: 0;
}

.patch-content {
	padding: 1.5rem;
	border-top: 1px solid var(--color-selected-1);
	max-width: 100%;
	box-sizing: border-box;
	opacity: 0;
	transform: translateY(-6px);
	transition: opacity 0.3s ease-in-out 0.06s, transform 0.3s var(--patch-ease) 0.06s;
}

.patch-item.expanded .patch-content {
	opacity: 1;
	transform: none;
}

.changes-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
	gap: 1.5rem;
	min-width: 0;
}

.change-category {
	background: var(--color-primary-a10);
	border-radius: 8px;
	padding: 1rem;
	border: 1px solid var(--color-primary-a20);
	max-width: 100%;
	min-width: 0;
	box-sizing: border-box;
	width: 100%;
}

.category-title {
	color: var(--color-text-1);
	font-family: "Circular Spotify", sans-serif;
	font-weight: 600;
	font-size: 1rem;
	margin: 0 0 0.8rem;
	padding-bottom: 0.5rem;
	border-bottom: 1px solid var(--color-primary-a30);
}

.category-title.new-features {
	color: var(--patch-cat-features);
}

.category-title.improvements {
	color: var(--patch-cat-improvements);
}

.category-title.bug-fixes {
	color: var(--patch-cat-fixes);
}

.category-title.videos {
	color: var(--color-selected-3);
}

.change-list {
	list-style: none;
	padding: 0 0 0 16px;
	margin-bottom: 0;
}

.change-list li {
	margin-bottom: 0.5rem;
	padding-left: 1rem;
	position: relative;
	font-family: "Circular Spotify", sans-serif;
	font-weight: 400;
	font-size: 0.85rem;
	line-height: 1.4;
	color: var(--color-text-2);
}

.change-list li::before {
	content: "•";
	position: absolute;
	left: 0;
	color: var(--patch-accent);
}

/* Search matches. The class already existed but nothing ever applied it — the
   renderer now wraps matches in <mark class="highlight">. */
.highlight,
mark.highlight {
	background: var(--color-selected-glow-1);
	padding: 0.1rem 0.2rem;
	border-radius: 3px;
	color: var(--color-text-1);
}

/* ---------------------------------------------------------------------------
   Spoilers

   The warning used to sit above notes that were already perfectly readable,
   which is not much of a warning. The contents stay veiled until asked for,
   and the answer is remembered for the session.
   --------------------------------------------------------------------------- */

.spoiler-veiled {
	position: relative;
}

.patch-content.spoiler-veiled > .changes-grid,
.patch-content.spoiler-veiled > .media-section {
	filter: blur(7px);
	opacity: 0.55;
	pointer-events: none;
	user-select: none;
	transition: filter 0.4s ease-in-out, opacity 0.4s ease-in-out;
}

img.patch-thumbnail.spoiler-veiled {
	filter: blur(10px);
	transition: filter 0.4s ease-in-out;
}

/* Anchored near the top rather than centred: on a long patch, centring puts
   the only way out of the veil halfway down a screen of blur. */
.spoiler-gate {
	position: absolute;
	inset: 0;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: flex-start;
	gap: 0.75rem;
	text-align: center;
	padding: 2.5rem 1rem 1rem;
}

.spoiler-gate p {
	margin: 0;
	font-family: "Circular Spotify", sans-serif;
	font-size: 0.9rem;
	color: var(--color-text-1);
}

/* ---------------------------------------------------------------------------
   Shared button used by the reveal, retry, reset and expand-all controls
   --------------------------------------------------------------------------- */

.patch-action {
	appearance: none;
	padding: 0.5rem 1.1rem;
	background: var(--color-primary-a10);
	border: 1px solid var(--color-selected-1);
	border-radius: 20px;
	color: var(--color-text-1);
	font-family: "Circular Spotify", sans-serif;
	font-weight: 500;
	font-size: 0.85rem;
	cursor: pointer;
	transition: background 0.2s ease-in-out, border-color 0.2s ease-in-out,
		transform 0.15s ease-in-out;
}

.patch-action:hover {
	background: var(--color-selected-2);
	border-color: var(--color-selected-2);
}

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

.spoiler-reveal {
	appearance: none;
	padding: 0.5rem 1.1rem;
	background: var(--color-warning-2);
	border: 1px solid var(--color-warning-1);
	border-radius: 20px;
	color: var(--color-text-1);
	font-family: "Circular Spotify", sans-serif;
	font-weight: 500;
	font-size: 0.85rem;
	cursor: pointer;
	transition: background 0.2s ease-in-out, transform 0.15s ease-in-out;
}

.spoiler-reveal:hover {
	background: var(--color-warning-3);
}

.spoiler-reveal:active {
	transform: scale(0.97);
}

/* ---------------------------------------------------------------------------
   Stats bar and controls
   --------------------------------------------------------------------------- */

.stats-bar {
	display: flex;
	align-items: center;
	gap: 2rem;
	margin-bottom: 1rem;
	padding: 1rem;
	background: var(--color-primary-a10);
	border-radius: 16px;
	font-family: "Circular Spotify", sans-serif;
	font-weight: 200;
	font-size: 0.85rem;
	color: var(--patch-muted);
}

.stat {
	display: flex;
	align-items: center;
	gap: 0.5rem;
}

.stat-number {
	color: var(--patch-accent);
	font-family: "Circular Spotify", sans-serif;
	font-weight: 600;
}

.expand-all {
	margin-left: auto;
	padding: 0.35rem 0.9rem;
	font-size: 0.8rem;
}

.controls {
	display: flex;
	justify-content: space-between;
	align-items: center;
	margin-bottom: 16px;
	gap: 16px;
	flex-wrap: wrap;
	/* The renderer scrolls here when the list changes underneath the reader;
	   the margin stops the search box ending up flush against the panel edge. */
	scroll-margin-top: 12px;
}

/* The input defaults to about 20 characters wide, which cut the placeholder off
   mid-word with ~590px of the row sitting empty next to it. It now takes the
   space it needs and shares what is left with the filter tabs. */
.search-bar {
	position: relative;
	display: flex;
	align-items: center;
	flex: 1 1 360px;
	min-width: 0;
	max-width: 440px;
}

.search-bar input {
	width: 100%;
	box-sizing: border-box;
	padding: 0.8rem 2.4rem 0.8rem 1rem;
	background: var(--color-primary-a10);
	border: 1px solid var(--color-primary-a20);
	border-radius: 25px;
	color: var(--color-text-1);
	font-size: 0.95rem;
	outline: none;
	transition: border-color 0.25s ease-in-out, box-shadow 0.25s ease-in-out;
	font-family: "Circular Spotify", sans-serif;
	font-weight: 400;
}

.search-bar input::placeholder {
	color: var(--patch-muted);
}

.search-bar input:focus {
	border-color: var(--color-selected-2);
	box-shadow: 0 0 0 3px var(--color-selected-glow-1);
}

/* Safari draws its own clear button on type=search, which would sit on top of
   ours and behave differently. */
.search-bar input::-webkit-search-cancel-button {
	appearance: none;
}

.search-clear {
	position: absolute;
	right: 0.6rem;
	width: 22px;
	height: 22px;
	display: flex;
	align-items: center;
	justify-content: center;
	appearance: none;
	background: none;
	border: 0;
	border-radius: 50%;
	color: var(--patch-muted);
	cursor: pointer;
	transition: color 0.2s ease-in-out, background 0.2s ease-in-out;
}

/* display:flex above would otherwise beat the [hidden] attribute and leave a
   clear button sitting on an empty field. */
.search-clear[hidden] {
	display: none;
}

.search-clear::before {
	content: "\00d7";
	font-size: 1.15rem;
	line-height: 1;
}

.search-clear:hover {
	color: var(--color-text-1);
	background: var(--color-primary-a20);
}

/* ---------------------------------------------------------------------------
   Filter tabs

   The selection used to jump between tabs: the old tab's background was removed
   and the new one's painted in the same frame. A single pill now travels to the
   tab you picked and takes on that type's colour on the way, so the eye follows
   the selection instead of having to re-find it.

   JS/patch-notes-backend.js inserts .filter-indicator and measures it into
   place; .is-ready is added after the first measurement so the pill does not
   animate in from the corner on load. Without JS there is no pill at all, and
   the .active rule below still paints a normal filled tab.
   --------------------------------------------------------------------------- */

.filter-tabs {
	position: relative;
	display: flex;
	gap: 0.5rem;
	/* A touch of overshoot — the pill settles rather than stopping dead. */
	--filter-ease: cubic-bezier(0.34, 1.32, 0.64, 1);
	--filter-pill: var(--color-selected-2);
}

.filter-tabs[data-active="major"] {
	--filter-pill: var(--color-warning-1);
}

.filter-tabs[data-active="minor"] {
	--filter-pill: var(--color-selected-3);
}

.filter-tabs[data-active="hotfix"] {
	--filter-pill: #feca57;
}

.filter-indicator {
	position: absolute;
	top: 0;
	left: 0;
	z-index: 0;
	border-radius: 20px;
	background: var(--filter-pill);
	box-shadow: 0 3px 12px -4px var(--filter-pill);
	pointer-events: none;
	transition: transform 0.45s var(--filter-ease), width 0.45s var(--filter-ease),
		height 0.45s var(--filter-ease), background 0.3s ease-in-out,
		box-shadow 0.3s ease-in-out;
}

.filter-tabs:not(.is-ready) .filter-indicator {
	opacity: 0;
	transition: none;
}

.filter-tab {
	position: relative;
	z-index: 1;
	appearance: none;
	padding: 0.5rem 1rem;
	background: var(--color-primary-a10);
	border: 1px solid var(--color-primary-a20);
	border-radius: 20px;
	cursor: pointer;
	transition: background 0.25s ease-in-out, border-color 0.25s ease-in-out,
		color 0.25s ease-in-out, transform 0.18s var(--filter-ease);
	font-family: "Circular Spotify", sans-serif;
	/* Deliberately the same weight in every state. Bolding the active tab
	   changed its width, and under the mobile `space-around` layout that
	   nudged every other tab sideways at the exact moment the pill was being
	   measured — so the pill kept landing on the previous tab's position. The
	   pill and the ink colour carry the selection instead. */
	font-weight: 500;
	font-size: 0.85rem;
	color: var(--patch-muted);
}

/* Fallback for a no-JS page: without the pill the active tab fills itself. */
.filter-tab.active {
	background: var(--filter-pill);
	border-color: var(--filter-pill);
	color: var(--patch-ink);
}

/* With the pill present the tab goes transparent and lets it show through.
   The ink is one fixed dark tone rather than --color-text-1, because all four
   pill colours are light and white sat at roughly 1.7:1 on the yellow one. */
.filter-tabs.is-ready .filter-tab.active {
	background: transparent;
	border-color: transparent;
	color: var(--patch-ink);
}

.filter-tab:hover:not(.active) {
	background: var(--color-primary-a20);
	border-color: var(--color-selected-1);
	color: var(--color-text-1);
	transform: translateY(-1px);
}

.filter-tab:active:not(.active) {
	transform: scale(0.96);
}

.filter-tab:focus-visible {
	outline: 2px solid var(--color-selected-3);
	outline-offset: 3px;
}

/* Pagination */
.pagination {
	display: flex;
	justify-content: center;
	align-items: center;
	gap: 1rem;
	margin-bottom: 2rem;
	padding: 1rem;
}

.pagination[hidden] {
	display: none;
}

.pagination button {
	padding: 0.5rem 1rem;
	background: var(--color-primary-a10);
	border: 1px solid var(--color-primary-a20);
	border-radius: 16px;
	color: var(--color-text-1);
	font-family: "Circular Spotify", sans-serif;
	font-weight: 400;
	cursor: pointer;
	transition: background 0.25s ease-in-out, border-color 0.25s ease-in-out,
		transform 0.15s ease-in-out;
}

.pagination button:hover:not(:disabled) {
	background: var(--color-selected-2);
	border-color: var(--color-selected-2);
}

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

.pagination button:disabled {
	opacity: 0.6;
	cursor: not-allowed;
	color: var(--patch-muted);
}

.pagination .page-info {
	color: var(--patch-muted);
	font-family: "Circular Spotify", sans-serif;
	font-weight: 500;
	font-size: 0.9rem;
}

/* ---------------------------------------------------------------------------
   Loading, empty and error states
   --------------------------------------------------------------------------- */

/* Three grey rows the shape of real entries, so the list does not collapse to
   nothing and then jolt back to full height when the data lands. */
.patch-skeleton {
	height: 104px;
	border-bottom: 1px solid var(--color-primary-a20);
	background-image: linear-gradient(
		90deg,
		var(--color-primary-a10) 0%,
		var(--color-primary-a20) 40%,
		var(--color-primary-a10) 80%
	);
	background-size: 300% 100%;
	animation: patch-shimmer 1.5s ease-in-out infinite;
	animation-delay: calc(var(--i, 0) * 120ms);
}

.patch-skeleton:last-child {
	border-bottom: none;
}

@keyframes patch-shimmer {
	from { background-position: 150% 0; }
	to { background-position: -150% 0; }
}

.loading,
.no-results,
.patch-error {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 1rem;
	text-align: center;
	padding: 2.5rem 1.5rem;
	color: var(--patch-muted);
	font-family: "Circular Spotify", sans-serif;
	font-weight: 500;
}

.no-results p,
.patch-error p {
	margin: 0;
}

.patch-error {
	color: var(--color-warning-1);
}

/* ---------------------------------------------------------------------------
   Media
   --------------------------------------------------------------------------- */

.media-title {
	border-top: 1px solid var(--color-primary-a20);
	font-family: "Circular Spotify", sans-serif;
	font-weight: 600;
	font-size: 1rem;
	color: var(--color-text-1);
	padding-top: 12px;
	margin: 16px 0;
	text-align: center;
}

.media-grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(256px, 1fr));
	gap: 16px;
}

.media-item {
	display: flex;
	flex-direction: column;
}

.media-image,
.media-video,
.youtube-embed,
.youtube-facade {
	max-width: 480px;
	width: 100%;
	aspect-ratio: 4 / 3;
	height: auto;
	border-radius: 16px;
	object-fit: cover;
	background: var(--color-primary-a10);
	display: block;
	margin: 0 auto;
	border: 0;
}

.media-image {
	cursor: zoom-in;
	transition: transform 0.18s ease-in-out, box-shadow 0.18s ease-in-out;
}

.media-image:hover,
.media-video:hover {
	transform: scale(1.025);
	box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
}

.media-caption {
	margin-top: 0.5rem;
	font-family: "Circular Spotify", sans-serif;
	font-weight: 400;
	font-size: 0.8rem;
	color: var(--patch-muted);
	text-align: center;
}

/* Click-to-play poster. Until this is clicked there is no iframe, no YouTube
   script and no request to Google beyond the thumbnail. */
.youtube-facade {
	position: relative;
	appearance: none;
	padding: 0;
	cursor: pointer;
	background-size: cover;
	background-position: center;
	overflow: hidden;
}

.youtube-facade::after {
	content: "";
	position: absolute;
	inset: 0;
	background: rgba(0, 0, 0, 0.25);
	transition: background 0.2s ease-in-out;
}

.youtube-facade:hover::after {
	background: rgba(0, 0, 0, 0.1);
}

.youtube-play {
	position: absolute;
	top: 50%;
	left: 50%;
	z-index: 1;
	width: 62px;
	height: 44px;
	transform: translate(-50%, -50%);
	border-radius: 12px;
	background: rgba(20, 20, 20, 0.75);
	transition: background 0.2s ease-in-out, transform 0.2s var(--patch-ease);
}

.youtube-play::before {
	content: "";
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-40%, -50%);
	border-style: solid;
	border-width: 9px 0 9px 15px;
	border-color: transparent transparent transparent #ffffff;
}

.youtube-facade:hover .youtube-play {
	background: #ff0000;
	transform: translate(-50%, -50%) scale(1.08);
}

.youtube-facade:focus-visible {
	outline: 2px solid var(--color-selected-3);
	outline-offset: 3px;
}

.patch-thumbnail {
	width: 256px;
	aspect-ratio: 16 / 9;
	object-fit: cover;
	border-radius: 6px;
	flex-shrink: 0;
	border: 1px solid var(--color-primary-a20);
	display: block;
	transition: width 0.4s var(--patch-ease), filter 0.4s ease-in-out;
}

.patch-thumbnail.expanded {
	width: 480px;
}

/* ---------------------------------------------------------------------------
   Responsive
   --------------------------------------------------------------------------- */

@media (max-width: 600px) {
	.filter-tabs {
		justify-content: space-around;
		flex-wrap: wrap;
		width: 100%;
	}

	.search-bar {
		width: 100%;
	}

	.search-bar input {
		width: 100%;
		box-sizing: border-box;
	}

	.patch-header {
		flex-direction: column;
		align-items: flex-start;
		gap: 0.5rem;
		padding: 1.25rem;
	}

	.patch-meta {
		align-self: stretch;
		justify-content: space-between;
		margin-top: 0.25rem;
	}

	.date {
		flex-direction: row;
		align-items: baseline;
		gap: 0.5rem;
	}

	.patch-info {
		flex-direction: column;
		align-items: flex-start;
		gap: 0.5rem;
		width: 100%;
	}

	.patch-summary {
		-webkit-line-clamp: 3;
		line-clamp: 3;
	}

	/* No hover on touch, so the copy button has to be permanently visible or it
	   may as well not exist.

	   It is taken out of the flow here. As a flex sibling of the header it
	   reserved a column down the right of the card, and that column — not the
	   image — is what stopped the thumbnail spanning the full width. Moving it
	   onto the bottom row next to the chevron gives the artwork the whole card
	   and keeps the icon off it. */
	.patch-header-row {
		position: relative;
	}

	.patch-copy {
		opacity: 1;
		position: absolute;
		right: 0.6rem;
		bottom: 0.6rem;
		z-index: 2;
		/* A 15px icon is not a tap target. Missing it landed on the header
		   underneath, which expanded the patch — so the copy button appeared to
		   open the dropdown. 44px is the smallest that reliably gets hit. */
		width: 44px;
		height: 44px;
		padding: 0;
		display: flex;
		align-items: center;
		justify-content: center;
		border-radius: 50%;
	}

	.patch-copy:active {
		background: var(--patch-hover-tint);
	}

	/* Keeps the chevron clear of the copy button. The extra room is deliberate:
	   they are two separate actions a thumb has to choose between. */
	.patch-meta {
		padding-right: 3.4rem;
	}

	.stats-bar {
		flex-wrap: wrap;
		gap: 1rem;
	}

	.expand-all {
		margin-left: 0;
	}

	.changes-grid {
		grid-template-columns: 1fr;
	}

	.patch-thumbnail {
		width: 100%;
		height: auto;
		border-radius: 8px;
		order: -1; /* floats it above everything else in the flex column */
		margin-bottom: 0.5em;
	}

	.patch-thumbnail.expanded {
		width: 100%;
	}
}

/* Coarse pointers get no hover-reveal anywhere, regardless of width — and a
   finger needs more than a 15px glyph to aim at, on a tablet as much as on a
   phone. The narrow layout above positions this button itself. */
@media (hover: none) {
	.patch-copy {
		opacity: 1;
		min-width: 44px;
		display: flex;
		align-items: center;
		justify-content: center;
	}
}

/* ---------------------------------------------------------------------------
   Reduced motion

   Entries still open and close, they just do it immediately. Nothing here
   removes a state change, only the movement between states.
   --------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
	.patch-list .patch-item,
	.patch-skeleton,
	.patch-new,
	.patch-item.patch-targeted {
		animation: none;
		opacity: 1;
	}

	.patch-list,
	.patch-item,
	.patch-details,
	.patch-content,
	.patch-thumbnail,
	.expand-icon,
	.media-image,
	.filter-tab,
	.filter-indicator,
	.patch-action,
	.pagination button,
	.youtube-play,
	.patch-copy-icon::before,
	.patch-copy-icon::after {
		transition: none;
	}

	.filter-tab:hover:not(.active) {
		transform: none;
	}

	.patch-item.patch-targeted {
		box-shadow: inset 0 0 0 2px var(--color-selected-3);
	}

	.media-image:hover,
	.media-video:hover,
	.youtube-facade:hover .youtube-play {
		transform: none;
	}
}
