design
How I design this site
Every decision here was made at least twice: once quickly, and once after the first version felt wrong. This is the record of what I settled on and why. It is not a style guide. It is closer to a set of running notes from someone who keeps rebuilding the same site until it stops bothering him.
Valyrian Steel and the one accent
Most of the site runs on nine shades of grey. Not because grey is safe, but because limiting colour to a single role gives it weight. Five semantic tones live alongside the neutral ramp for callouts, presence states, and the command palette. Each semantic colour only appears in context: paired with copy, position, shape, or an icon. Colour is never the only signal.
The nine greys are OKLCH at hue 240. Plain sRGB neutrals warm as they approach white, leaving an unintentional yellow cast on light surfaces. OKLCH at hue 240 stays perceptually neutral across the full ramp: the same temperature of light at every step. Light mode uses step 1 as canvas and step 9 as ink. Dark mode reverses them exactly, and the same hierarchy holds.
I call these greys Valyrian Steel. I like Game of Thrones, and the name stuck.
Five semantic tones carry intent across the palette: Green for success and availability, Amber for warnings and winding down, Blue for information, Violet for highlights, and Red for caution, error, and off-hours. Each appears only in context, always paired with copy, position, shape, or an icon.
available, success, live endpoints
warning, winding down
information, links
important, highlight
caution, error, asleep (off-hours)
Each swatch shows the light-mode value on the left and the dark-mode value on the right. The hue stays constant; only the lightness shifts.
Three typefaces, one job each
Inter carries navigation, prose, and UI. Newsreader Italic gets one serif moment: the naming note for the grey scale, literary asides, and quote attribution. JetBrains Mono is for code, tokens, hashes, timestamps, and measurements only. Each font stays inside its lane, which means each one can lean into what it does well.
Emphasis here comes from value, not weight. A word in ink against a sentence in ghost does what italic or bold usually does, without adding typographic clutter. When weight does change, it changes in deliberate steps. Click through them below.
I work from home; on a countryside region wrapped in coconut groves, close enough to the Western Ghats that the weather has opinions.
The exact values are in design.md. This page is the part I rewrite when the site teaches me something.
Newsreader Italic · 400 · 1.55 leading. One serif moment, used sparingly.oklch(0.58 0.2 25)oklch(0.58 0.2 25)
Left shifts 1 px down to align optically with Inter. Right sits on the natural baseline. Without the correction, monospace sits slightly high and reads as floating.
One rail, every page
Header, content, and footer share one fixed rail: clamp(680px, 52vw, 800px) wide, with 20 px gutters on each side. At 800 px and around 64 characters per line, a paragraph sits at the comfortable top of the reading range. Wider feels like a newspaper column that requires head movement; narrower starts to feel cramped on a desktop screen.
The same rail for every route. No "wide" library view, no "narrow" notes layout. Consistency means the reader's eye always starts from the same place.
A comfortable line length lets the eye glide effortlessly across ten to twelve words before sweeping back to the start of the next line. When a measure is too wide, the return sweep becomes a conscious search; when too narrow, the rhythm fractures into constant stops and starts. The 64-character rail keeps reading continuous and effortless.
Typography is ergonomics for the eyes: wider is not better, and narrower is not tidier.
Below 608 px the layout shifts: the top navigation disappears and five fixed tabs appear above the safe area at the bottom. The body reserves space beneath the bar via --mobile-chrome-clearance, so the last line of any page is never hidden behind it. Section spacing is larger above a heading than below it, which gives the eye a clear signal that a new topic is starting.
Five durations, two curves
Every animation on the site draws from five durations. Each has a semantic meaning tied to how much attention the movement should ask for. 80 ms for press feedback - faster than the eye expects, which makes a control feel physically responsive. 150 ms for colour and opacity - long enough to be visible, short enough to feel instant. 250 ms for elements that move through space. 400 ms for reveals that need to draw in gradually. 500 ms for overlays that deserve a breath.
Click a card to see how that duration feels. The same dot, five different paces.
--dur-180mspress feedback--dur-2150mscolour and opacity--dur-3250msspatial movement--dur-4400msdraw or reveal--dur-5500msoverlay and expandRoute transitions use blur only: never opacity, never position. Any opacity below 1 during the DOM swap causes a visible canvas flash, even for a single 16 ms frame. Blur keeps the element fully opaque while masking the content change. The outgoing page blurs in 80 ms; the incoming page clears in 250 ms. The header stays fixed throughout.
With reduced motion enabled, all travel is removed. What remains: colour transitions, opacity on show and hide, and focus feedback. The experience is complete without animation.
A tiny Web Audio engine
The site has fourteen interaction sounds. None of them are audio files. Each one is a few lines of Web Audio API: create an oscillator, set a frequency, ramp the gain down to near zero. The complete engine is a single TypeScript module with no runtime dependency, gentle organic micro-pitch variation to prevent auditory fatigue, and a master gain of 0.09 - quiet enough that it reads as a texture rather than a notification.
Sound is on by default, controlled by the toggle in the footer, and persisted in localStorage. Open tabs stay in sync via the storage event. The AudioContext is only created from a real user gesture - before that, any call to playAccent() is a no-op.
Sound carries no meaning alone. Every accent is redundant with a visual change: the theme toggle shows the sun shift before any tone plays. Someone with sound off misses nothing. The mute confirm plays before the gate flips, so the last sound you hear when muting is the descending toggle-off chord.
The details that compound
Individually, none of these are remarkable. Together, they account for most of the time I spend on this site.
Arrow direction
The shape on a link tells you what will happen before you leave the current page. This is the complete set.
- The library ArrowRight: forward navigation within site
- Back home ArrowLeft: back or previous route
- Source ArrowUpRight: external only, opens new tab
- ChevronDown: in-place progressive disclosure
More detail
ChevronDown: disclosure. Rotates 180 degrees when open. The same shape, a different orientation, which preserves visual memory.
Focus
A 2 px ink ring at 3 px offset, shown only for keyboard navigation via :focus-visible. Mouse clicks suppress it. Every interactive element has a visible focus state, and every touch target is at least 44 px tall.
:focus-visible.Hover paragraphs
The homepage intro and the colophon use a hover effect: hovering a paragraph brightens the whole block from ghost toward ink, and any keyword spans within it become semibold. It rewards reading slowly. It is not applied globally - only in blocks where the density of information makes the interaction useful.
The typographic stream
List items across the site sit directly on the canvas without heavy boxes or clumsy dividing lines. Hovering one item softly spotlights it with a 3.5% ink highlight while its siblings gently soften, mirroring the homepage hero intro.
Selection
Regular selection is ink on canvas. Quote text uses the same ink-on-canvas treatment, keeping selection consistent across the page.
A live clock in the footer
The footer shows the current time in Pollachi, Tamil Nadu (Asia/Kolkata, UTC+5:30). Clicking it reveals your own local time and zone for comparison. The dot to the left of the clock is the presence indicator: a small coloured circle that changes based on the current hour in IST.
Three states, driven by the hour. Available from 08:00 to 20:00. Winding down from 20:00 to midnight and from 06:00 to 08:00. Asleep from midnight to 06:00. Each state gets a different colour token and a label drawn at random from a small pool, so the tooltip text stays fresh across visits.
Reading current state...
Every part of the page that shows presence subscribes to one presence:change custom event. The footer clock dispatches it once per second; the event only fires when the state actually changes, not every tick. The dot, the map pin tooltip, and the contact CTA line all read from the same source with no duplicated logic.
When I am asleep, a data-accent="off-hours" attribute is applied to the root element. This desaturates the red accent slightly via a CSS override, making the whole site feel quieter. It is a small thing that is easy to miss.
The things you might not find
These are here because they should be, not because anyone asked. None of them affect the site's function.
The console API
On any page, open the browser console and type ashwin.help(). The object is read-only, frozen, and not enumerable on window.
» ashwin.help()» ashwin.status()» ashwin.source()» ashwin.secrets()ashwin.status() returns the current route, build commit hash, active theme, reduced motion state, and the live initialisation state of the Spotify embed and the MapLibre map. It is useful for debugging integrations without opening React DevTools, because there is no React.
The command palette
Open it with ⌘K or /, then try one of these.
› whoami› vanakkam› 42› sudo hire ashwinThe quote block
The homepage quote cycles daily based on a seed derived from the current date. Pressing J shuffles it immediately. There are enough of them that you might land on one you did not expect.
The clock
The footer clock shows my time by default. Pressing it switches to your local time and zone. Pressing it again returns to mine. It is not labelled anywhere on the page. The dot next to it is the presence indicator, described in the chapter above.