The design language for func.lol. Read this before changing any UI.
Two fonts. No serifs anywhere.
- Inter — h1, h2, h3, body, lede.
- JetBrains Mono — inputs, buttons, numbers, code, breadcrumbs, metadata, labels, dates. The data font, not the code font.
The contrast is sans vs. mono, not sans vs. serif.
| Use | Font | Size | Weight | Notes |
|---|---|---|---|---|
| Display h1 | Inter | 56px from sm and up; 40px below |
700 | letter-spacing -0.04em |
| Section h2 | Inter | 28px | 700 | letter-spacing -0.03em |
| Subsection h3 | Inter | 18px | 600 | normal |
| Body prose | Inter | 16px | 400 | line-height 1.65 |
| Lede | Inter | 18px | 400 | opacity 0.85 |
| Mono data / inputs | JetBrains Mono | 13px | 400 | for standalone mono (inputs, result numbers, dates) |
| Mono labels / metadata | JetBrains Mono | 10–11px UPPERCASE | 400 | letter-spacing 0.12–0.16em |
Inline code in prose |
JetBrains Mono | 0.9em |
400 | scales with parent so it stays proportional |
If a piece of UI doesn't fit one of these, rethink the UI.
Two tokens per mode. No accent color. Ever.
| Token | Value | Used for |
|---|---|---|
--paper |
#f5f3ec |
page background |
--ink |
#161616 |
all text, dither, borders, button background |
--subtle |
#ebe7d4 |
inline code background only |
| Token | Value | Used for |
|---|---|---|
--paper |
#0f0e0c |
page background |
--ink |
#ede9d8 |
all text, dither, borders, button background |
--subtle |
#1c1a16 |
inline code background only |
"Muted" is --ink at 0.55 opacity. No other ink shades.
In globals.css the tokens carry the --color- prefix (--color-paper etc.) that Tailwind v4's @theme requires to generate bg-paper / text-ink utilities. Same tokens, two forms.
An experiment may bind a constellation hue to a named role through a scoped token. No new hue enters the language. The Penrose explorer does this:
| Token | Reuses | Role |
|---|---|---|
--color-penrose-thick |
--color-moment-1 (gold) |
thick rhombus fill |
--color-penrose-thin |
--color-moment-4 (teal) |
thin rhombus fill |
Grout reuses --color-paper; the pin ring reuses --color-ink. The only new value is a dark teal (#4f7d92) that --color-penrose-thin takes in dark mode, nudged lighter for contrast on the dark paper. thick is identical in both modes.
The visual signature. Three roles, strict territories, strict density.
- V1 emblem — experiment header decoration. One per experiment, SVG, ~96×96.
- V2 data viz — inside article body, where there's real data. Dither carries meaning. No data → no V2.
- V3 texture — page chrome outside content. Mood only.
- Pages with V2 get the lightest possible V3. Opacity
0.25→0.10, one corner instead of two. - At most three dither elements per screen. Emblem = 1, data viz = 1 (regardless of cells), each texture region = 1.
- V3 is opt-in per page. Landing generously. V2 pages minimally. Dense pages skip it.
- Dither is always
--ink, never muted. Opacity fades it; color stays solid.
- Emblem: inline SVG with
<pattern>+<circle>. No image assets. - Data viz: HTML/CSS grid, cells tagged
prime/toups,radial-gradientpaints the dots. - Texture: absolutely-positioned
<div>with a CSSradial-gradientfill and amask-imagegradient to fade it into the page edge. - No dithering library. No image assets. Everything uses
currentColororvar(--color-ink)so it inverts with the theme.
- Dither as wallpaper
- Dither inside forms or buttons
- Dither in headings
- Dither in any color other than
--ink - Multiple emblems per experiment
- Decorative dither that doesn't mean something
- Single column. Always.
- Body prose max-width ~60ch. Page max-width ~720px.
- Centered. 64px horizontal padding desktop, 24px mobile.
- Vertical rhythm: multiples of 8px. Section gaps 32–48px, paragraph 16px, form rows 10px.
hyphens: autoon prose via.prose-hyphens.
- No card-in-card.
- No drop shadows. Anywhere.
- No rounded corners.
border-radius: 0. - No hover state lifts. Hover = opacity bump or hairline border.
- No gradients except dither texture.
- Section dividers are hairline rules (
1px solid currentColor). No doubled rules.
- Bare inputs:
1pxink underline, mono font, no background, no box. - Labels above each field in single-column forms: mono, UPPERCASE, 10–11px, opacity
0.5. - Compact multi-column forms (e.g., name + date on one row) use a group heading + placeholder text instead, with
sr-onlylabels for screen readers. - Buttons: flat rectangles. Mono, lowercase. Primary = ink-on-paper inverted. Secondary = paper-on-ink inverted.
- No icon buttons except the toggle and row-delete
×.
- Sans for names and prose context. Mono for dates, numbers, data.
- Rows separated by hairline rules, not cards.
- Punchline numbers: bold mono, no background. Inline
codeonly for real code.
- Hover. Opacity
0.85→1.0, or fill transparent → ink at0.05. No movement. - Focus.
1px solidink outline,2pxoffset. Neveroutline: none. - Active. Fill at ink
0.1. - Disabled. Opacity
0.4,cursor: not-allowed.
Animation is otherwise not in the language. Teaching experiments are the one exception, under a hard contract.
- User-initiated only. Motion happens on an explicit play, step, or slider drag. Never on load, never ambient, never on scroll.
- Reduced-motion is honored. Under
prefers-reduced-motion, play is disabled and the sketch renders its representative end state, stationary. The query is read live, so a mid-session change takes effect. - Confined to teaching sketches. This applies to the framed sketch primitive inside a teaching experiment (the Penrose spine's
Sketchharness). It does not relax the no-animation rule for site chrome, forms, or data viz.
- State. Cookie
theme="light"|"dark". Absent ="dark". - Mechanism. Root layout reads the cookie via
cookies()and emits<html data-theme="…">in SSR. No client script, no flash. All routes are dynamic. - Toggle component. src/components/ThemeToggle.tsx. On click, writes cookie and
data-theme; subsequent renders pick up the cookie. - Glyph.
☾when light (click to go dark),☼when dark (click to go light).aria-labelmatches. - No "follow system". First visit = dark. Clear the cookie to reset.
src/components/FuncImpMark.tsx. Inline SVG, currentColor. Follows the theme automatically.
- MDX for experiment writeups
- Open Graph / social cards
- Tags, categories, search
- A second accent color (relaxed: an experiment may bind an existing constellation hue to a named role through a scoped token, as the Penrose explorer does; no new hue)
- Custom icon set beyond toggle and row-delete
× - Photography or non-dithered illustration
- Animation beyond focus and hover (relaxed: user-initiated motion inside a teaching sketch, under the "Teaching animation" contract above)
- A second typeface family
- Per-experiment custom styling (relaxed: scoped tokens that reuse existing constellation hues are allowed)
- Multi-column layouts, dashboards, dense tables
- Add an entry to src/app/x/page.tsx.
- Create
src/app/x/<slug>/page.tsx. Copy the Prime Moments skeleton. - V1 emblem? A small dithered SVG that represents the experiment. If nothing comes to mind in 60 seconds, skip it.
- V2 data viz? Only if there's real data.
- V3 texture? Only if there's no V2.
Type, color, layout, chrome, toggle — all fixed.
Origin story: docs/superpowers/specs/2026-04-10-design-language-design.md.