diff --git a/deploy/caddy/Caddyfile.snapshot b/deploy/caddy/Caddyfile.snapshot
index 0ebf024..3d9450f 100644
--- a/deploy/caddy/Caddyfile.snapshot
+++ b/deploy/caddy/Caddyfile.snapshot
@@ -62,6 +62,11 @@ upbeatbytes.com {
@immutable path /_app/immutable/*
header @immutable Cache-Control "public, max-age=31536000, immutable"
+ # Static texture assets (frame wood grain, etc.) — large and unchanging. Cache
+ # them forever like immutable assets; rename the file if the texture ever changes.
+ @textures path /textures/*
+ header @textures Cache-Control "public, max-age=31536000, immutable"
+
# The SPA shell: "/" and extensionless client routes (try_files → index.html).
# Briefly cacheable at the CDN edge (s-maxage) so a first paint never depends
# on this origin's uplink; browsers still revalidate every visit (max-age=0).
@@ -80,6 +85,7 @@ upbeatbytes.com {
# classic blank-screen cause behind a CDN.
@revalidate {
not path /_app/immutable/*
+ not path /textures/*
path *.*
}
header @revalidate Cache-Control "no-cache"
diff --git a/frontend/src/routes/art/+page.svelte b/frontend/src/routes/art/+page.svelte
index fb54490..5d0db8e 100644
--- a/frontend/src/routes/art/+page.svelte
+++ b/frontend/src/routes/art/+page.svelte
@@ -18,6 +18,13 @@
let zoom = $state(false);
let frame = $state('walnut');
let thickness = $state(1); // frame-scale multiplier, 0.7–1.9 (mat caps at 1.5)
+ let lightboxEl = $state(null);
+
+ function onKey(e) {
+ if (e.key === 'Escape' && zoom) zoom = false;
+ }
+ // Move focus to the lightbox when it opens, so Escape/Enter work and focus is trapped sanely.
+ $effect(() => { if (zoom && lightboxEl) lightboxEl.focus(); });
let who = $derived(
art ? [art.artist || 'Unknown artist', art.date_text].filter(Boolean) : []
@@ -50,6 +57,8 @@
}
+
+
Daily Art · upbeatBytes
@@ -145,7 +154,7 @@
{#if zoom && art}
-