# Koval Core A theme builder for Astro and Tailwind CSS v4. It is not a component library and not an npm package. It produces two files, and those two files are the whole product: - `global.css` — six brand colors and a fluid type scale. A build input. Replace the project's stylesheet with it. Never load it into context; nothing in it is reading material. - `theme-rules.md` — spacing, radius, elevation, borders, motion, icons and fonts as plain Tailwind conventions. Goes in the project root, pointed at from `AGENTS.md`. **This is the file you read.** It is about 2,700 tokens and it is written for you. The conventions are classes you type, not tokens you set. There are no custom utilities to learn and nothing overrides Tailwind. That is why a project can be described in a markdown file instead of a config. ## Which path to take Two ways to use this, and they are not equivalent. **1. The MCP server — `@kovaldev/core-mcp`.** The complete toolset: it describes the vocabulary, reads a plain-language brief into ranked candidates per axis, derives a six-color palette from one anchor and repairs it against real contrast obligations, searches 2,096 Fontsource families and 193 monotone Iconify sets, validates a config, and emits both files plus the install line. Prefer it whenever you can add an MCP server: ```json { "mcpServers": { "kovalCore": { "command": "npx", "args": [ "-y", "@kovaldev/core-mcp" ] } } } ``` Call `describe_system` first — it hands over the vocabulary the other tools expect. **2. This URL.** Everything below works with nothing but HTTP. The one thing it cannot do is search the font and icon catalogues: you name a family and the resolver accepts it or refuses it with a reason. That is the whole difference. ## The procedure, over HTTP ### 1. Choose a config Every field is optional and every one has a default. A field you do not understand is a field to leave out. ```json { "t": { "--brand-primary": "oklch(L C H)" }, "g": { "mood": "…", "harmony": "…", "secondary": "…", "danger": "…" }, "y": { "headingFont": "…", "bodyFont": "…", "monoFont": "…", "ratioMax": 1.25 }, "c": { "borders": "…", "radius": "…", "elevation": "…", "density": "…", "container": "…", "motion": "…" }, "n": "ph" } ``` - `t` — colors. **Give one: `--brand-primary`.** With `g` alongside it, the other five are derived by hue geometry and then repaired against contrast obligations (`text-dark/55` on the ground has to clear 4.5:1). Picking six OKLCH values by hand is the one part of a theme you should never do — those guarantees are not recoverable afterwards. `g` without an anchor is refused rather than guessed at. - `g` — how the other five follow from the anchor. - `y` — the type scale. `headingFont` / `bodyFont` / `monoFont` take a Fontsource id: the lowercase hyphenated form, `source-sans-3`, not `Source Sans 3`. Any of the 2,096 families works, not just the presets. Omit for system fonts, which install nothing. - `c` — the conventions. This is the part that stops you being asked about corner radius every session. - `n` — an Iconify collection prefix, e.g. `ph` for Phosphor. Monotone sets only: a colored set cannot inherit `currentColor`, so a theme could never reach it. ### 2. Encode it and fetch the two files base64url of the compact JSON — standard base64 with `-`/`_` and no padding: ```bash curl https://core.koval.dev/r/.md # theme-rules.md curl https://core.koval.dev/r/.css # global.css ``` A worked example. This is a real payload, encoded from a real config when this page was built, so both URLs answer right now: ```json { "tokens": { "--brand-primary": "oklch(0.45 0.13 264)" }, "gen": { "mood": "dusk", "harmony": "analogous" }, "type": { "headingFont": "fraunces", "bodyFont": "inter", "ratioMax": 1.333 }, "conventions": { "radius": "sharp", "borders": "hairline", "motion": "snappy" }, "icons": "ph" } ``` ``` https://core.koval.dev/r/eyJ0Ijp7Ii0tYnJhbmQtcHJpbWFyeSI6Im9rbGNoKDAuNDUgMC4xMyAyNjQpIn0sInkiOnsiaGVhZGluZ0ZvbnQiOiJmcmF1bmNlcyIsImJvZHlGb250IjoiaW50ZXIiLCJyYXRpb01heCI6MS4zMzN9LCJjIjp7InJhZGl1cyI6InNoYXJwIiwiYm9yZGVycyI6ImhhaXJsaW5lIiwibW90aW9uIjoic25hcHB5In0sImciOnsibW9vZCI6ImR1c2siLCJoYXJtb255IjoiYW5hbG9nb3VzIn0sImwiOltdLCJuIjoicGgifQ.md https://core.koval.dev/r/eyJ0Ijp7Ii0tYnJhbmQtcHJpbWFyeSI6Im9rbGNoKDAuNDUgMC4xMyAyNjQpIn0sInkiOnsiaGVhZGluZ0ZvbnQiOiJmcmF1bmNlcyIsImJvZHlGb250IjoiaW50ZXIiLCJyYXRpb01heCI6MS4zMzN9LCJjIjp7InJhZGl1cyI6InNoYXJwIiwiYm9yZGVycyI6ImhhaXJsaW5lIiwibW90aW9uIjoic25hcHB5In0sImciOnsibW9vZCI6ImR1c2siLCJoYXJtb255IjoiYW5hbG9nb3VzIn0sImwiOltdLCJuIjoicGgifQ.css ``` A malformed field comes back as a 400 naming the field — never as a silent fall back to the default. If you get a 400, read it: it says which field and what the valid values are. A 200 is the theme you asked for. ### 3. Wire it into the project 1. Write `global.css` over the project stylesheet (`src/styles/global.css`). 2. Write `theme-rules.md` in the project root. 3. Add the block below to `AGENTS.md` (or the equivalent file for your tool). 4. Do what the **Fonts** and **Icons** sections of `theme-rules.md` say — they carry the `astro.config.mjs` block and the `npm i` line for exactly the theme you fetched. 5. Read `theme-rules.md` before writing any markup. ```markdown ## House style This project uses a Koval Core theme. Both files are generated — do not hand-edit them; re-export instead. - Design rules: https://core.koval.dev/r/.md - Color and type tokens: https://core.koval.dev/r/.css Read the rules before writing markup. They are conventions over stock Tailwind utilities: nothing overrides Tailwind, and there are no custom utilities to learn. ``` Other tools, same instruction: `CLAUDE.md`, `.cursor/rules/theme.mdc`, `.github/copilot-instructions.md`. Keep one copy, not four — a second copy is a second house style waiting to drift. `/theme` hands out all four ready to paste. **Prefer the files over the links.** A file in the repo is in context every session; a link costs a tool call and can be skipped. The URLs are for sharing a theme, and for the record of what a client's theme is. ## The vocabulary These are the conventions. Every id below is checked against this list — a value outside it is a 400, not a default. ### borders — Do surfaces get outlines? Default: `none` - `none` — Surfaces separate by sitting on adjacent near-tones. No hairlines. - `hairline` — A 1px ink border at 12% on cards and inputs. ### radius — How round is the shape language? Default: `soft` - `sharp` — Square corners everywhere. - `subtle` — Barely rounded — 2px on controls, 4px on surfaces. - `soft` — Rounded — 6px on controls, 12px on surfaces. - `round` — Generous — pill buttons, 16px surfaces. ### elevation — Do raised surfaces cast shadows? Default: `none` - `none` — No shadows. Depth comes from grounds. - `resting` — `shadow-sm` on raised surfaces; nothing on hover. - `lift` — `shadow-sm` at rest, `shadow-md` on hover. ### density — How much air between things? Default: `default` - `compact` — Dense — app-like, more on screen. - `default` — Balanced — the usual marketing-site rhythm. - `roomy` — Editorial — lots of air, fewer things per screen. ### container — How wide does content run? Default: `standard` - `narrow` — 64rem — editorial, long-form. - `standard` — 80rem — the usual marketing width. - `wide` — 96rem — dashboards, galleries. ### motion — How fast do hover states move? Default: `standard` - `none` — Instant state changes. - `snappy` — 150ms, decelerating. - `standard` — 200ms, decelerating. - `relaxed` — 300ms, decelerating. ### Palette generation (`g`) - `mood` — signature | soft | vivid | dusk - `harmony` — split | analogous | triad | complement - `secondary` — sibling | foil - `danger` — keyed | pinned Danger is the only status color. There is no success and no warning: a palette earns a hue when a project actually needs one. ### Type (`y`) - `ratioMin` / `ratioMax` — the modular ratio at each end of the viewport. A smaller ratio at the small end is what compresses hierarchy on narrow screens: 1.067, 1.125, 1.2, 1.25, 1.333, 1.414, 1.5, 1.618. - `baseMin` / `baseMax` — body size in rem. `steps` — how many heading levels. - Font presets, for a quick choice — any Fontsource id works, these are only the ones the builder lists: system, inter, outfit, manrope, figtree, plus-jakarta-sans, work-sans, space-grotesk, source-sans-3, fraunces, playfair-display, newsreader, lora, system-mono, jetbrains-mono, fira-code. ## Workflows **A new project.** Start from `koval-dev/astro-starter`. Build a theme, write both files, paste the wiring block, follow the Fonts and Icons sections. The starter does not ship a Core theme — replacing its stylesheet is the step that makes it one. **Re-theming an existing project.** Replace `global.css` and `theme-rules.md`, then rebuild. Nothing else moves: the conventions are Tailwind classes already sitting in the markup, and the six colors are aliased through `var()`, so the whole ink ladder follows the brand values rather than being baked against them. **Several clients.** The `/r/` URL is the record of what a theme is. Keep it in the project's `AGENTS.md`; the payload is the identity, so the same link can only ever produce the same bytes. **Extending Core.** The vocabulary is deliberately small. When a project needs a word it does not have, that belongs in Core rather than in the project — a one-off utility beside these conventions is the second house style all of this exists to prevent. ## What not to do - Do not hand-edit either generated file. Re-export instead. - Do not invent opacity steps. The ladder is `70 / 55 / 40 / 22 / 12` and nothing else. - Do not add `Container`, `Stack`, `Grid` or `Section` components. Page structure is stock Tailwind utilities in the markup; the conventions fix the vocabulary, not the composition. - Do not name a font family in markup. `font-heading`, `font-body`, `font-mono`. - Do not reach for `text-2xl` or `font-bold` on text that has a role utility. ## Pages - https://core.koval.dev/ — what Core is, and how to use it - https://core.koval.dev/theme — the builder, for a person tuning by hand - https://core.koval.dev/foundations — color, typography, shape, and the system model - https://core.koval.dev/components — the two primitives, Button and ButtonLink - https://core.koval.dev/patterns — worked compositions