Skip to content
VibeFather
Join

2 cases built from this template

Same prompt, different [VARIABLES]. Click one to preview it above.

About this template

Endless Hall is a one-page online exhibition built with three.js. Twenty-four framed photographs hang along a dark corridor that wraps around itself, so visitors can scroll, drag or swipe in either direction and never reach a wall. The works are split into four named rooms, each introduced by a large italic title floating in the fog, and a slim heads-up frame keeps track of the current room, the work number and progress through the loop. Clicking a photograph flies the camera up to it and opens a letterboxed detail view with the full title, maker, date, process, collection and license, plus a link to the source record. A separate lightbox shows the image at full size with click-to-zoom and keyboard paging, and an Index panel jumps straight to any work. The default case is a small archive of nineteenth-century photography, and every image is a real CC0 photograph from a museum open-access collection, downloaded into the project and credited in ASSETS.md. It suits photographers with a finished series, small museums and archives, print shops, and anyone who wants a collection to feel like a place rather than a grid. Design details: near-black fog, warm off-white type, Cormorant Garamond italics for titles, IBM Plex Mono for labels, film grain, a vignette, drifting dust, planes that bend slightly with speed, and a custom ring cursor. Motion calms down fully under reduced-motion settings, and browsers without WebGL get a clean catalogue page instead. To adapt it, change the name, exhibition title, room names and visiting line, then replace the 24 photographs and their captions in one content file; the corridor, rooms and detail view rebuild from that data.

The template prompt

The live demo was built from exactly this text — nothing added. The other cases only change the values of the [VARIABLES].

# 3D Photo Gallery

Build this website exactly as described below. Replace the [VARIABLES] with your own values; keep everything else.

## Brief
An immersive online exhibition for [BRAND]: twenty-four real public-domain photographs hang as framed planes along an endless dark corridor in WebGL. Visitors scroll, drag or swipe to walk forward and back; the corridor wraps around so the walk never ends. The works are grouped into four named rooms ([ROOMS]), each announced by a huge italic room title floating in the dark. Clicking a photograph flies the camera up to it and opens a letterboxed detail view with full caption, credit and license; from there a 2D lightbox shows the full image with zoom. A thin HUD frames the screen: brand, current room, rolling work counter, progress track, Index and About panels. One page, no backend, every image downloaded into the project and credited.

- Audience: Visitors of a small photography archive or museum who want to browse a collection slowly, plus photographers, curators and archives looking for a memorable way to show a set of 15–40 images.
- Goal: Make browsing a collection feel like walking through a quiet gallery, while every image stays properly captioned and credited: title, maker, date, process, collection and license one click away.
- Tone: Quiet, exact and curatorial. Short factual captions, no exclamation marks, no marketing language. Labels read like museum wall text.

## Stack
- Framework: Vite 5 + TypeScript + three.js
- Styling: Plain CSS, custom properties
- Libraries: three (MIT), @types/three (dev) (MIT), vite (MIT), typescript (Apache-2.0), @fontsource/cormorant-garamond (OFL-1.1), @fontsource-variable/hanken-grotesk (OFL-1.1), @fontsource/ibm-plex-mono (OFL-1.1)
- Build: `npm run build` → static files in `dist/`. No backend, no server code, no external APIs.
- Setup:
  - Scaffold with npm create vite@latest using the vanilla-ts template. No UI framework. One page: index.html plus src/main.ts.
  - vite.config.ts: base './', build { outDir: 'dist', sourcemap: false, assetsInlineLimit: 0 }. Do not copy src/ into dist/.
  - File layout: src/content.ts (brand strings, rooms, the 24 works with every caption field, About copy), src/scene/ (Renderer.ts, CameraRig.ts, Corridor.ts, WorkPlane.ts, RoomTitle.ts, Dust.ts, shaders.ts), src/ui/ (Loader.ts, Hud.ts, Panel.ts, Detail.ts, Lightbox.ts, Cursor.ts, Fallback.ts), src/styles.css.
  - Fonts: import '@fontsource/cormorant-garamond/400-italic.css', '@fontsource/cormorant-garamond/500-italic.css', '@fontsource/cormorant-garamond/500.css', '@fontsource-variable/hanken-grotesk' and '@fontsource/ibm-plex-mono/400.css', '@fontsource/ibm-plex-mono/500.css' in src/main.ts. Before drawing any canvas text, await document.fonts.load for 'italic 400 64px "Cormorant Garamond"' and '400 32px "IBM Plex Mono"'.
  - The 24 photographs live in public/photos/ under the exact file names in Assets and are referenced with relative paths ('./photos/01-effect-of-the-sun.jpg'). Write ASSETS.md at the project root before building.
  - All colors as CSS custom properties on :root in src/styles.css; the same hex values are imported into the three.js code from src/content.ts or a small src/theme.ts.
  - dist/ holds index.html, hashed JS/CSS, the woff2 fonts, favicon.svg and the 24 JPEGs (about 12 MB total). Keep it under 20 MB.

## Variables
- [BRAND] — Archive, museum or studio name (default: "Halvard Archive"). Shown as the italic wordmark top-left, in the loader, the <title> and the About panel.
- [EXHIBITION] — Exhibition or collection title (default: "First Light"). One to three words. Shown large in the loader and under the wordmark.
- [COLLECTION_NOTE] — One-line description of the collection (default: "Early photographs of water, stone, leaf and sky, 1841–1900")
- [ROOMS] — Four room names, in walking order (default: "Tide, Stone, Leaf, Sky"). Exactly four single words. Works 01–06 hang in the first room, 07–12 in the second, 13–18 in the third, 19–24 in the fourth.
- [VISIT_LINE] — Practical line: address, hours or price (default: "Reading room: 9 Cinder Wharf, Port Aldery · Thu–Sun 11:00–17:00 · Free entry")
- [CREDIT] — keep a small "Template by VibeFather" link to https://vibefather.ai/t/3d-photo-gallery in the footer (default: yes; delete this line to remove it)

The copy under Pages & Sections is written for the default values. If your values differ, rewrite every headline, paragraph, label and image for them — same length, tone and structure. Layout, colors, type and motion stay as described.
Keep all copy, image paths and links in one content file (for example `src/content.ts`), keyed by section, so the content can be swapped later without touching components.

## Design
### Colors
- Void `#0b0a09` — Page and scene background, fog color, loader background
- Soot `#15130f` — Index and About panels, mobile detail sheet, fallback page
- Frame `#2a241d` — Thin frame plane behind every photograph
- Bone `#ece4d6` — Primary text, room titles, cursor ring, dust particles
- Ash `#8f877a` — Secondary text, mono labels, dates, inactive controls
- Hairline `rgba(236,228,214,0.14)` — 1px dividers, progress track, panel borders
- Albumen `#c99a5b` — The only accent: progress fill, cursor dot, active index number, focus rings, the small dot in buttons. Never for large fills or body text.
- Letterbox `#000000` — Detail-view letterbox bars and the lightbox backdrop (at 96% opacity)

### Typography
- Display (wordmark, room titles, work titles): Cormorant Garamond (OFL), weights 400/500
- Text (captions, panels, buttons): Hanken Grotesk (OFL), weights 400/500
- Mono micro-labels (counters, room labels, hints, credits): IBM Plex Mono (OFL), weights 400/500

### Layout & motion
- Spacing: HUD sits on a fixed inset frame: 28px from each edge at ≥ 900px wide, 18px below. Panels use 40px padding (24px mobile). Mono labels are 11px uppercase with 0.16em letter-spacing. Detail text column starts at 6vw from the left, max-width 520px. Vertical rhythm in text blocks is 8px multiples (8, 16, 24, 40).
- Radius: 0 everywhere (frames, panels, images, lightbox). Only pill buttons use 999px, and the cursor ring is a circle.
- Motion: Easing: entrances cubic-bezier(.2,.8,.2,1); panels and letterbox cubic-bezier(.7,0,.2,1); camera flights easeInOutCubic. Damping helper damp(a, b, λ, dt) = a + (b − a)·(1 − e^(−λ·dt)) drives every continuous value.
- Motion: Inertial walk: wheel, drag and swipe write to a scroll target; the camera chases it with damp λ 5 and keeps momentum after release (velocity decays by e^(−3.5·dt)).
- Motion: Velocity bend: photograph planes curve slightly along their width while the camera moves (uBend = clamp(speed × 0.02, −0.35, 0.35)) and flatten when it stops.
- Motion: Pointer parallax: camera offsets up to 0.25 units in x and 0.15 in y and rotates up to 0.04 rad toward the pointer, damped λ 3.
- Motion: Camera flight to a photograph in 1.2 s with a FOV kick (+8° at the midpoint), other works dimming to 12% brightness in 0.6 s.
- Motion: Rolling digits: each digit is a masked vertical 0–9 strip that slides to the new value in 0.6 s; room names slide up out and in through a mask in 0.5 s.
- Motion: Loader shutter: the loader splits into a top and bottom half that slide off-screen in 0.9 s, then HUD items rise 12px and fade in with an 80ms stagger and the works reveal nearest-first.
- Motion: Film grain overlay steps through 8 offsets per second; dust drifts slowly. prefers-reduced-motion: no inertia (λ 14, no momentum), no bend, no FOV kick, no auto-walk, static grain and dust, camera jumps replaced by 0.35 s crossfades, rolling digits switch instantly.
- Imagery: Real nineteenth-century photographs only: 24 CC0 images from The Cleveland Museum of Art Open Access, kept in their original warm albumen, salted-paper, cyanotype and photochrom tones. No color grading, no filters, no crops: each plane uses the image's natural aspect ratio. Everything else (frames, labels, room titles, dust, grain, vignette, cursor, favicon) is drawn in code. No portraits and no logos.

## Pages & Sections
### Gallery (single page) (`index.html`)
1. **Document** — <title>[BRAND] — [EXHIBITION]</title>, lang 'en', theme-color #0b0a09, color-scheme dark. Favicon public/favicon.svg: a 32×32 square in #0b0a09 (rx 6) with a centered circle r 8 stroked #c99a5b at 2px and a filled #c99a5b dot r 2.5 in its center, like an aperture. body is overflow hidden, height 100dvh, background Void, user-select none on the stage.
   Layout: DOM order inside <main>: #stage (canvas container, fixed inset 0), .vignette, .grain, header.hud, aside#index, aside#about, section#detail (role dialog), div#lightbox (role dialog), div#loader, div#cursor, section#fallback (hidden). One H1, visually hidden, containing '[BRAND] — [EXHIBITION]'.
2. **Loader** — Top-left: '[BRAND]' in Cormorant Garamond italic 500, 28px, Bone. Center: '[EXHIBITION]' in Cormorant Garamond italic 400, clamp(72px, 13vw, 210px), line-height 0.9, Bone, centered. Under it, mono 11px Ash: 'HANGING PLATES  014 / 024' where the first number counts textures actually loaded (zero-padded to three digits). Bottom-left mono 11px Ash: '[COLLECTION_NOTE]'. Bottom-right mono 11px Ash: 'Drag, scroll or swipe'.
   Layout: Fixed full-screen, background Void, z-index above everything. A 1px Hairline line spans the full width at 50% height behind the title; an Albumen 1px line grows over it from left to right with load progress (width = loaded / 24).
   Motion: The exhibition title enters letter by letter: each letter rises from 110% to 0 inside an overflow-hidden line, 40ms stagger, 0.8s. When all 24 textures are loaded and the progress line has reached 100%, hold 300ms, then the loader splits: its top half translates to −100% and its bottom half to +100% (two stacked divs that each contain half the content, clip-path not needed), 0.9s cubic-bezier(.7,0,.2,1). Remove the loader from the DOM after the transition. Reduced motion: no letter animation, plain 0.4s fade.
3. **3D corridor (scene)** — three.js WebGLRenderer (antialias true, alpha false), pixel ratio min(devicePixelRatio, 2) desktop and min(devicePixelRatio, 1.75) when the viewport is under 900px wide, outputColorSpace SRGBColorSpace, no tone mapping, clear color #0b0a09. PerspectiveCamera FOV 45 (55 when aspect < 0.8), near 0.1, far 80, starting at (0, 0, 0) looking down −z. Corridor layout constants: work spacing 3.2 units, room length 25.2, loop length L = 100.8. Room r (0–3) has offset O = r × 25.2; its room title sits at z = −(O + 3); its work k (0–5) sits at z = −(O + 7 + k × 3.2). Per-room offsets for work k: x = [−1.6, 1.45, −1.35, 1.7, −1.5, 1.4][k], y = [0.12, −0.18, 0.22, −0.08, 0.05, −0.2][k], rotation.y = −sign(x) × 0.12 (each work turns slightly toward the corridor center). When aspect < 0.8 multiply x by 0.42 and plane size by 0.8.
   Layout: Infinite wrap: every work, room title and dust particle keeps a base z; each frame its rendered z = camZ + rel, where rel = ((baseZ − camZ − 4) mod L + L) mod L − L + 4, so objects stay between L − 4 units ahead and 4 units behind the camera. Each work is a THREE.Group with: a frame plane (Frame color, MeshBasicMaterial, size w + 0.08 by h + 0.08, z −0.01); the photo plane (PlaneGeometry w × h with 24×1 segments, 16×1 on mobile, custom ShaderMaterial); and a museum label plane 0.18 units below the frame's bottom edge, left-aligned with it: a 1024×128 canvas texture with mono 34px Ash '07' then, after 48px, Cormorant italic 52px Bone short title, drawn on transparency, plane width 1.4 units. Photo size: h = 1.7, w = 1.7 × imageAspect; if w > 2.3 then w = 2.3 and h = 2.3 / imageAspect. Photo shader uniforms: uTex, uReveal (0–1), uDim (1 = normal), uHover (0–1), uBend, uFogColor #0b0a09, uFogNear 6, uFogFar 34. Vertex: position.z += sin(uv.x × π) × uBend × w × 0.25; pass view depth. Fragment: color = texture × uDim × (1 + uHover × 0.08); mix toward uFogColor by smoothstep(uFogNear, uFogFar, depth); alpha = uReveal. Textures: TextureLoader, colorSpace SRGBColorSpace, anisotropy min(8, renderer max), mipmaps on. Raycast only against photo planes.
   Motion: Camera rig: scroll target s (positive = forward), position p, velocity v. Wheel: s += deltaY × 0.01 (deltaMode 1 → × 16, deltaMode 2 → × innerHeight). Pointer drag on the stage: s += (clientY − lastY) × 0.018 (0.03 for touch), pan x target −= (clientX − lastX) × 0.003, clamped to ±1.1 (±0.5 when aspect < 0.8); on release, v = average drag speed over the last 100ms, and pan target eases back to 0 with λ 1.5. Each frame: s += v·dt, v *= e^(−3.5·dt), p = damp(p, s, 5, dt); camera z = −p, camera x = pan + pointerX × 0.25, camera y = pointerY × 0.15, rotation.y = −pointerX × 0.04, rotation.x = pointerY × 0.03. Keys: ArrowDown / PageDown / S = s + 3.2; ArrowUp / PageUp / W = s − 3.2; Enter opens the current work; I toggles Index; Esc closes the top-most overlay. Auto-walk: after 6s with no input and no overlay open, s += 0.25·dt until the next input. Hover: when the pointer is over a photo plane, its uHover eases to 1 and its group scales to 1.035 (damp λ 8). Entrance after the loader: works reveal nearest-first, uReveal 0→1 and y offset −0.3→0 over 0.8s with 90ms stagger. A pointerup counts as a click only if the pointer moved less than 6px since pointerdown.
4. **Room titles** — One floating title per room, in order: 'ROOM I' / 'Tide', 'ROOM II' / 'Stone', 'ROOM III' / 'Leaf', 'ROOM IV' / 'Sky' (names from [ROOMS]). Under each name, mono 30px Ash: 'SIX PHOTOGRAPHS'.
   Layout: Each title is a plane 8 × 3 units at x 0, y 0.4, facing the camera, textured with a 2048 × 768 transparent canvas: mono 500 40px Bone with 0.3em tracking 'ROOM II' centered at y 150; Cormorant Garamond italic 400 360px Bone room name centered at y 520; mono 30px Ash 'SIX PHOTOGRAPHS' centered at y 690. MeshBasicMaterial transparent, depthWrite false. When aspect < 0.8 scale the plane to 0.55.
   Motion: Opacity from the title's rel z: 0 beyond −22, rising to 0.92 at −12, holding, then falling to 0 between −3 and −0.5. While the detail view is open all room titles fade to 0 in 0.5s.
5. **HUD** — Top-left: '[BRAND]' (Cormorant italic 500, 30px, Bone) and under it, 8px gap, mono 11px uppercase Ash with 0.16em tracking: '[EXHIBITION]' on line one and '[COLLECTION_NOTE]' on line two (max-width 300px). Top-center: mono 11px Ash 'NOW IN ROOM' over a line with a rolling Roman numeral in mono 13px Bone, a '· ' separator and the room name in Cormorant italic 24px Bone (e.g. 'II · Stone'). Top-right: two text buttons in mono 12px uppercase Bone, 24px apart: 'Index (24)' and 'About'. Bottom-left: rolling counter in mono 13px Bone '07' plus Ash ' / 24', then the current short title in Cormorant italic 26px Bone, then maker and date in Hanken Grotesk 13px Ash ('Timothy H. O'Sullivan, c. 1871'). Bottom-center: a 32px Hairline rule followed by mono 11px Ash 'Scroll or drag to walk · Click a photograph to look closer' (touch devices: 'Swipe to walk · Tap a photograph to look closer'). Bottom-right: the progress track, and under it mono 10px Ash 'Images: The Cleveland Museum of Art · CC0' followed by the [CREDIT] link if kept; this bottom-right line is the page footer.
   Layout: header.hud is fixed inset 0 with pointer-events none (its buttons and links re-enable them). Items are absolutely positioned on the 28px inset frame (18px under 900px). The current work is the one whose rendered z is closest to camZ − 4; the current room is that work's room. Progress track: 140px × 1px Hairline with an Albumen fill whose width = (p mod L) / L, four 1px × 6px Hairline ticks at the room starts (0%, 25%, 50%, 75%) and mono 10px Ash numerals I II III IV under the ticks. Under 900px: top-center room block is hidden and its content joins bottom-left as 'II · Stone — 07 / 24'; the hint moves above bottom-left; the track is 72px wide; top-left shows only the wordmark (24px) and the exhibition title.
   Motion: Counter digits and the room numeral use rolling digits; title and maker lines crossfade with a 6px upward slide in 0.4s when the current work changes. Text buttons get a 1px Albumen underline that grows from the left on hover in 0.3s. The bottom-center hint fades out for good once the camera has travelled 6 units. While the detail view or lightbox is open, the HUD fades to opacity 0 in 0.4s.
6. **Index panel** — Header: 'Index' in Cormorant italic 400, 44px, Bone, with a mono 12px uppercase 'Close ×' button on the right. Then four groups, each headed by mono 11px Ash 'ROOM I — TIDE' (numeral and room name from [ROOMS]), listing that room's six works. Each row: zero-padded number in mono 12px Ash, short title in Hanken Grotesk 500 15px Bone, date in mono 12px Ash right-aligned.
   Layout: aside#index fixed to the right, full height, width 440px (100vw under 900px), background Soot, 1px Hairline left border, 40px padding (24px mobile), its own vertical scroll. Rows are a 3-column grid (40px, 1fr, auto) with 12px vertical padding and a 1px Hairline bottom border. While open, the canvas dims: every photo's uDim eases to 0.4.
   Motion: Slides in from translateX(100%) to 0 in 0.55s cubic-bezier(.7,0,.2,1); rows fade up 8px with 25ms stagger. Row hover: title shifts 6px right and the number turns Albumen (0.25s). Clicking a row closes the panel, sets the scroll target so that the work's rendered z ends up at camZ − 4.5 (taking the shorter way around the loop), and opens the detail view as soon as |p − s| < 0.05 or after 1.4s, whichever comes first.
7. **About panel** — Header: 'About' in Cormorant italic 44px with 'Close ×'. Paragraph 1 (Hanken 16px/1.6 Bone): '[BRAND] is a small independent archive of nineteenth-century photography. [EXHIBITION] hangs twenty-four photographs made between 1841 and 1900 along a corridor that never ends: walk it in either direction and the rooms come round again.' Paragraph 2 (Ash): 'Every image here comes from the Open Access collection of The Cleveland Museum of Art and is dedicated to the public domain under CC0. Captions give the maker, date, process and fund that acquired each print; follow "Source record" in any detail view for the full museum entry.' Then a 1px Hairline and a definition list in mono 11px Ash labels / Hanken 15px Bone values: 'Visit' → '[VISIT_LINE]'; 'Write' → '[email protected]' as a mailto link; 'Keys' → '↑ ↓ walk · Enter look closer · I index · Esc close'. Bottom: mono 10px Ash 'Images: The Cleveland Museum of Art · CC0' and the [CREDIT] link if kept.
   Layout: Same shell, size and behavior as the Index panel (aside#about). Only one of Index and About can be open at a time.
   Motion: Same as the Index panel.
8. **Detail view** — For the focused work: kicker in mono 11px Ash 'ROOM II — STONE · 07 / 24'; full title in Cormorant Garamond italic 400, clamp(40px, 4.6vw, 76px), line-height 0.95, Bone (long titles allowed to wrap; for titles over 70 characters use clamp(30px, 3vw, 48px)); maker in Hanken Grotesk 500 18px Bone; the caption in Hanken 400 16px/1.6 Ash, max 40ch; a definition list with four rows, labels in mono 11px uppercase Ash and values in Hanken 14px Bone: 'Date', 'Process', 'Collection' ('The Cleveland Museum of Art, ' + fund), 'License' ('CC0 1.0 — free to reuse'). Buttons: a pill 'View full image' (1px Bone border, mono 12px uppercase, a 6px Albumen dot before the label, 44px high, 20px horizontal padding) and a text link 'Source record ↗' to the work's museum record (target _blank, rel noopener noreferrer). Top-right 'Close ×'; bottom-left '← Previous', bottom-right 'Next →' (mono 12px uppercase Bone, inside the letterbox bars).
   Layout: section#detail fixed inset 0, transparent, over the canvas. Two black Letterbox bars, top and bottom, each 8vh tall. Desktop: text column at left 6vw, vertically centered, max-width 520px, with a soft gradient from rgba(11,10,9,0.85) at the left edge to transparent at 50vw behind it for legibility. The camera frames the photograph to the right of center. Under 900px: no bottom bar; the text sits in a bottom sheet (52vh, Soot at 94% opacity, 1px Hairline top border, 24px padding, scrolls internally), and the photograph is framed in the upper half. Focus is trapped inside the dialog; Esc closes; ← / → go to previous / next.
   Motion: Opening a work: snapshot the free camera pose, then fly in 1.2s (easeInOutCubic) to the point 2.2 × max(h / 1.7, w / 2.3) units in front of the photo along its facing normal (× 1.5 under 900px), looking at the photo center shifted 0.6 units to the left on desktop (photo appears right of center) or 0.45 units down on mobile (photo appears in the upper half). FOV = base + 8 × sin(π t) during the flight. Other works' uDim eases to 0.12 in 0.6s. At 65% of the flight, the letterbox bars scale in from the screen edges (scaleY 0→1, 0.6s), the kicker and meta fade in, and the title's words rise from 100% to 0 in masks with 60ms stagger. While open, wheel and vertical drag do not walk; a horizontal drag over 80px or ← / → flies to the neighbouring work (0.9s) and swaps the text with a 0.3s crossfade; a wheel total over 120px down or Close flies back to the saved free pose in 1.0s while the overlay fades out in 0.3s and uDim returns to 1. Reduced motion: no flight; the camera cuts to the framed pose behind a 0.35s crossfade of the overlay.
9. **Lightbox** — The full photograph from public/photos/ as an <img> with its alt text. Caption bar along the bottom in mono 11px Ash: '07 — Iceberg Canyon, Colorado River Looking Above · Timothy H. O'Sullivan · c. 1871 · The Cleveland Museum of Art · CC0'. Top-right mono 12px 'Close ×'; left and right edge buttons '←' and '→' (48px square hit areas). A mono 10px Ash hint top-left: 'Click to zoom'.
   Layout: div#lightbox fixed inset 0, background rgba(0,0,0,0.96), image centered with max 92vw × 84vh and object-fit contain, no frame. Opened from 'View full image' in the detail view, and from any figure in the fallback catalogue. Focus trapped, Esc closes, ← / → change image across all 24 works.
   Motion: Opens with the backdrop fading in over 0.3s and the image scaling from 0.94 to 1 with opacity 0→1 in 0.35s. Clicking the image toggles 2× zoom centered on the click point (transform transition 0.35s); while zoomed, dragging pans the image, clamped to its edges. Changing image crossfades in 0.25s. Reduced motion: fades only, zoom without transition.
10. **Cursor and atmosphere** — Custom cursor (pointer: fine devices only, the native cursor hidden over the stage): a 36px circle ring with a 1px rgba(236,228,214,0.5) border and a 4px Albumen dot at its center, plus a mono 10px uppercase Bone label inside the ring. Over a photograph the ring grows to 72px and reads 'Look'; while dragging it squashes to 44 × 28px and reads 'Walk'; over buttons and links it shrinks to 20px with no label. Atmosphere overlays: .vignette = radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,0.65) 100%); .grain = an inline SVG feTurbulence noise (baseFrequency 0.9, numOctaves 2) as a data URI, tiled at 180px, opacity 0.07, mix-blend-mode overlay. Dust: 600 THREE.Points in a box x ±6, y ±3, z from −30 to +2 relative to the camera (wrapped like the works), size 0.018 with size attenuation, Bone at opacity 0.35, depthWrite false.
   Layout: Cursor, vignette and grain are fixed full-screen layers with pointer-events none; the cursor sits above all overlays except the loader. Hide the cursor element on touch devices and when the pointer leaves the window.
   Motion: Ring follows the pointer with damp λ 12, the dot with λ 30; size changes take 0.3s. Grain jumps between 8 preset background offsets 8 times a second (steps). Dust particles bob in y by sin(time × 0.2 + index) × 0.04. Reduced motion: grain and dust static, ring follows instantly.
11. **Fallback catalogue** — Shown only when WebGL is unavailable or scene setup throws. Header: '[BRAND]' wordmark, '[EXHIBITION]' in Cormorant italic clamp(56px, 9vw, 140px), '[COLLECTION_NOTE]' in mono 11px Ash, and the line 'This browser cannot open the walk-through, so here is the catalogue.' in Hanken 15px Ash. Then the four rooms, each with its mono 'ROOM I — TIDE' heading and six figures: the photo, then a figcaption with number (mono), short title (Cormorant italic 24px), maker and date (Hanken 14px Ash), and 'The Cleveland Museum of Art · CC0' (mono 10px Ash).
   Layout: section#fallback replaces the loader and stage; body scrolls. Max-width 1200px, centered, 40px side padding (20px mobile). Each room is a 3-column grid with 32px gaps (1 column under 700px); images keep their natural aspect, loading lazy. Clicking a figure opens the Lightbox. The [CREDIT] link sits at the bottom with the image credit.
   Motion: Figures fade up 12px as they enter the viewport (IntersectionObserver, 0.6s). None under reduced motion.

## Assets
- font: Self-hosted through Fontsource: @fontsource/cormorant-garamond (400 italic, 500 italic, 500), @fontsource-variable/hanken-grotesk (variable, use 400 and 500), @fontsource/ibm-plex-mono (400, 500), imported in src/main.ts. CSS families: 'Cormorant Garamond', 'Hanken Grotesk Variable', 'IBM Plex Mono', each with a serif / sans-serif / monospace fallback. Vite bundles the woff2 files into dist/.
- icon: No icon library. Arrows and close marks are text glyphs (←, →, ↑, ↓, ↗, ×) in IBM Plex Mono. public/favicon.svg is hand-written as described in Document.
- image: Drawn in code, no files: frames, museum labels and room titles (canvas textures), dust (THREE.Points), film grain (inline SVG data URI), vignette (CSS gradient), cursor (CSS).
- image: The 24 photographs below are CC0 images from The Cleveland Museum of Art Open Access. Download each with curl into public/photos/ under the exact file name, check that all 24 exist before building, and never hotlink. Put every caption field (number, room, short title, full title, maker, date, process, collection, license, record URL, alt, caption) in src/content.ts exactly as written. Write ASSETS.md at the project root with one row per photo: path, source image URL, record URL, author (the maker), license 'CC0 1.0 — The Cleveland Museum of Art Open Access'. Also list the three Fontsource packages (OFL-1.1) and three.js (MIT).
- image: Work 01 (Room I, Tide). Download https://openaccess-cdn.clevelandart.org/1987.54/1987.54_web.jpg to public/photos/01-effect-of-the-sun.jpg (1165×893 JPEG, keep as is). Short title: "An Effect of the Sun". Full title: "An Effect of the Sun, Normandy". Maker: Gustave Le Gray. Date: c. 1856. Process: Albumen print from wet collodion negative. Collection: The Cleveland Museum of Art, Andrew R. and Martha Holden Jennings Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1987.54. Alt: "Sun breaking through heavy clouds above a flat, glittering seashore". Caption: "Sun breaks through heavy cloud over a flat, glittering shore. The horizon sits low, so the sky does most of the work." Used as: corridor plane 01, detail view, lightbox, Index row and fallback catalogue.
- image: Work 02 (Room I, Tide). Download https://openaccess-cdn.clevelandart.org/1991.292/1991.292_web.jpg to public/photos/02-great-upper-waterfall.jpg (703×893 JPEG, keep as is). Short title: "Great Upper Waterfall". Full title: "Great Upper Waterfall, High Alps". Maker: Louis-Alphonse Davanne. Date: c. 1862. Process: Albumen print from wet collodion negative. Collection: The Cleveland Museum of Art, Gift of Russ Anderson. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1991.292. Alt: "A tall waterfall dropping through dark pine trees, a wooden railing in the foreground". Caption: "A waterfall drops through dark pines in one long white ribbon; a wooden railing crosses the foreground." Used as: corridor plane 02, detail view, lightbox, Index row and fallback catalogue.
- image: Work 03 (Room I, Tide). Download https://openaccess-cdn.clevelandart.org/2006.4/2006.4_web.jpg to public/photos/03-winter-trees-pond.jpg (1034×893 JPEG, keep as is). Short title: "Winter Trees Reflected in a Pond". Full title: "Winter Trees Reflected in a Pond". Maker: William Henry Fox Talbot. Date: 1841–42. Process: Salted paper print from calotype negative. Collection: The Cleveland Museum of Art, Purchase from the J. H. Wade Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/2006.4. Alt: "Bare winter trees on a bank, mirrored upside down in a still pond". Caption: "Bare winter trees stand on the far bank and again, upside down, in the still pond below." Used as: corridor plane 03, detail view, lightbox, Index row and fallback catalogue.
- image: Work 04 (Room I, Tide). Download https://openaccess-cdn.clevelandart.org/1997.44.a/1997.44.a_web.jpg to public/photos/04-river-landscape-scotland.jpg (1120×893 JPEG, keep as is). Short title: "River Landscape, Scotland". Full title: "River Landscape, Scotland". Maker: Captain Horatio Ross. Date: c. 1858. Process: Albumen print from wax paper negative. Collection: The Cleveland Museum of Art, John L. Severance Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1997.44.a. Alt: "A calm river bending between steep wooded banks and dark rocks". Caption: "A quiet river bends between steep wooded banks and dark rocks, its surface smoothed to a pale sheet." Used as: corridor plane 04, detail view, lightbox, Index row and fallback catalogue.
- image: Work 05 (Room I, Tide). Download https://openaccess-cdn.clevelandart.org/1990.33/1990.33_web.jpg to public/photos/05-miners-bridge.jpg (1048×893 JPEG, keep as is). Short title: "The Miners' Bridge". Full title: "The Miners' Bridge, on the Llugwy, North Wales". Maker: Roger Fenton. Date: 1857. Process: Albumen print from wet collodion negative. Collection: The Cleveland Museum of Art, Andrew R. and Martha Holden Jennings Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1990.33. Alt: "A slanting plank footbridge over a rocky stream below a wooded hillside". Caption: "A plank footbridge slants across a rocky stream under a wooded hillside." Used as: corridor plane 05, detail view, lightbox, Index row and fallback catalogue.
- image: Work 06 (Room I, Tide). Download https://openaccess-cdn.clevelandart.org/1997.47/1997.47_web.jpg to public/photos/06-city-island.jpg (1262×893 JPEG, keep as is). Short title: "City Island". Full title: "City Island, New York". Maker: Arthur Wesley Dow. Date: c. 1900. Process: Cyanotype. Collection: The Cleveland Museum of Art, John L. Severance Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1997.47. Alt: "A beached rowboat, a board fence and leafless trees, printed in deep blue". Caption: "A beached rowboat, a board fence and leafless trees, printed in the deep blue of a cyanotype." Used as: corridor plane 06, detail view, lightbox, Index row and fallback catalogue.
- image: Work 07 (Room II, Stone). Download https://openaccess-cdn.clevelandart.org/2002.46/2002.46_web.jpg to public/photos/07-iceberg-canyon.jpg (1111×893 JPEG, keep as is). Short title: "Iceberg Canyon". Full title: "Iceberg Canyon, Colorado River Looking Above". Maker: Timothy H. O'Sullivan. Date: c. 1871. Process: Albumen print from wet collodion negative. Collection: The Cleveland Museum of Art, John L. Severance Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/2002.46. Alt: "Dark canyon walls sloping down to a calm river, a tiny seated figure on the rocks". Caption: "Dark canyon walls slope down to a calm stretch of river; a small seated figure on the rocks gives the scale." Used as: corridor plane 07, detail view, lightbox, Index row and fallback catalogue.
- image: Work 08 (Room II, Stone). Download https://openaccess-cdn.clevelandart.org/2024.6.gg/2024.6.gg_web.jpg to public/photos/08-river-gorge.jpg (900×599 JPEG, keep as is). Short title: "The River Gorge". Full title: "The River Gorge". Maker: John Burke. Date: 1878–80. Process: Albumen print. Collection: The Cleveland Museum of Art, Mr. and Mrs. Richard W. Whitehill Art Purchase Endowment Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/2024.6.gg. Alt: "A wide braided river running out between bare hills under a pale sky". Caption: "A wide, braided river runs out between bare hills under a pale, empty sky." Used as: corridor plane 08, detail view, lightbox, Index row and fallback catalogue.
- image: Work 09 (Room II, Stone). Download https://openaccess-cdn.clevelandart.org/1987.130/1987.130_web.jpg to public/photos/09-bridge-of-palalda.jpg (900×658 JPEG, keep as is). Short title: "The Bridge of Palalda". Full title: "The Bridge of Palalda (near Amélie-les-Bains)". Maker: Gustave Le Gray. Date: 1851–52. Process: Salted paper print from waxed-paper negative. Collection: The Cleveland Museum of Art, John L. Severance Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1987.130. Alt: "An old stone arch bridge over a boulder-strewn stream below a dry hillside". Caption: "An old stone arch spans a boulder-strewn stream at the foot of a dry hillside." Used as: corridor plane 09, detail view, lightbox, Index row and fallback catalogue.
- image: Work 10 (Room II, Stone). Download https://openaccess-cdn.clevelandart.org/2004.32/2004.32_web.jpg to public/photos/10-troglodyte-farm.jpg (671×893 JPEG, keep as is). Short title: "Behind the Troglodyte Farm". Full title: "Behind the Troglodyte Farm". Maker: Henri Le Secq. Date: c. 1853. Process: Salted paper print from waxed paper negative. Collection: The Cleveland Museum of Art, Norman O. Stone and Ella A. Stone Memorial Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/2004.32. Alt: "A narrow lane between stacked stone walls and an arched doorway cut into rock". Caption: "Doorways cut into rock and stacked stone walls close in around a narrow lane." Used as: corridor plane 10, detail view, lightbox, Index row and fallback catalogue.
- image: Work 11 (Room II, Stone). Download https://openaccess-cdn.clevelandart.org/1995.237/1995.237_web.jpg to public/photos/11-dolmen.jpg (1194×893 JPEG, keep as is). Short title: "Dolmen, Cap del Puetch". Full title: "Dolmen, Cap del Puetch, Ariège, France". Maker: Arthur A. Taylor. Date: c. 1865–69. Process: G. Arosa et Cie process print from waxed paper negative. Collection: The Cleveland Museum of Art, John L. Severance Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1995.237. Alt: "A flat capstone resting on low stones in open scrubland, a lone tree above". Caption: "A flat capstone rests on low stones in open scrub, a lone tree leaning over it." Used as: corridor plane 11, detail view, lightbox, Index row and fallback catalogue.
- image: Work 12 (Room II, Stone). Download https://openaccess-cdn.clevelandart.org/1992.237/1992.237_web.jpg to public/photos/12-upper-glacier.jpg (1111×893 JPEG, keep as is). Short title: "Upper Glacier, Grindelwald". Full title: "Switzerland. Grindelwald, Upper Glacier, Source of the Lutschine". Maker: Adolphe Braun. Date: 1875–77. Process: Carbon print, toned, from wet collodion negative. Collection: The Cleveland Museum of Art, The A. W. Ellenberger, Sr., Endowment Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1992.237. Alt: "The broken face of a glacier with a dark ice cave at its foot". Caption: "The broken face of a glacier fills the frame, with a dark ice cave at its foot." Used as: corridor plane 12, detail view, lightbox, Index row and fallback catalogue.
- image: Work 13 (Room III, Leaf). Download https://openaccess-cdn.clevelandart.org/1994.93/1994.93_web.jpg to public/photos/13-birch-tree.jpg (616×893 JPEG, keep as is). Short title: "Study of a Birch Tree". Full title: "Study of a Birch Tree, Barbizon". Maker: Constant Alexandre Famin. Date: 1860s–1870s. Process: Albumen print from wet collodion negative. Collection: The Cleveland Museum of Art, John L. Severance Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1994.93. Alt: "A single twisting birch tree, pale bark against a dark wood". Caption: "A single birch twists upward, its pale bark bright against a dark wood." Used as: corridor plane 13, detail view, lightbox, Index row and fallback catalogue.
- image: Work 14 (Room III, Leaf). Download https://openaccess-cdn.clevelandart.org/1988.64/1988.64_web.jpg to public/photos/14-forest-pathway.jpg (1236×893 JPEG, keep as is). Short title: "Pathway in the Forest". Full title: "Pathway in the Forest of Fontainebleau". Maker: Gustave Le Gray. Date: 1849–52. Process: Salted paper print from waxed paper negative. Collection: The Cleveland Museum of Art, Norman O. Stone and Ella A. Stone Memorial Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1988.64. Alt: "A sandy track leading into a tall, sunlit forest". Caption: "A sandy track runs into a tall, sunlit forest." Used as: corridor plane 14, detail view, lightbox, Index row and fallback catalogue.
- image: Work 15 (Room III, Leaf). Download https://openaccess-cdn.clevelandart.org/1988.239/1988.239_web.jpg to public/photos/15-scotch-firs.jpg (1198×893 JPEG, keep as is). Short title: "Scotch Firs, Hawkhurst". Full title: "Scotch Firs, Hawkhurst". Maker: Benjamin Brecknell Turner. Date: 1853. Process: Albumen print from calotype. Collection: The Cleveland Museum of Art, John L. Severance Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1988.239. Alt: "Tall Scots pines leaning over a stream and a wooden fence by a country road". Caption: "Tall Scots pines lean over a stream and a wooden fence beside a country road." Used as: corridor plane 15, detail view, lightbox, Index row and fallback catalogue.
- image: Work 16 (Room III, Leaf). Download https://openaccess-cdn.clevelandart.org/1995.26/1995.26_web.jpg to public/photos/16-fern-cyanotype.jpg (621×893 JPEG, keep as is). Short title: "Scolopendrium Vulgare". Full title: "Scolopendrium Vulgare". Maker: Anna Atkins. Date: 1852–54. Process: Cyanotype. Collection: The Cleveland Museum of Art, Norman O. Stone and Ella A. Stone Memorial Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1995.26. Alt: "Four pale fern fronds laid flat on deep blue cyanotype paper". Caption: "Four hart's-tongue fern fronds, laid flat on sensitised paper and printed blue without a camera." Used as: corridor plane 16, detail view, lightbox, Index row and fallback catalogue.
- image: Work 17 (Room III, Leaf). Download https://openaccess-cdn.clevelandart.org/1992.226/1992.226_web.jpg to public/photos/17-woods-in-snow.jpg (653×893 JPEG, keep as is). Short title: "Woods in Snow". Full title: "Untitled (Woods in Snow)". Maker: William James Stillman, with J. W. Black. Date: 1859. Process: Salted paper print, varnished, from wet collodion negative. Collection: The Cleveland Museum of Art, John L. Severance Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1992.226. Alt: "Snow-laden branches in a dense forest, framed in a soft oval". Caption: "Fresh snow weighs down the branches of a dense forest, framed in a soft oval." Used as: corridor plane 17, detail view, lightbox, Index row and fallback catalogue.
- image: Work 18 (Room III, Leaf). Download https://openaccess-cdn.clevelandart.org/2007.26/2007.26_web.jpg to public/photos/18-mullein-in-bloom.jpg (728×900 JPEG, keep as is). Short title: "Mullein in Bloom". Full title: "Mullein in Bloom". Maker: Eugène Atget. Date: c. 1897–1899. Process: Albumen print. Collection: The Cleveland Museum of Art, John L. Severance Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/2007.26. Alt: "A tall flowering mullein plant in front of a white sheet hung on a hillside". Caption: "A tall mullein in flower, photographed against a white sheet hung behind it." Used as: corridor plane 18, detail view, lightbox, Index row and fallback catalogue.
- image: Work 19 (Room IV, Sky). Download https://openaccess-cdn.clevelandart.org/1988.191/1988.191_web.jpg to public/photos/19-sky-study-paris.jpg (1247×893 JPEG, keep as is). Short title: "Sky Study, Paris". Full title: "Sky Study, Paris". Maker: Charles Marville. Date: 1856–57. Process: Albumen print from wet collodion negative. Collection: The Cleveland Museum of Art, John L. Severance Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/1988.191. Alt: "Heavy clouds rolling over a dark band of city rooftops". Caption: "Heavy clouds roll over a dark band of rooftops and a single distant dome." Used as: corridor plane 19, detail view, lightbox, Index row and fallback catalogue.
- image: Work 20 (Room IV, Sky). Download https://openaccess-cdn.clevelandart.org/2011.172/2011.172_web.jpg to public/photos/20-lunar-photograph.jpg (674×900 JPEG, keep as is). Short title: "Photographie Lunaire". Full title: "Photographie Lunaire: Copernic-Képler-Aristarique". Maker: Maurice Loewy and Pierre Henri Puiseux. Date: 1896. Process: Photogravure (heliogravure). Collection: The Cleveland Museum of Art, Gift of Amy and Neil Viny. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/2011.172. Alt: "A close view of the moon's surface with craters, ridges and bright rays". Caption: "The moon's surface in close view: craters, ridges and bright rays across a grey plain." Used as: corridor plane 20, detail view, lightbox, Index row and fallback catalogue.
- image: Work 21 (Room IV, Sky). Download https://openaccess-cdn.clevelandart.org/2002.43/2002.43_web.jpg to public/photos/21-mariposa-trail.jpg (1151×893 JPEG, keep as is). Short title: "From Mariposa Trail". Full title: "Yosemite Valley from Mariposa Trail". Maker: Charles Leander Weed. Date: c. 1865. Process: Mammoth albumen print from wet collodion negative. Collection: The Cleveland Museum of Art, John L. Severance Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/2002.43. Alt: "A lone tall pine above a deep granite valley seen from a mountain trail". Caption: "A lone pine stands above a deep granite valley, seen from a mountain trail." Used as: corridor plane 21, detail view, lightbox, Index row and fallback catalogue.
- image: Work 22 (Room IV, Sky). Download https://openaccess-cdn.clevelandart.org/2022.36/2022.36_web.jpg to public/photos/22-view-from-chalais.jpg (900×699 JPEG, keep as is). Short title: "View from Chalais". Full title: "Vue prise du Couvent des Dominicains, Chalais (Isère), France". Maker: Félix Thiollier. Date: c. 1865. Process: Albumen print from collodion negative. Collection: The Cleveland Museum of Art, Gift of Michael Mattis and Judith Hochberg. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/2022.36. Alt: "Cliffs above a valley filling with low cloud, seen from high ground". Caption: "Cliffs rise above a valley slowly filling with cloud, seen from high ground." Used as: corridor plane 22, detail view, lightbox, Index row and fallback catalogue.
- image: Work 23 (Room IV, Sky). Download https://openaccess-cdn.clevelandart.org/2018.216/2018.216_web.jpg to public/photos/23-kutcherry-hill.jpg (900×712 JPEG, keep as is). Short title: "Kinchinjunga from Kutcherry Hill". Full title: "Darjeeling. Kinchinjunga and the Snows from Kutcherry Hill". Maker: Photoglob Co., Zurich. Date: 1890. Process: Photochrom. Collection: The Cleveland Museum of Art, Purchase from the Karl B. Goldfield Trust. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/2018.216. Alt: "Snow peaks floating above layered blue-grey ridges in soft colour". Caption: "Snow peaks float above layered blue-grey ridges, coloured by hand-prepared photochrom stones." Used as: corridor plane 23, detail view, lightbox, Index row and fallback catalogue.
- image: Work 24 (Room IV, Sky). Download https://openaccess-cdn.clevelandart.org/2024.5.24/2024.5.24_web.jpg to public/photos/24-upper-himalaya.jpg (900×711 JPEG, keep as is). Short title: "The Upper Himmalayahs". Full title: "The Upper Himmalayahs. View of the Mountain of Bunderpoonch with the Jumnootri Peak, 20,458 above the Level of the Sea". Maker: Samuel Bourne. Date: 1863–70. Process: Albumen print. Collection: The Cleveland Museum of Art, Severance and Greta Millikin Purchase Fund. License: CC0 1.0 (Open Access). Record: https://clevelandart.org/art/2024.5.24. Alt: "A pale mountain range fading into haze behind a screen of trees". Caption: "Through a screen of trees, a pale mountain range fades into the haze." Used as: corridor plane 24, detail view, lightbox, Index row and fallback catalogue.

## Don'ts
- No backend of any kind: no server code, API routes, databases, auth, analytics, forms that submit, or calls to the museum API at runtime. The only outbound links are the 'Source record ↗' links, the mailto link and the [CREDIT] link.
- Never hotlink images. All 24 photographs are downloaded into public/photos/ with the exact URLs and file names given, and listed in ASSETS.md with source, author and license.
- Do not swap in photos from Unsplash, Pexels, Pixabay, Google Images or any non-CC0 source. If you replace a photo, use another CC0 / public-domain open-access image and update content.ts and ASSETS.md.
- Do not crop, recolor, tint, desaturate or filter the photographs; show each at its natural aspect ratio. No portraits or recognizable faces as featured works, no logos.
- Do not load fonts from fonts.googleapis.com, fonts.gstatic.com, Adobe Fonts or any CDN. Self-host via Fontsource only. No CDN <script> or <link> tags and no import maps pointing to CDNs: three.js is installed from npm and bundled.
- No source maps in the production build (build.sourcemap: false) and no src/ in dist/. Relative asset paths only (Vite base './'). Every internal link points to ./index.html or an in-page element, never a bare directory.
- Do not change the stack (Vite + TypeScript + three.js + plain CSS + Fontsource) unless the user asks. No React, no Tailwind, no UI kits, no postprocessing libraries.
- No sound or audio of any kind, and no autoplaying media.
- Do not use the name 'Nocturne', the phrases 'An endless archive', 'Twenty works, four rooms', 'Lighting the rooms', 'Headphones optional' or 'Opening the doors', or any invented project titles, clients or procedurally painted artworks from other gallery demos. All titles here are the real titles of the 24 photographs.
- Do not use Instrument Serif, Inter Tight or JetBrains Mono. Use only Cormorant Garamond, Hanken Grotesk and IBM Plex Mono.
- Albumen (#c99a5b) is the only accent and only for small details. No gradients other than the vignette and the detail-view legibility gradient, no neon, no glassmorphism, no rounded cards, no emoji.
- No dependencies outside MIT, ISC, BSD, Apache-2.0, 0BSD, CC0 or Unlicense (OFL for fonts). No Aceternity UI, Tailwind Plus / Tailwind UI or purchased template code; write every effect from scratch.
- Respect prefers-reduced-motion exactly as described (no inertia, bend, FOV kick, auto-walk, animated grain or dust; crossfades instead of flights).
- Must work at 1440×900 and 390×844 with zero console errors, no page scrollbars on the gallery view, and every control reachable by keyboard with a visible 1px Albumen focus ring. Dispose nothing that is still in use; the render loop pauses while the tab is hidden.
- Keep dist/ under 20 MB. No extra bitmaps beyond the 24 photographs and the favicon.
- No mention of the template source inside the site except the optional [CREDIT] link. No lorem ipsum, no invented facts about the photographs beyond the captions given here.

---
Inspired by Nocturne — an endless 3D corridor gallery (Opus 5.5 showcase by @ohmiler) by @ohmiler — https://ohmiler.github.io/opus-5.5/infinite-gallery/
— Prompt by VibeFather · https://vibefather.ai/t/3d-photo-gallery · CC BY-NC 4.0 (non-commercial; commercial use needs a VibeFather Pro license) · Request removal: https://vibefather.ai/takedown

Free for personal projects under CC BY-NC 4.0. Commercial use comes with Pro.

DESIGN.md

The design system on its own — colors, type, spacing and motion. Apply this look to a project you already have.

---
name: "3D Photo Gallery"
colors:
  void: "#0b0a09"
  soot: "#15130f"
  frame: "#2a241d"
  bone: "#ece4d6"
  ash: "#8f877a"
  hairline: "rgba(236,228,214,0.14)"
  albumen: "#c99a5b"
  letterbox: "#000000"
typography:
  display_wordmark_room_titles_work_titles: { family: "Cormorant Garamond", weights: [400, 500] }
  text_captions_panels_buttons: { family: "Hanken Grotesk", weights: [400, 500] }
  mono_micro_labels_counters_room_labels_hints_credits: { family: "IBM Plex Mono", weights: [400, 500] }
spacing: "HUD sits on a fixed inset frame: 28px from each edge at ≥ 900px wide, 18px below. Panels use 40px padding (24px mobile). Mono labels are 11px uppercase with 0.16em letter-spacing. Detail text column starts at 6vw from the left, max-width 520px. Vertical rhythm in text blocks is 8px multiples (8, 16, 24, 40)."
radius: "0 everywhere (frames, panels, images, lightbox). Only pill buttons use 999px, and the cursor ring is a circle."
---

# 3D Photo Gallery — Design System

Apply this design system to an existing project. It describes the look and feel only; for the full site, use prompt.md.

## Colors
- **Void** `#0b0a09` — Page and scene background, fog color, loader background
- **Soot** `#15130f` — Index and About panels, mobile detail sheet, fallback page
- **Frame** `#2a241d` — Thin frame plane behind every photograph
- **Bone** `#ece4d6` — Primary text, room titles, cursor ring, dust particles
- **Ash** `#8f877a` — Secondary text, mono labels, dates, inactive controls
- **Hairline** `rgba(236,228,214,0.14)` — 1px dividers, progress track, panel borders
- **Albumen** `#c99a5b` — The only accent: progress fill, cursor dot, active index number, focus rings, the small dot in buttons. Never for large fills or body text.
- **Letterbox** `#000000` — Detail-view letterbox bars and the lightbox backdrop (at 96% opacity)

## Typography
- **Display (wordmark, room titles, work titles)** — Cormorant Garamond (OFL), weights 400/500. Self-host the font files.
- **Text (captions, panels, buttons)** — Hanken Grotesk (OFL), weights 400/500. Self-host the font files.
- **Mono micro-labels (counters, room labels, hints, credits)** — IBM Plex Mono (OFL), weights 400/500. Self-host the font files.

## Spacing & shape
- Spacing: HUD sits on a fixed inset frame: 28px from each edge at ≥ 900px wide, 18px below. Panels use 40px padding (24px mobile). Mono labels are 11px uppercase with 0.16em letter-spacing. Detail text column starts at 6vw from the left, max-width 520px. Vertical rhythm in text blocks is 8px multiples (8, 16, 24, 40).
- Radius: 0 everywhere (frames, panels, images, lightbox). Only pill buttons use 999px, and the cursor ring is a circle.

## Motion
- Easing: entrances cubic-bezier(.2,.8,.2,1); panels and letterbox cubic-bezier(.7,0,.2,1); camera flights easeInOutCubic. Damping helper damp(a, b, λ, dt) = a + (b − a)·(1 − e^(−λ·dt)) drives every continuous value.
- Inertial walk: wheel, drag and swipe write to a scroll target; the camera chases it with damp λ 5 and keeps momentum after release (velocity decays by e^(−3.5·dt)).
- Velocity bend: photograph planes curve slightly along their width while the camera moves (uBend = clamp(speed × 0.02, −0.35, 0.35)) and flatten when it stops.
- Pointer parallax: camera offsets up to 0.25 units in x and 0.15 in y and rotates up to 0.04 rad toward the pointer, damped λ 3.
- Camera flight to a photograph in 1.2 s with a FOV kick (+8° at the midpoint), other works dimming to 12% brightness in 0.6 s.
- Rolling digits: each digit is a masked vertical 0–9 strip that slides to the new value in 0.6 s; room names slide up out and in through a mask in 0.5 s.
- Loader shutter: the loader splits into a top and bottom half that slide off-screen in 0.9 s, then HUD items rise 12px and fade in with an 80ms stagger and the works reveal nearest-first.
- Film grain overlay steps through 8 offsets per second; dust drifts slowly. prefers-reduced-motion: no inertia (λ 14, no momentum), no bend, no FOV kick, no auto-walk, static grain and dust, camera jumps replaced by 0.35 s crossfades, rolling digits switch instantly.

## Imagery
Real nineteenth-century photographs only: 24 CC0 images from The Cleveland Museum of Art Open Access, kept in their original warm albumen, salted-paper, cyanotype and photochrom tones. No color grading, no filters, no crops: each plane uses the image's natural aspect ratio. Everything else (frames, labels, room titles, dust, grain, vignette, cursor, favicon) is drawn in code. No portraits and no logos.

## Tone
Quiet, exact and curatorial. Short factual captions, no exclamation marks, no marketing language. Labels read like museum wall text.

---
Inspired by Nocturne — an endless 3D corridor gallery (Opus 5.5 showcase by @ohmiler) by @ohmiler — https://ohmiler.github.io/opus-5.5/infinite-gallery/
— Prompt by VibeFather · https://vibefather.ai/t/3d-photo-gallery · CC BY-NC 4.0 (non-commercial; commercial use needs a VibeFather Pro license) · Request removal: https://vibefather.ai/takedown