What does web-design-base-v1 do?
Standard. Every site looks art-directed for its own subject, cannot be mistaken for a template, is technically correct in every detail, and uses typography as its main tool.
Method. Read the subject, name one concrete visual idea, build the system, plan the content, then code. The system exists before the first line of code, because choices made while coding drift toward defaults.
0. How to read this skill
| Level | Meaning |
|---|---|
| Gate | Checked at the end. A page that fails a gate is not finished. |
| MUST / MUST NOT | Default law. Deviate only when the user asks; say so in one sentence. |
| SHOULD | Strong default. Deviate silently when the subject calls for it. |
| MAY | Allowed when it serves the subject. |
Priority: Gates 1 and 2 (accessibility) → the user's explicit request → MUST → SHOULD → MAY. If the user asks for something that breaks a gate, build the closest version that passes and say what you changed. Within one level, the more specific rule wins.
Examples are demonstrations, not templates. Every palette, font pairing, ferry timetable, and code value below shows the format of a good answer. Copying one unchanged into an unrelated subject is the most common failure of this skill. If you pick a listed palette or pairing as is, justify it in the plan with a fact about the subject.
1. Five gates
- First paint and no-JS. The hero headline is visible from the first frame: unshifted, unclipped (including umlaut dots on Ä Ö Ü), never animated. With JavaScript disabled or reduced motion on, the page is complete.
- Contrast. Body text ≥ 7:1, muted text and accent-as-text ≥ 4.5:1, large headings ≥ 4.5:1, structural borders, focus rings, and UI parts ≥ 3:1. Computed for every text/surface pair actually used, never assumed (§6).
- Palette discipline. Background, ink, one accent, plus tints of bg/ink. No gradients, glow, glassmorphism, or colored shadows.
- Completeness. Every section has real substance (§8), every link goes somewhere intentional, every required state exists.
- Specific. If the palette and type could move to a different subject unchanged, the work is not done.
2. Choose the mode
| Mode | Use for | Rules that apply |
|---|---|---|
| Showcase | landing, portfolio, shop front, editorial, event, corporate | everything except §12 |
| Product UI | dashboard, admin, tool, app, form-heavy flows | §1–§6, §10, §11, §12; skip hero archetypes and the section pool |
| Docs | documentation, handbooks, model cards | Showcase typography and color, Product UI structure (TOC, code, states) |
| Audit | "review/improve my page" | list findings, patch only what is broken, keep the existing identity |
| Iterate | "change X" | change the named token or rule and its direct dependents only; no silent refactors |
When the user supplies a brand (colors, logo, fonts), the brand is the palette and type. Compute its contrast. If the brand accent fails 4.5:1 as text, use it only for large or non-text elements and add a darker text variant of the same hue.
3. Method for Build
Write the plan before code: 12–16 lines in the user's language (Iterate: a 3-line diff).
3.1 Read the subject. What must the visitor do or understand? What does the subject look, sound, and feel like in its real place (harbour, foundry, lab, kitchen, courtroom, archive)? What single thing must they remember?
3.2 Name the visual idea. One sentence naming a concrete object, material, or convention from the subject. Example for a ferry line: "a printed departure timetable: heavy condensed numerals, ruled rows, the board as hero." Abstract adjectives ("modern, clean, bold") and color lists are not ideas. Originality test: name three things a generic site in this category has, and confirm this design has none of them.
3.3 Build the system as a table in the plan:
| Field | Value |
|---|---|
| Palette | bg, ink, accent (hex), the material it is named after, computed ratios per used pair |
| Type | display + text face, reason tied to the subject, longest word and its mobile size |
| Hero | archetype (§7.1) + one-line justification |
| Layout | base logic (§7.2) and how sections vary it |
| Motion | one signature pattern + optional supporting pattern |
| Sections | ordered, each with its job and form |
| Signature moment | the one thing a visitor remembers |
3.4 Plan the content. List every section with concrete facts, steps, names, figures, dates, prices. Invented content is allowed if internally consistent and marked (§8.3).
4. Foundations
4.1 The head is part of the design
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Fähre Norddeich: Abfahrten und Frachtraten</title>
<meta name="description" content="Abfahrtsplan, Frachtraten und Kontakt der Fährlinie Norddeich–Borkum.">
<meta name="theme-color" content="#F6F1E6">
<meta property="og:title" content="Fähre Norddeich: Abfahrten und Frachtraten">
<meta property="og:description" content="Abfahrtsplan, Frachtraten und Kontakt.">
<meta property="og:locale" content="de_DE">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23C0430B'/%3E%3C/svg%3E">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Big+Shoulders+Display:wght@700;800&family=Public+Sans:wght@400;500;600&display=swap">
<script>
/* Motion gate: hidden start states exist only if this class is set (§9) */
if (!matchMedia("(prefers-reduced-motion: reduce)").matches && "IntersectionObserver" in window)
document.documentElement.classList.add("js-motion");
</script>
<style>/* §4.2 base, §4.3 tokens, then components */</style>
</head>
- MUST:
langmatches the copy,titleanddescriptionare specific, the font URL matches the families chosen in §5 and ends indisplay=swap, a favicon is present (it avoids a 404 and looks finished). - MUST NOT: empty head,
lang="en"on German copy, fonts withoutdisplay=swap. - Privacy (EU/DE). Google Fonts loaded from Google's servers transmits visitor IPs; German courts have treated this as a GDPR problem. For a prototype it is acceptable. For production, say in the report that fonts should be self-hosted (woff2 files,
@font-face). If the environment blocks external fonts, the fallback stacks (§4.5) must still look designed.
4.2 Base CSS
Start every page with this foundation. It prevents shifted, clipped, and inherited-broken typography.
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { overflow-x: clip; -webkit-text-size-adjust: 100%; scroll-padding-top: var(--header-h, 0px); }
body {
background: var(--bg); color: var(--ink);
font-family: var(--font-text); font-size: 1.0625rem; line-height: 1.6;
font-synthesis: none; /* no fake bold/italic if a weight is not loaded */
text-rendering: optimizeLegibility;
}
h1, h2, h3, h4, h5, h6, p, ul, ol, dl, dd, figure, blockquote, table, form { margin: 0; }
ul, ol { padding-inline-start: 1.25em; }
ul[role="list"], ol[role="list"] { list-style: none; padding: 0; }
img, svg, video, canvas { display: block; max-width: 100%; height: auto; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration-thickness: 0.08em; text-underline-offset: 0.2em; } /* links stay recognizable */
nav a, .btn { text-decoration: none; }
:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }
[id] { scroll-margin-top: var(--header-h, 0px); }
table { border-collapse: collapse; }
h1, h2, h3 { text-wrap: balance; }
p, li { text-wrap: pretty; }
::selection { background: var(--accent); color: var(--on-accent); }
.container { width: 100%; max-width: 90rem; margin-inline: auto; padding-inline: var(--gutter); }
.measure { max-width: var(--measure); }
.skip-link { position: absolute; inset-inline-start: -9999px; }
.skip-link:focus { inset-inline-start: var(--gutter); inset-block-start: 0.75rem; z-index: 20;
background: var(--accent); color: var(--on-accent); padding: 0.75rem 1rem; }
- MUST NOT:
100vwfor content,overflow: hiddento crop type, negative margins ortransformon the hero headline. Accept the font's natural side bearing instead of correcting it optically. - MUST put all text inside
.container. The hero headline shares the left edge of the body copy unless an offset layout is named in the plan. - MUST size full-height heroes with
min-height: 100vh; min-height: 100svh;(thesvhline wins where supported, so mobile browser bars do not cut the hero). - Body copy uses
rem, never fixedpx, so the visitor's browser font setting still works.
4.3 Tokens
Every color, font, size, and spacing value lives in one block. Replace the example values with your own palette (§6).
:root {
color-scheme: light; /* "dark" for dark palettes: form controls and scrollbars follow */
--bg: #F6F1E6; --bg-soft: #ECE3D3;
--ink: #1C1A17; --ink-muted: #635A4C; /* muted: labels and captions only */
--accent: #C0430B; --on-accent: #F6F1E6; /* text on accent fills */
--line: #857A66; /* structural rules: ≥ 3:1 on bg and bg-soft */
--line-soft: #C9BEA8; /* decorative hairlines only */
--focus: var(--accent); /* re-assigned inside inverted bands */
--font-display: "Big Shoulders Display", "Arial Narrow", sans-serif;
--font-text: "Public Sans", "Segoe UI", sans-serif;
--step-hero: clamp(3.25rem, 1rem + 11vw, 13rem);
--step-h2: clamp(2.25rem, 1rem + 5vw, 6rem);
--step-label: 0.8125rem;
--space-xs: 0.5rem; --space-s: 1rem;
--space-m: clamp(1.5rem, 1rem + 2vw, 3rem);
--space-l: clamp(3rem, 2rem + 4vw, 6.5rem);
--space-xl: clamp(5rem, 3rem + 8vw, 11rem);
--gutter: clamp(1.25rem, 0.5rem + 3vw, 4rem);
--measure: 66ch;
--header-h: 0px; /* set to the sticky header height, else 0 */
--rule: 1px solid var(--line);
}
MUST: every value in rules comes from these tokens. MUST NOT: inline hex in rules, or framework default palettes (gray-500, slate-200). Fluid sizes mix rem and vw so browser zoom and font settings still scale them (pure vw ignores zoom).
4.4 Language and locale
- Copy is in the language of the request.
langmatches. Sie/du stays consistent. - German:
hyphens: autoon narrow columns (needs correctlang); the font subset MUST cover äöüß and „ “ ‚ ‘ –. Typographic quotes („…“), no English marketing words on a German page, between number and unit (12 kg,9:30 Uhr), dates as1. November 2026. - German sites for businesses need Impressum and Datenschutz links in the footer (in-page demo sections are fine for a prototype). Since 2025 the BFSG makes accessibility a legal duty for many consumer-facing e-commerce services; WCAG 2.1 AA is the floor, and this skill's gates already exceed it.
4.5 Fonts that survive network failure
Pick fallbacks of the same class (condensed → "Arial Narrow"; serif → Georgia; grotesk → system-ui). The two fallbacks MUST stay visibly distinct, so the page keeps its hierarchy when the fonts fail. Load only weights you use. The weights used for display MUST exist in the loaded family (see §5).
4.6 Breakpoints and 375 px
Mobile-first. Breakpoints: 480, 760, 1024, 1320 px. At 375 px: no horizontal scroll, hero headline ≤ 5 lines, longest word fits, touch targets ≥ 44 × 44 px, sticky headers do not cover anchors (--header-h), every two-column layout has a designed mobile form.
Longest-word rule. Available width ≈ 375 − 2 × 19 ≈ 335 px. Max hero size in px ≈ 335 ÷ (letters in the longest word × width factor). Width factors: condensed 0.42, serif 0.52, grotesk 0.58, expanded 0.68. Set the clamp() minimum to that value. "Frachtabfertigung" (17 letters) in a condensed face allows ≈ 47 px, so the 52 px minimum above would overflow: lower the minimum or add soft hyphens (Fracht­abfertigung). Never let a long German compound force overflow-wrap: anywhere into the hero.
5. Typography: the main tool
Use exactly two families: one display, one text.
| Display | Text | Fits |
|---|---|---|
| Big Shoulders Display | Public Sans | timetables, logistics, freight, sport |
| Barlow Condensed | Barlow | transport, signage, clubs, events |
| Fraunces | Karla | food, craft, printing, hospitality |
| Instrument Serif (400 only) | Work Sans | editorial, fashion, architecture |
| Newsreader | Libre Franklin | legal, institutional, research |
| Familjen Grotesk | Source Serif 4 | magazines, culture, non-profits |
| IBM Plex Mono (display) | IBM Plex Sans | labs, tooling, data, documentation |
| Archivo (width axis 112–125) | Spline Sans | harbours, industry, product sheets |
| Anton (400 only) | Barlow | campaigns, venues, posters |
| Bodoni Moda | Karla | luxury goods, galleries, auctions |
These are starting points. Choose by subject; landing on row 1 for a non-logistics subject means you chose by position, not by subject. Check the font's real axes and weights: Instrument Serif and Anton have one weight, so "heavy" is not available and font-synthesis: none keeps the browser from faking it; Archivo needs wdth in the URL (Archivo:wdth,[email protected],700..900) and font-stretch in CSS.
MUST NOT use Inter, Roboto, Arial, Poppins, Playfair Display, Lora, or Merriweather as the identity pair. MUST NOT use one family for everything. Monospace as display only for technical subjects.
Scale
- Hero:
--step-hero, line-height 0.92–0.98 (German capitals with Ä Ö Ü need ≥ 0.92 so dots do not hit the line above). Letter-spacing depends on the face: condensed 0 to −0.01em, serif −0.02 to −0.03em, grotesk −0.03 to −0.04em. Test for touching glyphs; tight is not the goal, imposing is. - Section headings:
--step-h2. - Body: 1–1.125 rem, line-height 1.5–1.65,
max-width: var(--measure)(58–70 characters). - Labels: 12–14 px (
--step-label), uppercase, letter-spacing ≥ 0.12em, muted ink. Never below 12 px. - Every section pairs one small tracked label with one very large element. The size contrast is what makes the page feel expensive.
Weight. Display text is heavy/black or deliberately light at large size; body is regular or medium. Never one weight everywhere.
Optical detail (when the subject supports it): tabular numerals (font-variant-numeric: tabular-nums), small caps, hanging punctuation, custom rules, large initials. MUST NOT letter-space body copy.
6. Color and contrast
Contrast is the identity: a page reads as bold before it reads as pretty.
Compute, do not assert. Run this when a shell is available, with every text/surface pair the design uses (ink on bg, muted on bg and bg-soft, accent on bg and bg-soft, on-accent on accent, band text on ink, accent on ink):
python3 - <<'PY'
def lum(h):
h = h.lstrip("#")
c = [int(h[i:i+2], 16) / 255 for i in (0, 2, 4)]
c = [v / 12.92 if v <= 0.04045 else ((v + 0.055) / 1.055) ** 2.4 for v in c]
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
def ratio(a, b):
x, y = lum(a), lum(b)
return (max(x, y) + 0.05) / (min(x, y) + 0.05)
pairs = { # name: (foreground, background)
"ink on bg": ("#1C1A17", "#F6F1E6"),
"muted on bg-soft": ("#635A4C", "#ECE3D3"),
"accent on bg": ("#C0430B", "#F6F1E6"),
"accent on bg-soft": ("#C0430B", "#ECE3D3"),
"on-accent on accent": ("#F6F1E6", "#C0430B"),
"line on bg": ("#857A66", "#F6F1E6"),
}
for k, (a, b) in pairs.items():
print(f"{k:22s} {ratio(a, b):5.2f}")
PY
Without a shell, estimate conservatively and mark the ratios "estimated, not computed" in the report.
Surface matters. The example accent #C0430B is 4.60:1 on --bg but only 4.07:1 on --bg-soft and 3.35:1 on --ink. It passes as text on one surface and fails on the other two. Re-check the accent per surface; on a failing surface use it only as a fill, rule, or large numeral (≥ 3:1), or switch to --bg for the text.
Palette from materials. Name it after a material, place, or object ("wet slate harbour", "foundry paper", "safety vest on asphalt"), then compute. Starting points, with computed ink-on-bg / accent-on-bg ratios:
- Wet slate: bg
#12181C, ink#E8E4DA, accent#E0552C(14.1 / 4.7) - Foundry paper: bg
#F6F1E6, ink#1C1A17, accent#C0430B(15.4 / 4.6) - Operating theatre: bg
#F4F6F5, ink#16211F, accent#0B6E4F(15.2 / 5.8) - Hi-vis on asphalt: bg
#222420, ink#F2EFE8, accent#F4C21B(13.6 / 9.4; text on the accent fill must be dark, not light) - Bone + signal red: bg
#E9E7E2, ink#1A1C1E, accent#B62A24(13.8 / 5.1)
Rules
- Three hues at most: bg, ink, one accent. Neutrals are tints of bg or ink.
- Muted ink ≥ 4.5:1 on every surface it sits on, only for labels and captions.
- Accent marks the first thing to see or do. One focal use per screen (a word, a button, a rule, a block). Functional repeats are fine (focus ring, selection, links, primary buttons in a row of states); decoration is not. A full-width accent band is allowed once if planned.
- MUST NOT: mid-tone on mid-tone; pure black on pure white or grayscale-only unless the subject demands it; reflex pairings (navy+gold+cream, purple-on-white, red-black-neon, teal+coral); gradients, glow, colored shadows,
backdrop-filter. - Structural lines (table rules, form borders, dividers that carry meaning) use
--line(≥ 3:1).--line-softis for decorative hairlines only. - Dark scheme: the page owns its scheme. Add
prefers-color-scheme: darkonly when asked. Dark palettes setcolor-scheme: dark. - Inverted bands re-assign tokens locally:
.band { --focus: var(--bg); }.
7. Composition (Showcase and Docs)
7.1 Hero archetypes
Choose one and justify it in one line:
- Type monument (headline alone fills the first screen)
- Split statement (headline beside a cropped object or image)
- Full-bleed image with type on a solid panel
- Oversized figure or word
- Index (main sections as a numbered list on screen one)
- Document cover (masthead, headline, issue line, contents)
- Single declaration (one very large sentence, one supporting line, one action)
The archetype MUST carry an object or convention from the subject. MUST NOT: the centered stack of eyebrow + headline + subline + two buttons.
7.2 Base layout logic
Choose one base per page and vary it per section: strict column rhythm, asymmetric wide/narrow tracks, full-width bands with alternating treatments, offset blocks, or a narrow reading column with wide whitespace. At most one diagonal division per page. Two consecutive sections MUST NOT share the same structure.
7.3 Section pool
Draw from this pool; no form twice per page:
- Index with large numerals
- Two-column text + object (image side alternates)
- Inverted band (full width, ink or accent, text reversed)
- Timeline with ruled spine
- Table with tabular numerals and hairline rules
- Statement (one large sentence on a solid field)
- Asymmetric body + rail of facts
- Offset block (once per page)
- Glossary as definition list
- Numbered steps ending in an outcome
- Gallery band (one wide, two small, captioned)
- Ledger (items, prices, quantities, totals)
- Quote panel (attributed, with rule; never a carousel)
- Spec sheet (dimensions, tolerances, materials)
7.4 Rhythm
Spacing only from the token scale: sections separated by --space-l or --space-xl, content grouped with --space-m. Density follows the subject. MUST NOT use framework defaults like py-24 everywhere.
7.5 Imagery without image files
Published and sandboxed pages often block remote images. Build visuals from the subject: inline SVG (technical drawings, route maps, diagrams, charts, specimen sheets), oversized numerals, ruled tables, or solid blocks with a visible caption. SVG art follows the palette and uses currentColor. It depicts the subject; floating blobs and abstract decoration do not count. Real photos only when the user supplies them: set width, height, aspect-ratio, specific alt, fetchpriority="high" on the first image, loading="lazy" on the rest.
8. Content and depth
A page is judged by how finished it feels.
8.1 Minimum by page type (Showcase and Docs)
- Landing / product: 6 sections
- Portfolio / case study: 5 sections, 3 full entries
- Shop / catalogue: category structure + 8 items with real detail + a path to purchase
- Documentation: TOC + 5 real sections + 3 worked examples
- Editorial home: 4 articles (headline, standfirst, date) + one long-form feature
- Data report / model card: 6 sections, each with figure, table, or method note
- Corporate / service: 6 sections covering what, how, for whom, why trust
8.2 Substance
Every section has ≥ 80 words of specific text or an equivalent (a list of ≥ 4 concrete items, a table, a timeline). Generic reassurance does not count.
8.3 Truth
Use the user's real copy, prices, names, and data when supplied. Otherwise invent content that is internally consistent, mark it with <!-- EXAMPLE CONTENT: replace before publishing -->, and show a visible "Demo" label in the footer.
Invented content must stay harmless:
- No real people, real companies, or real brands as testimonial authors or "clients". Use clearly fictional names.
- No invented medical, legal, or financial claims, certifications, awards, or guarantees.
- Contact data uses reserved fiction: e-mail on the
.exampleTLD ([email protected]), German film-production phone numbers (Berlin030 23125 xxx, Hamburg040 66969 xxx, Köln0221 4710 xxx, München089 99998 xxx), and fictional street names. A "real-looking" address or number may belong to a real person.
MUST NOT leave placeholders: [PRICE], example.com, Lorem ipsum, 12345, John Doe, href="#", TODO, empty sections.
8.4 Copy
Concrete nouns, real verbs, real numbers; vary sentence length. MUST NOT use these or close variants: "Welcome to our platform", "Your journey starts here", "Unlock the power of", "Empower", "Seamless", "Cutting-edge", "Revolutionize", "Elevate your brand", "Trusted by industry leaders", "Take X to the next level", "Whether you are a", "In today's digital landscape" (and their German equivalents: "Willkommen auf unserer Plattform", "nahtlos", "maßgeschneiderte Lösungen", "auf das nächste Level"). No three generic feature cards. No chained em-dashes.
9. Motion
One clean, memorable signature moment per page, at most one supporting pattern. Everything else stays still.
9.1 Rules
- Motion reveals, orients, or confirms. It does not decorate.
- The hero headline never animates (gate 1). Hero motion, if any, belongs to non-text elements: a rule drawing, a figure clipping in, a numeral with its final value already in the HTML.
- Base CSS shows the final state. Hidden start states exist only under
.js-motion, which the head script sets only when motion is allowed andIntersectionObserverexists. - All motion CSS sits inside
@media (prefers-reduced-motion: no-preference). Reduced motion yields the complete, static page. - Durations: 700–1100 ms for reveals, 180–280 ms for hover and focus. Easing:
cubic-bezier(0.22, 1, 0.36, 1)orcubic-bezier(0.7, 0, 0.2, 1); no defaultease. - Animate only
transform,opacity,clip-path. MUST NOT animate width, height, margin, padding, top, left,letter-spacing, or usetransition: all. - MUST NOT: infinite loops, marquees, parallax on body copy, cursor effects, glitch, scanlines, typewriter, motion on paragraphs.
- Stagger at most 4 steps, 70–90 ms apart, on short lists only.
9.2 Patterns
Elements opt in with data-reveal. The script adds .is-visible to that element.
A. Line mask (section headings; not the hero)
@media (prefers-reduced-motion: no-preference) {
/* padding + negative margin leave room for umlaut dots without changing layout */
.js-motion .mask { overflow: hidden; padding-block: 0.14em; margin-block: -0.14em; }
.js-motion .mask > span { display: block; transform: translateY(115%);
transition: transform 900ms cubic-bezier(0.22, 1, 0.36, 1); }
.js-motion [data-reveal].is-visible .mask > span { transform: none; }
}
Markup: <h2 data-reveal><span class="mask"><span>Abfahrten</span></span></h2>.
B. Clip-path reveal (strong signature candidate, for figures and panels)
@media (prefers-reduced-motion: no-preference) {
.js-motion .clip-reveal { clip-path: inset(0 0 0 100%);
transition: clip-path 1000ms cubic-bezier(0.7, 0, 0.2, 1); }
.js-motion .clip-reveal.is-visible { clip-path: inset(0); }
}
Markup: <figure class="clip-reveal" data-reveal>.
C. Band fill (strong signature candidate) A full-width band fills left to right with ink while the text inverts. The base state is the final filled state.
.band { position: relative; isolation: isolate; background: var(--ink); color: var(--bg); --focus: var(--bg); }
@media (prefers-reduced-motion: no-preference) {
.js-motion .band { background: var(--bg); color: var(--ink); --focus: var(--accent); }
.js-motion .band::before { content: ""; position: absolute; inset: 0; z-index: -1;
background: var(--ink); transform: scaleX(0); transform-origin: left;
transition: transform 950ms cubic-bezier(0.7, 0, 0.2, 1); }
.js-motion .band.is-visible::before { transform: scaleX(1); }
.js-motion .band.is-visible { color: var(--bg); --focus: var(--bg);
transition: color 350ms cubic-bezier(0.22, 1, 0.36, 1) 500ms; }
}
Markup: <section class="band" data-reveal>.
D. Rule draw
@media (prefers-reduced-motion: no-preference) {
.js-motion .draw-rule { transform: scaleX(0); transform-origin: left;
transition: transform 850ms cubic-bezier(0.22, 1, 0.36, 1); }
.js-motion .draw-rule.is-visible { transform: scaleX(1); }
}
E. Counter settle The final value, with unit, is already in the HTML. The counter fades in and counts up once; assistive tech reads the final value.
<span data-reveal data-count="1248" data-decimals="0" aria-label="1.248 Fahrten pro Jahr">1.248</span>
@media (prefers-reduced-motion: no-preference) {
.js-motion [data-count] { opacity: 0; transition: opacity 300ms cubic-bezier(0.22, 1, 0.36, 1); }
.js-motion [data-count].is-visible { opacity: 1; }
}
F. Hover / focus slab (supporting only)
A solid accent slab slides in under a row or link via transform. It never moves neighbors. Focus equals hover. Text on the slab reaches 4.5:1.
9.3 The script
Place it at the end of <body>.
function settle(el) {
const lang = document.documentElement.lang || "en";
const target = Number(el.dataset.count);
const dec = Number(el.dataset.decimals || 0);
const fmt = (n) => n.toLocaleString(lang, { minimumFractionDigits: dec, maximumFractionDigits: dec });
const t0 = performance.now();
el.setAttribute("aria-hidden", "true"); /* visible digits change; the aria-label keeps the final value */
const step = (t) => {
const p = Math.min(1, (t - t0) / 750);
el.textContent = fmt(target * (1 - Math.pow(1 - p, 3)));
if (p < 1) requestAnimationFrame(step);
else { el.textContent = fmt(target); el.removeAttribute("aria-hidden"); }
};
requestAnimationFrame(step);
}
(() => {
const root = document.documentElement;
if (!root.classList.contains("js-motion")) return;
try {
const pending = new Set(document.querySelectorAll("[data-reveal]"));
const show = (el) => {
if (!pending.delete(el)) return;
el.classList.add("is-visible");
if (el.dataset.count) settle(el);
io.unobserve(el);
};
const io = new IntersectionObserver((entries) => {
for (const e of entries) if (e.isIntersecting) show(e.target);
}, { rootMargin: "0px 0px -8% 0px" });
pending.forEach((el) => io.observe(el));
/* elements near the page end may never enter the inset root: reveal them at the bottom */
addEventListener("scroll", () => {
if (innerHeight + scrollY >= root.scrollHeight - 4) [...pending].forEach(show);
}, { passive: true });
} catch (err) {
root.classList.remove("js-motion"); /* any failure shows the final page */
}
})();
The head script sets js-motion before first paint, so there is no flash of visible content snapping to hidden. Elements already on screen at load are revealed by the observer's first callback.
10. Components and craft
- Buttons. One primary + one secondary. Solid fills.
min-block-size: 2.75rem. Text ≥ 4.5:1 on the fill. Hover changes surface or adds a rule, never glow or lift. Icon-only buttons needaria-label. - Navigation. ≤ 5 items, real anchors,
aria-current="page"(or"true"for in-page). Mobile menu works without JS: an in-flow list, or<details>/<summary>. - Forms. Visible labels above fields.
autocomplete,inputmode,typethat matches the data. Errors linked viaaria-describedby, announced witharia-live="polite". A real success state. MUST NOT ship decorative forms that do nothing: a form with no backend shows a clear demo success state and says it is a demo. - Icons. One SVG set, identical box and stroke weight,
currentColor. No emoji as icons or bullets. - Tables.
<caption>,scopeon headers, tabular numerals, hairline rules, right-aligned numbers, horizontal scroll inside its own container on mobile. - Header / footer. Footer shows real-looking address, contact, and imprint (fictional per §8.3), plus the "Demo" label for invented content. No fake social icons or logo strips.
- Links. Every
hrefgoes to an in-page anchor,mailto:, ortel:. External destinations only when the user supplied them. MUST NOT usehref="#". Body links stay underlined. - Print. Content must print: see §11.
11. Accessibility
- Skip link, one
<main>, labelled landmarks, logical DOM order. - Exactly one
h1; no skipped heading levels. - Contrast also applies to placeholders, borders, focus outlines, icon strokes (≥ 3:1).
- Touch targets ≥ 44 × 44 px; hover states mirrored on focus.
- Usable at 200% zoom and with enlarged default font size.
- Decorative SVG
aria-hidden="true"; informative SVG hasrole="img"and a label. - Nothing flashes or loops. Reduced motion is complete.
Add these blocks to the stylesheet:
@media (prefers-contrast: more) { :root { --ink-muted: var(--ink); --line: var(--ink); } }
@media (forced-colors: active) {
.band, .btn, .slab { border: 2px solid CanvasText; } /* backgrounds vanish; borders stay */
}
@media print {
.js-motion [data-reveal], .js-motion [data-reveal] * { opacity: 1 !important; transform: none !important;
clip-path: none !important; transition: none !important; }
.js-motion .band::before { transform: scaleX(1) !important; }
.skip-link { display: none; }
}
12. Product UI mode
For dashboards, tools, and apps, the hero archetypes, section pool, section counts, and the signature moment do not apply. The first screen is the working screen.
- Hierarchy over drama. Base text 0.875–1 rem, headings 1.25–2.5 rem. Display face only for the title and key figures; the text face does the work. Density is deliberate: dense for experts, roomy for occasional use.
- Structure. One persistent navigation, one primary action per view, filters and sort near the data they affect, real column headers, tabular numerals, right-aligned figures.
- Every screen has its states with realistic content: empty (says what to do next), loading (skeleton or text, no spinner alone), loaded, error (what failed and how to recover), success, disabled (with a reason).
- Data. Realistic, consistent, varied: long names, zero values, outliers, a failing row. Charts are inline SVG with labels directly on the data, a table alternative, and colors that survive grayscale.
- Interaction. Keyboard complete: logical tab order, visible focus,
Esccloses overlays, focus returns to the trigger. Destructive actions confirm or offer undo. - Motion. Feedback only: 120–200 ms on state changes (
transform,opacity). No reveals on scroll. - Identity still comes from type, one accent, and the subject's own vocabulary and objects (a ledger, a lab sheet, a flight strip), not from decoration.
13. Frameworks and delivery
Plain HTML/CSS/JS is the default: one file, <style> in the head, scripts as specified, no external assets beyond fonts. Use React or Tailwind only when asked. Tailwind: put tokens in theme.extend (colors, fonts, spacing), never framework palette defaults; use motion-safe: and motion-reduce:. React: tokens stay in a global stylesheet as CSS variables; the same gates apply. When the environment cannot load external fonts or scripts, keep the design working with the fallback stack and say so.
14. Anti-patterns and replacements
| Do not | Instead |
|---|---|
| Black-and-white / grayscale as default | Palette named after a material |
| Mid-tone on mid-tone | Ink on background at ≥ 7:1 |
| Navy + gold + cream as "premium" | The subject's own color, computed |
| Gradient hero, text, or buttons | Solid field, real image, hairline |
| Neon, glitch, scanlines, cyberpunk mono | Condensed display face + rules |
| Centered eyebrow + headline + sub + 2 buttons | Archetype built around a subject object |
| Three identical cards | Ledger, timeline, index, asymmetric pair |
| Logo strips, carousels, count-ups from zero | Table, quote panel, real number already in HTML |
| Floating blobs and decorative shapes | Rules, cropped figure, large numeral, subject SVG |
| Emoji as icons | One SVG set, or none |
| SaaS section sequence on any subject | Section list derived from the subject |
| Hidden, animated, or clipped hero headline | Base CSS + static headline |
| Placeholders and empty sections | Specific, consistent, marked example content |
| Marketing boilerplate | Concrete nouns, verbs, numbers |
| More than two motion patterns, any loop | One signature + optional supporting |
| Copying the example palette, pairing, or ferry idea | A choice justified by the subject |
15. Verification and report
Run what you can actually run and report honestly what you did not. A check you could not perform is listed as "not verified", never as passed.
f=${1:-index.html}; min=${MIN_SECTIONS:-6}; fails=0
fail() { echo "FAIL: $1"; fails=$((fails+1)); }
chk() { echo "CHECK: $1"; }
has() { grep -qiE "$1" "$f"; }
has '\[PRICE\]|example\.(com|org|net)|lorem ipsum|john doe|12345|href="#"' && fail "placeholder"
grep -q 'TODO' "$f" && fail "TODO left in file"
has 'seamless|cutting-edge|elevate your brand|unlock the power|revolutioni[sz]e|trusted by industry|journey starts here|nahtlos|nächste level' && fail "banned copy"
has 'linear-gradient|radial-gradient|conic-gradient|backdrop-filter|text-shadow' && fail "gradient or glow"
has 'box-shadow' && chk "box-shadow: colored or soft shadows are not allowed"
has '100vw' && fail "100vw"
has 'transition: *all' && fail "transition: all"
has 'outline: *(none|0)' && ! has ':focus-visible' && fail "focus outline removed"
has '<html[^>]*lang=' || fail "html lang missing"
has 'name="viewport"' || fail "viewport missing"
has '<title>[^<]{8,}' || fail "title missing or too short"
has 'name="description"' || fail "meta description missing"
has 'fonts.googleapis.com' && ! has 'display=swap' && fail "fonts without display=swap"
{ grep -q -e '--font-display:' "$f" && grep -q -e '--font-text:' "$f"; } || fail "font tokens missing"
has 'prefers-reduced-motion' || fail "no reduced-motion handling"
[ "$(grep -oi '<h1[ >]' "$f" | wc -l)" -eq 1 ] || fail "need exactly one h1"
grep -oiE '<img[^>]*>' "$f" | grep -viE 'alt=' | grep -q . && fail "img without alt"
[ "$(grep -oi '<section' "$f" | wc -l)" -ge "$min" ] || chk "fewer than $min sections (fine for Product UI)"
echo "unique hex values: $(grep -oE '#[0-9A-Fa-f]{6}\b' "$f" | sort -u | wc -l) (tokens + theme-color + favicon should account for all)"
echo "visible words: $(sed -e '/<style/,/<\/style>/d' -e '/<script/,/<\/script>/d' -e 's/<[^>]*>/ /g' "$f" | wc -w)"
[ "$fails" -eq 0 ] && echo "PASS: no automatic failures" || exit 1
Also check by hand or in a browser when you have one:
- Every ratio from §6 is computed and listed, per surface.
- Open with JavaScript disabled and with reduced motion: the page is complete, nothing is hidden.
- 375 px: no horizontal scroll, hero ≤ 5 lines, longest word fits (§4.6 rule).
- No two consecutive sections share structure; hover and focus produce the same state.
- Final test: would this palette and type fit a different subject unchanged? If yes, redo §5 and §6.
Checklist
- Mode chosen; brand or user content used when supplied
- Visual idea in one concrete sentence + originality test written out
- Palette named after a material, ratios computed per surface and stated
- Two families justified by the subject, URL weights match the weights used, fallbacks distinct
- Head, base CSS, tokens present; all values from tokens
- Hero static, fully visible on first paint, aligned, clean at 375 px (umlauts included)
- Archetype and layout logic named; consecutive sections differ
- Section minimum met, each section with real substance
- No placeholders, no dead links, no missing states, invented content marked and harmless
- One signature motion (+ optional supporting); reduced motion, no-JS, and print complete
- Contrast, focus (also inside bands), forced-colors, alt text checked
- No banned copy, emoji icons, gradients, or glow
- Correct
lang, diacritics, quotes, locale formats - Design would not fit another subject unchanged
Report (3–5 lines, in the user's language): sections and visible word count, computed ratios (or "estimated"), motion patterns used, originality test result, deviations with reasons, and anything not verified (browser rendering at 375 px, self-hosting of fonts for production).