{$DOMAIN} {
	# Compress everything EXCEPT the SSE stream — gzip buffering delays
	# "real-time" likes/comments until the ~30s keep-alive tick.
	@compressible not path /api/v1/stream
	encode @compressible zstd gzip

	# Site-wide security headers (defense-in-depth). HSTS is free since Caddy
	# already terminates TLS. nosniff also covers all of /media/*.
	header {
		Strict-Transport-Security "max-age=31536000; includeSubDomains"
		X-Content-Type-Options "nosniff"
		Referrer-Policy "strict-origin-when-cross-origin"
	}

	# X-Frame-Options: DENY everywhere EXCEPT the keepsake download endpoints, which
	# are navigated in a HIDDEN, SAME-ORIGIN iframe so a 404/429 can't unload the PWA
	# (see frontend/src/routes/export/+page.svelte). WebKit enforces XFO *before*
	# honouring Content-Disposition, so a blanket DENY makes the download silently do
	# nothing on iOS Safari — the app's primary platform. SAMEORIGIN still blocks
	# cross-origin framing.
	#
	# Split into two disjoint matchers rather than an override: Caddy applies the
	# FIRST header directive outermost, so it wins on write — a later, more specific
	# `header` would be silently ignored.
	@framable path /api/v1/export/zip /api/v1/export/html
	@not_framable not path /api/v1/export/zip /api/v1/export/html
	header @framable X-Frame-Options "SAMEORIGIN"
	header @not_framable X-Frame-Options "DENY"

	# SvelteKit frontend — static assets with long-lived cache (content-hashed filenames)
	@hashed_assets path_regexp hashed /_app/immutable/.*\.[a-f0-9]{8,}\.(js|css|woff2)$
	header @hashed_assets Cache-Control "public, max-age=31536000, immutable"

	# Preview/thumbnail/display images. These are served by the app through a
	# visibility-checked alias (/api/v1/upload/{id}/{preview,thumbnail,display}) so
	# moderation can revoke access; the app serves no /media route at all, so there is no
	# direct path to the bytes. Privately cacheable for a short window (the app sets the
	# same header; this is the edge carve-out from the blanket no-store below). Kept short
	# so a moderated image stops being served to a direct-URL holder promptly.
	#
	# `display` was missing here while the backend set `private, max-age=300` on it, and
	# because `header` REPLACES, the blanket no-store below silently won. That route is the
	# ~2048px derivative the diashow uses exclusively, so a projector left running all
	# evening re-fetched a full-size JPEG for every slide — roughly 2-4 GB pulled through
	# the app over 8 hours, on the same venue uplink 100 guests are uploading over, and a
	# blank frame on every network hiccup.
	@media_api path /api/v1/upload/*/preview /api/v1/upload/*/thumbnail /api/v1/upload/*/display
	header @media_api Cache-Control "private, max-age=300"

	# API and health — never cache, EXCEPT the gated image routes above. A cached health
	# response would report the last known state rather than the current one.
	@api {
		path /api/* /health
		not path /api/v1/upload/*/preview /api/v1/upload/*/thumbnail /api/v1/upload/*/display
	}
	header @api Cache-Control "no-store"

	# Route API and media requests to the Rust backend.
	#
	# The app serves no /media route at all (see the note in backend/src/main.rs) — media
	# bytes are reachable only through the visibility-checked /api/v1/upload aliases, so
	# /media/* forwards to a plain 404. The proxy line is kept deliberately: it means the
	# edge faithfully hands /media to the app, so if a future change ever re-introduces a
	# static media route the e2e gating specs see it here exactly as production would,
	# instead of being masked by the SvelteKit 404 page.
	reverse_proxy /api/* app:3000
	reverse_proxy /media/* app:3000

	# The backend registers /health on its ROOT router, not under /api/v1, so it needs its
	# own line — without it the catch-all below hands /health to SvelteKit, which has no
	# such route and returns its 404 page. That made the documented post-deploy check
	# (`curl -fsS https://DOMAIN/health`) fail 100% of the time on a perfectly healthy
	# stack. e2e/Caddyfile.test has always carried this line; production never did.
	reverse_proxy /health app:3000

	# Everything else goes to SvelteKit frontend
	reverse_proxy frontend:3001
}
