Brand
Apple-restrained, builder's notebook. Neutral, typographic, shadow-free, real imagery only. One accent, used four ways.
The mark
Two chevrons at 45 degrees, round caps and round joins, interlocked with a gap where they pass. Stroke weight is 19.2 against a 140 unit height, which is roughly a quarter of the mark's own width. It is the only place the accent is allowed to be a shape.
Beside it the wordmark is set in the system sans at 600, tracked in slightly. The original logo used a rounded typeface; this one does not imitate it, because the mark carries the character and the name should stay quiet.
Colour
-
Canvas
-
Surface
-
Ink
-
Ink 2
-
Rule
-
Signal
-
Signal text
How the accent is used
Signal vermilion appears in exactly four places: the mark, links on hover, focus rings and the text selection, and the one drawn line under a hero. Buttons are never accent. A button is an ink fill with a canvas label, which gives it 16:1 contrast and keeps the accent meaningful.
The product demos are the one exception, and deliberately so. Shoptimal, Sayforth and the avatar are real product UI, so they keep their own palettes instead of being repainted in these colours. The two web demos sit in the 20px media tile on the surface tone, which is what lets them rest in the page. The avatar's own surfaces carry their own edges already, so they sit on the page without a tile around them, beside the copy that describes them.
- Signal is for graphic and large use. It measures 3.6:1 on canvas, which clears the bar for shapes but not for body text.
- Signal text is the variant for links and focus rings. 5.0:1 on canvas in light, 7.6:1 in dark.
- Secondary text clears 4.5:1 in both themes, so nothing on this site relies on colour alone.
Type
-
Say it plainly
-
Businesses I built
-
A storefront the owner can run
-
Designer turned operator. I manage web experience and build businesses of my own.
-
The body face is the system sans at a 17px base, set to a 62 character measure. Headings balance, body text uses pretty wrapping, and every number on the site is tabular so dates line up in a column.
Stacks
No web fonts. Nothing to download, nothing to reflow, and on the machines this site is mostly read on it renders in the face the operating system was designed around. Mono is reserved for dates, metadata and tokens, never for body copy.
Layout and radius
Two radii, no pills
Content maxes out at 1120px, prose at 680px, gutters are 24px and 16px on a phone. Spacing comes off a scale of 4, 8, 12, 16, 24, 32, 48, 72, 112 and 160, and the rhythm is the contrast between them: 112 to 160 between sections, 4 to 16 inside a group.
There are no cards and no shadows anywhere on this site. Grouping is done with proximity first and a one pixel rule second. Media tiles get the 20px radius and sit on the surface tone; small controls get 12px. Nothing is a pill.
Motion
| Token | Value | For |
|---|---|---|
--ease-out | cubic-bezier(0.22, 1, 0.36, 1) | Anything entering or arriving |
--ease-in-out | cubic-bezier(0.77, 0, 0.175, 1) | Moving between two places on screen |
--ease-exit | cubic-bezier(0.55, 0, 1, 0.45) | Leaving, and not coming back |
--spring-settle | linear(…), 25 samples | The last few pixels finding rest |
--t-press | 140ms | Button press |
--t-hover | 200ms | Hover, colour, the nav hairline |
--t-reveal | 600ms | Scroll arrival, image scale |
--t-draw | 900ms | The mark drawing, the hero line |
--stagger | 70ms | Between staggered items |
The spring, worked out
The settle token is a real spring, not a guess. It is the position curve of a
second order system with a response of 0.28 seconds and a damping ratio of
0.88, sampled at 25 points across the 245ms it takes to settle, and expressed
as a CSS linear() function. Browsers without
linear() support get a cubic-bezier that follows the same shape.
At 0.88 damping it does not overshoot. That is deliberate. Bounce is for things a finger threw; everything on this site is arriving because a page loaded, so it decelerates and stops.
The rules
- Only transform, opacity and clip-path animate. Nothing that triggers layout.
- No ease-in. It starts slow and delays the moment the reader is watching.
- Never scale from zero. Entrances start at a small offset and fade up.
- Hover effects live behind
(hover: hover) and (pointer: fine), so a phone never inherits them. - Buttons scale to 0.97 on press, over 140ms.
- Staggered items are 70ms apart, and never more than a handful.
- One reduced-motion block collapses everything to a 200ms opacity fade.
- Scroll arrivals are hidden only when JavaScript is running. With it off, the page is simply complete.
The inventory
- The signature. On the home page a large mark draws itself stroke by stroke, the second chevron 180ms behind the first, landing on the spring with a decaying swing. It then sits in the hero and scrolls away with the page. The nav logo is visible at every width, always.
- Masked title rises. Headings sit in an overflow-hidden mask and rise from 105% over 900ms, 90ms apart. Reserved for titles, so not every element shares one entrance.
- The pinned stage. On a screen at least 900px wide, "Businesses I built" sticks for three screens: the first shot settles from 0.86 to 1 with its radius easing 28px to 20px, then a clip-path wipe from the right hands over to the second while the copy crossfades. Below that width it is two stacked blocks.
- Scroll arrivals. Rise 14px and fade over 600ms, once each.
- Pointer tilt. Large media tiles follow the pointer up to 6px and 1.5 degrees, then settle back on the spring. Fine pointers only.
- Hover detail. Media images scale to 1.025; list rows dim their neighbours and nudge a chevron 4px; the nav mark's two chevrons ease 3px apart and settle.
- The footer mark. A very large outlined chevron in the rule colour draws once behind the links. Stroke only, so the accent stays reserved.
- The product demos. The two tiles in "Businesses I built" ship a screenshot as their content. Near the viewport, an iframe holding the real animated product demo is overlaid at the identical aspect ratio, so nothing shifts. Each runs only while on screen and the tab is visible, and paints a static end state under reduced motion.
- The menu-bar app, live. "What I'm working on" is three blocks in ordinary flow. The first two put copy on one side and the app's own interface on the other, sides alternating; the third puts its copy above a full width stand-in screen, because the bar it shows is 760 wide in the app. No pinning and no wipe, because the section above it already owns that arrangement. Three things move inside it. The glass, drawn as inline SVG from the app's own glass view, on the app's own numbers: rim width s × 0.075, liquid level 0.16 + 0.76 × progress, wave amplitudes s × 0.028 and s × 0.022 drifting one period every 2.6s and 4.1s, linear and endless, colour on the hue ramp (2 + 103 × progress) / 360 so an empty day reads red and a finished one green, dragged inside a space of its own with the app's slosh behind it, and scaling 1.06 lifted and 0.93 pressed. Its critical state flashes the rim at 0.45s, pulses three sonar rings from 1 to 1.4 while they fade, throbs a badge and shakes the whole glass on the app's own keyframes. It is drawn a second time, small, resting bottom right on the stand-in screen of the third block, where clicking it brings the bar up or puts it away. The listening pill, the app's own dictation indicator, at its own size, 44 high with nine bars at 3 + 19 × level, raised from below the screen edge after a 0.22s hold, narrowing from 0.92 by 1.08 and settling on the spring, and dropped straight back out in 240ms. The bar, the app's own command surface: one white panel on a hairline holding the day above the input. It arrives as a 20px sliver at the screen edge that grows into the bar on cubic-bezier(0.16, 1, 0.30, 1), a true stadium at 76 high with its corner easing to 24 as it opens. Every change of height is one morph lasting 0.26 + 0.16 × min(1, delta / 440) seconds, drawn as a clip-path over content pinned to the foot, and it leaves in 150ms on the exit curve. Messages enter over 170ms from 5px down, an approval enters heavier over 220ms from 12px down at 0.985 and states its amber border once, and finishing is the status dot turning green and beating once to 1.16. Ticking a task in the bar ticks it in the first block and fills both glasses.
- Between pages. A 200ms crossfade where the browser supports view transitions.
All of it is driven by one requestAnimationFrame loop with lerp smoothing, on passive listeners, that parks itself as soon as nothing is moving. Every scrubbed value is read from the real layout, so resizing recomputes rather than guesses.