diff --git a/.gitignore b/.gitignore
index a961549c..0e38d571 100644
--- a/.gitignore
+++ b/.gitignore
@@ -155,3 +155,6 @@ native/macos/MCPProxy/.build/
.wrangler/
.run
+
+# demo pipeline: playwright node_modules symlink (recreated at capture time)
+scripts/demo/node_modules
diff --git a/README.md b/README.md
index 29015511..bb4f27e2 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,36 @@
-# MCPProxy β Smart Proxy for AI Agents
+# π‘οΈ MCPProxy β Supercharge AI Agents, Safely
-**MCPProxy** is an open-source desktop application that super-charges AI agents with intelligent tool discovery, massive token savings, and built-in security quarantine against malicious MCP servers.
+> One safe endpoint in front of every MCP server.
-### **π [Read the Documentation](https://docs.mcpproxy.app/)**
+[](https://github.com/smart-mcp-proxy/mcpproxy-go/releases)
+[](https://github.com/smart-mcp-proxy/mcpproxy-go/actions/workflows/unit-tests.yml)
+[](https://goreportcard.com/report/github.com/smart-mcp-proxy/mcpproxy-go)
+[](https://pkg.go.dev/github.com/smart-mcp-proxy/mcpproxy-go)
+[](LICENSE)
+[](https://github.com/smart-mcp-proxy/mcpproxy-go/stargazers)
-
-[](https://youtu.be/2aKrgJnbbcw)
+
+
+
-π Visit mcpproxy.app
+
+
+
+
+
+ πΊ Watch the full walkthrough Β·
+ π Read the docs Β·
+ π mcpproxy.app
+
+
+> The demo above shows the **embedded web UI**. The MCPProxy **core is a single binary for macOS, Linux, and Windows** β the web UI ships inside it, with no extra service to run. On **macOS**, an optional **menuβbar app** adds oneβclick convenience (start/stop, server health, quarantine, logs).
-
-
-
+
-
-
-
+
-
System Tray - Upstream Servers System Tray - Quarantine Management
+
macOS menuβbar app Β· Activity log & audit in the macOS app
@@ -27,7 +39,7 @@
- **Scale beyond API limits** β Federate hundreds of MCP servers while bypassing Cursor's 40-tool limit and OpenAI's 128-function cap.
- **Save tokens & accelerate responses** β Agents load just one `retrieve_tools` function instead of hundreds of schemas. Research shows ~99 % token reduction with **43 % accuracy improvement**.
- **Advanced security protection** β Automatic quarantine blocks Tool Poisoning Attacks until you manually approve new servers.
-- **Works offline & cross-platform** β Native binaries for macOS (Intel & Apple Silicon), Windows (x64 & ARM64), and Linux (x64 & ARM64) with system-tray UI.
+- **Works offline & cross-platform** β A single core binary for macOS (Intel & Apple Silicon), Windows (x64 & ARM64), and Linux (x64 & ARM64), with the **web UI embedded**. macOS additionally ships an optional menu-bar app.
---
diff --git a/docs/demo.gif b/docs/demo.gif
new file mode 100644
index 00000000..56742381
Binary files /dev/null and b/docs/demo.gif differ
diff --git a/docs/logo.svg b/docs/logo.svg
new file mode 100644
index 00000000..fda93e8e
--- /dev/null
+++ b/docs/logo.svg
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+ mcpproxy shield logo
+ Blue shield with MCP circles beneath
+
+
+
+
+
+
+ M
+ C
+ P
+
diff --git a/docs/screenshot-macos-activity.png b/docs/screenshot-macos-activity.png
new file mode 100644
index 00000000..13d3896e
Binary files /dev/null and b/docs/screenshot-macos-activity.png differ
diff --git a/docs/screenshot-macos-tray.png b/docs/screenshot-macos-tray.png
new file mode 100644
index 00000000..bf475dc2
Binary files /dev/null and b/docs/screenshot-macos-tray.png differ
diff --git a/docs/social.html b/docs/social.html
new file mode 100644
index 00000000..65e9d39b
--- /dev/null
+++ b/docs/social.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
MCPProxy
+
Supercharge AI Agents, Safely
+
One safe endpoint in front of every MCP server.
+
+
π Discover
+
π‘οΈ Quarantine
+
π Federate
+
β‘ β99% tokens
+
+
+ mcpproxy.app
+
+
diff --git a/docs/social.png b/docs/social.png
new file mode 100644
index 00000000..2053c3cb
Binary files /dev/null and b/docs/social.png differ
diff --git a/scripts/demo/README.md b/scripts/demo/README.md
new file mode 100644
index 00000000..1cef342d
--- /dev/null
+++ b/scripts/demo/README.md
@@ -0,0 +1,20 @@
+# Demo asset pipeline
+
+Regenerates the README hero banner and the (web-UI-only) demo GIF. Run from repo root.
+
+1. `scripts/demo/render-banner.sh` # docs/social.html (+ docs/logo.svg) -> docs/social.png
+2. Boot a demo mcpproxy and capture the web UI:
+ ```
+ cd scripts/demo
+ ln -sfn ../../e2e/playwright/node_modules ./node_modules
+ MCPPROXY_BASE_URL=http://127.0.0.1:18082 MCPPROXY_API_KEY= \
+ ./node_modules/.bin/playwright test --config=playwright.config.ts
+ ```
+ # produces /tmp/demo-webui//video.webm for the 4 beats
+3. `scripts/demo/build-demo.sh` # stitches the 4 web beats -> docs/demo.gif + docs/demo.webp
+
+The macOS tray menu and native app are shown as **static screenshots** in the README
+(`docs/screenshot-macos-tray.png`, `docs/screenshot-macos-activity.png`), not in the GIF.
+
+All outputs are committed under docs/. social.png is also uploaded manually to
+GitHub Settings -> Social preview (one-time, cannot be scripted).
diff --git a/scripts/demo/build-demo.sh b/scripts/demo/build-demo.sh
new file mode 100755
index 00000000..5e575299
--- /dev/null
+++ b/scripts/demo/build-demo.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+# Stitch the demo GIF from four web-UI video segments (web-UI-only; the macOS tray
+# is shown as a static screenshot in the README instead).
+#
+# Inputs (produced by capture-webui.spec.ts):
+# /tmp/demo-webui/*1-servers*/video.webm server cards / federation
+# /tmp/demo-webui/*2-tools*/video.webm tools / discovery
+# /tmp/demo-webui/*3-activity*/video.webm activity log / audit (detail drawer)
+# /tmp/demo-webui/*4-security*/video.webm quarantine close-up
+# Outputs: docs/demo.gif + docs/demo.webp
+set -euo pipefail
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+WEB=/tmp/demo-webui
+WORK=$(mktemp -d)
+W=860; H=538; FPS=15; BG="#0f172a"; WEBSPEED=1.8 # speed up web segments to fit size budget
+
+# Ordered web segments (the leading number in the test name fixes the order).
+# Portable array fill (avoid mapfile β macOS ships bash 3.2).
+WEBS=()
+for pat in 1-servers 2-tools 3-activity 4-security; do
+ f=$(find "$WEB" -name '*.webm' -path "*$pat*" | head -1)
+ [ -n "$f" ] && WEBS+=("$f")
+done
+[ "${#WEBS[@]}" -eq 4 ] || { echo "expected 4 web videos in $WEB, got ${#WEBS[@]} (run capture-webui.spec.ts)"; exit 1; }
+
+# Segments 0..3 β web videos, scaled to canvas, sped up, no audio
+i=0
+for v in "${WEBS[@]}"; do
+ ffmpeg -y -i "$v" -an \
+ -vf "setpts=PTS/${WEBSPEED},scale=${W}:${H}:force_original_aspect_ratio=decrease,pad=${W}:${H}:(ow-iw)/2:(oh-ih)/2:color=${BG},fps=${FPS},format=yuv420p" \
+ -c:v libx264 -pix_fmt yuv420p "$WORK/seg${i}.mp4"
+ i=$((i+1))
+done
+
+# Concat with the concat FILTER (re-encodes β normalizes SAR/timebase; the concat
+# demuxer with -c copy silently drops mismatched segments).
+ffmpeg -y -i "$WORK/seg0.mp4" -i "$WORK/seg1.mp4" -i "$WORK/seg2.mp4" -i "$WORK/seg3.mp4" \
+ -filter_complex "[0:v]setsar=1,fps=${FPS}[a];[1:v]setsar=1,fps=${FPS}[b];[2:v]setsar=1,fps=${FPS}[c];[3:v]setsar=1,fps=${FPS}[d];[a][b][c][d]concat=n=4:v=1:a=0[v]" \
+ -map "[v]" -c:v libx264 -pix_fmt yuv420p "$WORK/full.mp4"
+
+# Palette-optimized GIF (-threads 1 dodges an ffmpeg 8.0 paletteuse threading bug)
+ffmpeg -y -threads 1 -i "$WORK/full.mp4" -vf "fps=${FPS},scale=${W}:-2:flags=lanczos,palettegen=stats_mode=diff" "$WORK/pal.png"
+ffmpeg -y -threads 1 -i "$WORK/full.mp4" -i "$WORK/pal.png" \
+ -lavfi "fps=${FPS},scale=${W}:-2:flags=lanczos,paletteuse=dither=bayer:bayer_scale=3" "$ROOT/docs/demo.gif"
+
+# WebP (smaller; also autoplays in README). Non-fatal β the GIF is the README embed.
+ffmpeg -y -threads 1 -i "$WORK/full.mp4" -vcodec libwebp -filter:v "fps=${FPS},scale=${W}:-2" \
+ -lossless 0 -compression_level 6 -q:v 55 -loop 0 -an "$ROOT/docs/demo.webp" || \
+ { echo "WARN: webp encode failed (non-fatal); removing partial"; rm -f "$ROOT/docs/demo.webp"; }
+
+echo "Wrote docs/demo.gif ($(du -h "$ROOT/docs/demo.gif" | cut -f1)) and docs/demo.webp ($(du -h "$ROOT/docs/demo.webp" | cut -f1))"
+rm -rf "$WORK"
diff --git a/scripts/demo/capture-tray.md b/scripts/demo/capture-tray.md
new file mode 100644
index 00000000..d4474804
--- /dev/null
+++ b/scripts/demo/capture-tray.md
@@ -0,0 +1,18 @@
+# Tray capture shot-list (mcpproxy-ui-test MCP)
+
+Goal: ~12 still frames of the macOS tray menu showing upstream servers + health,
+to be assembled into a ~5s montage (beat 1 of the demo GIF).
+
+Prereq: MCPProxy tray .app running with 3-4 healthy demo servers configured;
+`mcpproxy-ui-test` MCP connected (bundle id com.smartmcpproxy.mcpproxy.dev).
+
+Capture into /tmp/demo-tray/ as frame-01.png, frame-02.png, ... in this order:
+
+1. frame-01..03: `screenshot_status_bar_menu` β closed β menu opening (3 shots)
+2. frame-04..06: `list_menu_items` then `screenshot_status_bar_menu` with the
+ "Upstream Servers" submenu expanded (servers + green health dots)
+3. frame-07..09: hover/select a single server submenu (its tools count, status)
+4. frame-10..12: `screenshot_status_bar_menu` returning to the top menu (close)
+
+Naming MUST be zero-padded frame-NN.png so ffmpeg globbing is ordered.
+mkdir -p /tmp/demo-tray before capturing.
diff --git a/scripts/demo/capture-webui.spec.ts b/scripts/demo/capture-webui.spec.ts
new file mode 100644
index 00000000..3819c160
--- /dev/null
+++ b/scripts/demo/capture-webui.spec.ts
@@ -0,0 +1,83 @@
+import { test, type Page } from '@playwright/test';
+
+// Records four web-UI walkthroughs as video (saved to outputDir by the `video`
+// config). Navigation + dwell driven, robust to frontend version drift, with a
+// spinner guard so we never film a loading state. Run against a LIVE mcpproxy:
+// MCPPROXY_BASE_URL=http://127.0.0.1:18082 MCPPROXY_API_KEY=... \
+// ./node_modules/.bin/playwright test --config=playwright.config.ts
+
+const KEY = process.env.MCPPROXY_API_KEY || '';
+const q = `?apikey=${KEY}`;
+
+// Seed the API key into localStorage before any page script runs. The frontend
+// reads it there (api.ts) without depending on the ?apikey URL param surviving the
+// SPA router, and it persists across this context's navigations.
+test.beforeEach(async ({ context }) => {
+ await context.addInitScript((key) => {
+ try { localStorage.setItem('mcpproxy-api-key', key); } catch { /* ignore */ }
+ }, KEY);
+});
+
+// Settle: domcontentloaded, dismiss the onboarding wizard + auth modal if present,
+// wait for any spinner to disappear, then dwell. Never networkidle β SSE keeps the
+// network busy forever.
+async function ready(page: Page, dwellMs: number) {
+ await page.waitForLoadState('domcontentloaded');
+ // Dismiss the first-run onboarding wizard if it overlays the page.
+ await page.locator('[data-test="close-wizard"]').click({ timeout: 2500 }).catch(() => {});
+ // Re-submit the key if the auth modal shows (transient validation failure).
+ try {
+ const keyInput = page.locator('input[placeholder*="api key" i]').first();
+ if (await keyInput.isVisible({ timeout: 2000 })) {
+ await keyInput.fill(KEY);
+ await page.getByRole('button', { name: /set key/i }).click({ timeout: 3000 });
+ await keyInput.waitFor({ state: 'detached', timeout: 6000 }).catch(() => {});
+ }
+ } catch { /* no modal */ }
+ await page
+ .locator('.loading, .spinner, [class*="spinner"], [class*="loading"], [role="progressbar"]')
+ .first()
+ .waitFor({ state: 'detached', timeout: 6000 })
+ .catch(() => {});
+ await page.waitForTimeout(dwellMs);
+}
+
+test('1 servers', async ({ page }) => {
+ await page.goto(`/ui/servers${q}`);
+ await ready(page, 3000); // KPI cards (total/connected/quarantined/tools) + server cards
+ await page.mouse.wheel(0, 350);
+ await page.waitForTimeout(1600);
+ await page.mouse.wheel(0, -350);
+ await page.waitForTimeout(700);
+});
+
+test('2 tools discovery', async ({ page }) => {
+ await page.goto(`/ui/tools${q}`);
+ await ready(page, 3000); // 39 tools, approval states β no search interaction
+ await page.mouse.wheel(0, 320);
+ await page.waitForTimeout(1800);
+ await page.mouse.wheel(0, -320);
+ await page.waitForTimeout(700);
+});
+
+test('3 activity log', async ({ page }) => {
+ await page.goto(`/ui/activity${q}`);
+ await ready(page, 2600); // populated: successes + a sensitive-data flag
+ // Open the detail drawer for the newest *Tool Call* (the sensitive-data echo) to
+ // show full event details. Each row's last button is the chevron -> selectActivity().
+ // Targeting a Tool Call (not System Start/Stop) gives a meaningful payload + the
+ // sensitive-data detection.
+ try {
+ const toolRow = page.locator('tbody tr').filter({ hasText: /Tool Call/i }).first();
+ await toolRow.locator('button').last().click({ timeout: 4000 });
+ await page.waitForTimeout(3200); // dwell on the detail drawer
+ } catch {
+ await page.waitForTimeout(2500);
+ }
+});
+
+test('4 security quarantine', async ({ page }) => {
+ await page.goto(`/ui/servers/memory${q}`);
+ await ready(page, 3400); // ServerDetail: 'Security Quarantine' alert + Approve action
+ await page.waitForTimeout(800);
+});
diff --git a/scripts/demo/playwright.config.ts b/scripts/demo/playwright.config.ts
new file mode 100644
index 00000000..84f8e59a
--- /dev/null
+++ b/scripts/demo/playwright.config.ts
@@ -0,0 +1,25 @@
+import { defineConfig } from '@playwright/test';
+
+// Captures the live mcpproxy web UI as video for the README demo GIF.
+// Point baseURL at a running mcpproxy; pass the API key via MCPPROXY_API_KEY.
+export default defineConfig({
+ testDir: '.',
+ timeout: 60000,
+ fullyParallel: false,
+ workers: 1,
+ retries: 0,
+ // The @playwright/test runner manages contexts itself, so use `video` (not the
+ // raw recordVideo context option) and collect the .webm files from outputDir.
+ outputDir: '/tmp/demo-webui',
+ use: {
+ headless: true,
+ viewport: { width: 1440, height: 900 },
+ deviceScaleFactor: 2,
+ baseURL: process.env.MCPPROXY_BASE_URL || 'http://127.0.0.1:8080',
+ launchOptions: {
+ executablePath:
+ '/Users/user/Library/Caches/ms-playwright/chromium-1217/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
+ },
+ video: { mode: 'on', size: { width: 1440, height: 900 } },
+ },
+});
diff --git a/scripts/demo/render-banner.sh b/scripts/demo/render-banner.sh
new file mode 100755
index 00000000..c3a30138
--- /dev/null
+++ b/scripts/demo/render-banner.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+# Render docs/social.html -> docs/social.png at 1280x640 via headless Chromium.
+set -euo pipefail
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+CHROME="/Users/$(whoami)/Library/Caches/ms-playwright/chromium-1217/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
+[ -x "$CHROME" ] || { echo "Chromium 1217 not found at: $CHROME"; exit 1; }
+
+"$CHROME" --headless=new --hide-scrollbars --force-device-scale-factor=2 \
+ --window-size=1280,640 \
+ --screenshot="$ROOT/docs/social.png" \
+ --default-background-color=00000000 \
+ "file://$ROOT/docs/social.html"
+
+echo "Wrote $ROOT/docs/social.png"
diff --git a/specs/051-readme-hero-demo/design.md b/specs/051-readme-hero-demo/design.md
new file mode 100644
index 00000000..d20cfe4a
--- /dev/null
+++ b/specs/051-readme-hero-demo/design.md
@@ -0,0 +1,120 @@
+# Design β README hero revamp + demo asset pipeline
+
+**Spec:** 051-readme-hero-demo
+**Date:** 2026-05-21
+**Status:** Approved (brainstorming) β pending implementation plan
+**Owner:** @Dumbris
+
+## Summary
+
+Replace the top of the repo `README.md` with a CodexBar-style hero: a designed
+full-width banner plus one cohesive animated demo GIF, in the project's
+blueβgreen-on-navy brand palette. Build the assets through a scripted, re-runnable
+pipeline so they can be regenerated rather than hand-crafted once.
+
+This is Phase-1 item #5 from the OSS repo-hygiene work (PR #487 shipped the badge
+row; this completes the hero/demo half that could not be auto-generated).
+
+## Motivation
+
+The repo underperforms the product visually. The README currently opens with a bare
+YouTube thumbnail link and two small externally-hosted tray screenshots. CodexBar
+(steipete) demonstrates the target: emoji H1 + tagline + flat badge row + a designed
+banner + one tight product shot. We add motion (a GUI demo GIF) on top of that polish.
+
+## Key constraint that shapes the design
+
+- The **macOS UI-test MCP** (`mcpproxy-ui-test`) produces **still screenshots**, not
+ video (`screenshot_window`, `screenshot_status_bar_menu`, `click_menu_item`, β¦).
+- **Playwright** *can* record **real video** of the web UI (`recordVideo` on the
+ browser context).
+
+Therefore the "one cohesive GIF" is assembled from two source types:
+true-motion web-UI video segments + a tray frame-montage (stills with synthetic
+pans/cross-fades), stitched with ffmpeg. The whole thing is scriptable and CI-friendly.
+
+## Decisions (locked during brainstorming)
+
+| Topic | Decision |
+|-------|----------|
+| Hero treatment | Option A β designed banner (static), with a demo GIF directly beneath |
+| Banner style | #2 "frosted tiles" |
+| Headline | "Supercharge AI Agents, Safely" |
+| Banner palette | navy `#0f172a`, blue `#3b82f6`/`#60a5fa`, green `#005533`/`#006644` |
+| Banner pills | π Discover Β· π‘οΈ Quarantine Β· π Federate Β· β‘ β99% tokens |
+| Demo structure | One cohesive ~16s GIF (not multiple clips) |
+| Demo surfaces & order | tray menu β web-UI server cards β web-UI activity log |
+| Recording | tray via UI-test MCP (frames); web UI via Playwright (video) |
+
+## README top-of-page structure
+
+Replaces current README lines ~1β25:
+
+```
+# π‘οΈ MCPProxy β Supercharge AI Agents, Safely
+> One safe endpoint in front of every MCP server. (β€8-word tagline)
+[ badge row β already shipped in #487, unchanged ]
+[ FULL-WIDTH HERO BANNER ] docs/social.png, wrapped in
+[ ONE COHESIVE DEMO GIF ] docs/demo.gif
+πΊ Watch the full walkthrough β (existing YouTube link, demoted to one line)
+## Why MCPProxy? (keep existing bold-lead bullets)
+```
+
+- **Remove**: the two `mcpproxy.app/images/menu_*.png` thumbnails (superseded by GIF).
+- **Demote**: bare YouTube thumbnail β a one-line text link under the GIF.
+- **Keep**: everything from "Why MCPProxy?" downward, including the install/config
+ sections and the existing local screenshots used in feature sections.
+
+## Assets
+
+### 1. Hero banner
+- `docs/social.html` β checked-in 1280Γ640 canvas (frosted-tiles design above).
+- `docs/social.png` β rendered from it via headless Chromium (reuse the
+ Playwright/Chromium already installed at `e2e/playwright/node_modules`).
+- Same PNG is uploaded to **GitHub Settings β Social preview** (manual, one-time).
+
+### 2. Demo GIF β storyboard (~16s)
+1. **Tray menu** (~5s) β open menubar menu, show upstream servers with live health,
+ hover a server. Source: UI-test MCP frame sequence β montage.
+2. **Server cards** (~6s) β web UI dashboard, healthy cards, click into a server
+ detail. Source: Playwright `recordVideo` (real motion).
+3. **Activity log** (~5s) β web UI activity stream; a tool call with a sensitive-data
+ flag expands. Source: Playwright `recordVideo`.
+
+Stitch: ffmpeg concat (tray montage + 2 webm segments) β palette-optimized
+`docs/demo.gif` (β€900px wide, 15fps, target <8MB) and optional `docs/demo.webp`.
+
+### 3. Existing static screenshots
+Keep `docs/screenshots/tray-macos/*.png` (4 files) for lower feature sections. No
+re-shoot.
+
+## Production pipeline β `scripts/demo/`
+
+| File | Purpose |
+|------|---------|
+| `render-banner.sh` | Headless-Chromium screenshot of `social.html` β `social.png` |
+| `capture-tray.md` | UI-test MCP shot-list (menu items + order) for the tray montage |
+| `capture-webui.spec.ts` | Playwright spec: boot throwaway mcpproxy (CLAUDE.md UI-test pattern), seed 3β4 demo servers, record the two web-UI segments |
+| `build-demo.sh` | ffmpeg stitch β `demo.gif` + `demo.webp` |
+
+The Playwright spec follows the existing UI-verification pattern documented in
+CLAUDE.md (throwaway data-dir, pinned Chromium 1217, `domcontentloaded` not
+`networkidle`, `data-test` selectors).
+
+## Scope / non-goals
+
+**In scope:** banner (`social.html` + `social.png`), one demo GIF, README hero
+rewrite, scripted `scripts/demo/` pipeline, social-preview PNG.
+
+**Out of scope (this round):** asciinema/terminal GIF; DiΓ‘taxis docs restructure;
+website redesign. The glass-pill motif is available to reuse on the website later.
+
+**Requires the user (cannot be scripted):** uploading `social.png` to GitHub
+Settings β Social preview.
+
+## Success criteria
+
+- README opens with banner + autoplaying demo GIF; renders correctly on github.com.
+- `demo.gif` β€ 8MB, β€ 900px wide, autoplays + loops inline.
+- All four `scripts/demo/` artifacts run end-to-end and regenerate the committed assets.
+- Social-preview PNG produces a correct unfurl on X/Slack/HN (manual verify).
diff --git a/specs/051-readme-hero-demo/plan.md b/specs/051-readme-hero-demo/plan.md
new file mode 100644
index 00000000..2aae00e5
--- /dev/null
+++ b/specs/051-readme-hero-demo/plan.md
@@ -0,0 +1,593 @@
+# README Hero Banner + Demo GIF β 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:** Replace the top of `README.md` with a CodexBar-style designed hero banner plus one cohesive ~16s demo GIF (tray menu β web-UI server cards β activity log), produced by a scripted, re-runnable pipeline.
+
+**Architecture:** A checked-in `docs/social.html` is rendered to `docs/social.png` via headless Chromium. The demo GIF is assembled from a macOS-tray still-frame montage (captured via the `mcpproxy-ui-test` MCP) and two real-motion web-UI video segments (captured via Playwright `recordVideo`), stitched with ffmpeg into `docs/demo.gif`. A `scripts/demo/` folder holds all generators so assets can be regenerated, not hand-made once.
+
+**Tech Stack:** HTML/CSS (banner), headless Chromium (existing Playwright/Chromium at `e2e/playwright/node_modules`, pinned 1217), Playwright/TypeScript (web-UI capture), `mcpproxy-ui-test` MCP (tray capture), ffmpeg (stitch/encode), bash (orchestration).
+
+**Prerequisites (verify before Task 1):**
+- `ffmpeg` available: `which ffmpeg` (install via `brew install ffmpeg` if missing).
+- Built `mcpproxy` binary at repo root (`make build`).
+- The `mcpproxy-ui-test` MCP server built per CLAUDE.md (`/tmp/mcpproxy-ui-test`) and the MCPProxy tray `.app` running, for Task 5 only.
+
+---
+
+## File Structure
+
+| File | Responsibility |
+|------|----------------|
+| `docs/social.html` | The 1280Γ640 banner source (frosted-tiles design) |
+| `docs/social.png` | Rendered banner (committed; also GitHub social preview) |
+| `docs/demo.gif` | Final stitched demo (committed, README embed) |
+| `docs/demo.webp` | Optional smaller variant (committed) |
+| `scripts/demo/render-banner.sh` | social.html β social.png via headless Chromium |
+| `scripts/demo/capture-tray.md` | UI-test MCP shot-list for the tray montage |
+| `scripts/demo/capture-webui.spec.ts` | Playwright spec: boot throwaway mcpproxy, seed demo servers + a flagged activity, record 2 web-UI segments |
+| `scripts/demo/playwright.config.ts` | Pins Chromium 1217, viewport, recordVideo dir |
+| `scripts/demo/build-demo.sh` | ffmpeg: tray montage + 2 webm β demo.gif + demo.webp |
+| `scripts/demo/README.md` | How to regenerate every asset (run order) |
+| `README.md` | Hero rewrite (lines ~1β25) |
+
+---
+
+## Task 1: Scaffold `scripts/demo/` and verify ffmpeg
+
+**Files:**
+- Create: `scripts/demo/README.md`
+
+- [ ] **Step 1: Verify ffmpeg present**
+
+Run: `which ffmpeg && ffmpeg -version | head -1`
+Expected: a path and a version line. If missing: `brew install ffmpeg`, then re-run.
+
+- [ ] **Step 2: Create the folder + run-order doc**
+
+Create `scripts/demo/README.md`:
+
+```markdown
+# Demo asset pipeline
+
+Regenerates the README hero banner and demo GIF. Run from repo root.
+
+1. `scripts/demo/render-banner.sh` # docs/social.html -> docs/social.png
+2. Follow `scripts/demo/capture-tray.md` # produces /tmp/demo-tray/frame-*.png
+3. `cd scripts/demo && ../../e2e/playwright/node_modules/.bin/playwright test capture-webui.spec.ts`
+ # produces /tmp/demo-webui/*.webm
+4. `scripts/demo/build-demo.sh` # stitches -> docs/demo.gif + docs/demo.webp
+
+All outputs are committed under docs/. social.png is also uploaded manually to
+GitHub Settings -> Social preview (one-time, cannot be scripted).
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add scripts/demo/README.md
+git commit -m "docs(051): scaffold demo asset pipeline folder"
+```
+
+---
+
+## Task 2: Build the hero banner source (`docs/social.html`)
+
+**Files:**
+- Create: `docs/social.html`
+
+- [ ] **Step 1: Write the banner HTML**
+
+Create `docs/social.html` (1280Γ640, frosted-tiles design, brand palette):
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
Supercharge AI Agents, Safely
+
One safe endpoint in front of every MCP server.
+
+
π Discover
+
π‘οΈ Quarantine
+
π Federate
+
β‘ β99% tokens
+
+
+ mcpproxy.app
+
+
+```
+
+- [ ] **Step 2: Eyeball it in a browser**
+
+Run: `open docs/social.html`
+Expected: navy banner, gradient-clipped headline "Supercharge AI Agents, Safely", four frosted pills, aurora glow. Window is 1280Γ640.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add docs/social.html
+git commit -m "feat(051): add hero banner source (frosted-tiles social.html)"
+```
+
+---
+
+## Task 3: Render banner β `docs/social.png`
+
+**Files:**
+- Create: `scripts/demo/render-banner.sh`
+- Create (output): `docs/social.png`
+
+- [ ] **Step 1: Write the render script**
+
+Create `scripts/demo/render-banner.sh`:
+
+```bash
+#!/usr/bin/env bash
+# Render docs/social.html -> docs/social.png at 1280x640 via headless Chromium.
+set -euo pipefail
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+CHROME="/Users/$(whoami)/Library/Caches/ms-playwright/chromium-1217/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
+[ -x "$CHROME" ] || { echo "Chromium 1217 not found at: $CHROME"; exit 1; }
+
+"$CHROME" --headless=new --hide-scrollbars --force-device-scale-factor=2 \
+ --window-size=1280,640 \
+ --screenshot="$ROOT/docs/social.png" \
+ --default-background-color=00000000 \
+ "file://$ROOT/docs/social.html"
+
+echo "Wrote $ROOT/docs/social.png"
+```
+
+- [ ] **Step 2: Make executable and run**
+
+Run:
+```bash
+chmod +x scripts/demo/render-banner.sh && scripts/demo/render-banner.sh
+```
+Expected: "Wrote .../docs/social.png".
+
+- [ ] **Step 3: Verify dimensions (retina β 2560Γ1280)**
+
+Run: `sips -g pixelWidth -g pixelHeight docs/social.png`
+Expected: pixelWidth 2560, pixelHeight 1280 (2Γ scale of 1280Γ640). If 1280Γ640, the `--force-device-scale-factor=2` flag was ignored β acceptable but note it.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add scripts/demo/render-banner.sh docs/social.png
+git commit -m "feat(051): render social.png banner via headless chromium"
+```
+
+---
+
+## Task 4: Write the tray capture shot-list
+
+**Files:**
+- Create: `scripts/demo/capture-tray.md`
+
+- [ ] **Step 1: Write the shot-list**
+
+Create `scripts/demo/capture-tray.md`:
+
+```markdown
+# Tray capture shot-list (mcpproxy-ui-test MCP)
+
+Goal: ~12 still frames of the macOS tray menu showing upstream servers + health,
+to be assembled into a ~5s montage (beat 1 of the demo GIF).
+
+Prereq: MCPProxy tray .app running with 3-4 healthy demo servers configured;
+`mcpproxy-ui-test` MCP connected (bundle id com.smartmcpproxy.mcpproxy.dev).
+
+Capture into /tmp/demo-tray/ as frame-01.png, frame-02.png, ... in this order:
+
+1. frame-01..03: `screenshot_status_bar_menu` β closed β menu opening (3 shots)
+2. frame-04..06: `list_menu_items` then `screenshot_status_bar_menu` with the
+ "Upstream Servers" submenu expanded (servers + green health dots)
+3. frame-07..09: hover/select a single server submenu (its tools count, status)
+4. frame-10..12: `screenshot_status_bar_menu` returning to the top menu (close)
+
+Naming MUST be zero-padded frame-NN.png so ffmpeg globbing is ordered.
+mkdir -p /tmp/demo-tray before capturing.
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add scripts/demo/capture-tray.md
+git commit -m "docs(051): add tray capture shot-list for demo montage"
+```
+
+---
+
+## Task 5: Capture tray frames (manual, via UI-test MCP)
+
+**Files:**
+- Create (output, NOT committed): `/tmp/demo-tray/frame-*.png`
+
+> This task runs the `mcpproxy-ui-test` MCP tools. It is interactive and macOS-only.
+> The frames are scratch input to Task 7; they are not committed.
+
+- [ ] **Step 1: Prep demo config + start tray**
+
+Ensure 3β4 demo servers exist in the tray's config (e.g. `github`, `filesystem`,
+`fetch`, `time`) and the `.app` is running per CLAUDE.md tray build steps.
+
+Run: `mkdir -p /tmp/demo-tray`
+
+- [ ] **Step 2: Capture frames per the shot-list**
+
+Use the MCP tools in this order, saving each returned screenshot to
+`/tmp/demo-tray/frame-NN.png`:
+- `check_accessibility` (sanity)
+- `screenshot_status_bar_menu` Γ3 (frames 01β03)
+- `list_menu_items` then `screenshot_status_bar_menu` Γ3 (frames 04β06)
+- `click_menu_item` into a server, `screenshot_status_bar_menu` Γ3 (frames 07β09)
+- `screenshot_status_bar_menu` Γ3 (frames 10β12)
+
+- [ ] **Step 3: Verify frame set**
+
+Run: `ls /tmp/demo-tray/frame-*.png | wc -l`
+Expected: 12 (or however many the shot-list produced; β₯8 minimum).
+
+No commit (scratch files).
+
+---
+
+## Task 6: Capture web-UI video via Playwright
+
+**Files:**
+- Create: `scripts/demo/playwright.config.ts`
+- Create: `scripts/demo/capture-webui.spec.ts`
+- Create (output, NOT committed): `/tmp/demo-webui/*.webm`
+
+- [ ] **Step 1: Write the Playwright config**
+
+Create `scripts/demo/playwright.config.ts`:
+
+```ts
+import { defineConfig } from '@playwright/test';
+export default defineConfig({
+ testDir: '.',
+ timeout: 60000,
+ fullyParallel: false,
+ workers: 1,
+ retries: 0,
+ use: {
+ headless: true,
+ viewport: { width: 1280, height: 800 },
+ baseURL: 'http://127.0.0.1:18082',
+ launchOptions: {
+ executablePath: '/Users/user/Library/Caches/ms-playwright/chromium-1217/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing',
+ },
+ recordVideo: { dir: '/tmp/demo-webui', size: { width: 1280, height: 800 } },
+ },
+});
+```
+
+- [ ] **Step 2: Write the capture spec**
+
+Create `scripts/demo/capture-webui.spec.ts`. It assumes a throwaway mcpproxy is
+already running on :18082 with demo servers + at least one flagged activity (Step 3
+boots it). The spec records two videos: server cards, then activity log.
+
+```ts
+import { test, expect } from '@playwright/test';
+
+const KEY = 'uitest';
+
+test('server cards walkthrough', async ({ page }) => {
+ await page.goto(`/ui/?apikey=${KEY}`);
+ await page.waitForLoadState('domcontentloaded');
+ // Dashboard with server cards
+ await page.locator('[data-test="server-card"]').first().waitFor({ timeout: 15000 });
+ await page.waitForTimeout(1500); // let cards settle (green health)
+ await page.locator('[data-test="server-card"]').first().hover();
+ await page.waitForTimeout(1000);
+ await page.locator('[data-test="server-card"]').first().click(); // into server detail
+ await page.waitForLoadState('domcontentloaded');
+ await page.waitForTimeout(2500); // dwell on detail
+});
+
+test('activity log walkthrough', async ({ page }) => {
+ await page.goto(`/ui/activity?apikey=${KEY}`);
+ await page.waitForLoadState('domcontentloaded');
+ await page.locator('[data-test="activity-row"]').first().waitFor({ timeout: 15000 });
+ await page.waitForTimeout(1500);
+ // expand the flagged (sensitive-data) row
+ const flagged = page.locator('[data-test="activity-row"]').filter({ hasText: /flag|sensitive|critical/i }).first();
+ await (await flagged.count() ? flagged : page.locator('[data-test="activity-row"]').first()).click();
+ await page.waitForTimeout(2500); // dwell on detail
+});
+```
+
+> NOTE on selectors: verify `data-test="server-card"`, `data-test="activity-row"`
+> exist in `frontend/src/`. If a selector is missing, grep the relevant Vue view for
+> the real `data-test` attribute and update the spec β do NOT invent one. If activity
+> rows lack `data-test`, add it to the component in this task (follow the project's
+> existing `data-test` convention) and rebuild the frontend with `make build`.
+
+- [ ] **Step 3: Boot a throwaway mcpproxy with demo data**
+
+Run (adapts the CLAUDE.md UI-test pattern; seeds 4 servers):
+```bash
+pkill -f 'mcpproxy serve.*18082' 2>/dev/null; sleep 1
+rm -rf /tmp/mcpproxy-demo/{config.db,index.bleve,logs} 2>/dev/null; mkdir -p /tmp/mcpproxy-demo
+cat > /tmp/mcpproxy-demo/mcp_config.json <<'EOF'
+{ "listen":"127.0.0.1:18082","data_dir":"/tmp/mcpproxy-demo","api_key":"uitest",
+ "enable_web_ui":true,"enable_socket":false,"telemetry":{"enabled":false},
+ "mcpServers":[
+ {"name":"github","command":"npx","args":["-y","@modelcontextprotocol/server-github"],"protocol":"stdio","enabled":true},
+ {"name":"filesystem","command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/tmp"],"protocol":"stdio","enabled":true},
+ {"name":"fetch","command":"uvx","args":["mcp-server-fetch"],"protocol":"stdio","enabled":true},
+ {"name":"time","command":"uvx","args":["mcp-server-time"],"protocol":"stdio","enabled":true}
+ ] }
+EOF
+./mcpproxy serve --config=/tmp/mcpproxy-demo/mcp_config.json --listen=127.0.0.1:18082 --log-level=info > /tmp/mcpproxy-demo/server.log 2>&1 &
+until curl -sf -H "X-API-Key: uitest" http://127.0.0.1:18082/api/v1/status >/dev/null; do sleep 1; done
+echo "mcpproxy demo up on :18082"
+```
+
+To guarantee a flagged activity row exists, make one tool call carrying a fake
+secret (detected by the sensitive-data scanner):
+```bash
+curl -s -H "X-API-Key: uitest" -X POST http://127.0.0.1:18082/api/v1/... # see note
+```
+> NOTE: if no simple REST path triggers a tool call, instead use the CLI:
+> `./mcpproxy call_tool_read ...` is not guaranteed; simplest reliable path is to let
+> the activity log show normal `tool_call` rows and drop the `/flag/` filter in the
+> spec (it already falls back to the first row). Flagged-row is best-effort.
+
+- [ ] **Step 4: Run the capture**
+
+Run:
+```bash
+cd scripts/demo
+ln -sfn /Users/user/repos/mcpproxy-go/e2e/playwright/node_modules ./node_modules
+./node_modules/.bin/playwright test --config=playwright.config.ts --reporter=list
+```
+Expected: 2 passed. Two `.webm` files appear in `/tmp/demo-webui/`.
+
+- [ ] **Step 5: Verify videos**
+
+Run: `ls -la /tmp/demo-webui/*.webm && echo "count: $(ls /tmp/demo-webui/*.webm | wc -l)"`
+Expected: 2 webm files, each non-zero size.
+
+- [ ] **Step 6: Commit the specs (not the videos)**
+
+```bash
+git add scripts/demo/playwright.config.ts scripts/demo/capture-webui.spec.ts
+git commit -m "feat(051): playwright web-ui capture for demo (server cards + activity)"
+```
+
+---
+
+## Task 7: Stitch the demo GIF (`build-demo.sh`)
+
+**Files:**
+- Create: `scripts/demo/build-demo.sh`
+- Create (output): `docs/demo.gif`, `docs/demo.webp`
+
+- [ ] **Step 1: Write the build script**
+
+Create `scripts/demo/build-demo.sh`:
+
+```bash
+#!/usr/bin/env bash
+# Stitch tray montage (stills) + 2 web-UI webm segments -> docs/demo.gif + .webp
+set -euo pipefail
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+TRAY=/tmp/demo-tray
+WEB=/tmp/demo-webui
+WORK=$(mktemp -d)
+W=900; FPS=15
+
+[ -d "$TRAY" ] || { echo "missing $TRAY (run Task 5)"; exit 1; }
+ls "$WEB"/*.webm >/dev/null 2>&1 || { echo "missing $WEB/*.webm (run Task 6)"; exit 1; }
+
+# 1) tray montage: each still held 0.42s, gentle crossfade via framerate ramp
+ffmpeg -y -framerate 1/0.42 -pattern_type glob -i "$TRAY/frame-*.png" \
+ -vf "scale=${W}:-2:flags=lanczos,fps=${FPS}" -pix_fmt yuv420p "$WORK/seg0.mp4"
+
+# 2) normalize the two web-UI segments to same width/fps (ordered: cards, then activity)
+i=1
+for v in $(ls "$WEB"/*.webm | sort); do
+ ffmpeg -y -i "$v" -vf "scale=${W}:-2:flags=lanczos,fps=${FPS}" -pix_fmt yuv420p "$WORK/seg${i}.mp4"
+ i=$((i+1))
+done
+
+# 3) concat
+: > "$WORK/list.txt"
+for f in "$WORK"/seg0.mp4 "$WORK"/seg1.mp4 "$WORK"/seg2.mp4; do echo "file '$f'" >> "$WORK/list.txt"; done
+ffmpeg -y -f concat -safe 0 -i "$WORK/list.txt" -c copy "$WORK/full.mp4"
+
+# 4) palette-optimized GIF
+ffmpeg -y -i "$WORK/full.mp4" -vf "fps=${FPS},scale=${W}:-2:flags=lanczos,palettegen=stats_mode=diff" "$WORK/pal.png"
+ffmpeg -y -i "$WORK/full.mp4" -i "$WORK/pal.png" \
+ -lavfi "fps=${FPS},scale=${W}:-2:flags=lanczos,paletteuse=dither=bayer:bayer_scale=3" "$ROOT/docs/demo.gif"
+
+# 5) webp (smaller, also autoplays in README)
+ffmpeg -y -i "$WORK/full.mp4" -vcodec libwebp -filter:v "fps=${FPS},scale=${W}:-2" \
+ -lossless 0 -compression_level 6 -q:v 55 -loop 0 -an "$ROOT/docs/demo.webp"
+
+echo "Wrote docs/demo.gif ($(du -h "$ROOT/docs/demo.gif" | cut -f1)) and docs/demo.webp ($(du -h "$ROOT/docs/demo.webp" | cut -f1))"
+rm -rf "$WORK"
+```
+
+- [ ] **Step 2: Run it**
+
+Run: `chmod +x scripts/demo/build-demo.sh && scripts/demo/build-demo.sh`
+Expected: "Wrote docs/demo.gif (...) and docs/demo.webp (...)".
+
+- [ ] **Step 3: Verify size + dimensions budget**
+
+Run:
+```bash
+GIFKB=$(du -k docs/demo.gif | cut -f1); echo "gif KB: $GIFKB"
+sips -g pixelWidth docs/demo.gif
+test "$GIFKB" -lt 8192 && echo "OK under 8MB" || echo "TOO BIG β lower W to 760 or FPS to 12 in build-demo.sh and re-run"
+```
+Expected: width 900, "OK under 8MB". If too big, reduce `W` or `FPS` and re-run Step 2.
+
+- [ ] **Step 4: Eyeball the GIF**
+
+Run: `open docs/demo.gif`
+Expected: tray menu beat β server cards beat β activity log beat, ~16s, loops.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add scripts/demo/build-demo.sh docs/demo.gif docs/demo.webp
+git commit -m "feat(051): build stitched demo gif (tray + web-ui)"
+```
+
+---
+
+## Task 8: Rewrite the README hero
+
+**Files:**
+- Modify: `README.md:1-25`
+
+- [ ] **Step 1: Read the current top of README**
+
+Run: `sed -n '1,26p' README.md`
+Confirm lines 1β25 match the block being replaced (H1, doc link, old video comment,
+YouTube thumbnail, the two `mcpproxy.app/images/menu_*.png` `` block).
+
+- [ ] **Step 2: Replace lines 1β25 with the new hero**
+
+Replace from `# MCPProxy β Smart Proxy for AI Agents` through the closing `
` of
+the tray-thumbnail block (the `` caption line) with:
+
+```markdown
+# π‘οΈ MCPProxy β Supercharge AI Agents, Safely
+
+> One safe endpoint in front of every MCP server.
+
+[](https://github.com/smart-mcp-proxy/mcpproxy-go/releases)
+[](https://github.com/smart-mcp-proxy/mcpproxy-go/actions/workflows/unit-tests.yml)
+[](https://goreportcard.com/report/github.com/smart-mcp-proxy/mcpproxy-go)
+[](https://pkg.go.dev/github.com/smart-mcp-proxy/mcpproxy-go)
+[](LICENSE)
+[](https://github.com/smart-mcp-proxy/mcpproxy-go/stargazers)
+
+
+
+
+
+
+
+
+
+
+ πΊ Watch the full walkthrough Β·
+ π Read the docs Β·
+ π mcpproxy.app
+
+```
+
+- [ ] **Step 3: Verify nothing below "Why MCPProxy?" changed**
+
+Run: `grep -n '## Why MCPProxy?' README.md`
+Expected: exactly one match; content after it unchanged. Also:
+Run: `grep -c 'mcpproxy.app/images/menu_' README.md`
+Expected: `0` (old thumbnails removed).
+
+- [ ] **Step 4: Verify asset links resolve locally**
+
+Run: `ls docs/social.png docs/demo.gif`
+Expected: both exist (relative paths in README will resolve on github.com).
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add README.md
+git commit -m "docs(051): new README hero β banner + demo gif"
+```
+
+---
+
+## Task 9: Final verification + handoff
+
+**Files:** none (verification only)
+
+- [ ] **Step 1: Confirm all committed assets present**
+
+Run: `git ls-files docs/social.html docs/social.png docs/demo.gif docs/demo.webp scripts/demo/`
+Expected: all listed, including the 5 `scripts/demo/` files.
+
+- [ ] **Step 2: Render README locally to sanity-check markdown**
+
+Run: `grep -n 'docs/social.png\|docs/demo.gif' README.md`
+Expected: banner ` ` and ` ` present.
+
+- [ ] **Step 3: Push branch + open PR**
+
+```bash
+git push -u origin feat/051-readme-hero-demo
+gh pr create --base main --title "docs(051): README hero banner + demo GIF" \
+ --body "Implements specs/051-readme-hero-demo. Adds frosted-tiles hero banner (docs/social.html -> docs/social.png), one cohesive demo GIF (tray + web UI, docs/demo.gif), and the scripted scripts/demo/ pipeline. README hero rewritten. Follow-up: upload docs/social.png to Settings -> Social preview."
+```
+
+- [ ] **Step 4: Remind user of the one manual step**
+
+Print: "Upload `docs/social.png` to GitHub β Settings β Social preview to control
+HN/Slack/X unfurls. This is the only step that cannot be scripted."
+
+---
+
+## Self-review notes (author)
+
+- **Spec coverage:** banner (T2β3) β, demo GIF tray+webUI (T4β7) β, README hero with
+ remove/demote/keep rules (T8) β, scripted pipeline (T1,3,4,6,7 + scripts/demo/README) β,
+ social-preview reminder (T9) β, keep existing screenshots (untouched by T8) β.
+- **Known soft spots flagged inline, not hidden:** (a) `data-test` selectors in the
+ Playwright spec must be verified against `frontend/src/` β instruction says grep, do
+ not invent; (b) the flagged-activity row is best-effort with a first-row fallback;
+ (c) retina scale factor may be ignored by headless Chromium β acceptable.
+- **Non-TDD task types:** asset generation uses verification steps (dimensions, file
+ size, visual eyeball) in place of unit tests, which is the correct analog here.