diff --git a/.project/designs/app/diag-lab.mjs b/.project/designs/app/diag-lab.mjs
new file mode 100644
index 00000000..0b3fd418
--- /dev/null
+++ b/.project/designs/app/diag-lab.mjs
@@ -0,0 +1,57 @@
+#!/usr/bin/env node
+// Diagnose /world-gen/lab — capture console, page errors, canvas presence.
+// Uses playwright from the guide's node_modules (already installed).
+import { chromium } from '../../../public/games/age-of-dwarves/guide/node_modules/@playwright/test/index.mjs';
+
+const BASE = process.env.BASE_URL || 'http://localhost:7777';
+const TARGET = process.argv[2] || '/world-gen/lab';
+const URL = `${BASE}${TARGET}`;
+
+const browser = await chromium.launch();
+const page = await browser.newPage();
+const errors = [];
+const consoleErrors = [];
+
+page.on('pageerror', (err) => errors.push(`PAGE_ERROR: ${err.message}\n${err.stack ?? ''}`));
+page.on('console', (msg) => {
+ if (msg.type() === 'error' || msg.type() === 'warning') {
+ consoleErrors.push(`${msg.type().toUpperCase()}: ${msg.text()}`);
+ }
+});
+
+console.log(`navigating to ${URL}`);
+await page.goto(URL, { waitUntil: 'networkidle', timeout: 30_000 });
+
+// Give React a moment to render
+await page.waitForTimeout(1500);
+
+// Snapshot
+const title = await page.title();
+const rootHTML = await page.locator('#root').innerHTML();
+const canvasCount = await page.locator('canvas').count();
+const errorOverlay = await page.locator('vite-error-overlay').count();
+const sliderCount = await page.locator('input[type="range"]').count();
+const buttonCount = await page.locator('button').count();
+const bodyText = (await page.locator('body').innerText()).slice(0, 600);
+
+console.log(`\n=== ${URL} ===`);
+console.log(`title: ${title}`);
+console.log(`root.innerHTML length: ${rootHTML.length}`);
+console.log(`canvas count: ${canvasCount}`);
+console.log(`vite-error-overlay: ${errorOverlay > 0 ? 'PRESENT' : 'absent'}`);
+console.log(`: ${sliderCount}`);
+console.log(`