Ship Season 01 · Build NN
Title
One-line promise.
# Build 06 -- Matcha Streak -- Product Requirements Document
> Example PRD used by `/prd-vibe` as a model. The product is fictional. Do not implement.
> If the launch tweet is boring, the build is boring -- pivot before writing code.
---
## TL;DR `P0`
Matcha Streak is a single-page PWA that lets daily matcha drinkers track their streak with one tap. No login, no friends, no leaderboard. Open the URL, tap the bowl, see the number grow. Built to prove that habit utility doesn't need accounts -- and that a 50kb app can outperform a 200MB one.
```spec-strip
- Product type: PWA
- Stack: Vite + React 18, no backend
- Day: 06 of 30
- Bundle: <50kb gzip
- Lighthouse: 95+ mobile
- Status: kickoff
```
---
## ICP card `P0`
```persona
name: Maya
age: 28
location: Brooklyn
role: Junior PM at a Series B SaaS
quote: I bought matcha last week and have no idea if it's actually doing anything.
workaround: A tally in iOS Notes. Easy to forget, easy to lie to herself.
channels: X (lifestyle-startup side), follows @rikventure and 5 wellness builders
watering-holes: #buildinpublic, r/Matcha, the Lenny's Slack #products channel
```
---
## Positioning `P0`
```positioning
for: matcha-curious remote workers
need: want to know if their habit is actually sticking
product: Matcha Streak
category: no-account streak tracker
unique: works in one tap, no setup, runs forever in a browser tab
alternative: Streaks app, Notes-app tally, doing nothing
differentiator: don't make you sign up
```
---
## Competitive alternatives `P0`
- **Streaks ($5 iOS app)** -- multi-habit framework; setup wall; iOS-only; opinionated about what counts as a habit
- **Notes-app tally** -- free and frictionless but invisible; you forget to write the line; no streak math
- **Notion habit DB** -- overengineered; the database becomes the hobby
- **Doing nothing** -- the real competitor; people accept "I think I'm drinking matcha most days"
---
## Distribution plan `P0`
| Channel | Asset | When | Owner | Status |
|---|---|---|---|---|
| X launch thread | 4-tweet thread + 30s screen-rec demo | Day 6, 8am ET | Rik | drafted |
| IG reel | 12s magic-moment cut (bowl tap → streak) | Day 7, morning | Rik | shotlist done |
| TheRikOS Substack | "Apps that don't ask for your email" issue mention | 2026-05-15 | Rik | queued |
| GodModePod EP13 cold open | Verbal mention + URL on lower-third | 2026-05-16 | Rik | added to docket |
| vibecode.fun listing | Full publish-form pack via `/captions` | Day 6 | auto | pending |
| Reply Radar (r/Matcha) | One organic reply on a streak-question thread | Day 7 | Rik | watching |
---
## Launch copy pack `P0`
**Tagline:** Tap the bowl. Track your matcha.
**One-liner:** A no-account streak tracker for daily matcha drinkers. One bowl. One tap. One number.
**Founder launch tweet:**
```launch-tweet
name: Rik Ventura
handle: @rikventure
scheduled: 2026-05-11 08:00 ET
day 6 of 30 builds in 30 days.
i wanted to know if i'm actually drinking matcha daily.
no app made me do this in one tap. so i built one.
no signup. no friends. no leaderboard.
open url. tap bowl. see number.
matcha-streak.midcurved.com
```
**Landing hero block:**
- H1: Tap the bowl. Track your matcha.
- Sub: A streak tracker that respects your time. No account, no friends, no notifications.
- CTA: Tap the bowl
- Micro-proof: Day 06 of Ship Season 01 -- 30 builds in 30 days
**OG / app description:** A one-tap matcha streak tracker. No login. Built in 6 hours on day 6 of Ship Season.
---
## Activation event + magic moment `P0`
**Activation event:** `matcha_tapped` fires with `new_streak >= 1`. The first verified tap is activation -- the user has done the thing once and seen the number tick.
**Magic moment:** Bowl fills with green ink, steam rises, the number flips from "0 days" to "1 day" in 300ms. Demo timecode: 0:08 in `script.md`. This frame becomes:
- IG reel cover (frozen at peak ink-fill)
- X tweet GIF (loop frames 6--14)
- OG image (rendered version with "1 day" label)
- Landing hero shot
---
## First 60 seconds `P0`
```storyboard
- T+0s: User taps the launch tweet link in their X feed
- T+3s: Page loaded. Empty bowl, "0 days" in big mono numerals
- T+8s: User taps the bowl. Animation plays.
- T+12s: "1 day" appears. Small pop, no sound. Dopamine hit lands.
- T+30s: User screenshots and quote-tweets the launch with "lol fine"
- T+60s: User comes back tomorrow. Streak ticks to "2 days". The loop closes.
```
---
## Acquisition funnel `P0`
```funnel
- 80000: Tweet impressions (target reach via Rik's audience + reply guys)
- 2400: Link clicks (3% CTR)
- 2000: Page loads (matcha_streak_landed)
- 1200: Activation tap (matcha_tapped, new_streak >= 1)
- 180: Returns next day (matcha_streak_landed, is_returning=true)
- 60: Day-7 retention (the only number that matters at month 1)
```
---
## Core flow `P0`
```
1. User lands on matcha-streak.midcurved.com
2. App loads instantly (<30kb HTML+JS+SVG)
3. Sees giant matcha bowl + current streak ("0 days")
4. Taps the bowl
5. Bowl animates -- steam rises, bowl fills with green ink
6. Streak ticks to 1 ("1 day"); date written to localStorage
7. User closes tab; comes back tomorrow
8. App reads localStorage; if last_tapped == yesterday, ready to tick to 2 on next tap
9. If they miss a day, streak resets to 1 with a "fresh start" message (not shame)
```
### Why this flow
- **No auth** -- removes the #1 drop-off point in habit apps
- **Single tap** -- the only interaction; zero forms, zero settings
- **Visual streak** -- the number IS the dopamine, no leaderboard required
- **Fresh-start framing** -- missed days don't punish; they restart gently
### User flow diagram
```mermaid
flowchart LR
A[Open URL] --> B{Tapped today?}
B -->|no| C[Show bowl + streak]
B -->|yes| D[Show 'see you tomorrow']
C --> E[Tap bowl]
E --> F[Animate + increment]
F --> G[Write to localStorage]
```
---
## Functional requirements `P0`
```features
title: One-tap check-in
tier: p0
trigger: User taps the matcha bowl SVG
behavior: Compare today vs last_tapped in localStorage. Today -> no-op. Yesterday -> increment. Older -> reset to 1.
result: Bowl animates 300ms. Streak number updates. Today's date written as last_tapped.
acceptance: Tap; streak increments visually within 300ms; refresh; streak persists.
---
title: Streak persistence across sessions
tier: p0
trigger: Page load
behavior: Read streak, last_tapped, started_at from localStorage. Recompute streak based on today vs last_tapped.
result: Correct streak shown before user can interact.
acceptance: Tap today (streak = 1). Close browser. Reopen tomorrow. Tap. Streak = 2.
---
title: PWA install
tier: p0
trigger: User opens on mobile browser
behavior: Manifest + service worker make Add-to-Home-Screen available natively
result: App installs as standalone icon
acceptance: iPhone Safari Add-to-Home works. Android Chrome beforeinstallprompt fires.
---
title: Share streak
tier: p1
trigger: User long-presses the streak number
behavior: Web Share API opens native share sheet with text "Day N of my matcha streak. matcha-streak.midcurved.com"
result: User can share to any installed app
acceptance: Long-press triggers native share on iOS Safari.
```
---
## Tech stack -- LOCKED `P0`
| Layer | Pick | Version | Why |
|---|---|---|---|
| Frontend | Vite + React | `vite@5`, `react@18` | Smallest bundle, no Next.js overhead for one screen |
| Styling | Vanilla CSS + CSS variables | -- | One screen of CSS doesn't need Tailwind |
| Hosting | Vercel | -- | Vault default, edge cache for free |
| Storage | localStorage | -- | No accounts, no backend |
| PWA | Manual `manifest.json` + service worker | -- | One-page app, no framework needed |
| Animation | CSS transitions | -- | No GSAP / framer-motion overhead |
| Analytics | PostHog | latest | Vault standard, project 162766 |
### DO NOT USE
- Next.js -- overkill for a 1-page no-backend app
- Tailwind -- 1 screen of CSS doesn't justify the toolchain
- React Router -- there is one route
- Any UI library (shadcn, MUI, Mantine) -- one button, one number
- Auth library -- there is no auth
- Database -- localStorage only
---
## NON-GOALS `P0`
```nogoals
- Accounts / login / sync across devices
- Reminders / push notifications
- Leaderboards / friends / social
- Multiple habits
- Streak freezes / vacation mode
- Custom matcha types
- Native iOS / Android app
- Onboarding tutorial
- Settings page
```
---
## Success criteria `P0`
- [ ] Bundle under 50kb gzip
- [ ] First paint under 500ms on 3G throttled
- [ ] Lighthouse PWA score = 100
- [ ] Lighthouse Performance >= 95 mobile
- [ ] Tap-to-animation latency under 100ms
- [ ] Works offline after first load
- [ ] PostHog `matcha_tapped` fires on every tap
- [ ] README has working "How to run locally" (`npm install && npm run dev`)
- [ ] Deployed to `matcha-streak.midcurved.com`
- [ ] Magic-moment frame exported as standalone PNG for IG cover
---
## Telemetry contract `P0`
PostHog project 162766. Dashboard 665748.
| Event | When | Properties |
|---|---|---|
| `matcha_streak_landed` | Page load | `is_pwa`, `is_returning_user`, `current_streak`, `referrer` |
| `matcha_tapped` | Bowl tap (counts) | `new_streak`, `was_reset` |
| `matcha_already_tapped` | Bowl tap (no-op, today done) | `current_streak` |
| `matcha_pwa_installed` | beforeinstallprompt accepted | -- |
| `matcha_shared` | Web Share API used | `target` |
---
Retention loop `P1`
Primary mechanic: **Habit / intrinsic reward** -- the streak number itself is the loop.
Secondary: **Home-screen install** -- PWA prompt fires on the 3rd tap (not the 1st; respect the user). Once on the home screen, the bowl icon IS the reminder.
**Why this mechanic:** Push notifications would compromise the brand promise ("respects your time"). The streak going to 0 is its own punishment-free pressure. The home-screen install is the only retention asset that survives a phone reboot.
---
Content angle `P1`
- **Thesis it proves:** Vibe-coded apps don't need accounts to provide real utility. The smallest possible product is often the right product.
- **Hook for the build video:** "I built a habit tracker in an afternoon. It has no accounts, no settings, no notifications. It's 50kb. And it's better than the $5 app I uninstalled."
- **GodModePod angle:** "What if 90% of habit-app code is plumbing for features users actively don't want?"
- **Pull-quote from Rik mid-build:** "the moment i added settings, i felt the build die a little"
---
Pricing narrative `P1`
`Free forever; portfolio piece. If 1k DAU sticks past day 30, add a $5 one-time tip jar with a custom bowl color as the only reward. No subscription, ever -- breaks the brand promise.`
---
Emotion curve `P1`
1. **Sees launch tweet** (curious) -- "wait, no signup? prove it"
2. **Lands** (skeptical) -- "this loaded too fast, where's the catch"
3. **Taps** (delighted) -- "that animation is fucking *clean*"
4. **Sees streak** (validated) -- "1 day. okay. cool."
5. **Returns next day** (committed) -- "i don't want to break this"
---
Data model `P1`
localStorage only. One key, one JSON blob.
```mermaid
erDiagram
LOCALSTORAGE {
string streak_data
}
```
```js
localStorage["matcha_streak_v1"] = {
streak: 7,
last_tapped: "2026-05-11",
started_at: "2026-05-04",
total_taps: 7
}
```
### Key field notes
- `streak` -- recomputed on load if last_tapped < yesterday
- Schema versioned via key suffix (`_v1`) so future migrations don't break existing users
- No PII, no fingerprinting -- intentional brand promise
---
Sitemap / IA `P1`
One route. Skipped.
---
State diagram `P1`
```mermaid
stateDiagram-v2
[*] --> FreshUser: localStorage empty
[*] --> ReturningUser: streak data found
FreshUser --> StreakActive: tap
ReturningUser --> AlreadyTappedToday: last_tapped == today
ReturningUser --> StreakActive: last_tapped == yesterday, tap
ReturningUser --> StreakReset: last_tapped < yesterday, tap
StreakReset --> StreakActive: streak = 1
AlreadyTappedToday --> [*]
```
---
Design direction `P1`
### Vibe
A Japanese tea-house calm meets builder-log restraint. Mostly empty screen. One giant matcha bowl, hand-drawn (rough SVG, not crisp vector). One soft sage-green accent. The streak number is hero -- huge, monospace. Background is warm off-white, not pure white. Should feel like a paper diary, not a SaaS dashboard. References: Are.na profiles, Studio Neat, Field Notes inside cover. Should NOT feel like: a Lovable demo, a Calm app screen, a Headspace gradient.
### Color tokens
```swatches
- bg: oklch(96% 0.012 95)
- fg: oklch(20% 0.01 90)
- muted: oklch(50% 0.012 90)
- accent: oklch(65% 0.13 145)
- accent-soft: oklch(92% 0.04 145)
- ink-stroke: oklch(28% 0.03 145)
```
### Typography
- Display (streak number): Geist Mono 700, 8rem, tabular numerals
- Body: Geist 400
- Decorative caption: Instrument Serif italic for "1 day" / "fresh start"
### Component vibe checks
- Matcha bowl: hand-drawn SVG, soft brush stroke, tap target = entire bowl
- Streak number: 8rem mono, no leading zeros, no commas
- "1 day" / "N days" -- pluralization correct, no exclamation marks
- No buttons. The bowl IS the button.
---
Locked surfaces -- DO NOT CHANGE `P1`
None -- everything is greenfield.
---
File structure `P1`
```
.
├── README.md
├── package.json
├── vite.config.ts
├── index.html
├── public/
│ ├── manifest.json
│ ├── icon-192.png
│ ├── icon-512.png
│ └── sw.js
├── src/
│ ├── main.tsx
│ ├── App.tsx
│ ├── bowl.svg
│ ├── styles.css
│ ├── streak.ts
│ └── posthog.ts
└── tests/
└── streak.test.ts
```
### Naming conventions
- Files: lowercase
- Components: PascalCase
- Pure functions: camelCase
- Test files: `*.test.ts` next to the unit
---
## Open questions
1. Sound on tap? Default: silent ships first. Sound is v1.
2. What happens at streak = 365? Default: nothing special. Log a `matcha_year` event for the content angle.
3. Domain -- subdomain or buy `matchastreak.com`? Default: subdomain ($0, ships today).
---
## Decisions log
- `2026-05-11` -- localStorage only, no accounts -- removes the #1 drop-off point and matches the brand promise
- `2026-05-11` -- one habit only (matcha) -- a focused tool beats a generic framework at this scope
- `2026-05-11` -- no notifications, ever -- the streak is its own pressure; reminders break the "respects your time" promise
---
## CLAUDE.md block (paste into agent context)
```markdown
# Build 06 -- Matcha Streak -- Agent Context
You are building Build 06 of Ship Season 01 (`matcha-streak`). Read this entire block before writing any code.
## What you're building (one line)
A single-page PWA that lets daily matcha drinkers track their streak with one tap. No login, no backend, localStorage only.
## Positioning
For matcha-curious remote workers who want to know if their habit is sticking, Matcha Streak is a no-account streak tracker that works in one tap. Unlike Streaks or Habitica, we don't make you sign up.
## Stack -- LOCKED
- Vite + React 18
- Vanilla CSS + CSS variables (no Tailwind)
- Vercel hosting
- localStorage only (no DB, no API)
- Manual PWA: manifest.json + service worker
- PostHog analytics (project 162766)
## DO NOT USE
- Next.js (overkill)
- Tailwind (1 screen)
- React Router (1 route)
- Any UI library
- Auth library
- Database
## NON-GOALS (do not build these)
- Accounts / sync across devices
- Push notifications / reminders
- Leaderboards / social / friends
- Multiple habits
- Streak freezes / vacation mode
- Native app
- Onboarding tutorial
- Settings page
## File structure (create exactly this tree)
.
├── README.md
├── package.json
├── vite.config.ts
├── index.html
├── public/{manifest.json, icon-192.png, icon-512.png, sw.js}
├── src/{main.tsx, App.tsx, bowl.svg, styles.css, streak.ts, posthog.ts}
└── tests/streak.test.ts
## Acceptance checklist (verify before claiming done)
- [ ] Bundle under 50kb gzip
- [ ] First paint under 500ms on 3G
- [ ] Lighthouse PWA = 100
- [ ] Lighthouse Performance >= 95 mobile
- [ ] Tap-to-animation under 100ms
- [ ] Works offline after first load
- [ ] PostHog matcha_tapped fires on every tap
- [ ] README has working how-to-run
- [ ] Deployed to matcha-streak.midcurved.com
## Activation event
PostHog event `matcha_tapped` with `new_streak >= 1` fires when the user has truly activated. Wire this first; everything else is in service of it.
## Magic moment
Bowl fills with green ink, steam rises, the number flips from "0 days" to "1 day" in 300ms. This is the IG reel cover frame. Make it look right -- it's the asset the marketing depends on.
## Telemetry contract
- Init PostHog via src/posthog.ts to project 162766
- Fire: matcha_streak_landed, matcha_tapped, matcha_already_tapped, matcha_pwa_installed, matcha_shared
## Locked surfaces (do not refactor)
None -- greenfield.
## Voice / copy rules
- No em-dashes in user-visible copy
- "Day N" not "Day N!" -- no marketing exclamation
- Reset framing is "fresh start", not "you broke it"
- No emoji in UI
## When in doubt
Re-read this file. Do not invent scope not listed here. The whole thing is one bowl, one tap, one number.
```
${escapeHtml(p.name || 'Rik Ventura')}
${escapeHtml(p.handle || '@rikventure')}
${escapeHtml(body.trim())}
${p.scheduled ? `scheduled: ${escapeHtml(p.scheduled)}
` : ''}
`;
}
function renderStoryboard(src) {
const items = parseListKv(src);
return `${items.map(i => `
${escapeHtml(i.key)}
${escapeHtml(i.val)}
`).join('')}
`;
}
function renderFunnel(src) {
const items = parseListKv(src).map(i => ({ num: i.key, label: i.val }));
const max = Math.max(...items.map(i => parseFloat(i.num.replace(/[^\d.]/g,'')) || 0));
return `${items.map(i => {
const v = parseFloat(i.num.replace(/[^\d.]/g,'')) || 0;
const pct = max ? Math.max(2, (v / max) * 100) : 100;
return `
${escapeHtml(i.num)}
${escapeHtml(i.label)}
`;
}).join('')}
`;
}
function renderSwatches(src) {
const items = parseListKv(src);
return `${items.map(i => `
${escapeHtml(i.key)}
${escapeHtml(i.val)}
`).join('')}
`;
}
function renderSpecStrip(src) {
const items = parseListKv(src);
const html = items.map(i => `
${escapeHtml(i.key)}
${escapeHtml(i.val)}
`).join('');
document.getElementById('spec-strip').innerHTML = html;
}
function renderFeatures(src) {
const blocks = src.split(/\n---\n/);
return `${blocks.map(b => {
const f = parseKv(b);
const tier = (f.tier || 'p0').toLowerCase();
const dl = ['trigger','behavior','result','acceptance'].filter(k=>f[k]).map(k=>`
${k}${escapeHtml(f[k])}`).join('');
return `
${tier.toUpperCase()}
${escapeHtml(f.title || 'Feature')}
${dl}
`;
}).join('')}
`;
}
function renderNogoals(src) {
const items = parseList(src);
return `${items.map(i => `${escapeHtml(i)}`).join('')}
`;
}
function renderCompare(src) {
const sides = src.split(/\n---\n/);
return `${sides.map(s => {
const lines = s.trim().split('\n');
const head = lines.shift() || '';
return `
${escapeHtml(head.replace(/^#+\s*/,''))}
${marked.parse(lines.join('\n'))}`;
}).join('')}
`;
}
// ---- Slugify and section split
function slugify(s){return s.toLowerCase().trim().replace(/[`*_~]/g,'').replace(/[^\w\s-]/g,'').replace(/\s+/g,'-').replace(/-+/g,'-');}
function splitSections(md) {
const lines = md.split('\n');
const sections = [];
let cur = { heading: null, id: null, body: [] };
let inFence = false;
for (const line of lines) {
if (/^```/.test(line)) inFence = !inFence;
if (!inFence && /^##\s+/.test(line)) {
if (cur.heading || cur.body.length) sections.push(cur);
const txt = line.replace(/^##\s+/, '').trim();
cur = { heading: txt, id: slugify(txt.replace(/`P[01]`|`LOCKED`|`DONE`/g, '')), body: [line] };
} else cur.body.push(line);
}
if (cur.heading || cur.body.length) sections.push(cur);
return sections;
}
function pillify(html) {
return html
.replace(/P0<\/code>/g, 'P0')
.replace(/P1<\/code>/g, 'P1')
.replace(/LOCKED<\/code>/g, 'LOCKED')
.replace(/DONE<\/code>/g, 'DONE');
}
// ---- Render pipeline
const sections = splitSections(sourceMd);
const main = $('#content');
const tocList = $('#toc');
// Strip the markdown H1 — the hero owns the title
const preamble = sections.length && !sections[0].heading ? sections.shift() : null;
if (preamble) {
const noH1 = preamble.body.join('\n').replace(/^#\s.+$/m, '').trim();
if (noH1) {
const wrap = document.createElement('div');
wrap.innerHTML = pillify(marked.parse(noH1));
main.appendChild(wrap);
}
}
const distributionRegex = /distribution|launch.*plan|channel/i;
let secNum = 0;
for (const sec of sections) {
secNum++;
const sectionMd = sec.body.join('\n');
const html = pillify(marked.parse(sectionMd));
const wrap = document.createElement('section');
wrap.id = sec.id;
wrap.innerHTML = html;
const h2 = wrap.querySelector('h2');
if (h2) {
h2.id = sec.id;
// Numbered marker
const num = document.createElement('span');
num.className = 'h2-num';
num.textContent = String(secNum).padStart(2, '0');
const txt = document.createElement('span');
txt.className = 'h2-text';
while (h2.firstChild) txt.appendChild(h2.firstChild);
h2.append(num, txt);
// Tier class for minimal mode + section coloring
if (h2.querySelector('.pill.p1')) wrap.classList.add('tier-p1');
else wrap.classList.add('tier-p0');
// Hover copy-as-prompt
const actions = document.createElement('div');
actions.className = 'section-actions';
const btn = document.createElement('button');
btn.className = 'btn';
btn.textContent = 'Copy section';
btn.addEventListener('click', () => {
navigator.clipboard.writeText(sectionMd).then(() => toast('Copied: ' + sec.heading.replace(/`.*`/g, '').trim()));
});
actions.appendChild(btn);
h2.insertAdjacentElement('afterend', actions);
}
// Wrap distribution-shaped tables for nicer styling
if (distributionRegex.test(sec.heading || '')) {
wrap.querySelectorAll('table').forEach(t => {
const w = document.createElement('div');
w.className = 'distribution-table';
t.parentNode.insertBefore(w, t);
w.appendChild(t);
});
}
main.appendChild(wrap);
// TOC entry
const li = document.createElement('li');
if (wrap.classList.contains('tier-p1')) li.className = 'tier-p1';
const a = document.createElement('a');
a.href = '#' + sec.id;
const cleanHeading = sec.heading.replace(/`P[01]`|`LOCKED`|`DONE`/g, '').trim();
a.innerHTML = `${String(secNum).padStart(2, '0')}${escapeHtml(cleanHeading)}`;
li.appendChild(a);
tocList.appendChild(li);
}
// ---- Render Mermaid (wait for ESM import)
function renderMermaid() {
if (!window.__mermaid) return;
window.__mermaid.run({ querySelector: '.mermaid' }).catch(e => console.warn('mermaid', e));
}
if (window.__mermaid) renderMermaid();
else {
const t = setInterval(() => { if (window.__mermaid) { clearInterval(t); renderMermaid(); } }, 50);
}
// ---- Scroll-spy
const tocLinks = $$('aside.toc a');
const headingEls = $$('main section[id]');
function onScroll() {
let active = null;
const offset = 120;
for (const el of headingEls) {
if (el.getBoundingClientRect().top - offset <= 0) active = el.id;
}
tocLinks.forEach(a => a.classList.toggle('active', a.getAttribute('href') === '#' + active));
}
window.addEventListener('scroll', onScroll, { passive: true });
onScroll();
// ---- Toast
function toast(msg) {
const t = $('#toast'); t.textContent = msg; t.classList.add('show');
clearTimeout(toast._t); toast._t = setTimeout(() => t.classList.remove('show'), 1600);
}
// ---- Theme
function setTheme(t) {
document.documentElement.setAttribute('data-theme', t);
localStorage.setItem('prd-theme', t);
if (window.__mermaid) {
window.__mermaid.initialize({ startOnLoad: false, theme: t === 'light' ? 'default' : 'dark' });
$$('.mermaid').forEach(el => { el.removeAttribute('data-processed'); });
window.__mermaid.run({ querySelector: '.mermaid' }).catch(()=>{});
}
}
const savedTheme = localStorage.getItem('prd-theme');
if (savedTheme) setTheme(savedTheme);
$('#toggle-theme').addEventListener('click', () => {
setTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark');
});
// ---- Minimal mode
function setMinimal(on) {
document.body.classList.toggle('minimal', on);
$('#toggle-minimal').textContent = on ? 'All' : 'P0';
}
if (new URLSearchParams(location.search).get('minimal') === '1') setMinimal(true);
$('#toggle-minimal').addEventListener('click', () => {
setMinimal(!document.body.classList.contains('minimal'));
});
// ---- Copy CLAUDE.md block
$('#copy-claudemd').addEventListener('click', () => {
const target = sections.find(s => /CLAUDE\.md|agent\s*context/i.test(s.heading || ''));
if (!target) { toast('No CLAUDE.md block found'); return; }
const body = target.body.join('\n');
const fence = body.match(/```(?:markdown)?\n([\s\S]*?)\n```/);
const text = fence ? fence[1] : body;
navigator.clipboard.writeText(text).then(() => toast('CLAUDE.md block copied'));
});
})();