sadi.dev/design

DESIGN GUIDELINES

Flat pastel, deliberate motion.

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.

theme lightfollows your system until you choose
01

Principles

Five rules. Where a decision is not covered, take the quieter option.

  1. 01

    Flat and graphic

    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.

  2. 02

    One accent per section

    A section gets one colour beyond ink. Two accents compete and the eye stops knowing what matters.

  3. 03

    Colour is meaning

    Coral acts, mint confirms, sky links. Never choose a token because it looks good in that spot.

  4. 04

    Motion is state

    Something moves because it changed or because you just reached it. Nothing loops, nothing floats, nothing animates twice.

  5. 05

    Both themes are finished

    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.

02

Color

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

coral

Primary action, active state, emphasis, errors.

ink 0.0:1

mint

Shipped, live, success. Code accents.

ink 0.0:1

sky

Links and info. Never a button fill.

ink 0.0:1

SECONDARY

lavender

Tags, decorative, special content.

ink 0.0:1

sun

Warnings and callouts. Dark text only.

ink 0.0:1

rose

Warm highlight, personality.

ink 0.0:1

TERTIARY

peach

Soft warmth, gentle blocks.

ink 0.0:1

sage

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

surfaceinkaccent

If a screenshot of a page looks colourful, it is over budget.

03

Themes

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

  • Reach for semantic tokens: bg-bg-off, text-dim.
  • Use text-coral-ink for coloured text.
  • Use bg-coral for fills and rules.
  • Check both themes before calling a component done.

DON'T

  • Hardcode #FFFFFF or #111111.
  • Patch with dark: instead of adding a token.
  • Put small text on a raw pastel fill.
  • Animate the theme flip past 260ms.

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.

04

Typography

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-tight

H1

Page title.

Sheak Sadi

text-4xl font-black tracking-tight

H2

Section heading.

Sheak Sadi

text-2xl font-bold tracking-tight

H3

Project name, card title.

Sheak Sadi

text-lg font-bold

Body

Paragraphs.

Sheak Sadi

text-sm leading-relaxed text-dim

Meta

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.

05

Space

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

4
8
12
16
24
32
48
64
Hairline — the quiet dividerborder-line
Rule — structural, rareborder-line-strong
Radius — pill, block, card9999px / 1rem / 1.25rem
06

Motion

Everything 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

quint-out 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)
standard hover to run

Anything reversible — hover, toggle, theme. Symmetric, so out mirrors in.

cubic-bezier(0.40, 0, 0.20, 1)

THE FIVE ENTRANCES

An element arrives
rise — the defaulttranslate3d(0,10px,0) → 0
  • first
  • second
  • third
  • fourth
stagger — a group in sequence--i × 70ms
draw — a rule creates itselfscaleX(0) → 1, 600ms
One word after another
write — words leave their line boxclip + 105% → 0, 52ms apart
stroke — a path draws itselfdashoffset → 0, 800ms
grow — a bar rises from its baselinescaleY(0) → 1, origin bottom

TRANSFORM, 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.

iftemperatureis above>30:

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.

07

Components

Two primitives carry the whole site: the soft block and the pastel pill. Everything else is spacing.

Link
Buttonspill / outline / link
RustTypeScriptTauri
Tags.tag .tag-{color}
live archived
Statusa 6px dot
gdjump to design
Keyboard hintrounded-lg bg-bg-off

PROJECT ROW — THE ONE COMPOSITE

insa-live-map

2026

Reverse-engineered the INSA HAFAS endpoint to plot live trains, trams and buses across Saxony-Anhalt. Desktop and Android from one Rust codebase.

RustTauri 2Leaflet
08

Code

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.

insa/journey.rsrust
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)?)
}
keyword function string type file marker

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.

09

Voice

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."

  • Lead with the hard part, not the stack.
  • Numbers beat adjectives: 52 GB migrated, 277 commits, six job boards.
  • Credit honestly. "Contributor" when the repo was someone else's.
  • Project names stay lowercase, exactly as they are on disk.