insa-live-map
2026Reverse-engineered the INSA HAFAS endpoint to plot live trains, trams and buses across Saxony-Anhalt. Desktop and Android from one Rust codebase.
DESIGN GUIDELINES
One font, bold pastels, soft corners, no shadows. Colour carries meaning and nothing else. Every element arrives on the same curve, once, and then holds still — the restraint of a Manim scene, not a page asking for attention.
Five rules. Where a decision is not covered, take the quieter option.
No shadows, no gradients, no blur. Separation comes from a change of surface and the space around it. If an element needs a shadow to read, the spacing is wrong.
A section gets one colour beyond ink. Two accents compete and the eye stops knowing what matters.
Coral acts, mint confirms, sky links. Never choose a token because it looks good in that spot.
Something moves because it changed or because you just reached it. Nothing loops, nothing floats, nothing animates twice.
Light is the default, the system preference wins on arrival, the choice is remembered. A component is done when it holds in both, not when it looks right in one.
Every colour is two tokens. --x fills blocks and borders; --x-ink is its text-safe twin. Raw coral on white is 2.8:1 and unreadable, so text always takes the ink. Each swatch measures itself live — flip the theme and watch the ratios re-report.
PRIMARY
Primary action, active state, emphasis, errors.
ink 0.0:1
Shipped, live, success. Code accents.
ink 0.0:1
Links and info. Never a button fill.
ink 0.0:1
SECONDARY
Tags, decorative, special content.
ink 0.0:1
Warnings and callouts. Dark text only.
ink 0.0:1
Warm highlight, personality.
ink 0.0:1
TERTIARY
Soft warmth, gentle blocks.
ink 0.0:1
Growth, subtle positive. Dark text only.
ink 0.0:1
SURFACE & INK
bg
Page. The only full-bleed surface.
mint-tint
A tinted block. One accent, one section.
bg-warm
Hero and section intros.
text — headings, primary copy
dim — body, descriptions
muted — captions, chrome
THE 60 / 30 / 10 BUDGET
If a screenshot of a page looks colourful, it is over budget.
Light is the default. The first visit follows prefers-color-scheme; the toggle writes portfolio-theme to localStorage and that override wins until 'follow system' clears it. An inline script sets data-theme before first paint, so there is no flash to hide.
DO
bg-bg-off, text-dim.text-coral-ink for coloured text.bg-coral for fills and rules.DON'T
#FFFFFF or #111111.dark: instead of adding a token. Only surfaces carry data-themed. The flip transitions those and nothing else, so a theme change never animates a hover state that happens to be mid-flight.
JetBrains Mono, weight 100–800, for everything. A monospace body is the point: it reads as a terminal and it matches the work. Ligatures are off — an arrow should look like what was typed.
Display
Hero name. Once per site.
Sheak Sadi
text-6xl font-black tracking-tightH1
Page title.
Sheak Sadi
text-4xl font-black tracking-tightH2
Section heading.
Sheak Sadi
text-2xl font-bold tracking-tightH3
Project name, card title.
Sheak Sadi
text-lg font-boldBody
Paragraphs.
Sheak Sadi
text-sm leading-relaxed text-dimMeta
Dates, captions, chrome.
Sheak Sadi
text-[11px] text-muted Prose caps at 62ch. Monospace runs wide, and past roughly 75 characters the eye loses the start of the next line.
Space divides, borders emphasise. The default separator is space or a hairline; a 2px rule is a statement and appears maybe twice on a page. Corners are soft and consistent: pills are fully round, blocks take 1rem, nothing is square.
SCALE — 4PX BASE
border-lineborder-line-strong9999px / 1rem / 1.25remEverything on this page has already used the system to arrive. One curve, one distance, one direction: 10px up on quint-out, the deck's slide entrance. Siblings cascade 70ms apart so a group reads as a sequence rather than a flash. Nothing loops, and nothing replays on a second scroll past.
CURVES — HOVER TO RUN
The house curve, inherited from the deck. Decelerates for a long time, so things settle rather than snap.
cubic-bezier(0.22, 1, 0.36, 1)Anything reversible — hover, toggle, theme. Symmetric, so out mirrors in.
cubic-bezier(0.40, 0, 0.20, 1)THE FIVE ENTRANCES
translate3d(0,10px,0) → 0--i × 70msscaleX(0) → 1, 600msclip + 105% → 0, 52ms apartdashoffset → 0, 800msscaleY(0) → 1, origin bottomTRANSFORM, NOT REPLACE
Borrowed from Manim's TransformMatchingTex: when two states share meaning, move the part that survives and only fade what genuinely changed. A reader who looks away for a second can still follow what happened.
Duration
150ms colour · 250ms hover · 260ms theme · 450ms entrance · 600ms draw. Nothing exceeds 800ms.
Properties
opacity, transform, color only. Never height, width or top — those relayout and stutter.
Reduced motion
One media query resolves every reveal to its finished state. The preference removes the motion, never the content.
Two primitives carry the whole site: the soft block and the pastel pill. Everything else is spacing.
pill / outline / link.tag .tag-{color}a 6px dotrounded-lg bg-bg-offPROJECT ROW — THE ONE COMPOSITE
Reverse-engineered the INSA HAFAS endpoint to plot live trains, trams and buses across Saxony-Anhalt. Desktop and Android from one Rust codebase.
The code block never inverts. It is Catppuccin Mocha in both themes — the one dark element on a light page, and the surface the whole palette was tuned against.
pub async fn journey_geo_pos(bbox: Bbox) -> Result<Vec<Vehicle>> { let res = client.post("https://reiseauskunft.insa.de/bin/mgate.exe") .json(&build_request(bbox)) .send().await?; Ok(parse(res)?) }
Code fades in rather than rising. Text that is read line by line should not have moved while you were finding your place in it.
Say what it does and what it cost. No adjective doing a verb's work.
WRITE THIS
"Reverse-engineered the INSA HAFAS endpoint to plot live vehicles. Desktop and Android from one Rust codebase."
NOT THIS
"A beautiful, cutting-edge transit visualisation platform leveraging modern technologies to deliver a seamless experience."