Setup
Ask the user first: "Would you like a single HTML page or a full repository with libraries and frameworks?"
| Choice |
Deliverable |
| Single HTML |
One self-contained .html file. Inline <style>, one IIFE <script>, IntersectionObserver for scroll reveals, Google Fonts + display=swap. No framework. |
| Full repo |
Astro 6 + Tailwind CSS v4 + anime.js + self-hosted Fontsource. Use the structure and patterns below. |
Quick Reference — The Non-Negotiables
Comfort is warm, unhurried editorial minimalism. Fine print magazine meets a sunlit room.
| Domain |
Rule |
| Palette |
Warm paper + warm ink. One earthy accent hue (5°–55° coral/terracotta/amber/ochre; or alternate dusty rose 340°–360° / olive 95°–150°). No pure #000 / #fff, no neon, no cold tech blues. |
| Typography |
Strict two-font system: serif display for hero headlines + numerals + pull-quotes + footer wordmark; humanist sans for body/labels/UI. Serif never for running body; sans never for hero/display headline. |
| Headlines |
Hero h1: full serif. Section h2: bold sans with one short serif-italic accent phrase in accent color. |
| Theme |
`data-theme="light |
| Motion |
Ease-out everything. No bounce, no elastic, no marquees. Hero animates on load (autoplay: true); every other section reveals on scroll. Full prefers-reduced-motion kill switch. |
| Layout |
Generous whitespace, soft rounded corners (12–28px), gentle shadows, subtle paper-grain noise overlay on body::before. |
| Scrollbar |
Style html, never body. |
| Images |
CSS/SVG mocks and template placeholders only. Zero binary assets shipped. |
Core Conviction
Comfort is not a template. It is a set of tendencies: warm, tactile, calm, generous, quietly confident, hand-made. Every detail is deliberate — the paper grain, the single italic accent, the breathing room, the soft shadow. A page should feel like a warm room you don't want to leave.
Two hard rules:
- Do not reproduce exact copy from any reference. Invent brand names, taglines, and body text that match the voice.
- Do not reproduce exact layout. Vary section selection, order, and count every run.
Vary vs. Freeze
| Always vary |
Never break |
| Serif + sans pairing from approved lists |
Warm paper-and-ink foundation |
| Accent hue, saturation, lightness |
Exactly one accent hue |
| Brand name, copy, vocabulary |
data-theme + no-flash script + circular theme reveal |
| Section selection/order (4–10, no repeats) |
Two-font system + headline strategy |
| Radius, shadow softness, motif |
Hero uses autoplay: true; others use onScroll |
| Durations/easings within ranges |
Ease-out, no-bounce motion character |
| Optional flourishes (0–2 of: sticky stack, ambient drift, custom cursor) |
Named-peer checkbox menus; scrollbar on html |
| Deliverable format (single HTML or Astro repo) |
Full reduced-motion opt-out |
Voice
Warm, plainspoken, quietly confident. Like a maker hosting you in their workshop.
- Lead with the concrete thing, not an abstraction.
- No superlatives or hype. Avoid: "revolutionary", "world-class", "seamless", "game-changing", "cutting-edge", "curated", "bespoke", "artisan".
- Keep paragraphs short (1–3 sentences). Vary sentence length.
- Section transitions are gentle: eyebrow label sets context, headline does the work.
- Names should feel hand-lettered and earthy: "Hearth", "Mill Lane", "Slow Sunday", "Ember & Oat", "Northlight".
- Vocabulary palette (pick a handful per run): small-batch, hand, warm, slow, honest, grain, linen, hearth, kettle, table, season, gather, roast/bake/steep, care, home, light, morning, ritual.
Mood Snapshot
A typical Comfort hero for a small roastery:
Eyebrow pill: "SMALL-BATCH • ROASTED TO ORDER"
Headline: "Coffee worth slowing down for" — full serif, italic accent word in terracotta
Subhead: two short sentences about sourcing and roasting.
CTAs: two rounded pills — primary filled (accent), secondary outlined.
Visual: large template placeholder below or beside, captioned quietly.
Scroll cue: a small, gently bobbing chevron fading to 0.3 opacity.
This is the rhythm: mostly air, one warm accent, serif authority, sans clarity.
Visual System
Color
Two-mode system: warm paper default and warm lamplit dark. Both are warm — dark mode is a dim room, never cold black.
:root {
--bg: /* warm cream, e.g. hsl(44 38% 94%) */;
--bg-surface: /* slightly lifted from --bg */;
--text: /* warm near-black, e.g. hsl(28 18% 16%) */;
--text-dim: /* ~70% toward --bg */;
--muted: /* low-contrast warm grey-brown for hairlines */;
--accent: /* warm/earthy, dynamic per run */;
--accent-dim: /* desaturated + darkened */;
--accent-soft: /* lifted/lightened for hovers & dark panels */;
}
:root[data-theme="dark"] {
--bg: /* warm near-black, e.g. hsl(28 12% 10%) */;
--bg-surface: /* lamplit lift */;
--text: /* warm off-white, e.g. hsl(40 30% 88%) */;
/* re-derive dim/muted/accent variants */
}
For Tailwind v4, register frozen --brand-* tokens and forward them through @theme inline so bg-paper, text-accent, etc. resolve to living custom properties. Every @theme inline value must be var(--brand-*) — never raw hex/HSL/RGB.
Accent rules:
- Hue 5°–55° (coral, terracotta, clay, rust, amber, ochre, caramel). Alternates: dusty rose 340°–360° or muted olive 95°–150°.
- Saturation 45%–85%; lightness 40%–60%.
- Never reuse the same hue across runs.
--accent-soft must be re-derived for dark mode (lighter + more saturated for legibility).
- One accent hue only. A couple of muted support earth tones are allowed as neutrals.
Surfaces:
--bg-surface is slightly lifted from --bg for alternating sections, cards, drawers.
- Subtle warm radial gradients or faint paper-grain/noise overlay (
body::before, opacity 0.015–0.025) are allowed.
- Never harsh pure-color blocks.
Refuse: pure black/white surfaces; neon/electric accents; cold blue-grey palettes; hard 90° corners; harsh 1px-black borders; heavy drop shadows; glassmorphism; busy gradients.
Typography
Serif display (pick one): Fraunces, Lora, Newsreader, Spectral, Cormorant, EB Garamond, Playfair Display, Source Serif, Junicode.
Sans/grotesque body (pick one): Bricolage Grotesque, Hanken Grotesk, Work Sans, Inter, Mona Sans, Schibsted Grotesk, Figtree, Geist Sans.
Headline strategy:
- Hero
h1: full serif, weight 500–600, letter-spacing -0.01em, line-height ~0.9–1.05. Optional italic accent phrase inside.
- Section
h2: bold sans + one short serif-italic accent phrase in accent color.
- Italic accent phrase: one word, occasionally two, never a whole clause, sometimes zero. Never more than one per headline.
- Serif also carries: large numerals, pull-quote marks, stat figures, oversized footer wordmark.
Sans rhythm:
| Element |
Size |
Weight |
Spacing |
Case |
| Eyebrow |
0.75–0.875rem |
600–700 |
0.1–0.2em |
UPPERCASE |
| Body |
1–1.125rem |
400–500 |
0 |
— |
| Lead/subhead |
1.125–1.375rem |
400 |
0 |
— |
| Nav / UI |
0.9–1rem |
500 |
0 |
— |
| Meta / caption |
0.8rem |
500 |
0.02em |
— |
Rules:
- Never a third font; never serif body; never sans hero headline.
- Load only weights used — about six variants total (e.g., sans 400/500/600 + serif 400-italic/500/600).
- Self-host via Fontsource (framework) or
preconnect + display=swap (single file).
Composition
- Hero: generous top padding, left- or center-aligned. Eyebrow pill, large serif headline, short subhead, 1–2 CTAs, one visual. Mostly air. Scroll cue optional.
- Separators: generous vertical whitespace; occasional 1px low-opacity warm hairline. Avoid loud dividers.
- Spacing: use clamp-based CSS variables (
--py-section, --px, --gap). Roomy, not tight.
- Cards: soft rounded cards on
--bg-surface, hairline or no border, gentle shadow. One card style per page. Dashed low-opacity warm borders are on-brand for newsletter/CTA panels.
- Corners: one radius scale per run (~12–24px;
rounded-xl/2xl/3xl + rounded-full for pills).
- Shadows: soft, low, with a faint warm tint.
Motion
Motion is soft and ambient — it warms, it never performs.
Reduced motion (mandatory)
A three-level kill switch:
- JS guard at the top of every scoped
<script>:if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelectorAll('#section-id .reveal-item').forEach(el => {
el.style.opacity = '';
el.style.transform = '';
el.style.filter = '';
});
return;
}
- CSS fallback in
global.css:@media (prefers-reduced-motion: reduce) {
.reveal-item { opacity: 1 !important; transform: none !important; filter: none !important; transition: none !important; }
}
- CSS
@keyframes must be wrapped in @media not (prefers-reduced-motion: reduce) or overridden to animation: none under reduced motion.
For framework builds, centralize this in src/utils/reveal.ts as revealSection(containerId, options?).
Signature 1: Hero cinematic entrance
The hero is above the fold — it must not use revealSection() / onScroll().
- Set initial hidden state via CSS classes scoped to the hero inside
@media not (prefers-reduced-motion: reduce).
- Text lines: stagger 120–220ms, duration 600–1000ms, ease-out (e.g.
'outExpo'), from opacity: 0, translateY: 20–28px, filter: blur(4–6px).
- Hero visual: delay ~400–600ms, duration 800–1000ms, ease-out, from
opacity: 0, translateY: 16–24px, blur(4–6px), scale: 0.94.
- Scroll cue: delay ~1200–1600ms, fade to
opacity: 0.3.
- After entrance, fade cue with scroll via
onScroll({ target: heroSection, sync: true }) from 0.3 → 0.
Signature 2: Blur-in scroll reveal
Every non-hero section uses scroll-triggered reveals.
- Targets:
.reveal-item inside each section.
- From:
opacity: 0.001, translateY: 14–24px, filter: blur(4–6px).
- To: resting state.
- Duration 300–600ms, ease-out (
'outExpo', 'outQuad', 'out(3)').
- Siblings stagger 90–160ms.
- Trigger:
onScroll({ target: sectionEl, enter: 'bottom', leave: 'top' }).
- In single-file builds, use
IntersectionObserver.
Signature 3: Themed circular reveal
On theme toggle, wash the new mode across the page in a circle from the toggle/pointer.
- Use View Transitions API: animate
::view-transition-new(root) clip-path from circle(0 at Xpx Ypx) to a radius covering the farthest corner.
- Add accent ring burst and brief icon spin/scale.
- Fallback: instant swap where
startViewTransition is unavailable.
- Disabled under
prefers-reduced-motion: reduce.
Optional flourishes (choose 0–2)
- Sticky media stack: pinned visuals that crossfade as feature list items are selected.
- Ambient drift: very slow warm gradient or grain breathing.
- Soft custom cursor: small accent dot with inertia, pointer-fine devices only.
Motion rules
- Ease-out for entrances; never ease-in-out, never bounce, never elastic.
- Hovers: scale ≤ 1.03, soft shadow lift, underline grow; 120–250ms.
- Respect
prefers-reduced-motion, prefers-contrast: more, and pointer: coarse.
Interaction & Navigation
Top bar
- Serif or eyebrow-sans wordmark left; sans nav links center/right; theme toggle + primary CTA right.
- Scroll-aware frosted glass when hero scrolls out (
backdrop-filter: blur(8px) + lifted paper background).
- Active nav link: smooth underline + opacity tween; never snap.
Menus
- Drawer (small screens): checkbox
peer/drawer toggle, paper panel, staggered serif links, Escape/backdrop/link close, body scroll lock via JS enhancement only.
- Rail (wide screens, optional): slim persistent vertical rail via
peer/hd, expanding on hover, pin-opened by an HD-only Menu trigger.
- Independent named peers are mandatory. A generic
peer can cross-trigger drawer and rail.
Custom scrollbar
- Style on
html — never body, never a utility class on body.
- Firefox:
html { scrollbar-width: thin; scrollbar-color: var(--brand-accent-dim) var(--brand-paper); }
- WebKit:
html::-webkit-scrollbar { width: 8px; } with track --brand-paper, thumb --brand-accent-dim (hover → --brand-accent).
- Use a
scrollbar-slim class for inner scroll areas applied to the scrolling element itself.
Forms
- Prefer direct
mailto: links with accent underline that grows on hover.
- One newsletter field allowed: soft rounded input, real
<label>, one field + button.
Focus & accessibility
:focus-visible outline: 2px solid --accent (or --text under high-contrast), 2px offset.
- All interactive controls keyboard reachable; custom toggles use real
<button>/<input>.
- Decorative elements (motifs, mocks, rings) marked
aria-hidden="true".
- Touch targets ≥ 44×44px.
Layout Sections (Approved Roster)
Use 4–10 of these in any order. Never repeat a type; never reuse a prior run's order or count.
- Hero — eyebrow + serif headline (one italic accent) + subhead + CTAs + visual.
- Press / Logos strip — quiet "as featured in" serif wordmarks at low opacity.
- Manifesto / About — short editorial statement; optional value blocks or stat row.
- Feature / Craft stack — pinned media or alternating layout; 3–5 features; collapses to tabs + visual on mobile.
- Process / Steps — numbered steps with serif numerals.
- Collection / Grid — products, works, places as soft cards or editorial grid.
- Tiers / Offerings — pricing/membership; one tier softly emphasized (filled, not loud).
- Voices / Testimonials — pull-quotes with attribution; serif quotation mark accent.
- Notes / Journal — preview cards linking to writing or guides.
- Visit / Contact — address, hours, map placeholder,
mailto CTA.
- Newsletter — single-field signup band on
--bg-surface.
- Footer — link columns + oversized serif wordmark bleeding along bottom; quiet motif; copyright + back-to-top.
Choose one decorative motif per run (concentric arcs / soft rays / paper grain / blob field / hairline grid) and use it sparingly and consistently.
Build & Technical Signatures
Canonical framework stack
- Astro 6 with static output,
.astro components, per-component <script> and scoped <style>.
- Tailwind CSS v4 (CSS-first) via
@tailwindcss/vite.
- anime.js v4 for scroll reveals and micro-interactions.
- @fontsource/ self-hosted fonts; only weights used.
- Path alias
~/* → src/*.
Tailwind v4 essentials
@import 'tailwindcss';
@theme inline {
--color-paper: var(--brand-paper);
--color-accent: var(--brand-accent);
--color-accent-dim: var(--brand-accent-dim);
--color-accent-soft: var(--brand-accent-soft);
--color-dark: var(--brand-dark);
--color-subtle: var(--brand-subtle);
--color-muted: var(--brand-muted);
--font-serif-display: var(--font-serif);
--font-size-h1: var(--h1);
--font-size-h2: var(--h2);
--spacing-section: var(--py-section);
--spacing-gutter: var(--px);
--spacing-gap: var(--gap);
}
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
@utility container { /* page measure */ }
Register a --breakpoint-hd (~1600px) for wide-screen rail behavior.
anime.js v4 critical syntax
animate(targets, params) — not ({ targets, ...params }).
ease not easing; bare names: 'outExpo', 'outQuad', 'out(3)', 'inOutCirc'.
- Never use string-format
'cubicBezier(...)' or 'easeOutExpo'.
onScroll({ target: sectionEl, enter: 'bottom', leave: 'top' }) — target must be a DOM element, not a selector string.
- Use
utils.$(selector) and utils.set(targets, props).
stagger() is a top-level import.
Theme system
- Single source of truth:
data-theme="light|dark" on <html>.
- Do not toggle a
.dark class on <html>.
- Render-blocking inline no-flash script in
<head> reads localStorage / prefers-color-scheme before first paint.
- Toggle:
document.documentElement.setAttribute('data-theme', value) + localStorage.setItem('theme', value).
Project structure
<project>/
├─ astro.config.mjs
├─ tsconfig.json
├─ package.json
├─ public/favicon.svg
└─ src/
├─ styles/global.css
├─ layouts/Layout.astro
├─ utils/merge.ts
├─ utils/reveal.ts
├─ icons/*.astro
├─ components/
│ ├─ NavBar.astro
│ ├─ Sidebar.astro
│ ├─ SidebarHD.astro
│ ├─ ThemeSwitch.astro
│ ├─ Footer.astro
│ ├─ Logo.astro
│ ├─ Button.astro
│ ├─ Title.astro
│ ├─ Description.astro
│ ├─ Placeholder.astro
│ ├─ <Motif>.astro
│ ├─ <Mock>.astro
│ └─ <Section>.astro ...
└─ pages/
├─ index.astro
└─ 404.astro
Single-file fallback
- Inline
<style> + one IIFE <script>.
- Same tokens, same motion principles,
IntersectionObserver for scroll reveals.
- Target payload: under ~80KB uncompressed (excluding fonts).
HTML Head & Meta
Every page needs:
charset, responsive viewport.
<title>: "<Brand> — <short warm descriptor>".
<meta name="description">: one or two warm concrete sentences, ≤155 chars.
<meta name="theme-color"> matching accent or --bg (with dark media variant).
<meta name="color-scheme" content="light dark">.
- Open Graph (
og:title, og:description, og:type) + twitter:card.
- Favicon: simple warm SVG mark generated per run.
Accessibility Checklist
Ship only when all are true:
- Semantic landmarks (
header/nav, main, contentinfo).
- Correct
aria-labelledby linking section headings to regions.
- Strict heading hierarchy: one
h1, then h2 → h3.
type on every <button>.
aria-label on icon-only controls.
- Touch targets ≥ 44×44px.
aria-expanded / aria-hidden accurate on menus/drawers/dropdowns.
- Decorative elements
aria-hidden="true".
prefers-reduced-motion: reduce fully disables motion.
prefers-contrast: more hardens text contrast and outlines.
pointer: coarse disables hover-only affordances and custom cursor.
Common Failure Modes — Do Not Regress
| Bug |
Prevention |
Scrollbar styles on body or a utility class |
Always target html |
Hero uses onScroll / revealSection() |
Hero uses autoplay: true only |
Generic peer checkbox |
Use peer/drawer and peer/hd independently |
Hardcoded colors in @theme inline |
Every value must be var(--brand-*) |
anime.js v3 syntax (easing, string targets, cubicBezier(...)) |
Use v4 API strictly |
Forgetting reduced motion on CSS @keyframes |
Wrap or override in @media (prefers-reduced-motion: reduce) |
.dark class toggle |
Use data-theme attribute only |
Unused CSS tokens / @keyframes |
Audit global.css before shipping; delete zero-reference tokens |
| More than one accent hue |
One accent only; support earth tones are neutrals |
| Third font or serif body |
Two-font system is absolute |
| Pure black/white surfaces |
Warm near-black and cream only |
| Reused section order or copy |
Vary every run |
| Binary image assets |
Use CSS/SVG mocks and template placeholders |
Performance & Privacy
- Zero analytics, trackers, or third-party calls beyond font host.
- Self-host fonts in framework builds; use
preconnect + display=swap in single-file.
- About six font variants total; system-font fallbacks required.
- Framework builds: pin a single
vite version via overrides so @tailwindcss/vite and Astro share one Vite instance.
- Clean token surface: every custom property and keyframe must be consumed somewhere; delete unused ones.
1---2name: comfort-skill3description: A warm, tactile, unhurried design language for artisanal brands, cafés, roasteries, makers, wellness and hospitality studios, and editorial single-page sites. Paper surfaces, a serif voice with italic accents over humanist-sans structure, soft ambient motion, and hand-made warmth.4---56# Setup78**Ask the user first:** *"Would you like a single HTML page or a full repository with libraries and frameworks?"*910| Choice | Deliverable |11|--------|-------------|12| **Single HTML** | One self-contained `.html` file. Inline `<style>`, one IIFE `<script>`, `IntersectionObserver` for scroll reveals, Google Fonts + `display=swap`. No framework. |13| **Full repo** | Astro 6 + Tailwind CSS v4 + anime.js + self-hosted Fontsource. Use the structure and patterns below. |1415# Quick Reference — The Non-Negotiables1617Comfort is **warm, unhurried editorial minimalism**. Fine print magazine meets a sunlit room.1819| Domain | Rule |20|--------|------|21| **Palette** | Warm paper + warm ink. One earthy accent hue (5°–55° coral/terracotta/amber/ochre; or alternate dusty rose 340°–360° / olive 95°–150°). No pure `#000` / `#fff`, no neon, no cold tech blues. |22| **Typography** | Strict two-font system: **serif display** for hero headlines + numerals + pull-quotes + footer wordmark; **humanist sans** for body/labels/UI. Serif never for running body; sans never for hero/display headline. |23| **Headlines** | Hero `h1`: full serif. Section `h2`: bold sans with **one short serif-italic accent phrase** in accent color. |24| **Theme** | `data-theme="light|dark"` on `<html>`. Render-blocking no-flash init script reads `localStorage` / `prefers-color-scheme`. Toggle uses View Transitions circular `clip-path` reveal from pointer. |25| **Motion** | Ease-out everything. No bounce, no elastic, no marquees. Hero animates **on load** (`autoplay: true`); every other section reveals **on scroll**. Full `prefers-reduced-motion` kill switch. |26| **Layout** | Generous whitespace, soft rounded corners (12–28px), gentle shadows, subtle paper-grain noise overlay on `body::before`. |27| **Scrollbar** | Style `html`, never `body`. |28| **Images** | CSS/SVG mocks and template placeholders only. Zero binary assets shipped. |2930# Core Conviction3132Comfort is not a template. It is a set of tendencies: warm, tactile, calm, generous, quietly confident, hand-made. Every detail is deliberate — the paper grain, the single italic accent, the breathing room, the soft shadow. A page should feel like a warm room you don't want to leave.3334**Two hard rules:**35- Do not reproduce exact copy from any reference. Invent brand names, taglines, and body text that match the voice.36- Do not reproduce exact layout. Vary section selection, order, and count every run.3738# Vary vs. Freeze3940| Always vary | Never break |41|-------------|-------------|42| Serif + sans pairing from approved lists | Warm paper-and-ink foundation |43| Accent hue, saturation, lightness | Exactly one accent hue |44| Brand name, copy, vocabulary | `data-theme` + no-flash script + circular theme reveal |45| Section selection/order (4–10, no repeats) | Two-font system + headline strategy |46| Radius, shadow softness, motif | Hero uses `autoplay: true`; others use `onScroll` |47| Durations/easings within ranges | Ease-out, no-bounce motion character |48| Optional flourishes (0–2 of: sticky stack, ambient drift, custom cursor) | Named-peer checkbox menus; scrollbar on `html` |49| Deliverable format (single HTML or Astro repo) | Full reduced-motion opt-out |5051# Voice5253Warm, plainspoken, quietly confident. Like a maker hosting you in their workshop.5455- Lead with the concrete thing, not an abstraction.56- No superlatives or hype. Avoid: "revolutionary", "world-class", "seamless", "game-changing", "cutting-edge", "curated", "bespoke", "artisan".57- Keep paragraphs short (1–3 sentences). Vary sentence length.58- Section transitions are gentle: eyebrow label sets context, headline does the work.59- Names should feel hand-lettered and earthy: "Hearth", "Mill Lane", "Slow Sunday", "Ember & Oat", "Northlight".60- Vocabulary palette (pick a handful per run): *small-batch, hand, warm, slow, honest, grain, linen, hearth, kettle, table, season, gather, roast/bake/steep, care, home, light, morning, ritual*.6162# Mood Snapshot6364A typical Comfort hero for a small roastery:6566> Eyebrow pill: **"SMALL-BATCH • ROASTED TO ORDER"** 67> Headline: **"Coffee worth *slowing down* for"** — full serif, italic accent word in terracotta 68> Subhead: two short sentences about sourcing and roasting. 69> CTAs: two rounded pills — primary filled (accent), secondary outlined. 70> Visual: large template placeholder below or beside, captioned quietly. 71> Scroll cue: a small, gently bobbing chevron fading to `0.3` opacity.7273This is the rhythm: mostly air, one warm accent, serif authority, sans clarity.7475# Visual System7677## Color7879Two-mode system: warm **paper** default and warm **lamplit** dark. Both are warm — dark mode is a dim room, never cold black.8081```css82:root {83 --bg: /* warm cream, e.g. hsl(44 38% 94%) */;84 --bg-surface: /* slightly lifted from --bg */;85 --text: /* warm near-black, e.g. hsl(28 18% 16%) */;86 --text-dim: /* ~70% toward --bg */;87 --muted: /* low-contrast warm grey-brown for hairlines */;88 --accent: /* warm/earthy, dynamic per run */;89 --accent-dim: /* desaturated + darkened */;90 --accent-soft: /* lifted/lightened for hovers & dark panels */;91}92:root[data-theme="dark"] {93 --bg: /* warm near-black, e.g. hsl(28 12% 10%) */;94 --bg-surface: /* lamplit lift */;95 --text: /* warm off-white, e.g. hsl(40 30% 88%) */;96 /* re-derive dim/muted/accent variants */97}98```99100For Tailwind v4, register frozen `--brand-*` tokens and forward them through `@theme inline` so `bg-paper`, `text-accent`, etc. resolve to living custom properties. **Every `@theme inline` value must be `var(--brand-*)` — never raw hex/HSL/RGB.**101102**Accent rules:**103- Hue 5°–55° (coral, terracotta, clay, rust, amber, ochre, caramel). Alternates: dusty rose 340°–360° or muted olive 95°–150°.104- Saturation 45%–85%; lightness 40%–60%.105- Never reuse the same hue across runs.106- `--accent-soft` must be re-derived for dark mode (lighter + more saturated for legibility).107- One accent hue only. A couple of muted support earth tones are allowed as neutrals.108109**Surfaces:**110- `--bg-surface` is slightly lifted from `--bg` for alternating sections, cards, drawers.111- Subtle warm radial gradients or faint paper-grain/noise overlay (`body::before`, opacity 0.015–0.025) are allowed.112- Never harsh pure-color blocks.113114**Refuse:** pure black/white surfaces; neon/electric accents; cold blue-grey palettes; hard 90° corners; harsh 1px-black borders; heavy drop shadows; glassmorphism; busy gradients.115116## Typography117118**Serif display (pick one):** Fraunces, Lora, Newsreader, Spectral, Cormorant, EB Garamond, Playfair Display, Source Serif, Junicode.119120**Sans/grotesque body (pick one):** Bricolage Grotesque, Hanken Grotesk, Work Sans, Inter, Mona Sans, Schibsted Grotesk, Figtree, Geist Sans.121122**Headline strategy:**123- **Hero `h1`:** full serif, weight 500–600, letter-spacing `-0.01em`, line-height ~0.9–1.05. Optional italic accent phrase inside.124- **Section `h2`:** bold sans + one short serif-italic accent phrase in accent color.125- **Italic accent phrase:** one word, occasionally two, never a whole clause, sometimes zero. Never more than one per headline.126- Serif also carries: large numerals, pull-quote marks, stat figures, oversized footer wordmark.127128**Sans rhythm:**129130| Element | Size | Weight | Spacing | Case |131|---------|------|--------|---------|------|132| Eyebrow | 0.75–0.875rem | 600–700 | 0.1–0.2em | UPPERCASE |133| Body | 1–1.125rem | 400–500 | 0 | — |134| Lead/subhead | 1.125–1.375rem | 400 | 0 | — |135| Nav / UI | 0.9–1rem | 500 | 0 | — |136| Meta / caption | 0.8rem | 500 | 0.02em | — |137138**Rules:**139- Never a third font; never serif body; never sans hero headline.140- Load only weights used — about six variants total (e.g., sans 400/500/600 + serif 400-italic/500/600).141- Self-host via Fontsource (framework) or `preconnect` + `display=swap` (single file).142143## Composition144145- **Hero:** generous top padding, left- or center-aligned. Eyebrow pill, large serif headline, short subhead, 1–2 CTAs, one visual. Mostly air. Scroll cue optional.146- **Separators:** generous vertical whitespace; occasional 1px low-opacity warm hairline. Avoid loud dividers.147- **Spacing:** use clamp-based CSS variables (`--py-section`, `--px`, `--gap`). Roomy, not tight.148- **Cards:** soft rounded cards on `--bg-surface`, hairline or no border, gentle shadow. One card style per page. Dashed low-opacity warm borders are on-brand for newsletter/CTA panels.149- **Corners:** one radius scale per run (~12–24px; `rounded-xl/2xl/3xl` + `rounded-full` for pills).150- **Shadows:** soft, low, with a faint warm tint.151152# Motion153154Motion is soft and ambient — it warms, it never performs.155156## Reduced motion (mandatory)157158A three-level kill switch:1591601. **JS guard** at the top of every scoped `<script>`:161 ```js162 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {163 document.querySelectorAll('#section-id .reveal-item').forEach(el => {164 el.style.opacity = '';165 el.style.transform = '';166 el.style.filter = '';167 });168 return;169 }170 ```1712. **CSS fallback** in `global.css`:172 ```css173 @media (prefers-reduced-motion: reduce) {174 .reveal-item { opacity: 1 !important; transform: none !important; filter: none !important; transition: none !important; }175 }176 ```1773. **CSS `@keyframes`** must be wrapped in `@media not (prefers-reduced-motion: reduce)` or overridden to `animation: none` under reduced motion.178179For framework builds, centralize this in `src/utils/reveal.ts` as `revealSection(containerId, options?)`.180181## Signature 1: Hero cinematic entrance182183The hero is above the fold — it must **not** use `revealSection()` / `onScroll()`.184185- Set initial hidden state via CSS classes scoped to the hero inside `@media not (prefers-reduced-motion: reduce)`.186- Text lines: stagger 120–220ms, duration 600–1000ms, ease-out (e.g. `'outExpo'`), from `opacity: 0`, `translateY: 20–28px`, `filter: blur(4–6px)`.187- Hero visual: delay ~400–600ms, duration 800–1000ms, ease-out, from `opacity: 0`, `translateY: 16–24px`, `blur(4–6px)`, `scale: 0.94`.188- Scroll cue: delay ~1200–1600ms, fade to `opacity: 0.3`.189- After entrance, fade cue with scroll via `onScroll({ target: heroSection, sync: true })` from `0.3 → 0`.190191## Signature 2: Blur-in scroll reveal192193Every non-hero section uses scroll-triggered reveals.194195- Targets: `.reveal-item` inside each section.196- From: `opacity: 0.001`, `translateY: 14–24px`, `filter: blur(4–6px)`.197- To: resting state.198- Duration 300–600ms, ease-out (`'outExpo'`, `'outQuad'`, `'out(3)'`).199- Siblings stagger 90–160ms.200- Trigger: `onScroll({ target: sectionEl, enter: 'bottom', leave: 'top' })`.201- In single-file builds, use `IntersectionObserver`.202203## Signature 3: Themed circular reveal204205On theme toggle, wash the new mode across the page in a circle from the toggle/pointer.206207- Use View Transitions API: animate `::view-transition-new(root)` `clip-path` from `circle(0 at Xpx Ypx)` to a radius covering the farthest corner.208- Add accent ring burst and brief icon spin/scale.209- Fallback: instant swap where `startViewTransition` is unavailable.210- Disabled under `prefers-reduced-motion: reduce`.211212## Optional flourishes (choose 0–2)213214- **Sticky media stack:** pinned visuals that crossfade as feature list items are selected.215- **Ambient drift:** very slow warm gradient or grain breathing.216- **Soft custom cursor:** small accent dot with inertia, pointer-fine devices only.217218## Motion rules219220- Ease-out for entrances; never ease-in-out, never bounce, never elastic.221- Hovers: scale ≤ 1.03, soft shadow lift, underline grow; 120–250ms.222- Respect `prefers-reduced-motion`, `prefers-contrast: more`, and `pointer: coarse`.223224# Interaction & Navigation225226## Top bar227- Serif or eyebrow-sans wordmark left; sans nav links center/right; theme toggle + primary CTA right.228- Scroll-aware frosted glass when hero scrolls out (`backdrop-filter: blur(8px)` + lifted paper background).229- Active nav link: smooth underline + opacity tween; never snap.230231## Menus232- **Drawer (small screens):** checkbox `peer/drawer` toggle, paper panel, staggered serif links, Escape/backdrop/link close, body scroll lock via JS enhancement only.233- **Rail (wide screens, optional):** slim persistent vertical rail via `peer/hd`, expanding on hover, pin-opened by an HD-only Menu trigger.234- **Independent named peers are mandatory.** A generic `peer` can cross-trigger drawer and rail.235236## Custom scrollbar237- Style on `html` — never `body`, never a utility class on `body`.238- Firefox: `html { scrollbar-width: thin; scrollbar-color: var(--brand-accent-dim) var(--brand-paper); }`239- WebKit: `html::-webkit-scrollbar { width: 8px; }` with track `--brand-paper`, thumb `--brand-accent-dim` (hover → `--brand-accent`).240- Use a `scrollbar-slim` class for inner scroll areas applied to the scrolling element itself.241242## Forms243- Prefer direct `mailto:` links with accent underline that grows on hover.244- One newsletter field allowed: soft rounded input, real `<label>`, one field + button.245246## Focus & accessibility247- `:focus-visible` outline: 2px solid `--accent` (or `--text` under high-contrast), 2px offset.248- All interactive controls keyboard reachable; custom toggles use real `<button>`/`<input>`.249- Decorative elements (motifs, mocks, rings) marked `aria-hidden="true"`.250- Touch targets ≥ 44×44px.251252# Layout Sections (Approved Roster)253254Use **4–10** of these in any order. Never repeat a type; never reuse a prior run's order or count.255256- **Hero** — eyebrow + serif headline (one italic accent) + subhead + CTAs + visual.257- **Press / Logos strip** — quiet "as featured in" serif wordmarks at low opacity.258- **Manifesto / About** — short editorial statement; optional value blocks or stat row.259- **Feature / Craft stack** — pinned media or alternating layout; 3–5 features; collapses to tabs + visual on mobile.260- **Process / Steps** — numbered steps with serif numerals.261- **Collection / Grid** — products, works, places as soft cards or editorial grid.262- **Tiers / Offerings** — pricing/membership; one tier softly emphasized (filled, not loud).263- **Voices / Testimonials** — pull-quotes with attribution; serif quotation mark accent.264- **Notes / Journal** — preview cards linking to writing or guides.265- **Visit / Contact** — address, hours, map placeholder, `mailto` CTA.266- **Newsletter** — single-field signup band on `--bg-surface`.267- **Footer** — link columns + oversized serif wordmark bleeding along bottom; quiet motif; copyright + back-to-top.268269Choose one decorative motif per run (concentric arcs / soft rays / paper grain / blob field / hairline grid) and use it sparingly and consistently.270271# Build & Technical Signatures272273## Canonical framework stack274- **Astro 6** with static output, `.astro` components, per-component `<script>` and scoped `<style>`.275- **Tailwind CSS v4 (CSS-first)** via `@tailwindcss/vite`.276- **anime.js v4** for scroll reveals and micro-interactions.277- **@fontsource/** self-hosted fonts; only weights used.278- Path alias `~/* → src/*`.279280### Tailwind v4 essentials281```css282@import 'tailwindcss';283@theme inline {284 --color-paper: var(--brand-paper);285 --color-accent: var(--brand-accent);286 --color-accent-dim: var(--brand-accent-dim);287 --color-accent-soft: var(--brand-accent-soft);288 --color-dark: var(--brand-dark);289 --color-subtle: var(--brand-subtle);290 --color-muted: var(--brand-muted);291 --font-serif-display: var(--font-serif);292 --font-size-h1: var(--h1);293 --font-size-h2: var(--h2);294 --spacing-section: var(--py-section);295 --spacing-gutter: var(--px);296 --spacing-gap: var(--gap);297}298@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));299@utility container { /* page measure */ }300```301302Register a `--breakpoint-hd` (~1600px) for wide-screen rail behavior.303304### anime.js v4 critical syntax305- `animate(targets, params)` — not `({ targets, ...params })`.306- `ease` not `easing`; bare names: `'outExpo'`, `'outQuad'`, `'out(3)'`, `'inOutCirc'`.307- **Never** use string-format `'cubicBezier(...)'` or `'easeOutExpo'`.308- `onScroll({ target: sectionEl, enter: 'bottom', leave: 'top' })` — target must be a DOM element, not a selector string.309- Use `utils.$(selector)` and `utils.set(targets, props)`.310- `stagger()` is a top-level import.311312### Theme system313- Single source of truth: `data-theme="light|dark"` on `<html>`.314- Do **not** toggle a `.dark` class on `<html>`.315- Render-blocking inline no-flash script in `<head>` reads `localStorage` / `prefers-color-scheme` before first paint.316- Toggle: `document.documentElement.setAttribute('data-theme', value)` + `localStorage.setItem('theme', value)`.317318### Project structure319```text320<project>/321├─ astro.config.mjs322├─ tsconfig.json323├─ package.json324├─ public/favicon.svg325└─ src/326 ├─ styles/global.css327 ├─ layouts/Layout.astro328 ├─ utils/merge.ts329 ├─ utils/reveal.ts330 ├─ icons/*.astro331 ├─ components/332 │ ├─ NavBar.astro333 │ ├─ Sidebar.astro334 │ ├─ SidebarHD.astro335 │ ├─ ThemeSwitch.astro336 │ ├─ Footer.astro337 │ ├─ Logo.astro338 │ ├─ Button.astro339 │ ├─ Title.astro340 │ ├─ Description.astro341 │ ├─ Placeholder.astro342 │ ├─ <Motif>.astro343 │ ├─ <Mock>.astro344 │ └─ <Section>.astro ...345 └─ pages/346 ├─ index.astro347 └─ 404.astro348```349350### Single-file fallback351- Inline `<style>` + one IIFE `<script>`.352- Same tokens, same motion principles, `IntersectionObserver` for scroll reveals.353- Target payload: under ~80KB uncompressed (excluding fonts).354355## HTML Head & Meta356357Every page needs:358- `charset`, responsive `viewport`.359- `<title>`: `"<Brand> — <short warm descriptor>"`.360- `<meta name="description">`: one or two warm concrete sentences, ≤155 chars.361- `<meta name="theme-color">` matching accent or `--bg` (with dark media variant).362- `<meta name="color-scheme" content="light dark">`.363- Open Graph (`og:title`, `og:description`, `og:type`) + `twitter:card`.364- Favicon: simple warm SVG mark generated per run.365366# Accessibility Checklist367368Ship only when all are true:369- Semantic landmarks (`header`/`nav`, `main`, `contentinfo`).370- Correct `aria-labelledby` linking section headings to regions.371- Strict heading hierarchy: one `h1`, then `h2 → h3`.372- `type` on every `<button>`.373- `aria-label` on icon-only controls.374- Touch targets ≥ 44×44px.375- `aria-expanded` / `aria-hidden` accurate on menus/drawers/dropdowns.376- Decorative elements `aria-hidden="true"`.377- `prefers-reduced-motion: reduce` fully disables motion.378- `prefers-contrast: more` hardens text contrast and outlines.379- `pointer: coarse` disables hover-only affordances and custom cursor.380381# Common Failure Modes — Do Not Regress382383| Bug | Prevention |384|-----|------------|385| Scrollbar styles on `body` or a utility class | Always target `html` |386| Hero uses `onScroll` / `revealSection()` | Hero uses `autoplay: true` only |387| Generic `peer` checkbox | Use `peer/drawer` and `peer/hd` independently |388| Hardcoded colors in `@theme inline` | Every value must be `var(--brand-*)` |389| anime.js v3 syntax (`easing`, string targets, `cubicBezier(...)`) | Use v4 API strictly |390| Forgetting reduced motion on CSS `@keyframes` | Wrap or override in `@media (prefers-reduced-motion: reduce)` |391| `.dark` class toggle | Use `data-theme` attribute only |392| Unused CSS tokens / `@keyframes` | Audit `global.css` before shipping; delete zero-reference tokens |393| More than one accent hue | One accent only; support earth tones are neutrals |394| Third font or serif body | Two-font system is absolute |395| Pure black/white surfaces | Warm near-black and cream only |396| Reused section order or copy | Vary every run |397| Binary image assets | Use CSS/SVG mocks and template placeholders |398399# Performance & Privacy400401- Zero analytics, trackers, or third-party calls beyond font host.402- Self-host fonts in framework builds; use `preconnect` + `display=swap` in single-file.403- About six font variants total; system-font fallbacks required.404- Framework builds: pin a single `vite` version via `overrides` so `@tailwindcss/vite` and Astro share one Vite instance.405- Clean token surface: every custom property and keyframe must be consumed somewhere; delete unused ones.