Skip to content
UTC

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.

Launch SpinSignal

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
Built 2026-08Agent Claude Code, Opus 5, Sonnet 5GitHub Coming Soonactive