Design system. Quiet, ruled and fast.
The tokens, type and components behind farhan.app. Every example on this page is rendered with the live stylesheet, and the values are read from it, so this page is always current.
Sections
Principles
A handful of rules decide almost every choice on the site.
One accent
Everything is ink on near-white. Blue appears only for links, focus and the current place.
Hairlines, not boxes
Structure comes from one-pixel rules and space. Cards and containers are the exception.
One weight, tight tracking
Regular weight at every size, with letter-spacing tightening as type grows. Medium is only for emphasis.
Atmosphere in panels
The page stays flat. All colour and depth live in grainy pastel panels.
Fluid, not breakpointed
Type and space scale with the viewport; rows and grids reflow by the width of their container.
Accessible by default
Semantic HTML first, AA contrast, visible focus, keyboard everywhere, reduced motion respected.
Colour
Light only. A near-white ground, charcoal ink at three strengths, hairlines in translucent black and a single blue. Values are shown as they appear in the stylesheet.
Surfaces
-
Page
--bg-page
#f8f8f8
The ground. Every page sits on it.
-
Surface
--bg-surface
#fcfcfc
Raised bands and quiet containers.
-
Elevated
--bg-elevated
#ffffff
Inputs, code blocks, floating controls.
-
Primary
--bg-primary
#000000
The one thing you want clicked.
Ink
- Aa
Ink
--text-primary
#191919
16.6:1 on page · AA
Headings, titles, interface text.
- Aa
Ink 78%
--text-secondary
#191919 · 78%
8.3:1 on page · AA
Body copy and descriptions.
- Aa
Ink 64%
--text-tertiary
#191919 · 64%
5.1:1 on page · AA
Meta, captions and dates. The lightest text that still passes AA.
- Aa
Lead 50%
--lead
#191919 · 50%
3.3:1 on page · AA large text
The quiet first line of a two-tone heading. Large text only.
Accent and status
-
Accent
--accent
#317cff
Focus rings and active states. Never body text.
-
Link
--link
#1a5fe0
Inline links and the current page in the menu. AA on the page.
-
Success
--success
#16a34a
Tip callouts and positive status.
-
Warning
--warning
#ea7814
Warning callouts and the "Required" badge.
-
Danger
--danger
#dc2626
Danger callouts and errors.
Lines and fills
-
Border
--border
#000000 · 8%
Controls, cards and the main hairlines.
-
Border subtle
--border-subtle
#000000 · 6%
Row dividers and section rules.
-
Fill 4%
--fill
#000000 · 4%
Hover and current backgrounds.
-
Fill 6%
--fill-2
#000000 · 6%
Inline code and pressed states.
Tints
-
Tint blue
--tint-blue
#337df4 · 10%
Note callouts, "Recommended" badge.
-
Tint green
--tint-green
#16a34a · 10%
Tip callouts.
-
Tint orange
--tint-orange
#f5821e · 14%
Warning callouts.
-
Tint red
--tint-red
#dc2626 · 9%
Danger callouts.
Gradients
-
Sky
--panel-sky
Panel gradient with grain overlay.
-
Haze
--panel-haze
Panel gradient with grain overlay.
-
Mist
--panel-mist
Panel gradient with grain overlay.
Typography
Three self-hosted families. Geist carries the voice, Inter handles small interface text, and Geist Mono labels things that are data.
Geist
Headings, body and navigation. Weights 400 and 500.
--font-sans
Inter
Descriptions, captions and small labels.
--font-ui
Geist Mono
Dates, kickers, numbers and code.
--font-mono
Scale
-
Data, models and web apps
-
Notes on finance and self-hosting
-
Why locks beat retries
-
A short sentence that tells you what this page is for.
-
Most of my work sits in the gap between a finance team’s question and the system that could answer it.
-
Notes on finance, Excel and self-hosting.
Two-tone headings
A muted first line followed by a confident statement. The first line is ink at 50%, which is why it is reserved for large type.
Analyst and developer.I build the thing that answers the question.
Spacing and layout
Space is fluid: every value is a clamp() that grows with the viewport between a floor and a ceiling.
| Token | Range | Preview at maximum | Use |
|---|---|---|---|
| --gutter | 18 → 64 px | Page gutter: the side padding of every frame. | |
| --space-hero-t | 64 → 120 px | Hero, top. | |
| --space-hero-b | 56 → 96 px | Hero, bottom. | |
| --space-head-t | 40 → 72 px | Page header, top. | |
| --space-head-b | 28 → 48 px | Page header, bottom. | |
| --space-sec-t | 40 → 60 px | Section, top. | |
| --space-sec-b | 56 → 90 px | Section, bottom. | |
| --space-body-t | 28 → 48 px | Body content, top. |
Frame and widths
- Frame
- 80rem (1280px) with dashed side rules
- Header
- 3.25rem (52px), sticky
- Article column
- 42rem, centred
- Docs column
- 44rem, with sidebar and outline
- Menu breakpoint
- 45rem
- Docs layout
- 3 columns, 2 below 75rem, 1 below 56rem
Rows and grids reflow by container width, not screen width: rows change layout at 40rem and 45rem of their own width, and card grids use auto-fit columns with a minimum size.
Shape, depth and motion
Marketing surfaces stay flat. Rounding and depth are small and reserved for things you interact with.
Radii
--r-xs
4 px
--r-sm
6 px
--r-md
12 px
--r-lg
14 px
--r-xl
16 px
--r-full
pill
Elevation
--shadow-raised
Hovered cards.
--shadow-float
Floating controls.
Motion
- Fast
- .15s
- Link and fill changes.
- Base
- .2s
- Buttons, borders, transforms.
- Easing
- cubic-bezier(.2,0,0,1)
- Quick out, no bounce.
- Marquee
- 40s linear
- Pauses on hover.
Colour and fill shifts, a few pixels of travel, and nothing else. When a visitor prefers reduced motion, animation and smooth scrolling are switched off.
Actions
Actions are pills. One black primary per view, a hairline outline for the alternative, and a ghost for everything else.
Buttons
Primary, outline and ghost, each in a default (42px) and small (28px) size. The arrow marks a link that leaves the site.
<div class="acts" style="margin:0">
<a class="btn primary" href="#" data-demo>Primary</a>
<a class="btn outline" href="#" data-demo>Outline</a>
<a class="btn ghost" href="#" data-demo>Ghost</a>
<a class="btn primary" href="#" data-demo>Visit project <svg aria-hidden="true" focusable="false"><use href="#out" /></svg></a>
</div>
<div class="acts" style="margin-top:.75rem">
<button class="btn sm primary" type="button">Small</button>
<button class="btn sm outline" type="button">Small outline</button>
<button class="btn sm ghost" type="button">Small ghost</button>
</div> Links
In running text, links are the one accent blue with a soft underline that firms up on hover. Quiet navigation links use the muted style.
Read the case study or go back to all projects.
<p style="margin:0">Read the <a class="a" href="#" data-demo>case study</a> or <a class="link-muted" href="#" data-demo>go back to all projects</a>.</p> Lists and data
Information sits directly on the page. Rows are separated by hairlines, not boxes, and reflow by the width of their own container rather than the screen.
Rows
The standard list: date, title, tag, chevron. External rows rotate the chevron into an arrow. Under 40rem of container width the date and tag move onto their own lines.
<ul class="rows" role="list">
<li><a class="rowline changelog" href="#" data-demo><time class="d" datetime="2026-10">Oct 2026</time><span class="t">I built my first SaaS. Then the client bought it.</span><span class="c">SaaS</span><svg class="ic" aria-hidden="true" focusable="false"><use href="#cr" /></svg></a></li>
<li><a class="rowline changelog" href="#" data-demo><time class="d" datetime="2026-05">May 2026</time><span class="t">Docker cleanup commands to free up disk space on a VPS</span><span class="c">VPS</span><svg class="ic" aria-hidden="true" focusable="false"><use href="#cr" /></svg></a></li>
<li><a class="rowline changelog ext" href="#" data-demo><time class="d" datetime="2026-01">Jan 2026</time><span class="t">CSS design system generator</span><span class="c">Web app</span><svg class="ic" aria-hidden="true" focusable="false"><use href="#cr" /></svg><span class="vh">(external site, opens in a new tab)</span></a></li>
</ul> Resource rows
Name, what it is for, a tag and the external arrow.
<ul class="rows" role="list">
<li><a class="rowline res ext" href="#" data-demo><span class="n">Claude</span><span class="w">The best AI tool for writing high quality code.</span><span class="c">AI · Referral</span><svg class="ic" aria-hidden="true" focusable="false"><use href="#cr" /></svg><span class="vh">(external site, opens in a new tab)</span></a></li>
<li><a class="rowline res ext" href="#" data-demo><span class="n">Revolut</span><span class="w">My go-to banking app for travel and subscriptions.</span><span class="c">Banking · Referral</span><svg class="ic" aria-hidden="true" focusable="false"><use href="#cr" /></svg><span class="vh">(external site, opens in a new tab)</span></a></li>
</ul> Facts and stats
A definition list under a hairline. Mono labels over plain values; a stat swaps in a large figure above its caption.
- Year
- 2026
- Role
- Developer
- Built with
- JavaScript, Leaflet
- Routes mapped
- 700+
- Stops indexed
- 19k
- Initial render
- <1s
<dl class="facts" style="margin-bottom:1.5rem">
<div><dt>Year</dt><dd>2026</dd></div>
<div><dt>Role</dt><dd>Developer</dd></div>
<div><dt>Built with</dt><dd>JavaScript, Leaflet</dd></div>
</dl>
<dl class="facts" style="margin-bottom:0">
<div><dt>Routes mapped</dt><dd class="kpi">700+</dd></div>
<div><dt>Stops indexed</dt><dd class="kpi">19k</dd></div>
<div><dt>Initial render</dt><dd class="kpi"><1s</dd></div>
</dl> Tables
Ruled, not boxed. Muted column headers over a single hairline, tabular figures, numbers right-aligned. The wrapper scrolls on narrow screens and is keyboard-focusable.
| Period | Base rate | Uplift | Revenue |
|---|---|---|---|
| Q1 | £118.00 | 0.0% | £1.42m |
| Q3 | £118.00 | 3.2% | £1.48m |
<div class="tw" tabindex="0">
<table class="table">
<caption class="vh">Illustrative quarterly revenue</caption>
<thead><tr><th scope="col">Period</th><th scope="col" class="num">Base rate</th><th scope="col" class="num">Uplift</th><th scope="col" class="num">Revenue</th></tr></thead>
<tbody>
<tr><th scope="row">Q1</th><td class="num">£118.00</td><td class="num">0.0%</td><td class="num">£1.42m</td></tr>
<tr><th scope="row">Q3</th><td class="num">£118.00</td><td class="num">3.2%</td><td class="num">£1.48m</td></tr>
</tbody>
</table>
</div> Cards and surfaces
Atmosphere lives in grainy pastel panels. Cards are a panel (or a real image) above a mono meta line, a title and a short description.
Panels
Three gradient panels, each with a fine grain overlay. They cycle across cards and stand in when a project has no thumbnail.
<div style="display:grid;gap:.75rem;grid-template-columns:repeat(auto-fit,minmax(min(100%,10rem),1fr))">
<div class="panel" style="aspect-ratio:16/10;border-radius:var(--r-lg)" role="img" aria-label="Sky panel"></div>
<div class="panel haze" style="aspect-ratio:16/10;border-radius:var(--r-lg)" role="img" aria-label="Haze panel"></div>
<div class="panel mist" style="aspect-ratio:16/10;border-radius:var(--r-lg)" role="img" aria-label="Mist panel"></div>
</div> Card
The whole card is one link; the title turns accent blue on hover or focus.
<div style="max-width:24rem">
<a class="acard" href="#" data-demo>
<span class="panel haze" aria-hidden="true"></span>
<p class="meta"><time datetime="2026-04">Apr 2026</time><span aria-hidden="true"> · </span>API</p>
<h3>London buses</h3>
<p>TfL's public network data as an interactive map, with first paint under a second.</p>
</a>
</div> Long-form content
Articles are one narrow column (42rem) with ink at 80%, generous leading and headings that tighten as they grow. WordPress HTML is styled by the same rules, so a post needs no special markup.
Prose
Headings, paragraphs, links, lists with small ink bullets, ordered steps with mono numerals, and inline code.
Why it matters
You end up screening on the wrong signals. Outcomes beat duties, and a good title carries information. Use docker system df to look first.
- The system or process you own
- What changed because you were there
- Open Settings and choose Security.
- Paste your metadata URL.
<div class="prose">
<h3>Why it matters</h3>
<p>You end up screening on the wrong signals. <strong>Outcomes</strong> beat duties, and <a href="#" data-demo>a good title</a> carries information. Use <code>docker system df</code> to look first.</p>
<ul><li>The system or process you own</li><li>What changed because you were there</li></ul>
<ol><li>Open <strong>Settings</strong> and choose Security.</li><li>Paste your metadata URL.</li></ol>
</div> Quote and pull quote
A block quote has a hairline rail. The pull quote is a two-tone display line.
If the title tells a hiring manager nothing, the CV has to do all the work.
The queue, not the model,decides what lands.
<div class="prose">
<blockquote>If the title tells a hiring manager nothing, the CV has to do all the work.</blockquote>
<p class="pull"><span style="color:rgb(var(--lead))">The queue, not the model,</span><span style="display:block">decides what lands.</span></p>
</div> Callouts
Four tinted callouts with an icon. A tint carries the meaning; the icon and a bold lead word repeat it, so colour is never the only signal.
<div style="display:grid;gap:.625rem">
<aside class="callout note" role="note"><svg aria-hidden="true" focusable="false"><use href="#info" /></svg><div><p><strong>Note:</strong> SAML requires an enterprise plan.</p></div></aside>
<aside class="callout tip" role="note"><svg aria-hidden="true" focusable="false"><use href="#check" /></svg><div><p><strong>Tip:</strong> Test in a staging organisation before enforcing.</p></div></aside>
<aside class="callout warn" role="note"><svg aria-hidden="true" focusable="false"><use href="#warn" /></svg><div><p><strong>Warning:</strong> Existing sessions will be signed out.</p></div></aside>
<aside class="callout danger" role="note"><svg aria-hidden="true" focusable="false"><use href="#warn" /></svg><div><p><strong>Danger:</strong> Disabling SSO removes all linked accounts.</p></div></aside>
</div> Code
A bordered block with a header strip holding the language and a copy button. The button sits outside the scrolling area, so it never moves while long lines scroll.
# stopped containers, unused networks, dangling images
docker system prune -f
# build cache only: usually the biggest single win
docker builder prune -af
<div class="cb">
<div class="cb-h"><span>bash</span><button class="copyb" type="button" data-copy-self><svg aria-hidden="true" focusable="false"><use href="#copy" /></svg><span>Copy</span></button></div>
<pre tabindex="0"><code><span class="tk-c"># stopped containers, unused networks, dangling images</span>
docker system prune <span class="tk-f">-f</span>
<span class="tk-c"># build cache only: usually the biggest single win</span>
docker builder prune <span class="tk-f">-af</span></code></pre>
</div> Forms and filters
Hairline controls on a white surface. Focus is the one moment the accent appears at full strength: a blue outline with a soft halo.
Search field
A labelled input with a search icon and a keyboard hint. The label is visually hidden but always present for assistive technology.
<div class="fld" style="max-width:22rem">
<svg aria-hidden="true" focusable="false"><use href="#search" /></svg>
<label class="vh" for="ds-search">Search posts</label>
<input id="ds-search" class="input" type="search" placeholder="Search posts" autocomplete="off" />
<span class="r" aria-hidden="true"><kbd>/</kbd></span>
</div> Filter chips and badges
Filter chips are outlined pills that fill black when selected or current. Badges are quiet tinted labels; the tint says what kind.
<ul class="chips" role="list" aria-label="Example filters" style="margin-bottom:1rem">
<li><a class="fchip" href="#" data-demo aria-current="page">All</a></li>
<li><a class="fchip" href="#" data-demo>Technical</a></li>
<li><a class="fchip" href="#" data-demo>Guides</a></li>
</ul>
<div class="chips">
<span class="chip">Neutral</span>
<span class="chip blue">Recommended</span>
<span class="chip green">Success</span>
<span class="chip orange">Required</span>
<span class="chip red">Danger</span>
</div> Empty state
Centred, short and helpful, with a way out.
Nothing matches that
Try a different word.
<div class="empty">
<h3>Nothing matches that</h3>
<p>Try a different word.</p>
<button type="button" class="btn sm outline" style="margin-top:.375rem">Clear search</button>
</div> Marquee
A slow, hover-to-pause scroll of names or logos, softened at both edges. It is hidden from assistive technology (the same names exist once as a plain list) and stops moving for anyone who prefers reduced motion.
Name marquee
Forty seconds per loop, linear. Two identical sets make the loop seamless.
<div class="marquee" aria-hidden="true">
<div class="marq-track">
<div class="marq-set"><span class="logo-ph">Stagecoach London</span><span class="logo-ph">Shell Energy</span><span class="logo-ph">SEGA</span><span class="logo-ph">MiQ</span></div>
<div class="marq-set" data-copy="1"><span class="logo-ph">Stagecoach London</span><span class="logo-ph">Shell Energy</span><span class="logo-ph">SEGA</span><span class="logo-ph">MiQ</span></div>
</div>
</div> Accessibility
Accessibility is part of the system, not a layer on top of it.
Contrast
All text meets WCAG AA. The lightest ink (64%) is still above 4.5:1 on the page, and the 50% lead colour is only used for large type.
Focus
A 2px accent outline on everything interactive, plus the field halo on inputs. Focus is never removed.
Semantics
Real landmarks, one H1 per page, an unbroken heading order, native buttons and links, and ARIA only where HTML cannot say it.
Keyboard
Skip link, a modal menu that traps focus and closes on Escape, and scrollable regions that can be focused.
Motion
Reduced-motion preferences switch off animation and smooth scrolling, and the marquee stops.
Meaning beyond colour
Callouts pair a tint with an icon and a lead word; the current page is shown by shape, not only hue.