diff --git a/site/package.json b/site/package.json
index 8798a4640..0b8955161 100644
--- a/site/package.json
+++ b/site/package.json
@@ -19,6 +19,7 @@
"astro": "^7.2.8",
"motion": "^12.34.0",
"reading-time": "^1.5.0",
+ "sharp": "0.35.5",
"tailwindcss": "^4.1.10"
}
}
diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml
index f9067e35a..f33a40b65 100644
--- a/site/pnpm-lock.yaml
+++ b/site/pnpm-lock.yaml
@@ -41,6 +41,9 @@ importers:
reading-time:
specifier: ^1.5.0
version: 1.5.0
+ sharp:
+ specifier: ^0.35.5
+ version: 0.35.5(@types/node@24.13.5)
tailwindcss:
specifier: ^4.1.10
version: 4.3.0
@@ -2400,8 +2403,7 @@ snapshots:
'@fontsource/inter@5.2.8': {}
- '@img/colour@1.1.0':
- optional: true
+ '@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.35.5':
optionalDependencies:
@@ -4210,7 +4212,6 @@ snapshots:
'@img/sharp-win32-ia32': 0.35.5
'@img/sharp-win32-x64': 0.35.5
'@types/node': 24.13.5
- optional: true
shiki@4.4.3:
dependencies:
diff --git a/site/public/_headers b/site/public/_headers
index f2b459a65..42e3ffbfa 100644
--- a/site/public/_headers
+++ b/site/public/_headers
@@ -7,3 +7,8 @@
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
+
+# Built assets carry a content hash in their name, so a new build is a new
+# URL and the old one can be cached for good.
+/_astro/*
+ Cache-Control: public, max-age=31536000, immutable
diff --git a/site/public/textures/frost-blue.webp b/site/public/textures/frost-blue.webp
deleted file mode 100644
index f16f729c7..000000000
Binary files a/site/public/textures/frost-blue.webp and /dev/null differ
diff --git a/site/public/textures/lake-mist.webp b/site/public/textures/lake-mist.webp
deleted file mode 100644
index 43851bc16..000000000
Binary files a/site/public/textures/lake-mist.webp and /dev/null differ
diff --git a/site/public/textures/lake-motion.webp b/site/public/textures/lake-motion.webp
deleted file mode 100644
index 9f5bd7d9d..000000000
Binary files a/site/public/textures/lake-motion.webp and /dev/null differ
diff --git a/site/public/textures/paint-aqua.webp b/site/public/textures/paint-aqua.webp
deleted file mode 100644
index 8c53d2c30..000000000
Binary files a/site/public/textures/paint-aqua.webp and /dev/null differ
diff --git a/site/public/textures/paint-blue.webp b/site/public/textures/paint-blue.webp
deleted file mode 100644
index a79d96368..000000000
Binary files a/site/public/textures/paint-blue.webp and /dev/null differ
diff --git a/site/public/textures/reeded-glass.webp b/site/public/textures/reeded-glass.webp
deleted file mode 100644
index 5628cc4f7..000000000
Binary files a/site/public/textures/reeded-glass.webp and /dev/null differ
diff --git a/site/public/textures/sheer-blue.webp b/site/public/textures/sheer-blue.webp
deleted file mode 100644
index 65639d2e3..000000000
Binary files a/site/public/textures/sheer-blue.webp and /dev/null differ
diff --git a/site/public/textures/sheer-cream.webp b/site/public/textures/sheer-cream.webp
deleted file mode 100644
index 5526d1c6e..000000000
Binary files a/site/public/textures/sheer-cream.webp and /dev/null differ
diff --git a/site/public/textures/sheer-lavender.webp b/site/public/textures/sheer-lavender.webp
deleted file mode 100644
index aca669e3a..000000000
Binary files a/site/public/textures/sheer-lavender.webp and /dev/null differ
diff --git a/site/src/assets/textures/frost-blue.webp b/site/src/assets/textures/frost-blue.webp
new file mode 100644
index 000000000..7696e89b7
Binary files /dev/null and b/site/src/assets/textures/frost-blue.webp differ
diff --git a/site/src/assets/textures/lake-mist.webp b/site/src/assets/textures/lake-mist.webp
new file mode 100644
index 000000000..2cf7fa951
Binary files /dev/null and b/site/src/assets/textures/lake-mist.webp differ
diff --git a/site/src/assets/textures/lake-motion.webp b/site/src/assets/textures/lake-motion.webp
new file mode 100644
index 000000000..bc444e7f5
Binary files /dev/null and b/site/src/assets/textures/lake-motion.webp differ
diff --git a/site/src/assets/textures/paint-aqua.webp b/site/src/assets/textures/paint-aqua.webp
new file mode 100644
index 000000000..6d1eecb59
Binary files /dev/null and b/site/src/assets/textures/paint-aqua.webp differ
diff --git a/site/src/assets/textures/paint-blue.webp b/site/src/assets/textures/paint-blue.webp
new file mode 100644
index 000000000..153231007
Binary files /dev/null and b/site/src/assets/textures/paint-blue.webp differ
diff --git a/site/src/assets/textures/paint-teal.webp b/site/src/assets/textures/paint-teal.webp
new file mode 100644
index 000000000..a36f33086
Binary files /dev/null and b/site/src/assets/textures/paint-teal.webp differ
diff --git a/site/src/assets/textures/reeded-glass.webp b/site/src/assets/textures/reeded-glass.webp
new file mode 100644
index 000000000..a40d2ad71
Binary files /dev/null and b/site/src/assets/textures/reeded-glass.webp differ
diff --git a/site/src/assets/textures/sheer-blue.webp b/site/src/assets/textures/sheer-blue.webp
new file mode 100644
index 000000000..3b4c112f3
Binary files /dev/null and b/site/src/assets/textures/sheer-blue.webp differ
diff --git a/site/src/assets/textures/sheer-cream.webp b/site/src/assets/textures/sheer-cream.webp
new file mode 100644
index 000000000..2fe4ec51d
Binary files /dev/null and b/site/src/assets/textures/sheer-cream.webp differ
diff --git a/site/src/assets/textures/sheer-lavender.webp b/site/src/assets/textures/sheer-lavender.webp
new file mode 100644
index 000000000..e939ee29f
Binary files /dev/null and b/site/src/assets/textures/sheer-lavender.webp differ
diff --git a/site/src/assets/textures/sheer-peach.webp b/site/src/assets/textures/sheer-peach.webp
new file mode 100644
index 000000000..270251fcf
Binary files /dev/null and b/site/src/assets/textures/sheer-peach.webp differ
diff --git a/site/src/assets/textures/sheer-sky.webp b/site/src/assets/textures/sheer-sky.webp
new file mode 100644
index 000000000..870e9cf7f
Binary files /dev/null and b/site/src/assets/textures/sheer-sky.webp differ
diff --git a/site/src/components/Photo.astro b/site/src/components/Photo.astro
new file mode 100644
index 000000000..d733dcea9
--- /dev/null
+++ b/site/src/components/Photo.astro
@@ -0,0 +1,47 @@
+---
+/**
+ * A background photo from src/assets/textures, built into AVIF (WebP for the
+ * few browsers without it) at a few widths, so every screen downloads only
+ * what it draws. Quality 50 keeps the grain of these photos at a fraction of
+ * WebP's size. A ~300-byte blurred copy is inlined as the image's own
+ * background, so the frame is never empty while the real file arrives.
+ */
+import { Picture } from 'astro:assets';
+import sharp from 'sharp';
+import path from 'node:path';
+
+interface Props {
+ name: string;
+ /** The rendered width at each breakpoint, as an list. */
+ sizes: string;
+ /** Above the fold: load first instead of lazily. */
+ priority?: boolean;
+ class?: string;
+}
+const { name, sizes, priority = false, class: className = '' } = Astro.props;
+
+const images = import.meta.glob<{ default: ImageMetadata }>('../assets/textures/*.webp', { eager: true });
+const image = images[`../assets/textures/${name}.webp`]?.default;
+if (!image) throw new Error(`Photo: no texture named "${name}" in src/assets/textures`);
+
+const lqip = await sharp(path.join(process.cwd(), 'src/assets/textures', `${name}.webp`))
+ .resize(24)
+ .webp({ quality: 50 })
+ .toBuffer();
+const placeholder = `background-image:url(data:image/webp;base64,${lqip.toString('base64')});background-size:cover;background-position:center;`;
+---
+
diff --git a/site/src/components/ScaledStage.astro b/site/src/components/ScaledStage.astro
index d4a0a235a..58e6bb761 100644
--- a/site/src/components/ScaledStage.astro
+++ b/site/src/components/ScaledStage.astro
@@ -8,17 +8,23 @@
* (`.is-live`), `[data-tick]` numbers count up while live, and `enter-*`
* pieces play once, the first time it scrolls in (`.is-seen`).
*/
+import Photo from './Photo.astro';
+
interface Props {
w: number;
h: number;
- /** Background photo under /textures. */
- photo: string;
+ /** Background photo in src/assets/textures; without one the stage is a plain grey frame. */
+ photo?: string;
class?: string;
}
const { w, h, photo, class: className = '' } = Astro.props;
+// Wide scenes fill half the page at md+, the small bento ones a third at lg.
+const sizes = w >= 600
+ ? '(min-width: 768px) 50vw, 100vw'
+ : '(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw';
---
-