Reference
Style guide
Generated from src/config/theme.ts — every value below is the real token, not a copy.
Colour
Edit these in the light and dark objects insrc/config/theme.ts. Each one generates two CSS variables — the hex (--accent) and an RGB triplet (--accent-rgb) forrgba() use — plus Tailwind utilities like bg-accent andtext-muted/60.
Brand
Primary drives links, buttons and active states. Alt is a second accent — badges, charts, highlights. Never body text.
accent
light #57634e
dark #a3b096
accentHover
light #3b4536
dark #bcc7b1
accentAlt
light #9a7449
dark #c99f6d
accentAltHover
light #7a5b37
dark #dab588
Surfaces
Page and card backgrounds. `surfaceAlt` is for insets — form fields, code blocks, footers.
surface
light #fbfaf7
dark #161613
surfaceAlt
light #f2efe9
dark #1e1e1a
Text
Three weights of emphasis, plus one special case. Headings lead with `ink`, paragraphs sit on `body`, metadata recedes to `muted`. `onImage` is for copy over a photograph, where the background is no longer a known flat colour — pushed harder than `body` in both themes for exactly that reason.
ink
light #1b1a17
dark #f2f0ea
body
light #3d3b35
dark #d7d4cb
muted
light #6f6b61
dark #98948a
onImage
light #26241f
dark #fbfaf7
Structure & status
Hairlines, and the two feedback colours used by the contact form.
line
light #e2ded4
dark #2e2d28
success
light #3f6b46
dark #8fc39a
danger
light #9c3b30
dark #e39288
Rounding
Named for what they wrap, not for their size — so restyling the whole site's feel means editing four values. Available as rounded-button, rounded-input,rounded-card, rounded-panel and rounded-pill.
rounded-button
rounded-input
rounded-card
rounded-panel
rounded-pill
Typography
Configured in src/config/fonts.ts. Astro self-hosts and subsets each family, so there are no third-party font requests at runtime. Only the families registered there appear below — the template ships with one, and the heading andbrand roles fall back to it until you add your own.
body · Atkinson Hyperlegible · preloaded
Aa Bb Cc 123
--typeface-body
Heading 1
Heading 2
Heading 3
Heading 4
Choosing a typeface
A shortlist of Google Fonts that suit this template, kept in src/config/fonts.ts. All of them are open-licensed — free to use commercially and free to self-host, which is what Astro does with them at build time. Nothing is loaded from Google at runtime, so no visitor IP address is ever sent there.
Every sample below is set in the font it names — really loaded, not an image. That costs one file per family, so it happens on this page only: the shortlist is handed to the layout through fontVariables, and Astro emits the@font-face rules where they're rendered. No other page in the site requests any of them. Each is fetched at a single weight, which is enough to judge the letterforms and not enough to typeset with.
pairings
One face, two jobs
Handgloves & quartz
The quick brown fox jumps over the lazy dog — 0123456789, Il1 O0.
Atkinson Hyperlegiblefor headings ·Atkinson Hyperlegiblefor body
What's installed. Hierarchy comes from size, spacing and case instead of from a second typeface — the hardest version to get wrong, and the least likely to look dated in three years.
export const fonts: FontDefinition[] = [
{
role: 'body',
cssVariable: '--typeface-body',
provider: 'google',
family: 'Atkinson Hyperlegible',
weights: [400, 700],
fallbacks: ['system-ui', 'sans-serif'],
preload: true,
},
];Editorial
Handgloves & quartz
The quick brown fox jumps over the lazy dog — 0123456789, Il1 O0.
Instrument Seriffor headings ·Interfor body
A high-contrast display serif over a neutral sans. This is the combination behind most of the minimal hospitality and studio sites people point at as a reference.
export const fonts: FontDefinition[] = [
{
role: 'body',
cssVariable: '--typeface-body',
provider: 'google',
family: 'Inter',
weights: [400, 500, 600, 700],
fallbacks: ['system-ui', 'sans-serif'],
preload: true,
},
{
role: 'heading',
cssVariable: '--typeface-heading',
provider: 'google',
family: 'Instrument Serif',
weights: [400],
fallbacks: ['Georgia', 'serif'],
preload: false,
},
];Warm
Handgloves & quartz
The quick brown fox jumps over the lazy dog — 0123456789, Il1 O0.
Frauncesfor headings ·Work Sansfor body
Same structure, softer voice. Fraunces has enough character to carry a heading on its own, so the sans underneath can stay completely plain.
export const fonts: FontDefinition[] = [
{
role: 'body',
cssVariable: '--typeface-body',
provider: 'google',
family: 'Work Sans',
weights: [400, 500, 700],
fallbacks: ['system-ui', 'sans-serif'],
preload: true,
},
{
role: 'heading',
cssVariable: '--typeface-heading',
provider: 'google',
family: 'Fraunces',
weights: [400, 600],
fallbacks: ['Georgia', 'serif'],
preload: false,
},
];Bookish
Handgloves & quartz
The quick brown fox jumps over the lazy dog — 0123456789, Il1 O0.
Newsreaderfor headings ·Newsreaderfor body
Serif throughout, at two sizes. Suits a site that is mostly writing — and it means one family to load rather than two.
export const fonts: FontDefinition[] = [
{
role: 'body',
cssVariable: '--typeface-body',
provider: 'google',
family: 'Newsreader',
weights: [400, 500, 600],
fallbacks: ['Georgia', 'serif'],
preload: true,
},
];the shortlist
What this template ships with. Designed by the Braille Institute to keep letterforms distinguishable at small sizes — b/d, I/l/1 and O/0 are all unmistakable. Only two weights.
The default neutral UI sans. Enormously capable and completely unopinionated, which is both why it works everywhere and why a site set in it looks like every other site.
Neutral, slightly squarer than Inter, drawn for the US government design system. A good body face when the headings are doing the talking.
A grotesque with slightly warmer, less mechanical curves. Sits well under a serif heading without going cold.
A text serif built for reading on screen. One of the few serifs as comfortable in body copy at 18px as it is in a heading.
A classical book serif. Runs small for its nominal size, so set it a step larger than you would a sans.
A sturdy workhorse serif with a touch of calligraphic contrast. Holds up in long-form text better than most Google serifs.
A variable soft-serif with real personality — wonky, warm, and adjustable for optical size. Excellent large, busy small.
High-contrast, tightly spaced, one weight only. The current shorthand for expensive-looking minimalism — which also means it will date as that look does.
Very fine, very high contrast, very elegant. Falls apart below about 24px — hairlines that thin disappear on a low-density screen.
The classic luxury-brand serif. Effective and instantly legible as a signal, and so widely used that it reads as a category rather than a choice.
Geometric and deliberately strange, from the art world. A wordmark face — never set a paragraph in it.
For code blocks. Tall x-height and clearly distinct l/1/I. Only needed if you replace the CSS monospace stack with a real font.
suits is the part worth paying attention to. A high-contrast display serif set as body copy is the most common way a site with good taste in fonts ends up hard to read — and each weight you list is another file, so take only the ones you'll use.
Buttons
<Button> renders an <a> when givenhref and a <button> otherwise, so a link that looks like a button is still a real link.
Cards
Static card
A plain div. No hover on the title, because it doesn't go anywhere.
Link card
Given href, renders as an anchor and highlights its title on hover.
With a badge
For a category, a status, or a short callout.
With texture
Pass texture to add the dot grid. Off by default.
No icon
The icon is optional.
Custom content
The default slot replaces description when you need more than a line of text.
Article cards
<ArticleCard> — a picture, a title and a description, in three layouts. None of them draws a box: the image supplies the edge, and the space around the text does the rest. All three collapse to text on their own when a post has noheroImage, and none of them stacks on a phone — narrow the window to see it.
layout="stacked" — for a grid, two-up on a phone

Welcome to the starter
What ships in this template, what to change first, and where the reference documentation lives.

Using components inside MDX
An MDX post that imports and renders real Astro components alongside its prose.

Deploying to Cloudflare Pages
The short version of getting this template onto a real domain, with D1 and Pages Functions wired up.
layout="row" — a single-column list, picture still worth looking at
layout="list" — dense, ruled. For an archive or a sidebar

Welcome to the starter
What ships in this template, what to change first, and where the reference documentation lives.

Using components inside MDX
An MDX post that imports and renders real Astro components alongside its prose.

Deploying to Cloudflare Pages
The short version of getting this template onto a real domain, with D1 and Pages Functions wired up.
Motion
Two reveal mechanisms. Load-in is pure CSS and fires on paint — use it above the fold. Scroll uses an IntersectionObserver — use it below. Both are switched off entirely under prefers-reduced-motion.
loadIn
distance 14px
duration 0.8s
easing cubic-bezier(0.16, 1, 0.3, 1)
scroll
distance 10px
duration 0.55s
easing cubic-bezier(0.16, 1, 0.3, 1)
stagger 80ms
staggered scroll reveal (index 0-3)
Layout
Content measures, available on <Section width="..."> and as Tailwindmax-w-* utilities.
Prose
The .prose-copy class, used for long-form content and rendered Markdown.
A heading inside prose
A paragraph with a link, some bold text, and a bit ofinline code.
A blockquote, marked with a left rule in the accent colour.
- An unordered list item
- Another one
- An ordered list item
- Another one
const tokens = themeCss();