<!-- audience: AI coding agents first — optimize for activation, not prose aesthetics -->
# mod-cms — Visual Design System

Default editorial design for the mod-cms blueprint. Opinionated, non-SaaS, non-AI-default. An agent that violates any rule here has shipped a defect.

> **One instance of the canonical MOD design system.** The platform-wide doctrine (the layered model, theme-agnostic constraints, anti-slop floor, marketplace forward-target) is `docs/standards/mod-design-system.md` — canonical, dogfooding-scoped. THIS file is mod-cms's concrete instance: the warm-paper Fraunces + Schibsted Grotesk palette below is **one theme**, not the constraint. Constraints live up there; pick values down here. Where the two differ, the canonical doc wins (e.g. the hero: the constraint is "headline never depends on the photo for legibility" — mod-cms's no-image editorial hero is one valid satisfaction, not the rule).

---

## §1 Color tokens (OKLCH only)

NEVER hex, NEVER HSL, NEVER rgb(). One violation fails review. All values below are final — do not approximate.

```css
:root {
  /* Background */
  --color-bg:        oklch(97% 0.008 80);   /* warm paper */
  --color-surface:   oklch(94% 0.010 80);   /* card / inset surface */

  /* Ink */
  --color-ink:       oklch(18% 0.015 60);   /* near-black warm */
  --color-ink-soft:  oklch(42% 0.015 60);   /* secondary text */
  --color-ink-faint: oklch(68% 0.010 60);   /* disabled / metadata */

  /* Structure */
  --color-line:      oklch(84% 0.012 80);   /* borders, dividers */

  /* Accent — ink-blue, single use per visible section */
  --color-accent:    oklch(40% 0.150 255);  /* links, active states, focus rings */
  --color-accent-bg: oklch(96% 0.025 255);  /* accent-tinted surface (hover bg only) */

  /* Danger — reserved for destructive confirmation ONLY */
  --color-danger:    oklch(45% 0.180 25);   /* delete/remove confirm, never decorative */
}
```

Contrast floor: `--color-ink` on `--color-bg` ≥ 12:1. Verify with Huetone or equivalent — never eyeball.

`--color-accent` appears at most ONCE per visible viewport section. If two accent-colored elements are simultaneously visible in the same section, one must be changed.

`--color-danger` reserved exclusively for destructive action confirmation (delete button, remove prompt). Never use it for warning messages, error copy, or decorative purposes.

---

## §2 Typography

### Typefaces

| Role | Face | Source |
|------|------|--------|
| Display / section openings | **Fraunces** (Italic, weight 300–700) | Google Fonts |
| Body / UI | **Schibsted Grotesk** (Regular 400, Medium 500) | Google Fonts |
| Metadata / spec tables / code | **JetBrains Mono** (Regular 400) | Google Fonts |

### Banned faces — automatic fail if present in font-family

Inter, Geist, system-ui as primary, Roboto, DM Sans, Plus Jakarta Sans, Open Sans, Nunito, Poppins, Montserrat, Segoe UI, -apple-system, BlinkMacSystemFont.

`ui-sans-serif` and `system-ui` are permitted ONLY as final fallbacks in the Schibsted Grotesk stack, never as the named face.

### Type scale

Non-adjacent sizes only — never use two consecutive steps on the same screen. Skip at least one step between hierarchy levels.

| Token | Size | Line-height | Tracking | Use |
|-------|------|-------------|----------|-----|
| `--text-display` | 4rem (64px) | 1.05 | −0.03em | Page hero h1 only |
| `--text-heading` | 2.25rem (36px) | 1.15 | −0.02em | Section h2 |
| `--text-subhead` | 1.375rem (22px) | 1.3 | −0.01em | Card titles, module names |
| `--text-body` | 1rem (16px) | 1.65 | 0 | Body copy, form labels |
| `--text-small` | 0.8125rem (13px) | 1.5 | 0.01em | Metadata, tags, captions |
| `--text-mono` | 0.875rem (14px) | 1.6 | 0 | Code, spec tables, token labels |

Fraunces: `--text-display` and `--text-heading` ONLY. Never at `--text-body` or smaller.
Schibsted Grotesk: `--text-subhead`, `--text-body`, `--text-small`, all UI labels.
JetBrains Mono: `--text-mono` ONLY. Never decoratively. Never at display scale.

---

## §3 Spacing

Single 8px baseline. Only the values listed below are permitted. Any other value is a defect.

`8 · 16 · 24 · 32 · 40 · 48 · 64 · 80 · 96 · 128 · 160 · 192px`

Do NOT use: 10, 12, 20, 28, 36, 44, 56, 72, or any other value.

Section gap (between unrelated sections): minimum 96px. Target 128px on desktop.
Group gap (elements within same logical group): maximum 48px.
Label-to-input gap: 8px. Always.
Eyebrow-to-headline gap: 8px, eyebrow BELOW the headline — not above.

---

## §4 Layout grid

12 columns, 1320px max-width, centered container, 24px column gutters.

**Default content zone: columns 2–9.** Content does not span full 12 columns on desktop.
**Wide content (tables, graphs): columns 1–10 maximum.**
**Full-bleed: background washes only** — no text content at full bleed.

Left-anchor rule: every page MUST have at least one section where primary content occupies columns 2–8 with columns 9–12 empty. If every section fills the grid edge-to-edge, the layout rule is broken.

Headlines never exceed column 9. A headline that stretches to column 12 is a defect.

---

## §5 Components

Three primitives. One canonical form each. Same markup, same tokens, every page. Never invent a fourth primitive for a single context.

### Card

Surface: `--color-surface`. Border: 1px solid `--color-line`. Radius: 2px. Padding: 24px.

No box-shadow on default state. No box-shadow on hover. State change on hover: border-color → `--color-accent`, background → `--color-accent-bg`. Transition: border-color 120ms ease.

Structure (in order): title at `--text-subhead` Schibsted Grotesk Medium, optional tagline at `--text-small` ink-soft, optional metadata row at `--text-mono` ink-faint.

DO NOT: add drop shadows, lift effects, scale transforms, or border-radius larger than 2px.

### Field

Label: `--text-small` Schibsted Grotesk Medium, `--color-ink`, above the input, 8px gap.
Input: 1px solid `--color-line`, 2px radius, padding 8px 12px, `--text-body`, `--color-ink`.
Focus: outline 2px solid `--color-accent`, offset 2px. Never change border on focus alone.
Error: inline prose below input, `--text-small`, `--color-danger`, 8px gap. Not a banner. Not a toast.

DO NOT: use placeholder text as labels. Never.

### Action (button)

Primary: background `--color-accent`, text `--color-bg`, `--text-body` Schibsted Grotesk Medium. Padding 10px 20px. Radius 2px. No shadow. Hover: background lightness +5%.

Secondary (text link only): `--color-accent`, underline on hover. No ghost button outline variants.

Exactly ONE primary action per screen. If a screen shows two filled buttons, one must become a text link.

Label: verb + object ("Publish post", "Delete entry"). Not bare verbs ("Submit", "Confirm") where context is ambiguous.

---

## §6 Per-page principles

**index**
Left column holds the hero. Hero = eyebrow identifier (8px below h1, not above) + h1 at `--text-display` Fraunces + body paragraph Schibsted Grotesk. No stacked CTAs. No image behind text. No full-width section fills. Module cards occupy columns 2–9 in a fixed column count (3 or 4) — never `auto-fill` or `minmax()`. Section gap between hero and modules: 128px minimum.

**blueprint/[slug]**
Dependency graph is the dominant element — it occupies the wide zone (columns 1–10). Module list below in columns 2–9. Never render the graph and module list side by side on mobile. Graph empty state: "No modules configured." + one action to add a module.

**module/[slug]**
Capability spec table is the primary content — left-anchored, JetBrains Mono for values. Description paragraph at `--text-body` above the table, columns 2–8. Dependency graph below the table in wide zone. No decorative icons in capability names.

**post/[slug]**
Body text columns 2–8. Line-height 1.65. `--text-body` Schibsted Grotesk. Headings in post body: Fraunces at `--text-heading`. No sidebar. No related-posts grid. No social share buttons. Date in `--text-small` ink-faint above the title.

**ui-kit**
Token reference table only. Left-anchored. Swatches are 24×24px squares, no radius. Token names in JetBrains Mono. No hero. No marketing copy on this page.

**404**
One sentence. One Action button. Columns 2–6. No illustration. No search box. No "you might also like" section. Sentence describes what happened declaratively — "That page doesn't exist." Not "Oops, we lost that one!"

---

## §7 Empty states

One sentence. One action. No illustration. No icons. Written like a person.

Pattern: "[This thing] hasn't been added yet." + primary Action button.

Examples:
- "No posts published yet." + "Write the first post"
- "This blueprint has no modules configured." + "Add a module"
- "No blueprints found." + "Create a blueprint"

DO NOT: use illustrations, decorative icons, loading spinners that persist, centered stacked layouts with image → headline → body → two CTAs. That pattern is the anti-pattern.

---

## §8 Loading / skeleton states

Skeleton blocks match the final layout geometry exactly. Same dimensions, same grid position.

Color: `--color-surface`. No shimmer animation. No pulse animation. Static.

A skeleton for a Card is a rectangle at Card dimensions. A skeleton for a heading is a rectangle at 60% of the heading's expected width (left-anchored).

DO NOT: use "Loading…" text unless the skeleton geometry is genuinely impossible to implement. DO NOT: use spinner icons for inline content loading.

---

## §9 Anti-patterns — automatic fail

Any screen that contains ONE of the following is rejected. Check explicitly before shipping.

- **Centered hero with stacked CTAs** — two buttons center-aligned below a centered headline. Always a fail.
- **Bento card grid** — `auto-fill`, `minmax()`, or `repeat(auto-fill, …)` for content cards. Always a fail. Use a fixed column count.
- **Shadow lift on hover** — `box-shadow` added or intensified on card hover. Always a fail.
- **Mixed radius language** — any element using a radius other than 2px. Always a fail.
- **Banned typeface** — any font from the §2 banned list appearing in font-family anywhere. Always a fail.
- **Hex/HSL/rgb in CSS** — any color value not in OKLCH. Always a fail.
- **Emoji in product UI** — in any heading, label, button, empty state, error message, or navigation. Always a fail.
- **Decorative icon in heading or label** — Lucide, Heroicons, or equivalent used as visual decoration in a heading, section opener, label, or navigation item. Always a fail.
- **"Trusted by" or social proof strip** — logos, avatars, testimonials, or star ratings. Always a fail.
- **Placeholder text as label** — input where the label is only visible as placeholder text. Always a fail.
- **Two primary actions on one screen** — two filled/accent-colored buttons simultaneously visible on one page. Always a fail.
- **Gradient backgrounds** — linear-gradient, radial-gradient on any UI surface. Always a fail.
- **"Oops!" copy** — any error message starting with "Oops", "Uh oh", "Whoops", or similar informal exclamations. Always a fail.
- **Full-width headlines** — any h1 or h2 that spans all 12 columns on desktop. Always a fail.

---

## §10 Quality bar — wordmark test

Cover the logo with your hand (or remove it from the viewport). Ask: does this screen still read as a specific, opinionated product from typography, rhythm, and alignment alone?

Pass criteria:
- Fraunces + Schibsted Grotesk pairing is immediately recognizable as a deliberate editorial choice
- Left-anchored grid with clear empty columns to the right reads as confident, not timid
- Near-black ink on warm paper reads as print-quality, not screen-default
- No section could be swapped into a SaaS dashboard unchanged

Fail indicators:
- Screen could belong to any Tailwind UI starter — identical visual rhythm, neutral grays, centered layout
- Typography could be Inter — nothing signals a deliberate type selection
- Every section touches the grid edges — no breathing room, no editorial pacing

If the wordmark test fails, the screen is not finished regardless of whether individual rules pass.
