The Mezzanine

Behindthe Curtain

How this cinema was built, and how the Oracle thinks.

What This Is

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.

Site Structure

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

adamr-312.github.io/Film-Oracle
index.html
Grand Lobby — homepage, room navigation, live filmstrip banner
Live
adams-oracle.html
The Movie Oracle — 14-question AI film recommendation tool
Live
top25.html
Top 25 Lists — navigation hub for 14 genre list pages
Live
list.html
Universal list page — URL-param driven, renders any of the 14 genre lists via TMDB
Live
collection.html
Adam's Collection — personal film archive sourced from Letterboxd export
Live
indie.html
Indie & Local Theatres — resources, forums, and state of independent exhibition
Live
staff.html
The Mezzanine — this page
Live
classic.html
The Archive — Internet Archive public domain film browser with lightbox player
Live
style.css
Shared design system — CSS variables, curtains, ceiling/floor, theatre rails, nav
Live
sixdegrees.html
Four Degrees — Four Degrees of Cinema, Claude-propose + TMDB-verify traces real cast/crew chains up to 3 hops; Claude narrates each link
Live
concession.html
The Concession Stand — fill-in-the-blank famous quotes, Claude responds in character
Live
doublefeature.html
The Double Bill — Double Feature Matchmaker, Claude pairs any film with a double bill and writes the programmer's note
Live
CLAUDE.md
Repository guide for Claude Code — architecture, external services, conventions
Repo
Hosted On
GitHub Pages — free static hosting served directly from the repository's main branch at adamr-312.github.io/Film-Oracle.
Technology Stack
HTML5 & CSS3
Each page is a single self-contained HTML file with embedded CSS. No preprocessors, no build step. CSS custom properties handle the design system — colors, spacing, and typography are defined once in :root and referenced everywhere else.
Vanilla JavaScript
All interactivity — curtain animations, progress tracking, option selection, API calls, and result rendering — is plain JS with no libraries. The fetch API handles all external requests. No jQuery, no bundler, nothing to install or update.
Anthropic API
The 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.
TMDB API
Used across three features. The Top 25 lists query 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.
Cloudflare Workers
Two Workers handle API proxying. 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.
GitHub Pages
Free static hosting served directly from the repository's main branch. No CI pipeline, no deployment configuration — pushing a commit to main publishes it. The entire deploy process is a git push.
Google Fonts
Three typefaces define the visual identity: Playfair Display for display headings, EB Garamond for body text, and Cinzel for labels and structural chrome. Courier Prime is used for code on this page.
Letterboxd Export
The Collection page is built from a real Letterboxd data export — ratings, curated lists, watch history. The CSV data was parsed and hardcoded into the HTML, so the page needs no live data connection and loads instantly. To update it, re-export from Letterboxd and re-run the parse.
Wikipedia REST API
When the Oracle returns a recommendation, the director's name is sent directly to 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.
Internet Archive API
The Archive page queries 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.
How It Works

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).

1
User Completes the Form
14 questions across option grids, 1–5 scales, a runtime range input, and two free-text fields. JavaScript tracks answers in a plain object keyed by question number. The progress bar and remaining-question counter update on every interaction. The submit button enables only when all 14 keys are present.
2
Answers Are Assembled Into a Prompt
JavaScript maps each answer value back to its human-readable label — option data-val attributes become their display text, scale numbers become descriptive phrases, and the runtime range becomes a plain sentence. The 14 answers are then interpolated into a structured prompt string.
3
Prompt Is Sent via Cloudflare Worker
The prompt POSTs to oracle-proxy.adamrowe312.workers.dev — a Cloudflare Worker that holds the Anthropic API key as a secret. The Worker forwards the request to Anthropic's /v1/messages endpoint using claude-sonnet-4-20250514, then returns the response to the browser. The API key never appears in client-side code.
4
Response Is Parsed and Rendered
The response is expected in a fixed format — TITLE, DIRECTED BY, WHY, FIND IT — extracted with regex. Each field is injected into the result card, which slides up styled as a theatre marquee. If the response deviates from the format, the raw text renders as a fallback.
5
TMDB Enriches the Result Card
The film title is sent to TMDB's search/movie endpoint to find its ID. Two parallel requests then fire: movie/{id}?append_to_response=videos (fetches details and trailer in one call) and movie/{id}/recommendations. The result card is updated with a w342 poster, a backdrop image behind the card, a ▶ Trailer button linking to YouTube if an official trailer exists, and a three-card grid of similar films below.
6
A Second Claude Call Generates the Viewing Dossier
After the result card is visible, a second call fires to the oracle-proxy Worker — this time with max_tokens: 180. The prompt passes the film title, year, and director, asking Claude for a 2-3 sentence viewing guide: the film's cultural significance, one specific cinematic technique or moment worth noticing, and the emotional register of watching it. The response appears beneath "Find It" as On This Film.
// 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')}`
How the Lists Work

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);
How the Chain Works

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

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.

Files Used From Letterboxd Export
Four of the seven exported CSVs were used to build the Collection page:
ratings.csv profile.csv the-best-movies-i-have-ever-seen.csv fever-dream-films.csv