Duotone Identity Design System
Anthropic-inspired duotone design language for Identity Platform. Strict cream/charcoal palette with intentional typography and animated SVG illustrations.
Quick Reference
| Element | Value |
|---------|-------|
| Primary Light | #FAF9F5 (cream) |
| Primary Dark | #141413 (charcoal) |
| Display Font | Instrument Serif |
| Body Font | Inter |
| SVG viewBox | 500 x 500 |
| Chapter Number | clamp(6rem, 15vw, 12rem) italic |
Color Palette
Core Duotone Colors
:root {
--cream: #FAF9F5; /* Light backgrounds, dark text */
--charcoal: #141413; /* Dark backgrounds, light text */
}
Usage Pattern
| Section | Background | Text |
|---------|------------|------|
| Light (Hero, Ch1, Ch3) | var(--cream) | var(--charcoal) |
| Dark (Ch2, Ch4, Finale) | var(--charcoal) | var(--cream) |
Rule: Never use other colors for primary UI. Accent colors only for interactive elements (wallet button, links).
Section Alternating
// Chapter 1, 3: Light
<section className="bg-[var(--cream)] text-[var(--charcoal)]">
// Chapter 2, 4: Dark
<section className="bg-[var(--charcoal)] text-[var(--cream)]">
Typography
Font Families
/* Display / Headings */
font-family: 'Instrument Serif', Georgia, serif;
/* Body / UI */
font-family: 'Inter', -apple-system, sans-serif;
Display Headings (H1, H2)
.landing-h1 {
font-size: clamp(3rem, 12vw, 8rem);
line-height: 1;
letter-spacing: -0.02em;
}
.landing-h2 {
font-size: clamp(2rem, 6vw, 4.5rem);
line-height: 1.15;
letter-spacing: -0.02em;
}
Chapter Labels
<p className="text-xs md:text-sm font-semibold uppercase tracking-[0.2em] opacity-50 flex items-center gap-4">
<span className="w-10 h-px bg-current opacity-50"></span>
{label}
</p>
Pattern: 40px horizontal dash + uppercase text + 0.2em letter-spacing
Chapter Numbers
<span className="font-serif italic opacity-[0.08]"
style={{ fontSize: 'clamp(6rem, 15vw, 12rem)', lineHeight: 1 }}>
{String(number).padStart(2, '0')}
</span>
Positioning: absolute top-4 left-4 md:top-8 md:left-8
Layout Patterns
Chapter Section Structure
<section className={`chapter ${isLight ? 'light' : 'dark'} min-h-screen flex items-center justify-center relative p-6 md:p-12 ${isLight ? 'bg-[var(--cream)] text-[var(--charcoal)]' : 'bg-[var(--charcoal)] text-[var(--cream)]'}`}>
{/* Chapter Number - large, positioned */}
<span className="chapter-number absolute top-4 left-4 md:top-8 md:left-8 font-serif italic opacity-[0.08]"
style={{ fontSize: 'clamp(6rem, 15vw, 12rem)', lineHeight: 1 }}>
{String(number).padStart(2, '0')}
</span>
<div className="chapter-inner grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 max-w-6xl w-full relative z-10">
{/* Content */}
<div className="chapter-content">
<p className="chapter-label text-xs md:text-sm font-semibold uppercase tracking-[0.2em] opacity-50 mb-4 flex items-center gap-4">
<span className="w-10 h-px bg-current opacity-50"></span>
{label}
</p>
<h2 className={`landing-h2 ${isLight ? 'text-[var(--charcoal)]' : 'text-[var(--cream)]'}`}>
{title}
</h2>
</div>
{/* Illustration */}
</div>
</section>
Key Rules:
- Even chapters (2, 4): reverse layout (illustration left, content right)
- Odd chapters (1, 3): standard layout (content left, illustration right)
- Always explicitly set h2 color based on
isLight(global CSS overrides)
SVG Illustrations
Standard viewBox
Always use viewBox="0 0 500 500" for consistency.
All coordinates must stay within 0-500 range to prevent clipping.
Path Classes
| Class | Purpose | Animation |
|-------|---------|-----------|
| .fill-path | Shapes, filled elements | Opacity 0→0.9, scale 0.8→1 |
| .draw-path | Lines, strokes | strokeDashoffset animation |
| .label-path | Text labels | Opacity 0→0.7 |
CSS Initial States
.fill-path {
opacity: 0;
transform-origin: center;
vector-effect: non-scaling-stroke;
}
.draw-path {
opacity: 0;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
}
.label-path {
opacity: 0;
font-size: 14px;
font-weight: 500;
text-anchor: middle;
pointer-events: none;
}
Common SVG Patterns
Person/Figure (Chapter 1):
<circle className="fill-path" cx="250" cy="140" r="35" fill="#141413" opacity="0"/>
<path className="fill-path" d="M 215 180 Q 200 220 210 280..." fill="#141413" opacity="0"/>
Process Flow (Chapter 2):
{/* Horizontal left-to-right flow */}
<rect className="fill-path" x="60" y="200" width="80" height="60" fill="#FAF9F5" opacity="0"/>
<path className="draw-path" d="M 140 230 L 180 230" stroke="#FAF9F5" strokeWidth="2" opacity="0"/>
Growth Trajectory (Chapter 3):
<path className="draw-path" d="M 50 420 Q 120 400 160 350..." stroke="#141413" strokeWidth="3" opacity="0"/>
<circle className="fill-path" cx="160" cy="350" r="10" fill="#141413" opacity="0"/>
Central Hub (Chapter 4):
<circle className="fill-path" cx="250" cy="250" r="45" fill="#FAF9F5" opacity="0"/>
<circle className="draw-path" cx="250" cy="250" r="65" fill="none" stroke="#FAF9F5" strokeWidth="2" opacity="0"/>
GSAP Animations
Animation Pattern for Chapters
import { gsap } from 'gsap';
import ScrollTrigger from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
export const useChapterAnimations = () => {
useEffect(() => {
const chapters = document.querySelectorAll('.chapter');
if (chapters.length === 0) return;
const ctx = gsap.context(() => {
chapters.forEach((chapter) => {
const fillPaths = chapter.querySelectorAll('.fill-path');
const drawPaths = chapter.querySelectorAll('.draw-path');
// Fill paths: opacity + scale
if (fillPaths.length > 0) {
gsap.to(fillPaths, {
opacity: 0.9,
scale: 1,
duration: 0.8,
stagger: 0.1,
ease: 'back.out(1.5)',
scrollTrigger: {
trigger: chapter,
start: 'top 80%',
toggleActions: 'play none none reverse',
},
});
}
// Draw paths: stroke animation
if (drawPaths.length > 0) {
drawPaths.forEach(path => {
const length = (path as SVGPathElement).getTotalLength() || 1000;
(path as SVGPathElement).style.strokeDasharray = String(length);
(path as SVGPathElement).style.strokeDashoffset = String(length);
});
gsap.to(drawPaths, {
strokeDashoffset: 0,
opacity: 0.6,
duration: 1.5,
stagger: 0.1,
ease: 'power2.inOut',
scrollTrigger: {
trigger: chapter,
start: 'top 75%',
toggleActions: 'play none none reverse',
},
});
}
});
});
return () => ctx.revert();
}, []);
};
Key GSAP Rules
- Always use
gsap.context()for cleanup toggleActions: 'play none none reverse'for reversible scroll animations- Never use inline
style={{ transform: 'scale' }}- let GSAP handle all transforms - Initialize strokeDasharray in the effect, not inline styles
- stagger: 0.1 for sequential element animations
Resources
references/
palette.md- Full color definitions with usage examplestypography.md- Font loading, sizing, and heading stylessvg-patterns.md- Common SVG illustration templatesgsap-recipes.md- Copy-paste animation patterns
scripts/
validate_colors.sh- Check if colors match duotone palettegenerate_chapter.sh- Scaffold new chapter section
Usage Examples
Creating a new chapter:
1. Use scripts/generate_chapter.sh 5
2. Add SVG to illustration container (500x500 viewBox)
3. Use fill-path/draw-path/label-path classes
4. Set isLight based on position (odd=light, even=dark)
Validating colors:
Use scripts/validate_colors.sh before committing to ensure only cream/charcoal are used
Anti-Patterns
| ❌ Don't | ✅ Do |
|---------|--------|
| Use other colors for UI | Only cream/charcoal |
| Inline style={{ scale }} | Let GSAP animate |
| Different viewBox sizes | Always 500x500 |
| Coordinates outside 0-500 | Keep within bounds |
| Generic class names | Use fill-path/draw-path/label-path |