§1The Concept
The Midnight Bureau is a fictional two-person detective agency in a city where it never stops raining. The site draws from film noir — the 1940s/50s school of hard shadows, venetian-blind light, wet asphalt, and one neon sign doing all the emotional labor. The palette is committed: near-black #0c0d10, a range of smoke grays, aged manila paper, and exactly one accent — the red #e5484d of the agency's sign. Typography carries the fiction: Special Elite (a typewriter face) for every document, label, and rate card; Oswald (a condensed sans) for the display work, like a movie title card.
The organizing idea: information in this city must be earned. So the case-files section literally keeps its documents in the dark until you bring a light.
§2The Techniques
Exhibit A — the flashlight mask
The case files sit under a .shade element: an absolutely-positioned overlay whose background is a radial gradient that is transparent at the center and 98% opaque near-black at the edge. The gradient's center and radius are CSS custom properties, updated every frame from JavaScript. The pointer position is eased toward with a lerp inside requestAnimationFrame, so the beam has the lag and weight of a real hand-held torch. A second .beam layer in mix-blend-mode: screen adds a warm tungsten tint.
.shade {
background: radial-gradient(
circle calc(var(--fr, 0px) * 2.3)
at var(--fx, -600px) var(--fy, -600px),
rgba(6,7,10, 0) 0%,
rgba(6,7,10, .42) 46%,
rgba(6,7,10, .982) 76%);
}
bx += (tx - bx) * 0.16; // ease toward pointer beamR += (targetR - beamR) * 0.12; // beam swells on entry casesEl.style.setProperty('--fx', (bx - rect.left) + 'px'); casesEl.style.setProperty('--fy', (by - rect.top) + 'px'); casesEl.style.setProperty('--fr', beamR + 'px');
Exhibit B — per-word de-redaction
Certain phrases are wrapped in .redact spans — black bars with color: transparent. Each animation frame, the script measures every bar with getBoundingClientRect(), finds the nearest point of that rectangle to the beam center, and if the distance falls inside the beam's core it toggles a .lit class that turns the bar translucent red and the text legible. Leave, and it seals itself again. It reads like clearance-level magic; it's a distance check. Try it: hover this bar.
var wr = word.getBoundingClientRect();
var nx = Math.max(wr.left, Math.min(bx, wr.right));
var ny = Math.max(wr.top, Math.min(by, wr.bottom));
var dx = bx - nx, dy = by - ny;
word.classList.toggle('lit',
dx*dx + dy*dy < core * core);
Accessibility is not optional in this office: an evidence lamp button lights the whole section (and prefers-reduced-motion defaults to lit), the full text is always present in the DOM for screen readers, and touch users press-and-hold to carry the light.
Exhibit C — the rain system
A fixed, DPR-aware <canvas> behind the page draws three layers of rain — far, middle, near — each with its own speed, streak length, opacity, and wind drift, so the storm has parallax depth. Streaks are simple line segments angled along their velocity vector. Every 9–25 seconds, lightning: a two-frame white fill on the canvas plus a short-lived body.flash class that hardens the venetian-blind shadows and over-drives the neon glow. The loop pauses on visibilitychange; reduced motion gets a static misty backdrop instead.
var LAYERS = [
{ density:.085, speed:1550, len:24, alpha:.32, drift:-190 },
{ density:.06, speed:1050, len:15, alpha:.19, drift:-130 },
{ density:.05, speed:680, len:9, alpha:.10, drift:-85 }
];
// per streak: lineTo(x + drift/speed * len, y - len)
Exhibit D — the sign, the blinds, the grain
The neon sign is pure CSS: layered red text-shadow glow, a per-letter neonOn keyframe animation with hand-staggered delays (triggered by IntersectionObserver), and one letter — the R in BUREAU — assigned a second, infinite dying animation so it never quite holds its charge. The wet-street strip below is the same text flipped with scaleY(-1), blurred, masked, and given a slow shimmer. Venetian blinds are one repeating-linear-gradient of near-horizontal slat shadows laid across sections at 174°; film grain is an SVG feTurbulence data URI stepped through six positions.
.blinds::after {
background: repeating-linear-gradient(174deg,
rgba(0,0,0,0) 0 54px,
rgba(0,0,0,var(--blind-a)) 54px 86px);
}
body.flash { --blind-a: 0.5; } /* lightning hardens them */
§3How It Was Made
Built by Claude (Fable 5) writing vanilla HTML, CSS, and JavaScript by hand — no frameworks, no build step, no libraries, one canvas and a flashlight. It is one of twenty-five sites in a showcase of wildly different design schools, each written the same way. The rest of the precinct is at fable-25-dhb.pages.dev.
§4Steal This
- Drive gradients with custom properties, not repaint-heavy JS. Updating
--fx/--fy/--fronce per frame and letting a CSS radial-gradient do the rendering is far cheaper than redrawing a mask in canvas — and it degrades to nothing when the vars are unset. - Lerp every pointer-follower.
pos += (target - pos) * 0.16is one line and it's the difference between "cursor gimmick" and "object with mass." - Hide with an overlay, not with the content. Because the case-file text is ordinary DOM under a shade layer, screen readers, find-in-page, and the lamp toggle all work for free. Never encode "hidden" into the content itself.
- Nearest-point-on-rect beats center distance. For proximity effects on inline elements (which can be wide), clamp the pointer to the rect before measuring — long redaction bars light up at their ends, not just their middles.
- Layer your weather. Any particle system reads as 3D the moment you split it into three depths with different speed, size, and opacity. The eye does the rest.