darkwatch/docs/plans/2026-04-12-brochure.md
Aaron Wood 7c7bdf2ee5 chore: consolidate docs into flat structure and commit all plans
- 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>
2026-04-11 23:55:45 -04:00

37 KiB
Raw Blame History

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.html with 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 12 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"

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 &nbsp;·&nbsp; 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 &nbsp;·&nbsp; 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 (add site/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 visible
  • character-sheet.png — full character stats visible
  • character-creation.png — stat rolling step, 3d6 dice area visible
  • dice-roll.png — dice visible (mid-roll or result), roll log visible
  • spellcasting.png — spell list with slot indicators
  • atmosphere.png — fog or fire effect clearly visible over the UI
  • initiative-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>
  1. 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 the run() function, in the same order as the HTML row
  2. Run the screenshot script for the new capture:

    cd site && node screenshots.js --only feature-name
    
  3. 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

  1. Delete the feature's .feature-row block from site/index.html
  2. Recheck alternating pattern — renumber remaining rows if needed
  3. Delete the capture* function and shot() call from site/screenshots.js
  4. Delete the PNG from site/assets/screenshots/
  5. Commit

Process: Feature changed (copy update only)

  1. Update the heading and description in the relevant .feature-row in site/index.html
  2. Re-run the screenshot if the UI changed visually:
    cd site && node screenshots.js --only feature-name
    
  3. Commit

Process: New RPG system added

  1. Update the system support strip in site/index.html:
    <!-- Before (Shadowdark only): -->
    <p>Currently: Shadowdark RPG &nbsp;·&nbsp; Next: based on demand</p>
    
    <!-- After (example with Cairn added): -->
    <p>Currently: Shadowdark RPG &nbsp;·&nbsp; Cairn &nbsp;·&nbsp; More based on demand</p>
    
  2. 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.html has 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 img tags reference assets/screenshots/*.png
  • Hero CTA links to Discord (placeholder is acceptable pre-launch)
  • Publisher section uses mailto:email@email.com placeholder
  • site/screenshots.js has a named capture function for each of the 7 PNGs
  • --only flag works: node screenshots.js --only dm-cards captures only that one
  • .claude/skills/update-brochure.md covers add, remove, change, and new-system processes
  • CLAUDE.md lists update-brochure in the Project Skills table
  • site/node_modules/ is gitignored
  • Screenshots are committed to site/assets/screenshots/