RutgerWillemsIf you're not sure, call me Will

Everything this site is assembled from. Colour tokens and type specs are read out of src/styles/global.css at build time, so this page cannot drift from the stylesheet — if a utility is added there and left undocumented here, the build fails.

Color

Declared once in @theme, from which Tailwind derives bg-*, text-* and border-* utilities. The site is dark-only: there is no light theme and no dark: variant anywhere.

Surfacesbg-*
--color-bg#020202
--color-surface#333333
--color-surface-secondary#141414

Page and panel backgrounds.

Texttext-*
--color-text-primary#ffffff
--color-text-secondary#ebebeb
--color-text-third#a2a2a2
--color-text-inactive#757575

Foreground hierarchy, brightest to dimmest.

Bordersborder-*
--color-border#232323
--color-border-secondary#292929

Hairlines and separators.

Page backgroundradial-gradient · --color-bg

A warm brown glow at 8% opacity, anchored near the top-left and layered over --color-bg. It is painted once on the document body, so it spans the viewport rather than repeating per section.

CATEGORY_PALETTEcomponents/outbox/outbox.ts
#f5c518
#4d9de0
#9b59b6
#2ecc71
#e67e22
#1abc9c
#e74c3c
#f1c40f
#1c1c1cempty cell

Handed to outbox categories in first-seen order, wrapping modulo the palette length. These live outside @theme because they are data, not theme.

Typography

Inter for everything, Gochi Hand for accents. Style text with a type-* utility rather than ad-hoc size and weight classes. Specs below read family · size · weight · line-height.

Families@theme · :root

The quick brown fox jumps over the lazy dog

--font-sansfont-sans

The quick brown fox jumps over the lazy dog

--font-accentfont-accent

The quick brown fox jumps over the lazy dog

--monospace-font-familyno utility
type-heading-124px · 500 · 1.4

The "Generic MIDI controller"

Article titles. Its only call site pulls the weight back down with font-normal.

type-heading-220px · 500 · 1.5

Learn to manage large Vue.js components

Defined, but nothing uses it yet.

type-heading-318px · 500 · 1.6

Using scoped BEM with Tailwind

Defined, but nothing uses it yet.

type-body16px · inherit · 1.7

Product engineer at Deducta, previously Devsync.

type-body-small14px · inherit · 1.7

Short notes, reports, and resources.

type-body-large18px · inherit · 1.7

Hi there, I’m Will

type-label14px · 450 · 1.4

Email

Form labels, tabs and controls. Weight 450 needs a variable Inter; only static 400/500/600 are fetched, so it resolves to the nearest available weight.

type-title16px · 400 · 1.2

Outbox

The workhorse. Section headings, page titles and card names all use this rather than type-heading-*.

type-monospacedJetBrains Mono · 14px · 500 · 1.4

npm run validate

JetBrains Mono is named in :root but never loaded, so this falls back to the system monospace.

type-accentGochi Hand · 1.35em · 500 · 1.7

Hi there, I’m Will

Sized in em, so it scales with whatever it sits in. Built to be dropped inline into body copy.

Prose

One .prose class styles every piece of rendered markdown — blog posts and Obsidian notes alike. It reaches descendants through :where(), so any utility you add always wins over it.

.prose14px · 1.7

Rendered markdown

Body copy sits at 14px on a 1.7 line-height. Inline links pick up an underline offset by 3px, and inline code gets a chip in the secondary surface colour.

  • Unordered lists use discs
  • indented by 1.5em

Blockquotes take a 2px leading rule and drop to the tertiary text colour.

export function excerpt(content: string, length = 160): string {
	const stripped = content.replace(/\s+/g, ' ').trim();
	return stripped.length <= length ? stripped : stripped.slice(0, length) + '…';
}

Third-level heading

Headings shift to the secondary text colour at weight 500, with generous space above and tight space below.

Code blocks are highlighted by Shiki with the min-dark theme, which brings its own background — .prose only contributes radius, padding and overflow.

Icons

Inline SVG components sharing one signature: { class?: string; size?: number }. They paint with currentColor, so colour them from the parent — the two badges are the exception and carry a fixed blue.

components/icons7 icons · currentColor
ArrowRight
AtSign
ChevronDown
ExternalLink
GitHub
LabExperiment
VerifiedBadge
sizedefault 16 · ChevronDown 13

Height follows the intrinsic ratio, so non-square icons stay undistorted.

Components

The reusable pieces. Everything here is server-rendered Astro with no client framework; interactivity is a handful of inline scripts.

Slidermin · max · value · step

Wraps a native range input and keeps the filled track in sync through a --progress custom property, seeded server-side so there is no flash. It holds no logic — listen for input on the consumer.

ProjectCardname · description · href? · verified? · status? · icon?

Devsync

Team tooling

Deducta

Product engineering

Tray

Parts inventory

Archive

No icon, badge or link

Wrapped in ProjectsGrid, which is a bare two-column grid. The badge appears for verified, the flask for status='experiment', and the external-link affordance only when href is set.

FilterTabstabs: { label, href, filter?, active? }[]

Active state is server-rendered, then reconciled client-side against the ?filter= query. The outbox uses the same visual recipe on buttons instead of links.

Ghost controltype-label · hover:text-text-third

The only button recipe on the site: stripped to text, with hover lifting it one step up the text ramp.

Text linktext-text-third → text-text-primary
Send

Inline links inside prose underline instead; this recipe is for standalone actions.

Text fieldstype-label + type-body-small

Deliberately chrome-free: transparent background, no border, no focus ring. The label and placeholder carry the affordance.

Tooltipgroup + group-hover
Hover meIf you're not sure, call me Will

CSS only. The trigger owns group and cursor-help; the bubble fades and slides in. Used on the wordmark in the header.

FormattedDatedate: Date

Emits a <time> element with a machine-readable ISO datetime and an en-US short display date.

Patterns

Recurring layout decisions that are not components. Nothing in Tailwind's spacing, radius or breakpoint scales is overridden — only the colours and fonts are.

Page shellheader · main · footer
<main class="mx-auto max-w-5xl px-6 sm:px-8 py-6 sm:py-8">

Every page shares this measure and gutter, so the header aligns to page content. Reading columns narrow further with max-w-2xl.

Spacing0.25rem step · shown at true size
gap-3An icon beside its label
gap-4A label above its field
gap-6Card internals, tab rows
gap-8Between items in a section
gap-10Between sections
gap-12Between documented examples

Vertical page rhythm is py-6 sm:py-8 per section, the same padding the header uses.

Divided rows[[data-outbox-row]+&]:border-t

The rule lives on the row and keys off a preceding sibling, so the first row is never capped — and filtering by detaching rows keeps that true without extra bookkeeping.

Horizontal railno-scrollbar · snap-x
1
2
3
4
5
6

Used by the home carousel: scrollbars hidden, scrolling untouched, slides snapping to the start edge.

Empty statetype-body-small · text-text-inactive

No posts in this category yet.

Centred, dimmed, and generously padded so a filtered list never collapses.