
SpinSignal
SpinSignal is a media monitoring platform: four independent OSINT verticals, each one a single niche topic tracked in depth and updated daily. Its front door is one shared instrument, a software radio whose stations, signal strength and layout are all derived from a single manifest file, never authored by hand.
Live at spinsignal.liminallayers.com
Problem & approach
Four unrelated OSINT projects, on outbreak reporting, a war archive, subsea cable security and offensive AI, each needed to stand on its own subdomain with its own identity, but there was nowhere that tied them together as one body of work. Whatever did that also had to hold to the same restraint as the rest of the practice: no backend, no analytics, no third-party requests of any kind, and nothing that exists just to look busy.
A single manifest file is the source of truth: a zero-dependency Node build reads it and derives everything else, station position on a frequency band, colour, signal strength, the accessible link list, the generated interaction CSS, so the dial and the real anchors underneath it can never disagree. The site ships as static HTML with both scripts and styles inlined and its fonts self-hosted, and the build fails outright on a malformed manifest rather than shipping something broken.
Challenges
Problem
Scroll-driven CSS animation only exists in Chromium and recent Safari, and the band, the meter and the static all had to read identically without it.
Solution
Every station's animation window is baked into literal keyframes at build time and handed to a JavaScript fallback as plain data from the same constants, so both paths read the same number. The test for it does not just skip unsupported browsers: it rewrites the feature-support check to force the JS path and drives the fallback for real.
Problem
The signal meter's needle and the coloured trace on the band came from separate code paths, and nothing enforced that they would ever agree about where a station's signal actually was.
Solution
Both now sample the same underlying data through a simulated receiver passband, so a station's colour and its needle deflection are the same number read twice, not two numbers that happen to usually match.
Problem
A block-scoped function named the same as another function already in the same scope silently won the collision: JavaScript's sloppy-mode function hoisting rebound a fallback's requestAnimationFrame call onto the wrong function, so the fallback path did nothing at all. Nothing threw.
Solution
Caught by comparing behaviour, not by a crash. Renamed the fallback's function to remove the collision, and added a test that actually forces a browser with no scroll-timeline support down that path, rather than trusting that Chromium coverage alone was enough.
Stack
- Node (zero-dependency build)
- Vanilla JS (CSS scroll-driven animation with a full JS fallback)
- Python 3, Playwright and Pillow (verification suite, favicon rasterisation)
- Self-hosted variable web fonts
- Caddy (deployment)
Demonstrates
- Manifest-driven build: one JSON file derives every visual and interactive surface
- Zero-dependency Node build, no npm install and no network at build time
- CSS scroll-driven animation with a full JavaScript fallback
- Behaviour-measured test suite (Python and Playwright), not visual inspection
- Strict third-party isolation: no analytics, no external requests, self-hosted fonts