Design

Ink on paper, and nothing that pretends otherwise.

This site used to be near-black, with thin hairlines and soft glows. It is a warm sheet now, with hard rules, offset shadows and a marker. Everything below is the live system, rendered from the same stylesheet the rest of the pages use - so when one of them changes, this page has already changed with it.

01

Nothing is pure white or pure black. #F4F1E9 and #14130F.

02

Borders are 2-3px of solid ink. No hairlines, no translucent whites.

03

Shadows are hard offsets in ink. Never blurred, never coloured.

I The surface

Two sheets and some grain

The page is --paper. Anything laid on it - a card, the footer, a well - is --sheet, which is the same stock a shade darker rather than a tint of the accent. Over both, two fixed overlays: a generated grain at 16% multiply, and four nearly-flat gradients standing in for folds, held under 4% contrast because a crease you can actually see reads as a dirty screen.

Both textures are feTurbulence data URIs in base.css, so the whole surface costs no network requests and no image bytes. They are also position: fixed: a texture locked to the document slides as you scroll, which is the tell that gives away a fake paper background.

--paper #F4F1E9
--sheet #E9E3D5
II Colour

Four tokens and six pigments

Every ratio below is computed from the hex beside it at build time, against --paper unless the label says otherwise. The palette itself is imported from src/data/palette.mjs, the same literal tailwind.config.mjs reads - this page holds no copy of it.

The stock

The pigments

These are pigments in the literal sense: the 3D on the home page is composited mix-blend-mode: multiply onto the sheet, so it can only ever subtract. No part of a scene can be brighter than the paper it is printed on.

Why cyan is the marker. Ink on cyan measures 10.3:1, so a stripe can sit behind body-size text and still clear AA. Ink on violet is only 3.4:1, which is why .hl-violet lightens to #C3A3F7 (8.8:1) rather than using the pigment straight.

III Typography

Two faces, two jobs

Inter at 900 is the voice: a grotesque heavy enough to hold a marker stripe behind it. JetBrains Mono is everything functional - nav, labels, meta, and the body copy you are reading. The split is what keeps the page from reading as one long shout.

Inter Variable · 900

Headings

Every h1, h2, h3 and .display. Letter-spacing -0.03em, line-height 1.02.

JetBrains Mono Variable

Nav, labels, body

15px at 1.7. The one exception is longform: posts and lessons switch to Inter, because a monospace face at three thousand words stops being a style and starts being work.

The scale

h1 / 2.75rem

h2 / 2rem

h3 / 1.35rem

body / 0.9375rem, mono, 1.7

eyebrow / 0.6875rem, 0.18em, uppercase

Plain

Printed

.inked

Printed

A turbulence mask with scattered transparent nicks, so the letterforms read as printed rather than rendered. Display sizes only: at body size the nicks land on stems rather than between them and it stops being texture and starts being a rendering bug. It fails safe - if the mask never parses, the text is simply solid.

IV The marker

A highlighter, not a background

.hl paints its stripe in a pseudo-element and runs an SVG displacement filter over it, so the edges chew but the glyphs never warp. The inset is asymmetric on purpose: a real marker overshoots the word at both ends and rides slightly high.

The default is cyan, the second is violet, and the warm one is coral.

Links use the underscore, which grows from a 0.3em stripe to a full fill on hover - a background gradient rather than a pseudo-element, because the reveal animation opens a stacking context and a z-index:-1 stripe drops out of sight for exactly as long as the element is animating in.

Try selecting this sentence: ::selection is the marker too.

V Blocks

One idiom, four weights

A bordered block sitting slightly off its own shadow. Everything that needs to look like an object is this: cards, the nav, buttons, the footer columns, the dossier on the contact page.

.block-hard 3px rule · 6px shadow · paper
.block-soft 2px rule · 4px shadow · paper
.panel 2px rule · 4px shadow · sheet
.block-card Presses on hover. Point at it.

The shadow ladder

Offsets are never blurred and never coloured. The distance scales with how much the object is meant to weigh.

VI Controls

Things that press

Nothing on this site lifts on hover. A block slides into its own shadow instead - the same distance the old translateY(-1px) travelled, but on paper a thing that rises has nowhere to rise to.

Buttons

Both are 3px rules with a 5px shadow, dropping to 1px on hover. .btn-outline-soft also fills with the marker.

Chips
React TypeScript three.js

The bar behind the label is the rating, wiped in on hover with a cyan-to-violet gradient. Printed tags, not frosted pills: a blur over a light sheet just produces a lighter, blurrier sheet.

Labelled rows
At a glance
Based in Skopje, North Macedonia
Working Remotely, with teams anywhere

An ink header bar with the label reversed out, then rows divided by 2px rules. Used wherever a handful of facts would otherwise be a paragraph nobody reads.

VII Motion

Short, and mostly optional

Press

120ms ease-out, translate(4px, 4px), shadow down to 1px. Every interactive block.

Reveal

.reveal ties an 18px rise to view() - no JavaScript, no observer. Browsers without scroll-driven animation land on the final frame, so unsupported means already revealed.

Reduced

Under prefers-reduced-motion the 3D engine is never imported at all, and a footer toggle appears to opt back in.

VIII Generated art

The images are code

No image model, and no design file. Every card and hero on the site is drawn by a script in these tokens, run by hand when the art changes. The output is committed; the tooling is not - all four need @napi-rs/canvas installed with --no-save.

One node fanning out into five, then into twenty-six smaller ones
One node fanning out into five, then into twenty-six smaller ones
Coloured sediment layers, newest at the top
Coloured sediment layers, newest at the top
A grid of component primitives, a handful promoted to accent colours
A grid of component primitives, a handful promoted to accent colours
A crescent moon inside a ring of countdown ticks
A crescent moon inside a ring of countdown ticks

Two constraints the heroes keep. They are wordless, so the alt text carries the meaning and a reworded sentence never costs a re-render. And the grain baked into them is in 2x2 blocks rather than per pixel - noise is incompressible by construction, and at one pixel it put 350KB on five images.

IX Not this

Six things that would put it back

Every one of these is something the site did before the redesign, so none of it is hypothetical.