Plain+ Design Specification v1.0
A design system for interfaces that don't look AI-generated. Premium through restraint, not decoration. The HTML is the design.
LLM-generated UIs have developed a recognizable aesthetic: rounded corners, soft gradients, purple-to-blue color ramps, geometric sans-serif fonts, centered card layouts, decorative shadows. These are individually fine choices. Collectively, they're a fingerprint.
Plain+ is a deliberate counter-aesthetic. It starts from the premise that unformatted HTML — a browser's default rendering of semantic markup — already communicates structure clearly. The job of the stylesheet is not to add decoration but to refine what's already there: tune the type, control the measure, establish rhythm, and get out of the way.
The "plus" is the gap between a raw index.html and one that feels like it was set by someone who knows typography. That gap is crossed with:
Nothing else. No motion. No texture. No visual effects. The premium is in what you don't see.
Core test: If someone screenshots the artifact and posts it, could a viewer confidently say "an AI made this"? If yes, something needs to be removed.
Use this to decide whether an artifact should use Plain+, the default bold aesthetic, or something custom.
Is the artifact content-forward?
Content-forward = the user came for information, data, or a tool. The interface is a vehicle, not the experience. Examples: documentation, dashboards, settings panels, reference tables, forms, CLIs with web frontends, admin UIs.
Experience-forward = the interface itself is the point. The user came to be engaged, persuaded, or entertained. Examples: landing pages, marketing sites, games, creative showcases, portfolios.
Content-forward → Does it need to feel "designed"?
Yes, visually rich: Use the default frontend-design skill. Dashboards with data visualization, consumer-facing tools where visual polish is a feature.
No, it should feel transparent: Use Plain+. The user should focus on content, not chrome. Docs, admin panels, dev tools, internal tools, reference pages.
Experience-forward → What kind?
Marketing / persuasion / portfolio: Use the default frontend-design skill with a bold direction.
Game / interactive art / experiment: Custom aesthetic, designed from scratch for the concept.
Still unsure?
Ask: "Would serif body text feel wrong here?" If serif would feel stuffy or out of place (e.g., a music app, a social feed), Plain+ probably isn't the right fit. If serif would feel authoritative and natural (e.g., a settings page, a data table, a writing tool), Plain+ is the move.
Every value in Plain+ is defined as a CSS custom property. These are the atomic units — nothing else is used.
Minor third ratio (1.2), fluid between viewport sizes using clamp().
| Token | Role | Min | Max |
|---|---|---|---|
--step--1 | Captions, labels, hints | 0.80rem | 0.89rem |
--step-0 | Body text (base) | 1.00rem | 1.06rem |
--step-1 | Lede, h3 | 1.20rem | 1.35rem |
--step-2 | h2 | 1.44rem | 1.69rem |
--step-3 | Large display (rare) | 1.73rem | 2.11rem |
--step-4 | h1, page title | 2.07rem | 2.64rem |
| Token | Value (Light) | Value (Dark) | Usage |
|---|---|---|---|
--ink | #1a1a1a | #e8e6e3 | Primary text, headings |
--ink-light | #555 | #aaa | Secondary text, lede |
--ink-faint | #999 | #777 | Labels, captions, markers |
--surface | #fdfdfc | #1a1a19 | Page background |
--rule | #d4d0cb | #333 | Borders, dividers |
--accent | #b44 | #d66 | Flourish, interactive states |
--code-bg | #f4f3f1 | #242422 | Code blocks, disabled inputs |
--highlight-bg | #fef9f0 | #2a2520 | Accent notices |
The accent can be swapped per-artifact. Always muted, always singular.
| Token | Value | Usage |
|---|---|---|
--space | 1.6rem | Base unit; paragraph margin, padding |
--space × 0.25 | 0.4rem | Tight: below h3, between label and input |
--space × 0.5 | 0.8rem | Below h2 |
--space × 2 | 3.2rem | Section breaks, above h3, page padding |
--space × 3 | 4.8rem | Above h2 (major sections) |
| Context | Value | ≈ Characters |
|---|---|---|
| Default (reading) | 38em | ~65 chars |
| Spec / documentation | 42em | ~72 chars |
| Wide (dashboards) | 60em | Content blocks still 38em |
| Full (data tables) | 72em | Prose still capped at 38em |
| Token | Stack | Usage |
|---|---|---|
--font-body | Charter → Bitstream Charter → Iowan Old Style → Palatino Linotype → Palatino → Georgia → Cambria → serif | Body text, blockquotes, input values |
--font-heading | -apple-system → Helvetica Neue → Segoe UI → sans-serif | Headings, labels, buttons, UI chrome |
--font-mono | SF Mono → Cascadia Code → JetBrains Mono → Consolas → monospace | Code, token values |
This is the single most important design decision in Plain+. Body text is set in a serif (Charter by preference) and headings in the system sans-serif. The contrast between the two creates visual hierarchy without any decoration — no size tricks, no color changes, no weight extremes. Just two different voices.
Why this matters as an anti-tell: virtually every LLM-generated UI uses a geometric or grotesque sans for body text (Inter, Roboto, system-ui). Choosing a serif immediately places the output in a different visual category. It reads as editorial, considered, human.
| Level | Size | Weight | Spacing | Decoration |
|---|---|---|---|---|
| h1 | --step-4 | 600 | letter-spacing: -0.025em | None |
| h2 | --step-2 | 600 | letter-spacing: -0.01em | 1px bottom rule |
| h3 | --step-1 | 600 | letter-spacing: -0.01em | None |
| h4 | --step-0 | 600 | Default | None |
Note: only h2 gets a bottom rule. This is deliberate — it marks major section boundaries without cluttering sub-sections.
Lede — the first paragraph after h1. Larger, lighter color. Sets the tone.
This is a lede paragraph. It introduces the page at a larger size and in a softer ink, creating a gentle ramp from the title into the body text.
Caption / label — small, uppercase, tracked. Uses the heading font.
This is a caption or label
Inline emphasis — italic for aside, bold for stress, code for values.
Plain+ uses warm neutrals throughout. The ink is not pure black (#000) but a very dark warm gray (#1a1a1a). The surface is not pure white (#fff) but a barely perceptible warm off-white (#fdfdfc). The rules are not cool gray but a warm stone tone (#d4d0cb).
This warmth is subtle — most people won't consciously notice it — but it's the difference between "screen" and "paper." It contributes to the sense that a human set this type.
The accent color appears in exactly three places maximum:
body::before)If you find the accent in a fourth place, remove it from somewhere.
| Color | Why it's banned |
|---|---|
| Purple (#7c3aed, #8b5cf6, etc.) | The #1 Claude/AI-UI color. Instantly recognizable. |
| Teal (#14b8a6, #06b6d4, etc.) | The #2 AI-UI color. Common in ChatGPT-adjacent design. |
| Blue-to-purple gradients | The gradient direction itself is a tell. |
| Startup blue (#2563eb, #3b82f6) | Reads as generic SaaS / landing page. |
| Pure black (#000) | Too harsh. Use --ink instead. |
| Pure white (#fff) | Only for input backgrounds. Use --surface for page background. |
All vertical spacing is a multiple of --space (1.6rem). This creates a consistent visual rhythm down the page. The key spacings:
| Between | Space | Value |
|---|---|---|
| Paragraphs | 1× | 1.6rem |
| h3 → content | 0.25× | 0.4rem |
| h2 → content | 0.5× | 0.8rem |
| Content → h3 | 2× | 3.2rem |
| Content → h2 | 3× | 4.8rem |
| Section break (hr) | 2× above and below | 3.2rem |
| Page top/bottom padding | 2× | 3.2rem |
The asymmetry matters: more space above a heading than below it. This binds the heading to its content rather than floating equidistant between sections.
Square, bordered, uppercase small text. The anti-pill.
| State | Border | Background | Text | Cursor |
|---|---|---|---|---|
| Default | 1.5px --ink | transparent | --ink | pointer |
| Hover | 1.5px --ink | --ink | --surface | pointer |
| Focus-visible | 1.5px --ink | transparent | --ink | — (2px --accent outline, 2px offset) |
| Active | 1.5px --ink | --ink | --surface | pointer (opacity 0.85) |
| Disabled | 1.5px --rule | transparent | --ink-faint | not-allowed (opacity 0.5) |
| State | Border | Background | Text |
|---|---|---|---|
| Default | 1px --rule | white / #222 (dark) | --ink |
| Focus | 1px --ink | white / #222 (dark) | --ink |
| Error | 1px --accent | white / #222 (dark) | --ink |
| Disabled | 1px --rule | --code-bg | --ink-faint |
No zebra striping. No cell backgrounds. Structure comes from the header weight and thin row rules.
| Feature | Treatment |
|---|---|
| Header row | Uppercase, tracked, 2px bottom border in --ink |
| Body rows | 1px bottom border in --rule |
| Row hover | None (optional: subtle --code-bg background on interactive tables) |
| Alignment | Left by default. Right-align numbers. Top-align multi-line cells. |
| Cell padding | 0.5em vertical, 0.75em horizontal |
Default notice. Uses --code-bg background with a 2px left border in --ink-faint. For neutral information.
Accent notice. Uses --highlight-bg with a 2px left border in --accent. For warnings, important notes, or key callouts. Use sparingly — one per section maximum.
Links are same-color as body text with a light underline that shifts to the accent on hover. This is intentional — colored links (especially blue ones) would introduce a second color into the palette and break the single-accent rule. The underline alone signals interactivity.
Inline code has a border and tinted background. Code blocks have the same treatment at block scale:
function greet(name) {
return `Hello, ${name}.`;
}
// Plain+ code blocks: no syntax highlighting by default.
// If highlighting is needed, use a muted, warm palette —
// not the typical saturated VS Code colors.
A blockquote is indented with a 2px left rule and set in italic at a lighter ink weight. It's for attributions, asides, and quoted material — not for callouts (use the notice component for those).
These are pre-solved layout patterns. When building a Plain+ artifact, pick the closest archetype and adapt it. Don't invent new layout approaches — the system works because it's consistent.
Single column, centered, --measure width. For articles, specs, documentation, READMEs. This page is an example.
For settings panels, navigation + detail views, admin layouts. Fixed sidebar width, fluid content.
For comparisons, side-by-side data, split views. Equal columns.
Full-width table with filter/search controls above it. Controls are left-aligned, inline.
For metrics, KPIs, monitoring. Three columns of equal width. No card containers — use thin rules or whitespace to separate cells.
Single column, narrow (24em max). Labels above inputs. Stacked vertically with --space between fields. Submit button left-aligned below.
These are non-negotiable. If any of them are broken, the artifact is not Plain+.
| # | Rule | Why |
|---|---|---|
| 1 | No border-radius | The #1 AI-generated UI tell. Square = intentional. |
| 2 | No gradients | Flat or nothing. Gradients signal "design effort" in the wrong direction. |
| 3 | No box-shadow | Depth from hierarchy, not effects. |
| 4 | No purple, teal, or startup blue | These are AI-UI signature colors. |
| 5 | Serif body text | Breaks the sans-serif monoculture of AI output. |
| 6 | System sans for headings only | The contrast is the design system. |
| 7 | Whitespace over containers | Space separates; boxes decorate. |
| 8 | Thin rules over thick borders | 1px communicates; 3px decorates. |
| 9 | One accent, ≤ 3 uses | Discipline, not decoration. |
| 10 | Warm neutrals | "Paper" not "screen." |
When building a React artifact with Plain+, inject the design tokens and font stacks via a <style> block inside the component. Then use Tailwind utilities that reference them.
export default function App() {
return (
<>
<style>{`
:root {
--ink: #1a1a1a;
--ink-light: #555;
--ink-faint: #999;
--surface: #fdfdfc;
--rule: #d4d0cb;
--accent: #b44;
--code-bg: #f4f3f1;
--font-body: "Charter", "Bitstream Charter",
"Iowan Old Style", "Palatino Linotype",
"Georgia", serif;
--font-heading: -apple-system,
"Helvetica Neue", "Segoe UI", sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--ink: #e8e6e3;
--ink-light: #aaa;
--ink-faint: #777;
--surface: #1a1a19;
--rule: #333;
--accent: #d66;
--code-bg: #242422;
}
}
`}</style>
<div style={{
fontFamily: 'var(--font-body)',
color: 'var(--ink)',
background: 'var(--surface)',
}}>
{/* Content */}
</div>
</>
);
}
| Plain+ concept | Tailwind approach |
|---|---|
| No border-radius | Don't add rounded-* classes. Tailwind's default is square. |
| Serif body | style={{ fontFamily: 'var(--font-body)' }} on the root div |
| Sans headings | style={{ fontFamily: 'var(--font-heading)' }} on h-tags |
| Ink color | style={{ color: 'var(--ink)' }} or text-[color:var(--ink)] |
| Thin rules | border-b with style={{ borderColor: 'var(--rule)' }} |
| Spacing | mb-6 ≈ --space, mb-3 ≈ half, mb-12 ≈ double |
| No shadows | Don't add shadow-* classes. |
Before finalizing any Plain+ artifact, verify every item. This is a grep-able list — literally search the CSS/JSX for violations.
| Check | Search for | Expected |
|---|---|---|
| No border-radius | border-radius, rounded | 0 results |
| No box-shadow | box-shadow, shadow- | 0 results |
| No gradients | linear-gradient, radial-gradient, bg-gradient | 0 results |
| No forbidden colors | #7c3aed, #8b5cf6, #14b8a6, purple, teal | 0 results |
| Serif body font | font-family on body / root | Charter / Georgia stack |
| Sans heading font | font-family on h1–h4 | System sans stack |
| Accent usage | --accent, #b44, #d66 | ≤ 3 locations |
| Dark mode | prefers-color-scheme: dark | Present and complete |
| Measure set | max-width on body / root | ≤ 72em; prose ≤ 42em |
| Warm neutrals | #000, #fff as backgrounds/text | 0 results (use --ink, --surface) |
Plain+ Design Specification v1.0 — This document is self-demonstrating: every element uses the system it describes.
The source is the specification.