Eight decisions, each one answering something v3 got wrong rather than a style I liked.
Constraint
Hand-written, no build, no dependency
The rule that fixed the rest: if it does not fit in a file I typed myself, it does not go in. No framework to hide a bad layout behind, and no way to accidentally ship 114KB again — every byte is one I typed.
files: 5
dependencies: 0
build step: none
Type
One family, four sizes, one ratio
IBM Plex Sans and Plex Mono replace the system stack. Display sizes are clamp(), tracking tightens as size grows (−.03em to −.05em), and mono is reserved for labels, numbers and metadata — so a glance at the typeface already tells you whether you are reading prose or data.
clamp() sizes: 12
families: 2
label style: mono only
Colour
Near-monochrome, accent under 5%
v3 painted #0a7cff on every button, so emphasis meant nothing. Now the canvas is ink on paper and the accent (#ff4a17) is spent only on state: the live dot, the active nav icon, the focus ring, the hairline under the cursor. Emphasis is scarce, so it works.
hues: 1 + accent
accent coverage: <5%
dark mode: 6 token flips
Layout
Describe intent, let the browser solve
Absolute positioning out; repeat(auto-fit,minmax(230px,1fr)) in. The grid is told a minimum column, not a column count, so the same markup is three columns on a monitor and one on a phone with no breakpoint involved. Breakpoints are only where the intent genuinely changes.
@media queries: 6
fixed widths: 0
grid: auto-fit
Structure
A panel, not a header
The site is one page, so navigation should be ambient rather than something that pushes content down. Desktop gets a persistent 300px utility panel — identity, nav, status, contact in one place; under 880px the exact same element becomes a bottom tab bar with the rest in a drawer. One component, two shapes, no duplicate markup.
nav components: 1
breakpoint: 880px
duplicate nav markup: 0
Motion
Per element kind, not per page
v3 faded everything the same way, which is motion carrying no information. Here each kind moves like itself: headline words rise and rotate in, images wipe open with clip-path, tags pop, metrics count up out of blur. Durations sit at 120–520ms, and prefers-reduced-motion collapses all of it to 1ms with iteration-count 1 — a 1ms infinite loop is a strobe, not a stopped animation.
durations: 120–520ms
entrance kinds: 5
reduced-motion: honoured
Semantics
Reachable without a mouse
Real tab pattern on the case toggle, a real dialog for project detail — background gets inert so Tab cannot walk out of it, Escape closes, focus returns to the card that opened it. Focus is visible everywhere, in accent, on purpose.
aria / role attrs: 28
focus-visible: yes
focus return: yes
Detail
Hairlines that survive a reflow
Cells are drawn with a 1px box-shadow ring rather than the usual gap-filled grid trick, because auto-fit rows are uneven and the gap trick leaves broken lines when a row is short. One 5px base radius on every static corner, applied through a single selector list. The grid covers are the exception: they animate 14→26px on hover, chips 4→16px, so the corners stay concentric through the transition.
radius values: 6
grid gap hacks: 0
border method: ring