Chapter 09Build
Theme and styles
One file to adapt in order to dress a site. How schema tokens become classes, and why the client never picks a free-form colour.
Two files, two responsibilities
| File | Where | Contains | Changed |
|---|---|---|---|
src/styles/theme.css |
the site | The values: palette, type scale, weights, rhythm. | every site |
inline-core/styles/tokens.css |
the package | The mapping: one class per schema value, pointing at a variable. | never |
That separation is not cosmetic. A missing class in tokens.css and a token
accepted by the schema would produce valid content that does not display — a bug
invisible in review. So the two files live together, versioned together.
The site theme
This is the only file to adapt when dressing a new site. It defines variables only:
:root {
/* Palette — one schema token name, one variable */
--color-primary: #101828;
--color-secondary: #344054;
--color-muted: #667085;
--color-accent: #0b5cff;
--color-inverse: #ffffff;
--color-surface: #ffffff;
--color-surface-alt: #f7f8fa;
--color-border: #e4e7ec;
/* Type scale — one step per value of the “size” enum */
--size-xs: 0.75rem;
--size-sm: 0.875rem;
--size-base: 1rem;
--size-lg: 1.25rem;
--size-xl: 1.5rem;
--size-2xl: 2rem;
--size-3xl: 2.75rem;
/* Weights — one step per value of the “weight” enum */
--weight-thin: 100;
--weight-light: 300;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
--font-body: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--line-height: 1.5;
--space-1: 0.5rem;
--space-2: 1rem;
--space-3: 1.5rem;
--space-4: 2rem;
--space-6: 3rem;
--space-8: 4rem;
--measure: 48rem;
}Every colour, size and weight variable corresponds to a schema value. Removing one leaves a schema token with no effect: the client would pick “very large” and nothing would change. You can change the values, never the names.
The mapping
tokens.css turns each enum value into a class. No hard-coded values: only
variables, defined by the site.
/* size */
.cms-size-xs { font-size: var(--size-xs); }
.cms-size-base { font-size: var(--size-base); }
.cms-size-3xl { font-size: var(--size-3xl); }
/* weight */
.cms-weight-regular { font-weight: var(--weight-regular); }
.cms-weight-bold { font-weight: var(--weight-bold); }
/* italic, align, color */
.cms-italic { font-style: italic; }
.cms-align-center { text-align: center; }
.cms-color-accent { color: var(--color-accent); }The full path, from the content file to the screen:
And the other way round: the toolbar's “accent” button is built from the same list Zod draws its enum from. So a button offering a value the build would reject cannot exist — there are not two lists to keep in agreement.
Dressing a new site
-
Set the palette
Five content colours (
primary,secondary,muted,accent,inverse) and three interface colours (surface,surface-alt,border). That is few, and it is deliberate: five colours are chosen well, fifteen are chosen badly. -
Lay out the type scale
Seven steps, from
xsto3xl. Keep a constant ratio between steps rather than values picked one by one: that is what stops a page falling apart when the client changes a size. -
Choose the typeface
--font-body. A self-hosted font is declared here, with its@font-facerule:css css @font-face { font-family: 'Inter'; src: url('/fonts/inter.woff2') format('woff2'); font-weight: 100 900; font-display: swap; } :root { --font-body: 'Inter', system-ui, sans-serif; }Font files go in
public/fonts/— unlike content images, they do not go through<Image />and have no business insrc/media. -
Check with real content
npm run devis enough: layout work does not need the functions. Exercise every size step and every colour at least once — a token never used is a token never verified.
Layout
tokens.css also carries a minimal layout skeleton (.page,
.site-nav, .site-footer, .hero, .showcase,
.testimonials, .site-langs). It serves the shipped template and the
/admin and /aide pages, so a fresh site is not naked.
These classes are not an imposed framework. For a site-specific layout, write your own: in
theme.css after the import (specificity is identical, order decides), or as
scoped styles inside your Astro components — which is preferable, because the style stays
with the structure it dresses.
What the client can change
Clicking a text brings up a bar offering exactly five settings:
| Setting | What they see | What is written |
|---|---|---|
| Size | seven steps, smallest to largest | "size": "2xl" |
| Weight | six weights | "weight": "semibold" |
| Italic | a switch | "italic": true |
| Alignment | left, centre, right | "align": "center" |
| Colour | five swatches from their theme | "color": "accent" |
On a richtext field, the bar offers bold, italic, link and lists — and nothing
else: exactly the tags sanitising lets through.
A hex colour in the content. A size in pixels. A font pasted from a word processor. An
inline style="". All of those are either impossible to produce from the
interface, stripped by sanitising, or refused by the schema.
The client cannot break the look of the page, even on purpose.
Pasting from a word processor
This happens in the first week. A paragraph pasted from Word brings its fonts, its sizes in points, its colours and a pile of invisible tags. Paste cleaning overwrites everything, without exception: only the text, the bold and the italic survive.
Without it, the page ends up with Calibri 11 pt in the middle of the theme — and nobody notices before the client complains.
Adding a style token
Adding a value (a sixth colour, an eighth size step) happens in the package, in three places that must move together:
src/style-tokens.ts— the list, single source;styles/tokens.css— the matching class;- the theme of every site — the variable, otherwise the class points at nothing.
The schema draws its enums from the first file: the change is therefore an evolution of the content model, to be propagated to every site. Removing a value would invalidate published content using it: that is a major version, even if the code compiles.
Checks
- Every variable
tokens.cssexpects exists intheme.css. - The five content colours are legible on the surfaces where they appear.
inverseis legible on dark backgrounds — that is its only purpose.- The seven size steps stay visually ordered.
- Body text contrast reaches 4.5:1 (AA).
- The page fits a 360 px wide screen.