- Move docs/superpowers/{plans,specs}/ → docs/{plans,specs}/
- Add 4 previously untracked implementation plans to git
- Update CLAUDE.md with docs path overrides for superpowers skills
- Update HANDBOOK.md repo structure and workflow paths
- Add per-enemy dice rolls to ROADMAP planned section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
37 KiB
The Brochure — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build a single-page showcase site (site/) for Darkwatch with a dark hero section, parchment feature rows with Playwright-generated screenshots, a system support strip, and a publisher contact section.
Architecture: Plain HTML/CSS/JS with no build step or framework. A Playwright Node script (site/screenshots.js) logs into the running dev app and captures feature screenshots automatically. A update-brochure Claude skill keeps the site current as features change.
Tech Stack: HTML · CSS · Vanilla JS · Playwright (Node, screenshot script only)
File Map
| Action | Path | Purpose |
|---|---|---|
| Create | site/index.html |
The page — all sections |
| Create | site/style.css |
All styles — CSS variables, fonts, layout |
| Create | site/main.js |
Smooth scroll + hero parallax |
| Create | site/assets/screenshots/.gitkeep |
Ensures directory exists in git |
| Create | site/package.json |
Playwright dependency for screenshot script |
| Create | site/screenshots.js |
Playwright script — captures all feature screenshots |
| Create | .claude/skills/update-brochure.md |
Maintenance skill |
| Modify | CLAUDE.md |
Register update-brochure skill |
| Modify | .gitignore |
Ignore site/node_modules/ |
Task 1: CSS foundation
Files:
-
Create:
site/style.css -
Create:
site/assets/screenshots/.gitkeep -
Step 1: Create
site/assets/screenshots/.gitkeep
mkdir -p site/assets/screenshots
touch site/assets/screenshots/.gitkeep
- Step 2: Create
site/style.css
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Alegreya:ital,wght@0,400;0,500;1,400&display=swap');
/* ── Variables ─────────────────────────────────────────── */
:root {
--gold: #c9a84c;
--gold-dark: #8b6914;
--gold-dim: rgba(201, 168, 76, 0.12);
--gold-border: rgba(201, 168, 76, 0.3);
--bg-dark: #0d0a05;
--bg-dark-2: #1a1408;
--bg-parchment: #f5f0e8;
--bg-parchment-alt: #ede8d5;
--border-dark: #2a2010;
--border-parchment: #d4c9a8;
--border-light: #e0d5b5;
--text-gold: #c9a84c;
--text-parchment: #e8dcc8;
--text-dark: #2a1f0a;
--text-muted: #6a5a3a;
--text-label: #8b6914;
--font-display: 'Cinzel', serif;
--font-body: 'Alegreya', serif;
}
/* ── Reset ──────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body { font-family: var(--font-body); background: var(--bg-dark); color: var(--text-parchment); }
img { display: block; max-width: 100%; height: auto; border-radius: 6px; }
a { text-decoration: none; }
/* ── Hero ───────────────────────────────────────────────── */
.hero {
min-height: 100vh;
background: var(--bg-dark);
border-bottom: 2px solid var(--border-dark);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 80px 24px;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse 80% 60% at 50% 40%, rgba(201,168,76,0.07) 0%, transparent 70%);
pointer-events: none;
}
.hero-content { position: relative; max-width: 680px; }
.wordmark {
font-family: var(--font-display);
font-size: 11px;
letter-spacing: 4px;
text-transform: uppercase;
color: var(--gold);
margin-bottom: 28px;
opacity: 0.9;
}
.hero-headline {
font-family: var(--font-display);
font-size: clamp(2.2rem, 5vw, 3.6rem);
font-weight: 700;
color: var(--text-parchment);
line-height: 1.2;
margin-bottom: 20px;
text-shadow: 0 0 40px rgba(201,168,76,0.15);
}
.hero-subhead {
font-family: var(--font-body);
font-size: 1.15rem;
color: #9a8a6a;
line-height: 1.65;
margin-bottom: 40px;
max-width: 520px;
margin-left: auto;
margin-right: auto;
}
.hero-actions {
display: flex;
align-items: center;
justify-content: center;
gap: 28px;
flex-wrap: wrap;
}
.btn-primary {
font-family: var(--font-display);
font-size: 13px;
letter-spacing: 2px;
text-transform: uppercase;
background: var(--gold);
color: var(--bg-dark);
padding: 14px 32px;
border-radius: 3px;
font-weight: 700;
transition: background 0.2s, transform 0.1s;
}
.btn-primary:hover { background: #e0bc60; transform: translateY(-1px); }
.scroll-hint {
font-family: var(--font-body);
font-size: 0.95rem;
color: var(--text-muted);
transition: color 0.2s;
}
.scroll-hint:hover { color: var(--gold); }
/* ── Features ───────────────────────────────────────────── */
.features { background: var(--bg-parchment); }
.features-intro {
padding: 64px 24px 40px;
text-align: center;
border-bottom: 1px solid var(--border-parchment);
}
.section-label {
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 3px;
text-transform: uppercase;
color: var(--text-label);
margin-bottom: 10px;
}
.features-intro h2 {
font-family: var(--font-display);
font-size: clamp(1.4rem, 3vw, 2rem);
color: var(--text-dark);
font-weight: 600;
}
.feature-row {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: 0;
border-bottom: 1px solid var(--border-light);
background: var(--bg-parchment);
}
.feature-row:nth-child(even) { background: var(--bg-parchment-alt); }
.feature-image {
padding: 48px 40px;
display: flex;
align-items: center;
justify-content: center;
}
.feature-image img {
box-shadow: 0 8px 32px rgba(42,31,10,0.18);
border: 1px solid var(--border-parchment);
}
.feature-text {
padding: 48px 56px 48px 24px;
}
.feature-row.reverse .feature-image { order: 2; }
.feature-row.reverse .feature-text { order: 1; padding: 48px 24px 48px 56px; }
.feature-label {
font-family: var(--font-display);
font-size: 10px;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text-label);
margin-bottom: 10px;
}
.feature-heading {
font-family: var(--font-display);
font-size: 1.5rem;
font-weight: 600;
color: var(--text-dark);
margin-bottom: 14px;
line-height: 1.3;
}
.feature-desc {
font-family: var(--font-body);
font-size: 1.05rem;
color: #4a3a1a;
line-height: 1.7;
}
/* Torch & Luck callout cards */
.callout-row {
padding: 56px 40px;
background: var(--bg-parchment-alt);
border-bottom: 1px solid var(--border-light);
display: flex;
gap: 32px;
justify-content: center;
}
.callout-card {
background: var(--bg-parchment);
border: 1px solid var(--border-parchment);
border-radius: 6px;
padding: 32px 36px;
max-width: 320px;
text-align: center;
}
.callout-icon { font-size: 2rem; margin-bottom: 12px; }
.callout-card h3 {
font-family: var(--font-display);
font-size: 1rem;
font-weight: 600;
color: var(--text-dark);
margin-bottom: 8px;
}
.callout-card p {
font-family: var(--font-body);
font-size: 0.95rem;
color: #4a3a1a;
line-height: 1.6;
}
/* ── System Support ─────────────────────────────────────── */
.system-support {
background: var(--bg-dark-2);
border-top: 2px solid var(--border-dark);
border-bottom: 2px solid var(--border-dark);
padding: 48px 24px;
text-align: center;
}
.system-support .section-label { color: var(--gold); opacity: 0.7; }
.system-support h2 {
font-family: var(--font-display);
font-size: 1.4rem;
color: var(--text-parchment);
font-weight: 600;
margin-bottom: 10px;
}
.system-support p {
font-family: var(--font-body);
color: var(--text-muted);
font-size: 1rem;
}
/* ── Publisher ──────────────────────────────────────────── */
.publisher {
background: var(--bg-parchment);
border-bottom: 1px solid var(--border-parchment);
padding: 80px 24px;
text-align: center;
}
.publisher .section-label { margin-bottom: 14px; }
.publisher h2 {
font-family: var(--font-display);
font-size: clamp(1.4rem, 3vw, 2rem);
color: var(--text-dark);
font-weight: 600;
margin-bottom: 20px;
}
.publisher p {
font-family: var(--font-body);
font-size: 1.1rem;
color: #4a3a1a;
line-height: 1.75;
max-width: 520px;
margin: 0 auto 32px;
}
.btn-secondary {
font-family: var(--font-display);
font-size: 12px;
letter-spacing: 2px;
text-transform: uppercase;
background: var(--bg-dark);
color: var(--gold);
padding: 13px 30px;
border-radius: 3px;
font-weight: 600;
border: 1px solid var(--border-dark);
transition: background 0.2s;
display: inline-block;
}
.btn-secondary:hover { background: var(--bg-dark-2); }
/* ── Footer ─────────────────────────────────────────────── */
.footer {
background: var(--bg-dark);
border-top: 1px solid var(--border-dark);
padding: 28px 40px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.footer-left {
font-family: var(--font-display);
font-size: 11px;
letter-spacing: 1px;
color: #4a3a1a;
}
.footer-right { display: flex; gap: 20px; }
.footer-right a {
font-family: var(--font-display);
font-size: 11px;
letter-spacing: 1px;
color: #4a3a1a;
transition: color 0.2s;
}
.footer-right a:hover { color: var(--gold); }
/* ── Responsive ─────────────────────────────────────────── */
@media (max-width: 768px) {
.feature-row,
.feature-row.reverse {
grid-template-columns: 1fr;
}
.feature-row.reverse .feature-image { order: 0; }
.feature-row.reverse .feature-text { order: 0; }
.feature-image { padding: 32px 24px 0; }
.feature-text,
.feature-row.reverse .feature-text { padding: 24px 24px 40px; }
.callout-row { flex-direction: column; align-items: center; }
.footer { justify-content: center; text-align: center; }
}
- Step 3: Verify fonts load
open site/index.html # will 404 — we'll create it in Task 2. Just confirm style.css has no syntax errors:
node -e "require('fs').readFileSync('site/style.css', 'utf8'); console.log('OK')"
Expected: OK
- Step 4: Commit
git add site/style.css site/assets/screenshots/.gitkeep
git commit -m "feat: add brochure CSS foundation and screenshot directory"
Task 2: Hero section
Files:
-
Create:
site/index.html -
Step 1: Create
site/index.htmlwith skeleton + hero
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Darkwatch — Real-Time Shadowdark Companion</title>
<meta name="description" content="A real-time session companion for Shadowdark RPG. Characters, dice, spells, and atmosphere — synced live across every device at the table.">
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- ── Hero ─────────────────────────────────────────── -->
<section class="hero" id="top">
<div class="hero-content">
<div class="wordmark">◈ Darkwatch</div>
<h1 class="hero-headline">Your whole party.<br>One screen. No paper.</h1>
<p class="hero-subhead">
A real-time session companion for Shadowdark RPG. Characters, dice, spells,
and atmosphere — synced live across every device at the table.
</p>
<div class="hero-actions">
<a href="https://discord.gg/PLACEHOLDER" class="btn-primary">Join the Beta</a>
<a href="#features" class="scroll-hint">↓ See what it does</a>
</div>
</div>
</section>
<!-- features, system support, publisher, footer go here in later tasks -->
<script src="main.js"></script>
</body>
</html>
- Step 2: Open in browser and verify hero
open site/index.html
Expected: dark background, gold wordmark, large serif headline, parchment-colored subhead, gold CTA button. Fonts may not load from file:// — if Cinzel/Alegreya don't appear, use a local server:
npx serve site -l 4000
# then open http://localhost:4000
- Step 3: Commit
git add site/index.html
git commit -m "feat: add brochure hero section"
Task 3: Feature rows
Files:
-
Modify:
site/index.html -
Step 1: Add features intro and all 7 feature rows
Replace <!-- features, system support, publisher, footer go here in later tasks --> with:
<!-- ── Features ──────────────────────────────────────── -->
<section class="features" id="features">
<div class="features-intro">
<div class="section-label">Everything at the table</div>
<h2>Built for Shadowdark. Designed for the whole party.</h2>
</div>
<!-- Row 1: screenshot left, text right -->
<div class="feature-row">
<div class="feature-image">
<img src="assets/screenshots/character-sheet.png" alt="Darkwatch character sheet showing stats, gear, and talents">
</div>
<div class="feature-text">
<div class="feature-label">Characters</div>
<h2 class="feature-heading">Every stat, always in sync.</h2>
<p class="feature-desc">
Full Shadowdark character sheets — STR, DEX, CON, INT, WIS, CHA, HP, AC, XP,
gear, and talents. Click any value to edit it. Changes sync to the DM and every
player at the table in real time.
</p>
</div>
</div>
<!-- Row 2: text left, screenshot right -->
<div class="feature-row reverse">
<div class="feature-image">
<img src="assets/screenshots/character-creation.png" alt="Character creation wizard showing 3d6 stat rolling">
</div>
<div class="feature-text">
<div class="feature-label">Character Creation</div>
<h2 class="feature-heading">Roll to begin.</h2>
<p class="feature-desc">
A guided four-step wizard: name, class, and ancestry — then roll 3d6 for each
stat and watch them land. Background, alignment, and deity last. HP, gear slots,
gold, and your title are all derived automatically.
</p>
</div>
</div>
<!-- Row 3: screenshot left, text right -->
<div class="feature-row">
<div class="feature-image">
<img src="assets/screenshots/dice-roll.png" alt="3D dice rolling with character-colored dice and shared roll log">
</div>
<div class="feature-text">
<div class="feature-label">Dice Rolling</div>
<h2 class="feature-heading">3D dice. Shared results. No hiding rolls.</h2>
<p class="feature-desc">
Dice roll in 3D, in your character's color. Results are determined server-side —
no one can fudge them. Every roll appears in a shared log that the whole table
sees at the same time.
</p>
</div>
</div>
<!-- Row 4: text left, screenshot right -->
<div class="feature-row reverse">
<div class="feature-image">
<img src="assets/screenshots/spellcasting.png" alt="Spellcasting panel with spell slots and exhaustion tracking">
</div>
<div class="feature-text">
<div class="feature-label">Spellcasting</div>
<h2 class="feature-heading">Cast, fail, and suffer — together.</h2>
<p class="feature-desc">
34 Tier 1–2 spells for Wizard and Priest. Every cast rolls a d20 — fail and
lose a spell slot. Wizards risk the mishap table. Priests do penance. Rest to
recover. The DM sees your spell focus state at a glance.
</p>
</div>
</div>
<!-- Row 5: screenshot left, text right -->
<div class="feature-row">
<div class="feature-image">
<img src="assets/screenshots/atmosphere.png" alt="Atmospheric fog effect covering the campaign view">
</div>
<div class="feature-text">
<div class="feature-label">Atmosphere</div>
<h2 class="feature-heading">Set the scene.</h2>
<p class="feature-desc">
The DM controls atmosphere effects that every player sees in real time:
creeping fog, crackling fire, rain, or drifting embers. One click and the
whole table feels the dungeon shift.
</p>
</div>
</div>
<!-- Row 6: text left, screenshot right -->
<div class="feature-row reverse">
<div class="feature-image">
<img src="assets/screenshots/initiative-active.png" alt="Initiative tracker showing party vs enemies with round counter">
</div>
<div class="feature-text">
<div class="feature-label">Initiative Tracker</div>
<h2 class="feature-heading">Shadowdark initiative, handled.</h2>
<p class="feature-desc">
Everyone rolls. The tracker takes the party's highest result against the DM's
single enemy roll. Whoever wins goes first — then the DM clicks Next Turn and
the round counter climbs. Enemy HP is visible to the DM only.
</p>
</div>
</div>
<!-- Row 7: screenshot left, text right -->
<div class="feature-row">
<div class="feature-image">
<img src="assets/screenshots/dm-cards.png" alt="DM view showing compact character cards for the whole party">
</div>
<div class="feature-text">
<div class="feature-label">DM View</div>
<h2 class="feature-heading">The whole party, at a glance.</h2>
<p class="feature-desc">
The DM sees every character in a compact grid: HP, AC, luck token, torch timer,
and all six stat modifiers — without opening a single sheet. Drill in when you
need to, stay out when you don't.
</p>
</div>
</div>
<!-- Torch + Luck callout cards (no screenshot) -->
<div class="callout-row">
<div class="callout-card">
<div class="callout-icon">🕯</div>
<h3>Torch Timer</h3>
<p>A real 60-minute countdown, synced across every device. The DM starts it when
the torch is lit. Everyone watches it burn.</p>
</div>
<div class="callout-card">
<div class="callout-icon">✦</div>
<h3>Luck Tokens</h3>
<p>Each character has a luck token the DM and player can both see and toggle.
No more forgetting who spent their luck last session.</p>
</div>
</div>
</section>
<!-- system support, publisher, footer go here in Task 4 -->
- Step 2: Open in browser and verify feature rows
npx serve site -l 4000
# open http://localhost:4000
Expected: seven alternating rows with broken image icons (screenshots don't exist yet) and text descriptions. Parchment background. Callout cards at the bottom. Layout alternates left/right correctly.
- Step 3: Commit
git add site/index.html
git commit -m "feat: add brochure feature rows"
Task 4: Lower sections — system support, publisher, footer
Files:
-
Modify:
site/index.html -
Step 1: Replace the
<!-- system support... -->comment with the remaining sections
Replace <!-- system support, publisher, footer go here in Task 4 --> with:
<!-- ── System Support ────────────────────────────────── -->
<section class="system-support">
<div class="section-label">System Support</div>
<h2>Built for Shadowdark RPG. More systems coming.</h2>
<p>Currently: Shadowdark RPG · Next: based on demand</p>
</section>
<!-- ── Publisher ─────────────────────────────────────── -->
<section class="publisher">
<div class="section-label">For game publishers</div>
<h2>"We're not here to sell your rules."</h2>
<p>
Darkwatch is a session companion — a torch timer, a dice roller, an initiative
tracker. Your players still need the book. We just want to make the table more
fun. If you'd like to talk, we'd love to.
</p>
<a href="mailto:email@email.com" class="btn-secondary">Get in touch</a>
</section>
<!-- ── Footer ────────────────────────────────────────── -->
<footer class="footer">
<div class="footer-left">Darkwatch · Built for the table</div>
<div class="footer-right">
<a href="https://discord.gg/PLACEHOLDER">Discord</a>
<a href="https://github.com/PLACEHOLDER/darkwatch">GitHub</a>
</div>
</footer>
- Step 2: Open in browser and verify complete page
npx serve site -l 4000
# open http://localhost:4000
Scroll through the full page. Expected:
-
Dark hero → parchment features → dark system strip → parchment publisher → dark footer
-
Publisher section has the "Get in touch" button
-
Footer has Discord and GitHub links (both currently
PLACEHOLDER) -
Step 3: Commit
git add site/index.html
git commit -m "feat: add brochure system support, publisher, and footer sections"
Task 5: main.js — smooth scroll and hero parallax
Files:
-
Create:
site/main.js -
Step 1: Create
site/main.js
// Smooth scroll for anchor links (CSS scroll-behavior covers most cases,
// this handles the ↓ scroll-hint click on browsers that need it)
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', e => {
const target = document.querySelector(anchor.getAttribute('href'));
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
// Subtle parallax on hero background glow
const hero = document.querySelector('.hero');
if (hero) {
window.addEventListener('scroll', () => {
const scrolled = window.scrollY;
const heroHeight = hero.offsetHeight;
if (scrolled < heroHeight) {
// Move the pseudo-element glow upward slightly as user scrolls
hero.style.setProperty('--parallax-offset', `${scrolled * 0.3}px`);
}
}, { passive: true });
}
- Step 2: Wire the parallax offset into CSS
In site/style.css, update the .hero::before rule to use the variable:
Find:
.hero::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse 80% 60% at 50% 40%, rgba(201,168,76,0.07) 0%, transparent 70%);
pointer-events: none;
}
Replace with:
.hero::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse 80% 60% at 50% calc(40% - var(--parallax-offset, 0px)), rgba(201,168,76,0.07) 0%, transparent 70%);
pointer-events: none;
}
- Step 3: Verify in browser
npx serve site -l 4000
# open http://localhost:4000
Scroll slowly down from the hero. Expected: the gold glow in the hero drifts upward subtly as you scroll. Effect is gentle — if it's too strong, reduce 0.3 multiplier in main.js.
- Step 4: Commit
git add site/main.js site/style.css
git commit -m "feat: add smooth scroll and hero parallax"
Task 6: Playwright screenshot script
Files:
-
Create:
site/package.json -
Create:
site/screenshots.js -
Modify:
.gitignore(addsite/node_modules/) -
Step 1: Add
site/node_modules/to .gitignore
Append to .gitignore:
site/node_modules/
- Step 2: Create
site/package.json
{
"name": "darkwatch-brochure-screenshots",
"type": "module",
"scripts": {
"screenshots": "node screenshots.js",
"screenshots:only": "node screenshots.js --only"
},
"devDependencies": {
"playwright": "^1.43.0"
}
}
- Step 3: Install Playwright and its Chromium browser
cd site
npm install
npx playwright install chromium
cd ..
Expected: node_modules/ created in site/, Chromium downloaded.
- Step 4: Create
site/screenshots.js
import { chromium } from 'playwright';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
import { mkdirSync } from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const SHOT_DIR = resolve(__dirname, 'assets', 'screenshots');
const APP = 'http://localhost:5173';
const DM_EMAIL = 'dm@darkwatch.test';
const DM_PASS = 'password';
const ONLY = process.argv.includes('--only')
? process.argv[process.argv.indexOf('--only') + 1]
: null;
mkdirSync(SHOT_DIR, { recursive: true });
// ── helpers ────────────────────────────────────────────────────────────────
async function shot(page, name, fn) {
if (ONLY && name !== ONLY) return;
console.log(` capturing ${name}...`);
await fn(page);
await page.screenshot({
path: resolve(SHOT_DIR, `${name}.png`),
fullPage: false,
});
console.log(` ✓ ${name}.png`);
}
async function login(page) {
await page.goto(`${APP}/login`);
await page.fill('input[type="email"]', DM_EMAIL);
await page.fill('input[type="password"]', DM_PASS);
await page.click('button[type="submit"]');
await page.waitForURL(`${APP}/`);
}
async function openCampaign(page) {
// Seeded campaign is named "Tomb of the Serpent King"
await page.click('text=Tomb of the Serpent King');
await page.waitForURL(/\/campaign\/\d+/);
await page.waitForTimeout(1500); // socket connect + data load
}
// ── captures ───────────────────────────────────────────────────────────────
async function captureCharacterSheet(page) {
// DM view — click the first character card to open full sheet
// Selector: first .characterCard element in the card grid
// Adjust class name if different in the actual DOM
const card = page.locator('[class*="characterCard"]').first();
await card.click();
await page.waitForTimeout(500);
}
async function captureCharacterCreation(page) {
// Open character creation wizard and advance to step 2 (stat rolling)
await page.click('button:has-text("New Character")');
await page.waitForTimeout(400);
// Fill required fields on step 1 so Next is enabled
const nameInput = page.locator('input[placeholder*="name" i]').first();
await nameInput.fill('Screenshot Hero');
// Advance to step 2
await page.click('button:has-text("Next")');
await page.waitForTimeout(400);
}
async function captureDiceRoll(page) {
// Roll a d20 — find the d20 button in the dice panel
await page.click('button:has-text("d20")');
await page.waitForTimeout(2000); // wait for 3D animation to settle
}
async function captureSpellcasting(page) {
// Open the Spells tab/panel for the first Wizard or Priest character
// The spell panel may be a tab within the character sheet
await page.click('text=Spells');
await page.waitForTimeout(400);
}
async function captureAtmosphere(page) {
// Open atmosphere panel and enable fog
await page.click('button[title*="tmosphere" i], button:has-text("Atmosphere")');
await page.waitForTimeout(300);
await page.click('button:has-text("Fog")');
await page.waitForTimeout(600); // wait for fog to render
}
async function captureInitiativeActive(page) {
// Click the Combat button to open the start combat modal
await page.click('button:has-text("Combat")');
await page.waitForTimeout(300);
// Start combat with whatever defaults are present
await page.click('button:has-text("Start Combat")');
await page.waitForTimeout(500);
// Roll initiative for enemies (DM roll)
const rollBtn = page.locator('button:has-text("Roll")').first();
await rollBtn.click();
await page.waitForTimeout(400);
}
async function captureDmCards(page) {
// The DM compact card grid is the default campaign view.
// Navigate back to campaign root to ensure no modals are open.
const url = page.url();
await page.goto(url);
await page.waitForTimeout(1500); // let socket reconnect
}
// ── main ───────────────────────────────────────────────────────────────────
async function run() {
console.log('Starting Darkwatch screenshot capture...');
console.log(`App: ${APP}`);
if (ONLY) console.log(`Only: ${ONLY}`);
console.log('');
const browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 1280, height: 800 },
});
const page = await context.newPage();
await login(page);
console.log('✓ Logged in as DM');
await openCampaign(page);
console.log('✓ Campaign open');
console.log('');
await shot(page, 'dm-cards', captureDmCards);
await shot(page, 'character-sheet', captureCharacterSheet);
await shot(page, 'character-creation',captureCharacterCreation);
await shot(page, 'dice-roll', captureDiceRoll);
await shot(page, 'spellcasting', captureSpellcasting);
await shot(page, 'atmosphere', captureAtmosphere);
await shot(page, 'initiative-active', captureInitiativeActive);
await browser.close();
console.log('');
console.log('Done! Screenshots saved to site/assets/screenshots/');
}
run().catch(err => {
console.error(err);
process.exit(1);
});
- Step 5: Run the script against the running dev server
Ensure the dev server is running first (npm run dev in server/ and client/). Then:
cd site
node screenshots.js
Expected output:
Starting Darkwatch screenshot capture...
App: http://localhost:5173
✓ Logged in as DM
✓ Campaign open
capturing dm-cards...
✓ dm-cards.png
capturing character-sheet...
...
Done! Screenshots saved to site/assets/screenshots/
If a capture fails: The most likely cause is a selector mismatch. Open Playwright in headed mode to debug:
// Temporarily change launch() to:
const browser = await chromium.launch({ headless: false, slowMo: 500 });
Use browser DevTools to find the correct selector, update the relevant capture* function, then switch back to headless.
- Step 6: Verify screenshots look good
open site/assets/screenshots/
Open each PNG and verify:
dm-cards.png— compact DM character card grid, 2+ cards visiblecharacter-sheet.png— full character stats visiblecharacter-creation.png— stat rolling step, 3d6 dice area visibledice-roll.png— dice visible (mid-roll or result), roll log visiblespellcasting.png— spell list with slot indicatorsatmosphere.png— fog or fire effect clearly visible over the UIinitiative-active.png— tracker showing two sides, round counter
If any screenshot is blank, too dark, or shows the wrong view, debug with headed mode (Step 5 note) and re-run node screenshots.js --only <name>.
- Step 7: Commit
git add site/package.json site/screenshots.js .gitignore site/assets/screenshots/
git commit -m "feat: add Playwright screenshot script for brochure"
Note: site/node_modules/ is gitignored. The PNG screenshots are committed so the site renders without running the script.
Task 7: update-brochure skill + CLAUDE.md
Files:
-
Create:
.claude/skills/update-brochure.md -
Modify:
CLAUDE.md -
Step 1: Create
.claude/skills/update-brochure.md
# update-brochure
Use this skill when a Darkwatch feature is added, changed, or removed, and the brochure needs updating. It keeps `site/index.html` and `site/screenshots.js` in sync with the app.
## Scope
- `site/index.html` — add, update, or remove feature rows
- `site/screenshots.js` — add, update, or remove capture blocks
- System support strip in `site/index.html` — update when a new RPG system is added
**Not in scope:** Hero copy, publisher section copy, footer links. Those are editorial and manual.
## Process: Feature added
1. Add a new `.feature-row` block to `site/index.html` after the last existing feature row (before the `.callout-row`). Follow the alternating pattern:
- Rows 1, 3, 5, 7 (odd positions): screenshot on the left, text on the right (no `reverse` class)
- Rows 2, 4, 6 (even positions): `class="feature-row reverse"` (screenshot right, text left)
- Count existing rows to determine whether to add `reverse` or not
Template for a new row:
```html
<div class="feature-row">
<div class="feature-image">
<img src="assets/screenshots/FEATURE-NAME.png" alt="Description of screenshot">
</div>
<div class="feature-text">
<div class="feature-label">Short Label</div>
<h2 class="feature-heading">Punchy headline.</h2>
<p class="feature-desc">
Two sentences. Write for players and DMs, not developers.
</p>
</div>
</div>
-
Add a capture block to
site/screenshots.js:- Write a
captureFeatureName(page)async function that navigates to the right UI state - Add
await shot(page, 'feature-name', captureFeatureName);to therun()function, in the same order as the HTML row
- Write a
-
Run the screenshot script for the new capture:
cd site && node screenshots.js --only feature-name -
Commit all three files:
git add site/index.html site/screenshots.js site/assets/screenshots/feature-name.png git commit -m "docs: add [feature name] to brochure"
Process: Feature removed
- Delete the feature's
.feature-rowblock fromsite/index.html - Recheck alternating pattern — renumber remaining rows if needed
- Delete the
capture*function andshot()call fromsite/screenshots.js - Delete the PNG from
site/assets/screenshots/ - Commit
Process: Feature changed (copy update only)
- Update the heading and description in the relevant
.feature-rowinsite/index.html - Re-run the screenshot if the UI changed visually:
cd site && node screenshots.js --only feature-name - Commit
Process: New RPG system added
- Update the system support strip in
site/index.html:<!-- Before (Shadowdark only): --> <p>Currently: Shadowdark RPG · Next: based on demand</p> <!-- After (example with Cairn added): --> <p>Currently: Shadowdark RPG · Cairn · More based on demand</p> - Commit
Tone guidelines
Write feature descriptions for players and DMs, not developers.
- ✓ "Everyone rolls. The tracker takes the party's highest result."
- ✗ "The server computes max(party_rolls) and compares to the enemy roll."
Keep headings punchy and short. Keep descriptions to two sentences max.
- [ ] **Step 2: Read current CLAUDE.md to find the Project Skills table**
```bash
grep -n "update-changelog\|update-handbook\|Project Skills" CLAUDE.md
- Step 3: Add update-brochure to the Project Skills table in CLAUDE.md
Find the existing table:
| `update-changelog` | `.claude/skills/update-changelog.md` | After a feature ships, bug is fixed, or feature is removed |
| `update-handbook` | `.claude/skills/update-handbook.md` | After a feature is added, changed, or removed |
Add a row:
| `update-changelog` | `.claude/skills/update-changelog.md` | After a feature ships, bug is fixed, or feature is removed |
| `update-handbook` | `.claude/skills/update-handbook.md` | After a feature is added, changed, or removed |
| `update-brochure` | `.claude/skills/update-brochure.md` | After a feature is added, changed, or removed — keeps the site current |
- Step 4: Commit
git add .claude/skills/update-brochure.md CLAUDE.md
git commit -m "feat: add update-brochure skill and register in CLAUDE.md"
Self-Review Checklist
After all tasks complete, verify against the spec:
site/index.htmlhas all 5 sections: hero, features (7 rows + callout cards), system support, publisher, footer- Feature rows alternate left/right correctly (1,3,5,7 image-left; 2,4,6 image-right)
- All 7 screenshot
imgtags referenceassets/screenshots/*.png - Hero CTA links to Discord (placeholder is acceptable pre-launch)
- Publisher section uses
mailto:email@email.complaceholder site/screenshots.jshas a named capture function for each of the 7 PNGs--onlyflag works:node screenshots.js --only dm-cardscaptures only that one.claude/skills/update-brochure.mdcovers add, remove, change, and new-system processesCLAUDE.mdlistsupdate-brochurein the Project Skills tablesite/node_modules/is gitignored- Screenshots are committed to
site/assets/screenshots/