Repository navigation
Design Tokens
This page is the single source of truth for every design value on the website: colour, type, space, radius, shadow, motion, layout and stacking order. It gives the tokens in the W3C Design Tokens format, the CSS custom properties they become, the Tailwind v4 @theme block and the mapping onto Fumadocs UI's variables.
Other pages in Design System explain when to use each token. If a value elsewhere disagrees with this page, this page wins and the other page is a bug.
- Edit the token in
website/tokens/, using a value from a mockup the owner approved, and record the choice in the decision log on Design System. - Copy the value into
website/app/global.cssin the same pull request. CSS names match token names one to one (color.text-2→--wc-text-2). - Tailwind utilities and Fumadocs UI both read the CSS variables, so components need no change.
- Brand files made in Canva use the same hex values, typed in by hand (Brand).
Nothing converts the JSON to CSS: a converter such as Style Dictionary would be a new dependency. Proposed: a dependency-free Node script, website/scripts/check-tokens.mjs, that resolves the JSON and fails CI if global.css disagrees.
| File | Holds |
|---|---|
website/tokens/base.tokens.json |
Primitive colours, type, space, radius, motion, layout, z-index; the same in light and dark |
website/tokens/light.tokens.json |
Semantic colours and the popover shadow, light |
website/tokens/dark.tokens.json |
The same names, dark |
website/tokens/workclock.resolver.json |
Merges base with light or dark |
website/app/global.css |
The CSS the site loads (Fumadocs' default location) |
The four tokens/ paths are Proposed. The JSON follows the Design Tokens Format Module 2025.10, with colours as in the Color module and light/dark as a "theme" modifier of the Resolver module.
| Kind | Pattern | Example | Used in components |
|---|---|---|---|
| Primitive colour | palette.<hue>.<step> |
palette.neutral.920 |
Never; primitives only feed semantic tokens |
| Semantic colour | color.<role> |
color.text-2 |
Always, as var(--wc-text-2) or text-text-2
|
| Everything else | <group>.<name> |
radius.lg, duration.base
|
Through the CSS variable or utility below |
The colour step is (100 − CIELAB L*) × 10, rounded to the nearest 5: neutral.0 is white, neutral.960 the darkest grey, and a new grey slots in by its lightness.
Roles and contrast ratios are on Color.
| Token | Light | Dark | CSS variable | Tailwind utility |
|---|---|---|---|---|
color.bg |
#FFFFFF |
#0B0E14 |
--wc-bg |
bg-bg |
color.surface |
#F6F7F9 |
#121620 |
--wc-surface |
bg-surface |
color.raised |
#FFFFFF |
#1A1F2B |
--wc-raised |
bg-raised |
color.rule |
#E3E6EB |
#242B37 |
--wc-rule |
border-rule |
color.rule-strong |
#CDD2DA |
#343C4A |
--wc-rule-strong |
border-rule-strong |
color.text |
#0F1729 |
#E8ECF2 |
--wc-text |
text-text |
color.text-2 |
#4A5363 |
#A3ACBA |
--wc-text-2 |
text-text-2 |
color.text-3 |
#6B7484 |
#8A93A3 |
--wc-text-3 |
text-text-3, border-text-3
|
color.work |
#DBE2EC |
#1D2532 |
--wc-work |
bg-work, fill-work
|
color.used |
= text
|
= text
|
--wc-used |
bg-used, fill-used
|
color.accent |
#C27C00 |
#FFB224 |
--wc-accent |
bg-accent, border-accent, fill-accent (graphics only) |
color.accent-text |
#8F5A00 |
#FFB224 |
--wc-accent-text |
text-accent-text |
color.focus |
#1F5BD8 |
#7AA2FF |
--wc-focus |
outline-focus |
color.link |
= text
|
= text
|
--wc-link |
global a style |
color.link-underline |
= rule-strong
|
= rule-strong
|
--wc-link-underline |
global a style |
color.link-underline-hover |
= accent
|
= accent
|
--wc-link-underline-hover |
global a style |
color.success |
#0E7A55 |
#4CC38A |
--wc-success |
text-success |
color.danger |
#C8331F |
#FF6B5A |
--wc-danger |
text-danger |
color.info |
#1F5BD8 |
#7AA2FF |
--wc-info |
text-info |
color.warning |
= accent
|
= accent
|
--wc-warning |
border-warning |
color.warning-text |
= accent-text
|
= accent-text
|
--wc-warning-text |
text-warning-text |
color.overlay |
rgb(15 23 41 / 0.2) |
rgb(0 0 0 / 0.5) |
--wc-overlay |
through Fumadocs (--color-fd-overlay) |
color.logo-dot |
#D98A00 |
#FFB224 |
none | Logo files only, never CSS |
color.overlay is the dimmed layer behind the search dialog and other dialogs.
| Group | Values | CSS / Tailwind | Details |
|---|---|---|---|
font.family |
sans Instrument Sans, mono JetBrains Mono |
--font-sans, --font-mono
|
Typography |
font.weight |
400, 500, 600 |
font-normal, font-medium, font-semibold
|
Typography |
font.size |
12, 13, 14, 16, 18, 20, 24, 30, 38, 48, 60 px | role utilities such as text-body, text-h1
|
Typography |
font.leading |
1.1, 1.25, 1.3, 1.5, 1.6, 1.7 | set with each --text-* size |
Typography |
font.tracking |
−0.035, −0.03, −0.02, −0.01, 0 em | set with each --text-* size |
Typography |
space |
0, 4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96 px | Tailwind steps 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24 | Layout and Spacing |
radius |
none 0, sm 4, md 6, lg 8, xl 12, full
|
rounded-none … rounded-xl, rounded-full (Tailwind's defaults) |
Shape Elevation and Motion |
border.width |
1 px | border |
Shape Elevation and Motion |
shadow.popover |
in the theme files |
--wc-shadow-popover → shadow-popover
|
Shape Elevation and Motion |
duration |
fast 100, base 160, slow 240 ms |
--wc-duration-*; duration-100, duration-160, duration-240
|
Shape Elevation and Motion |
easing.standard |
cubic-bezier(0.2, 0, 0, 1) |
--wc-ease, ease-standard
|
Shape Elevation and Motion |
layout |
header 64, sidebar 280, prose 640, toc 220, page 1400, landing 1200, gutter 16 px |
--wc-header … --wc-gutter
|
Layout and Spacing |
z |
base 0, sticky 10, sidebar 20, header 30, drawer 40, popover 50, skip-link 60 |
z-0 … z-60
|
Layout and Spacing |
Values match the tables above. Hex values are lowercase in the JSON, as in the Color module's examples.
{
"$schema": "https://www.designtokens.org/schemas/2025.10/resolver.json",
"version": "2025.10",
"name": "workclock website",
"sets": {
"base": { "sources": [{ "$ref": "base.tokens.json" }] }
},
"modifiers": {
"theme": {
"description": "Light or dark: the visitor's OS setting, or the toggle",
"contexts": {
"light": [{ "$ref": "light.tokens.json" }],
"dark": [{ "$ref": "dark.tokens.json" }]
},
"default": "light"
}
},
"resolutionOrder": [
{ "$ref": "#/sets/base" },
{ "$ref": "#/modifiers/theme" }
]
}{
"color": {
"$type": "color",
"bg": {"$value": "{palette.neutral.0}", "$description": "Page"},
"surface": {"$value": "{palette.neutral.30}", "$description": "Active sidebar item, code blocks, demo frame"},
"raised": {"$value": "{palette.neutral.0}", "$description": "Popovers, search dialog"},
"rule": {"$value": "{palette.neutral.90}", "$description": "Hairline borders, dividers, grid lines"},
"rule-strong": {"$value": "{palette.neutral.160}", "$description": "Focused table rows, link underlines, hover borders"},
"text": {"$value": "{palette.neutral.920}", "$description": "Body and headings"},
"text-2": {"$value": "{palette.neutral.650}", "$description": "Secondary text, nav"},
"text-3": {"$value": "{palette.neutral.515}", "$description": "Captions, placeholders, form-control borders"},
"work": {"$value": "{palette.neutral.105}", "$description": "Working-hour bars"},
"used": {"$value": "{color.text}", "$description": "Counted time in diagrams"},
"accent": {"$value": "{palette.amber.420}", "$description": "Due marker, active tab, current page, warning callouts; graphics only"},
"accent-text": {"$value": "{palette.amber.570}", "$description": "Amber that must be read as text"},
"focus": {"$value": "{palette.blue.575}", "$description": "Focus rings"},
"link": {"$value": "{color.text}"},
"link-underline": {"$value": "{color.rule-strong}"},
"link-underline-hover": {"$value": "{color.accent}"},
"success": {"$value": "{palette.green.550}"},
"danger": {"$value": "{palette.red.545}", "$description": "Errors, danger callouts"},
"info": {"$value": "{palette.blue.575}", "$description": "Note callouts"},
"warning": {"$value": "{color.accent}", "$description": "Warning callout bar and icon"},
"warning-text": {"$value": "{color.accent-text}"},
"overlay": {"$value": {"colorSpace": "srgb", "components": [0.0588, 0.0902, 0.1608], "alpha": 0.2, "hex": "#0f1729"}, "$description": "Behind dialogs"},
"logo-dot": {"$value": "{palette.amber.360}", "$description": "Logo files only"}
},
"shadow": {
"$type": "shadow",
"popover": {"$value": [{"color": {"colorSpace": "srgb", "components": [0.0588, 0.0902, 0.1608], "alpha": 0.06, "hex": "#0f1729"}, "offsetX": {"value": 0, "unit": "px"}, "offsetY": {"value": 1, "unit": "px"}, "blur": {"value": 2, "unit": "px"}, "spread": {"value": 0, "unit": "px"}}, {"color": {"colorSpace": "srgb", "components": [0.0588, 0.0902, 0.1608], "alpha": 0.08, "hex": "#0f1729"}, "offsetX": {"value": 0, "unit": "px"}, "offsetY": {"value": 8, "unit": "px"}, "blur": {"value": 24, "unit": "px"}, "spread": {"value": 0, "unit": "px"}}], "$description": "Popovers only"}
}
}{
"color": {
"$type": "color",
"bg": {"$value": "{palette.neutral.960}", "$description": "Page"},
"surface": {"$value": "{palette.neutral.925}", "$description": "Active sidebar item, code blocks, demo frame"},
"raised": {"$value": "{palette.neutral.880}", "$description": "Popovers, search dialog"},
"rule": {"$value": "{palette.neutral.825}", "$description": "Hairline borders, dividers, grid lines"},
"rule-strong": {"$value": "{palette.neutral.750}", "$description": "Focused table rows, link underlines, hover borders"},
"text": {"$value": "{palette.neutral.65}", "$description": "Body and headings"},
"text-2": {"$value": "{palette.neutral.300}", "$description": "Secondary text, nav"},
"text-3": {"$value": "{palette.neutral.395}", "$description": "Captions, placeholders, form-control borders"},
"work": {"$value": "{palette.neutral.855}", "$description": "Working-hour bars"},
"used": {"$value": "{color.text}", "$description": "Counted time in diagrams"},
"accent": {"$value": "{palette.amber.220}", "$description": "Due marker, active tab, current page, warning callouts; graphics only"},
"accent-text": {"$value": "{palette.amber.220}", "$description": "Amber that must be read as text"},
"focus": {"$value": "{palette.blue.325}", "$description": "Focus rings"},
"link": {"$value": "{color.text}"},
"link-underline": {"$value": "{color.rule-strong}"},
"link-underline-hover": {"$value": "{color.accent}"},
"success": {"$value": "{palette.green.290}"},
"danger": {"$value": "{palette.red.360}", "$description": "Errors, danger callouts"},
"info": {"$value": "{palette.blue.325}", "$description": "Note callouts"},
"warning": {"$value": "{color.accent}", "$description": "Warning callout bar and icon"},
"warning-text": {"$value": "{color.accent-text}"},
"overlay": {"$value": {"colorSpace": "srgb", "components": [0, 0, 0], "alpha": 0.5, "hex": "#000000"}, "$description": "Behind dialogs"},
"logo-dot": {"$value": "{palette.amber.220}", "$description": "Logo files only"}
},
"shadow": {
"$type": "shadow",
"popover": {"$value": [{"color": {"colorSpace": "srgb", "components": [0, 0, 0], "alpha": 0.5, "hex": "#000000"}, "offsetX": {"value": 0, "unit": "px"}, "offsetY": {"value": 8, "unit": "px"}, "blur": {"value": 24, "unit": "px"}, "spread": {"value": 0, "unit": "px"}}], "$description": "Popovers only; dark popovers also keep their 1px rule border"}
}
}Show the file (146 lines)
{
"palette": {
"$type": "color",
"neutral": {
"0": {"$value": {"colorSpace": "srgb", "components": [1, 1, 1], "hex": "#ffffff"}},
"30": {"$value": {"colorSpace": "srgb", "components": [0.9647, 0.9686, 0.9765], "hex": "#f6f7f9"}},
"65": {"$value": {"colorSpace": "srgb", "components": [0.9098, 0.9255, 0.949], "hex": "#e8ecf2"}},
"90": {"$value": {"colorSpace": "srgb", "components": [0.8902, 0.902, 0.9216], "hex": "#e3e6eb"}},
"105": {"$value": {"colorSpace": "srgb", "components": [0.8588, 0.8863, 0.9255], "hex": "#dbe2ec"}},
"160": {"$value": {"colorSpace": "srgb", "components": [0.8039, 0.8235, 0.8549], "hex": "#cdd2da"}},
"300": {"$value": {"colorSpace": "srgb", "components": [0.6392, 0.6745, 0.7294], "hex": "#a3acba"}},
"395": {"$value": {"colorSpace": "srgb", "components": [0.5412, 0.5765, 0.6392], "hex": "#8a93a3"}},
"515": {"$value": {"colorSpace": "srgb", "components": [0.4196, 0.4549, 0.5176], "hex": "#6b7484"}},
"650": {"$value": {"colorSpace": "srgb", "components": [0.2902, 0.3255, 0.3882], "hex": "#4a5363"}},
"750": {"$value": {"colorSpace": "srgb", "components": [0.2039, 0.2353, 0.2902], "hex": "#343c4a"}},
"825": {"$value": {"colorSpace": "srgb", "components": [0.1412, 0.1686, 0.2157], "hex": "#242b37"}},
"855": {"$value": {"colorSpace": "srgb", "components": [0.1137, 0.1451, 0.1961], "hex": "#1d2532"}},
"880": {"$value": {"colorSpace": "srgb", "components": [0.102, 0.1216, 0.1686], "hex": "#1a1f2b"}},
"920": {"$value": {"colorSpace": "srgb", "components": [0.0588, 0.0902, 0.1608], "hex": "#0f1729"}},
"925": {"$value": {"colorSpace": "srgb", "components": [0.0706, 0.0863, 0.1255], "hex": "#121620"}},
"960": {"$value": {"colorSpace": "srgb", "components": [0.0431, 0.0549, 0.0784], "hex": "#0b0e14"}}
},
"amber": {
"220": {"$value": {"colorSpace": "srgb", "components": [1, 0.698, 0.1412], "hex": "#ffb224"}},
"360": {"$value": {"colorSpace": "srgb", "components": [0.851, 0.5412, 0], "hex": "#d98a00"}, "$description": "Logo dot in light mode only. Fails 3:1 on white, so never used on the site."},
"420": {"$value": {"colorSpace": "srgb", "components": [0.7608, 0.4863, 0], "hex": "#c27c00"}},
"570": {"$value": {"colorSpace": "srgb", "components": [0.5608, 0.3529, 0], "hex": "#8f5a00"}}
},
"blue": {
"325": {"$value": {"colorSpace": "srgb", "components": [0.4784, 0.6353, 1], "hex": "#7aa2ff"}},
"575": {"$value": {"colorSpace": "srgb", "components": [0.1216, 0.3569, 0.8471], "hex": "#1f5bd8"}}
},
"green": {
"290": {"$value": {"colorSpace": "srgb", "components": [0.298, 0.7647, 0.5412], "hex": "#4cc38a"}},
"550": {"$value": {"colorSpace": "srgb", "components": [0.0549, 0.4784, 0.3333], "hex": "#0e7a55"}}
},
"red": {
"360": {"$value": {"colorSpace": "srgb", "components": [1, 0.4196, 0.3529], "hex": "#ff6b5a"}},
"545": {"$value": {"colorSpace": "srgb", "components": [0.7843, 0.2, 0.1216], "hex": "#c8331f"}}
}
},
"font": {
"family": {
"$type": "fontFamily",
"sans": {"$value": ["Instrument Sans", "ui-sans-serif", "system-ui", "sans-serif"]},
"mono": {"$value": ["JetBrains Mono", "ui-monospace", "SFMono-Regular", "Menlo", "monospace"]}
},
"weight": {
"$type": "fontWeight",
"regular": {"$value": 400},
"medium": {"$value": 500},
"semibold": {"$value": 600}
},
"size": {
"$type": "dimension",
"12": {"$value": {"value": 12, "unit": "px"}},
"13": {"$value": {"value": 13, "unit": "px"}},
"14": {"$value": {"value": 14, "unit": "px"}},
"16": {"$value": {"value": 16, "unit": "px"}},
"18": {"$value": {"value": 18, "unit": "px"}},
"20": {"$value": {"value": 20, "unit": "px"}},
"24": {"$value": {"value": 24, "unit": "px"}},
"30": {"$value": {"value": 30, "unit": "px"}},
"38": {"$value": {"value": 38, "unit": "px"}},
"48": {"$value": {"value": 48, "unit": "px"}},
"60": {"$value": {"value": 60, "unit": "px"}}
},
"leading": {
"$type": "number",
"tight": {"$value": 1.1, "$description": "H1, display"},
"snug": {"$value": 1.25, "$description": "H2"},
"heading": {"$value": 1.3, "$description": "H3"},
"ui": {"$value": 1.5, "$description": "UI text, captions"},
"code": {"$value": 1.6},
"prose": {"$value": 1.7, "$description": "Docs body text"}
},
"tracking": {
"$type": "number",
"$description": "Letter spacing in em. DTCG dimensions allow only px and rem, so these are plain numbers.",
"display": {"$value": -0.035},
"h1": {"$value": -0.03},
"h2": {"$value": -0.02},
"h3": {"$value": -0.01},
"normal": {"$value": 0}
}
},
"space": {
"$type": "dimension",
"0": {"$value": {"value": 0, "unit": "px"}},
"4": {"$value": {"value": 4, "unit": "px"}},
"8": {"$value": {"value": 8, "unit": "px"}},
"12": {"$value": {"value": 12, "unit": "px"}},
"16": {"$value": {"value": 16, "unit": "px"}},
"20": {"$value": {"value": 20, "unit": "px"}},
"24": {"$value": {"value": 24, "unit": "px"}},
"32": {"$value": {"value": 32, "unit": "px"}},
"40": {"$value": {"value": 40, "unit": "px"}},
"48": {"$value": {"value": 48, "unit": "px"}},
"64": {"$value": {"value": 64, "unit": "px"}},
"80": {"$value": {"value": 80, "unit": "px"}},
"96": {"$value": {"value": 96, "unit": "px"}}
},
"radius": {
"$type": "dimension",
"none": {"$value": {"value": 0, "unit": "px"}},
"sm": {"$value": {"value": 4, "unit": "px"}, "$description": "Inline code, badges"},
"md": {"$value": {"value": 6, "unit": "px"}, "$description": "Buttons, inputs, callouts"},
"lg": {"$value": {"value": 8, "unit": "px"}, "$description": "Code blocks, cards"},
"xl": {"$value": {"value": 12, "unit": "px"}, "$description": "Demo frame, dialogs"},
"full": {"$value": {"value": 9999, "unit": "px"}, "$description": "Pills"}
},
"border": {
"$type": "dimension",
"width": {"$value": {"value": 1, "unit": "px"}}
},
"duration": {
"$type": "duration",
"fast": {"$value": {"value": 100, "unit": "ms"}},
"base": {"$value": {"value": 160, "unit": "ms"}},
"slow": {"$value": {"value": 240, "unit": "ms"}}
},
"easing": {
"$type": "cubicBezier",
"standard": {"$value": [0.2, 0, 0, 1]}
},
"layout": {
"$type": "dimension",
"header": {"$value": {"value": 64, "unit": "px"}},
"sidebar": {"$value": {"value": 280, "unit": "px"}},
"prose": {"$value": {"value": 640, "unit": "px"}},
"toc": {"$value": {"value": 220, "unit": "px"}},
"page": {"$value": {"value": 1400, "unit": "px"}},
"landing": {"$value": {"value": 1200, "unit": "px"}},
"gutter": {"$value": {"value": 16, "unit": "px"}, "$description": "Minimum side gutter on phones"}
},
"z": {
"$type": "number",
"base": {"$value": 0},
"sticky": {"$value": 10},
"sidebar": {"$value": 20},
"header": {"$value": 30},
"drawer": {"$value": 40},
"popover": {"$value": 50},
"skip-link": {"$value": 60}
}
}Notes on the format:
- Letter spacing is in em, but a DTCG
dimensionallows onlypxandrem, so tracking is stored asnumbertokens with the unit in the group's$description. -
{…}values are aliases. The resolver merges base and one theme file before resolving them, so a theme file may point atpalette.*in the base file.
Light values sit on :root, dark values on .dark. Fumadocs UI's RootProvider wraps the site in next-themes with attribute: "class", defaultTheme: "system" and disableTransitionOnChange: true, so class="dark" lands on <html> when the OS or the toggle asks for dark (Fumadocs: Themes; fumadocs-ui 16.17.8, dist/provider/base.js). Fumadocs' preset.css defines Tailwind's dark: variant on the same class. Don't set --wc-* variables on inner elements. Keep suppressHydrationWarning on <html>, as next-themes requires.
/* website/app/global.css — part 1: tokens */
@import 'tailwindcss';
@import 'fumadocs-ui/css/preset.css';
/* No Fumadocs colour preset (neutral.css and friends): part 3 sets every --color-fd-* itself. */
:root {
--wc-bg: #FFFFFF;
--wc-surface: #F6F7F9;
--wc-raised: #FFFFFF;
--wc-rule: #E3E6EB;
--wc-rule-strong: #CDD2DA;
--wc-text: #0F1729;
--wc-text-2: #4A5363;
--wc-text-3: #6B7484;
--wc-work: #DBE2EC;
--wc-used: var(--wc-text);
--wc-accent: #C27C00;
--wc-accent-text: #8F5A00;
--wc-focus: #1F5BD8;
--wc-link: var(--wc-text);
--wc-link-underline: var(--wc-rule-strong);
--wc-link-underline-hover: var(--wc-accent);
--wc-success: #0E7A55;
--wc-danger: #C8331F;
--wc-info: #1F5BD8;
--wc-warning: var(--wc-accent);
--wc-warning-text: var(--wc-accent-text);
--wc-overlay: rgb(15 23 41 / 0.2);
--wc-shadow-popover: 0 1px 2px rgb(15 23 41 / 0.06), 0 8px 24px rgb(15 23 41 / 0.08);
--wc-duration-fast: 100ms;
--wc-duration-base: 160ms;
--wc-duration-slow: 240ms;
--wc-ease: cubic-bezier(0.2, 0, 0, 1);
--wc-header: 64px;
--wc-sidebar: 280px;
--wc-prose: 640px;
--wc-toc: 220px;
--wc-page: 1400px;
--wc-landing: 1200px;
--wc-gutter: 16px;
}
.dark {
--wc-bg: #0B0E14;
--wc-surface: #121620;
--wc-raised: #1A1F2B;
--wc-rule: #242B37;
--wc-rule-strong: #343C4A;
--wc-text: #E8ECF2;
--wc-text-2: #A3ACBA;
--wc-text-3: #8A93A3;
--wc-work: #1D2532;
--wc-accent: #FFB224;
--wc-accent-text: #FFB224;
--wc-focus: #7AA2FF;
--wc-success: #4CC38A;
--wc-danger: #FF6B5A;
--wc-info: #7AA2FF;
--wc-overlay: rgb(0 0 0 / 0.5);
--wc-shadow-popover: 0 8px 24px rgb(0 0 0 / 0.5);
}Aliases (--wc-used, --wc-link, --wc-warning …) are written once in :root and follow the dark values automatically.
Tailwind v4 makes utilities from theme variables in known namespaces: --color-* gives bg-*, text-*, border-*, fill-*; --text-* gives font sizes with optional --line-height, --letter-spacing and --font-weight companions; --font-*, --ease-*, --shadow-* likewise (Tailwind: Theme variables, font-size). Variables that point at other variables go in @theme inline, so the utility uses var(--wc-…) directly and follows .dark.
/* website/app/global.css — part 2: Tailwind theme */
@theme inline {
--color-bg: var(--wc-bg);
--color-surface: var(--wc-surface);
--color-raised: var(--wc-raised);
--color-rule: var(--wc-rule);
--color-rule-strong: var(--wc-rule-strong);
--color-text: var(--wc-text);
--color-text-2: var(--wc-text-2);
--color-text-3: var(--wc-text-3);
--color-work: var(--wc-work);
--color-used: var(--wc-used);
--color-accent: var(--wc-accent);
--color-accent-text: var(--wc-accent-text);
--color-focus: var(--wc-focus);
--color-success: var(--wc-success);
--color-danger: var(--wc-danger);
--color-info: var(--wc-info);
--color-warning: var(--wc-warning);
--color-warning-text: var(--wc-warning-text);
--font-sans: var(--font-instrument-sans), ui-sans-serif, system-ui, sans-serif;
--font-mono: var(--font-jetbrains-mono), ui-monospace, SFMono-Regular, Menlo, monospace;
--shadow-popover: var(--wc-shadow-popover);
}
@theme {
--text-caption: 0.75rem;
--text-caption--line-height: 1.5;
--text-code: 0.8125rem;
--text-code--line-height: 1.6;
--text-ui: 0.875rem;
--text-ui--line-height: 1.5;
--text-body: 1rem;
--text-body--line-height: 1.7;
--text-lead: 1.125rem;
--text-lead--line-height: 1.6;
--text-h3: 1.25rem;
--text-h3--line-height: 1.3;
--text-h3--letter-spacing: -0.01em;
--text-h3--font-weight: 600;
--text-h2: 1.5rem;
--text-h2--line-height: 1.25;
--text-h2--letter-spacing: -0.02em;
--text-h2--font-weight: 600;
--text-section: 1.875rem;
--text-section--line-height: 1.25;
--text-section--letter-spacing: -0.02em;
--text-section--font-weight: 600;
--text-h1: 2.375rem;
--text-h1--line-height: 1.1;
--text-h1--letter-spacing: -0.03em;
--text-h1--font-weight: 600;
--text-display-sm: 2.375rem;
--text-display-sm--line-height: 1.1;
--text-display-sm--letter-spacing: -0.035em;
--text-display-sm--font-weight: 600;
--text-display-md: 3rem;
--text-display-md--line-height: 1.1;
--text-display-md--letter-spacing: -0.035em;
--text-display-md--font-weight: 600;
--text-display: 3.75rem;
--text-display--line-height: 1.1;
--text-display--letter-spacing: -0.035em;
--text-display--font-weight: 600;
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
--default-transition-duration: 160ms;
--default-transition-timing-function: cubic-bezier(0.2, 0, 0, 1);
}Deliberately not in @theme:
-
Spacing, radius and breakpoints. Tailwind's defaults already match:
--spacing: 0.25rem,--radius-sm…--radius-xl= 4, 6, 8, 12 px, breakpoints 640 to 1536 px (tailwindcss4.3.3,theme.css). -
Tailwind's colour palette and
text-xs…text-6xl. Fumadocs UI's classes use them, so they stay defined. Our components use only the role utilities (text-body, nottext-base;text-text-2, nottext-gray-600).
Fumadocs UI colours every component through --color-fd-* variables (Fumadocs: Themes, colors). Since we import no colour preset, part 3 defines the full set that fumadocs-ui 16.17.8 declares in css/lib/default-colors.css. @theme static makes Tailwind emit them even when no utility uses them, which matters because Fumadocs also reads them directly (base styles, callout inline styles).
Most mappings are one to one (background → bg, border → rule, info → info …). The ones that are not obvious:
-
--color-fd-muted-foreground→text-2, nottext-3, which fails 4.5:1 onsurfacein light mode. Fumadocs uses it for nav, TOC and code block titles. -
--color-fd-card→surface. Fumadocs also paints the sidebar with it; part 4 sets the sidebar back tobg. -
--color-fd-primary→text. Fumadocs uses it for primary buttons, active nav and TOC text and highlighted code lines; amber there would spread to buttons. -
--color-fd-accent→rule. Despite the name it is a hover background, not our amber. -
--color-fd-idea→info. Fumadocs' default is orange, which would read as amber; idea callouts are not used. - Diff backgrounds are
successanddangerat 12%.
/* website/app/global.css — part 3: Fumadocs UI colours */
@theme static {
--color-fd-background: var(--wc-bg);
--color-fd-foreground: var(--wc-text);
--color-fd-muted: var(--wc-surface);
--color-fd-muted-foreground: var(--wc-text-2);
--color-fd-popover: var(--wc-raised);
--color-fd-popover-foreground: var(--wc-text);
--color-fd-card: var(--wc-surface);
--color-fd-card-foreground: var(--wc-text);
--color-fd-border: var(--wc-rule);
--color-fd-primary: var(--wc-text);
--color-fd-primary-foreground: var(--wc-bg);
--color-fd-secondary: var(--wc-surface);
--color-fd-secondary-foreground: var(--wc-text);
--color-fd-accent: var(--wc-rule);
--color-fd-accent-foreground: var(--wc-text);
--color-fd-ring: var(--wc-focus);
--color-fd-overlay: var(--wc-overlay);
--color-fd-info: var(--wc-info);
--color-fd-warning: var(--wc-warning);
--color-fd-error: var(--wc-danger);
--color-fd-success: var(--wc-success);
--color-fd-idea: var(--wc-info);
--color-fd-diff-add: color-mix(in srgb, var(--wc-success) 12%, transparent);
--color-fd-diff-add-symbol: var(--wc-success);
--color-fd-diff-remove: color-mix(in srgb, var(--wc-danger) 12%, transparent);
--color-fd-diff-remove-symbol: var(--wc-danger);
}Fumadocs marks the current page and the active tab with --color-fd-primary, which is ink for us. These rules add the amber markers, keep the sidebar on bg, and set the one focus style. They sit outside any @layer, so they win over Tailwind's @layer utilities regardless of specificity (MDN: @layer). The selectors were read from fumadocs-ui 16.17.8 (#nd-sidebar, data-active, Radix role="tab" with data-state); they are not a documented API, so check them after every Fumadocs upgrade.
/* website/app/global.css — part 4: overrides (un-layered) */
/* Sidebar sits on the page background, not on the card grey. */
#nd-sidebar {
--color-fd-card: var(--wc-bg);
}
/* Current page: surface background, medium weight, 2px amber bar on the start edge. */
#nd-sidebar a[data-active='true'] {
background-color: var(--wc-surface);
color: var(--wc-text);
font-weight: 500;
box-shadow: inset 2px 0 0 var(--wc-accent);
border-start-start-radius: 0;
border-end-start-radius: 0;
}
/* Active tab: amber underline instead of ink. */
[role='tab'][data-state='active'] {
border-bottom-color: var(--wc-accent);
}
/* Links: ink, underlined. */
.prose a {
color: var(--wc-link);
text-decoration-line: underline;
text-decoration-color: var(--wc-link-underline);
text-underline-offset: 4px;
}
.prose a:hover {
text-decoration-color: var(--wc-link-underline-hover);
}
/* One focus style everywhere; switch off Fumadocs' ring so there is no double ring. */
:focus-visible {
outline: 2px solid var(--wc-focus);
outline-offset: 2px;
--tw-ring-shadow: 0 0 #0000;
}The current-page bar is an inset box-shadow so the text does not shift. It is a 2 px bar, not a shadow, so the no-shadows rule does not apply.
| Do | Don't |
|---|---|
text-text-2, bg-surface, border-rule
|
text-gray-600, bg-[#F6F7F9], border-neutral-200
|
border-text-3 on a text input |
border-rule-strong on a text input (below 3:1) |
fill-accent on the due marker |
text-accent on words (use text-accent-text) |
rounded-lg on a card |
rounded-[10px] |
duration-160 ease-standard |
duration-300 ease-in-out |
var(--wc-work) in an inline style or SVG |
#DBE2EC |
- Run the site in light and dark mode; look at a docs page, the landing demo and the search dialog.
- Recompute any contrast ratio the change touches and update Color.
- If it is a brand colour, update the Canva files and exports listed on Brand.
-
Proposed: the
website/tokens/file paths and thecheck-tokens.mjsCI check.
workclock is MIT licensed. The repository documentation is authoritative; this wiki summarises it.