Anti-Generic Frontend
Generic output is the expected output, not a failure. Models regress to the statistical centre of the web, shared component defaults look alike, and every system prompt chases the same safe adjectives. So the fix has to be an upstream constraint, never a downstream nudge. "Make it nicer" produces a nicer average. Deciding the tokens before writing markup produces a design.
Written from the kamalov.systems build, 2026-08-14, where every value came out of a measured reference stylesheet rather than taste.
🛑 STOP GATE — do not write markup until this is answered
Name the cluster you are closest to, and say what structural change breaks it. A hue swap is not a change. #0B0A09 to #1A0A0A is the same design.
| # | Cluster | Signature |
|---|---|---|
| a | Warm cream + high-contrast serif + terracotta | #F4F1EA ground, Fraunces or Instrument Serif, rust accent |
| b | Near-black + one acid accent + gradient-clipped headline | #0a0801 + #caa100, gradient text |
| c | Broadsheet | hairline rules, zero radius, dense newspaper columns |
| d | Navy/slate + gold or purple | emoji headings, identical rounded stat cards, centred everything |
| e | Premium-consumer beige and brass | bg #f5f1ea/#f7f5f1, accent #b08947/#b6553a, ink #1a1714 |
Ground the alternative in the subject's own world — its materials, instruments, artifacts, vernacular. A brokerage has order books, session clocks, spreads, latency. A bakery has proof times, flour, steam. Take the signature from there. A style menu is just a smaller distribution to collapse onto.
Rotation rule: if the last project in this category used a palette family, this one may not.
0.5 If the brief says "premium" — measure the reference, do not imagine it
Go and instrument the site they named before writing a line. "Premium" imagined is big type, big claims and a dense hero. Premium measured is almost always the opposite, and the gap between the two burns entire rebuilds.
izanami-official.com, instrumented 2026-08-14 after two rejected attempts:
| izanami | the rejected build | |
|---|---|---|
| largest type on the page | 45px | 102px |
| distinct font weights | 1 (400) | 3 |
| distinct text colours | 3 | 6 |
| sections | 6 | 9 |
| images in 8,665px of page | 4 | 5 + a marquee |
| nav | brand + 2 words, in dead corners | 5-link bar over the content |
| CTA | a 46px hairline and a label | filled buttons |
// paste into the reference page's console
const f=new Set(),s=new Set(),w=new Set(),c=new Set();
document.querySelectorAll('h1,h2,h3,p,a,span,div').forEach(e=>{const g=getComputedStyle(e);
if(e.textContent.trim()&&e.offsetHeight){f.add(g.fontFamily.split(',')[0]);
s.add(g.fontSize); w.add(g.fontWeight); c.add(g.color)}});
console.log({bg:getComputedStyle(document.body).backgroundColor,
sizes:[...s].sort((a,b)=>parseFloat(b)-parseFloat(a)).slice(0,6),
weights:[...w], colors:[...c], fonts:[...f],
sections:document.querySelectorAll('section').length,
imgs:document.querySelectorAll('img').length,
canvas:document.querySelectorAll('canvas').length});
The rule this produces: restraint is the luxury. Expensive pages are mostly empty. Content occupies roughly a quarter of the page height, type stays small, one weight does everything, and the frame of the page carries quiet true information (live clocks, coordinates, an index) instead of marketing furniture. A loud page is never an expensive page, no matter how good the typeface is.
Ambient beats scroll-triggered. A slow continuous canvas (drifting fog, ten soft radial blobs at ~0.03 alpha, paused by IntersectionObserver when off-screen) reads far more expensive than a page full of scroll reveals, and costs ~40 lines.
1. Design read — one line, before anything
Reading this as: a
<page kind>for<audience>, in a<vibe>language, leaning<direction>.
If that sentence could describe any site, it is not a read.
2. Token contract before markup
Write it to UI-SPEC.md in the project. Not a comment, a file, so a later audit can grade the build against it.
- 4 to 6 named hex. Ground, surface, ink, muted, accent, plus semantic status colours if the subject needs them.
Accent reserved for:an explicit list. "All interactive elements" is not an answer. Name the four things. mora.com ships with no accent at all and still reads expensive.- 60/30/10 by area.
- 2 or 3 type roles, real families, with the scale ratio fixed here.
- Layout concept plus an ASCII wireframe.
- One signature element, with a
whywritten in terms of the subject. "Depth" is not a why.
Then re-derive the plan from a generic version of the same brief. Anything that survives both is a default. Revise it, and state what changed.
3. Countable rules
These are checkable, which is the point. Felt quality is unfalsifiable.
Type
- 🛑 The single loudest AI tell is the 12px uppercase letter-spaced MONO micro-label sitting above a grotesque-sans heading. That pairing is every AI-generated SaaS page. It reads "competent startup", never "expensive". Rejected in production 2026-08-14 with: "fonts reveal ugly claude generic design."
- Expensive means a display serif. Luxury references (izanami, Vogue, private banks, editorial) run Didone or high-contrast serif at large size with air. If the brief says premium, fancy, luxury or expensive, reach for Bodoni Moda, Cormorant, Cinzel, Playfair or Marcellus and set eyebrows in serif italic, not mono uppercase. Fraunces and Instrument Serif stay banned; they are the LLM-favourite two.
- Didone hairlines need care on dark grounds. Verify contrast at the real display size before shipping.
- Text-range font sizes ≤ 4. Weights ≤ 3.
- Ratio ~1.25 through text, opening to ~1.4 at display. A real canyon between body and hero is what makes a claim land.
- Force comes from size and tracking, not weight. mParticle uses weight 700 fifteen times on its entire site.
font-variant-numeric: tabular-numson every figure. Non-negotiable when the subject is numbers.- Prose measure ≤ 719px.
- Uppercase mono eyebrows: 12px,
letter-spacing: .09em.
Colour
- Accent on ≤ 4 unique element types (audit tools flag at 10; aim far tighter).
- Never
#000000. Off-black with a cast. - No neon or outer glow. No gradient headline text. No purple-blue gradient.
- Shadows take a colour cast, never pure black.
rgb(0,2,4)reads better than#000.
Layout
- Radius: two values, nothing between. A 4/8/12/16/20/24 ladder is what makes a page look assembled. Pill (
999px) is free. Device geometry is not a token — a phone bezel's 42/32/17px is a physical shape being drawn, so exempt it from the budget rather than bending the design system around it. - Eyebrows ≤ ceil(sections / 3). Count
uppercase trackinginstances. Panel labels inside a card are a different job; give them a different class and do not count them. - Hero: headline ≤ 2 lines, subtext ≤ 20 words, ≤ 4 text elements.
- Consecutive image-text zigzags ≤ 2. Distinct layout families ≥ 4 across 8 sections.
- Bento cells == item count exactly. An empty cell means you planned wrong.
- No three equal feature cards. The most common AI layout tell.
- Asymmetric splits, never 50/50. Nav one line, ≤ 80px.
Copy
- Zero em-dashes. Binary because "sparingly" gets ignored. Headlines, eyebrows, pills, body, quotes, captions, buttons, alt text,
<title>. - No filler verbs: elevate, seamless, unleash, revolutionize, next-gen, empower.
- No fake-perfect numbers.
99.99%and50%read as invented;47.2%and$0.65read as measured. - No Acme, Nexus, John Doe. No section-number eyebrows (
001 · Capabilities). Middle-dot rationed to one per line. - No decorative status dots. A dot must mean something.
Motion
- One orchestrated moment beats scattered micro-interactions.
- Entrance decelerates, exit accelerates, never
linearfor spatial movement. - Nothing over 700ms. Total stagger under 500ms.
- Marquees at ~32 px/s with
translate3d(0 → -50%)over a duplicated group, and atransparent 0% / black 5% / black 95% / transparent 100%mask. - Only
transformandopacityanimate per frame. prefers-reduced-motiongets a legible fallback, not "animations off".
4. Assets and honesty
- Never fabricate client logos, awards, testimonials or metrics. A marquee of real integrations is visually identical and entirely true. Invented logos imply clients that do not exist.
- Real photo means a real URL with a real extension. Anything conceptual emits a delimited image-generation prompt block instead. Never invent a link.
- No div-built fake screenshots. A live component showing real data is fine and better; a stack of divs pretending to be a product screenshot is not.
- The strongest premium device available is the real product, embedded. If there is a live app, proxy it onto the marketing domain and put it in a browser chrome frame in an
<iframe>, rather than shipping a picture of it. Same-origin so noX-Frame-Optionscan block it; a transparent link over the frame instead of an interactive iframe, because an iframe that eats the scroll wheel is worse than a static image. Nothing an illustrator can draw competes with a panel of numbers that changed since the visitor loaded the page. - If real user content is displayed, anonymize it. Publishing customer conversations exposes them.
5. Verification — score it, do not eyeball it
Reading the diff is not verification. Open the page.
Mechanical pass, all must hold:
grep -c '—' page.html # 0
grep -oE 'font-weight:[0-9]+' page.html | sort -u | wc -l # <= 3
grep -oE 'border-radius:[^;}]+' page.html | sort -u # 2 values
grep -c 'class="eyebrow"' page.html # <= ceil(sections/3)
grep -o 'var(--accent)' page.html | wc -l # <= ~6 including hover states
Two defects survive every mechanical pass above and every casual glance. Automate both.
a. Run-together text. Sibling <span>s with no display:block render as
"Messages handledOne deployment...". Grid and flex children are blockified so they
look fine; a nested wrapper's children are not. Shipped twice before this became code:
document.querySelectorAll('body *').forEach(p=>{
const ks=[...p.children].filter(k=>k.textContent.trim()&&k.offsetWidth);
for(let i=0;i<ks.length-1;i++){
const a=ks[i].getBoundingClientRect(), b=ks[i+1].getBoundingClientRect();
if(Math.abs(a.top-b.top)<4 && Math.abs(a.height-b.height)<8 && b.left-a.right<1.5)
console.warn('run-together', ks[i].className, ks[i+1].className);
}});
Exempt marquee loop groups: a seamless track requires its duplicated halves to touch.
b. Crushed imagery. A photograph under a dark overlay dies silently and the section reads as flat black, which is the cheap look, not the expensive one. Sample painted luminance; mean under ~35/255 means the image is not there:
from PIL import Image
im = Image.open(shot).convert("L"); w, h = im.size
band = im.crop((int(w*.55), int(h*.15), w, int(h*.85))) # away from the copy
px = list(band.getdata()); print(sum(px)/len(px), max(px))
Effective visibility is image_opacity × (1 − scrim_alpha). At opacity .5 under a
.62 scrim only 19% of the photograph reaches the eye. Use a two-axis scrim
rather than one heavy vertical one: a vertical pass for the top and bottom edges, plus
a horizontal pass heavy under the copy that clears to 0 on the open side. Copy stays
legible, image survives at full strength where nothing sits on it.
Visual pass at 1440 / 768 / 375. Score six pillars 1 to 4 — copy, visual, colour, type, spacing, experience — for a total out of 24, assuming every pillar fails until a screenshot proves otherwise. Publish gate: no pillar at 1, total ≥ 20.
Also: console clean, no 404 assets, keyboard focus visible, reduced-motion path exercised, and every script the page claims to support actually renders without tofu.
⚠️ A backgrounded tab throttles transitions and rAF. document.hidden === true means CSS transitions may sit at their start value and screenshots capture a blank or pre-animation frame. Assert document.hidden before trusting any visual check, and never conclude "the element is broken" from a hidden-tab capture. This wasted several rounds on 2026-08-14, first misdiagnosed as a mix-blend-mode compositing bug.
⚠️ A JS error in one script block kills every script after it. A dead getElementById left behind after removing a section threw null.appendChild, which silently killed an unrelated component further down the file. Read the console after any structural edit — the symptom appears in a component you did not touch.
⚠️ resize_window cannot go below Chrome's minimum window width (~500px). Asking for 375 silently returns ~1448 and you will "verify" mobile at desktop width. Use devtools viewport emulation for anything under 500px.
⚠️ Patching a stylesheet that lives inside a source file: never locate the block by a string two blocks share. A dashboard's CSS and its login page's CSS both opened :root{--bg:#0d0d0f, so a search for that string resolved the login block for both, and the patch wrote the dashboard stylesheet into the login page and the login stylesheet into a third injection point. The live product came back centred like a login card. Anchor on exact line numbers or on a string proven unique, assert every boundary before writing, and assert the post-conditions too (block count unchanged, file still parses). Cheap insurance on anything already serving traffic.
The mirror test: would a design-led studio ship this, or does it read "an AI made a landing page"? If the latter, return to the gate. Do not tune spacing and re-ship.
6. Failure modes
| Tell | How it gets in | Mechanical check | Fix |
|---|---|---|---|
| "Still generic" after a recolour | Changed hue, not structure | Cluster named in the gate? | Change what carries meaning |
| Three identical cards | Default feature-row shape | Count equal-width siblings | Zigzag, asymmetric grid, or a table |
| Value and caption run together | Sibling <span>s with no display:block |
Render and read it | Block the child |
| Headline overflows to 3 lines | max-width in ch set against display size |
Measure height / line-height | Widen the measure or cut words |
| Marquee shows dead gaps | Items too wide for the loop | Item width vs viewport | Cap item width, ellipsis |
| Mobile "verified" but broken | Window would not shrink | Assert clientWidth |
devtools emulation |
| Tofu in non-Latin text | Display face lacks the script | Render every claimed script | Explicit Noto fallback chain |
| Beautiful, nothing moves right | Motion added ad hoc | One personality named? | Lock easing, 3 durations, one entrance |
| Janky scroll | Animating layout properties | Which properties animate | transform and opacity only |
| Impressive tech, hollow page | Technology chosen first | Does the effect have a subject-grounded why | Cut it |
| Page reads flat black despite 5 images | Scrim tuned by eye on a bright monitor | Mean luma of each image band | Two-axis scrim, raise image opacity |
| Embedded app has a white slab beside it | align-items:stretch stretched the frame past the scaled iframe |
Frame height vs iframe_height × scale |
align-items:start, set height from the scale |
| Embedded app is an unreadable smudge on mobile | One hard-coded scale for all widths | width/logical_width at 390px |
Compute the scale; below ~760px hold a legible scale and crop |
| Photos render as black rectangles in headless shots | loading="lazy" never fires in full-page captures |
img.complete false before capture |
Flip to eager, await all complete, then shoot |
| Build contradicts its own UI-SPEC | Direction changed mid-build, spec did not | Read the spec against the build | Dated amendment section at the top of the spec |
| Generated assets never shipped | Pipeline output not wired into markup | grep -rq asset . per file |
Wire it in or delete it; inventory is not design |
7. Route to another skill
taste-skill(v2, 1,206 lines) is the deep rule library. This skill is the gate and the countable subset. Use both.signature-webfor motion identity and the CSS → Canvas → GSAP → Three.js effect-tier decision with budgets.premium-pdf-designfor print, decks and one-pagers.super-book-coverfor covers and thumbnails.de-aifor the copy itself. The em-dash ban and the filler-verb ban are the same rule in both places.
Worked example, kamalov.systems, 2026-08-14
Cluster risk was (b) and (d). Broken structurally: light bone ground with a green-grey undertone rather than near-black, olive neutrals rather than slate, cobalt rather than gold, accent used 4 times on the whole page.
Type: Archivo variable at wdth 115. The expanded width axis is the characterful choice and it is why the hero reads as instrument-panel authority rather than default grotesque.
Signature: a full-bleed drift of real anonymized agent messages, each pill stamped with channel, language and reply latency. Eleven languages is a claim no headline can prove; five scripts drifting past in peripheral vision proves it in two seconds. It also removed any need for client logos.
Three bugs found only by looking: metric values rendering inline with their captions, a hero wrapping to three lines against a two-line cap, and marquee chips wide enough to drift a dead gap through the viewport.
v4 lessons, kamalov.systems rebuild, 2026-08-17
The spec is a living lock, not scripture. The client picked the dark prototype over the light scheme the spec had locked. The wrong move is a build that contradicts its own spec file; the right move is a dated amendment section at the top of UI-SPEC.md that says exactly which clauses are superseded and which still govern. An auditor a month later cannot tell a deliberate divergence from drift unless you write it down.
loading="lazy" images never paint in headless full-page captures. They load on scroll proximity, the capture resizes the viewport after the scroll, and the screenshot ships with black rectangles where the photography should be. Before any headless visual check: document.querySelectorAll('img[loading=lazy]').forEach(i=>i.loading='eager'), then await every img.complete. Symptom looked exactly like a broken scrim, which is the expensive misdiagnosis.
Metrics that can never go stale, demos that can never show zero. A "days live" figure computed in JS from the real launch date is honest and self-updating; a hard-coded one is a lie in progress. And any live demo embedded on a marketing page must lead with cumulative numbers, because real-time figures fluctuate through zero and a visitor who sees "0 messages today" reads a dead product. Cumulative-first is not inflation, it is the only honest framing that survives an arbitrary visit time.
Grep every asset against the markup before ship. Four transcript screenshots were generated, paid for, and referenced nowhere. A generation pipeline that is not wired into the page is inventory, not design. for f in img/*; do grep -rq "$f" . || echo "orphan: $f"; done
Shared chrome is generated, never copied. Subpages get their header, footer and head from one template function (_pages.py pattern) with the copy rules asserted at generation time (assert "—" not in html). Pages that cannot drift apart do not drift apart, and the em-dash gate stops being something you remember to check.
Thin real pages beat missing pages. A footer that links Privacy, Terms and Status needs those pages to exist, but a sole operator does not need legalese: three short, plainly-true pages in the same chrome beat both the absence and the boilerplate. A status page updated by hand, that says so, is more credible than an automated one that always says green.