
Hallmark
- 29.4k installs
- 18.9k repo stars
- Updated June 26, 2026
- nutlope/hallmark
Hallmark is a design skill that generates production-ready UI pages avoiding AI-generated aesthetics through theme selection and anti-pattern enforcement.
About
Hallmark is a design skill for Claude Code, Cursor, and Codex that refuses to look AI-generated. It picks a macrostructure for each brief, applies theme-specific rules, runs fifty-seven slop-test gates, and refuses on-distribution defaults every LLM was trained into. Two pages by Hallmark for different briefs feel like different sites, not color-swaps of the same template.
- Twenty catalog themes plus custom made-to-measure designs
- 57+ slop-test gates enforcing anti-generic standards
- Redesign verb to rebuild with different fingerprint
Hallmark by the numbers
- 29,443 all-time installs (skills.sh)
- +7,966 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #34 of 1,896 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
hallmark capabilities & compatibility
- Capabilities
- ui generation · design audit · design study
- Use cases
- ui design · web design
npx skills add https://github.com/nutlope/hallmark --skill hallmarkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 29.4k |
|---|---|
| repo stars | ★ 18.9k |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 26, 2026 |
| Repository | nutlope/hallmark ↗ |
How do you detect AI-generated UI slop before shipping?
Generate production-ready UI pages that avoid generic AI aesthetics while matching unique briefs.
Who is it for?
Creating distinctive, non-generic website pages and UIs
Skip if: Generic template-based websites or designs
When should I use this skill?
Building a new landing page, product page, or marketing site
What you get
Named anti-pattern audit report with per-tell fixes for hero gradients, typography, and layout tells
- Anti-pattern audit report
- Per-tell remediation guidance
Files
Anti-patterns — the named tells
The hallmark audit verb flags these by name. Every one of these is a signature of AI-generated UI. Seeing one is a problem; seeing two in the same view is a confirmation.
Each entry: the tell, why it reads as AI-generated, and the fix.
---
Critical (ships as slop)
The purple-gradient hero
A hero section with a background gradient from purple to blue or purple to pink, often with white centred text. This is the single most-recognised AI aesthetic.
Fix. Pick a single anchor hue. One accent. No gradient backgrounds on heroes. If you want warmth, tint the neutrals.
Inter-everywhere
Inter (or Roboto, or Open Sans) used as both display and body, with no pairing face. A one-font page is a template page.
Fix. Pair a distinctive display face with a refined body face. See `typography.md`.
The 3-column feature grid
Three equal columns, each with an icon above a two-line heading above a three-line body. Usually spanned full-width with 24px gap. Every LLM emits this.
Fix. Break the grid. Vary column widths. Mix card heights. Remove one card and use negative space. Move the icons inline, not above. Or drop the cards entirely and use typographic rhythm.
Card-in-card
A bordered container with cards inside it. Or: a card containing another card containing a small "micro-card". Visual nesting with no semantic reason.
Fix. Pick one containment layer. Usually the outer one is the wrong one.
The gradient headline
A headline with background-clip: text fill set to a linear gradient (usually purple-to-pink or blue-to-cyan). Signals "AI generated" faster than almost anything else.
Fix. Solid ink. If you want the headline to feel alive, use weight or italic or a display face — not a gradient fill.
The side-stripe card
A card with a thick coloured border on one edge (usually left, 4–6px, purple or green). Very recognisable; very 2018-SaaS-AI.
Fix. Use a hairline border all around, or no border, or a small accent square beside the heading. Never an asymmetric thick stripe.
Full-viewport centred hero
min-height: 100vh (or 100dvh), everything centred, one short sentence, one big CTA. The default LLM landing page.
Fix. Let the hero be the height of its content. Bias left or right. Put more than a sentence in it.
Pure black, pure white
#000000 background or #ffffff surface. Both read as flat and synthetic.
Fix. Tint toward your anchor hue. See `color.md`.
Default-attractor sameness
Two consecutive Hallmark outputs in the same project use the same macrostructure. The first emitted left-margin numbered labels + huge serif + asymmetric spans (Specimen); the second did exactly the same. The page looks redesigned only because copy changed.
Why it fails. Hallmark's whole point is that two pages for two briefs feel like different sites, not colour-swaps of one template. Repeating a macrostructure across outputs is the structural fingerprint of templating, which is the AI tell Hallmark exists to defeat.
Fix. Before writing code, look in the project's CSS for a /* Hallmark · macrostructure: <name> · ... */ stamp. If one exists, your pick must be a different macrostructure — categorically different where possible (a serif-led editorial macrostructure paired with a sans-led grid one, not two editorial variants). See `macrostructures.md` for the twenty-one named choices.
Specimen fall-through
Producing the Specimen macrostructure (numbered left-margin labels like 01 — HELLO. + huge serif display + asymmetric spans + hairline rules + typographic-only CTA + sometimes a hand-drawn SVG accent) when the brief did not explicitly request editorial / foundry / specimen energy. This is the single most-repeated Hallmark output, and it's the reason the skill felt like it had one shape.
Why it fails. Specimen is a beautiful pattern when the brief is editorial.
Anti-patterns — the named tells
The hallmark audit verb flags these by name. Every one of these is a signature of AI-generated UI. Seeing one is a problem; seeing two in the same view is a confirmation.
Each entry: the tell, why it reads as AI-generated, and the fix.
---
Critical (ships as slop)
The purple-gradient hero
A hero section with a background gradient from purple to blue or purple to pink, often with white centred text. This is the single most-recognised AI aesthetic.
Fix. Pick a single anchor hue. One accent. No gradient backgrounds on heroes. If you want warmth, tint the neutrals.
Inter-everywhere
Inter (or Roboto, or Open Sans) used as both display and body, with no pairing face. A one-font page is a template page.
Fix. Pair a distinctive display face with a refined body face. See `typography.md`.
The 3-column feature grid
Three equal columns, each with an icon above a two-line heading above a three-line body. Usually spanned full-width with 24px gap. Every LLM emits this.
Fix. Break the grid. Vary column widths. Mix card heights. Remove one card and use negative space. Move the icons inline, not above. Or drop the cards entirely and use typographic rhythm.
Card-in-card
A bordered container with cards inside it. Or: a card containing another card containing a small "micro-card". Visual nesting with no semantic reason.
Fix. Pick one containment layer. Usually the outer one is the wrong one.
The gradient headline
A headline with background-clip: text fill set to a linear gradient (usually purple-to-pink or blue-to-cyan). Signals "AI generated" faster than almost anything else.
Fix. Solid ink. If you want the headline to feel alive, use weight or italic or a display face — not a gradient fill.
The side-stripe card
A card with a thick coloured border on one edge (usually left, 4–6px, purple or green). Very recognisable; very 2018-SaaS-AI.
Fix. Use a hairline border all around, or no border, or a small accent square beside the heading. Never an asymmetric thick stripe.
Full-viewport centred hero
min-height: 100vh (or 100dvh), everything centred, one short sentence, one big CTA. The default LLM landing page.
Fix. Let the hero be the height of its content. Bias left or right. Put more than a sentence in it.
Pure black, pure white
#000000 background or #ffffff surface. Both read as flat and synthetic.
Fix. Tint toward your anchor hue. See `color.md`.
Default-attractor sameness
Two consecutive Hallmark outputs in the same project use the same macrostructure. The first emitted left-margin numbered labels + huge serif + asymmetric spans (Specimen); the second did exactly the same. The page looks redesigned only because copy changed.
Why it fails. Hallmark's whole point is that two pages for two briefs feel like different sites, not colour-swaps of one template. Repeating a macrostructure across outputs is the structural fingerprint of templating, which is the AI tell Hallmark exists to defeat.
Fix. Before writing code, look in the project's CSS for a /* Hallmark · macrostructure: <name> · ... */ stamp. If one exists, your pick must be a different macrostructure — categorically different where possible (a serif-led editorial macrostructure paired with a sans-led grid one, not two editorial variants). See `macrostructures.md` for the twenty-one named choices.
Specimen fall-through
Producing the Specimen macrostructure (numbered left-margin labels like 01 — HELLO. + huge serif display + asymmetric spans + hairline rules + typographic-only CTA + sometimes a hand-drawn SVG accent) when the brief did not explicitly request editorial / foundry / specimen energy. This is the single most-repeated Hallmark output, and it's the reason the skill felt like it had one shape.
Why it fails. Specimen is a beautiful pattern when the brief is editorial. Applied to a SaaS pricing page, a developer tool, an e-commerce site, or a personal app, it looks like the AI defaulted — because it did.
Fix. The Specimen macrostructure is one of twenty-one in `macrostructures.md`, not a default. If the brief is vague, pick from the first ten in that file (Bento Grid, Long Document, Marquee Hero, Stat-Led, Workbench, Conversational FAQ, Manifesto, Photographic, Quote-Led, then Specimen). Reach for Specimen only when the brief explicitly says "editorial", "specimen sheet", "type foundry", or names the Specimen theme.
The AI nav
Wordmark hard-left, 4–5 inline text links (Features · Pricing · Docs · Blog · About) centred or right-grouped, a CTA button hard-right, full viewport width, sticky on scroll, white background, 1 px hairline border-bottom. This is the most-recognised AI nav fingerprint — every LLM emits it because every SaaS site that fed the training data shipped it.
Why it fails. The shape is genre-blind: it lands the same on a wedding photographer's portfolio, a bakery, a B2B SaaS, and a manifesto. When the nav can't tell you what kind of site you're on, the page is templated.
Fix. Pick from the routing table in `component-cookbook.md` § Navigation. The genre routes you to one of N5–N9: Floating pill (modern-minimal / atmospheric), Newspaper masthead (editorial), Brutal slab (playful), Terminal command (CLI), Edge-aligned minimal (luxury / quiet). Reach for N1 only when the page genuinely has 2 destinations and the routing table allows it. State the rationale in a one-line comment.
The AI footer
4 columns of links (Product · Company · Resources · Legal), social-icon row beneath, copyright line at the very bottom, faint 1 px top-border, neutral grey background. Standard SaaS footer, identical across thousands of pages.
Why it fails. Same as the AI nav — the shape is genre-blind. A bakery doesn't have a "Resources" column. An editorial page doesn't have a four-link "Legal". The footer should close the page, not catalogue its absent sitemap.
Fix. Pick from the routing table in `component-cookbook.md` § Footers. Default to Ft1 Mast-headed, Ft2 Inline single line, Ft4 Dense colophon, Ft5 Statement, Ft6 Letter close, Ft7 Newsletter-first, or Ft8 Marquee scroll. Use Ft3 Index columns only on a genuine hub or docs root with a real sitemap — and even then, never with the social-icon row + tiny copyright tail.
Aurora-blob background
Flowing organic mesh blobs in purple-to-pink-to-cyan, layered behind hero text. Looks "premium" until you've seen it on every Dribbble shot since 2022.
Why it fails. It's the 2022–2023 generated-design default. Audiences pattern-match this in milliseconds: AI template.
Fix. Solid surface. Or a subtle two-stop CSS gradient + SVG <feTurbulence> grain at < 0.1 opacity. See `hero-enrichment.md` E7 for the recipe.
Floating-orb decoration
Ambient generic 3D spheres or blurred coloured circles drifting behind the hero, often added "for depth". They have no semantic role.
Why it fails. Generic 3D ambience is the new corporate-stock-photo. It implies "I needed something here, so I added something here."
Fix. Cut them. The hero doesn't need depth; it needs a strong typographic anchor.
Sound-on autoplay
A hero video that auto-plays with audio. Browsers block it anyway, but intent matters: a video element shipped without muted is a video that wanted to shout at the user.
Why it fails. Hostile to the audience. Accessibility fail. SEO penalty. Browser blocked.
Fix. <video autoplay muted loop playsinline> — always all four. A separate audio toggle button if sound is genuinely useful.
Lazy-loaded LCP
loading="lazy" on the hero image or hero video — the LCP element. The page waits to start downloading until the user scrolls to it, except they're already looking at it, so the page just sits there blank.
Why it fails. Tanks Largest Contentful Paint. Real-world data: lazy-loaded LCP images show p75 of 720 ms vs. 364 ms for preloaded — 2× slower, 4× more "poor" experiences.
Fix. fetchpriority="high" and preload="metadata" on the LCP element. Lazy-load only below-the-fold media.
---
Major (looks AI-generated)
Bounce and elastic easing
Buttons that bounce in, icons that wobble on hover. These easings were trendy a decade ago.
Fix. Exponential ease-out. See `motion.md`.
Centred everything
Headline centred, body centred, button centred, section after section of centred columns.
Fix. Bias the layout. Wide left margin, narrow right. Or the reverse. Breaking symmetry once is enough.
Italic headers
A roman headline with one word flipped to italic — "Built to think in real time" — or an all-italic display face used on every heading. The italicised emphasis-word-in-a-header is among the most reliable AI tells: it reads as "trying to look editorial" and appears on a huge share of generated pages.
Fix. Headers are roman (font-style: normal). Carry emphasis with weight, an accent colour, or a drawn underline beneath the word. Keep italic for body-copy emphasis inside running paragraphs only.
Eyebrow on every section
Every section starts with an uppercase mono-cap eyebrow — 01 / EXAMPLES, 02 / WHAT'S INSIDE, 03 / INSTALL, 01 · THE TOUR — above (or worse, beside) its heading. The labels look like editorial chapters but read as a tic. The page becomes a list of labelled lists.
Eyebrows are default OFF. They are not a stylistic flourish you reach for to look editorial — they are an ordinal device, valid only when the section is genuinely numbered or chaptered. Used as decoration they erase the hierarchy they were meant to create; when every section is "chaptered," none of them are.
Fix. Ship the page with zero eyebrows unless either (a) the user explicitly asked for chapter / step / section numbering, OR (b) the macrostructure is Long Document, Manifesto, or Catalogue numbered AND the content is genuinely ordinal (steps you must do in order, chapters that build, items in a deliberate sequence). Even then: cap at 1–2 per page.
Hard ban: tag-left / header-right two-column section heads. Patterns like 01 · THE TOUR in a narrow left column with the section title to its right (a.k.a. hanging header, left-margin label) are the single most reliable AI-templated tell for editorial-style SaaS pages. When an eyebrow IS used, the heading goes directly underneath it, in the same column — vertical stack only. Never a two-column section-head grid. Gate 54 enforces this.
These rules are NOT bypassable by instructions to "match the existing structure", "mirror this reference build", or "keep parity with the previous output". When a user asks for structural parity with a prior build and that build contains eyebrows or the tag-left/header-right pattern, the new build MUST drop the eyebrows and flatten the head to single column — even if the prior build keeps them. Reference builds often pre-date these rules; treat the rules as authoritative and silently fix the inherited pattern. Don't perpetuate a banned pattern to honour a parity instruction.
Shadow-glow on dark
A card on a dark background with a box-shadow that leaves a soft coloured halo around it.
Fix. On dark surfaces, use elevation via lightness (brighter surface = higher), not shadow. If you must shadow, keep it tight and dark.
Icon-tile feature card
Rounded rectangle, icon in a coloured square at the top-left, heading below it, two lines of copy, optional "Learn more →" link. The universal template.
Fix. If you need these, let them be asymmetric — vary sizes, vary alignments, pull the icon inline with the heading, or drop the icon entirely.
Glassmorphism without purpose
Frosted-glass panels everywhere — usually layered over a gradient that you also shouldn't have.
Fix. Glassmorphism can work when it communicates depth (overlay over content). It cannot work as decoration.
Hover-only affordances
Hover reveals a menu; hover shows a delete button; hover triggers a tooltip that contains crucial information. Touch users get nothing.
Fix. Every hover affordance has a focus state and is accessible via tap/click on coarse pointers.
Tabular data without tabular-nums
A list of prices, dates, or metrics where the numbers don't align vertically because the font uses proportional figures.
Fix. font-variant-numeric: tabular-nums; on any container displaying columns of numbers.
Animate-on-scroll on everything
Every section fades in when it enters the viewport. Every list staggers. The page never settles.
Fix. Pick one orchestrated entrance. Let the rest just be there.
Mismatched icon sets
Material Icons in the navbar, Heroicons in the feature cards, Lucide in the footer, an emoji "✨" in a hero badge. Each library has its own stroke voice; mixing them is the icon-set tell.
Why it fails. Icons are typography. You wouldn't ship a page with three different body fonts; don't ship one with three different icon strokes.
Fix. Pick one library per project. Lucide is the default for SaaS, Phosphor when you need weight variants, Heroicons for Tailwind/shadcn projects. See `assets.md` for the canon.
AI-illustration look
Smooth-mesh-blob characters with no joint articulation, mid-2010s "modern flat" stock poses, unmistakably-Midjourney compositions with the symmetric default lighting. Hand-drawn SVG humans (the "doodle person with one eye larger than the other") fall under this — corporate-doodle is the late-2010s Slack/Figma marketing template, and the audience reads it as AI immediately.
Why it fails. It reads as AI in milliseconds. The 2026 audience pattern-matches this faster than any other tell.
Fix. Hand-build the illustration in pure CSS or SVG (Tier A or B in `hero-enrichment.md`). If you must generate, use Nanobanana 2 or Recraft V4 with reference images, asymmetric crop, and grain post-processing — never raw output. See `custom-craft.md` Tier E.
Invented metrics
A stat-led layout, comparison row, or proof bar carrying numbers the user never supplied — "10× faster", "saves 5 hours per week", "trusted by 50,000+ teams", "99.9 % uptime", "+47 % conversion". The model reached for a stat to fill a stat slot and made one up.
Why it fails. Audiences read invented stats as fast as they read invented testimonials. A page that lies on its proof bar can't be trusted on its claims either, and the AI tell is unmistakable: every fabricated number reads "this was generated, not written".
Fix. Three options, in order of preference: (1) replace the number with — and a labelled grey block ("metric to confirm" or "stat pending"); (2) ask the user for the real number and pause the run; (3) rebuild the section without the proof slot — a stat-led macrostructure with no real stats is the wrong macrostructure. The number-shaped hole is honest; the fabricated number is slop. (Slop-test gate 46.)
Generic emoji as feature icon
A feature card, value prop, step number, or pricing tier with ✨ 🚀 ⚡ 🔥 🎯 ✅ rendered as the primary icon. The "sparkle hero" badge with a ✨ glyph beside the eyebrow. Emoji standing in for an icon library because the model didn't pick one.
Why it fails. Emoji are typography of a sort, but they are not part of the page's typographic system — they're rendered by the OS and look different on every device, they break the icon's stroke voice (you've now mixed a Phosphor-style line icon with a Twemoji blob), and the choice is recognisably the AI default. Sparkle-emoji-as-AI-shortcut is the cliché of the 2024–2025 era.
Fix. Pick a single icon library and ship it (assets.md names the canon). Or build a custom SVG mark. Or omit the icon entirely and lead with typography — most feature lists don't need icons. (Slop-test gate 30.)
Re-drawn UI chrome
A fake browser bar (URL pill + traffic-light dots) wrapping a screenshot. A fake phone frame (rounded rectangle + notch + speaker slit) around a mobile mockup. A fake code-block window (mock title bar + close/minimise dots) wrapping a <pre>. A fake IDE chrome (file tabs + activity bar) around an editor screenshot. All hand-built in HTML/CSS or SVG.
Why it fails. The user already has the chrome — their browser, their phone, their IDE all are chrome. Redrawing it in a page is like printing a photograph of a picture frame inside a real picture frame. The fakery is also bad: the URL is wrong, the dots aren't macOS dots, the notch is the wrong shape. Audiences pattern-match re-drawn chrome as "AI invented a UI that already exists" within a glance.
Fix. Use a real screenshot wrapped in <figure> (with a hairline border at most). For phone mockups, use a transparent-PNG device frame from a vendor or a real product photograph — never a CSS-drawn one. For code blocks, use the system <pre> with a typographic frame (top rule + label + bottom rule), not a faked window-chrome. The page's job is to show content, not to imitate the OS. (Slop-test gate 47.)
Mid-render token improvisation
A theme is selected at the top of the run, but the artifact contains inline colour values (#5b6cff, oklch(74% 0.18 245), rgb(...)) or font-family declarations that aren't drawn from the token block. Or: the artifact ships with the theme's token set plus one extra hex tucked into a hover state, a focus ring, or a single border. The model picked the theme, then drifted.
Why it fails. Token discipline is the difference between a system and a freestyle. Once a theme is locked, every colour and every font in the file must reference a named token (var(--color-accent), font-family: var(--font-display)). Inline values are how cohesion erodes — by the third edit pass, the page has eight colours instead of three, and the editorial restraint that made the theme work is gone. Audiences don't see the inline value, but they feel the looseness.
Fix. Every colour and every font in the artifact must come through var(--token-name). If you need a value that doesn't exist as a token, add it to the token block first (--color-accent-warm: oklch(...)) and then reference it. Inline OKLCH or one-off hex values mid-render are not allowed. (Slop-test gate 48. See also [SKILL.md § Locked tokens](../SKILL.md).)
Wrap-to-two-lines clickable text
A button label, nav link, footer link, breadcrumb, or CTA reads on two lines because the viewport got narrow and the label was long. Visually, the affordance now looks broken — readers can't tell whether the line break is intentional. Worst case: the second line is one word ("free", "more", "started"), which reads as a styling error.
Why it fails. Clickable affordances are one-line objects. The reader scans the label, decides whether to click, moves on. A two-line label slows the scan, breaks the row's vertical rhythm (button height grows, sibling buttons stay the same), and signals "this page wasn't tested at this width". It's a responsive-discipline tell.
Fix. In order of preference: (1) shorten the label — "Get started free" → "Start free"; "Read the documentation" → "Read docs". Most CTA labels are too long. (2) Set white-space: nowrap on the affordance and let the parent flex container reflow. (3) Drop a non-essential nav item at narrow widths via hidden=until-found or display: none. (4) Collapse the nav into a sheet/menu under a threshold. Never let a primary CTA or nav link wrap. (Slop-test gate 49. See [responsive.md § Clickable text — never wraps](responsive.md).)
Lottie shortcut
Reaching for a LottieFiles community animation — the spinning logo, the checkmark draw, the loading spinner, the "loading dots" loop — when pure CSS or hand-built SVG would have produced it stronger and lighter.
Why it fails. Lottie pulls were an AI-tool shortcut throughout 2023–2024; the audience now reads them as one. The 50–500 KB JSON file plus the runtime cost is a tax on a job CSS does in zero bytes.
Fix. Build it custom. Spinning logo → CSS @keyframes rotate. Checkmark → SVG stroke-dasharray animated. Loading dots → CSS @property + animation-delay. Lottie is Tier F in the enrichment hierarchy — last resort, only for genuinely articulated character motion.
Three.js for a still object
A WebGL hero where the 3D doesn't earn its place by being interactive. A stationary spinning thing the user can't touch, can't reorient, can't customise — just a model rotating because someone wanted "3D".
Why it fails. The 100–300 KB Three.js bundle, the model, the textures, the GPU work — all for a thing that could be a static photograph or an SVG.
Fix. If the user can't manipulate it, it doesn't justify Three.js. Use a still photograph or a hand-built SVG.
---
Microinteraction tells
These are the named tells of AI-generated motion. See `microinteractions.md` for the full catalogue and recipes.
transition-all
Every property animating, including ones that should be instant (visibility, focus rings).
Fix. Specify the properties. transition: background-color var(--dur-short) var(--ease-out), transform 100ms var(--ease-out).
Universal hover:scale-105
Every card lifts on hover, with no shadow change, no easing specified, no purpose.
Fix. Pick one signal per element. A 1px translate, or a colour shift, or an underline thickening — never all four.
Bouncy overshoot easings on UI
cubic-bezier(0.34, 1.56, 0.64, 1) and friends on buttons, modals, tooltips. Tasteless throwback.
Fix. Reserve overshoots for genuine physical interactions (drag-and-drop release). For UI state, use --ease-out from motion.md.
Animated hover gradients
Background gradient slides through colour space on hover.
Fix. Cut. Or pick one colour shift, instant.
Cursor follower dots
A trailing dot that lags behind the pointer.
Fix. Cut.
Auto-rotating carousels with no pause
WCAG 2.2.2 failure.
Fix. Manual advance only, or pause-on-hover-and-focus, or autoplay disabled by default.
Celebratory success toasts
"Done!" when the user just saved a thing they can see was saved.
Fix. Silent success. Toasts only for failures, async actions whose effect isn't visible, and explicit confirmations the user will need.
Confirmation dialogs for reversible actions
"Are you sure you want to delete this?" before a one-row delete.
Fix. Optimistic delete + 5–10s Undo toast. Reserve the modal for irreversible destructive actions, and even then, type-the-name confirmation, not click-OK.
Tooltips with the same delay on hover and focus
Both delay 800ms.
Fix. Hover delay 800–1000ms. Focus delay 0ms. Different intents, different timing.
Focus rings that animate in
The ring fades in over 200ms — keyboard users have no indicator at the start of the transition.
Fix. Focus rings appear instantly. Always. Don't transition outline or box-shadow when the element gains focus.
Toasts that shift layout
New toast pushes content down; dismissed toast lets it spring back.
Fix. Stack at a viewport corner, fixed positioning. Existing toasts don't move when a new one arrives.
Universal scroll-triggered fade-up
Every section fades in on intersection. The page never settles.
Fix. One orchestrated entrance on first load. After that, content is just there.
Spinners that flash
A spinner appears for 50ms while a fast action completes.
Fix. Either delay-show the spinner (150ms before showing) or enforce a minimum visible duration (300ms once shown). Skeletons over spinners when the layout is known.
---
Minor (small taste issues)
Straight quotes
"Hello" and 'word' in rendered text. A sign nothing was proof-read.
Fix. Curly quotes: "Hello", 'word'.
Double-hyphen dashes
-- in body copy where an em-dash belongs.
Fix. — (U+2014).
Three periods instead of ellipsis
... in body copy.
Fix. … (U+2026).
Placeholder names
"Jane Doe", "John Smith", "Example User".
Fix. Plausible placeholder names reflecting the audience, or pull from a seeded faker. "Maya Okonkwo", "Sam Tan", "Elena Ruiz".
Startup-cliché product names
"Acme", "Nexus", "Pulse", "Unleash", "Seamless", "Supercharge".
Fix. Name the thing concretely. If it's a demo, use a domain-specific placeholder — "Maple Weekly", "Ridgeline Inventory" — not abstract startup bingo.
z-index: 9999
Arbitrary large z-values.
Fix. Use the six-level named scale. See `layout-and-space.md`.
Every section padded the same
Top padding, bottom padding, horizontal padding — all equal across every section.
Fix. Vary. Tighten one, expand another.
100vw widths
width: 100vw on anything. Breaks on scrollbar-visible desktops.
Fix. width: 100% with container padding.
---
How hallmark audit should report
For each finding:
[severity] Tell name — file:line
why it's a tell (one line)
→ fix (one line)Then:
Summary — N critical · M major · K minor
Verdict — [ships as slop | reads as AI-generated | close, fix the minors]Assets — sourcing canon for icons, logos, illustrations, photography, video
This file is loaded when an enrichment archetype actually needs an external asset (load-on-demand). It catalogues the 3–5 canonical sources per category, the licence terms, the import patterns, the rules for using them, and the sources to avoid.
The reflex. Before reaching here, ask two questions in order: (1) Does the brief actually need imagery at all? See `hero-enrichment.md` § Image-need detection. (2) If yes, can it be hand-built? See `custom-craft.md`. The assets in this file are for the moments when both answers send you here.
---
Placeholder strategy
When imagery is needed and the user hasn't supplied real assets, pick from this canon — in order. Skipping tiers is the slop move.
| # | Source | When |
|---|---|---|
| 1 | Hallmark imagery kit (`imagery-kit.md`) | Brief allows non-photographic imagery: SaaS landings, manifestos, agency / studio splash, type-led portfolio, editorial-led marketing. Always preferred when the kit's register fits. |
| 2 | Hand-built SVG composition (Tier B from custom-craft.md) | Editorial-typographic brief where "imagery" can be a stamp / wordmark / colour-blocked composition. Use when the kit doesn't carry the register. |
| 3 | Picsum — https://picsum.photos/seed/<seed>/<w>/<h> | Generic photo slot, keyword anchoring not critical. Use a deterministic seed (brand-name + slot-name) so the same render produces the same image. |
| 4 | Unsplash Source — https://source.unsplash.com/<w>x<h>/?<keywords> | Keyword-anchored photo slot — food, travel, portrait, real product. Pass 1–2 specific keywords, never zero. |
| 5 | Local `public/placeholder-<type>.{jpg,svg}` | Self-contained projects with no third-party deps. Single neutral grey-block SVG checked into the repo. |
Swappability — non-negotiable:
- Every placeholder image carries an HTML comment immediately above it:
<!-- TODO: Replace with real <thing>, target size: <WxH> -->. - All placeholder URLs reference a single constant — a
--placeholder-baseCSS variable orPLACEHOLDER_BASEconfig constant. User edits one place to swap the entire site. - Alt text describes the intended subject ("Hand-thrown ceramic mug, top-down on linen") not the placeholder ("Picsum image"). When the user swaps in the real photo, alt is already correct.
Remote asset safety:
- Treat third-party image, logo, video, icon, and font URLs as prototype defaults, not production defaults. Before shipping production code, prefer vendored or self-hosted assets unless the user explicitly wants third-party hosting.
- Do not add a third-party script, tracking pixel, widget, or API dependency as an asset shortcut. Asset sources provide files; they do not get to execute code in the page.
- When remote assets remain in production, state the privacy and availability tradeoff in the handoff: visitors will request those third-party hosts, and the page depends on their uptime and integrity.
- For user-supplied brand or customer logos, prefer official asset pages or checked-in files. Do not hotlink a logo from an unrelated site.
Anti-patterns:
- Never inline base64 placeholder images (bloats CSS).
- Never call random Unsplash without keywords (returns un-curated stock-photo-ish results).
- Never use kittens / lorempixel / "tiger.jpg" / cute-default services. The placeholder must read as an obvious slot, not as content.
- Never ship a kit image where the brief actually calls for a real product photo (e.g. abstract bottle for an actual coffee-shop hero). The kit is for atmosphere; photos are for subject.
---
Icons
Canon
| Library | URL | Count | Best for |
|---|---|---|---|
| [Lucide](https://lucide.dev) | lucide.dev | 1,600+ | Modern SaaS / dev-tool default. The 2026 baseline. Active maintenance. |
| [Phosphor Icons](https://phosphoricons.com) | phosphoricons.com | 9,000+ across 6 weights (thin / light / regular / bold / fill / duotone) | Tonal variants without mixing sets. The right pick when you need different weights of the same icon for emphasis. |
| [Heroicons](https://heroicons.com) | heroicons.com | ~300 | Tailwind / shadcn projects. Tightly curated, opinionated. |
| [Tabler Icons](https://tabler-icons.io) | tabler-icons.io | 5,900+ on a 24×24 grid | Breadth — when neither Lucide nor Heroicons covers the symbol you need. |
| [Iconoir](https://iconoir.com) | iconoir.com | ~1,500 | Hand-drawn character with a generous free tier. |
The rules
1. Pick one library per project. Mixing Material + Heroicons + Lucide on the same page is the icon-set tell. The skill's audit verb catches this. 2. Sizes 16 / 20 / 24 / 32 only. Snap to grid. 18-px icons don't exist in this canon. 3. Stroke 2 px default (most libraries' regular weight). Switch to bold (2.5 px) only for icons under 20 px or as emphasis. 4. Monochrome with `currentColor`. Icons inherit text colour. Brand-coloured icons only on the singular primary CTA — not as decoration. 5. No emoji-as-icon. Emoji break alignment, accessibility, and brand consistency. Use a real icon library.
Import patterns
// Lucide — React (most common)
import { ArrowRight, Check, X } from "lucide-react";
<ArrowRight size={20} strokeWidth={2} />
// Phosphor — React, with weight prop
import { ArrowRight } from "@phosphor-icons/react";
<ArrowRight size={20} weight="regular" />
// Heroicons — React or static HTML
import { ArrowRightIcon } from "@heroicons/react/24/outline";
// Tabler — vanilla HTML via CDN
<svg width="20" height="20"><use href="https://cdn.jsdelivr.net/npm/@tabler/icons@latest/icons/arrow-right.svg" /></svg>Avoid
- Font Awesome free — bloated, dated. The 2018-SaaS look; 600+ generic glyphs that all read as "I picked the icons before designing the page".
- Material Icons in a non-Material project — gives a Google look that doesn't match anything else.
- Icon packs with inconsistent stroke widths — pick a library whose icons share weight; eclectic mixes read as random.
- Emoji as semantic icons — colour, size, weight, alignment all uncontrolled.
---
Brand / company logos
Canon
| Source | URL | Count | Best for |
|---|---|---|---|
| [Simple Icons](https://simpleicons.org) | simpleicons.org | 3,400+ | The industry standard. Monochrome SVG + official hex per brand. MIT licensed. The default for logo walls. |
| [SVGL](https://svgl.app) | svgl.app | 600+ | Curated, hand-picked, no spam. Higher quality bar than Simple Icons. |
| [theSVG](https://thesvg.org) | thesvg.org | 4,000+ with dark/light/mono/wordmark variants | npm + MCP server for AI. Superset over Simple Icons if your stack supports it. |
| [Brandfetch](https://brandfetch.com) | brandfetch.com | 22M+ brands | Paid API. Logo + colours + fonts + guardrails. Useful when building a CMS / form that asks "what's your domain?" and back-fills the brand. |
| Official brand asset pages | Per company | — | Always check first if accuracy matters. Most brands ship a media-kit page (e.g., vercel.com/design). |
The rules
1. Logo walls: monochrome only. Use Simple Icons' default colour or the official monochrome variant. Mixed full-colour logos read as 2018-SaaS. 2. Height-aligned, not width-aligned. Pick a baseline (typically 32–48 px height) and let the width float. Brand proportions matter; stretching is a tell. 3. 2–3× height as gutter. Logos need breathing room. A wall with 32-px logos wants 64–96-px gutters. 4. No hairline borders, no glow halos. Just the marks on the page.
Import patterns
<!-- Simple Icons via CDN — easiest -->
<img src="https://cdn.simpleicons.org/github" alt="GitHub" height="32">
<img src="https://cdn.simpleicons.org/figma/aaaaaa" alt="Figma" height="32"> <!-- monochrome override -->
<!-- npm -->
<!-- npm install simple-icons -->
import { siGithub } from 'simple-icons/icons';
<svg viewBox="0 0 24 24"><path d={siGithub.path} fill="currentColor" /></svg>
<!-- SVGL via API -->
<img src="https://api.svgl.app/?slug=vercel" alt="Vercel">Avoid
- Full-colour logo grids. Visual chaos; reads as 2018.
- Stretched / squished marks. Always preserve aspect ratio.
- Placeholder customer logos from template kits ("ACME", "Initech", "Hooli"). Use real customer logos, or skip the wall entirely — fake social proof is worse than no social proof.
- Mixing wordmarks with marks. Pick a treatment (all-monogram or all-wordmark) for any single wall.
---
Generated illustration (Tier C in the enrichment hierarchy)
When characters or specific scenes can't be hand-built economically. Always post-process. See `custom-craft.md` Tier E for full discipline.
Canon
| Model | URL | Cost | Best for | Output |
|---|---|---|---|---|
| [Nanobanana 2 / Gemini 2.5 Flash Image](https://ai.google.dev/gemini-api/docs/image-generation) | Google AI | $0.039 / image | Character consistency across panels, fast iteration, brand-style adherence via reference images, infographics with text | PNG (transparent supported) |
| [Recraft V4](https://www.recraft.ai/) | recraft.ai | ~$0.04 / image | The only model with production-grade SVG output. Logos, icons, illustrations that need to scale. | SVG + PNG |
| [Midjourney v8](https://www.midjourney.com) | midjourney.com | ~$0.14 / image | Aesthetic beauty, atmospheric stills, artistic direction | PNG |
| [Flux 2](https://blackforestlabs.ai/) | blackforestlabs.ai | ~$0.03 / image | Photorealism — skin, fabric, product detail, hands | PNG |
The rules
1. Always post-process. Add grain, asymmetric crop, hand-drawn overlays, colour grading. Raw model output reads as AI 100 % of the time. 2. Use reference images for brand consistency. Nanobanana 2's character-consistency feature is its differentiator vs. Midjourney; feed it your brand assets so generations stay on-style. 3. Stamp the model in the macrostructure comment (generated: nanobanana-2 · post-processed). Provenance matters. 4. Verify SynthID watermark is present on Google-generated images. 5. No animation. None of these models output multi-frame; assemble via custom-craft if motion is needed.
Avoid
- Symmetrical compositions — algorithmic; the AI tell. Always crop asymmetrically.
- Smooth-mesh-blob faces — the 2024 generic AI character look.
- Default lighting + blue-tinted backgrounds — the generic AI aesthetic. Specify brand-anchored colour and unusual lighting in the prompt.
- Six fingers / doubled furniture / impossible rooms — less common in 2026 but still lurking. Inspect.
- Shipping unmodified output — see rule 1.
Prompting recipe (Nanobanana 2)
Subject: <one specific concrete subject> in <one specific concrete pose>.
Style: <named style — "risograph print", "1960s editorial illustration",
"ink-on-paper line drawing", NOT "modern flat" or "clean illustration">.
Composition: asymmetric, <off-centre subject>, <unusual crop>.
Lighting: <named lighting — "side-lit, late afternoon", "overcast diffuse">.
Reference: <attach brand asset / mood board for character consistency>.
Constraints: no smooth mesh-gradient, no aurora background, no symmetric layout,
no smiling people-on-laptops poses.A specific prompt produces a specific image. A generic prompt produces the AI tell.
---
Library illustrations (Tier D — not first choice)
When budget and timeline force a shortcut and even Tier C is overkill.
Canon
| Source | URL | Licence | Best for |
|---|---|---|---|
| [Storyset](https://storyset.com) | storyset.com (Freepik) | Free with attribution; paid removes | Animated SVG illustrations with toggleable element animation and on-site colour customisation. Onboarding flows, feature explanations. |
| [Humaaans](https://www.humaaans.com) | humaaans.com (Pablo Stanley) | CC0 | Mix-and-match characters with diverse poses / outfits / skin tones. Hero sections that need humans without stock-photo territory. |
| [unDraw](https://undraw.co) | undraw.co | MIT | Open SVG illustrations with on-export colour swap. Still respected if customised — saturated and instantly recognisable if not. |
| [IRA Design](https://www.iradesign.io) | iradesign.io (Creative Tim) | Free / paid | Moody, sophisticated, isometric scenes for B2B / enterprise. |
| [Open Peeps](https://www.openpeeps.com) | openpeeps.com | CC0 | Hand-drawn character library, naive style. Sits between photography and illustration. |
The rules
1. Always customise colour to your brand anchor hue. The library default colour is the library look. Swap it. 2. Crop or recompose if you can. The unmodified illustration is on a hundred competitor sites; even a crop change differentiates. 3. One library per project. Mixing Storyset + Humaaans + unDraw = visual chaos. Pick one, stick to it. 4. Avoid the giveaway poses — guy on laptop with floating speech bubble, woman in headset on cloud, character holding giant phone. Whatever you saw on Dribbble in 2021, audiences saw too. 5. Commission custom for >3 uses. If the illustration appears in the hero, a feature block, AND promotional material, the per-piece commission cost ($200–$600 freelancer, $399–$999/month subscription for unlimited) wins on brand consistency over libraries.
Avoid
- Open Doodles — dated. The 2019 hand-drawn aesthetic that's been displaced by 2026's tactile rebellion.
- "Modern flat" generic poses — that whole aesthetic is the AI training-distribution default.
- AI-generated illustrations put through library filters — the worst of both worlds.
- Stock photography with character cutouts pasted on top — fights physics, looks haunted.
---
App mockups / device frames
Canon
| Source | URL | Best for |
|---|---|---|
| [Browserframe](https://browserframe.com) | browserframe.com | Browser + mobile device frames with annotation. Built for SaaS demo screenshots. |
| [Ray.so](https://ray.so) | ray.so | Code snippets in macOS window frames. Perfect for developer-tool landing pages. |
| [Cleanmock](https://cleanmock.com) | cleanmock.com | Mobile device frames; minimalist; good for app-store-listing-style heroes. |
| [Mockup.style](https://mockup.style) | mockup.style | Versatile device + browser builder, Figma-friendly export. |
| [Device Shots](https://deviceshots.com) | deviceshots.com | Free device generator with multiple frame styles, fast turnaround. |
The rules
1. Browser frame for SaaS / web apps. Communicates "this is real, on the web". Use Browserframe or hand-build (a 1-px hairline + three macOS dots is enough). 2. Floating-no-frame for clean splits. When the screenshot is beautiful enough to stand naked. Demands a high-quality screenshot. 3. Device frame (iPhone / iPad) sparingly. One hero mockup max — beyond that it reads as generic template work. 4. Tilt 1–3°. Adds life. 0° reads as flat; 5°+ reads as drunk. 5. Numbered-pin annotations only. Numbered circles (1, 2, 3) with a corresponding callout legend below. No arrow-and-label callouts (dated 2018 UX). Label only the novel features, not the obvious.
Avoid
- Glossy plastic device bezels — looks 2015. Use minimalist frames or no frame.
- Annotation chaos — more pins than pixels. Three numbered pins is a lot; five is too many.
- Stretched aspect ratios — never resize a mockup beyond its natural ratio.
- Visible Figma prototyping artifacts in the screenshot (ghost-out frames, "hover" indicators). Clean the export.
---
Hero / demo video
Canon (when you don't have your own footage)
| Source | URL | Licence | Best for |
|---|---|---|---|
| [Mixkit](https://mixkit.co) | mixkit.co (Envato) | No registration, no attribution required, 1080p+ HD | The quality-to-effort sweet spot. |
| [Coverr](https://coverr.co) | coverr.co | Free commercial use | Optimised for hero-section backgrounds and ambient loops. |
| [Pexels Videos](https://www.pexels.com/videos/) | pexels.com/videos | CC0 | Largest free library; 4K available. Volume play. |
| [Videvo](https://www.videvo.net) | videvo.net | Tiered (free + pro) | Community footage + motion graphics. |
The rules
1. Codec chain in the `<source>` order: AV1 → WebM VP9 → MP4 H.264. Browsers pick the first they support. AV1 is 30–50 % smaller than H.264 at equivalent quality; H.264 is the universal fallback. 2. Always autoplay-muted-loop-playsinline.
<video autoplay muted loop playsinline preload="metadata"
poster="/hero-poster.webp" fetchpriority="high">
<source src="/hero.av1.mp4" type='video/mp4; codecs="av01.0.05M.08"'>
<source src="/hero.vp9.webm" type="video/webm">
<source src="/hero.h264.mp4" type="video/mp4">
</video>3. Always include a `poster=""` — prevents layout shift, gives reduced-motion users a static fallback. 4. `fetchpriority="high"` on the LCP element. Never `loading="lazy"` on the hero — that kills LCP. 5. VTT captions for accessibility. Even on muted demo loops; people may unmute. 6. No sound on autoplay. Browsers block it anyway, but the principle is firm.
Compression
- [ffmpeg](https://ffmpeg.org) for control:
- VP9:
ffmpeg -i input.mp4 -c:v libvpx-vp9 -b:v 0 -crf 30 -c:a libopus -b:a 128k output.webm - AV1:
ffmpeg -i input.mp4 -c:v libaom-av1 -crf 30 -c:a aac output.mp4 - H.264:
ffmpeg -i input.mp4 -c:v libx264 -preset slow -crf 23 -c:a aac output.mp4 - [HandBrake](https://handbrake.fr) for GUI / batch: start with the "Vimeo YouTube HQ 1080p" preset, drop bitrate to 3–4 Mbps for web.
Avoid
- Watermarked stock — visible "Pexels.com" stamps in the corner.
- 30 fps labelled as 60 fps — reveals itself on modern displays.
- Music-heavy demos without a mute toggle — alienates accessibility users and noisy environments.
- `loading="lazy"` on hero video — kills LCP, tanks Core Web Vitals.
---
Photography
Canon
| Source | URL | Licence | Best for |
|---|---|---|---|
| [Unsplash](https://unsplash.com) | unsplash.com | CC0 | Largest free collection, moody / cinematic, weekly community uploads. The starting point. |
| [Pexels](https://www.pexels.com) | pexels.com | CC0 | 3.5M+ free photos, diverse photographers. |
| [Nappy.co](https://www.nappy.co) | nappy.co | Free + paid | Curated for diversity and representation. Premium visual direction. |
| [Shotstash](https://www.shotstash.com) | shotstash.com | Free | Lifestyle / minimal aesthetic. Smaller but carefully curated. |
| [Open Peeps](https://www.openpeeps.com) | openpeeps.com | CC0 | Illustrated character library when you want diversity without the photo-stock look. |
The rules
1. Always tweak the source. Gradient overlay, crop, desaturation, blur, or brand-colour wash. The unmodified Unsplash photo is on a hundred competitor sites; even a crop change differentiates. 2. Match tone to brief. Enterprise / B2B: neutral palettes, natural lighting, real workspaces. Consumer / lifestyle: warm lighting, human emotion. Tech / startup: minimal backgrounds, hands-on interaction. 3. Diverse representation. Nappy.co is the best free source for intentional curation; Unsplash and Pexels carry diversity but require search effort. 4. Aspect ratios that fit. Hero photography typically wants 16/9 desktop, 4/3 or 9/16 mobile.
Avoid
- Photos with visible logos / trademarks — copyright risk.
- Over-processed HDR — looks dated, unrealistic.
- Staged "team photo" shots — generic, reads as stock.
- Unmodified Unsplash — a hundred competitor sites used the same photo this week.
---
Abstract backgrounds
Canon
| Source | URL | Output | Best for |
|---|---|---|---|
| CSS gradients (native) | n/a — write them | Zero bytes, GPU-composited | The default. Linear or radial; 2–3 colour stops max. |
| [Mesh Gradient Generator](https://www.learnui.design/tools/mesh-gradient-generator.html) | learnui.design tools | Figma / SVG export | Apple-style mesh gradients; export carries organic noise. |
| [fffuel.co](https://www.fffuel.co/) | fffuel.co | SVG | gggrain for grain noise; ffflux for fluid gradients; uuunion for wavy meshes. Composable. |
| [CSS Gradient](https://cssgradient.io) | cssgradient.io | CSS strings | Quick gradient picker; copy-paste ready. |
The rules
1. CSS gradients first. Zero bytes; scale infinitely; animate smoothly with @property. If a CSS gradient does the job, never reach for SVG or images. 2. Two to three colour stops. More than three reads as generated. Pick stops that share hue and step in lightness. 3. Grain via SVG `<feTurbulence>` at < 0.1 opacity, mix-blend-mode: multiply. Cheap, no asset, looks like paper. 4. Hero or accent card only — never page-wide. A 100-vh gradient is a tell; a 40-vh hero gradient with the rest of the page on flat paper is intentional. 5. No animation on whole-page gradients. A subtle 30-s drift on a hero accent is allowed; a slowly-rotating mesh-gradient on the entire page is the new aurora-blob anti-pattern.
Recipe (CSS gradient + SVG grain)
.hero {
background:
linear-gradient(135deg,
color-mix(in oklch, var(--color-paper) 100%, var(--color-accent) 4%),
color-mix(in oklch, var(--color-paper) 100%, var(--color-paper-2) 50%));
position: relative;
}
.hero::after {
content: "";
position: absolute; inset: 0;
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg'><filter id='n'><feTurbulence baseFrequency='0.9' numOctaves='2'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
opacity: 0.06;
mix-blend-mode: multiply;
pointer-events: none;
}Avoid
- Aurora blobs — the 2022 Dribbble look. Critical anti-pattern.
- Purple-to-cyan mesh — the 2023 default. Critical anti-pattern.
- Floating orbs / spheres — generic 3D ambient. Critical anti-pattern.
- Particle / starfield — 2010s nostalgia, distracting.
- Animated mesh-gradient on the whole page — modern equivalent of the rotating gradient banner.
---
Lottie / Rive (Tier F — last resort)
Canon
| Source | URL | Best for |
|---|---|---|
| [LottieFiles](https://lottiefiles.com) | lottiefiles.com | The Lottie ecosystem. Free + pro tiers; npm + CDN; Figma plugin; AI creator. |
| [Rive](https://rive.app) | rive.app | Interactive real-time animations with state machines. Native runtime; better for app UI micro-interactions than Lottie. |
The rules
1. Lottie is last resort. Reach for it only when complex character motion can't be hand-built. See `custom-craft.md` Tier F. 2. Custom-commissioned over library pulls. A LottieFiles community animation that fits your brand exists; one that fits and doesn't look like every other LottieFiles community animation is rare. Commission ($100–$300 on Upwork; $1,000+ from a studio) for hero work. 3. < 2 MB file size. Anything heavier loses to its own loading state. 4. Pause / resume support. Required for accessibility (motion-sensitive users need control). 5. Reduced-motion fallback to a static keyframe. Required. 6. Don't use Lottie for what CSS can do. Spinning logos, checkmark draws, loading spinners, hover micro-interactions — all CSS territory. The skill catches the "Lottie shortcut" anti-pattern in its slop test.
Avoid
- 2019-era over-smooth animations. Looks dated, lacks character.
- Animations heavier than the page itself — 5 MB Lottie files for a 200 KB page.
- Animations without pause / resume — accessibility fail.
- LottieFiles community pulls used unmodified — reads as "I picked this from a library".
---
Quick-reference: which source for which job
| Need | First reach | Second reach |
|---|---|---|
| UI icon (chevron, check, X) | Lucide | Phosphor / Heroicons |
| Brand logo for a wall | Simple Icons | SVGL / theSVG |
| A hero illustration the brand owns | Hand-build (Tier A or B) | Commission custom |
| A hero illustration that's character-driven | Nanobanana 2 (Tier C) | Commission, then library |
| An SVG-format illustration that needs to scale | Recraft V4 | Hand-build in Figma → SVG |
| A photograph with diversity | Nappy.co | Unsplash with manual tone-tweak |
| A demo video of your product | Custom screen recording | (skip; no stock fits) |
| A textured background | CSS gradient + SVG grain | Mesh Gradient Generator |
| A character animation | Custom Lottie commission | LottieFiles community + customise |
| A loading spinner | CSS conic-gradient | (don't reach for Lottie) |
| A checkmark draw on confirm | SVG stroke-dasharray | (don't reach for Lottie) |
When in doubt: build it. The path of least resistance and the path of least-AI-tell are the same path in 2026.
Colour
Most AI-generated UI fails on colour. It picks blue. It uses pure black. It draws a gradient from purple to cyan. It leaves accents on 30% of the page. Fix all of this.
Principles
- OKLCH only. Perceptually uniform; predictable lightness; consistent hue across tints.
hsl()andrgb()lie about brightness. - One accent. Maximum two. Everything else is neutral. The accent should occupy 3% or less of any given viewport.
- No pure extremes. No
#000, no#fff. Always tint with a trace of chroma toward the palette's anchor hue. - Tint the greys. If your anchor hue is orange, your neutrals lean warm. If it's blue, they lean cool. A page with a warm accent and cool grey body copy looks wrong and most people can't name why.
Palette construction
A complete Hallmark palette has four layers.
1. Paper — the base surface. oklch(96–98% 0.005–0.015 <anchor hue>) for light mode, oklch(12–16% 0.008–0.015 <anchor hue>) for dark. 2. Ink — the primary text. oklch(16–22% 0.005–0.015 <anchor hue>) for light mode, oklch(92–96% 0.005–0.01 <anchor hue>) for dark. 3. Neutrals — 5 to 9 steps between Paper and Ink, each with the anchor's chroma tint at low values (0.005–0.015). 4. Accent — one saturated colour with meaningful chroma (0.12–0.22). Used for links, active states, highlights, focus rings. Never as a background fill that covers more than a few percent of the surface.
Example (warm-oat anchor, hue 80):
:root {
--color-paper: oklch(96% 0.012 80);
--color-paper-2: oklch(93% 0.014 80);
--color-rule: oklch(82% 0.010 80);
--color-neutral: oklch(56% 0.008 80);
--color-muted: oklch(40% 0.008 70);
--color-ink: oklch(18% 0.010 60);
--color-accent: #FC4C02; /* signal orange */
--color-focus: oklch(55% 0.19 55);
}Example (midnight anchor, hue 40):
:root {
--color-paper: oklch(14% 0.008 40);
--color-paper-2: oklch(18% 0.010 40);
--color-rule: oklch(30% 0.008 40);
--color-neutral: oklch(58% 0.008 40);
--color-muted: oklch(72% 0.006 40);
--color-ink: oklch(94% 0.006 80);
--color-accent: #FC4C02;
--color-focus: oklch(70% 0.19 55);
}Contrast
Use the APCA contrast check when you can; otherwise WCAG 2.1 ratios.
| Content | Minimum | Target |
|---|---|---|
| Body text | 4.5:1 | 7:1 |
| Large text (≥ 18.66px bold or 24px) | 3:1 | 4.5:1 |
| UI component boundaries | 3:1 | 4.5:1 |
| Placeholder / helper text | 4.5:1 | 4.5:1 |
Verify with the browser devtools vision-deficiency emulator before shipping.
Dark mode recipe
- Paper: lightness 12–18% (not
#000). - Ink: lightness 92–96% (not
#fff). - Body font-weight: reduce by 50 units (400 → 350) to compensate for the optical weight of light text on dark.
- Accent: reduce chroma by 0.02–0.04; increase lightness by 5–10%.
- Elevation: higher surfaces are lighter, not darker. Add ~3% lightness per level.
- Never switch the hue between modes. Keep the anchor. Only lightness and chroma move.
Bans
- Pure `#000000` anywhere. Use
oklch(16% 0.01 <hue>)or similar. - Pure `#ffffff` as a base surface. Use a tinted paper.
- Flat grey (
oklch(L 0 H)with zero chroma). Add at least 0.005. - Purple-to-cyan gradients, purple-to-blue gradients, orange-to-pink gradients. Every LLM picks these. Don't.
- Accent as background fill covering more than ~5% of any view.
- Grey text on coloured background. Always reads washed out.
- Red–green pairing as the only signal. Add an icon or pattern.
- Alpha transparency as the definition of a colour. If it's a named token, it's opaque. Transparency is a modifier for overlays and shadows, not a palette.
- Three-colour gradients. Two-stop gradients only. The third stop is vanity.
Use of the accent
The accent is a highlighter, not a colour block. Reach for it to:
- Mark an active nav item.
- Draw a focus ring.
- Underline a link on hover.
- Indicate a primary CTA's border or text.
- Place a small square beside a heading as a visual anchor.
Do not fill giant buttons with it. Do not set whole sections on it. Do not use it for decorative gradients. If you feel the urge to use more, that's the slop defaulting. Use less.
Component cookbook
Fifty component archetypes you can compose into any macrostructure. Every entry: a shape, a one-line "use when", a one-line "don't confuse with", and a short structural sketch (DOM + minimal CSS). Pick from this file when you're building a section and don't know which shape to reach for.
The same macrostructure (e.g., Bento Grid) can be built from many different combinations of these archetypes. The macrostructure picks the page shape; this file picks the components inside it.
Diversification rule: within a single page, no two sections should use the same archetype. A Bento Grid might pair Bento feature block with Inline form CTA with Logo wall (hairline). The next page Hallmark builds should pick different archetypes from the same categories.
---
---
Archetype index — load ONLY the picks you need
Pick your archetype names here, then read ONLY those individual files from references/components/. Do not load the whole cookbook. A typical build needs 5–7 files: 1 hero + 1 section head + 1–2 features + 1 CTA + 1 footer + 1 nav.
Heroes
- H1 · Marquee — A single statement fills the fold. No subhead, no CTA in view. `components/h1-marquee.md`
- H2 · Split diptych — Headline + lede on one side, image or product capture on the other. 6/6 or 7/5 columns. `components/h2-split-diptych.md`
- H3 · Quote led — A pull-quote with attribution is the hero. Your headline is borrowed credibility. `components/h3-quote-led.md`
- H4 · Stat led — A giant number or metric is the hero. A small qualifier line below. `components/h4-stat-led.md`
- H5 · Letter hero — First-person opening — "Dear reader,". No buttons in fold. Reads as personal correspondence. `components/h5-letter-hero.md`
- H6 · Photographic fold — Single full-bleed image fills the viewport. Caption sits in a corner. `components/h6-photographic-fold.md`
- H7 · Demo video clipped by viewport edge — Display headline left, demo video right, the rightmost ~10–20 % extending past the viewport so it's intentionally cut off. The clip is the design — implies "there's more product `components/h7-demo-video-clipped-by-viewport-edge.md`
- H8 · Mockup split browser framed — Headline left, browser-frame mockup right, the mockup tilted 1–3° for life. Frame can be browser chrome, macOS toolbar, minimal hairline, or floating no-frame. `components/h8-mockup-split-browser-framed.md`
- H9 · Custom illustration centerpiece — A single hand-built SVG (Tier B in the enrichment hierarchy — or pure CSS at Tier A for simpler shapes) sitting on the hero as one illustrative element. The bakery loaf, the studio `components/h9-custom-illustration-centerpiece.md`
Section heads
- S1 · Left margin numbered — A narrow left column holds
01 — LABEL.; the wide right column holds the heading and content. `components/s1-left-margin-numbered.md` - S2 · Hanging — Heading floats above the section in negative space; no border, no rule. `components/s2-hanging.md`
- S3 · Sticky pinned — Heading remains in viewport while content scrolls beneath. Orientation aid. `components/s3-sticky-pinned.md`
- S4 · Inline no break — The heading is a small caps phrase that emerges inside the body flow; no spatial break. `components/s4-inline-no-break.md`
- S5 · Bottom anchored — The label or heading sits below the section's content. Inverts hierarchy. `components/s5-bottom-anchored.md`
Feature blocks
- F1 · Bento grid — Asymmetric grid of 8–15 tiles in mixed spans (1×1, 2×1, 1×2, 2×2). Visual rhythm via size. `components/f1-bento-grid.md`
- F2 · Sticky scroll stack — Sticky left pane, scrolling right pane that cycles through related screenshots. `components/f2-sticky-scroll-stack.md`
- F3 · Tabular spec sheet — Each row is a feature; columns hold name, value, footnote. Hairline rules between rows. Tabular numerics. `components/f3-tabular-spec-sheet.md`
- F4 · Step sequence — Numbered stages (
1.0 → 2.0 → 3.0) flow vertically. Each stage has a heading, a paragraph, sometimes a small visual. `components/f4-step-sequence.md` - F5 · Annotated screenshot — A product capture sits centre-stage with arrows or short labels pointing to UI details. `components/f5-annotated-screenshot.md`
- F6 · Product card grid — Each card is a product, not a feature. Image · name · price · one micro-action. Reads like a shop floor, not a marketing site. `components/f6-product-card-grid.md`
CTAs / signups
- C1 · Outlined chip — A bordered, transparent button with a typographic verb ("Save changes"). `components/c1-outlined-chip.md`
- C2 · Inline form as cta — The CTA is the form — a single email input with a "Submit →" beside it. No separate landing for sign-up. `components/c2-inline-form-as-cta.md`
- C3 · Typographic link — Just a word, an arrow, and a 1-px underline. No box, no fill. `components/c3-typographic-link.md`
- C4 · Sticky bottom bar — A horizontal bar pinned to the viewport bottom, holding a CTA + a brief reassurance line. `components/c4-sticky-bottom-bar.md`
Testimonials / proof
- T1 · Pull quote with marginalia — A quote sits in the wide column; the attribution and source link float in the narrow margin column. `components/t1-pull-quote-with-marginalia.md`
- T2 · Logo wall hairline — A row of customer logos, monochromatic, separated by hairline rules. No card boxes, no shadows. `components/t2-logo-wall-hairline.md`
- T3 · Single huge quote — One quote, set big, centered, taking a whole section. No supporting text, no attribution boxes — attribution is a small caps line beneath. `components/t3-single-huge-quote.md`
- T4 · Numbered stat strip — A horizontal strip of 3–5 stats (count + qualifier) running across one row. Tabular nums. `components/t4-numbered-stat-strip.md`
Footers
- Ft1 · Mast headed — A wordmark and tagline anchor a single horizontal band. Two or three small links beside, address or licence below. `components/ft1-mast-headed.md`
- Ft2 · Inline rule single line — A single horizontal line of credits, address, copyright. Hairline rule above. No columns. `components/ft2-inline-rule-single-line.md`
- Ft3 · Index style category list — Three or four short columns, each headed by a category in small caps, holding 4–6 links each. `components/ft3-index-style-category-list.md`
- Ft4 · Dense typographic — One large block of text — credits, references, licence, address — in a small monospace font, fully justified or ragged-right. Editorial colophon energy. `components/ft4-dense-typographic.md`
- Ft5 · Statement — One large display sentence dominates the footer — a closing line, not a sitemap. Wordmark, minimal links, copyright sit beneath in muted small type. Stripe (older), Mailchimp pre-r `components/ft5-statement.md`
- Ft6 · Letter close — Closes the page like a letter —
Yours, the team. 2026.Optional postscript line beneath. Sets the page as a piece of writing rather than a product. `components/ft6-letter-close.md` - Ft7 · Newsletter first — The form (label + input + submit) is the primary element of the footer; everything else (wordmark, links, copyright) is set in 12 px muted type beneath. Stratechery, Substack-sha `components/ft7-newsletter-first.md`
- Ft8 · Marquee scroll — A horizontal infinite-scroll line of repeating tagline + dot separator:
STUDIO · 2026 · STUDIO · 2026 · STUDIO · 2026 ·. Sport-genre sites, fashion lookbooks, brand-forward agenc `components/ft8-marquee-scroll.md`
Navigation
- N1 (N1a) · Wordmark 2 links — Top-of-page bar: wordmark on the left, two text links on the right ("Pricing" / "Sign in"). No logo image, no menu icon. The minimal variant — for the dense canonical SaaS bar use N1b. `components/n1-wordmark-2-links.md`
- N2 · Floating chip — A small fixed chip in a corner — wordmark + a single action ("Try it"). Doesn't sit in document flow. `components/n2-floating-chip.md`
- N3 · Side rail — A thin vertical strip on the left edge — wordmark rotated, plus 2–3 dot-indicators for sections. Editorial / portfolio energy. `components/n3-side-rail.md`
- N4 · Hidden behind k — No visible nav. The user opens a command palette via
⌘Kto get anywhere. Designed for keyboard-first audiences. `components/n4-hidden-behind-k.md` - N5 · Floating pill — A rounded full-pill nav, visibly detached from the page edges, sitting ~
var(--space-md)from the top, soft blur backdrop, soft shadow. Reads as contemporary modern-minimal — Ve `components/n5-floating-pill.md` - N6 · Newspaper masthead — Full-width header, large centred wordmark on the top row, thin issue/date line above or below in serif small caps, optional inline link row beneath, double-rule below the whole thi `components/n6-newspaper-masthead.md`
- N7 · Brutal slab — A heavy, full-width nav with a 2 px solid border-bottom, all-caps wordmark and tracked uppercase link row, dense rhythm, no shadow, no rounded corners. Reads as Pentagram project p `components/n7-brutal-slab.md`
- N8 · Terminal command — A nav formatted as a CLI prompt:
> studio --catalog --voice --get▮. The "links" are command flags. The blinking cursor (▮) is allowed only here (it has purpose — signals "you `components/n8-terminal-command.md` - N9 · Edge aligned minimal — Wordmark hard-left, single CTA hard-right, vast empty space between, no link row at all. The absence is the design — Apple product pages, Carl Hauser, luxury sites. `components/n9-edge-aligned-minimal.md`
- N10 · Floating on scroll morph — A sticky bar at the top that morphs into a floating pill as the user scrolls past a threshold. Two visual modes share one DOM —
.nav(outer) owns the bar look,.nav__inner`components/n10-floating-on-scroll-morph.md` - N1b · Canonical SaaS three-section — Wordmark-left · centred 4–6-link cluster (some with hover dropdowns) · sign-in + filled CTA right. The dominant modern marketing nav; frosts on scroll. N1/N1a is the minimal two-link variant; this is the dense, balanced one. `components/n1b-saas-three-section.md`
- N11 · Mega-menu panel — Top bar whose triggers open a full-width multi-column panel (icon · title · description per item, grouped, + a feature card); page dims behind a scrim. For platforms/hubs with many grouped destinations. `components/n11-mega-menu.md`
- N12 · Announcement banner + retracting nav — A coloured promo banner stacked above one real nav; banner retracts on scroll-down, returns on scroll-up, dismisses via ×. Banner ≠ second nav (colour contrast keeps them distinct). `components/n12-banner-retract.md`
- N13 · Inline ⌘K search pill — A visible search pill in the bar (placeholder +
⌘Khint) opening a spotlight modal with grouped, keyboard-navigable results. The visible opposite of N4. For search/docs-heavy products. `components/n13-inline-cmdk-pill.md`
---
Within-archetype variation knobs
Picking an archetype is the first axis of variety. The second is how you build it. Two pages built with the same archetype should not be identical — each archetype below has 2–3 variation knobs. Pick one value per knob per output. This prevents "every Bento I build looks like the same Bento."
When you pick an archetype, state the knob values you chose in the macrostructure stamp comment, e.g.:
/* Hallmark · macrostructure: Bento Grid · F1 Bento knobs: tiles=6, spans=irregular, accent=corner-only · ... */| Archetype | Knob A | Knob B | Knob C |
|---|---|---|---|
| H1 Marquee | Display size: xxl (clamp 4–12rem) · xl (clamp 3–8rem) | Alignment: left-bias · centred · right-bias | Underlay: none · single rule above · single rule below |
| H2 Split Diptych | Ratio: 7/5 · 6/6 · 5/7 | Right side: photo · proof column · pull-quote | Divider: hairline · negative space · vertical rule |
| H3 Quote-Led | Quote weight: italic display · roman display · roman body large | Attribution position: under quote · margin-aligned · right-flush | Length: ≤80 chars · 80–160 chars |
| H4 Stat-Led | Number style: tabular display · italic display · monospace | Qualifier position: below · inline-right · stacked-above | Secondary stats: none · two below · row of four |
| H5 Letter | Salutation: greeting · "Dear X," · time-stamp | Body length: 1 paragraph · 2 paragraphs · 3 paragraphs | Signoff: typed name · drawn signature SVG · initials |
| H6 Photographic | Image area: full-bleed · 16/7 · 4/3 · 1/1 square | Caption position: lower-left · upper-right · margin | Text below or overlaid |
| H7 Demo Video Clipped-Edge | Clip side: right · left · both | Aspect ratio: 16/10 · 16/9 · 4/3 | Frame: hairline · browser chrome · none |
| H8 Mockup Split | Frame style: browser chrome · macOS toolbar · minimal hairline · floating no-frame | Tilt: 0° · 1.5° · 3° | Screenshot count: 1 · stack-of-3 · orbit-of-3 |
| H9 Custom Illustration | Build method: Tier-A pure-CSS · Tier-B hand-SVG · Tier-C generated · Tier-D library | Animation: none · loop · scroll-linked | Scale: small accent · dominant |
| F1 Bento (feature) | Tiles: 4 · 6 · 7 · 9 | Spans: regular · irregular · mosaic | Border: hairline all · accent corners · none |
| F2 Sticky-scroll stack | Pinned side: left · right | Right pane content: code · screenshot · diagram | Pin steps: 3 · 4 · 5 |
| F3 Tabular spec sheet | Columns: 2 (key/val) · 3 (key/val/unit) · 4 (with footnote) | Rule density: every row · groups of 3 · headers only | Numbers: tabular · proportional |
| F4 Step sequence | Numbering: I/II/III · 01/02/03 · 1.0/2.0/3.0 | Layout: vertical stack · horizontal flow · diagonal | Connector: line · arrow · none |
| F5 Annotated screenshot | Callouts: numbered pins · margin labels · inline arrows | Frame: device · plain · floating | Anchor: image-led or text-led |
| F6 Product card grid | Card ratio: 3/4 portrait · 1/1 square · 4/3 landscape | Density: 3-up · 4-up · 5-up | Micro-action: Add · Save · View → · none |
| C1 Outlined chip | Shape: rectangular · pill (only allowed for tactile/playful tones) · slab | Density: spacious · compact | Adornment: arrow · plus · none |
| C2 Inline form-as-CTA | Field count: 1 · 2 · 3 | Submit position: end-of-row · separate line · embedded button | Helper: above · below · none |
| C3 Typographic link CTA | Underline: solid · dashed · double · none | Hover behaviour: thicken · slide · colour shift | Arrow: → · ↗ · none |
| C4 Sticky bottom bar | Reveal: always · scroll-up · after fold | Anchored: viewport bottom · viewport top · inline at bottom | Shadow: hairline · none · subtle |
| T1 Pull quote w/ marginalia | Quote treatment: italic display · roman large · serif italic | Attribution: signed · stamped · timestamped | Marginalia: none · timeline · 1 footnote |
| T2 Logo wall (hairline) | Layout: single row · 2 rows · grid 3×N | Logo treatment: monochrome ink · brand colour · ghosted | Divider: hairline cells · none |
| T3 Single huge quote | Quote face: serif italic · roman display · italic mono | Width: full-bleed · 60ch · 40ch | Attribution position: same line · separate band |
| T4 Numbered stat strip | Layout: 3-up · 4-up · 5-up · 6-up | Number weight: display · body large | Qualifier position: under · inline · above |
| Ft1 Mast-headed | Wordmark size: display 3xl · display 2xl · xl | Tagline: italic serif · roman body · none | Links row: inline · 2-line stack |
| Ft2 Inline single line | Order: wordmark/links/credit · credit/wordmark/links | Separator: middot · pipe · em-dash · vertical rule | Density: dense · spaced |
| Ft3 Index columns | Columns: 3 · 4 · 5 | Heading style: small caps · italic · monospace | Bullet: hairline · none |
| Ft4 Dense colophon | Family: monospace · serif · sans | Layout: single block · paragraphs · log-style | Includes: build hash · date · attribution |
| N1 Wordmark + 2 links | Position: left/right split · centred · right-flush | Links: text · text+icon · pill | Sticky: yes · no |
| N2 Floating chip | Anchor: top · bottom · top-right · bottom-left | Content: theme picker · search · navigation | Backdrop: blur · solid · none |
| N3 Side-rail | Side: left · right | Width: 12ch · 16ch · 20ch | Indicator: filled bar · text-only · numbered |
| N4 Hidden behind ⌘K | Trigger: button · keyboard only · both | Surface: modal · sheet · spotlight | Recents: shown · hidden |
| N5 Floating pill | Width: content-sized · max ~720 px · max ~560 px | Backdrop: blur+saturate · solid · subtle gradient | Anchor: top-centred · top-right · top-left |
| N6 Newspaper masthead | Issue line: above wordmark · below wordmark · none | Wordmark size: 3xl · 2xl · xl | Rule: double · single · none |
| N7 Brutal slab | Border weight: 2 px · 3 px · 4 px | Letter-spacing: tracked uppercase · normal | CTA: filled slab · outline block · text-only |
| N8 Terminal command | Prompt: > · $ · ~/$ | Cursor: in-line at end · after final flag · none | Width: full bleed · content · ~80 ch |
| N9 Edge-aligned minimal | CTA shape: outlined · filled pill · text+arrow | Wordmark: serif italic · sans · monospace | Padding-block: tight · default · spacious |
| N1b SaaS three-section | Centre links: 3 · 4 · 5–6 | Dropdowns: none · 1 · 2 | Scroll: frost-on-scroll · always-solid · transparent-fixed |
| N11 Mega-menu | Columns: 2 · 3 · 4 | Feature cell: none · promo card · code sample | Scrim: dim+blur · dim only · none |
| N12 Banner + retract | Banner fill: solid · gradient · tint+ink | Dismiss: yes · none | Bar scroll: sticky · also-frosts |
| N13 Inline ⌘K-pill | Pill placement: centred · right-of-brand | Result groups: flat · grouped | Footer hints: shown · hidden |
| Ft5 Statement | Sentence width: 28 ch · 38 ch · 50 ch | Wordmark position: under sentence · top-right · none | Rule above meta: hairline · double · none |
| Ft6 Letter close | Signoff: italic · roman · monogram | Postscript: yes · no | Width: 40 ch · 60 ch · 80 ch |
| Ft7 Newsletter-first | Layout: stacked · inline · split (form left · meta right) | Submit style: filled · outline · arrow link | Privacy line: yes · no |
| Ft8 Marquee scroll | Speed: 24 s · 32 s · 48 s | Direction: left · right · alternate (rare) | Glyph: middot · em-dash · slash |
Anti-pattern: picking the same knob values across two different outputs is the same kind of templating as picking the same archetype. If your last Bento was tiles=6, spans=irregular, accent=corner-only, the next one must change at least one knob.
---
Routing — which footer fits which genre
| Genre | Default | Also OK |
|---|---|---|
| editorial | Ft1 Mast-headed | Ft2, Ft4, Ft6, Ft7 |
| modern-minimal | Ft2 Inline single line | Ft1, Ft5 |
| atmospheric | Ft5 Statement | Ft1, Ft2 |
| playful | Ft8 Marquee scroll | Ft5, Ft3 |
| terminal | Ft4 Dense colophon | Ft2 |
| docs / reference | Ft3 Index columns | Ft1 |
Diversification. Same rule as nav — across consecutive Hallmark runs in the same session, no two outputs should share the same footer archetype.
Default away from Ft3. The 4-column index footer is the AI fingerprint when used reflexively (Product · Company · Resources · Legal + social row + tiny copyright). Reach for Ft3 only when the page is a hub or docs-root with a genuine sitemap; default to Ft1, Ft2, Ft4, Ft5, Ft6, Ft7, or Ft8 otherwise.
---
Routing — which nav fits which genre / theme
| Genre / cluster | Default nav | Acceptable also |
|---|---|---|
| editorial (Newsprint · Garden · Atelier · Carnival · …) | N6 Masthead | N1a, N9, N12 |
| modern-minimal (Coral · Cobalt) | N1b SaaS three-section | N5, N11, N13, N9 |
| atmospheric (Bloom · Aurora · Midnight · Lumen) | N5 Floating pill (blur backdrop sells the mood) | N9, N4, N13, N1b |
| playful (Hum) | N1b SaaS three-section | N5, N11, N12, N13, N7 (rounded) |
| terminal / CLI (Terminal) | N8 Terminal command | N4 ⌘K-only, N13 |
| docs / reference (Almanac) | N3 Side-rail | N13, N1a, N4 |
| commerce / product launch | N12 Banner + retract | N1b, N11, N9 |
Diversification — state it out loud, every build. Across consecutive Hallmark runs in the same project session (and across multiple test builds of the same theme), no two outputs may share the same nav archetype. Before writing nav markup, write one line: "Previous nav: <X>. This build: <Y>, because <reason>." This is the same accountability step as the macrostructure rotation. A theme with 4 test builds should show 4 different navs — e.g. Hum across Curio/Sprout/Tally/Mixtape uses N5 → N1b → N12 → N13. Reaching for the genre default on every build is exactly the failure this rule exists to prevent; rotate through the "Acceptable also" column deliberately.
Default away from N1a. The most-recognised AI fingerprint is N1a (wordmark + inline link row + button-right) used reflexively. For a real product nav reach for N1b (the dense, balanced canonical bar) or N5/N11/N13 first; reach for N1a only when the page genuinely has 2 destinations.
---
Picking from this file
When building a section:
1. Identify the section's role (hero / section-head / feature / CTA / testimonial / footer / navigation). 2. Glance at the archetypes in that category. 3. Pick the one whose "Use when" fits the brief. 4. Make sure no two sections in the same page use the same archetype. 5. If the macrostructure suggests a default (e.g., Bento Grid → F1 Bento), use it; if it doesn't suggest, vary deliberately.
The goal is composed variety — within a page, sections feel different from each other; across pages Hallmark builds, sections feel different from the last.
---
Mobile collapse — per archetype
Every archetype has a defined collapse behaviour at narrow viewports. The two breakpoints to know:
- 60 rem (~960 px) — the layout breakpoint. Multi-column grids collapse to single column. Tilts and clip effects drop. Sticky panes unstick.
- 40 rem (~640 px) — the typography breakpoint. Display sizes shrink one step. Side-margin labels move inline. Annotations consolidate.
Below 60 rem the archetype must still feel like itself — same hierarchy, same tone, same rhythm — but in a stacked single-column form. Below 40 rem the page is a phone; treat space like a luxury.
| Archetype | Below 60 rem | Below 40 rem |
|---|---|---|
| H1 Marquee | unchanged (typography-only; centres / left-biases naturally) | display size step down (xl → lg); reduce side padding |
| H2 Split Diptych | grid 1fr (text top, proof column below); divider becomes hairline-rule between | proof column collapses to a 2-column compact grid for items |
| H3 Quote-Led | quote stays full width; attribution wraps to its own line | quote size step down; attribution font-size step down |
| H4 Stat-Led | number stays full width, text stacks below; secondary stats become 2-up grid | number size step down (clamp floor lifts); qualifier text wraps |
| H5 Letter | unchanged single column; aside (if present) moves below body, divider becomes top border | salutation size step down; signoff tightens |
| H6 Photographic | image stays full-bleed; caption moves from absolute corner to inline below image | caption font-size step down; corner caption never overlaps text on phones |
| H7 Demo Video Clipped-Edge | drops the clip; goes 1fr stacked, full-width media; tilt removed (clipping at 375 px reads as broken) | media reduces to 16/9; poster image used (auto-playing on cellular is hostile) |
| H8 Mockup Split | drops the tilt; grid 1fr; mockup goes full-width below text | annotation pins consolidate; numbered legend moves below mockup |
| H9 Custom Illustration | grid 1fr; illustration moves below text (or above — pick by tone) | illustration scales to ≤ 40 % viewport width; never dominates |
| F1 Bento | grid drops from 6/4-col to 2-col; large tiles span 2; small tiles span 1 | drops to 1-col; tile order respects information priority |
| F2 Sticky-scroll stack | sticky pane unsticks; content becomes linear sequence of paired text+visual blocks | the visuals shrink to 16/9 inline; no sticky behaviour at all |
| F3 Tabular spec sheet | columns reduce: 4-col → 2 (key + value), drop unit + footnote | spec list goes vertical; each row is dt above dd |
| F4 Step sequence | numbering moves from left margin to inline-with-step | step containers tighten; connector lines drop |
| F5 Annotated screenshot | screenshot full-width; annotations restack as a numbered list below | screenshot 16/9; annotations consolidate into a legend |
| F6 Product card grid | grid 3-up → 2-up | grid 2-up → 1-up; card height becomes flexible |
| C1 Outlined chip | unchanged (chips wrap onto multiple lines if needed) | full-width single chip ; min-height 44 px hit target |
| C2 Inline form-as-CTA | input + button stack vertically; full-width | label moves above input; button is full-width below |
| C3 Typographic link | unchanged (links wrap naturally) | unchanged |
| C4 Sticky bottom bar | unchanged (already designed for narrow); ensure 44 px min-height | label truncates if needed; CTA stays right-aligned |
| T1 Pull quote w/ marginalia | marginalia move below quote; divider becomes hairline | marginalia consolidate into a single line |
| T2 Logo wall | grid 6-up → 3-up | grid 3-up → 2-up; logo height step down (32 px → 24 px) |
| T3 Single huge quote | quote remains full width; attribution wraps below | quote size step down by 1.4× |
| T4 Numbered stat strip | strip 4-up → 2-up | strip becomes vertical; 1 stat per row |
| Ft1 Mast-headed | links wrap to two lines; tagline below wordmark | wordmark size step down; tagline italicises in if not already |
| Ft2 Inline single line | links wrap to multiple lines; separator becomes a soft return | becomes a vertical list |
| Ft3 Index columns | grid 4-col → 2-col | grid 2-col → 1-col; column heads remain |
| Ft4 Dense colophon | unchanged (mono/wraps naturally); reduce padding | font-size step down |
| Ft5 Statement | sentence stays full width; meta row stacks | sentence size step down (clamp floor lifts); meta wraps |
| Ft6 Letter close | unchanged single column; postscript wraps | signoff size step down; postscript italicises if not already |
| Ft7 Newsletter-first | input + button stack vertically; full-width | label moves above input; button is full-width below |
| Ft8 Marquee scroll | unchanged (already designed for narrow); slow speed by ~25 % | speed slows further; track height step down |
| N1 Wordmark + 2 links | unchanged | links wrap to second line if long; wordmark stays |
| N2 Floating chip | chip remains floating; reduce padding | chip widens to support 44 px hit target; never below 280 px |
| N3 Side-rail | rail unsticks and becomes a hamburger trigger above | hamburger becomes the only nav |
| N4 ⌘K-only | hamburger appears for users who don't know ⌘K | unchanged (⌘K equivalent is on-screen tap) |
| N5 Floating pill | pill drops link list, keeps wordmark + CTA; stays detached | becomes a top-anchored corner chip — wordmark left, hamburger right |
| N6 Newspaper masthead | issue line stacks above wordmark; nav links wrap to a second row | wordmark size step down; nav row collapses behind a "menu" disclosure |
| N7 Brutal slab | links wrap to second line; CTA stays right-aligned | links collapse to hamburger; wordmark + hamburger only |
| N8 Terminal command | flags wrap to a second > line if needed; cursor stays at the end | becomes a single hamburger labelled > menu; cursor visible at line end |
| N9 Edge-aligned minimal | unchanged (already designed for breathing room) | wordmark + CTA stay edge-aligned; CTA pads to 44 px hit target |
Cross-cutting rules:
- All hit targets ≥ 44 × 44 px below 40 rem (WCAG AA). Never below.
- Padding-inline ≥
clamp(1rem, 4vw, 1.5rem)on the page container so content doesn't kiss the screen edge. - Disable any scroll-linked animation below 40 rem (mobile scroll has its own physics; layered animations fight it).
- Image
loading="lazy"always below the fold; never on the LCP element regardless of viewport. - Auto-play video respects
data-saver(navigator.connection.saveData) — replaces with poster when set.
C1 · Outlined chip
A bordered, transparent button with a typographic verb ("Save changes"). Use when: the page has one primary action; you want it visible but quiet. Don't confuse with: C2 Oversized solid (which is statement-loud).
<a class="cta-outline">Open your studio →</a>.cta-outline { display: inline-flex; align-items: center; gap: 0.4em; padding: 0.7rem 1.2rem; border: 1px solid var(--color-ink); min-height: 44px; }C2 · Inline form-as-CTA
The CTA is the form — a single email input with a "Submit →" beside it. No separate landing for sign-up. Use when: the action is collecting an email. Don't confuse with: C1 Outlined chip (which navigates, not submits).
<form class="cta-form">
<label for="email" class="visually-hidden">Email</label>
<input id="email" type="email" placeholder="you@example.com" />
<button type="submit">Send →</button>
</form>.cta-form { display: grid; grid-template-columns: 1fr auto; border-bottom: 1px solid var(--color-ink); }
.cta-form input { background: none; border: 0; padding: 0.7rem 0; min-height: 44px; }C3 · Typographic link
Just a word, an arrow, and a 1-px underline. No box, no fill. Use when: the page is editorial / Long Document; CTAs should not shout. Don't confuse with: C1 Outlined chip (which is bordered).
<a class="link">Read the case study →</a>C4 · Sticky bottom bar
A horizontal bar pinned to the viewport bottom, holding a CTA + a brief reassurance line. Use when: the page is long and the CTA needs to be reachable always. Don't confuse with: anything in the fold; this is a persistent element, not a hero CTA.
<aside class="cta-sticky">
<span>Try it free for 14 days.</span>
<a class="cta-outline">Start →</a>
</aside>.cta-sticky { position: fixed; left: 0; right: 0; bottom: 0; padding: var(--space-sm) var(--space-md); background: var(--color-paper); border-top: 1px solid var(--color-rule); display: flex; justify-content: space-between; align-items: center; }---
F1 · Bento grid
Asymmetric grid of 8–15 tiles in mixed spans (1×1, 2×1, 1×2, 2×2). Visual rhythm via size. Use when: multiple equally-valid entry points; SaaS feature page. Don't confuse with: F2 Sticky-scroll (which stacks vertically with sticky pacing).
<section class="bento">
<article class="cell span-2x2">…</article>
<article class="cell span-1x1">…</article>
<article class="cell span-2x1">…</article>
</section>.bento { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 12rem; gap: var(--space-md); }
.span-2x2 { grid-column: span 2; grid-row: span 2; }
.span-2x1 { grid-column: span 2; }
.span-1x2 { grid-row: span 2; }
@media (max-width: 56rem) { .bento { grid-template-columns: repeat(2, 1fr); } }F2 · Sticky-scroll stack
Sticky left pane, scrolling right pane that cycles through related screenshots. Use when: feature has multiple sub-states worth showing in sequence. Don't confuse with: F4 Step sequence (which is linearly numbered, not synced).
<section class="sticky-stack">
<div class="pane-sticky"><h3>…</h3><p>…</p></div>
<div class="pane-scroll">
<figure>1</figure><figure>2</figure><figure>3</figure>
</div>
</section>.sticky-stack { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2xl); }
/* `calc(--banner-height + --space-xl)` so the sticky pane docks below the
nav with breathing room. Falls back to --space-xl alone when no sticky
nav is on the page (slop-test gate 56). */
.pane-sticky { position: sticky; top: calc(var(--banner-height, 0px) + var(--space-xl)); align-self: start; z-index: var(--z-sticky); }F3 · Tabular spec sheet
Each row is a feature; columns hold name, value, footnote. Hairline rules between rows. Tabular numerics. Use when: features compare quantitatively. Don't confuse with: F1 Bento (which is non-tabular and visually rhythmic).
<table class="spec-sheet tnum">
<tr><th>Latency</th><td>p99 < 50 ms</td><td class="muted">measured externally</td></tr>
<tr>…</tr>
</table>F4 · Step sequence
Numbered stages (1.0 → 2.0 → 3.0) flow vertically. Each stage has a heading, a paragraph, sometimes a small visual. Use when: the product is a workflow, not a single moment. Don't confuse with: F2 Sticky-scroll (which doesn't number stages).
<ol class="steps">
<li><span class="stage">1.0</span><h3>Intake.</h3><p>…</p></li>
<li><span class="stage">2.0</span><h3>Plan.</h3><p>…</p></li>
</ol>F5 · Annotated screenshot
A product capture sits centre-stage with arrows or short labels pointing to UI details. Use when: the product UI itself is the explanation. Don't confuse with: F2 Sticky-scroll (which uses multiple screenshots in sequence).
<figure class="annotated">
<img src="" />
<span class="callout" style="--x:60%; --y:30%;">→ assigns automatically.</span>
</figure>F6 · Product card grid
Each card is a product, not a feature. Image · name · price · one micro-action. Reads like a shop floor, not a marketing site. Use when: the brief is commerce, catalogue, lookbook, marketplace — anything where the page sells things, not features. Don't confuse with: F1 Bento (which sells features; tiles vary in size and span). Product cards are uniform on purpose — the rhythm comes from the products, not the layout.
Variation knobs: card ratio (3/4 portrait · 1/1 square · 4/3 landscape) · density (3-up · 4-up · 5-up) · price treatment (under name · over image · hover-reveal) · micro-action (Add · Save · View → · none).
<section class="product-grid">
<article class="product">
<a class="product__media" href=""><img src="" alt="" loading="lazy" /></a>
<div class="product__meta">
<h3 class="product__name">Linen Apron · Indigo</h3>
<p class="product__price tabular-nums">¥ 6,400</p>
</div>
<button class="product__add" aria-label="Add Linen Apron to bag">+</button>
</article>
<!-- ... more products, uniform shape ... -->
</section>.product-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-xl) var(--space-lg); }
@media (max-width: 60rem) { .product-grid { grid-template-columns: repeat(2, 1fr); } }
.product { display: grid; gap: var(--space-sm); position: relative; }
.product__media { display: block; aspect-ratio: 3 / 4; background: var(--color-paper-2); overflow: hidden; }
.product__media img { width: 100%; height: 100%; object-fit: cover; transition: transform var(--dur-long) var(--ease-out); }
.product__media:hover img { transform: scale(1.02); }
.product__name { font-family: var(--font-body); font-size: var(--text-md); margin: 0; }
.product__price { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--color-ink-2); }
.product__add { position: absolute; top: var(--space-sm); right: var(--space-sm); width: 32px; height: 32px; background: var(--color-paper); border: var(--rule-hair) solid var(--color-rule-2); cursor: pointer; opacity: 0; transition: opacity var(--dur-short) var(--ease-out); }
.product:hover .product__add, .product:focus-within .product__add { opacity: 1; }
@media (pointer: coarse) { .product__add { opacity: 1; } }Anti-patterns to avoid in product grids:
- Don't borrow Bento's irregular spans — products want uniform rhythm.
- Don't put feature-style two-line descriptions under product names. The price is the description.
- Don't auto-scale the image on idle — only on hover, and only by 1.02× max.
- Don't use cards with shadow + radius + border + tile + ribbon. Pick one container signal.
---
Ft1 · Mast-headed
A wordmark and tagline anchor a single horizontal band. Two or three small links beside, address or licence below. Use when: the page has heavy content; the footer should be quiet and singular. Don't confuse with: Ft2 Inline-rule (which is even more reduced).
<footer class="foot-mast">
<p class="wordmark">Studio Name</p>
<p class="tagline muted">Designs that don't look generated.</p>
<p class="links muted">Imprint · Privacy · Contact</p>
</footer>Ft2 · Inline-rule single line
A single horizontal line of credits, address, copyright. Hairline rule above. No columns. Use when: the page is editorial and the footer is afterthought. Don't confuse with: Ft4 Dense typographic (which packs more in).
<footer class="foot-line">
<p>© 2026 · 137 Marlow Street · MIT licensed</p>
</footer>Ft3 · Index-style category list
Three or four short columns, each headed by a category in small caps, holding 4–6 links each. Use when: the page is a hub or a documentation root. Don't confuse with: Ft4 Dense typographic (which is one big block, not columns).
<footer class="foot-index">
<div><p class="caps">Product</p><ul>…</ul></div>
<div><p class="caps">Company</p><ul>…</ul></div>
<div><p class="caps">Resources</p><ul>…</ul></div>
</footer>Ft4 · Dense typographic
One large block of text — credits, references, licence, address — in a small monospace font, fully justified or ragged-right. Editorial colophon energy. Use when: the brand is editorial and a colophon-style sign-off fits. Don't confuse with: Ft3 Index (which navigates).
<footer class="foot-dense mono">
<p>Hallmark v0.2.0. Built with The Future, Fraunces, IBM Plex Mono. MIT licensed. Powered by Together AI. 137 Marlow Street, 2026.</p>
</footer>Ft5 · Statement
One large display sentence dominates the footer — a closing line, not a sitemap. Wordmark, minimal links, copyright sit beneath in muted small type. Stripe (older), Mailchimp pre-rebrand, agency portfolio closers. Use when: the page wants a closing line — editorial, manifesto, atmospheric. The sentence pairs with the page's argument. Don't confuse with: Ft1 Mast-headed (which leads with the wordmark, not a sentence).
<footer class="foot-stmt">
<p class="foot-stmt__line">Build something they'll remember.</p>
<div class="foot-stmt__meta">
<span class="wordmark">Studio</span>
<span class="muted">© 2026 · MIT</span>
</div>
</footer>.foot-stmt { padding: var(--space-2xl) var(--page-gutter) var(--space-xl); display: grid; gap: var(--space-lg); }
.foot-stmt__line { font-family: var(--font-display); font-size: clamp(1.75rem, 5vw, 3.25rem); line-height: 1.0; letter-spacing: -0.02em; max-width: 28ch; margin: 0; }
.foot-stmt__meta { display: flex; justify-content: space-between; align-items: baseline; padding-block-start: var(--space-sm); border-top: var(--rule-hair) solid var(--color-rule); }Anti-pattern: using a Statement footer on a docs root or hub. The sentence reads as marketing fluff there; default Ft3 instead.
Ft6 · Letter close
Closes the page like a letter — Yours, the team. 2026. Optional postscript line beneath. Sets the page as a piece of writing rather than a product. Use when: the page voice is warm, hand-written, editorial-quiet — Garden, Atelier, personal sites. Don't confuse with: Ft1 Mast-headed (which is a wordmark anchor, not a signoff).
<footer class="foot-letter">
<p class="foot-letter__close">Yours,<br><span class="foot-letter__sign">— Studio</span></p>
<p class="foot-letter__ps muted">P.S. — letters back welcome at <a href="mailto:hello@studio">hello@studio</a>.</p>
</footer>.foot-letter { padding: var(--space-2xl) var(--page-gutter); max-width: 60ch; }
.foot-letter__close { font-family: var(--font-display); font-style: italic; font-size: var(--text-lg); line-height: 1.4; }
.foot-letter__sign { font-style: normal; font-weight: 600; }
.foot-letter__ps { font-size: var(--text-sm); margin-top: var(--space-md); }Anti-pattern: using Ft6 on a stat-led / B2B product page — voice mismatch reads as twee. Reserve for genuinely letter-shaped pages.
Ft7 · Newsletter-first
The form (label + input + submit) is the primary element of the footer; everything else (wordmark, links, copyright) is set in 12 px muted type beneath. Stratechery, Substack-shaped sites, indie magazines. Use when: the brand legitimately publishes — and the page above the fold has already offered a subscription. The footer is a final invitation, not an ambush. Don't confuse with: Ft1 (which doesn't ask for anything).
<footer class="foot-news">
<form class="foot-news__form" action="/subscribe" method="post">
<label for="foot-email">Letters from the studio · monthly</label>
<div class="foot-news__row">
<input id="foot-email" name="email" type="email" required placeholder="you@domain">
<button type="submit" class="cta-fill">Subscribe</button>
</div>
</form>
<p class="foot-news__meta muted">Studio · © 2026 · <a href="/imprint">Imprint</a></p>
</footer>.foot-news { padding: var(--space-2xl) var(--page-gutter); display: grid; gap: var(--space-lg); max-width: 56ch; }
.foot-news__form label { display: block; font-size: var(--text-sm); margin-block-end: var(--space-2xs); }
.foot-news__row { display: flex; gap: var(--space-2xs); }
.foot-news__row input { flex: 1; min-height: 44px; padding-inline: var(--space-sm); border: var(--rule-hair) solid var(--color-rule); border-radius: var(--radius-input); background: var(--color-paper); }
.foot-news__row input:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 1px; }
.foot-news__meta { font-size: var(--text-xs); }Anti-pattern: Ft7 when the page never said "subscribe" above the fold. The footer is an honest conclusion; if you didn't ask, don't ambush. Drop to Ft2 instead.
Ft8 · Marquee scroll
A horizontal infinite-scroll line of repeating tagline + dot separator: STUDIO · 2026 · STUDIO · 2026 · STUDIO · 2026 ·. Sport-genre sites, fashion lookbooks, brand-forward agencies. Use when: the brand voice is loud, kinetic, sport-or-manifesto. Don't confuse with: Ft4 Dense colophon (which is static text).
<footer class="foot-marquee" aria-label="Footer">
<div class="foot-marquee__track" aria-hidden="true">
<span>STUDIO · 2026 · STUDIO · 2026 · STUDIO · 2026 · STUDIO · 2026 ·</span>
<span>STUDIO · 2026 · STUDIO · 2026 · STUDIO · 2026 · STUDIO · 2026 ·</span>
</div>
<p class="visually-hidden">Studio · 2026 · MIT licensed</p>
</footer>.foot-marquee { overflow: hidden; border-top: 2px solid var(--color-ink); }
.foot-marquee__track { display: flex; gap: var(--space-2xl); white-space: nowrap; padding-block: var(--space-md); animation: foot-marquee 32s linear infinite; }
.foot-marquee__track span { font-family: var(--font-display); font-weight: 700; letter-spacing: 0.08em; font-size: clamp(1rem, 2.5vw, 1.5rem); }
@keyframes foot-marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .foot-marquee__track { animation: none; } }Anti-pattern: using Ft8 on editorial / quiet contexts — the motion reads as loud. Pair only with playful / sport / manifesto voices, and always honour prefers-reduced-motion: reduce.
---
H1 · Marquee
A single statement fills the fold. No subhead, no CTA in view. Use when: the brand or person is the message. Don't confuse with: H4 Stat-Led (which is a number, not a statement).
<section class="hero-marquee">
<h1 class="display-xxl">A statement.</h1>
</section>.hero-marquee { min-height: 80dvh; display: grid; align-content: end; padding: 0 var(--page-gutter) var(--space-2xl); }
.display-xxl { font-size: clamp(4rem, 12vw, 12rem); line-height: 0.92; }H2 · Split Diptych
Headline + lede on one side, image or product capture on the other. 6/6 or 7/5 columns. Use when: you can pair every claim with a visual proof. Don't confuse with: H6 Photographic (which puts the image full-bleed, not paired).
<section class="hero-split">
<div><h1>…</h1><p>…</p><a class="cta-outline">…</a></div>
<figure><img src="" /></figure>
</section>.hero-split { display: grid; grid-template-columns: 7fr 5fr; gap: var(--space-2xl); align-items: center; }
@media (max-width: 56rem) { .hero-split { grid-template-columns: 1fr; } }H3 · Quote-Led
A pull-quote with attribution is the hero. Your headline is borrowed credibility. Use when: you have a real testimonial that earns the front page. Don't confuse with: T3 Single huge quote (which lives mid-page, not in the hero slot).
<section class="hero-quote">
<blockquote class="display-italic">"…"</blockquote>
<p class="attribution">— Name, Role, Company</p>
</section>H4 · Stat-Led
A giant number or metric is the hero. A small qualifier line below. Use when: you have one defensible, externally-verifiable number. Don't confuse with: T4 Numbered stat strip (which is several stats in a row, not one focal).
<section class="hero-stat">
<p class="figure tnum">99.97<span class="unit">%</span></p>
<p class="qualifier">…</p>
</section>.figure { font-size: clamp(6rem, 18vw, 16rem); font-variant-numeric: tabular-nums; line-height: 0.85; }H5 · Letter Hero
First-person opening — "Dear reader,". No buttons in fold. Reads as personal correspondence. Use when: the founder's voice is the brand. Don't confuse with: H1 Marquee (which is impersonal declaration).
<section class="hero-letter">
<p class="salutation"><em>Dear reader,</em></p>
<p class="lede">…</p>
</section>H6 · Photographic Fold
Single full-bleed image fills the viewport. Caption sits in a corner. Use when: you have real photography that earns full-bleed. Don't confuse with: H2 Split (which pairs image with text in a grid).
<section class="hero-photo">
<img class="bleed" src="" alt="" />
<p class="caption">Spring, 2026.</p>
</section>.hero-photo { position: relative; height: 80dvh; }
.hero-photo .bleed { width: 100%; height: 100%; object-fit: cover; }
.hero-photo .caption { position: absolute; bottom: var(--space-md); right: var(--space-md); }H7 · Demo Video — Clipped-by-viewport-edge
Display headline left, demo video right, the rightmost ~10–20 % extending past the viewport so it's intentionally cut off. The clip is the design — implies "there's more product than fits the screen". Pioneered by Linear, refined by Vercel / Resend / Cursor. Use when: the brief is SaaS / dev-tool / dashboard / platform AND you have real footage of the product (or a hand-built CSS-art mockup of it). Don't confuse with: H4 Stat-Led (number-led, no video) or H8 Mockup Split (still screenshot, not video).
See `hero-enrichment.md` for the full E1 recipe (codec chain, autoplay rules, prefers-reduced-motion fallback, mobile collapse). The cookbook entry below is the structural sketch.
<section class="hero hero--clipped">
<div class="hero__copy">
<h1>Plan, build, ship.</h1>
<p>The project tracker your engineering team won't ignore.</p>
</div>
<figure class="hero__media">
<video autoplay muted loop playsinline preload="metadata"
poster="/hero-poster.webp" fetchpriority="high">
<source src="/hero.av1.mp4" type='video/mp4; codecs="av01.0.05M.08"'>
<source src="/hero.h264.mp4" type="video/mp4">
</video>
</figure>
</section>.hero--clipped { display: grid; grid-template-columns: 1fr 1.4fr; gap: var(--space-2xl); overflow: visible; }
.hero__media { width: calc(100% + 12vw); aspect-ratio: 16 / 10; border-radius: 12px; overflow: hidden; }
@media (max-width: 60rem) { .hero--clipped { grid-template-columns: 1fr; } .hero__media { width: 100%; } }H8 · Mockup Split (browser-framed)
Headline left, browser-frame mockup right, the mockup tilted 1–3° for life. Frame can be browser chrome, macOS toolbar, minimal hairline, or floating no-frame. Use when: you're selling a web app and you have a clean, well-lit screenshot. Don't confuse with: H7 Clipped-Edge (which extends past the viewport) or H2 Split Diptych (which uses photography or proof column, not a product mockup).
<section class="hero-mock">
<div>
<h1>The studio's new mute button.</h1>
<p>Press <kbd>⌘ M</kbd> from anywhere.</p>
</div>
<figure class="mock">
<header class="mock__chrome"><span></span><span></span><span></span></header>
<div class="mock__body"><!-- screenshot or CSS-art mockup --></div>
</figure>
</section>.hero-mock { display: grid; grid-template-columns: 1fr 1.2fr; gap: var(--space-2xl); align-items: center; }
.mock { transform: rotate(1.5deg); border-radius: 12px; overflow: hidden; box-shadow: 0 24px 60px -20px oklch(20% 0.02 60 / 0.18); }
.mock__chrome { display: flex; gap: 6px; padding: 10px 12px; background: var(--color-paper-2); border-block-end: var(--rule-hair) solid var(--color-rule); }
.mock__chrome span { width: 10px; height: 10px; border-radius: 50%; background: var(--color-rule-2); }H9 · Custom Illustration Centerpiece
A single hand-built SVG (Tier B in the enrichment hierarchy — or pure CSS at Tier A for simpler shapes) sitting on the hero as one illustrative element. The bakery loaf, the studio's mascot, the workflow diagram. Use when: the brand has a thing that benefits from being drawn — a craft, a character, a process. Don't confuse with: H6 Photographic (real photography) or H8 Mockup (a product screenshot, not artwork).
The illustration itself is built, not picked from Storyset / Humaaans / unDraw / Lottie. See `custom-craft.md` for full recipes (CSS art, hand-built SVG, declarative animation). The cookbook entry below is the page-level structural sketch.
<section class="hero-art">
<div>
<p class="eyebrow">Maple Street Bread · est. 2026</p>
<h1>Sourdough, every morning.</h1>
<p>Slow-fermented overnight, baked on stone, before you wake.</p>
</div>
<svg viewBox="0 0 200 100" class="loaf" aria-label="A loaf of bread">
<path class="loaf__body" d="M 20 70 Q 100 10 180 70 L 180 90 L 20 90 Z" />
<path class="loaf__score" d="M 60 50 L 90 30 M 100 45 L 130 25 M 140 50 L 165 35" />
</svg>
</section>.hero-art { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2xl); align-items: center; }
.loaf__body { fill: oklch(72% 0.14 50); }
.loaf__score{ stroke: oklch(38% 0.10 35); stroke-width: 2; fill: none; stroke-linecap: round; }---
N1 · Wordmark + 2 links
Top-of-page bar: wordmark on the left, two text links on the right ("Pricing" / "Sign in"). No logo image, no menu icon. Use when: the page has very few destinations. Don't confuse with: N3 Side-rail (which is vertical).
<nav class="nav-min">
<a class="wordmark">Studio</a>
<ul><li><a>Pricing</a></li><li><a>Sign in</a></li></ul>
</nav>N10 · Floating-on-scroll morph
A sticky bar at the top that morphs into a floating pill as the user scrolls past a threshold. Two visual modes share one DOM — .nav (outer) owns the bar look, .nav__inner (inner) owns the pill look. Cross-faded on a single class toggle (.is-floating) with one timing curve. Active layer feels seamless; AI defaults always botch this. Use when: atmospheric / modern-minimal pages where the kinetic micro-moment earns its place. Adds a single tasteful surprise; resists novelty. Don't confuse with: N5 Floating pill (always-on, no scroll behaviour). N10 is N5 plus a default-bar state that morphs into it.
<header class="nav">
<div class="nav__inner">
<a class="wordmark">Hallmark</a>
<ul class="nav__links">…</ul>
</div>
</header>The full recipe — the four laws (height-constant, transform-for-offset, cross-fade-everything, single-curve), the property-morph table, the scroll-handler script, and the eight anti-patterns Hallmark refuses — lives in `floating-nav.md`. Reach for that file before building this archetype. Skipping the four laws is what makes 90% of attempts read as broken.
Anti-pattern (one of eight in floating-nav.md): swapping two <header> elements via opacity instead of cross-fading one DOM. Doubles markup, fights focus order, desyncs content.
---
N11 · Mega-menu panel
A standard top bar whose triggers open a full-width multi-column panel — icon · title · description per item, grouped under column headers, often with a promoted feature card on one side. The page dims behind a scrim. Vercel "Products", Figma "Products", Notion "Resources". Use when: the brand has many destinations that need grouping + explanation (a platform with 6+ products, or docs/resources hubs). The payload is the design problem, not the bar. Don't confuse with: N1b (small single-column dropdowns); N1a (no dropdowns at all).
<header class="nav">
<div class="nav__inner">
<a class="nav__brand">Northwind</a>
<nav class="nav__center">
<div class="mega" data-mega="products"><button class="nav__link" aria-controls="mega-products" aria-expanded="false">Products <span class="nav__caret"></span></button></div>
</nav>
<div class="nav__right"><a class="btn btn--accent">Get started</a></div>
</div>
<div class="mega-panel" id="mega-products" data-panel="products">
<div class="mega-panel__inner">
<div class="mega-col"><p class="mega-col__head">Move money</p><a class="mega-link"><span class="mega-link__ico"></span><span><b>Payments</b><i>cards, ACH, wires</i></span></a></div>
<a class="mega-feature"><p class="mega-feature__title">Vault</p><p class="mega-feature__desc">stablecoin settlement</p></a>
</div>
</div>
</header>
<div class="nav-scrim" id="scrim"></div>.mega-panel { position: absolute; top: 100%; left: 0; right: 0; opacity: 0; visibility: hidden; transform: translateY(-10px);
background: color-mix(in oklch, var(--color-paper) 96%, transparent); backdrop-filter: blur(20px) saturate(160%);
border-bottom: 1px solid var(--color-rule); box-shadow: 0 30px 60px -28px oklch(0% 0 0 / 0.35);
transition: opacity 240ms, transform 280ms var(--ease-spring), visibility 240ms; }
.mega-panel.is-open { opacity: 1; visibility: visible; transform: none; }
.mega-panel__inner { max-width: var(--page-max); margin: 0 auto; padding: 2rem var(--page-gutter);
display: grid; grid-template-columns: repeat(3, 1fr) 1.1fr; gap: 2rem; }
.nav-scrim { position: fixed; inset: 0; z-index: 400; background: oklch(18% 0.01 250 / 0.28); backdrop-filter: blur(2px);
opacity: 0; visibility: hidden; transition: opacity 260ms, visibility 260ms; }
.nav-scrim.is-active { opacity: 1; visibility: visible; }JS: hover opens (with a ~140ms close-grace timer so the pointer can travel into the panel), click toggles, Esc closes, only one panel open at a time, scrim + aria-expanded follow state.
Knobs — Columns: 2 · 3 · 4 · Feature cell: none · promo card · code sample · Scrim: dim+blur (default) · dim only · none · Open on: hover+click (default) · click only. Anti-pattern: never more than ~4 columns; never a panel taller than ~60vh; never open on hover with no close-grace timer (the menu flickers when the pointer crosses the gap). Items must carry a one-line description — a bare link grid is just N1b in disguise. Mobile: collapse the whole thing to a drawer; the columns stack as accordion groups.
N12 · Announcement banner + retracting nav
A coloured promo banner stacked above one real nav. On scroll-down the banner slides up and retracts, leaving a single clean nav docked to the top; on scroll-up it slides back. A dismiss × removes the banner for good (its height zeroes so no gap is left). Apple-style coupled bars, but the top tier is a banner, not a second nav — the colour contrast is what stops it reading as "two navs". Use when: there's a genuine, time-bound announcement (a launch, a sale, free shipping) worth a persistent strip, over a product/marketing page. Great for stat-led or commerce pages. Don't confuse with: a static announcement bar that never moves (fine, but not N12); N1b (single bar, no banner).
<header class="nav" id="nav">
<div class="nav__banner" id="banner">
<p class="nav__banner-text"><span class="nav__banner-spark"></span> New — <b>shared habits</b>. <a class="nav__banner-link">Try it →</a></p>
<button class="nav__banner-x" id="banner-x" aria-label="Dismiss"><span></span></button>
</div>
<div class="nav__bar"><div class="nav__bar-inner">
<a class="nav__brand">Tally</a>
<nav class="nav__links">…</nav>
<a class="btn btn--accent">Start</a>
</div></div>
</header>:root { --banner-h: 42px; --bar-h: 64px; }
.nav { position: fixed; inset: 0 0 auto; z-index: 500; transform: translateY(0); transition: transform 320ms var(--ease-out); }
.nav.is-compact { transform: translateY(calc(var(--banner-h) * -1)); } /* banner hides, bar docks to top */
.nav.is-dismissed { transform: none; }
.nav.is-dismissed .nav__banner { display: none; }
.nav__banner { height: var(--banner-h); display: flex; align-items: center; justify-content: center;
background: linear-gradient(100deg, var(--color-accent), var(--color-accent-deep)); color: var(--color-paper); }
/* content clears both at rest; zero --banner-h on dismiss so calc() reflows with no gap */
.demo-hero { padding-top: calc(var(--banner-h) + var(--bar-h) + 4rem); }JS: track scroll direction — past ~48px going down → .is-compact; going up → remove it; near top → always show. Dismiss × sets --banner-h: 0px (via documentElement.style) and adds .is-dismissed.
Knobs — Banner fill: solid accent · gradient (default) · tint+ink · Dismiss: yes (default) · none · Bar scroll: sticky (default) · also-frosts · Banner content: promo · status · countdown. Anti-pattern: never make the top tier a second set of nav links — that's the "two nav bars" smell the banner exists to avoid. Keep the banner one line, one link, one dismiss. Don't animate banner height directly (janky); translate the whole .nav and zero the height only on dismiss. Mobile: banner text truncates / drops the leading glyph; nav links collapse; the Buy/primary CTA stays.
N13 · Inline ⌘K search pill
A visible search pill sits inline in the bar — placeholder text plus a ⌘K kbd hint — alongside (not replacing) the links. Click it, or press ⌘K / Ctrl K, to open a spotlight modal with grouped, keyboard-navigable results. The opposite of N4 (which hides nav behind the shortcut): here the affordance is on the surface for newcomers, with the shortcut for power users. Tailwind, Linear, Raycast, docs sites. Use when: the product is search-heavy or docs-heavy and search is a primary action (dev tools, music/library apps, large content sites). Don't confuse with: N4 (no visible nav, ⌘K only); a plain search icon that just focuses an input in place.
<header class="nav" id="nav"><div class="nav__inner">
<a class="nav__brand">Crank</a>
<button class="searchpill" id="searchpill" aria-label="Search (⌘K)">
<span class="searchpill__ico"></span><span class="searchpill__text">Search docs…</span>
<span class="searchpill__kbd"><kbd>⌘</kbd><kbd>K</kbd></span>
</button>
<nav class="nav__right"><a class="nav__link">Docs</a><a class="btn btn--accent">Start</a></nav>
</div></header>
<div class="cmdk" id="cmdk" aria-hidden="true">
<div class="cmdk__backdrop" data-close></div>
<div class="cmdk__panel" role="dialog" aria-modal="true">
<div class="cmdk__field"><span class="cmdk__field-ico"></span><input id="cmdk-input" placeholder="Search docs…"><kbd>esc</kbd></div>
<div class="cmdk__results"><p class="cmdk__group">Suggested</p><button class="cmdk__item is-active">…</button></div>
<div class="cmdk__foot"><span><kbd>↑</kbd><kbd>↓</kbd> navigate</span><span><kbd>↵</kbd> open</span><span><kbd>esc</kbd> close</span></div>
</div>
</div>.searchpill { display: flex; align-items: center; gap: 0.6rem; height: 40px; padding: 0 0.55rem 0 0.85rem;
background: var(--color-paper-2); border: 1px solid var(--color-rule); border-radius: 999px; color: var(--color-muted);
transition: border-color 200ms, box-shadow 200ms; }
.searchpill:hover { border-color: var(--color-rule-2); box-shadow: 0 4px 16px -10px oklch(0% 0 0 / 0.3); }
.cmdk { position: fixed; inset: 0; z-index: 700; opacity: 0; visibility: hidden; transition: opacity 200ms, visibility 200ms; }
.cmdk.is-open { opacity: 1; visibility: visible; }
.cmdk__panel { position: absolute; top: 14vh; left: 50%; transform: translateX(-50%) translateY(-8px) scale(0.98);
width: min(560px, calc(100vw - 2rem)); transition: transform 240ms var(--ease-spring); }
.cmdk.is-open .cmdk__panel { transform: translateX(-50%) translateY(0) scale(1); }JS: ⌘K / Ctrl K toggles, Esc closes, backdrop-click closes, ↑/↓ move the active item, Enter selects, focus the input on open and lock body scroll.
Knobs — Pill placement: centred (default) · right-of-brand · Result groups: flat · grouped (default) · Footer hints: shown (default) · hidden · Open trigger: pill+⌘K (default) · ⌘K only (→ that's N4, not N13). Anti-pattern: don't fake the modal with a <div> that traps no focus and ignores Esc — if you ship the pill you ship the keyboard model. The pill must look like search (icon + placeholder), not a generic button. Mobile: the pill collapses to a search icon; the modal goes full-height sheet.
N1b · Canonical SaaS three-section
Wordmark hard-left · a centred cluster of 4–6 links (some opening hover dropdowns) · a sign-in text link + filled CTA hard-right. The dominant marketing-nav of 2024–26 (Stripe, Linear, Vercel, Figma, Notion, PostHog). The structural opposite of N1's minimal two-link variant — this one is dense and balanced. Use when: a SaaS / product / dev-tool page with several real destinations and a clear primary action. The default reach for modern-minimal and (Hum-styled) playful product pages. Don't confuse with: N1a (wordmark + 2 links, no centre cluster); N5 (detached pill); N11 (mega-menu panels, not small dropdowns).
<header class="nav"><div class="nav__inner">
<a class="nav__brand">Conduit</a>
<nav class="nav__center">
<div class="nav__item nav__item--menu">
<button class="nav__link" aria-expanded="false">Product <span class="nav__caret"></span></button>
<div class="nav__dropdown"><a class="nav__dropitem"><b>Gateway</b><i>one endpoint</i></a></div>
</div>
<a class="nav__link">Docs</a><a class="nav__link">Pricing</a>
</nav>
<div class="nav__right"><a class="btn btn--text">Sign in</a><a class="btn btn--accent">Start</a></div>
</div></header>.nav { position: fixed; inset: 0 0 auto; z-index: 500; background: transparent; border-bottom: 1px solid transparent;
transition: background 240ms, border-color 240ms, box-shadow 240ms; }
.nav.is-scrolled { background: color-mix(in oklch, var(--color-paper) 72%, transparent);
backdrop-filter: blur(18px) saturate(160%); border-bottom-color: var(--color-rule); box-shadow: 0 8px 28px -18px oklch(0% 0 0 / 0.4); }
.nav__inner { max-width: var(--page-max); margin: 0 auto; padding-inline: var(--page-gutter); height: 64px;
display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; }
.nav__brand { justify-self: start; } .nav__center { justify-self: center; display: flex; gap: 0.35rem; } .nav__right { justify-self: end; }
.nav__dropdown { position: absolute; opacity: 0; visibility: hidden; transform: translateY(-6px) scale(0.98);
transition: opacity 200ms, transform 220ms var(--ease-spring), visibility 200ms; }
.nav__item--menu:hover .nav__dropdown, .nav__item--menu:focus-within .nav__dropdown { opacity: 1; visibility: visible; transform: none; }Knobs — Centre links: 3 · 4 · 5–6 · Dropdowns: none · 1 · 2 · Scroll state: frost-on-scroll (default) · always-solid · transparent-fixed · CTA pair: sign-in + fill · fill only. Scroll behaviour (default): transparent at rest over the hero, frosts (blur backdrop + hairline border + soft shadow) past ~24px, and tightens height ~8px. Always rAF-throttle the scroll handler. Anti-pattern: don't let the centre cluster collide with brand/CTA — if it can't sit centred with breathing room, drop to 3 links or route to N1a. Never ship a dropdown that opens on click only with no hover/focus affordance. Mobile: hide .nav__center below ~900px; brand + CTA (or hamburger) remain.
N2 · Floating chip
A small fixed chip in a corner — wordmark + a single action ("Try it"). Doesn't sit in document flow. Use when: the page is fold-heavy and traditional nav would fight the content. Don't confuse with: C4 Sticky bottom bar (which is full-width).
<aside class="nav-chip">
<a class="wordmark">Studio</a>
<a class="cta-outline">Try →</a>
</aside>.nav-chip { position: fixed; top: var(--space-md); right: var(--space-md); display: inline-flex; gap: var(--space-md); padding: 0.5rem 0.75rem; background: var(--color-paper); border: 1px solid var(--color-rule); }N3 · Side-rail
A thin vertical strip on the left edge — wordmark rotated, plus 2–3 dot-indicators for sections. Editorial / portfolio energy. Use when: the page is long and section-numbered. Don't confuse with: N1 Top wordmark (which is horizontal).
<nav class="nav-rail">
<p class="wordmark vertical">Studio</p>
<ul class="dots"><li></li><li></li><li></li></ul>
</nav>.nav-rail { position: fixed; left: 0; top: 0; bottom: 0; width: 3rem; padding: var(--space-md); writing-mode: vertical-rl; }N4 · Hidden behind ⌘K
No visible nav. The user opens a command palette via ⌘K to get anywhere. Designed for keyboard-first audiences. Use when: the page is for technical users who expect this affordance. Don't confuse with: N2 Floating chip (which is visible always).
<button class="kbd-hint">⌘ K</button>
<dialog class="palette">…</dialog>N5 · Floating pill
A rounded full-pill nav, visibly detached from the page edges, sitting ~var(--space-md) from the top, soft blur backdrop, soft shadow. Reads as contemporary modern-minimal — Vercel, Linear, Framer, Raycast. Use when: the page is modern-minimal / atmospheric and the hero has a distinct surface or imagery beneath the pill that the blur can sit over. Don't confuse with: N1 Wordmark + 2 links (which is full-width); N2 Floating chip (which is corner-anchored).
<nav class="nav-pill" aria-label="Primary">
<a class="wordmark">Studio</a>
<ul class="nav-pill__links"><li><a>Catalog</a></li><li><a>Voice</a></li></ul>
<a class="cta-fill">Get →</a>
</nav>.nav-pill {
position: fixed; inset: var(--space-md) auto auto 50%;
transform: translateX(-50%);
display: inline-flex; align-items: center; gap: var(--space-md);
padding: 0.5rem 0.875rem;
background: color-mix(in oklch, var(--color-paper) 78%, transparent);
backdrop-filter: blur(14px) saturate(120%);
border: var(--rule-hair) solid var(--color-rule);
border-radius: 999px;
box-shadow: 0 8px 24px -12px oklch(0% 0 0 / 0.18);
z-index: 20;
}Anti-pattern: a "pill" that's ~95 % viewport-wide is just a full-width nav with rounded ends — defeats the point. The pill must be visibly detached and content-sized; if your link list pushes it past ~720 px, drop a link or switch to N1.
N9 · Edge-aligned minimal
Wordmark hard-left, single CTA hard-right, vast empty space between, no link row at all. The absence is the design — Apple product pages, Carl Hauser, luxury sites. Use when: the page is luxury / quiet / Atelier / Garden and the brand earns the silence. Don't confuse with: N1 Wordmark + 2 links (which fills the middle).
<header class="nav-edge">
<a class="wordmark">Studio</a>
<a class="cta-outline">Get →</a>
</header>.nav-edge { display: flex; justify-content: space-between; align-items: center; padding: var(--space-md) var(--page-gutter); }
.nav-edge .wordmark { font-family: var(--font-display); font-size: var(--text-md); }Anti-pattern: adding 4 inline links between the wordmark and CTA "to fill the space". The space is the design; if you fill it, you've made N1 with extra steps.
Related skills
FAQ
How many design themes does Hallmark offer?
Twenty catalog themes plus custom made-to-measure designs for briefs that don't fit existing themes.
What makes Hallmark different from generic design generators?
Hallmark runs 57+ slop-test gates and refuses on-distribution defaults, ensuring each design has a unique fingerprint based on the brief.
Is Hallmark safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.