Hwaro Design
Your job is to design Hwaro sites that look intentional, distinctive, and
genuinely good — and that reflect this user's taste, not a generic default.
A static site generator makes the plumbing trivial; the design is where the site
either feels considered or feels like every other template. Aim for the former.
This skill assumes the project is a Hwaro site (config.toml + templates/ +
static/). For scaffolding, building, and serving, use the hwaro skill;
iterate with hwaro serve (live reload) so the user sees changes instantly.
Every rule below is contextual — none fires automatically. First read the
brief, then pull only what fits. But the bans in Section 9 and the Pre-Flight
Check in Section 12 are hard filters: output that violates them is not done.
0. The Design Read — read the room before anything else
Before touching code or setting dials, infer what the user actually wants.
Most AI design output is bad because the model jumps to a default aesthetic
instead of reading the room.
0.A Read these signals first
- Site kind — blog / docs / portfolio (dev, designer, studio) / landing
(product, event, agency) / book / shop-adjacent brochure / redesign
(preserve vs. overhaul).
- Vibe words the user used — "minimalist", "calm", "Linear-style",
"Awwwards", "brutalist", "premium consumer", "Apple-y", "playful",
"serious B2B", "editorial", "glassy", "dark tech", "warm-handcrafted".
- Reference signals — URLs they linked, screenshots they pasted, products
or brands they named.
- Audience — recruiters scanning a portfolio vs. developers reading docs
vs. design-conscious consumers. The audience picks the aesthetic, not your
taste.
- Brand assets that already exist — logo, color, type, photography. For
redesigns these are starting material, not optional input (Section 10).
- Quiet constraints — accessibility-first audiences, public-sector,
regulated industries, kids' content, CJK-primary readership (keep the CJK
font fallbacks — see Section 4.1). These constraints OVERRIDE aesthetic
preference.
0.B Declare a one-line Design Read before generating
Before any code, state in one line: "Reading this as: <site kind> for
<audience>, with a <vibe> language, leaning toward <aesthetic family>" —
followed by the three dial values (Section 1) and why.
Example reads:
- "Reading this as: developer blog for peers, with a calm technical-editorial
language, leaning toward mono-accented minimalism. Dials: 5 / 3 / 4."
- "Reading this as: product landing for design-conscious indie devs, with a
confident dark-tech language, leaning toward near-black canvas + single
electric accent. Dials: 7 / 6 / 3."
- "Reading this as: redesign-preserve of an existing docs site — extract the
current tokens first, evolve typography and rhythm, keep the IA. Dials: match
existing."
0.C Question policy — hybrid
- If you can confidently infer the direction from the brief, conversation, or
the existing site: do not ask. Declare the Design Read and proceed. A
confident direction the user can react to beats an interview they didn't want.
- If the read genuinely diverges (e.g. "Linear-clean or
Awwwards-experimental?" would produce different sites), ask a short round —
1–2 focused questions, not a survey. Use
AskUserQuestion when available:
2–4 concrete options per question with short descriptions, and lean on its
option previews — a token-value block or a small ASCII layout sketch per
option beats adjectives; people pick a palette they can see.
- Run a full interview only when the user asks to explore taste ("interview
me", "help me figure out what I want") or a greenfield brief gives you nothing
to read. Then cover, in order, stopping once you can commit: purpose &
audience · personality + 1–3 reference sites · color mood incl. light/dark ·
type feel · density & layout · motion appetite · constraints.
- Either way, for any substantial engagement play the brief back in 4–6
lines ("Warm editorial blog: cream paper background, single rust accent,
Charter-style serif headings over a clean sans body, generous whitespace,
motion limited to gentle link transitions") and confirm before building big.
The brief is the contract you design against.
- If a confirmed brief already exists earlier in the conversation, don't
re-interview — proceed from it, and only revisit taste if the user's
direction changed.
0.D Anti-Default Discipline
Do not default to: AI-purple gradients, centered hero over dark mesh, three
equal feature cards with an emoji each, generic glassmorphism on everything,
infinite-loop micro-animations, system-font-with-no-personality — and, the
Hwaro-specific one, shipping the ember scaffold palette unchanged and calling
it a design. The ember tokens are a starting vocabulary, not the answer to
a brief. Reach past the defaults deliberately, based on the Design Read.
1. The Three Dials
After the Design Read, set three dials. Every layout, motion, and density
decision below is gated by these.
DESIGN_VARIANCE — 1 = perfect symmetry, 10 = artsy chaos
MOTION_INTENSITY — 1 = static, 10 = cinematic choreography
VISUAL_DENSITY — 1 = art gallery / airy, 10 = cockpit / packed data
State the values in the Design Read. Never silently assume a baseline —
reason them from the brief. Overrides happen conversationally ("crank the
motion to 8").
1.A Dial inference (Design Read → dial values)
| Signal |
VARIANCE |
MOTION |
DENSITY |
| "minimalist / clean / calm / editorial / Linear-style" |
5–6 |
3–4 |
2–3 |
| "premium consumer / Apple-y / luxury / brand" |
7–8 |
5–7 |
3–4 |
| "playful / wild / Awwwards / experimental / agency" |
9–10 |
8–10 |
3–4 |
| Blog (personal / technical) |
5–6 |
3–4 |
3–4 |
| Docs / book |
3–4 |
2–3 |
4–6 |
| Landing / portfolio (default) |
7–9 |
5–7 |
3–5 |
| "trust-first / public-sector / accessibility-critical" |
3–4 |
2–3 |
4–5 |
| Redesign — preserve |
match existing |
+1 |
match existing |
| Redesign — overhaul |
+2 |
+2 |
match existing |
1.B Dial definitions (technical reference, plain-CSS terms)
DESIGN_VARIANCE
- 1–3 (Predictable): symmetrical grid (equal
fr units), equal paddings,
centered alignment. Right for docs, books, trust-first.
- 4–7 (Offset): negative-margin overlaps, varied image aspect ratios (4:3
next to 16:9), left-aligned headers over centered content blocks.
- 8–10 (Asymmetric): masonry, fractional grids
(
grid-template-columns: 2fr 1fr 1fr), massive deliberate empty zones
(padding-inline-start: 20vw).
- MOBILE OVERRIDE: for levels 4–10, asymmetric layouts MUST collapse to a
strict single column below 768px. Declare the collapse in the same rule
block, not "it'll probably reflow."
MOTION_INTENSITY
- 1–3 (Static):
:hover / :active / :focus-visible transitions only.
- 4–7 (Fluid CSS):
transition: … 0.3s cubic-bezier(0.16, 1, 0.3, 1),
animation-delay cascades for load-ins, transform/opacity only.
- 8–10 (Choreography): scroll-driven reveals via CSS
animation-timeline: view() (behind @supports) or an IntersectionObserver
(Section 7). window.addEventListener("scroll", …) is a hard ban, not a
prefer-not.
VISUAL_DENSITY
- 1–3 (Art gallery): huge section gaps (8–12rem), few elements, expensive
whitespace.
- 4–7 (Daily site): standard rhythm (4–6rem section padding) — the
--space-* scale as-shipped.
- 8–10 (Cockpit): tight paddings, no card boxes, hairlines separate data,
--font-mono for all numbers.
2. How design lives in a Hwaro site
Know the actual surfaces you can touch — this is what makes the design real
and not just advice.
Templates (Crinja / Jinja2-compatible) — the structure
Under templates/. Hwaro supports two composition styles and the built-in
scaffolds use both, so open the project's templates/ and see which before
editing: {% extends "base.html" %} inheritance filling {% block content %}
(the docs scaffold works this way), and {% include "header.html" %} /
{% include "footer.html" %} partial composition (blog, book, simple).
Other files you'll meet: page.html / section.html (content layouts),
index.html (home), taxonomy*.html, and partials like nav.html /
sidebar.html. Layout changes (what wraps what) happen here; look (color,
type, spacing) happens in CSS. Keep markup semantic and class-driven so the CSS
can do the work.
Internal URLs must carry the base URL. The scaffolds write every
site-internal href/src as {{ base_url }}{{ page.url }},
{{ base_url }}{{ lang_prefix }}/about/, {{ base_url }}/css/style.css —
never a bare /about/. Keep that pattern in every template you author, or the
site 404s the moment it's deployed under a subpath (GitHub Pages project
sites).
CSS delivery — three options, pick deliberately
- Inlined
<style> — the simple scaffold ships its CSS in a <style>
block inside header.html: zero extra requests, great for small sites; edit
that block directly. (blog / book / docs instead ship an external
static/css/style.css — option 2; bare ships no CSS at all, so you bring
your own.)
- Static stylesheet +
[auto_includes] — drop files in static/assets/css/
(and …/js) and emit {{ auto_includes_css }} / {{ auto_includes_js }} in
your head/body. (These output raw HTML; Hwaro disables template autoescape, so
the scaffolds write them without | safe — the docs add | safe as a
harmless, portable convention you can keep.) Good when CSS grows past "one block."
[assets] pipeline — declare bundles in config.toml, reference with
{{ asset(name='main.css') }}; you get concatenation, minification, and
content-hash fingerprinting (cache busting) for free.
There is a built-in SCSS compiler (opt-in via [sass] enabled = true),
but no Tailwind step and no component framework. The built-in compiler
covers the practical subset — variables, nesting/&, partials with
@use/@import, mixins with @content, interpolation — but NOT control
flow (@if/@each), arithmetic, or built-in functions (lighten() etc.),
so framework-grade SCSS won't compile. Default to modern plain CSS (custom
properties, nesting where supported, color-mix(), clamp()); use [sass]
when the brief calls for it, or run Tailwind/PostCSS/full dart-sass via
build hooks and point Hwaro at the compiled output. Don't assume an npm
design system exists. When a brief names an aesthetic
(glass, bento, brutalist, editorial, dark tech, aurora), that's a CSS
language you build honestly by hand — there is no official package for any of
them, on any stack.
Design tokens are your single source of truth
Every Hwaro scaffold themes itself through CSS custom properties in :root.
This is the right pattern: define the system once, theme by editing tokens, and
get dark mode + restyles almost for free. All eight scaffolds share one
"Hwaro Ember" vocabulary: every color token is a light-dark(light, dark) pair
resolved by color-scheme, so every scaffold (including simple) follows the
reader's OS scheme automatically. The vocabulary is (values exact; font stacks
abbreviated — the real ones are longer and include CJK fallbacks like
"Noto Serif KR", so don't strip those if your audience needs CJK):
:root {
color-scheme: light dark;
/* Accent — the single ember. */
--primary: light-dark(#b35454, #ec7a66);
--primary-strong: light-dark(#8f4040, #f39683); /* hover/active accent */
--primary-tint: color-mix(in srgb, var(--primary) 8%, transparent);
--selection: color-mix(in srgb, var(--primary) 22%, transparent);
/* Ember rule — the one mark every scaffold shares. */
--rule-from: light-dark(#c46262, #f39683);
--rule-to: light-dark(#8f4040, #cc5d4b);
/* Ink — a three-step ramp (not pure black/white). */
--heading: light-dark(#241f1a, #f5f2ed);
--text: light-dark(#2a241f, #dedad3);
--text-secondary: light-dark(#5c5248, #a7a199);
--text-muted: light-dark(#6f6358, #7d776e);
/* Surfaces & edges. */
--bg: light-dark(#faf7f2, #0f0f0e);
--bg-subtle: light-dark(#f1eae0, #1a1917);
--bg-code: light-dark(#f1eae0, #1e1c19);
--border: light-dark(#e4dacd, #2b2926);
--border-subtle: light-dark(#efe8dd, #201e1c);
--edge: color-mix(in srgb, var(--text) 8%, transparent);
--glass: color-mix(in srgb, var(--bg) 85%, transparent);
--scrim: light-dark(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6));
/* Support hues (info boxes only — the accent stays singular). */
--warn: light-dark(#b07d2e, #d9a45a);
--ok: light-dark(#5e8c61, #8fb491);
/* Syntax — nine slots the .hljs-* theme reads from. */
--code-comment: light-dark(#a1907c, #8a8073);
--code-keyword: light-dark(#b03a2e, #f0846f);
--code-string: light-dark(#5f7032, #b7c06a);
--code-number: light-dark(#9a6a14, #e8a83f);
--code-func: light-dark(#2f6a5a, #8ec5a3);
--code-type: light-dark(#b0641c, #e6914f);
--code-variable: light-dark(#8a4a3a, #e8b0a0);
--code-attr: light-dark(#45617a, #93b5c8);
--code-symbol: light-dark(#8a4368, #d79bb8);
/* Type scale — minor third (1.2), fluid via clamp(). */
--step--1: clamp(0.83rem, 0.81rem + 0.11vw, 0.89rem);
--step-0: clamp(1rem, 0.96rem + 0.22vw, 1.125rem);
--step-1: clamp(1.2rem, 1.13rem + 0.35vw, 1.4rem);
--step-2: clamp(1.44rem, 1.32rem + 0.61vw, 1.78rem);
--step-3: clamp(1.73rem, 1.53rem + 0.98vw, 2.28rem);
--step-4: clamp(2.07rem, 1.77rem + 1.52vw, 2.92rem);
/* Space — 8px rhythm. */
--space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem; --space-4: 1rem;
--space-5: 1.5rem; --space-6: 2.5rem; --space-7: 4rem; --space-8: 6rem;
/* Shape, depth, motion, measure. */
--measure: 68ch;
--radius: 10px;
--radius-sm: 6px;
--shadow-sm: 0 1px 2px light-dark(rgba(42, 36, 31, 0.05), rgba(0, 0, 0, 0.3));
--shadow: 0 2px 8px light-dark(rgba(42, 36, 31, 0.08), rgba(0, 0, 0, 0.4));
--shadow-lg: 0 16px 70px light-dark(rgba(42, 36, 31, 0.18), rgba(0, 0, 0, 0.5));
--transition: 0.15s ease;
/* Faces. */
--font-serif: "Charter", Georgia, "Noto Serif KR", serif; /* headings (+CJK) */
--font-sans: -apple-system, "Segoe UI", Roboto, sans-serif; /* body */
--font-mono: ui-monospace, "SF Mono", Menlo, monospace; /* code */
}
Every scaffold shares this exact vocabulary. It is emitted from one place —
DesignTokens.root_block in src/services/scaffolds/design_tokens.cr in the
Hwaro repo — so the old per-scaffold name drift is gone (--primary-hover,
--bg-secondary, and --border-light were renamed to --primary-strong,
--bg-subtle, and --border-subtle). The only per-scaffold additions are
layout tokens (--header-h, --sidebar-w, --content-max-w, --bg-sidebar,
radius overrides) injected into the same :root. A
@supports not (color: light-dark(#000, #fff)) block pins the static light
palette for pre-2024 browsers, so a *-dark site renders light there —
readable and on-brand rather than broken.
Retheme by rewriting token values, not by adding parallel systems. The type
scale (--step--1…--step-4), spacing rhythm (--space-1…--space-8),
--measure, radii, shadows, and --transition already exist — don't re-create
them under new names; override their values (a different ratio, a denser rhythm)
and build every component against them. Never hardcode a raw color in a rule:
reach for the existing pair, or color-mix() off one.
/* A custom retheme = override the light-dark() pairs in :root.
Always set BOTH sides — a light-only override ships a broken dark scheme. */
:root {
--primary: light-dark(#3a6ea5, #7fb2e5);
--primary-strong: light-dark(#2c567f, #a3c9ee);
--bg: light-dark(#f7f9fb, #101418);
/* …continue through the ink ramp, surfaces, and --code-* slots… */
}
/* Manual user toggle (scaffolds do NOT ship one): flip color-scheme via a
data attribute + a small JS toggle that sets it. */
:root[data-theme="dark"] { color-scheme: dark; }
:root[data-theme="light"] { color-scheme: light; }
Reality check: dark mode is automatic now — don't re-plumb it.
- Every scaffold is light and dark out of the box: all color tokens are
light-dark() pairs under color-scheme: light dark, so the site follows
the OS scheme with zero extra CSS.
- Forcing dark permanently = append
:root { color-scheme: dark; } as the
last rule of the sheet — that is literally all the *-dark scaffolds do.
(Delete that rule to restore automatic switching.)
- A custom retheme = override the
light-dark() pairs in :root — always
supply both sides of each pair, or one scheme ships broken.
- A user-facing toggle = the
[data-theme] pattern above plus a small JS
toggle; you write the toggle, the tokens do the rest.
- A hygiene spec (
spec/unit/services/scaffolds/scaffold_token_hygiene_spec.cr) enforces that
scaffold CSS has no hardcoded colors outside the token definitions —
when you edit scaffold source CSS, keep it green by routing every color
through a token.
Code blocks are a design surface
Hwaro colors code server-side (Tartrazine), and the scaffolds' .hljs-*
rules read entirely from the nine --code-* tokens — so recoloring syntax
means editing those light-dark() pairs in :root, not touching the .hljs-*
rules themselves. Tie them to your palette (e.g. keywords near your accent) for
cohesion; both schemes come along automatically.
3. The quality bar
Production-grade design is a stack of deliberate decisions. Hit every layer.
- A point of view. Pick one organizing idea (Swiss/grid precision, warm
editorial, brutalist mono, retro terminal, glassy modern, dark tech…) and let
it govern every choice. Coherence reads as quality.
- Typography does the heavy lifting. A deliberate pairing (or one great
family with real weight range), a modular scale, tuned line-height and
measure. Details in Section 4.1.
- Color with intent. One dominant surface family, considered neutrals, ONE
confident accent. Details in Section 4.2.
- A spatial system. One spacing scale used everywhere. Consistent rhythm,
alignment, and generous negative space are what make a layout feel composed.
- Clear hierarchy & a focal point. Every screen has an obvious entry point
and an unambiguous reading order — established by size, weight, color, and
space, not boxes everywhere.
- Texture & detail, used with restraint. Hairline borders, a single
considered shadow elevation, a subtle gradient or grain, one small recurring
motif (the scaffolds use one short accent rule under the page title). Detail
signals care; clutter signals the opposite.
- Motion with purpose. Section 7. Always honor
prefers-reduced-motion.
- Responsive by construction. Design the small screen and the large screen,
not just a desktop that shrinks. Fluid type/space via
clamp(), real touch
targets.
- Accessibility is part of "good," not a checkbox. Semantic HTML, visible
:focus-visible, a skip link, alt text, sufficient contrast, logical heading
order.
4. Design engineering directives (bias correction)
LLMs default to clichés. Override these defaults proactively. Each rule has a
context-aware override path.
4.1 Typography
Display / headlines: large steps of the scale (--step-3/--step-4),
letter-spacing: -0.02em-ish tightening, line-height near 1–1.15,
text-wrap: balance.
Body: --step-0, line-height 1.5–1.7, max-width: var(--measure)
(60–75ch), text-wrap: pretty on paragraphs.
Fonts are self-hosted. @font-face + font-display: swap, subset files,
limit families/weights — fonts are usually the biggest perf cost of a
"designed" site. Never <link> Google Fonts in production. The scaffolds
embed Charter via OFL Charis SIL so headings render the same off-Apple.
Keep the CJK fallbacks. The shipped stacks include "Noto Serif KR"-class
fallbacks; if the audience reads CJK, stripping them breaks the design where
it matters most. (This also applies to OG images — CJK-capable font chains.)
SERIF DISCIPLINE. The ember scaffolds ship Charter serif headings, and
for content sites (blog, docs, book — genuinely editorial surfaces) that
default is legitimate. But when the brief is a landing page, portfolio,
agency, or product site, serif is very discouraged as the default
reach. "It feels creative / premium / editorial" is NOT a reason. Serif is
acceptable only when the brand brief literally names a serif, or the
aesthetic family is genuinely editorial / luxury / publication / heritage AND
you can articulate why this serif fits this brand. Otherwise default to a
characterful sans display. Specifically banned as default reaches:
Fraunces and Instrument Serif (the two LLM-favorite display serifs). If a
serif is justified, don't reuse the same one across consecutive projects.
EMPHASIS RULE. To emphasize a word inside a headline, use italic or
bold of the SAME family. Do not inject a serif word into a sans headline
(or vice versa) for visual interest — mixed-family emphasis is amateur.
ITALIC DESCENDER CLEARANCE. Italic display words containing y g j p q
get clipped at line-height: 1. Use ≥ 1.1 and reserve a few px of
padding-bottom on the wrapper. Audit every italic display word before
shipping.
No oversized H1s that just scream. Control hierarchy with weight, color,
and space, not raw scale alone.
4.2 Color calibration
- Max 1 accent color, saturation < 80% by default. Neutrals are never pure
#000/#fff (the ember ink ramp is the model). Use color-mix() for
tints/states instead of inventing new hex values.
- THE LILA RULE. The "AI purple/blue glow" aesthetic is banned as a
default. No automatic purple button glows, no random neon gradients. Neutral
bases with one high-contrast singular accent (emerald, electric blue, deep
rose, burnt orange…). Override: if the brand explicitly is purple, embrace
it — with a consistent palette and restrained gradients, not gradient slop.
- COLOR CONSISTENCY LOCK. Once the accent is chosen, it is THE accent for
the whole site. A warm-gray site does not get a blue CTA in the footer; a
rose-accented site does not get a teal badge. In Hwaro terms: everything
reads from
--primary / --primary-strong — if you're tempted to add a
second accent variable, you're breaking the lock.
- One palette per site. Don't fluctuate between warm and cool grays.
- PREMIUM-CONSUMER PALETTE BAN. For premium-consumer briefs (cookware,
wellness, artisan goods, luxury, DTC home goods) the LLM default is warm
cream/beige surfaces + brass/clay/oxblood accents + espresso near-black text.
Note: the ember scaffold palette IS this family (warm paper
#faf7f2,
rust accent, warm ink). That's fine as Hwaro's out-of-the-box identity — but
reaching for it (or just keeping it) as the design for a premium-consumer
client brief is the lazy move. Rotate to a genuinely different family:
- Cold luxury: silver-gray + chrome + smoke
- Forest: deep green + bone + amber accent
- Black and tan: true off-black + warm tan, sharp contrast, no beige
- Cobalt + cream: saturated blue against a single neutral
- Terracotta + slate: warm rust against cool gray
- Monochrome + one pop: off-white + off-black + one bright accent
Override: warm-craft beige is acceptable when the brand brief explicitly
names those colors or the identity is genuinely vintage/artisan AND you can
say why. Defaulting to it because "this is a cookware brief" is banned.
- Both sides, always. Every custom color is a
light-dark() pair with both
sides supplied (Section 2). A light-only override ships a broken dark scheme.
4.3 Layout diversification
- ANTI-CENTER BIAS. Centered hero / H1 sections are avoided when
DESIGN_VARIANCE > 4. Prefer split (50/50), left-aligned content with
right-aligned asset, asymmetric whitespace, or a pinned structure.
Override: centered is fine for editorial / manifesto briefs where the
message itself is the design — and for docs/book content pages, which are
reading surfaces, not compositions.
4.4 Materiality, shadows, cards
- Use cards ONLY when elevation communicates real hierarchy. Otherwise group
with a top border, row dividers used sparsely, or negative space.
- When a shadow is used, tint it toward the background hue (the ember
--shadow* tokens already do this). No pure-black drop shadows on light
backgrounds.
- For
VISUAL_DENSITY > 7: generic card containers are banned; data breathes
in plain layout with hairlines.
- SHAPE CONSISTENCY LOCK. One corner-radius system per site — that's what
--radius / --radius-sm are for. All-sharp (0), all-soft (12–16px), or
all-pill for interactive — pick one, or document the mixed rule ("buttons
pill, cards 16px, inputs 8px") and follow it everywhere. Round buttons in a
square layout is broken design.
4.5 Interactive states
- Tactile feedback: on
:active, a 1px translate or scale(0.98) to
simulate a physical push. Short eased transitions on all interactive
elements (var(--transition)).
- Visible
:focus-visible on every interactive element. A skip link at the
top of base.html/header.html.
- BUTTON CONTRAST CHECK. Every CTA's text is readable against its own
background: WCAG AA (4.5:1 body, 3:1 large/UI). White-on-white, ghost
buttons over photos with no scrim/stroke — banned. Audit every CTA.
- CTA BUTTON WRAP BAN. Button text fits on one line at desktop. If a label
wraps, shorten the label (1–3 words for primary CTAs) or widen the button.
- NO DUPLICATE CTA INTENT. "Get in touch" + "Contact us" + "Let's talk" on
one page = one intent, three labels = fail. Pick ONE label per intent and use
it everywhere (nav, hero, footer). Same for "View work" / "Browse projects".
- FORM CONTRAST CHECK. Inputs, placeholders, focus rings, helper and error
text all pass WCAG AA against the section background. Label ABOVE input;
error text below. No placeholder-as-label. Ever.
4.6 Layout discipline (hard rules — failing any of these is shipping broken work)
- Hero MUST fit the initial viewport. Headline ≤ 2 lines on desktop,
subtext ≤ 20 words AND ≤ 3–4 lines, CTA visible without scroll. If the copy
is too long, cut copy or reduce scale — never let the hero overflow.
Use
min-height: 100dvh for full-height heroes, never 100vh (iOS address
bar jump).
- Hero font-scale discipline. Plan font size and asset size together.
--step-4 territory only when the headline is 3–5 words; a 4-line hero
headline is always a font-size error, never a copy-length error.
- HERO TOP PADDING CAP. Max ~6rem top padding at desktop. If the hero needs
more breathing room, increase font scale or asset size, not top padding.
- HERO STACK DISCIPLINE (max 4 text elements): eyebrow OR brand strip (or
neither) · headline · subtext · CTAs (1 primary + max 1 secondary). BANNED in
the hero: tiny tagline below CTAs, trust micro-strip, pricing teaser, feature
bullet list, social-proof avatar row — those move to sections below.
- "Trusted by" logo wall belongs UNDER the hero, never inside it.
- Navigation renders on ONE line at desktop, height ≤ 80px (default
64–72px, the scaffolds'
--header-h). If items don't fit at 1024px, condense
labels or move to a menu. A two-line desktop nav is broken.
- Bento grids have rhythm and exact cell count. N items → N cells; no blank
filler tiles. Vary composition; and at least 2–3 cells in any multi-cell grid
need real visual variation (an image, a brand-appropriate gradient, a
pattern, a tinted background) — an all-same-surface bento with only
typography inside reads as AI default.
- SECTION-LAYOUT-REPETITION BAN. One layout family (3-col cards, full-width
quote, split text+image…) appears at most ONCE per page. A landing page with
8 sections uses at least 4 different layout families.
- ZIGZAG ALTERNATION CAP. Max 2 consecutive image+text split sections. The
3rd consecutive zigzag is a pre-flight fail — break it with a full-width
section, a vertical stack, a bento, or a marquee.
- EYEBROW RESTRAINT (the #1 violated rule). An eyebrow is the small
uppercase wide-tracking label above a section headline (CSS signature:
text-transform: uppercase; letter-spacing: 0.18em; font-size ~11px). Hard
rule: max 1 eyebrow per 3 sections, hero counts as one; if section A has
one, the next two don't. The check is mechanical: count
uppercase-tracked micro-labels across templates; count > ⌈sections / 3⌉
fails. Instead of an eyebrow: drop it — the headline alone is enough.
- SPLIT-HEADER BAN. "Left big headline + right small floating explainer
paragraph" as a section header is banned as default. Stack vertically
(headline, then body at
--measure). Reach for a split header only when the
right column carries a real visual or interactive element.
- Mobile collapse is explicit per section. Every multi-column layout
declares its < 768px fallback in the same stylesheet section. No "the grid
will probably handle it."
4.7 Content density & copy
Landing pages live on the first impression, not the full read. Cut
ruthlessly.
- Default section shape: short headline (≤ 8 words) + short sub-paragraph
(≤ 25 words) + one visual asset OR one CTA. More must be justified by the
section's job. (Docs and book content pages are exempt — they're reading
surfaces; this governs marketing/landing/home compositions.)
- No data-dump sections. A 20-row table or 30-item list on a marketing page
is the wrong layout: top 3–5 highlights + "view full list" link, a marquee /
scroll-snap row for breadth, or a separate page if the data is the product.
- Long lists need a different component, not a longer list. > 5 items:
2-column grouped split, card grid, tabs/accordion, horizontal scroll-snap
pills, or a marquee. A 10-row spec sheet with a hairline under every row is
the worst default — group into 2–3 chunks with sparse dividers, or promote
3–4 hero specs to display tiles and collapse the rest behind a disclosure.
- COPY SELF-AUDIT (mandatory before ship). Re-read every visible string
(headlines, eyebrows, buttons, captions, alt text, footer). Flag and rewrite
anything grammatically broken, with unclear referents, or that reads like an
LLM trying to sound thoughtful (forced metaphors, mock-poetic micro-meta,
fake-craftsman labels). If unsure whether a string makes sense, replace it
with a plain functional sentence. AI-cute copy is worse than boring copy.
- Fake-precise numbers are flagged.
92%, 4.1×, 5.8 mm either come
from real data, are explicitly labeled as sample data, or are banned. Don't
fake engineering precision the brand doesn't claim.
- One copy register per page. Don't mix technical-mono metadata, editorial
prose, and marketing punch unless the brand voice explicitly calls for it.
- Quotes & testimonials: max 3 lines of quote body — a landing-page quote
is a snippet, not the review. Attribution is name + role (+ company), never
name alone. Real typographic quotes ("") or none — not straight ASCII.
4.8 Image & visual asset strategy
Landing pages and portfolios are visual products. Text-only pages with
fake-screenshot divs are slop.
Priority order for visual assets:
- Image-generation tool first. If ANY image-gen tool is available in the
environment, use it to create section-specific assets: hero photography,
product shots, texture backgrounds — at the right aspect ratio per section.
Save the results into the page bundle or
static/images/ so they're real
files the build owns.
- Placeholder photography second. No gen tool →
https://picsum.photos/seed/{descriptive-seed}/{w}/{h} (seed describes the
section), or stock/brand URLs the brief provides. Treat hotlinks as
placeholders: for the shipped site, download assets into the project so
the static build is self-contained.
- Last resort: tell the user. Leave clearly-labeled placeholder slots
(
<!-- TODO: hero product photo, 1600x1200 -->) and end with: "This page
needs real images at: [placements]. Please generate or provide them." Do
NOT fill the page with hand-rolled SVG illustrations or div-based fakes.
Plumbing (Hwaro): run every real raster asset through
resize_image(path=…, width=…) — it returns .url, plus .lqip (blur-up
placeholder) and .dominant_color when LQIP is enabled — and build proper
srcset/sizes with lazy-loading. Don't ship one giant image. The built-in
processor resizes JPEG/PNG (BMP) only; for WebP/AVIF or aggressive optimization
run a build hook — resize_image passes unsupported formats through unresized.
Rules:
- Even minimalist sites need real images. A pure-text landing page is not
minimalism, it's incomplete. Even a restrained editorial site needs 2–3 real
images (hero, one product/lifestyle shot, one supporting). Blog/docs content
pages are exempt; their home/landing compositions are not.
- Div-based fake screenshots are banned. No fake task lists, fake
terminals, fake dashboards built from styled
<div>s. Use a real screenshot,
a generated image, or skip the preview.
- Hero needs a real visual. Text + gradient blob is a placeholder, not a
hero. (Exception: genuine editorial-manifesto heroes where type IS the
design.)
- Real logos for social proof. A "Trusted by" wall uses real SVG marks —
vendor them into
static/ (e.g. from Simple Icons) rather than hotlinking a
CDN; ensure they render in both schemes (single-color via currentColor or a
token). For invented brands, draw a simple monogram mark — a plain text
wordmark row looks generic. Logo wall = logos only: no industry/category
captions under each logo.
4.9 Icons & emoji
- Use an established icon set, one family per site. Vendor the SVGs you
need (Phosphor, Tabler, Radix, Lucide et al. all offer per-icon SVG
downloads) into
static/icons/ or inline them as partials. Standardize
stroke-width across the set. Never hand-draw icon paths from scratch.
- Hand-rolled decorative SVGs (custom illustrations, logos) are strongly
discouraged as a default — acceptable only for a single simple geometric
mark, or when the user explicitly asks.
- Emoji are discouraged in markup and visible text — the emoji-per-card
feature row is a top AI tell. Use icon glyphs. Override: an explicitly
playful/chat-style brief, sparingly, with intent.
5. Signature detail
Give the design ONE small recurring mark — an accent rule (the scaffolds use a
short ember rule under the page title), a marker bullet, a consistent hover
behavior — rather than decorating everything. One motif, applied consistently,
reads as identity; five motifs read as noise.
6. Pattern vocabulary (names to design with)
A vocabulary, not a library — know the names so you can propose and reason
about them. Implementation cost on a static site matters: prefer pure CSS,
then small vanilla JS, and treat heavy JS as a deliberate, justified
exception (there is no bundler unless you add one via build hooks).
- Hero paradigms: asymmetric split hero · editorial manifesto hero (type as
poster) · media-mask hero (type cut out over image/video) · kinetic-type
hero · scroll-pinned hero (CSS
position: sticky).
- Layout & grids: bento grid · masonry · split-screen scroll ·
sticky-stack sections (sticky + scroll-driven scale/fade).
- Cards & containers: spotlight border card · glassmorphism panel
(
backdrop-filter + 1px inner border + inset highlight; solid fallback under
prefers-reduced-transparency) · morphing modal (<dialog> + view
transitions).
- Scroll effects: sticky scroll stack · zoom parallax · scroll progress
bar (
animation-timeline: scroll()) · reveal-on-enter
(animation-timeline: view() or IntersectionObserver).
- Typography & text: kinetic marquee (CSS keyframes; max one per page) ·
text mask reveal · circular text path (SVG
<textPath>) · gradient stroke
text.
- Micro-interactions: directional hover fill · ripple click · skeleton
shimmer · animated SVG line drawing (
stroke-dasharray).
Heavy-JS patterns (scroll hijack, WebGL scenes, physics cursors) are usually
the wrong trade for a static site; if the brief truly demands one, isolate it
in one small script, lazy-load it, and degrade gracefully without it.
7. Motion for a static site
No React, no animation framework — and none needed. The toolkit, in order:
- CSS transitions for all interactive states (
var(--transition),
cubic-bezier(0.16, 1, 0.3, 1) for bigger moves).
- CSS keyframes +
animation-delay cascades for load-in staggers
(animation-delay: calc(var(--i) * 60ms) with a per-item --i).
- CSS scroll-driven animations (
animation-timeline: view() /
scroll()) for reveal and progress effects — always behind
@supports (animation-timeline: view()) so unsupported browsers get static
content, not broken content.
- IntersectionObserver (small vanilla script) as the portable reveal
mechanism:
<script>
const io = new IntersectionObserver((entries) => {
for (const e of entries) if (e.isIntersecting) {
e.target.classList.add("is-visible");
io.unobserve(e.target);
}
}, { threshold: 0.3 });
document.querySelectorAll("[data-reveal]").forEach((el) => io.observe(el));
</script>
[data-reveal] { opacity: 0; translate: 0 24px;
transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
translate 0.6s cubic-bezier(0.16, 1, 0.3, 1); }
[data-reveal].is-visible { opacity: 1; translate: 0 0; }
@media (prefers-reduced-motion: reduce) {
[data-reveal] { opacity: 1; translate: none; transition: none; }
}
Hard rules:
- MOTION MUST BE MOTIVATED. Before adding any animation ask: what does it
communicate? Valid: hierarchy, storytelling sequence, feedback, state
transition. Invalid: "it looked cool." If you can't articulate the reason in
one sentence, drop it. Not every card needs an infinite loop — informational
sections stay still.
- "Motion claimed, motion shown." If
MOTION_INTENSITY > 4, the page
actually moves: hero entry, key-section reveals, CTA hover physics at
minimum. Can't ship working motion in scope? Drop the dial to 3 and ship a
clean static page. Never half-build motion that breaks.
- MARQUEE MAX-ONE-PER-PAGE. Two marquees on one page is lazy filler.
window.addEventListener("scroll", …) is banned. Use scroll-driven CSS,
IntersectionObserver, or nothing. Same ban for rAF loops recomputing layout
from scrollY.
- Animate ONLY
transform/translate/scale and opacity. Never top,
left, width, height. will-change sparingly.
- Reduced motion is non-negotiable. Anything above
MOTION_INTENSITY 3
honors prefers-reduced-motion: reduce: infinite loops, parallax, and
reveals collapse to static/instant.
- Grain/noise overlays live exclusively on a fixed
pointer-events: none
element — never on scrolling containers (continuous repaints destroy mobile
FPS).
- z-index restraint. A documented scale for systemic la
…(truncated)
1---2name: hwaro-design3description: Use when designing or restyling a Hwaro site — choosing a visual direction, customizing templates and CSS, building or retheming a theme, picking typography and color, or improving an existing layout. Read the brief, declare a Design Read and set the three dials, interview only when intent is genuinely ambiguous, then produce distinctive, production-grade design within Hwaro's Crinja template + CSS-variable token system — under a strict anti-slop discipline and a mechanical pre-flight check. Pair with the `hwaro` skill, which covers operating the CLI itself.4---56# Hwaro Design78Your job is to design Hwaro sites that look **intentional, distinctive, and9genuinely good** — and that reflect *this user's* taste, not a generic default.10A static site generator makes the plumbing trivial; the design is where the site11either feels considered or feels like every other template. Aim for the former.1213This skill assumes the project is a Hwaro site (config.toml + `templates/` +14`static/`). For scaffolding, building, and serving, use the **`hwaro`** skill;15iterate with `hwaro serve` (live reload) so the user sees changes instantly.1617Every rule below is **contextual** — none fires automatically. First read the18brief, then pull only what fits. But the bans in Section 9 and the Pre-Flight19Check in Section 12 are hard filters: output that violates them is not done.2021---2223## 0. The Design Read — read the room before anything else2425Before touching code or setting dials, **infer what the user actually wants**.26Most AI design output is bad because the model jumps to a default aesthetic27instead of reading the room.2829### 0.A Read these signals first30311. **Site kind** — blog / docs / portfolio (dev, designer, studio) / landing32 (product, event, agency) / book / shop-adjacent brochure / redesign33 (preserve vs. overhaul).342. **Vibe words** the user used — "minimalist", "calm", "Linear-style",35 "Awwwards", "brutalist", "premium consumer", "Apple-y", "playful",36 "serious B2B", "editorial", "glassy", "dark tech", "warm-handcrafted".373. **Reference signals** — URLs they linked, screenshots they pasted, products38 or brands they named.394. **Audience** — recruiters scanning a portfolio vs. developers reading docs40 vs. design-conscious consumers. The audience picks the aesthetic, not your41 taste.425. **Brand assets that already exist** — logo, color, type, photography. For43 redesigns these are starting material, not optional input (Section 10).446. **Quiet constraints** — accessibility-first audiences, public-sector,45 regulated industries, kids' content, CJK-primary readership (keep the CJK46 font fallbacks — see Section 4.1). These constraints OVERRIDE aesthetic47 preference.4849### 0.B Declare a one-line Design Read before generating5051Before any code, state in one line: **"Reading this as: \<site kind> for52\<audience>, with a \<vibe> language, leaning toward \<aesthetic family>"** —53followed by the three dial values (Section 1) and why.5455Example reads:5657- *"Reading this as: developer blog for peers, with a calm technical-editorial58 language, leaning toward mono-accented minimalism. Dials: 5 / 3 / 4."*59- *"Reading this as: product landing for design-conscious indie devs, with a60 confident dark-tech language, leaning toward near-black canvas + single61 electric accent. Dials: 7 / 6 / 3."*62- *"Reading this as: redesign-preserve of an existing docs site — extract the63 current tokens first, evolve typography and rhythm, keep the IA. Dials: match64 existing."*6566### 0.C Question policy — hybrid6768- **If you can confidently infer the direction from the brief, conversation, or69 the existing site: do not ask.** Declare the Design Read and proceed. A70 confident direction the user can react to beats an interview they didn't want.71- **If the read genuinely diverges** (e.g. "Linear-clean or72 Awwwards-experimental?" would produce different sites), ask a **short round —73 1–2 focused questions**, not a survey. Use `AskUserQuestion` when available:74 2–4 concrete options per question with short descriptions, and lean on its75 **option previews** — a token-value block or a small ASCII layout sketch per76 option beats adjectives; people pick a palette they can *see*.77- **Run a full interview only when the user asks to explore taste** ("interview78 me", "help me figure out what I want") or a greenfield brief gives you nothing79 to read. Then cover, in order, stopping once you can commit: purpose &80 audience · personality + 1–3 reference sites · color mood incl. light/dark ·81 type feel · density & layout · motion appetite · constraints.82- Either way, for any substantial engagement **play the brief back** in 4–683 lines ("Warm editorial blog: cream paper background, single rust accent,84 Charter-style serif headings over a clean sans body, generous whitespace,85 motion limited to gentle link transitions") and confirm before building big.86 The brief is the contract you design against.87- If a confirmed brief already exists earlier in the conversation, don't88 re-interview — proceed from it, and only revisit taste if the user's89 direction changed.9091### 0.D Anti-Default Discipline9293Do not default to: AI-purple gradients, centered hero over dark mesh, three94equal feature cards with an emoji each, generic glassmorphism on everything,95infinite-loop micro-animations, system-font-with-no-personality — and, the96Hwaro-specific one, **shipping the ember scaffold palette unchanged and calling97it a design**. The ember tokens are a *starting vocabulary*, not the answer to98a brief. Reach past the defaults deliberately, based on the Design Read.99100---101102## 1. The Three Dials103104After the Design Read, set three dials. Every layout, motion, and density105decision below is gated by these.106107- **`DESIGN_VARIANCE`** — 1 = perfect symmetry, 10 = artsy chaos108- **`MOTION_INTENSITY`** — 1 = static, 10 = cinematic choreography109- **`VISUAL_DENSITY`** — 1 = art gallery / airy, 10 = cockpit / packed data110111State the values in the Design Read. Never silently assume a baseline —112reason them from the brief. Overrides happen conversationally ("crank the113motion to 8").114115### 1.A Dial inference (Design Read → dial values)116117| Signal | VARIANCE | MOTION | DENSITY |118|---|---|---|---|119| "minimalist / clean / calm / editorial / Linear-style" | 5–6 | 3–4 | 2–3 |120| "premium consumer / Apple-y / luxury / brand" | 7–8 | 5–7 | 3–4 |121| "playful / wild / Awwwards / experimental / agency" | 9–10 | 8–10 | 3–4 |122| Blog (personal / technical) | 5–6 | 3–4 | 3–4 |123| Docs / book | 3–4 | 2–3 | 4–6 |124| Landing / portfolio (default) | 7–9 | 5–7 | 3–5 |125| "trust-first / public-sector / accessibility-critical" | 3–4 | 2–3 | 4–5 |126| Redesign — preserve | match existing | +1 | match existing |127| Redesign — overhaul | +2 | +2 | match existing |128129### 1.B Dial definitions (technical reference, plain-CSS terms)130131**DESIGN_VARIANCE**132- **1–3 (Predictable):** symmetrical grid (equal `fr` units), equal paddings,133 centered alignment. Right for docs, books, trust-first.134- **4–7 (Offset):** negative-margin overlaps, varied image aspect ratios (4:3135 next to 16:9), left-aligned headers over centered content blocks.136- **8–10 (Asymmetric):** masonry, fractional grids137 (`grid-template-columns: 2fr 1fr 1fr`), massive deliberate empty zones138 (`padding-inline-start: 20vw`).139- **MOBILE OVERRIDE:** for levels 4–10, asymmetric layouts MUST collapse to a140 strict single column below 768px. Declare the collapse in the same rule141 block, not "it'll probably reflow."142143**MOTION_INTENSITY**144- **1–3 (Static):** `:hover` / `:active` / `:focus-visible` transitions only.145- **4–7 (Fluid CSS):** `transition: … 0.3s cubic-bezier(0.16, 1, 0.3, 1)`,146 `animation-delay` cascades for load-ins, transform/opacity only.147- **8–10 (Choreography):** scroll-driven reveals via CSS148 `animation-timeline: view()` (behind `@supports`) or an IntersectionObserver149 (Section 7). **`window.addEventListener("scroll", …)` is a hard ban**, not a150 prefer-not.151152**VISUAL_DENSITY**153- **1–3 (Art gallery):** huge section gaps (8–12rem), few elements, expensive154 whitespace.155- **4–7 (Daily site):** standard rhythm (4–6rem section padding) — the156 `--space-*` scale as-shipped.157- **8–10 (Cockpit):** tight paddings, no card boxes, hairlines separate data,158 `--font-mono` for all numbers.159160---161162## 2. How design lives in a Hwaro site163164Know the actual surfaces you can touch — this is what makes the design *real*165and not just advice.166167### Templates (Crinja / Jinja2-compatible) — the structure168169Under `templates/`. Hwaro supports two composition styles and the built-in170scaffolds use **both**, so open the project's `templates/` and see which before171editing: `{% extends "base.html" %}` inheritance filling `{% block content %}`172(the `docs` scaffold works this way), and `{% include "header.html" %}` /173`{% include "footer.html" %}` partial composition (`blog`, `book`, `simple`).174Other files you'll meet: `page.html` / `section.html` (content layouts),175`index.html` (home), `taxonomy*.html`, and partials like `nav.html` /176`sidebar.html`. Layout changes (what wraps what) happen here; *look* (color,177type, spacing) happens in CSS. Keep markup semantic and class-driven so the CSS178can do the work.179180**Internal URLs must carry the base URL.** The scaffolds write every181site-internal href/src as `{{ base_url }}{{ page.url }}`,182`{{ base_url }}{{ lang_prefix }}/about/`, `{{ base_url }}/css/style.css` —183never a bare `/about/`. Keep that pattern in every template you author, or the184site 404s the moment it's deployed under a subpath (GitHub Pages project185sites).186187### CSS delivery — three options, pick deliberately1881891. **Inlined `<style>`** — the `simple` scaffold ships its CSS in a `<style>`190 block inside `header.html`: zero extra requests, great for small sites; edit191 that block directly. (`blog` / `book` / `docs` instead ship an external192 `static/css/style.css` — option 2; `bare` ships *no* CSS at all, so you bring193 your own.)1942. **Static stylesheet + `[auto_includes]`** — drop files in `static/assets/css/`195 (and `…/js`) and emit `{{ auto_includes_css }}` / `{{ auto_includes_js }}` in196 your head/body. (These output raw HTML; Hwaro disables template autoescape, so197 the scaffolds write them without `| safe` — the docs add `| safe` as a198 harmless, portable convention you can keep.) Good when CSS grows past "one block."1993. **`[assets]` pipeline** — declare bundles in `config.toml`, reference with200 `{{ asset(name='main.css') }}`; you get concatenation, minification, and201 content-hash fingerprinting (cache busting) for free.202203> **There is a built-in SCSS compiler (opt-in via `[sass] enabled = true`),204> but no Tailwind step and no component framework.** The built-in compiler205> covers the practical subset — variables, nesting/`&`, partials with206> `@use`/`@import`, mixins with `@content`, interpolation — but NOT control207> flow (`@if`/`@each`), arithmetic, or built-in functions (`lighten()` etc.),208> so framework-grade SCSS won't compile. Default to modern plain CSS (custom209> properties, nesting where supported, `color-mix()`, `clamp()`); use `[sass]`210> when the brief calls for it, or run Tailwind/PostCSS/full dart-sass via211> **build hooks** and point Hwaro at the compiled output. Don't assume an npm212> design system exists. When a brief names an aesthetic213> (glass, bento, brutalist, editorial, dark tech, aurora), that's a CSS214> language you build honestly by hand — there is no official package for any of215> them, on any stack.216217### Design tokens are your single source of truth218219Every Hwaro scaffold themes itself through **CSS custom properties** in `:root`.220This is the right pattern: define the system once, theme by editing tokens, and221get dark mode + restyles almost for free. All eight scaffolds share **one**222"Hwaro Ember" vocabulary: every color token is a `light-dark(light, dark)` pair223resolved by `color-scheme`, so every scaffold (including `simple`) follows the224reader's OS scheme automatically. The vocabulary is (values exact; font stacks225abbreviated — the real ones are longer and include CJK fallbacks like226`"Noto Serif KR"`, so don't strip those if your audience needs CJK):227228```css229:root {230 color-scheme: light dark;231232 /* Accent — the single ember. */233 --primary: light-dark(#b35454, #ec7a66);234 --primary-strong: light-dark(#8f4040, #f39683); /* hover/active accent */235 --primary-tint: color-mix(in srgb, var(--primary) 8%, transparent);236 --selection: color-mix(in srgb, var(--primary) 22%, transparent);237238 /* Ember rule — the one mark every scaffold shares. */239 --rule-from: light-dark(#c46262, #f39683);240 --rule-to: light-dark(#8f4040, #cc5d4b);241242 /* Ink — a three-step ramp (not pure black/white). */243 --heading: light-dark(#241f1a, #f5f2ed);244 --text: light-dark(#2a241f, #dedad3);245 --text-secondary: light-dark(#5c5248, #a7a199);246 --text-muted: light-dark(#6f6358, #7d776e);247248 /* Surfaces & edges. */249 --bg: light-dark(#faf7f2, #0f0f0e);250 --bg-subtle: light-dark(#f1eae0, #1a1917);251 --bg-code: light-dark(#f1eae0, #1e1c19);252 --border: light-dark(#e4dacd, #2b2926);253 --border-subtle: light-dark(#efe8dd, #201e1c);254 --edge: color-mix(in srgb, var(--text) 8%, transparent);255 --glass: color-mix(in srgb, var(--bg) 85%, transparent);256 --scrim: light-dark(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6));257258 /* Support hues (info boxes only — the accent stays singular). */259 --warn: light-dark(#b07d2e, #d9a45a);260 --ok: light-dark(#5e8c61, #8fb491);261262 /* Syntax — nine slots the .hljs-* theme reads from. */263 --code-comment: light-dark(#a1907c, #8a8073);264 --code-keyword: light-dark(#b03a2e, #f0846f);265 --code-string: light-dark(#5f7032, #b7c06a);266 --code-number: light-dark(#9a6a14, #e8a83f);267 --code-func: light-dark(#2f6a5a, #8ec5a3);268 --code-type: light-dark(#b0641c, #e6914f);269 --code-variable: light-dark(#8a4a3a, #e8b0a0);270 --code-attr: light-dark(#45617a, #93b5c8);271 --code-symbol: light-dark(#8a4368, #d79bb8);272273 /* Type scale — minor third (1.2), fluid via clamp(). */274 --step--1: clamp(0.83rem, 0.81rem + 0.11vw, 0.89rem);275 --step-0: clamp(1rem, 0.96rem + 0.22vw, 1.125rem);276 --step-1: clamp(1.2rem, 1.13rem + 0.35vw, 1.4rem);277 --step-2: clamp(1.44rem, 1.32rem + 0.61vw, 1.78rem);278 --step-3: clamp(1.73rem, 1.53rem + 0.98vw, 2.28rem);279 --step-4: clamp(2.07rem, 1.77rem + 1.52vw, 2.92rem);280281 /* Space — 8px rhythm. */282 --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem; --space-4: 1rem;283 --space-5: 1.5rem; --space-6: 2.5rem; --space-7: 4rem; --space-8: 6rem;284285 /* Shape, depth, motion, measure. */286 --measure: 68ch;287 --radius: 10px;288 --radius-sm: 6px;289 --shadow-sm: 0 1px 2px light-dark(rgba(42, 36, 31, 0.05), rgba(0, 0, 0, 0.3));290 --shadow: 0 2px 8px light-dark(rgba(42, 36, 31, 0.08), rgba(0, 0, 0, 0.4));291 --shadow-lg: 0 16px 70px light-dark(rgba(42, 36, 31, 0.18), rgba(0, 0, 0, 0.5));292 --transition: 0.15s ease;293294 /* Faces. */295 --font-serif: "Charter", Georgia, "Noto Serif KR", serif; /* headings (+CJK) */296 --font-sans: -apple-system, "Segoe UI", Roboto, sans-serif; /* body */297 --font-mono: ui-monospace, "SF Mono", Menlo, monospace; /* code */298}299```300301> **Every scaffold shares this exact vocabulary.** It is emitted from one place —302> `DesignTokens.root_block` in `src/services/scaffolds/design_tokens.cr` in the303> Hwaro repo — so the old per-scaffold name drift is gone (`--primary-hover`,304> `--bg-secondary`, and `--border-light` were renamed to `--primary-strong`,305> `--bg-subtle`, and `--border-subtle`). The only per-scaffold additions are306> layout tokens (`--header-h`, `--sidebar-w`, `--content-max-w`, `--bg-sidebar`,307> radius overrides) injected into the same `:root`. A308> `@supports not (color: light-dark(#000, #fff))` block pins the static light309> palette for pre-2024 browsers, so a `*-dark` site renders light there —310> readable and on-brand rather than broken.311312**Retheme by rewriting token values, not by adding parallel systems.** The type313scale (`--step--1`…`--step-4`), spacing rhythm (`--space-1`…`--space-8`),314`--measure`, radii, shadows, and `--transition` already exist — don't re-create315them under new names; override their values (a different ratio, a denser rhythm)316and build every component against them. Never hardcode a raw color in a rule:317reach for the existing pair, or `color-mix()` off one.318319```css320/* A custom retheme = override the light-dark() pairs in :root.321 Always set BOTH sides — a light-only override ships a broken dark scheme. */322:root {323 --primary: light-dark(#3a6ea5, #7fb2e5);324 --primary-strong: light-dark(#2c567f, #a3c9ee);325 --bg: light-dark(#f7f9fb, #101418);326 /* …continue through the ink ramp, surfaces, and --code-* slots… */327}328329/* Manual user toggle (scaffolds do NOT ship one): flip color-scheme via a330 data attribute + a small JS toggle that sets it. */331:root[data-theme="dark"] { color-scheme: dark; }332:root[data-theme="light"] { color-scheme: light; }333```334335> **Reality check: dark mode is automatic now — don't re-plumb it.**336> - Every scaffold is light **and** dark out of the box: all color tokens are337> `light-dark()` pairs under `color-scheme: light dark`, so the site follows338> the OS scheme with zero extra CSS.339> - **Forcing dark permanently** = append `:root { color-scheme: dark; }` as the340> last rule of the sheet — that is literally all the `*-dark` scaffolds do.341> (Delete that rule to restore automatic switching.)342> - **A custom retheme** = override the `light-dark()` pairs in `:root` — always343> supply **both** sides of each pair, or one scheme ships broken.344> - **A user-facing toggle** = the `[data-theme]` pattern above plus a small JS345> toggle; you write the toggle, the tokens do the rest.346> - A hygiene spec (`spec/unit/services/scaffolds/scaffold_token_hygiene_spec.cr`) enforces that347> scaffold CSS has **no hardcoded colors outside the token definitions** —348> when you edit scaffold source CSS, keep it green by routing every color349> through a token.350351### Code blocks are a design surface352353Hwaro colors code **server-side** (Tartrazine), and the scaffolds' `.hljs-*`354rules read entirely from the nine `--code-*` tokens — so recoloring syntax355means editing those `light-dark()` pairs in `:root`, not touching the `.hljs-*`356rules themselves. Tie them to your palette (e.g. keywords near your accent) for357cohesion; both schemes come along automatically.358359---360361## 3. The quality bar362363Production-grade design is a stack of deliberate decisions. Hit every layer.364365- **A point of view.** Pick one organizing idea (Swiss/grid precision, warm366 editorial, brutalist mono, retro terminal, glassy modern, dark tech…) and let367 it govern every choice. Coherence reads as quality.368- **Typography does the heavy lifting.** A deliberate pairing (or one great369 family with real weight range), a modular scale, tuned line-height and370 measure. Details in Section 4.1.371- **Color with intent.** One dominant surface family, considered neutrals, ONE372 confident accent. Details in Section 4.2.373- **A spatial system.** One spacing scale used everywhere. Consistent rhythm,374 alignment, and generous negative space are what make a layout feel composed.375- **Clear hierarchy & a focal point.** Every screen has an obvious entry point376 and an unambiguous reading order — established by size, weight, color, and377 space, not boxes everywhere.378- **Texture & detail, used with restraint.** Hairline borders, a *single*379 considered shadow elevation, a subtle gradient or grain, one small recurring380 motif (the scaffolds use one short accent rule under the page title). Detail381 signals care; clutter signals the opposite.382- **Motion with purpose.** Section 7. Always honor `prefers-reduced-motion`.383- **Responsive by construction.** Design the small screen and the large screen,384 not just a desktop that shrinks. Fluid type/space via `clamp()`, real touch385 targets.386- **Accessibility is part of "good," not a checkbox.** Semantic HTML, visible387 `:focus-visible`, a skip link, alt text, sufficient contrast, logical heading388 order.389390---391392## 4. Design engineering directives (bias correction)393394LLMs default to clichés. Override these defaults proactively. Each rule has a395context-aware override path.396397### 4.1 Typography398399- **Display / headlines:** large steps of the scale (`--step-3`/`--step-4`),400 `letter-spacing: -0.02em`-ish tightening, `line-height` near 1–1.15,401 `text-wrap: balance`.402- **Body:** `--step-0`, `line-height` 1.5–1.7, `max-width: var(--measure)`403 (60–75ch), `text-wrap: pretty` on paragraphs.404- **Fonts are self-hosted.** `@font-face` + `font-display: swap`, subset files,405 limit families/weights — fonts are usually the biggest perf cost of a406 "designed" site. Never `<link>` Google Fonts in production. The scaffolds407 embed Charter via OFL Charis SIL so headings render the same off-Apple.408- **Keep the CJK fallbacks.** The shipped stacks include `"Noto Serif KR"`-class409 fallbacks; if the audience reads CJK, stripping them breaks the design where410 it matters most. (This also applies to OG images — CJK-capable font chains.)411412- **SERIF DISCIPLINE.** The ember scaffolds ship Charter serif headings, and413 for *content* sites (blog, docs, book — genuinely editorial surfaces) that414 default is legitimate. But when the brief is a **landing page, portfolio,415 agency, or product site**, serif is **very discouraged as the default416 reach**. "It feels creative / premium / editorial" is NOT a reason. Serif is417 acceptable only when the brand brief literally names a serif, or the418 aesthetic family is genuinely editorial / luxury / publication / heritage AND419 you can articulate why this serif fits this brand. Otherwise default to a420 characterful sans display. **Specifically banned as default reaches:421 Fraunces and Instrument Serif** (the two LLM-favorite display serifs). If a422 serif is justified, don't reuse the same one across consecutive projects.423- **EMPHASIS RULE.** To emphasize a word inside a headline, use *italic* or424 **bold of the SAME family**. Do not inject a serif word into a sans headline425 (or vice versa) for visual interest — mixed-family emphasis is amateur.426- **ITALIC DESCENDER CLEARANCE.** Italic display words containing `y g j p q`427 get clipped at `line-height: 1`. Use ≥ 1.1 and reserve a few px of428 padding-bottom on the wrapper. Audit every italic display word before429 shipping.430- **No oversized H1s that just scream.** Control hierarchy with weight, color,431 and space, not raw scale alone.432433### 4.2 Color calibration434435- **Max 1 accent color**, saturation < 80% by default. Neutrals are never pure436 `#000`/`#fff` (the ember ink ramp is the model). Use `color-mix()` for437 tints/states instead of inventing new hex values.438- **THE LILA RULE.** The "AI purple/blue glow" aesthetic is banned as a439 default. No automatic purple button glows, no random neon gradients. Neutral440 bases with one high-contrast singular accent (emerald, electric blue, deep441 rose, burnt orange…). *Override:* if the brand explicitly is purple, embrace442 it — with a consistent palette and restrained gradients, not gradient slop.443- **COLOR CONSISTENCY LOCK.** Once the accent is chosen, it is THE accent for444 the whole site. A warm-gray site does not get a blue CTA in the footer; a445 rose-accented site does not get a teal badge. In Hwaro terms: **everything446 reads from `--primary` / `--primary-strong`** — if you're tempted to add a447 second accent variable, you're breaking the lock.448- **One palette per site.** Don't fluctuate between warm and cool grays.449- **PREMIUM-CONSUMER PALETTE BAN.** For premium-consumer briefs (cookware,450 wellness, artisan goods, luxury, DTC home goods) the LLM default is warm451 cream/beige surfaces + brass/clay/oxblood accents + espresso near-black text.452 **Note: the ember scaffold palette IS this family** (warm paper `#faf7f2`,453 rust accent, warm ink). That's fine as Hwaro's out-of-the-box identity — but454 reaching for it (or just keeping it) as *the design* for a premium-consumer455 client brief is the lazy move. Rotate to a genuinely different family:456 - **Cold luxury:** silver-gray + chrome + smoke457 - **Forest:** deep green + bone + amber accent458 - **Black and tan:** true off-black + warm tan, sharp contrast, no beige459 - **Cobalt + cream:** saturated blue against a single neutral460 - **Terracotta + slate:** warm rust against cool gray461 - **Monochrome + one pop:** off-white + off-black + one bright accent462 *Override:* warm-craft beige is acceptable when the brand brief explicitly463 names those colors or the identity is genuinely vintage/artisan AND you can464 say why. Defaulting to it because "this is a cookware brief" is banned.465- **Both sides, always.** Every custom color is a `light-dark()` pair with both466 sides supplied (Section 2). A light-only override ships a broken dark scheme.467468### 4.3 Layout diversification469470- **ANTI-CENTER BIAS.** Centered hero / H1 sections are avoided when471 `DESIGN_VARIANCE > 4`. Prefer split (50/50), left-aligned content with472 right-aligned asset, asymmetric whitespace, or a pinned structure.473 *Override:* centered is fine for editorial / manifesto briefs where the474 message itself is the design — and for docs/book content pages, which are475 reading surfaces, not compositions.476477### 4.4 Materiality, shadows, cards478479- Use cards ONLY when elevation communicates real hierarchy. Otherwise group480 with a top border, row dividers used sparsely, or negative space.481- When a shadow is used, tint it toward the background hue (the ember482 `--shadow*` tokens already do this). No pure-black drop shadows on light483 backgrounds.484- For `VISUAL_DENSITY > 7`: generic card containers are banned; data breathes485 in plain layout with hairlines.486- **SHAPE CONSISTENCY LOCK.** One corner-radius system per site — that's what487 `--radius` / `--radius-sm` are for. All-sharp (0), all-soft (12–16px), or488 all-pill for interactive — pick one, or document the mixed rule ("buttons489 pill, cards 16px, inputs 8px") and follow it everywhere. Round buttons in a490 square layout is broken design.491492### 4.5 Interactive states493494- **Tactile feedback:** on `:active`, a 1px translate or `scale(0.98)` to495 simulate a physical push. Short eased transitions on all interactive496 elements (`var(--transition)`).497- **Visible `:focus-visible`** on every interactive element. A skip link at the498 top of `base.html`/`header.html`.499- **BUTTON CONTRAST CHECK.** Every CTA's text is readable against its own500 background: WCAG AA (4.5:1 body, 3:1 large/UI). White-on-white, ghost501 buttons over photos with no scrim/stroke — banned. Audit every CTA.502- **CTA BUTTON WRAP BAN.** Button text fits on one line at desktop. If a label503 wraps, shorten the label (1–3 words for primary CTAs) or widen the button.504- **NO DUPLICATE CTA INTENT.** "Get in touch" + "Contact us" + "Let's talk" on505 one page = one intent, three labels = fail. Pick ONE label per intent and use506 it everywhere (nav, hero, footer). Same for "View work" / "Browse projects".507- **FORM CONTRAST CHECK.** Inputs, placeholders, focus rings, helper and error508 text all pass WCAG AA against the section background. Label ABOVE input;509 error text below. **No placeholder-as-label. Ever.**510511### 4.6 Layout discipline (hard rules — failing any of these is shipping broken work)512513- **Hero MUST fit the initial viewport.** Headline ≤ 2 lines on desktop,514 subtext ≤ 20 words AND ≤ 3–4 lines, CTA visible without scroll. If the copy515 is too long, cut copy or reduce scale — never let the hero overflow.516 Use `min-height: 100dvh` for full-height heroes, never `100vh` (iOS address517 bar jump).518- **Hero font-scale discipline.** Plan font size and asset size *together*.519 `--step-4` territory only when the headline is 3–5 words; a 4-line hero520 headline is always a font-size error, never a copy-length error.521- **HERO TOP PADDING CAP.** Max ~6rem top padding at desktop. If the hero needs522 more breathing room, increase font scale or asset size, not top padding.523- **HERO STACK DISCIPLINE (max 4 text elements):** eyebrow OR brand strip (or524 neither) · headline · subtext · CTAs (1 primary + max 1 secondary). BANNED in525 the hero: tiny tagline below CTAs, trust micro-strip, pricing teaser, feature526 bullet list, social-proof avatar row — those move to sections below.527- **"Trusted by" logo wall belongs UNDER the hero, never inside it.**528- **Navigation renders on ONE line at desktop, height ≤ 80px** (default529 64–72px, the scaffolds' `--header-h`). If items don't fit at 1024px, condense530 labels or move to a menu. A two-line desktop nav is broken.531- **Bento grids have rhythm and exact cell count.** N items → N cells; no blank532 filler tiles. Vary composition; and at least 2–3 cells in any multi-cell grid533 need real visual variation (an image, a brand-appropriate gradient, a534 pattern, a tinted background) — an all-same-surface bento with only535 typography inside reads as AI default.536- **SECTION-LAYOUT-REPETITION BAN.** One layout family (3-col cards, full-width537 quote, split text+image…) appears at most ONCE per page. A landing page with538 8 sections uses at least 4 different layout families.539- **ZIGZAG ALTERNATION CAP.** Max 2 consecutive image+text split sections. The540 3rd consecutive zigzag is a pre-flight fail — break it with a full-width541 section, a vertical stack, a bento, or a marquee.542- **EYEBROW RESTRAINT** (the #1 violated rule). An eyebrow is the small543 uppercase wide-tracking label above a section headline (CSS signature:544 `text-transform: uppercase; letter-spacing: 0.18em; font-size ~11px`). Hard545 rule: **max 1 eyebrow per 3 sections**, hero counts as one; if section A has546 one, the next two don't. The check is mechanical: count547 uppercase-tracked micro-labels across templates; count > ⌈sections / 3⌉548 fails. Instead of an eyebrow: drop it — the headline alone is enough.549- **SPLIT-HEADER BAN.** "Left big headline + right small floating explainer550 paragraph" as a section header is banned as default. Stack vertically551 (headline, then body at `--measure`). Reach for a split header only when the552 right column carries a real visual or interactive element.553- **Mobile collapse is explicit per section.** Every multi-column layout554 declares its < 768px fallback in the same stylesheet section. No "the grid555 will probably handle it."556557### 4.7 Content density & copy558559Landing pages live on the **first impression**, not the full read. Cut560ruthlessly.561562- **Default section shape:** short headline (≤ 8 words) + short sub-paragraph563 (≤ 25 words) + one visual asset OR one CTA. More must be justified by the564 section's job. (Docs and book *content* pages are exempt — they're reading565 surfaces; this governs marketing/landing/home compositions.)566- **No data-dump sections.** A 20-row table or 30-item list on a marketing page567 is the wrong layout: top 3–5 highlights + "view full list" link, a marquee /568 scroll-snap row for breadth, or a separate page if the data is the product.569- **Long lists need a different component, not a longer list.** > 5 items:570 2-column grouped split, card grid, tabs/accordion, horizontal scroll-snap571 pills, or a marquee. A 10-row spec sheet with a hairline under every row is572 the worst default — group into 2–3 chunks with sparse dividers, or promote573 3–4 hero specs to display tiles and collapse the rest behind a disclosure.574- **COPY SELF-AUDIT (mandatory before ship).** Re-read every visible string575 (headlines, eyebrows, buttons, captions, alt text, footer). Flag and rewrite576 anything grammatically broken, with unclear referents, or that reads like an577 LLM trying to sound thoughtful (forced metaphors, mock-poetic micro-meta,578 fake-craftsman labels). If unsure whether a string makes sense, replace it579 with a plain functional sentence. AI-cute copy is worse than boring copy.580- **Fake-precise numbers are flagged.** `92%`, `4.1×`, `5.8 mm` either come581 from real data, are explicitly labeled as sample data, or are banned. Don't582 fake engineering precision the brand doesn't claim.583- **One copy register per page.** Don't mix technical-mono metadata, editorial584 prose, and marketing punch unless the brand voice explicitly calls for it.585- **Quotes & testimonials:** max 3 lines of quote body — a landing-page quote586 is a snippet, not the review. Attribution is name + role (+ company), never587 name alone. Real typographic quotes ("") or none — not straight ASCII.588589### 4.8 Image & visual asset strategy590591Landing pages and portfolios are **visual products**. Text-only pages with592fake-screenshot divs are slop.593594**Priority order for visual assets:**5955961. **Image-generation tool first.** If ANY image-gen tool is available in the597 environment, use it to create section-specific assets: hero photography,598 product shots, texture backgrounds — at the right aspect ratio per section.599 Save the results into the page bundle or `static/images/` so they're real600 files the build owns.6012. **Placeholder photography second.** No gen tool → 602 `https://picsum.photos/seed/{descriptive-seed}/{w}/{h}` (seed describes the603 section), or stock/brand URLs the brief provides. Treat hotlinks as604 *placeholders*: for the shipped site, download assets into the project so605 the static build is self-contained.6063. **Last resort: tell the user.** Leave clearly-labeled placeholder slots607 (`<!-- TODO: hero product photo, 1600x1200 -->`) and end with: *"This page608 needs real images at: [placements]. Please generate or provide them."* Do609 NOT fill the page with hand-rolled SVG illustrations or div-based fakes.610611**Plumbing (Hwaro):** run every real raster asset through612`resize_image(path=…, width=…)` — it returns `.url`, plus `.lqip` (blur-up613placeholder) and `.dominant_color` when LQIP is enabled — and build proper614`srcset`/`sizes` with lazy-loading. Don't ship one giant image. The built-in615processor resizes JPEG/PNG (BMP) only; for WebP/AVIF or aggressive optimization616run a build hook — `resize_image` passes unsupported formats through unresized.617618**Rules:**619620- **Even minimalist sites need real images.** A pure-text landing page is not621 minimalism, it's incomplete. Even a restrained editorial site needs 2–3 real622 images (hero, one product/lifestyle shot, one supporting). Blog/docs *content*623 pages are exempt; their home/landing compositions are not.624- **Div-based fake screenshots are banned.** No fake task lists, fake625 terminals, fake dashboards built from styled `<div>`s. Use a real screenshot,626 a generated image, or skip the preview.627- **Hero needs a real visual.** Text + gradient blob is a placeholder, not a628 hero. (Exception: genuine editorial-manifesto heroes where type IS the629 design.)630- **Real logos for social proof.** A "Trusted by" wall uses real SVG marks —631 vendor them into `static/` (e.g. from Simple Icons) rather than hotlinking a632 CDN; ensure they render in both schemes (single-color via `currentColor` or a633 token). For invented brands, draw a simple monogram mark — a plain text634 wordmark row looks generic. **Logo wall = logos only**: no industry/category635 captions under each logo.636637### 4.9 Icons & emoji638639- **Use an established icon set, one family per site.** Vendor the SVGs you640 need (Phosphor, Tabler, Radix, Lucide et al. all offer per-icon SVG641 downloads) into `static/icons/` or inline them as partials. Standardize642 `stroke-width` across the set. **Never hand-draw icon paths from scratch.**643- Hand-rolled *decorative* SVGs (custom illustrations, logos) are strongly644 discouraged as a default — acceptable only for a single simple geometric645 mark, or when the user explicitly asks.646- **Emoji are discouraged in markup and visible text** — the emoji-per-card647 feature row is a top AI tell. Use icon glyphs. *Override:* an explicitly648 playful/chat-style brief, sparingly, with intent.649650---651652## 5. Signature detail653654Give the design ONE small recurring mark — an accent rule (the scaffolds use a655short ember rule under the page title), a marker bullet, a consistent hover656behavior — rather than decorating everything. One motif, applied consistently,657reads as identity; five motifs read as noise.658659---660661## 6. Pattern vocabulary (names to design with)662663A vocabulary, not a library — know the names so you can propose and reason664about them. Implementation cost on a static site matters: prefer **pure CSS**,665then **small vanilla JS**, and treat **heavy JS** as a deliberate, justified666exception (there is no bundler unless you add one via build hooks).667668- **Hero paradigms:** asymmetric split hero · editorial manifesto hero (type as669 poster) · media-mask hero (type cut out over image/video) · kinetic-type670 hero · scroll-pinned hero (CSS `position: sticky`).671- **Layout & grids:** bento grid · masonry · split-screen scroll ·672 sticky-stack sections (sticky + scroll-driven scale/fade).673- **Cards & containers:** spotlight border card · glassmorphism panel674 (`backdrop-filter` + 1px inner border + inset highlight; solid fallback under675 `prefers-reduced-transparency`) · morphing modal (`<dialog>` + view676 transitions).677- **Scroll effects:** sticky scroll stack · zoom parallax · scroll progress678 bar (`animation-timeline: scroll()`) · reveal-on-enter679 (`animation-timeline: view()` or IntersectionObserver).680- **Typography & text:** kinetic marquee (CSS keyframes; max one per page) ·681 text mask reveal · circular text path (SVG `<textPath>`) · gradient stroke682 text.683- **Micro-interactions:** directional hover fill · ripple click · skeleton684 shimmer · animated SVG line drawing (`stroke-dasharray`).685686Heavy-JS patterns (scroll hijack, WebGL scenes, physics cursors) are usually687the wrong trade for a static site; if the brief truly demands one, isolate it688in one small script, lazy-load it, and degrade gracefully without it.689690---691692## 7. Motion for a static site693694No React, no animation framework — and none needed. The toolkit, in order:6956961. **CSS transitions** for all interactive states (`var(--transition)`,697 `cubic-bezier(0.16, 1, 0.3, 1)` for bigger moves).6982. **CSS keyframes + `animation-delay` cascades** for load-in staggers699 (`animation-delay: calc(var(--i) * 60ms)` with a per-item `--i`).7003. **CSS scroll-driven animations** (`animation-timeline: view()` /701 `scroll()`) for reveal and progress effects — always behind702 `@supports (animation-timeline: view())` so unsupported browsers get static703 content, not broken content.7044. **IntersectionObserver** (small vanilla script) as the portable reveal705 mechanism:706707```html708<script>709 const io = new IntersectionObserver((entries) => {710 for (const e of entries) if (e.isIntersecting) {711 e.target.classList.add("is-visible");712 io.unobserve(e.target);713 }714 }, { threshold: 0.3 });715 document.querySelectorAll("[data-reveal]").forEach((el) => io.observe(el));716</script>717```718719```css720[data-reveal] { opacity: 0; translate: 0 24px;721 transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),722 translate 0.6s cubic-bezier(0.16, 1, 0.3, 1); }723[data-reveal].is-visible { opacity: 1; translate: 0 0; }724@media (prefers-reduced-motion: reduce) {725 [data-reveal] { opacity: 1; translate: none; transition: none; }726}727```728729**Hard rules:**730731- **MOTION MUST BE MOTIVATED.** Before adding any animation ask: what does it732 communicate? Valid: hierarchy, storytelling sequence, feedback, state733 transition. Invalid: "it looked cool." If you can't articulate the reason in734 one sentence, drop it. Not every card needs an infinite loop — informational735 sections stay still.736- **"Motion claimed, motion shown."** If `MOTION_INTENSITY > 4`, the page737 actually moves: hero entry, key-section reveals, CTA hover physics at738 minimum. Can't ship working motion in scope? Drop the dial to 3 and ship a739 clean static page. Never half-build motion that breaks.740- **MARQUEE MAX-ONE-PER-PAGE.** Two marquees on one page is lazy filler.741- **`window.addEventListener("scroll", …)` is banned.** Use scroll-driven CSS,742 IntersectionObserver, or nothing. Same ban for rAF loops recomputing layout743 from `scrollY`.744- **Animate ONLY `transform`/`translate`/`scale` and `opacity`.** Never `top`,745 `left`, `width`, `height`. `will-change` sparingly.746- **Reduced motion is non-negotiable.** Anything above `MOTION_INTENSITY 3`747 honors `prefers-reduced-motion: reduce`: infinite loops, parallax, and748 reveals collapse to static/instant.749- **Grain/noise overlays** live exclusively on a fixed `pointer-events: none`750 element — never on scrolling containers (continuous repaints destroy mobile751 FPS).752- **z-index restraint.** A documented scale for systemic la753754…(truncated)