@layer components {
	/* ------------------------------------------------------------------- hero */

	.hero {
		position: relative;
		height: 100svh;
		display: grid;
		overflow: clip;
	}

	/*
	 * Media and scrim are taken out of flow deliberately. As grid items they
	 * have no definite height for `height: 100%` to resolve against, so a tall
	 * portrait photograph falls back to its intrinsic height and stretches the
	 * row — pushing the headline below the clipped edge.
	 */
	.hero-media,
	/*
	 * The veil, between the photograph and everything written on it.
	 *
	 * In the light theme the ink over a frame is dark, and a photograph does
	 * not lighten just because the page did — so this pushes it towards paper
	 * far enough for dark ink to hold. It is nothing at all in the dark theme,
	 * where the frame is already going the way the ink wants.
	 */
	.hero::before {
		content: "";
		position: absolute;
		inset: 0;
		z-index: 1;
		pointer-events: none;
		background: hsl(var(--over-scrim) / var(--over-veil, 0));
	}

	.hero-scrim {
		position: absolute;
		inset: 0;
	}

	.hero-media img {
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	/*
	 * The photograph drifts and dims as it scrolls away, so the headline stays
	 * readable without a heavy permanent scrim. Scroll-driven so there is no
	 * scroll listener.
	 */
	@supports (animation-timeline: view()) {
		@media (prefers-reduced-motion: no-preference) {
			.hero-media img {
				animation: hero-drift linear both;
				animation-timeline: view();
				animation-range: exit-crossing 0% exit 100%;
			}
		}
	}

	@keyframes hero-drift {
		to {
			transform: scale(1.12) translateY(4%);
			filter: brightness(0.55);
		}
	}

	.hero-scrim {
		background:
			linear-gradient(to bottom, hsl(var(--over-scrim) / 0.45) 0%, hsl(var(--over-scrim) / 0) 32%),
			linear-gradient(to top, hsl(var(--over-scrim) / 0.72) 0%, hsl(var(--over-scrim) / 0) 56%);
		pointer-events: none;
	}

	.hero-body {
		position: relative;
		align-self: end;
		color: var(--over-ink);
		padding: var(--gutter);
		padding-bottom: clamp(var(--space-xl), 10vh, var(--space-3xl));
		max-width: 24ch;
	}

	.hero-title {
		font-size: var(--step-7);
		line-height: 0.95;
		letter-spacing: -0.03em;
		margin: 0;
		text-wrap: balance;
	}

	.hero-lede {
		margin-top: var(--space-m);
		max-width: 46ch;
		font-size: var(--step-1);
		color: color-mix(in oklab, var(--over-ink) 82%, transparent);
	}

	.hero-cue {
		position: absolute;
		left: 50%;
		bottom: var(--space-m);
		translate: -50% 0;
		color: var(--over-ink);
		opacity: 0.7;
	}

	@media (prefers-reduced-motion: no-preference) {
		.hero-cue {
			animation: cue-bob 2.4s var(--ease-in-out) infinite;
		}
	}

	@keyframes cue-bob {
		0%, 100% { transform: translateY(0); }
		50% { transform: translateY(6px); }
	}

	/*
	 * A pinned hero. The category's photograph stays put while the sheet of
	 * photographs rises over it, so the backdrop is present the whole way down
	 * rather than scrolling away in the first flick.
	 */
	.hero.is-pinned {
		position: sticky;
		top: 0;
		z-index: 0;
	}

	.hero.is-pinned + .rail {
		position: relative;
		z-index: 1;
		background: var(--bg);
	}

	/* The drift animation fights a pinned hero; the sheet does the moving. */
	.hero.is-pinned .hero-media img {
		animation: none;
	}

	/* ---------------------------------------------------------------- filters */

	.filter-bar {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-2xs);
		align-items: center;
		padding-block: var(--space-l);
	}

	.filter-chip {
		display: inline-flex;
		align-items: center;
		gap: 0.55em;
		padding: 0.7em 1.25em;
		border: 1px solid var(--line);
		border-radius: 100px;
		font-size: 0.76rem;
		font-weight: 500;
		letter-spacing: 0.12em;
		text-transform: uppercase;
		text-decoration: none;
		color: var(--ink-2);
		white-space: nowrap;
		transition: border-color var(--dur-fast) var(--ease-out),
			color var(--dur-fast) var(--ease-out),
			background var(--dur-fast) var(--ease-out);
	}

	.filter-chip:hover {
		border-color: var(--line-strong);
		color: var(--ink);
	}

	.filter-chip[aria-current="true"] {
		background: var(--ink);
		border-color: var(--ink);
		color: var(--bg);
	}

	.filter-chip .count {
		font-size: 0.82em;
		font-variant-numeric: tabular-nums;
		/* Its own chip-coloured wafer, so the number reads as data. */
		padding: 0.15em 0.5em;
		border-radius: 100px;
		background: color-mix(in oklab, currentColor 12%, transparent);
		opacity: 0.85;
	}

	/*
	 * On a narrow screen the chips wrapped into three ragged rows and ate the
	 * section. One swipeable strip instead: it runs edge to edge, snaps, and
	 * fades at the trailing edge to show there is more to reach.
	 */
	@media (max-width: 48rem) {
		.filter-bar {
			flex-wrap: nowrap;
			overflow-x: auto;
			scroll-snap-type: x proximity;
			scrollbar-width: none;
			-webkit-overflow-scrolling: touch;
			margin-inline: calc(var(--gutter) * -1);
			padding-inline: var(--gutter);
			padding-block: var(--space-m);
			/* Trailing edge only — a leading fade would dim the first chip
			   while the strip is still at its start. */
			mask-image: linear-gradient(
				to right,
				#000 calc(100% - 2.5rem),
				transparent 100%
			);
		}

		.filter-bar::-webkit-scrollbar {
			display: none;
		}

		.filter-chip {
			flex: none;
			scroll-snap-align: start;
			padding: 0.75em 1.15em;
		}
	}

	/* ---------------------------------------------------------------- gallery */

	/*
	 * A real grid of equal tiles, not column-count masonry.
	 *
	 * Columns fill one after another and end at whatever height their contents
	 * happen to reach, so a short column left a black hole beside a tall one.
	 * Equal tiles cannot do that: the sheet is always flush, and the only gap
	 * possible is a part-filled last row.
	 */
	.gallery-grid {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: var(--grid-gap, 2px);
	}

	@media (min-width: 60rem) {
		.gallery-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
	}

	@media (min-width: 90rem) {
		.gallery-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
	}

	.gallery-grid.is-roomy {
		--grid-gap: var(--space-m);
	}

	.photo-card {
		position: relative;
		display: block;
		aspect-ratio: 4 / 5;
		margin: 0;
		overflow: clip;
		background: var(--surface-2);
		text-decoration: none;
		color: var(--over-ink);
	}

	.photo-card img {
		width: 100%;
		height: 100%;
		object-fit: cover;
		transition: transform var(--dur-slow) var(--ease-out),
			filter var(--dur) var(--ease-out);
	}

	.photo-card:hover img,
	.photo-card:focus-visible img {
		transform: scale(1.04);
	}

	.photo-card-body {
		position: absolute;
		inset: auto 0 0;
		padding: var(--space-m);
		display: flex;
		align-items: flex-end;
		justify-content: space-between;
		gap: var(--space-s);
		background: linear-gradient(
			to top,
			hsl(var(--over-scrim) / 0.75),
			hsl(var(--over-scrim) / 0)
		);
		opacity: 0;
		translate: 0 8px;
		transition: opacity var(--dur) var(--ease-out),
			translate var(--dur) var(--ease-out);
	}

	.photo-card:hover .photo-card-body,
	.photo-card:focus-visible .photo-card-body {
		opacity: 1;
		translate: 0 0;
	}

	/* Touch devices never hover; keep the caption permanently legible there */
	@media (hover: none) {
		.photo-card-body {
			opacity: 1;
			translate: 0 0;
		}
	}

	.photo-card-title {
		font-family: var(--font-ui);
		font-size: var(--step--1);
		letter-spacing: 0.04em;
		text-transform: uppercase;
	}

	.photo-card-cat {
		font-size: var(--step--1);
		opacity: 0.75;
	}

	/* Card reveal is handled by [data-reveal] in motion.css. */

	/* ------------------------------------------------------------ the deck */

	/*
	 * Phones get a deck rather than a column: one photograph face up, the next
	 * few stacked behind it, thrown aside with a finger. Built by deck.js only
	 * on a narrow viewport, so the class never lands on the desktop grid.
	 */
	/*
	 * Sized from the viewport rather than a fixed ratio, so the deck takes the
	 * room the phone actually has instead of leaving a band of dead space
	 * under it — and stands clear of the filter strip above.
	 */
	.gallery-grid.is-deck {
		display: block;
		position: relative;
		width: min(100%, 30rem);
		height: clamp(22rem, 58svh, 34rem);
		margin-inline: auto;
		margin-block-start: var(--space-l);
		perspective: 1200px;
		touch-action: pan-y;
	}

	/*
	 * Cards behind the top one step down and in. The origin is the bottom
	 * edge, so scaling alone would hide them completely behind the face-up
	 * card — the downward offset is what makes the stack read as a stack.
	 */
	.gallery-grid.is-deck .photo-card {
		position: absolute;
		inset: 0 0 auto 0;
		height: calc(100% - 3.2rem);
		aspect-ratio: auto;
		margin: 0;
		border-radius: var(--radius-lg);
		overflow: hidden;
		box-shadow: var(--shadow-2);
		transform-origin: 50% 100%;
		transition: transform var(--dur) var(--ease-out),
			opacity var(--dur) var(--ease-out),
			scale var(--dur) var(--ease-out),
			translate var(--dur) var(--ease-out);
		scale: calc(1 - var(--d, 0) * 0.055);
		translate: 0 calc(var(--d, 0) * 1.5rem);
		opacity: calc(1 - var(--d, 0) * 0.18);
		/* The reveal animation does not apply inside a deck. */
		filter: none;
	}

	.gallery-grid.is-deck .photo-card img {
		width: 100%;
		height: 100%;
		object-fit: cover;
		/* A native image drag would cancel the throw mid-gesture. */
		-webkit-user-drag: none;
		user-select: none;
		pointer-events: none;
	}

	.gallery-grid.is-deck .photo-card.is-top {
		cursor: grab;
	}

	/* While a finger is on it the card must not lag behind the pointer. */
	.gallery-grid.is-deck .photo-card.is-dragging {
		transition: none;
		cursor: grabbing;
	}

	.gallery-grid.is-deck .photo-card.is-thrown {
		transition: transform 260ms var(--ease-out), opacity 260ms var(--ease-out);
		opacity: 0;
	}

	.gallery-grid.is-deck .photo-card-body {
		opacity: 0;
		translate: 0 0;
		padding: var(--space-m);
	}

	/* Only the face-up card is captioned; behind it the stack is just edges. */
	.gallery-grid.is-deck .photo-card.is-top .photo-card-body {
		opacity: 1;
	}

	/* Clear of the cards that peek below the face-up one. */
	.deck-bar {
		/* Script-inserted, so it says where it goes rather than trusting to
		   auto-placement inside the rail's three-column grid. */
		grid-column: content;
		display: flex;
		align-items: center;
		justify-content: center;
		gap: var(--space-l);
		padding-block: var(--space-l) 0;
	}

	.deck-count {
		margin: 0;
		/* "1 / 12" is six characters, and at 5ch it wrapped onto two lines. */
		min-width: 7ch;
		white-space: nowrap;
		text-align: center;
		font-size: 0.72rem;
		letter-spacing: 0.16em;
		font-variant-numeric: tabular-nums;
		color: var(--ink-3);
	}

	.deck-nav {
		display: grid;
		place-items: center;
		width: 2.75rem;
		height: 2.75rem;
		border-radius: 50%;
		border: 1px solid var(--line);
		color: var(--ink-2);
		font-size: 1rem;
		transition: border-color var(--dur-fast) var(--ease-out),
			color var(--dur-fast) var(--ease-out);
	}

	.deck-nav:hover {
		border-color: var(--line-strong);
		color: var(--ink);
	}

	@media (prefers-reduced-motion: reduce) {
		.gallery-grid.is-deck .photo-card {
			transition: none;
		}
	}

	/*
	 * The deck ends at its own controls, so the section no longer needs the
	 * generous tail a scrolling grid wanted.
	 */
	@media (max-width: 48rem) {
		.section-tight:has(.gallery-grid.is-deck) {
			padding-block-end: var(--space-xl);
		}
	}

	/* -------------------------------------------------------- pagination */

	/*
	 * paginate_links() emits bare anchors; without this they inherit the plain
	 * underlined link style and read as unfinished markup under the grid.
	 */
	.gallery-grid + .page-numbers,
	.nav-links,
	nav.pagination {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		justify-content: center;
		gap: var(--space-2xs);
		padding-block: var(--space-2xl);
	}

	.page-numbers {
		display: grid;
		place-items: center;
		min-width: 2.75rem;
		height: 2.75rem;
		padding-inline: 0.75rem;
		border: 1px solid var(--line);
		border-radius: 100px;
		font-size: 0.78rem;
		letter-spacing: 0.08em;
		font-variant-numeric: tabular-nums;
		text-decoration: none;
		color: var(--ink-2);
		transition: border-color var(--dur-fast) var(--ease-out),
			color var(--dur-fast) var(--ease-out),
			background var(--dur-fast) var(--ease-out);
	}

	.page-numbers:hover {
		border-color: var(--line-strong);
		color: var(--ink);
	}

	.page-numbers.current {
		background: var(--ink);
		border-color: var(--ink);
		color: var(--bg);
	}

	.page-numbers.dots {
		border-color: transparent;
		min-width: 1.5rem;
	}

	/* --------------------------------------------------------- single photo */

	.photo-stage {
		display: grid;
		place-items: center;
		background: var(--surface-2);
		min-height: 60svh;
		padding: var(--space-m);
	}

	.photo-stage img {
		max-height: calc(100svh - var(--header-h) - var(--space-2xl));
		width: auto;
		object-fit: contain;
	}

	.photo-detail {
		display: grid;
		gap: var(--space-xl);
		grid-template-columns: 1fr;
		padding-block: var(--space-xl);
	}

	@media (min-width: 60rem) {
		.photo-detail {
			grid-template-columns: minmax(0, 2fr) minmax(16rem, 1fr);
			gap: var(--space-2xl);
		}
	}

	.exif-list {
		margin: 0;
		display: grid;
		grid-template-columns: auto 1fr;
		gap: var(--space-2xs) var(--space-m);
		font-size: var(--step--1);
	}

	.exif-list dt {
		color: var(--ink-3);
		letter-spacing: 0.04em;
		text-transform: uppercase;
	}

	.exif-list dd {
		margin: 0;
		color: var(--ink);
		font-variant-numeric: tabular-nums;
	}

	/* View Transitions: the thumbnail morphs into the full photo where supported */
	@media (prefers-reduced-motion: no-preference) {
		::view-transition-old(photo),
		::view-transition-new(photo) {
			animation-duration: var(--dur);
			animation-timing-function: var(--ease-out);
		}
	}

	/* ---------------------------------------------------------------- lightbox */

	.lightbox {
		border: 0;
		padding: 0;
		width: 100vw;
		height: 100svh;
		max-width: 100vw;
		max-height: 100svh;
		background: hsl(var(--over-scrim) / 0.95);
		color: var(--over-ink);
		overflow: hidden;
	}

	.lightbox::backdrop {
		background: hsl(var(--over-scrim) / 0.85);
	}

	@media (prefers-reduced-motion: no-preference) {
		.lightbox[open] {
			animation: lightbox-in var(--dur) var(--ease-out);
		}
	}

	@keyframes lightbox-in {
		from { opacity: 0; }
	}

	.lightbox-inner {
		display: grid;
		grid-template-rows: 1fr auto;
		height: 100%;
		padding: var(--space-l) var(--gutter) 0;
	}

	.lightbox-frame {
		margin: 0;
		display: grid;
		place-items: center;
		min-height: 0;
		touch-action: pan-y;
		transition: transform var(--dur) var(--ease-out),
			opacity var(--dur) var(--ease-out);
	}

	.lightbox-frame.is-dragging {
		transition: none;
	}

	.lightbox-frame img {
		max-width: min(94vw, 1800px);
		max-height: calc(100svh - 9rem);
		width: auto;
		height: auto;
		object-fit: contain;
		-webkit-user-drag: none;
		user-select: none;
		transition: opacity var(--dur-fast) var(--ease-out);
	}

	.lightbox-frame.is-loading img {
		opacity: 0.35;
	}

	/* The bar carries what the photograph is, and what can be done with it. */
	.lightbox-bar {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		justify-content: space-between;
		gap: var(--space-s) var(--space-m);
		padding-block: var(--space-m);
		border-top: 1px solid color-mix(in oklab, var(--over-ink) 16%, transparent);
	}

	.lightbox-caption {
		margin: 0;
		min-width: 0;
		font-size: 0.72rem;
		letter-spacing: 0.1em;
		text-transform: uppercase;
		color: color-mix(in oklab, var(--over-ink) 70%, transparent);
	}

	.lightbox-tools {
		display: flex;
		align-items: center;
		gap: var(--space-m);
		margin-left: auto;
	}

	.lightbox-download {
		display: inline-flex;
		align-items: center;
		gap: 0.5em;
		padding: 0.55em 1.1em;
		border: 1px solid color-mix(in oklab, var(--over-ink) 34%, transparent);
		border-radius: 100px;
		font-size: 0.7rem;
		letter-spacing: 0.14em;
		text-transform: uppercase;
		text-decoration: none;
		color: var(--over-ink);
		transition: background var(--dur-fast) var(--ease-out),
			border-color var(--dur-fast) var(--ease-out);
	}

	.lightbox-download:hover {
		background: var(--over-ink);
		border-color: var(--over-ink);
		color: hsl(var(--over-scrim));
	}

	.lightbox-counter {
		margin: 0;
		font-size: 0.72rem;
		letter-spacing: 0.14em;
		font-variant-numeric: tabular-nums;
		color: color-mix(in oklab, var(--over-ink) 60%, transparent);
	}

	.lightbox-close,
	.lightbox-prev,
	.lightbox-next {
		position: absolute;
		display: grid;
		place-items: center;
		width: 2.75rem;
		height: 2.75rem;
		border-radius: 50%;
		border: 1px solid color-mix(in oklab, var(--over-ink) 28%, transparent);
		color: var(--over-ink);
		transition: background var(--dur-fast) var(--ease-out),
			border-color var(--dur-fast) var(--ease-out);
	}

	.lightbox-close:hover,
	.lightbox-prev:hover,
	.lightbox-next:hover {
		border-color: var(--over-ink);
		background: color-mix(in oklab, var(--over-ink) 14%, transparent);
	}

	.lightbox-close {
		top: var(--space-m);
		right: var(--gutter);
		font-size: 1.4rem;
		line-height: 1;
	}

	.lightbox-prev { left: var(--gutter); top: 50%; translate: 0 -50%; }
	.lightbox-next { right: var(--gutter); top: 50%; translate: 0 -50%; }

	/* On a phone the frame is thrown, so the side arrows only get in the way. */
	@media (max-width: 48rem) {
		.lightbox-prev,
		.lightbox-next {
			display: none;
		}

		.lightbox-inner {
			padding-inline: var(--space-s);
		}

		.lightbox-frame img {
			max-height: calc(100svh - 11rem);
		}

		.lightbox-bar {
			justify-content: center;
			text-align: center;
		}

		.lightbox-tools {
			margin-left: 0;
		}
	}

	/* --------------------------------------------------------------- download */

	.download-row {
		display: flex;
		flex-wrap: wrap;
		gap: var(--space-s);
		align-items: center;
		margin-top: var(--space-m);
	}
}
