The goal is to ship real tools that solve real problems — with enough governance that the second one is easier than the first, and the tenth is almost automatic.
This playbook captures every decision, standard, and lesson learned from building Kate's portfolio of Claude-powered tools. It is a living document — updated every time something new is learned.
Next.js 15+ · App Router · TypeScript · Inline styles with CSS variables · No Tailwind color classes
Anthropic Claude API · claude-sonnet-4-20250514 · Vercel deployment · katehaan.dev subdomains
next/font/google — Cormorant Garamond · Outfit · DM Mono. Never import via URL in CSS.
Cursor + Claude Code · GitHub for version control · Mac (local dev) · Mac mini for agents
A palette drawn from the land — deep forest greens, warm bone, the amber of late-afternoon light, and the rust of dried terracotta. Sophisticated but alive. Organic without being rustic.
| Name | Hex | Role |
|---|---|---|
| Pine | #1a2421 | Dark backgrounds, hero sections, nav |
| Fern | #2E4A3E | Section labels, borders on dark, card accents |
| Sage | #536B5E | Secondary text on dark backgrounds |
| Bone | #FAF8F5 | Light section backgrounds, primary text on dark |
| Linen | #F0EDE8 | Card backgrounds on light sections |
| Canvas | #E4DFD8 | Borders and dividers on light sections |
| Mustard | #C4882A | PRIMARY ACCENT — all CTAs, labels, highlights |
| Amber | #D4A04A | Hover states, italic headline accent color |
| Rust | #A84832 | Warnings, tertiary accent |
| Ink | #221F1A | Headlines on light backgrounds |
| Mid | #4A4238 | Body text on light backgrounds |
| Muted | #8A8070 | Metadata, nav links, subdued labels |
Green family (pine, fern, sage) → backgrounds and borders ONLY. Mustard → every accent, CTA, label, and highlight. Never green text on green background.
Headlines
Italic, weight 300. Section headings, hero titles, pull quotes. Never body text.
Body text
Weight 300 for body, 500-600 for UI labels. Warm and readable.
SECTION LABELS
0.6rem, letter-spacing 0.2em, uppercase, fern green. Tags, metadata, nav.
Dark pine background · Full viewport height · Large Cormorant Garamond italic · Amber accent on key word · Subtle radial glows · Pill tags at bottom
Bone background · Ink/mid text · Linen cards with canvas borders · Colored left borders · Max-width 860px · Section padding 6rem top/bottom
Quote block: 3px mustard left border · rgba(196,136,42,0.05) bg · italic Cormorant Cards: linen bg · canvas border · 3px left border accent Tags/pills: DM Mono 0.55rem · canvas border · muted color · borderRadius 2px Buttons: DM Mono uppercase · mustard border · transparent bg · no border-radius Status Live: fern green border + bg tint Status In dev: mustard border + bg tint Section label: DM Mono · 0.6rem · 0.2em tracking · uppercase · fern green Scroll reveal: IntersectionObserver · opacity 0→1 · translateY(22px)→0 · 0.7s ease
Always use next/font/google. Never import Google Fonts via URL in CSS — it causes build errors.
// app/layout.tsx
import { Cormorant_Garamond, Outfit, DM_Mono } from "next/font/google";
const cormorant = Cormorant_Garamond({
subsets: ["latin"], weight: ["300","400","600"],
style: ["normal","italic"], variable: "--font-display", display: "swap",
});
const outfit = Outfit({
subsets: ["latin"], weight: ["300","400","500","600"],
variable: "--font-body", display: "swap",
});
const dmMono = DM_Mono({
subsets: ["latin"], weight: ["300","400","500"],
variable: "--font-mono", display: "swap",
});
// Apply to html element:
<html className={`${cormorant.variable} ${outfit.variable} ${dmMono.variable}`}>
Never add --font-display: var(--font-display) in globals.css — it creates a circular reference and breaks all fonts. The variables are set automatically by Next.js on the html element.
Every app goes through the same five phases. Don't skip ahead — the shortcuts always cost more time than they save.
Write a project brief. Define the problem, vision, audience, must-haves, and open questions. Pick repo name following governance. Don't write code until the brief is clear.
Copy globals.css from playbook. Set up layout.tsx with next/font/google. Lock in CSS variables before building any UI. Two hours here saves twenty hours later.
Use Cursor + Claude Code. Write files directly — don't paste into terminal. Use cp ~/Downloads/file.tsx to replace files. Commit frequently with proper convention.
Create byok branch for public demo. Add GateScreen component. Configure 5 constants. Test with a real Anthropic API key. See BYOK section for full details.
Create repo with correct naming. Add .gitignore first. Write README with status badges. Add description and topics. Create GitHub Release at v1.0.
Import repo. Set Framework Preset to Next.js manually — it defaults to Other. Add env vars. Connect custom domain on katehaan.dev. Never share .vercel.app URL.
Decide once, apply everywhere. These standards exist so every new project benefits from every previous lesson.
| Type | Pattern | Examples |
|---|---|---|
| OpenClaw Agents | openclaw-[name] | openclaw-heidi, openclaw-kristen, openclaw-sage |
| OpenClaw Framework | openclaw-[descriptor] | openclaw-starter, openclaw-playbook |
| Standalone Tools | descriptive-kebab | escalation-advisor, voice-tone-shifter |
| Playbooks / Guides | playbook-[topic] | playbook-app-build, playbook-analytics |
| Portfolio Site | [name]-dev | katehaan-dev |
tone-fixer.vercel.app was claimed by someone else before a custom domain was set up. voice-tone-shifter.katehaan.dev is now the canonical URL. Never share .vercel.app links publicly.
| Pattern | Use |
|---|---|
| [repo-name].katehaan.dev | All public-facing tool URLs |
| [repo-name]-demo.katehaan.dev | BYOK public demo versions |
| katehaan.dev | Portfolio site only |
To connect a subdomain: Vercel → Settings → Domains → add subdomain → copy CNAME → Squarespace DNS → Add Record → Type: CNAME, Name: [subdomain], Value: [from Vercel].
Format: [type]: [what you did] — lowercase, present tense, no period.
| Type | When to use | Example |
|---|---|---|
| add: | New file, feature, section | add: escalation brief output with mustard border |
| update: | Changed existing content | update: governance section in playbook |
| fix: | Corrected something broken | fix: byok branch missing Change Key button |
| remove: | Deleted something | remove: stale weekly meal plan files |
| docs: | README or docs only | docs: add lessons learned to openclaw-starter |
| style: | Visual only, no logic change | style: tighten card padding on mobile |
| deploy: | Deployment related | deploy: trigger vercel redeploy after env var update |
| Version | Meaning |
|---|---|
| v0.1 | Live but incomplete — scaffold exists, major sections missing |
| v1.0 | MVP — core functionality works, publicly shareable |
| v1.1, v1.2... | Incremental improvements, no breaking changes |
| v2.0 | Significant new capabilities or breaking changes |
Add to the top of every README. Status badge + type badge.
     
google_token.json *.json client_secret*.json .env / .env.* memory/ meal-planning/ crm/people.md FUTURE_KATE.md avatar/ FRANK_FLAG.md .DS_Store __pycache__/ *.pyc node_modules/
.env .env.* .env.local node_modules/ .next/ out/ dist/ build/ .DS_Store *.log
.DS_Store *.log node_modules/ drafts/ *.draft.md
| Slot | Repo | Rule |
|---|---|---|
| 1 (permanent) | openclaw-starter | Framework — most shareable |
| 2 (permanent) | escalation-advisor | Most polished live tool |
| 3 (permanent) | katehaan-dev | Portfolio site — always current |
| 4 (rotating) | Most recently shipped | Shows momentum |
| 5 (rotating) | Most career-relevant | Reflects current goals |
| 6 (rotating) | One personal/charming project | Personality |
All public tools use a BYOK (Bring Your Own Key) gate so Claude API costs aren't on Kate. The gate collects the user's Anthropic API key, validates it, and stores it in sessionStorage for the session.
Two versions of every tool: main branch for team use (no gate), byok branch for public demo (gate screen required).
voice-tone-shifter.katehaan.dev (formerly tone-fixer — renamed per governance). The GateScreen component lives in gate-screen-starter/ in this repo.
Configure these for each app — everything else stays the same.
PRODUCT_NAME — app name displayed on gate screen PRODUCT_TAGLINE — one-line description FEATURE_LIST — array of bullet points (what the tool does) ACCENT_COLOR — mustard (#C4882A) by default GITHUB_URL — link to the public repo
The gate screen stores the API key in sessionStorage — it clears when the browser tab closes. Users need to re-enter their key each session. This is intentional for security.
Nobody warns you that building your first app feels like death by a thousand cuts. Not because any single thing is hard — it isn't. It's that every step has a small gotcha attached to it. None of it is insurmountable. All of it is annoying. And most people quit somewhere in that gap.
The gotchas are just tax you pay once.
Node, Git, and a GitHub account. Do it the day before. Don't discover you're missing them mid-build. Install Homebrew first: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" then brew install node git gh.
Password authentication was deprecated in 2021. Use the GitHub CLI: brew install gh then gh auth login. Takes five minutes, works cleanly forever.
tone-fixer.vercel.app was claimed by someone else before a custom domain was set up. Always add [repo-name].katehaan.dev before sharing any link. Subdomains are free and permanent.
Add it before you add anything else. One accidental commit of google_token.json to a public repo means rotating credentials. Use the templates in this playbook. Create on first commit — never the second.
Paste corruption is real. Backticks, smart quotes, and special characters get mangled when copying from a browser into a terminal. Let Claude Code write directly to the file. For replacements, use cp ~/Downloads/file.tsx ~/project/app/file.tsx.
Lock in CSS variables before building a single page. The temptation is to get something on screen fast and style it later. Later never comes, and you end up with inconsistency that takes longer to fix than the design system would have taken to set up.
Check your framework preset in Vercel settings. It defaults to Other when it can't detect the project type. Set it to Next.js manually, then redeploy.
Never write --font-display: var(--font-display) in globals.css. It creates a circular reference and breaks all fonts. Next.js sets these variables automatically on the html element via layout.tsx.
The original design system used green for everything — backgrounds, text, accents, borders. Nothing had contrast. Rule: green family = structural (backgrounds, borders) only. Mustard = every single accent. Never mix.
The whole process — zero to a live, custom-domain app with a design system and a BYOK gate — takes one focused session. It wasn't smooth. It also wasn't that hard. The gap between "I could never build that" and "I just did" is mostly just not quitting when the third thing in a row doesn't work.
Every person who has a GitHub they're proud of went through the same frustrations. They just kept going.
Every site gets a custom favicon. It takes five minutes and immediately signals that someone intentional built this. Use favicon.io/favicon-generator — no design tools required.
| Site | Text | Background | Font Color |
|---|---|---|---|
| katehaan.dev | KH | #1a2421 (pine) | #FAF8F5 (bone) |
| brand-book.katehaan.dev | KH | #1a2421 (pine) | #FAF8F5 (bone) |
| release-playbook.katehaan.dev | {} | #1a2421 (pine) | #C4882A (mustard) |
| escalation-advisor.katehaan.dev | ↑ | #1a2421 (pine) | #C4882A (mustard) |
| voice-tone-shifter.katehaan.dev | ~ | #1a2421 (pine) | #C4882A (mustard) |
| Any new tool | 1-2 char symbol | #1a2421 (pine) | #C4882A (mustard) |
| Any brand property | KH | #1a2421 (pine) | #FAF8F5 (bone) |
Brand properties (portfolio, brand book, playbook) → KH in bone. Tools → short symbol in mustard that hints at what the tool does. Background always pine #1a2421.
Drop favicon.ico into the root folder alongside index.html. Add to <head>:
<link rel="icon" href="/favicon.ico"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
Drop favicon.ico directly into the app/ folder. Next.js picks it up automatically — no code changes needed.
Browsers cache favicons aggressively. After updating, hard refresh with Cmd + Shift + R to see the new one. Or open in an incognito window.