opopa.top

Design starts with input artifacts, not model choice

·6 min read·updated

engineeringdesign

Why “make it look good” is a poor brief for any model, and what to provide instead: the design workflow behind the blog you are reading.

This blog’s layout was built in one session, and a grep of src/components finds no literal colors. That was not a lucky choice of model. Before the first line of layout code, the inputs already existed: a token table, a list of constraints and ten concept images.

Why “make it look good” does not work

“Build a beautiful landing page” leaves the agent a hundred decisions: background, font, spacing, where to put the accent. It makes those choices from familiar patterns in its training data. The result is the same purple gradients and cards with 16px corners that keep appearing across generated sites.

The method described by @serejaris reverses that: design is provided as an input artifact. The agent receives design.md and uses the specified --color-iris, rather than choosing between #6366f1 and #8b5cf6 according to its own taste.

What goes into the input

Artifact What it fixes Without it
design.md Color roles, type scale, spacing and radii Each component gets its own shade of gray
llms.txt Constraints that can be checked The agent invents “one more accent color”
Concept images Composition and rhythm before layout A layout based on taste, with no reference to compare it against
UI kit (shadcn) Primitive behavior and states A custom reinvention of every dropdown

The order matters. Without design.md, llms.txt can forbid inventing colors but cannot say which ones to use. Concepts without tokens provide an image that is hard to reproduce consistently in code.

A reference theme cannot be adopted unchanged

A common mistake is to take someone else’s design.md and apply it as-is. We chose the Huly theme from styles.refero.design and started by calculating contrast for its palette:

def contrast(fg: str, bg: str) -> float:
    hi, lo = max(lum(fg), lum(bg)), min(lum(fg), lum(bg))
    return (hi + 0.05) / (lo + 0.05)

Results on the reference canvas, #303236:

Pair Contrast Verdict
Link #5683da on the canvas 3.46 Below AA for normal text
Secondary text #95979e 4.40 Below AA
White text on a #5683da button 3.71 Below AA

The theme looks good in SaaS landing-page screenshots, but those combinations fail the text-contrast requirements of a blog. We darkened the canvas to #0b0c0e and revised the text/accent combinations so the checked text pairs met AA.

Four details we learned while building

Tailwind v4 removes unused tokens. Declare tokens with @theme, then reference them from scoped Astro styles through var(--spacing-5), and the variable may be missing from the build: Tailwind emits the tokens used by utilities. One word fixes it:

@theme static {
  --spacing-5: 24px;
}

static emits all the variables regardless of usage.

A self-reference in @theme inline. Mapping a variable to itself, such as --color-canvas: var(--color-canvas), creates a cyclic reference in the built CSS. You can use the layer cascade, but declaring each token once directly in @theme is simpler than debugging the cycle.

Cyrillic narrows the display-font choice. The reference’s Esbuild font lacks Cyrillic, as do many distinctive sans-serif fonts in design galleries. We used @fontsource/unbounded 5.3.0, with Onest and Manrope recorded as alternatives in design.md. Our shortlist contained four names.

Preflight strips blocks of their meaning. Tailwind resets browser styles, including list-style on every list. Prose.astro never restored markers for article text, and for a while lists looked like broken-off paragraphs. Grep cannot catch this: there is nothing to forbid, the style is simply missing. We caught it three weeks after the build, while adding a test of Telegram’s in-app browser: it puts every block the author is allowed to use into an article and checks the computed styles — a list has markers, a quote has its rule, long code scrolls. A block allowed without a line in the test is red.

A constraint you can run through grep

A guideline is easy to ignore. Phrase a constraint so that grep can check it, and the document becomes a test:

# Literal colors and arbitrary pixel utilities: expected output is empty.
grep -rnE '#[0-9a-fA-F]{3,8}|rgb\(|hsl\(|\[[0-9]+px\]' src/components src/layouts src/pages

# Classes from an unrelated Tailwind palette: expected output is empty.
grep -rnE '(bg|text|border)-(slate|gray|zinc|blue|indigo)-[0-9]' src/

Both commands live in llms.txt, and the agent is instructed to run them before committing. A rule we cannot check with a command does not belong in that file.

The same tokens also produce social cards: the OG generator takes its colors from design.md, so the Telegram preview and the page use the same visual material.

The sequence

Start with the calculations: evaluate the reference palette against your own content. The link, the secondary text and white text on a button failed AA.

Then write design.md with tokens and roles and transfer those values mechanically to tokens.css. A mismatch is a defect, not a matter of taste. Follow with llms.txt, its constraints and its grep commands.

Only then create the concepts: six to ten different directions, choose one, and record why the others were rejected. Otherwise a week later nobody remembers why the card grid was rejected, and it gets proposed again.

The ten-image concept run cost twenty cents: that is the recorded cost_usd in run.json, not an estimate. I have not measured the cost of rebuilding a blog that “looks somehow wrong.” Without a selected concept, there is no agreed visual reference against which to judge the finished layout.