Design system
The vocabulary this site is built from
Every value below is read live from the stylesheet at render time, so this page cannot drift from the implementation. Flip the theme and watch the swatches follow.
src/styles/tokens.cssColour
Two layers: raw ramps, and the role-named tokens components actually use. Components never reference a ramp step directly — that indirection is what makes a re-skin a one-file change.
Warm neutral
The paper-and-ink spine. Every surface, ink and border token resolves into this ramp.
Rust
The single brand hue — oxidised orange, borrowed from film leader and darkroom safelight.
Surfaces
Background planes ordered back to front. Nesting a raised surface inside a sunken one is what creates depth — reach for elevation before you reach for a border.
| Token | Value | Use it for | |
|---|---|---|---|
--surface-sunken | — | Page well behind cards; inset areas like a dropzone | |
--surface | — | Default page background | |
--surface-raised | — | Cards, panels, popovers sitting above the page | |
--surface-overlay | — | Dialogs and menus floating over everything | |
--surface-inverse | — | Inverted blocks — footer, tooltips, toasts |
Ink
Text colours by role, not by lightness. Every pairing listed here clears WCAG AA on its intended surface in both themes.
| Token | Value | Use it for | |
|---|---|---|---|
--ink | — | Primary body and heading text | |
--ink-muted | — | Secondary text, descriptions, metadata | |
--ink-subtle | — | Tertiary text — placeholders, timestamps, eyebrows | |
--ink-inverse | — | Text on surface-inverse | |
--ink-on-accent | — | Text on a filled accent background |
Borders
Hairlines and dividers. `border` is the default for every bounded element.
| Token | Value | Use it for | |
|---|---|---|---|
--border | — | Default hairline on cards, inputs, dividers | |
--border-strong | — | Hover and emphasis states; active drop targets | |
--border-inverse | — | Dividers inside inverted blocks |
Accent
One brand hue, used sparingly. If more than a couple of things on a screen are accent-coloured, none of them read as important.
| Token | Value | Use it for | |
|---|---|---|---|
--accent | — | Primary buttons, active nav, links | |
--accent-hover | — | Hover state for accent surfaces | |
--accent-active | — | Pressed state for accent surfaces | |
--accent-subtle | — | Tinted background for selected rows and badges | |
--accent-subtle-ink | — | Text on accent-subtle | |
--accent-ring | — | Focus ring and selection glow |
Status
Reserved for feedback about state. Never decorative. Each has a solid, a tint, and an ink that is legible on the tint.
| Token | Value | Use it for | |
|---|---|---|---|
--success | — | Published, upload complete, saved | |
--success-subtle | — | Background for a success badge or banner | |
--success-ink | — | Text on success-subtle | |
--warning | — | Draft, processing, needs attention | |
--warning-subtle | — | Background for a warning badge or banner | |
--warning-ink | — | Text on warning-subtle | |
--danger | — | Errors, failed uploads, destructive actions | |
--danger-subtle | — | Background for an error badge or banner | |
--danger-ink | — | Text on danger-subtle |
Scrims
Wash layers that sit between the page and a floating surface.
| Token | Value | Use it for | |
|---|---|---|---|
--scrim | — | Behind dialogs and sheets | |
--scrim-heavy | — | Behind the full-screen photo viewer |
Typography
Fraunces for display, Inter for interface. A 1.200 minor third gives eleven steps that stay distinguishable next to each other.
text-6xlPhotographs, kept like a diary
text-4xlIceland, in February
text-2xlRecent rolls
text-lgPortra 400, pushed a stop and a half.
text-baseThe body size everything else is measured against.
text-smSecondary copy and form help text.
text-xsCaptions, badges, metadata.
Families
Fraunces carries the editorial, printed-diary voice for display type; Inter does the UI work. Both fall back to a well-matched system stack so a blocked font request never reflows the page.
| Token | Value | Use it for | |
|---|---|---|---|
--font-display-stack | — | Headings, folder titles, hero type | |
--font-sans-stack | — | Body copy, UI labels, form controls | |
--font-mono-stack | — | Token names, file names, dimensions |
Scale
A 1.200 minor third off a 16px root — tight enough that adjacent steps stay distinguishable.
| Token | Value | Use it for | |
|---|---|---|---|
--text-2xs | — | Image dimensions, dense metadata | |
--text-xs | — | Eyebrows, badges, captions | |
--text-sm | — | Secondary copy, form help text | |
--text-base | — | Body copy | |
--text-lg | — | Lead paragraphs | |
--text-xl | — | Card titles | |
--text-2xl | — | Section headings | |
--text-3xl | — | Page headings | |
--text-4xl | — | Folder titles on the public site | |
--text-5xl | — | Hero, desktop | |
--text-6xl | — | Hero, large desktop |
Rhythm
Line height and tracking. Display type gets tighter tracking; small caps type gets much looser.
| Token | Value | Use it for | |
|---|---|---|---|
--leading-tight | — | Display headings | |
--leading-snug | — | Card titles, multi-line labels | |
--leading-normal | — | Body copy | |
--leading-relaxed | — | Long-form captions | |
--tracking-tighter | — | Hero type at 3rem and up | |
--tracking-tight | — | Headings | |
--tracking-normal | — | Body | |
--tracking-wide | — | Buttons and small UI labels | |
--tracking-widest | — | Uppercase eyebrows |
Elevation & shape
Shadows are warm-tinted rather than neutral black — against a paper-coloured surface, a grey shadow reads as dirt.
xssmmdlgxlxssmmdlgxl2xlElevation
Warm-tinted shadows — a neutral black shadow reads as dirt against a warm paper surface. Elevation should track how far something floats above the page, so pick by role rather than by taste.
| Token | Value | Use it for | |
|---|---|---|---|
--shadow-xs | — | Barely-there lift on inputs and flat buttons | |
--shadow-sm | — | Cards at rest | |
--shadow-md | — | Cards on hover; sticky headers once scrolled | |
--shadow-lg | — | Popovers, dropdowns, toasts | |
--shadow-xl | — | Dialogs and the photo viewer chrome |
Radius
Corner rounding. Photos themselves stay at `md` or less — heavy rounding crops into the frame and reads as a UI element rather than a picture.
| Token | Value | Use it for | |
|---|---|---|---|
--radius-xs | — | Focus rings, tags | |
--radius-sm | — | Inputs, small buttons | |
--radius-md | — | Buttons, photo thumbnails | |
--radius-lg | — | Cards | |
--radius-xl | — | Folder cards, dialogs | |
--radius-2xl | — | Hero panels, sheets | |
--radius-full | — | Pills, avatars, icon buttons |
Motion
Two easings and three durations. Everything honours `prefers-reduced-motion`, which the base layer collapses globally.
| Token | Value | Use it for | |
|---|---|---|---|
--duration-fast | — | Hover and colour transitions | |
--duration-base | — | Dropdowns, toggles, badge swaps | |
--duration-slow | — | Dialogs, viewer open/close, carousel settle | |
--ease-out-soft | — | Anything entering the screen | |
--ease-in-out-soft | — | Anything moving within the screen |
Components
Every primitive the dashboard and the public gallery are built from. If something here needs a one-off style, that is a signal the token layer is missing something.
Buttons
Primary is the one action a screen wants you to take. If two primaries are visible at once, one of them is secondary.
Form controls
Label, hint and error share one layout, so a field reads the same whether it holds text, prose or a choice.
Shown as the card title on the home page.
Use lowercase letters, numbers and hyphens.
Badges
Status colour is reserved for state. A badge that is merely decorative should be neutral.
Cards
The building block of both the dashboard grid and the public folder carousels.
Iceland, in February
14 photos
Three days of sideways rain and very good light.
Interactive card
Hover me
Lifts on hover — used where the whole card is a link.
Feedback
Progress, spinners, toasts and the empty state that stands in for a screen with nothing on it yet.
No photos here yet
Drag a few JPEGs onto this folder, or use the upload button.
Using the system
How to extend it without breaking it.
Re-skinning the site
Edit the primitive ramps and the two semantic blocks in src/styles/tokens.css. Both the light block and the two dark blocks need the same keys — a token defined only inside a media query has no light value to fall back to.
Adding a token
Define it in the semantic layer, mirror it in the @theme inline block in app.css so a Tailwind utility exists, and add it to src/design/tokens.ts so it appears on this page with a note about when to use it.
Reaching for a raw colour
Don’t. If no semantic token fits, the palette is missing a role — add the role rather than hardcoding a hex value in a component. That is what keeps dark mode working without per-component effort.
Dark mode
Three states: an explicit data-theme attribute for light or dark, and no attribute at all for “follow the OS”. Every colour is defined on bare :root first, so a component always has a value even before a media query applies.
Focus and motion
One focus ring is defined globally in the base layer; components should never restyle it. All motion collapses under prefers-reduced-motion, which is handled once, globally, rather than per animation.