This is a static site — eleven HTML files, one CSS file, one GitHub repository; no build tools and nothing to install. Each page is self-contained CSS and JavaScript that runs directly in a browser. It's hosted on GitHub Pages and costs nothing to serve.
I built it in raw HTML and CSS. There's no state to manage across routes, no component reuse that would justify the overhead, and no team maintaining it. A single file that opens in a browser is the right tool. The places where the static constraint breaks are the Movie Oracle, which makes a live call to the Anthropic API on submission, and the Top 25 lists and dual filmstrip banner, which pull live data from TMDB. All external API calls are handled through Cloudflare Workers so no API keys appear in client-side code.
A fifth room — The Archive — serves public domain films directly from the Internet Archive. It fetches film metadata and thumbnails from archive.org with no API key and no proxy, then embeds the selected film in a lightbox player. This is a deliberate contrast to the proxied calls elsewhere: the Internet Archive is an open, credential-free API, and calling it directly from the browser is correct — no key to protect, no proxy needed.
The Grand Lobby header carries two animated filmstrips — one along the top edge showing films trending this week, one along the bottom showing upcoming releases. Hovering any poster pauses the strip and surfaces a popup card showing the film's title, year, rating, and synopsis. The Oracle result card uses TMDB to load a hi-res poster, backdrop, trailer link, and similar films via a single batched append_to_response call. It also pulls a director biography and portrait from the Wikipedia REST API — an open, key-free endpoint called directly from the browser, with no proxy required.
The aesthetic — velvet, gold, dimmed marquee lights, opening curtains — was a deliberate decision to make the site feel like a place rather than a page. Every design choice was made to support that.
Every page shares the same design system — CSS variables, typefaces, floor and ceiling strips, curtain animation — but each has its own layout and purpose. A shared style sheet has been created to centralize styling when possible
:root and referenced everywhere else.oracle-proxy.adamrowe312.workers.dev Worker holds the Anthropic API key and proxies Claude on behalf of four features: the Movie Oracle (film recommendation), the Concession Stand (in-character quote responses), Six Degrees of Cinema (chain narration), and Double Feature (double bill pairing and programmer's note). Each feature sends different prompts and max_tokens limits; the Worker passes them all through to Claude with CORS headers applied.discover/movie. The lobby filmstrips hit trending/movie/week and movie/upcoming. The Oracle enriches its result with search/movie, then fetches details, videos, and recommendations via append_to_response in a single call. All requests are proxied through the Cloudflare Worker.tmdb-proxy.adamrowe312.workers.dev sits in front of the TMDB API for the Top 25 lists and filmstrip. oracle-proxy.adamrowe312.workers.dev sits in front of the Anthropic API for the Oracle. Both validate the request origin, append the relevant API key from a Worker secret, and return responses with correct CORS headers. Both run on Cloudflare's free tier.en.wikipedia.org/api/rest_v1/page/summary/{name} — no proxy, no API key. The response includes a biography extract and portrait thumbnail, displayed as a panel below the director credit. This is a deliberate contrast: Wikipedia's REST API is fully open with CORS support, so routing it through a proxy would add latency for no security benefit.archive.org/advancedsearch.php directly from the browser — no key, no proxy. Eight curated programme cards (Silent Era, Westerns, Noir, Horror, Sci-Fi, Comedy, Serials, All) each map to a distinct Lucene query using AND syntax across collection, subject, and year fields. Film thumbnails come from archive.org/services/img/{id} and the selected film plays via an archive.org/embed/{id} iframe. The Internet Archive is a credential-free public API; calling it client-side is correct.The Oracle is a 14-question form that collects enough signal about a viewer's current mood and taste to make a single confident film recommendation. The submit button stays locked until all 14 are answered — the recommendation quality can be dependant on input quality.
The questions are split into four groups: taste anchors (a personal masterpiece and a film the user thinks is overrated — together these triangulate aesthetic position more efficiently than any genre checkbox), emotional state (weight tolerance, desired runtime, hope at the end), preferences (genre, themes, subtitles, pace, protagonist type, obscurity), and open-ended intent (what to avoid, what they want the film to do to them).
// System persona sent with every Oracle request
const prompt = `You are Adam's Movie Oracle — a world-class,
supremely confident film curator. Recommend exactly
ONE film. Be decisive and never hedge. Do not suggest
the most obvious choice.
Format your response EXACTLY as:
TITLE: [Film Title] ([Year])
DIRECTED BY: [Director Name]
WHY: [3-4 sentences explanation]
FIND IT: [One sentence on streaming/rental]
Their answers:
1. Personal masterpiece: ${fmt('1')}
2. Overrated film: ${fmt('2')}
3. Emotional weight (1=light, 5=heavy): ${fmt('3')}
...
14. What they want the film to do: ${fmt('14')}`
There are 14 genre lists, all served by a single page — list.html. The page reads a ?list= URL parameter on load, looks up the matching config object, and fires the appropriate TMDB query through the Cloudflare Worker. No separate HTML file per genre, no server-side routing.
Each list fetches four pages of TMDB results simultaneously — roughly 80 candidates — then filters out any film IDs already seen in the current session using sessionStorage. This prevents the same film appearing on multiple lists during a browsing session. The top 25 unique results are rendered. If there aren't 25 unique results after filtering, it falls back to the unfiltered set rather than showing a short list.
The Grand Lobby carries two filmstrips built the same way. The top strip fetches trending/movie/week and scrolls left; the bottom strip fetches movie/upcoming and scrolls right in the opposite direction. Both strips pause when the cursor enters them. Hovering an individual poster triggers a JS-positioned popup card showing the film's poster image, title, year, star rating, and a synopsis excerpt — the popup opens downward for the top strip and upward for the bottom. Poster images on all strips come directly from TMDB's image CDN, which requires no authentication.
// sessionStorage tracks seen movie IDs across list pages
function getSeenIds() {
return new Set(JSON.parse(
sessionStorage.getItem('seen_movie_ids') || '[]'
));
}
// Filter candidates, fall back if not enough unique results
const unique = allMovies.filter(m => !seen.has(m.id));
const movies = (unique.length >= 25 ? unique : allMovies).slice(0, 25);
Four Degrees (sixdegrees.html) takes two film titles and traces a factual cast or crew connection between them using the TMDB database — no invented chains. Both titles are resolved via search/movie and their full credits fetched. A direct intersection check runs first: if the two films share a person ID, the path is built immediately without any Claude call. Otherwise, Claude proposes a path — 1-hop (one shared person), 2-hop (one intermediate film), or 3-hop (two intermediate films) — in a structured PATH: / NARRATION: format. The client verifies the suggestion against real TMDB credits before accepting it. For 3-hop paths, both intermediate films are resolved and their credits fetched in parallel via Promise.all. If verification fails, a second attempt fires with the failure reason injected into the prompt.
Once a verified path is confirmed, Claude's narration is rendered as prose for each link in the chain. Each link displays a role badge — gold for Director, muted cream for Actor — above the narration sentence. The chain animates in step by step with staggered CSS delays, endpoint films distinguished by a gold border.
The Collection page doesn't make any API calls. It's built entirely from a Letterboxd data export — a zip of CSV files available under Settings → Data → Export Your Data. The relevant CSVs were parsed in Python to extract rated films, curated list positions, and profile data, then hardcoded directly into the HTML.
This means the page loads instantly and works with no network dependency beyond the initial page load. The tradeoff is that it needs to be manually rebuilt when the collection changes — re-export, re-parse, push the updated file.