# Design system | Gjorge Karakabakov

> The paper design system behind this site: the surface, a pigment palette with measured contrast, two typefaces, the marker, the blocks and the generated art.

Source: https://karakabakov.com/design/

---

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

- paper `#F4F1E9`

  The page. Warm newsprint, never #fff - pure white has no grain to catch.

  --paper · ink reads 16.5:1
- sheet `#E9E3D5`

  A second sheet laid on the first: cards, wells, the footer, the shaded fold.

  --sheet · ink reads 14.5:1
- ink `#14130F`

  Text and every hard rule. Never #000 - pure black reads as a hole in the page.

  --ink · paper reads 16.5:1
- mute `#57534A`

  Body copy and meta. The one token tuned to a threshold rather than a look.

  --mute · on paper 6.8:1
- void `#06070A`

  Kept for the 3D, which still clears to it, and for deep shadow. Not a page colour.

  --void · canvas only

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

- Aa

  cyan `#00D2FF`

  The marker. Highlights, nav hover, the current page, selection.

  ink on it 10.3:1 · as text 1.6:1
- Aa

  violet `#883AEA`

  The second marker, lightened to #C3A3F7 behind text. Skill bars.

  ink on it 3.4:1 · as text 4.8:1
- Aa

  indigo `#3A47D5`

  Deep accent. The only pigment that clears AA on paper as text.

  ink on it 2.7:1 · as text 6.1:1
- Aa

  magenta `#E834C8`

  Post art and the particle ramp.

  ink on it 5.1:1 · as text 3.2:1
- Aa

  coral `#FF6B5A`

  The third marker (.hl-coral), and the warm end of the ramp.

  ink on it 6.6:1 · as text 2.5:1
- Aa

  mint `#7CF5C4`

  Post art. The lightest pigment, so it only ever sits under ink.

  ink on it 13.9:1 · as text 1.2:1

**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](https://karakabakov.com/design/#), 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.](https://karakabakov.com/design/#)

### The shadow ladder

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

- 3px `.skill-chip` Smallest object on the site.
- 4px `.block-soft, .panel` Wells and inner cards.
- 5px `.btn-solid, .btn-outline-soft` Anything you press.
- 6px `.block-hard, .block-card` The default object.
- 7px `.header-block` Sits above the page.
- 8px `.closer` Reversed out of ink, so it needs more.

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

[Get in touch](https://karakabakov.com/design/#) [Download CV](https://karakabakov.com/design/#)

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

- `scripts/og-card.mjs`

  The share card every page falls back to.

  → og-default.png
- `scripts/course-card.mjs`

  One cover per course. Typographic, with the lesson rail along the bottom.

  → react-for-beginners.webp
- `scripts/lesson-cards.mjs`

  Twelve covers, titles read straight out of the lesson frontmatter.

  → lessons/react-for-beginners-NN.webp
- `scripts/post-heroes.mjs`

  Wordless. The alt text carries the meaning, so a reworded sentence never costs a re-render.

  → five post heroes

![One node fanning out into five, then into twenty-six smaller ones](https://karakabakov.com/images/claude-code-cost.webp)

*One node fanning out into five, then into twenty-six smaller ones*

![Coloured sediment layers, newest at the top](https://karakabakov.com/images/eleven-years-javascript.webp)

*Coloured sediment layers, newest at the top*

![A grid of component primitives, a handful promoted to accent colours](https://karakabakov.com/images/ui-library.webp)

*A grid of component primitives, a handful promoted to accent colours*

![A crescent moon inside a ring of countdown ticks](https://karakabakov.com/images/sleep-timer-macos.webp)

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

- Pure white or pure black #F4F1E9 and #14130F White has no grain to catch and black reads as a hole punched in the sheet.
- Hairlines and translucent borders 2px and 3px of solid ink A 1px rgba white line on paper is a smudge, not an edge.
- Blurred shadows Hard offsets in ink A blur says floating pane. An offset says block printed slightly off register.
- backdrop-filter and frosted glass A solid fill and a rule Blur exists to push a busy background behind glass. There is no glass on paper.
- Rounded corners by default Square by default daisyUI is still loaded and ships its own .card with a 1rem radius - which is why the post cards are .block-card.
- Colour-transition hovers The press-down A border going from 10% white to 40% cyan is a few pixels of tint nobody has ever noticed.
