flowchart LR A[Pull cord] -->|writes lights| S[(localStorage)] B[Catch a moth] -->|writes moths| S C[Finish a run] -->|writes best score| S S --> D[Arcade floor: cabinet cards and the 4th cabinet] S --> E[Every page: lighting state] S --> F[Game pages: best score in the HUD]
NOCTURNE
Static site · No framework · Cloudflare Pages
A browser arcade that behaves like a place
Six games sit on the floor and two stay sealed. A back room sits behind a bad address. A pull cord in the corner turns the lights off, a radio in the other corner plays something low, and some things only exist in the dark. What you do in one room shows up in another, because the arcade keeps notes.
The cabinets
01 · Reflex
PULSE
Drift through purple shards and collect green motes. Every eight motes buys a shockwave. Movement eases toward the cursor rather than snapping to it, so a sudden reversal costs you something.
02 · Puzzle
LATTICE
Rotate conduits until current reaches every lamp. Boards are generated as a random spanning tree and then scrambled, which makes them always solvable and always fully connected.
03 · Memory
ECHO
Repeat a growing sequence of pads. The nine tones are a pentatonic scale, so a wrong answer still sounds like music instead of a buzzer.
05 · Stealth
VEIL
Sweeping searchlights read motion, not position. Fans mount on the walls and bars slide the whole floor, so one side of the room is never safe. Sprint through a beam and exposure spikes; hold still and it still climbs, slower.
06 · Precision
SPLICE
Two columns of colored terminals. Match every pair before the panel’s clock runs out — a clean splice buys a sliver of time back, a wrong guess costs much more than that.
07 · Reflex
WYRM
Classic snake, re-skinned as a glowing trail on a dark grid. Every mote eaten grows the trail and tightens the tick rate.
04 · Sealed
MOTH
Climb toward a lamp while your glow burns down. The glow is also your draw radius, so running low on light means you cannot see the next beam coming.
08 · Sealed
STATIC
Hold a drifting station until it locks. Each lock makes the next band thinner and faster. Leave the band too long and the signal dies. Unlocks once VEIL, SPLICE, and WYRM each have a real score — no relation to the moths.
Unlisted · Puzzle
CIPHER
A crossword whose answers are already in the building. Lives in the back room, which the 404 door opens onto.
Unlisted · Search
SEEK
A letter field of words the arcade already named. Drag a straight line in any of eight directions.
Unlisted · Memory
TWIN
Sixteen tiles, eight pairs. A miss turns back over. Fewer turns score higher.
MOTH is not reachable on a first visit. It opens once all eight hidden moths have been found. STATIC is a second, unrelated secret: it opens once VEIL, SPLICE, and WYRM each have a real score. CIPHER, SEEK, and TWIN are a third secret: they live at /back.html, and the 404 corridor door is how you get there.
The radio
A small FM tuner fixed to the bottom-left corner of every page. It plays Icecast streams from Nightride FM through HTML5 audio. Clicks to other rooms on the same origin swap the page in place so the player is not torn down. A refresh, a new tab, or the back button still starts it off, because browsers will not autoplay with sound before a gesture. Station and volume persist in localStorage under nocturne.radio.station and nocturne.radio.volume.
What makes it feel like one building
Most of the work is not in the games. It is in the thin layer above them that gives four separate pages a shared memory.
Three values persist in localStorage, and every page reads all three:
| Key | Holds | Visible effect |
|---|---|---|
nocturne.lights |
Lit or dark | The pull cord changes the whole site, not one page |
nocturne.moths |
Which moths are caught | Drives the fourth cabinet |
nocturne.best.<game> |
High scores | The arcade floor reads them back onto the cards |
nocturne.js owns all three. The games never touch storage directly — they call Nocturne.submitScore(), Nocturne.revealMoth(), and Nocturne.toast(), and get persistence, notices, and the unlock chain for free.
Lighting is CSS, not JavaScript
data-lights="out" on <html> swaps one block of custom properties. Every rule that differs between lit and dark reads from those properties rather than hard-coding a colour, so the entire site changes with a single attribute.
That design is what lets the cord do more than theming. Several of the eight moths are rendered only in the dark, which makes the cord a search tool.
An inline script in each page’s <head> restores the lighting state before first paint. Waiting for nocturne.js to load would show a lit room for a frame or two to someone who deliberately turned the lights off — on every single navigation. It is three lines, and skipping them undoes the effect.
The moths
Eight, gated several different ways. The gating is the tutorial: finding them teaches you what the site can do.
| Moth | Where | How it is gated |
|---|---|---|
sign |
Arcade floor, resting on the neon sign | Visible, deliberately low contrast |
cellar |
Arcade floor, in the footer | Lights out only |
pulse |
PULSE, on the screen bezel | Lights out only |
lattice |
LATTICE, corner of the board | Clear one board |
echo |
ECHO, below the pads | Reach round 5 |
about |
About page, in the footer | Lights out only |
back |
Back room, by the sign | Lights out only |
wyrm |
WYRM, on the screen bezel | Grow to length 10 |
Two CSS classes keep those gates apart. .moth--hidden is revealed by darkness; .moth--locked stays invisible whatever the lights are doing, until a game calls revealMoth(). Using one class for both would have let a player collect the earned moths just by pulling the cord.
Catching the eighth rewrites the fourth cabinet in place — no reload — and opens /games/moth.html.
Arriving early
That URL is guessable, and that is fine. Someone who goes looking for it before finding the moths gets a SEALED panel that says how many they have and roughly where to look, rather than a redirect or a wall. Rewarding curiosity with information is the point.
Other things worth clicking
- The 404. Any bad URL lands you in a dark back corridor. Your cursor is a torch, there is exactly one door, and finding it opens the back room.
- The Konami code. Works anywhere. Does nothing useful.
- The greeting. The line under the sign reads your clock, so it is not the same text at 3am as it is at noon.
How it is built
No framework, no bundler, no build step. The whole site is HTML, CSS, and a handful of small JavaScript files on Cloudflare Pages.
site/
index.html Arcade floor
about.html What it keeps, plus hints
404.html The back corridor; the door opens `/back.html`
back.html Hidden room for CIPHER, SEEK, and TWIN
games/ One page per cabinet
assets/css/site.css Both lighting states
assets/js/
nocturne.js Lighting, moths, scores, toasts, Konami
radio.js The corner radio, and in-site navigation that keeps it playing
pulse.js lattice.js echo.js mothgame.js
veil.js splice.js wyrm.js static.js
cipher.js seek.js twin.js
Each game owns a fixed logical coordinate space — 800×600 for PULSE and MOTH, 640×640 for LATTICE — and the canvas backing store is scaled by devicePixelRatio. Nothing in any simulation thinks in screen pixels, so the games behave identically at every window size.
Deployment
Cloudflare Pages project nocturne. A push to main that touches site/ uploads that folder with wrangler pages deploy. The custom domain is nocturne.ethandbard.com.
Accessibility
Every interactive thing is a real <button> or <a>, so the moths and the ECHO pads are keyboard-reachable and announced by screen readers rather than being decorative <div>s with click handlers.
prefers-reduced-motion disables the sign flicker and the pad transitions. The games themselves need a pointer or the arrow keys; none is playable by keyboard alone on a touch device.
Nothing breaks if localStorage is unavailable. Every read and write is wrapped, and the failure mode is an arcade that forgets you between pages.