Open design system · v1.0

Violet Ink

The system behind this site. One token file feeds every page, both themes and both languages, and you can read all of it.

120tokens in one file
3layers: primitive, semantic, component
2themes from one semantic layer
AAcontrast on every text pair

Why it exists

The problem

The old site grew page by page. Colors drifted between sections, and every new section meant choosing values again. A light and a dark theme doubled that work.

The decisions

Components read semantic tokens only, never raw hex. Violet is reserved for action, focus and labels, so it always means something. One type family, Geist, with a strict scale. Motion has tokens too, and all of it turns off with reduced motion.

How it scales

A new page is built from existing components. A new theme is a remap of about twenty semantic tokens. The build exports tokens.json, so the same values can go into Figma variables or another codebase.

Color

Primitives are the raw palette. Nothing in the interface uses them directly.

--ink-950#07060b
--ink-900#0d0b13
--ink-850#13101b
--ink-800#1a1624
--ink-700#262133
--ink-500#5b546d
--ink-300#8d87a0
--ink-200#b5afc6
--ink-100#e4e0ee
--ink-50#f5f3fa
--paper-0#ffffff
--paper-50#faf9fd
--paper-100#f2eff9
--paper-200#e7e2f2
--violet-200#d9cfff
--violet-300#c2b3ff
--violet-400#a68dff
--violet-500#8a6cff
--violet-600#6c4cf5
--violet-700#5535d9
--violet-800#3d24a8
--violet-900#241566
--magenta-400#ff7ab6
--magenta-600#c2185b
--green-400#48e3a0
--green-600#0e7448

Semantic tokens

What a value is for. Each theme maps the same names to different primitives. Contrast is computed from the real values when the page is built.

TokenUseDarkLight
--bgPage background#07060b#faf9fd
--surfaceCards and panels#0d0b13#ffffff
--surface-raisedRaised elements, inputs#13101b#f2eff9
--surface-sunkenWells under content#050408#f2eff9
--fgPrimary text#f5f3fa18.4:1 · AAA#120f1a18.1:1 · AAA
--fg-mutedBody copy, secondary text#b5afc69.5:1 · AAA#4f49628.1:1 · AAA
--fg-subtleCaptions, metadata#8d87a05.9:1 · AA#6a64805.3:1 · AA
--lineDividers, card bordersrgba(255, 255, 255, .08)rgba(18, 15, 26, .09)
--line-strongControl bordersrgba(255, 255, 255, .16)rgba(18, 15, 26, .18)
--accentPrimary action background#6c4cf5#5535d9
--accent-hoverPrimary action, hover#8a6cff#3d24a8
--accent-fgText on accent#ffffff5.3:1 · AA#ffffff7.2:1 · AAA
--accent-inkLinks, labels, accent text#c2b3ff10.8:1 · AAA#5535d96.9:1 · AA
--accent-softHover wash, selected rowsrgba(138, 108, 255, .14)rgba(85, 53, 217, .09)
--accent-glowGlow behind key elementsrgba(108, 76, 245, .45)rgba(108, 76, 245, .22)
--successAvailability, confirmations#48e3a012.3:1 · AAA#0e74485.5:1 · AA
--highlightRare emphasis#ff7ab6#c2185b
--focus-ringKeyboard focus outline#c2b3ff10.8:1 · AAA#5535d96.9:1 · AA
--nav-bgTranslucent navrgba(7, 6, 11, .72)rgba(250, 249, 253, .78)
--scene-a3D scene, main color#8a6cff#5535d9
--scene-b3D scene, light#c2b3ff#8a6cff
--scene-c3D scene, shadow#241566#e7e2f2

Typography

Geist for everything, Geist Mono for labels and code. Large sizes get tighter tracking; body stays at 1.6 line height.

--text-5xlclamp(46px, 8.4vw, 112px)Hero and page titles

Products people come back to

--text-4xlclamp(40px, 6vw, 72px)Section titles

Products people come back to

--text-3xlclamp(32px, 4.2vw, 48px)Feature titles

Products people come back to

--text-2xlclamp(24px, 2.4vw, 30px)Card titles

Products people come back to

--text-xl21pxSmall headings

Products people come back to

--text-lg18pxLead text

Seven years designing apps, sites, platforms and games.

--text-md16pxBody

Seven years designing apps, sites, platforms and games.

--text-sm14pxUI text

Seven years designing apps, sites, platforms and games.

--text-xs12pxLabels, mono

Seven years designing apps, sites, platforms and games.

Space and radius

A 4px base. Sections breathe with a fluid --section value; gutters scale with the viewport.

--space-14px
--space-28px
--space-312px
--space-416px
--space-524px
--space-632px
--space-748px
--space-864px
--space-996px
--space-10144px
--radius-sm8px
--radius-md12px
--radius-lg20px
--radius-xl28px
--radius-pill999px
--shadow-1Resting cards
--shadow-2Featured media

Motion

Motion is saved for three moments: the hero deck, the Orra story and the How Vk is built teardown. Everything else stays still. Scroll-linked effects use transform and opacity only, and all of it stops with prefers-reduced-motion.

--dur-fast160ms
--dur-base280ms
--dur-slow640ms
--dur-reveal1000ms
--ease-outcubic-bezier(.16, 1, .3, 1)
--ease-in-outcubic-bezier(.65, 0, .35, 1)
--ease-standardcubic-bezier(.2, 0, 0, 1)

Components

Live components from this site, rendered with the same classes and tokens. Hover and focus them.

Button

SecondaryDownload CV
HoverHover
FocusFocus
DisabledDisabled
Use

One primary per view. Secondary for the second path, like the CV next to the work.

Avoid

Two primaries side by side, or a button for navigation inside a paragraph (use a link).

Accessibility

44px minimum target (40px for small in dense rows), visible 2px focus ring, white on --accent at 5.3:1 (7.2:1 in light).

Status, tag and link

StatusOpen to remote roles
Tag
  • UX planning
  • Front-end
Use

Status for one live fact (availability). Tags to scan skills, never to carry meaning alone.

Avoid

Tags that look clickable, or more than six on a card.

Accessibility

The green dot is decoration; the text carries the meaning. Links keep an underline.

Stat

  • 13+products shipped leading a team of 4 designers
  • 6xemail output with the internal builder
Use

Real numbers with the context that makes them useful, in one sentence.

Avoid

Numbers without a source, or a stat that needs a footnote.

Accessibility

Numbers are static text, so they read the same with or without script.

Accordion

Do you design, code, or both?

Both. Design is the main role; front-end makes the handoff short.

Can you start now?

Yes.

Use

Short answers people scan for, like time zone or contract.

Avoid

Hiding content people need to decide.

Accessibility

Native details and summary: keyboard and screen readers work without script.

Theme and language

Theme
Language
Use

Dark is the default. The choice is saved on the device.

Avoid

Translating by script: each language is its own page with its own URL.

Accessibility

The toggle's label says what it will do. Language links carry hreflang and lang.

Theming

Both themes side by side, from the same component. Dark is the default; light is a remap of the semantic tokens, scoped by a data-theme attribute on any element.

Open to remote roles

Products people come back to.

Same markup, same names. Only the semantic layer changes.

See my workCV
  • Figma
  • React
Open to remote roles

Products people come back to.

Same markup, same names. Only the semantic layer changes.

See my workCV
  • Figma
  • React

Accessibility rules

  1. Every text pair passes WCAG AA in both themes. The table above checks it at build time.
  2. Focus is always visible: a 2px --focus-ring outline with 3px offset.
  3. Touch targets are at least 44px, 40px for small buttons in dense rows.
  4. prefers-reduced-motion stops the 3D scene and the scroll effects. All the content is there without them.
  5. Every image has alt text and dimensions; the build fails if one is missing.
  6. One h1 per page, landmarks for nav, main and footer, and a skip link.

Changelog · v1.0, November 2026: first public version.