Most portfolio sites are decorated. This one is constructed. The whole site runs on a single 40-pixel module: every column, gap and block of type is a whole number of those units.
The idea is borrowed from Müller-Brockmann and Gerstner, who worked out that a grid isn’t a limit on design but the thing that makes it repeatable. What’s different here is that the grid is machine-readable — so an AI agent can build inside it without me redrawing every screen.
I didn’t want gloss. Rendered graphics and stock imagery signal effort without carrying information — decoration standing in for thinking. What I wanted was a whiteboard crossed with an architect’s working drawing: Swiss structure underneath, the marks of someone solving a problem on top.
That’s why emphasis is a highlighter stroke rather than bold type, and why a link draws a scribbled overline on hover. Working gestures, not finished ones. Twenty years of watching software get built, and now built with AI, has only sharpened the point: structure, principles and context matter more than ever.
Fig. 01 — The mark, drawn on the module. R67.5 / R37.5, centres 104 apart.
01
Where the marks come from
Before this was a system it was a room. Post-its climbing a wall, marker on a whiteboard nobody was allowed to erase, journey maps taped up by whoever got in first. Twenty years of product work starts like that on every project — loud, physical, unfinished on purpose. The site keeps those marks: the volt highlighter behind a phrase, the red scribble a link draws on hover. Working gestures, not finished ones.
The other half is the opposite instinct. An architect’s drawing is precise because the building has to stand: every line lands on the grid, nothing strays. That’s the 40px module — the drawing underneath the mess. The cells that appear at random in the background aren’t the system breaking; they’re the system showing itself at unplanned moments. Still a grid. Still structure. Mess can turn up anywhere, as long as it lands on the lines.
That’s the argument of the whole site, and of design in the AI era. Speed makes mess, and mess is fine — productive, even — when the foundation under it doesn’t move. Structure first. Then push.
03
Colour
Four working tones and two accents. Nothing is a one-off — every hairline and rule is the ink at reduced opacity.
Ink
#162A1A
All type and rules. A deep green-black, not pure black.
Paper
#F1F7F2
Page background. Warm, so it reads as paper rather than screen.
Mist
#DDE9DF
Cards and panels that sit back from the page. Sage, same hue as the paper.
Volt
#99E87C
The highlighter. Used sparingly, or it stops meaning anything. The original lime #BFFF00 lives on as the volt preset.
Signal Red
#E62D2D
One job only: the scribbled overline drawn under a link on hover.
02
The grid
One 40px module runs the whole site. Every column, gap, rule and block of type is a whole number of it — which is what makes the layout repeatable by someone who has never seen it before, human or not.
1 COLUMN · 33 MODULES
ONE MODULE = 40 →
2 COLUMNS · 15 + 15
3 COLUMNS · 9 + 9 + 9
4 COLUMNS · 6 × 4
6 COLUMNS · 3 × 6
↑ GUTTER 120 = 3 MODULES
MODULE
40px. Every width, height and offset is a whole number of modules. Nothing lands between lines.
CONTAINER
.main-container — max-width 1480px, left-anchored, 60px on the left. The right edge rounds down to a whole module, so the right gutter runs 60–99px and both frame lines always land on the grid. Drops to 40px padding at 991 and 20px at 479. At 1440 that leaves 1320px of content: 33 modules.
COLUMN CANON
1, 2, 3, 4 or 6. No other division exists here. Five columns would give 208px and seven 125.7px — neither is a whole module. A layout that seems to want five wants six with one left open.
GUTTER
40px, 80px or 120px — all whole modules. Which one is not taste. For N equal columns in a run of R modules with a gutter of g modules, the column is (R + g) / N − g, so N has to divide R + g exactly.
At the current container that arithmetic has a tidy answer: R = 33, g = 3 gives 36, and 36 divides by 1, 2, 3, 4 and 6 — the column canon, exactly. A 120px gutter serves every legal column count. This is also why an 80px gutter cannot hold three columns in a 27-module run, which is a real bug this site shipped.
FIELDS
Bleed off one edge and terminate on a column line. Symmetric insets read as a boxed panel, which is the habit this system rejects.
RULES
Horizontal rules land on document-absolute multiples of 40 — not merely 40 apart from each other. Alignment is measured against the page, not the parent.
04
Type
Three families, each with one job. Seven sizes — 11, 12, 14, 16, 20, 28, 56 — and every line height a multiple of 20. Weight stays at 400–500, nothing lighter; size and space carry hierarchy, not boldness. The same roles apply on every page.
DISPLAY MANROPE 500
Manrope
Headlines and card headings only. Never body copy — its tight tracking is drawn for large sizes and closes up badly under 20px.
BODY INTER 300 – 700
Inter
Everything you actually read. Loaded at 300, 400, 500, 600 and 700; 400 and 500 do almost all the work.
LABEL IBM PLEX MONO 400 / 500
IBM Plex Mono
Kickers, tokens, captions, dimension labels. Always tracked at 0.14em when it is a label. Its fallback is monospace — never sans-serif.
The scale
Eight roles. If a new piece of text does not fit one of them, the answer is almost always that it is one of them.
PAGE TITLE .big-text MANROPE 500 · 56 / 60
AI raises the ceiling.
CARD HEADING .rl-ai-head MANROPE 500 · 28 / 40
Built into your team, not around it.
SECTION HEADING .ds-h2 INTER 500 · 16 / 20
Where the marks come from
LEDE .ds-lede INTER 400 · 20 / 30 · LEAD
Most portfolio sites are decorated. This one is constructed.
BODY .measure-body INTER 400 · 14 / 20
The baseline has moved — everyone ships fast now. What separates the work is knowing which problems are worth solving, and having the craft to make the answer feel obvious.
SUPPORTING .ds-sub INTER 400 · 12 / 20
Four working tones and two accents. Nothing is a one-off.
20px. Two lines make one 40px module, so type and layout share a single grid — every heading, rule and section edge lands on a multiple of 20.
MEASURE
45 to 75 characters, 66 the target. .measure-body caps at 440px — eleven modules — which puts 14px Inter at roughly 65 characters.
EMPHASIS
Weight, not size, and never italic for emphasis. One highlighter stroke per heading, on the payoff word.
05
Spacing and rhythm
Four values for small spacing and nothing else. Anything smaller than 8 is optical correction, not spacing — and should be justified as such.
8
20
40
80
120
OPTICAL
IN A ROW
ONE MODULE
BLOCK GAP
SECTION GAP
THE 20PX BASELINE AGAINST THE 40PX MODULE
FINE 20 · STRONG 40
TWO LINES TO EVERY MODULE ↑
SUB-MODULE SCALE
20 / 40 / 160 — inside a group, between groups, between sections. 8 only for micro gaps inside a component. If a gap wants 12 or 30, the surrounding block is the thing that is wrong.
SECTION PADDING
80px top and bottom on .section, 40px at tablet and below — adjacent sections meet at 160. Modifiers: .filled (Ink ground, Paper type), .no-top-padding, .no-padding.
BETWEEN SECTIONS
160px between sections, 40px between groups, 20px inside a group. Three values, nothing in between. Drops to 80px between sections on small screens.
HEADING TO BODY
20px. Between body blocks, 20px. Between groups, 40px. All on the baseline.
BASELINE SYNC
Type sits on a 20px baseline and the layout on a 40px module. Two lines per module, so they agree everywhere — no re-sync interval to manage.
MOTION DISTANCE
Movement travels in modules too — 8, 20, 40, 80. A transform that moves 13px belongs to no grid and reads as drift.
06
Components
Live, not pictured — every specimen below is the real class doing its real job. Reuse these. Do not create a new class for something already here.
HIGHLIGHTER .text-span.underline-highlight-2
People still set the bar.
A textured marker stroke, not a flat fill. This is the correct way to emphasise a word in a heading — one per heading, on the payoff. It is the site's signature and the thing most often got wrong. .hd-mark is the heading-tuned variant — a tighter band, sat lower on the baseline.
FLAT VOLT .rl-claim-volt
Choosing what to build didn't.
Exists, but the highlighter is preferred. Use flat Volt only where the marker texture cannot read — small sizes, or over a busy ground.
MUTED CLAUSE .rl-claim-muted
Building got cheap. Choosing what to build didn't.
Only ever a substantial clause. Greying single words — an "I", an "and" — reads as a mistake rather than as hierarchy.
Inherits its colour, 1px underline at Ink 35%, 3px offset, darkening to 90% on hover. Navigation links get the Signal Red scribble instead — drawn by script, never a border.
Two sizes, by decision. Standard is 40px — one module — with 20px horizontal padding: Ink with Paper type, or Mist with Ink. Hero and call-to-action buttons step up to 60px: a deliberate break from the module, because beside 56px display type a 40px button reads undersized. Volt is reserved for the one action that matters — Email me — in the nav, the hero and the closing panel; on a Volt ground it inverts to Ink with Volt type. One Volt action per view.
SERVICE CHIP .marquee-inner-box
B2B SaaS Product DesignAI Product DesignDesign Systems at Scale
Mist ground, 40px tall, 0 20px padding, Volt on hover. Same metrics as a button so a row of chips and a row of buttons share a baseline.
NUMBER CHIP .ds-num
01Every width, height and offset is a whole number of 40px modules.
The only place a Volt fill sits behind small type. Volt is rationed: if more than roughly one element in six on a screen is Volt, it has stopped being emphasis and become pattern.
CALLOUT .ds-md + .ds-mdmark
A 24px Volt square, a hairline above and below, and the point. Used once or twice a page at most.
LABEL ROW .ds-trrow + .ds-trk + .ds-trv
A 280px label column
and a value beside it. This row and the specimen rows on this page are the same shape — one pattern, reused, rather than two that nearly match.
SQUARE CARD .hp-princ-cell + .ai-princ
01
One idea per square
A number at the top, a title and two or three lines at the bottom.
Mist ground, 40px padding, square by its column. Three to a row in .sw-3col + .ai-princ-grid. Copy limits keep it square at its smallest, six modules: title under 40 characters, body under 100. If it won’t fit, it’s two ideas. Used for principles and trade-offs: equal ideas, not a sequence.
TESTIMONIAL CARD .rec-card
The reviewer’s own words sit here, quoted exactly as they were given.
Name Surname
Role, Company
Mist ground, 480 by 360 — twelve by nine modules — with 40px padding. Quote at the top, attribution pinned to the bottom. Real people, real words, never trimmed into something they did not say.
The only full-bleed Volt field on the site. Display type, 80px padding, one Ink button with Volt type. It closes every page exactly once, so the ask is always the same ask in the same place.
PROJECT CARD .project-block
Image, then the title 20px below it, then one sentence of muted summary.
The whole card is the link — no “read more”. The image carries the client’s brand ground and logo (see Client marks); the title says what it was, the sentence says why it mattered. Live on /work.
NAVIGATION .navbar
120px tall — three modules. The loop mark, mono links, and the one Volt action filling the bar’s full height on the right.
Paths, not labels: /work, /about, /process — the site describes itself in its own syntax. The current page stays marked. Below 991px the links fold into a drawer for touch. Live at the top of this page.
FOOTER footer
Name and a live “// last deployed” stamp on the left, two link columns on the right, over a field of grid squares that fill in as you scroll.
The module made visible. Every square is one 40px cell; the field is drawn, not an image, and switches off with reduced motion. Live at the foot of this page.
FORM FIELDS .field-label + .text-field
Label above in Inter 14 at 500. The field is a single hairline at Ink 20%, square, on Paper. The placeholder asks a real question.
“What’s the problem? Two sentences is plenty.” beats “Enter your message”. The hairline darkens on focus; errors are said in words beside the field, never by colour alone. With JavaScript on, the contact pop-up replaces the page form.
ICONS .rl-ai-mark
Drawn for the system, not picked from a library. 80px — two modules — on a 4 × 4 sub-grid of 20px cells, with the grid left visible at Ink 14% so each icon shows its own construction.
Three roles only: Ink for structure, Mist for the quiet options, the accent for the one thing that matters — exactly one accent element per icon. Square corners; the only line is the grid, plus a dashed outline for “deliberately left empty”. Colour is set by class in CSS, never inside the SVG, so every theme preset recolours them. If an idea can’t be said in sixteen cells, it needs words, not an icon. Above: assist and decide, know when not to, built into the team.
07
Imagery
There is no photography on this site and that is a decision, not an omission. Rendered graphics and stock imagery signal effort without carrying information — decoration standing in for thinking. Everything here is either built on the grid or is a real artefact of real work.
Article graphic
Flat squares on an 80px grid. One shape in Volt marks the article's argument.
Same system, different argument
Composition changes; unit size, palette and accent logic never do.
Open Graph card
1200 × 630. The one place the mark sits on a Volt ground.
ARTICLE GRAPHICS CANVAS
Main image 2400 × 1280. Thumbnail 800 × 800. Grid pitch is 80px on both — do not scale the unit down to fit more detail into the thumbnail; it breaks the shared visual scale with every other post.
ARTICLE GRAPHICS CONSTRUCTION
Flat solid rectangles only. No gradients, no shadows, no text, no icons. Every shape unit is exactly one 80px cell, flush inside the cell rather than centred on a grid line. Leave a 1px inset so a hairline of grid shows between adjacent same-colour squares — 6px reads as shrunken, 0px reads as one undifferentiated blob.
Position with integer column arithmetic (left_col = centre_col − i), never centre_x − width/2. Continuous maths silently offsets odd-width rows by half a cell — invisible by eye, obvious the moment you check that every edge lands on a multiple of 80.
ARTICLE GRAPHICS COLOUR
Ground #F7F7F5, grid line #E0E1E0, shapes Ink. Exactly one shape or row in Volt per graphic — the accent marks the key concept. Mist only if a composition genuinely needs a third tone.
CASE STUDY IMAGES
Real product screens, never mockups on a floating device. 7:5, Mist placeholder ground, a 1px Ink 8% hairline instead of a shadow, and a zoom cursor. They sit at 55% opacity until hovered, so the page reads as type first.
CLIENT MARKS
Each case study wears its client's own brand colour as the card ground, with the supplied logo centred on it. Brand colour lives here and nowhere else — on the work, never in the interface around it. On the résumé the same tiles sit at reduced opacity so the timeline reads first. Logos are never redrawn, recoloured outside the client's palette or cropped to a circle.
TEXTURES
Two, and only two: the Volt highlighter stroke and the Signal Red scribble. Both are scans of real marker on paper, which is the entire point — they are the one place the system lets a human hand show.
FORMAT
WebP for photographic and raster artwork, SVG for marks and diagrams, PNG only where an article graphic needs exact flat colour. The Open Graph image is 1200 × 630 and is the one place the logo appears on a Volt ground.
NEVER
Stock photography. Gradients. Drop shadows. 3D renders. Device frames. Abstract AI-generated imagery. Faces that are not mine. Any image whose job is to fill space rather than carry information.
08
The rules
Five. Short enough to hold in your head, which is the only kind that get followed.
01
Every width, height and offset is a whole number of 40px modules. Nothing lands between lines.
02
Columns come from 1, 2, 3, 4 or 6. No other division exists here.
03
Fields bleed off one edge and stop on a column line. Never boxed in on all four sides.
04
Running text stays between 45 and 75 characters a line, with 66 as the target.
05
Type sits on a 20px baseline — half a module — so it agrees with the grid on every line.
The system is a file, not a PDF
All of the above lives in one file — the tokens with their variable IDs, the module, the column canon, the measure, and a log of the places where this site currently breaks its own rules. It's the context an AI agent is handed before it touches anything here, so constraints arrive as constraints rather than preferences. That's why the tenth screen matches the first. The drift log is the honest half: a system that only records its intentions is marketing.
One curve everywhere. Things settle; they don’t stop.
--dur-micro: 240ms
Hover, focus, chip states.
--dur-panel: 400ms
Drawers, reveals, anything that opens.
--dur-page: 520ms
Page-level moves.
The rule: motion moves in modules. Nothing travels a distance that isn’t made of 40px — 8, 20, 40, 80 — which is what makes movement feel like it belongs to this grid rather than easing applied on top of one. Where the site still moves in an older dialect, that’s drift, and it’s logged.
10
Responsive
Six breakpoints, but only three do real work. Every grid needs an explicit collapse — a grid with no breakpoint variant will hold its desktop column count on a phone, which has bitten this site before.
≥ 1600
Container caps at 1480px. Content locks at 1360px — 34 modules — and stops growing. Everything above this width is margin.
1440
The design width. 1320px of content, 33 modules, 60px padding. This is the width every figure on this page is drawn at, and the only one where the whole canon divides cleanly.
1280 – 1439
Fluid, in whole modules. Content takes what the viewport leaves, rounded down to the nearest 40px, so the left and right edges both stay on the grid at every width. Figures drawn at the 1440 design width scale down whole rather than crop.
≤ 991
Container padding drops to 40px. Multi-column grids collapse — two-ups to one, three-ups to two or one. The mobile drawer takes over from the nav bar.
≤ 767
Three-ups that survived 991 collapse here. The background margin rule and the random grid pockets switch off — at this width they are noise rather than structure.
≤ 479
Container padding drops to 20px. Display type steps 64 → 40 → 32. Nothing else changes: the module does not shrink, the page just holds fewer of them.
THE EXCEPTION
Mobile drawer rows are 64px, not 40 or 80. A touch target beats the module here, and it is the only place on the site where that is true.
11
Voice
The writing is part of the system. Same instinct as the grid: no adjective does work a fact could do.
PERSON
First person, present tense. "I work the whole arc", not "Reloop delivers end-to-end solutions". There is one person here and the writing should not pretend otherwise.
CLAIMS
Specific and falsifiable. "$6.99M ARR and 815 new doctor users" beats "drove significant growth" — and can be checked, which is the point.
HEADLINE PATTERN
Building got cheap. Choosing what to build didn't.
A flat statement, then the turn. The highlighter lands on the turn, never on the setup.
PUNCTUATION
Em-dashes for asides. Sentences may be short. Fragments are allowed. Semicolons almost never — they slow a line down without earning it.
Removed deliberately from positioning copy. They describe a posture rather than a capability, and a reader who has seen twenty portfolios has seen all six on every one of them.
AI, WHEN WRITING ABOUT IT
Never "working with AI" as a credential — it reads as vibe-coding. The claim is about judgement and delivery inside an existing team, not about tool access.
12
Known trade-offs
A system that only documents its intentions is marketing. These are the places where something else won, and why.
01
Readability beat arithmetic
A 20px baseline fits the module but caps body copy at 14px. Density traded for calm.
02
The measure is fixed at 440
Body text runs near 75 characters, above the 66 optimum. A narrower column would fix it.
03
Cards tighten before the grid breaks
On narrow screens card text drops near 34 characters. The grid should fall to two columns sooner.
04
The mobile drawer keeps its own rhythm
Nav and footer sit on the module. The drawer’s 64px rows don’t: a deliberate touch-target choice.
Tell me what you’re building. I’ll tell you if I can help.