diff --git a/CLAUDE.md b/CLAUDE.md
index dd57f97..02794af 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -49,3 +49,40 @@ When modifying this repo:
- Skills should be self-contained instructions that work without user interaction where possible
- Agents should encode domain knowledge that would take multiple files to discover, not generic advice
- Keep specialist agents in `agents/example-specialists/` to separate them from the generic set
+
+
+## CodeGraph
+
+CodeGraph builds a semantic knowledge graph of codebases for faster, smarter code exploration.
+
+### If `.codegraph/` exists in the project
+
+**Use codegraph tools for faster exploration.** These tools provide instant lookups via the code graph instead of scanning files:
+
+| Tool | Use For |
+|------|---------|
+| `codegraph_search` | Find symbols by name (functions, classes, types) |
+| `codegraph_context` | Get relevant code context for a task |
+| `codegraph_callers` | Find what calls a function |
+| `codegraph_callees` | Find what a function calls |
+| `codegraph_impact` | See what's affected by changing a symbol |
+| `codegraph_node` | Get details + source code for a symbol |
+
+**When spawning Explore agents in a codegraph-enabled project:**
+
+Tell the Explore agent to use codegraph tools for faster exploration.
+
+**For quick lookups in the main session:**
+- Use `codegraph_search` instead of grep for finding symbols
+- Use `codegraph_callers`/`codegraph_callees` to trace code flow
+- Use `codegraph_impact` before making changes to see what's affected
+
+### If `.codegraph/` does NOT exist
+
+At the start of a session, ask the user if they'd like to initialize CodeGraph:
+
+"I notice this project doesn't have CodeGraph initialized. Would you like me to run `codegraph init -i` to build a code knowledge graph?"
+
+# graphify
+- **graphify** (`~/.claude/skills/graphify/SKILL.md`) - any input to knowledge graph. Trigger: `/graphify`
+When the user types `/graphify`, invoke the Skill tool with `skill: "graphify"` before doing anything else.
diff --git a/skills/figma-driven-development/SKILL.md b/skills/figma-driven-development/SKILL.md
new file mode 100644
index 0000000..6a96681
--- /dev/null
+++ b/skills/figma-driven-development/SKILL.md
@@ -0,0 +1,528 @@
+---
+name: figma-driven-development
+description: Use when implementing UI from a Figma link. Required before writing any code when a Figma URL is provided. Also use when auditing an existing implementation for Figma fidelity.
+---
+
+# Figma-Driven Development
+
+Two modes: **Build** (Phases 1–6) and **Audit** (Phase 7).
+
+## Phase 1: Discover Figma Tools
+
+```
+ToolSearch("figma")
+ToolSearch("select:mcp__claude_ai_Figma__get_metadata,mcp__claude_ai_Figma__get_design_context,mcp__claude_ai_Figma__get_screenshot")
+```
+
+If ToolSearch returns nothing: ask the user to connect Figma via claude.ai settings (`/mcp`) and restart. Do not implement from memory or screenshots without Figma access.
+
+**Tools:**
+- `get_metadata` — XML structural tree (node IDs, names, sizes). Always call via subagent — root responses exceed context limits predictably.
+- `get_design_context` — Reference code + screenshot + design values. Use when exact values are needed.
+- `get_screenshot` — Visual render of a node. Use first — it's cheap. Escalate to `get_design_context` only to resolve discrepancies.
+
+## Phase 2: Parse the URL
+
+```
+https://www.figma.com/design/sHPq6WL754484sETatkL51/Name?node-id=9936-6
+ ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^
+ fileKey nodeId (convert - to :)
+```
+
+Use the URL's `node-id` directly — it's the designer's intended frame. Never start from `0:1` (page root) — it returns the entire file and exceeds token limits.
+
+## Phase 3: Check for Existing Catalogue
+
+Catalogue index lives at `.figma/[fileKey].md`. Section files live at `.figma/[fileKey]/[section-slug].md`. Shared styles live at `.figma/[fileKey]/shared-styles.md`.
+
+- **Index exists + section present** → Phase 5 (build) or Phase 7 (audit)
+- **Index exists + section missing** → Check for naming mismatch first. Then `get_screenshot(fileKey, rootNodeId)` to visually search. If still not found, ask the user for the specific node URL — do not guess.
+- **Not exists** → Phase 4
+
+## Phase 4: Build the Catalogue
+
+The catalogue is split into three file types so each subagent loads only what it needs:
+
+| File | Purpose | Who reads it |
+|------|---------|--------------|
+| `.figma/[fileKey].md` | Index: section list, node IDs, slugs, one-line descriptions | Every agent at the start |
+| `.figma/[fileKey]/shared-styles.md` | CSS tokens, typography scale, shared spacing | Every implementation agent |
+| `.figma/[fileKey]/[section-slug].md` | Full detail for one section: layout, colours, copy, assets | Only the agent implementing/auditing that section |
+
+### 4a. Fetch root metadata via subagent
+
+Always route `get_metadata` through a subagent — root frames for full pages predictably exceed context. Copy the exact file path from the tool result verbatim (UUID paths are easy to mistype).
+
+Subagent prompt:
+> "File at **[exact path]** is JSON `[{type, text}]` where text is XML. Using jq or python3 (NOT Read tool): (1) root node name, type, width, height; (2) markdown table of ALL direct children: name | id | type | width | height. Return markdown only."
+
+The XML `name` attribute is the authoritative section name. If frames are generically named (e.g. `Frame 168…`), screenshot each child before writing the catalogue and name from visual content.
+
+### 4b. Screenshot all sections in parallel
+
+```
+get_screenshot(fileKey, sectionNodeId) — one per section, all at once
+```
+
+A full-page overview is too small to read — screenshot sections individually.
+
+### 4c. Write the catalogue index
+
+Save to `.figma/[fileKey].md`. This file must stay small — it is loaded into every agent's context. No style values, no copy, no code blocks here.
+
+```markdown
+# Figma Catalogue — [File Name]
+File key: [key]
+Root node: `[nodeId]` ("[name]" — [width]×[height]px)
+Catalogued: [date]
+
+Shared styles: `.figma/[fileKey]/shared-styles.md`
+
+## Sections
+
+| Section | Node ID | Size | Slug | Description |
+|---------|---------|------|------|-------------|
+| Hero | `9936:51` | 1440×803px | `hero` | Full-width hero with gradient background, heading, subheading, two CTAs, and hero illustration |
+| Features | `9936:102` | 1440×960px | `features` | Three-column feature grid with icon cards |
+| Pricing | `9936:188` | 1440×720px | `pricing` | Two-tier pricing cards with CTA buttons |
+
+## DRY Opportunities
+- **[Pattern]**: Appears in [Section A] (`hero`), [Section B] (`features`) — candidate for ` ` or `.class`
+```
+
+### 4d. Write the shared styles file
+
+Save to `.figma/[fileKey]/shared-styles.md`. Include every value that appears in two or more sections. Values observed only from screenshots are flagged `[confirm]` — verify via `get_design_context` before use.
+
+```markdown
+# Shared Styles — [File Name]
+File key: [key]
+
+> Values marked `[confirm]` were observed from screenshots — verify exact values via get_design_context before use.
+
+## Colours
+```css
+--color-bg-primary: #0F0F1A; /* Page background */
+--color-bg-card: rgba(255,255,255,0.04); /* Card surface */
+--color-accent-start: #6C63FF; /* Gradient start [confirm] */
+--color-accent-end: #48CAE4; /* Gradient end [confirm] */
+--color-text-primary: #FFFFFF;
+--color-text-secondary: #8B8BA7;
+--color-border: rgba(255,255,255,0.08);
+```
+
+## Typography
+```css
+--font-family: 'Inter', sans-serif;
+--text-xs: 12px / 16px;
+--text-sm: 14px / 20px;
+--text-base: 16px / 24px;
+--text-lg: 20px / 28px;
+--text-2xl: 24px / 32px;
+--text-4xl: 36px / 44px;
+--text-5xl: 48px / 56px;
+```
+
+## Spacing
+```css
+--section-padding-y: 80px;
+--section-padding-x: 120px;
+--card-padding: 32px;
+--card-gap: 24px;
+--card-radius: 16px;
+```
+```
+
+### 4e. Write each section file
+
+**Write ALL section files before fetching `get_design_context` for any section.** Screenshots from 4b are enough to write the stub — design context is fetched later, per-section, during Phase 5. If you fetch design context before the section files are on disk, you will rationalise skipping the write ("I already have the values in context") and the catalogue will be incomplete.
+
+Save to `.figma/[fileKey]/[section-slug].md`. One file per section. Include all concrete detail — layout code, typography values, colours, copy, assets, component mappings. An implementer should be able to start without re-fetching Figma.
+
+```markdown
+# [Section Name] — [File Name]
+File key: [key]
+Node ID: `9936:51`
+Size: 1440×803px
+Slug: `hero`
+
+See shared styles: `shared-styles.md`
+
+## Complexity
+**Score: [1–3]** — [Simple / Moderate / Complex]
+
+| Factor | Assessment |
+|--------|------------|
+| Layout complexity | [e.g. Single-column text — low] |
+| Visual effects | [e.g. Two gradients, one with opacity overlay — medium] |
+| Assets | [e.g. One SVG illustration — low] |
+| Interactive states | [e.g. Three button variants with transitions — medium] |
+| Responsive variants | [e.g. Three breakpoints with layout changes — medium] |
+| Data / charts | [e.g. None — low] |
+| Overlapping / absolute elements | [e.g. Decorative blobs positioned with negative insets — high] |
+
+**Error likelihood: [1–3]** — [Low / Medium / High]
+
+Key risks on first pass:
+- [e.g. Gradient direction is subtle — easy to get wrong from screenshot alone]
+- [e.g. Illustration size uses absolute px that collapses at tablet — confirm parent has explicit width]
+- [e.g. CTA hover state not visible in static design — requires prototype inspection]
+
+> Complexity score drives the number of QA passes at the visual gate (1 pass / 2 passes / 3 passes). Error likelihood is a signal to the implementer about where to spend extra care.
+
+## Layout
+```css
+/* Outer wrapper */
+display: flex;
+flex-direction: column;
+align-items: center;
+padding: 80px 120px;
+gap: 48px;
+max-width: 1440px;
+```
+
+## Typography
+| Role | Font | Size | Weight | Line-height | Colour |
+|------|------|------|--------|-------------|--------|
+| Heading | Inter | 48px | 700 | 56px | #FFFFFF |
+| Subheading | Inter | 20px | 400 | 28px | #8B8BA7 |
+| CTA label | Inter | 16px | 600 | 24px | #FFFFFF |
+
+## Colours
+| Token | Hex / gradient | Usage |
+|-------|---------------|-------|
+| Background | `linear-gradient(180deg, #1A0F3C 0%, #0F0F1A 100%)` | Section background [confirm] |
+| CTA button | `linear-gradient(135deg, #6C63FF 0%, #48CAE4 100%)` | Primary button fill |
+| Border | `rgba(255,255,255,0.08)` | Card border |
+
+## Spacing & Sizing
+| Element | Value |
+|---------|-------|
+| Section padding (vertical) | `80px` |
+| Section padding (horizontal) | `120px` |
+| CTA gap | `16px` |
+| Illustration width | `560px` |
+
+## Component Mapping
+| Figma layer | Codebase component | Notes |
+|-------------|--------------------|-------|
+| `Button/Primary` | `` | Use existing; verify label copy |
+| `Button/Secondary` | `` | Use existing |
+| `Illustration/Hero` | — | Download from Figma; no codebase equivalent |
+
+## Assets
+| Asset | Node ID | Download path | Format |
+|-------|---------|---------------|--------|
+| Hero illustration | `9936:88` | `public/images/hero.svg` | SVG (confirm via curl -sI) |
+| Background texture | `9936:92` | `public/images/bg-texture.png` | PNG |
+
+## Interactive States
+| Element | State | Style delta |
+|---------|-------|-------------|
+| `Button/Primary` | hover | `opacity: 0.9; transform: scale(1.02)` |
+| `Button/Primary` | active | `transform: scale(0.98)` |
+| `Button/Primary` | disabled | `opacity: 0.4; cursor: not-allowed` |
+| `Button/Secondary` | hover | `border-color: #FFFFFF; color: #FFFFFF` |
+> States observed from Figma prototype or component variants. Mark `[confirm]` if inferred from visual only.
+
+## Transitions
+| Element | Property | Duration | Easing | Trigger |
+|---------|----------|----------|--------|---------|
+| `Button/Primary` | `opacity, transform` | `150ms` | `ease-out` | hover/active |
+| Section | `opacity, transform` | `600ms` | `ease-out` | scroll-into-view |
+> Mark `[confirm]` if not explicitly defined in Figma prototype — do not invent values.
+
+## Responsive Variants
+| Breakpoint | Node ID | Key differences |
+|------------|---------|-----------------|
+| Desktop (1440px) | `9936:51` | Two-column layout; illustration visible |
+| Tablet (768px) | `9936:204` | Single column; illustration hidden |
+| Mobile (375px) | `9936:218` | Single column; reduced padding; stacked CTAs |
+> Fetch each variant node separately — do not assume styles are identical to desktop.
+
+## Copy
+```
+Heading: "Build faster with confidence"
+Subheading: "The platform that helps teams ship without breaking things."
+CTA primary: "Get started free"
+CTA secondary: "See how it works"
+```
+```
+
+### 4f. Identify DRY opportunities
+
+Before implementing, scan across section files for: repeated background layers, shared button variants, common wrapper widths, repeated typography patterns. Record findings in the index under **DRY Opportunities**. Finding DRY after implementation is expensive — find it now.
+
+### 4g. Catalogue completion gate — mandatory blocking check
+
+**This gate is non-negotiable. You may not write a single line of implementation code until it passes.**
+
+Run this exact command and paste the output into your response:
+
+```bash
+ls .figma/[fileKey]/
+```
+
+Then count the files listed and confirm the count equals the number of rows in your index section table (excluding `shared-styles.md`). Write the result explicitly:
+
+```
+Section file count: 8 (matches index table ✓)
+```
+
+If the count is wrong, write the missing files now. Do not proceed until the count matches.
+
+**Why:** Once design context is in context, agents rationalise skipping the write ("I already have the values"). They don't come back. The gate prevents this.
+
+## Phase 5: Implement a Section
+
+**Reading order:** Load the index → load `shared-styles.md` → load the section file. Do not load other section files.
+
+1. Re-read the section file — confirm node ID, size, component mappings.
+2. Sanity-check: screenshot the node, verify it matches the section file description. Re-catalogue if wrong.
+3. **Sub-component check:** If the node is a sub-component (e.g. `PieChart`, `Card`, `Button`), fetch the **parent node's metadata** first — labels and decorators are often siblings, not children.
+4. Screenshot first. Fetch `get_design_context` only if you spot a discrepancy needing exact values not already in the section file.
+5. From `get_design_context`, extract and update the section file with:
+ - Layout: direction, alignment, padding, gap, max-width
+ - Background: colour, gradient stops, image refs
+ - **All text content** — copy every text node exactly. Screenshots can't catch copy mismatches.
+ - Interactive states: hover/active/disabled/focus style deltas from component variants
+ - Transitions: duration, easing, trigger — from Figma prototype connections or component properties. If absent, mark `[confirm]` and use project defaults; do not invent values.
+ - Responsive variants: node IDs for each breakpoint variant and what differs (layout, hidden elements, font sizes)
+ - Decorative elements: asset URLs, connector lines, dividers, positions
+6. **Check the config/data layer** (e.g. `site-config.ts`) for copy — a mismatch may live entirely in config, invisible in JSX.
+7. **Download assets immediately after fetching design context — before writing any code.** Asset URLs (`figma.com/api/mcp/asset/...`) expire in 7 days. Each new `get_design_context` call returns new URLs — do not mix URLs from different fetches.
+ - **Do not assume file format from context** — inspect the URL or run `curl -sI "[url]" | grep content-type` to confirm. Figma assets labelled as images are frequently SVGs, not PNGs or JPEGs.
+ - Images/illustrations → `curl -o public/images/[name].[ext] "[url]"` (use confirmed extension, e.g. `.svg`, `.png`, `.jpg`)
+ - Icons (SVG) → `curl -o public/icons/[name].svg "[url]"` — check for existing equivalents first
+ - Structural elements (connector lines, dividers, shapes) → **do not download** — reconstruct with CSS (`border`, `linear-gradient`, inline SVG). Never use expiring URLs for UI elements.
+ - A static asset approximating a Figma vector is technical debt — flag it; correct impl is CSS.
+8. Cross-reference every colour and spacing value against `shared-styles.md` and the existing design system before adding new tokens. Update `shared-styles.md` if you confirm a value that was previously `[confirm]`.
+9. Apply DRY opportunities from the index.
+10. **Fidelity over DRY:** If a shared component can't express the Figma layout, inline the markup. Don't distort layout to fit a component.
+11. Batch all edits, then run typecheck/build once at the end.
+12. **REQUIRED — Visual gate. Do not move on until this is done.**
+
+ > **Stop. Read this before continuing.**
+ >
+ > If you are thinking any of the following — you are rationalising. Stop and follow the gate anyway.
+ > - "I already looked at the screenshots" → You have confirmation bias from writing the code. A fresh subagent does not.
+ > - "The page looks right" → Eyeballing full-page thumbnails misses copy errors, colour deviations, clipped labels, and scale bugs. The binary decomposition finds what intuition misses.
+ > - "This is just a simple section" → Complexity score 1 still requires one subagent pass. Simple ≠ skip.
+ > - "I can review it myself faster" → You cannot QA your own implementation. Independent review is the point.
+ >
+ > **The subagent is mandatory because you have confirmation bias. You wrote the code. You cannot see your own mistakes reliably.**
+
+ **Step 1 — Add a blocking todo before starting:**
+ ```
+ TodoWrite: "QA visual gate — [section-name] (complexity [score], [N] pass/passes required)" → in_progress
+ ```
+ Do not mark it complete until all N subagent passes are done and findings are triaged.
+
+ **Step 2 — Gather the two images:**
+ - Take a **section-level browser screenshot** — scroll to the section, capture that viewport. Never a full-page thumbnail.
+ - Fetch the Figma reference: `get_screenshot(fileKey, sectionNodeId)`
+ - Save both to `/tmp/qa-[section]-browser.png` and `/tmp/qa-[section]-figma.png`
+
+ **Step 3 — Read and state the complexity score:**
+ Read it from the section file. Do not re-derive it. State it aloud:
+ > "Complexity score: [1/2/3]. Pass count: [N]. Key risks: [list from section file]."
+
+ **Step 4 — Spawn N QA subagent passes sequentially** (N = complexity score).
+
+ Fill in this template for each pass — every `[bracket]` must be replaced with real content:
+
+ ```
+ Section: [name from catalogue]
+ Node ID: [nodeId]
+ Expected size: [width×height]
+ Description: [description field verbatim from catalogue]
+ Key elements: [component mapping table verbatim]
+ Expected copy: [copy block verbatim]
+ Complexity score: [1/2/3] — [Simple/Moderate/Complex]
+ Known risks: [key risks list verbatim]
+ Figma image: /tmp/qa-[section]-figma.png
+ Browser image: /tmp/qa-[section]-browser.png
+ Pass number: [1/2/3] of [N]
+ Prior findings: [none | paste cumulative list from prior passes]
+ ```
+
+ Then append the full subagent instructions below. Each pass receives the same images and context plus the growing cumulative findings list. The subagent must:
+ - (a) invoke the `image-compare` skill first — binary spatial decomposition before any free-form visual analysis
+ - (b) re-examine everything independently, not just confirm prior findings
+ - (c) specifically hunt for what prior passes may have missed
+ - (d) add new findings to the cumulative list without removing prior ones
+
+ Subagent prompt body (append after the filled template above):
+
+ > You are a meticulous visual QA reviewer. Your job is to find mistakes — assume they are there, because they almost always are. Your disposition is skeptical and critical. Do not give the benefit of the doubt.
+ >
+ > **FIRST ACTION — mandatory before any other analysis:**
+ > Invoke the `image-compare` skill now. Use it to build colour trees for both images, diff the trees, and zoom into the top divergent leaf regions. Do not do any free-form visual analysis until the binary decomposition is complete. The decomposition tells you WHERE to look; your visual analysis then tells you WHAT changed.
+ >
+ > Use the section context below to anchor your diff — if the implementation is missing something the catalogue describes, that is a confirmed missing element.
+ >
+ > **[Pass 2/3 only] Prior findings:**
+ > (pasted from prior passes)
+ > Do NOT simply confirm these — re-examine independently first. Add new findings. Correct wrong ones.
+ >
+ > **Depth of analysis by complexity score:**
+ >
+ > *Score 1 — Simple:* image-compare decomposition + standard checklist pass. Flag anything that looks off.
+ >
+ > *Score 2 — Moderate:* image-compare decomposition first. Then for every visual effect (gradient, shadow, blur, opacity), zoom into that region again before moving on — do not rely on gestalt impression. For every icon/image asset, confirm asset identity, colour fill, and no clipping.
+ >
+ > *Score 3 — Complex:* image-compare decompose entire section into quadrants first, recurse into every quadrant that shows any difference until you reach 50×50px or find root cause. Then: (a) every data visualisation element (bar, arc, label, axis tick, legend item) is a separate finding; (b) every overlapping/absolute element — verify no clipping at each edge; (c) every gradient — describe exact direction and each stop colour from both images before deciding if they match; (d) every small repeated element (icon rows, stat blocks, tag lists) — verify each instance individually.
+ >
+ > **Scale warning:** Scale errors are extremely common and easy to miss — elements that are too small, too large, or incorrectly proportioned relative to their surroundings. Do not trust that something "looks about right." Actively compare the relative size of every element (icons, images, text blocks, buttons, cards) against the Figma design. Ask yourself: does this element occupy the same proportion of the section as it does in the design?
+ >
+ > Go through every category below. For each one, describe what you see in both images and call out any discrepancy, no matter how small:
+ > - **Layout**: flex direction, alignment (horizontal and vertical), gap, padding, margin — compare every axis
+ > - **Typography**: font size, weight, line height, letter spacing, colour, text-transform, text-decoration — read each text node character by character
+ > - **Copy**: every word, punctuation mark, and line break — do not skim
+ > - **Colour**: backgrounds, borders, text, icon fills — flag anything that looks even slightly off
+ > - **Spacing**: internal padding, gaps between elements, outer margins
+ > - **Scale & sizing**: widths, heights, aspect ratios — this is a high-risk category. Elements are frequently too small or too large. Compare proportions carefully against the Figma design, not just against your expectations.
+ > - **Borders & shadows**: radius, width, colour, box-shadow offsets and blur
+ > - **Visual effects**: gradients (direction, stops, colours), opacity, blur, overlay effects — these are the most commonly wrong
+ > - **Charts & data visualisations**: bar heights, line paths, colours, labels, axes, legends — treat every detail as suspect
+ > - **Icons & images**: correct asset, correct size, correct colour/fill, correct orientation
+ > - **Missing elements**: scan the Figma image for anything absent from the browser — decorative lines, badges, indicators, overlays, subtle background patterns
+ > - **Extra elements**: scan the browser image for anything not present in Figma
+ > - **Responsive state**: confirm the viewport matches the intended breakpoint variant
+ >
+ > Output a numbered diff list (cumulative across all passes). An empty list is almost certainly wrong — if you find nothing, re-examine. Justify an empty list explicitly.
+
+ - After all passes, merge findings into a final cumulative diff list. Triage using the priority order in Phase 7d. **Fix all layout, copy, and missing-element issues before proceeding to the next section.**
+
+## Phase 6: Zooming Into Child Nodes
+
+```
+get_screenshot(fileKey, childNodeId) — visual confirmation first (cheap)
+get_design_context(fileKey, childNodeId) — exact values only if needed
+```
+
+For repeated similar elements (connector lines, cards, icons): screenshot each child individually — `get_design_context` may flatten them, hiding style differences.
+
+**Watch for Figma coordinate artefacts** — these only work at the original design dimensions:
+| Artefact | Risk |
+|----------|------|
+| Percentage insets on absolute elements (`inset: "6.52% 3.42%"`) | Collapses without explicit parent size |
+| Negative insets >100% (`inset: "-102.42% -72.65%"`) | Overflows at different sizes |
+| `calc()` with large px offsets (`left: calc(50% + 227.91px)`) | Breaks at narrower containers |
+| Pixel positions referencing parent size (`top: -220.86px`) | Only valid at exact original height |
+
+Verify parent has an explicit fixed size matching Figma dimensions, or convert to size-safe values.
+
+## Phase 7: Audit Mode
+
+### 7a+7b. Read implementation and screenshot Figma sections in parallel
+
+Start both at the same time — they have no dependency on each other.
+
+- **7a** — Read all relevant source files: components, views, config/data layer. Read the catalogue index, `shared-styles.md`, and each section file being audited.
+- **7b** — Fetch screenshots of all Figma sections:
+
+```
+get_screenshot(fileKey, sectionNodeId) — one per section, all at once
+```
+
+### 7c. Build a diff list before touching code
+
+**Delegate the visual diff to a skeptical subagent — do not review it yourself.**
+
+**Read the complexity score from each section file** — do not re-derive it. Use it directly as the pass count.
+
+Spawn sections in parallel (one pipeline per section), but within each section run passes **sequentially** — each pass feeds its findings into the next. Pass the section file content inline in the prompt — do not ask the subagent to read files itself. Each subagent should use the `/image-compare` skill to systematically locate differences via binary spatial decomposition before doing free-form visual analysis. Subagent prompt (adapt for pass number):
+
+> You are a meticulous visual QA reviewer. Your job is to find mistakes — assume they are there, because they almost always are. You will be shown two images: a Figma design and a browser implementation. Your disposition is skeptical and critical. Do not give the benefit of the doubt.
+>
+> **Section context from the Figma catalogue:**
+> - Section name: [name from catalogue]
+> - Node ID: [nodeId]
+> - Expected size: [width×height from catalogue]
+> - Catalogue description: [description field verbatim]
+> - Key elements: [component mapping table verbatim]
+> - Expected copy: [copy block verbatim]
+> - Complexity score: [1–3] — [Simple / Moderate / Complex]
+> - Known error risks: [key risks list from catalogue verbatim — pay extra attention to these]
+>
+> Use this context to understand what elements should be present and to anchor your diff. If the implementation is missing something the catalogue describes, that is a confirmed missing element.
+>
+> **[Pass 2/3 only] Prior findings to build on:**
+> [paste cumulative diff list from previous passes]
+> Do NOT simply confirm these — re-examine the images independently first. Then add any new findings. Prior findings may also be wrong; correct them if needed.
+>
+> **Depth of analysis — follow the instructions for the complexity score above:**
+>
+> *Score 1 — Simple:* Standard checklist pass. Cover all categories below. Flag anything that looks off.
+>
+> *Score 2 — Moderate:* Standard checklist pass, plus for every visual effect (gradient, shadow, blur, opacity), zoom into that region using the `/image-compare` binary decomposition method before moving on. Do not rely on a gestalt impression of colour or lighting — isolate each effect and compare it explicitly. For every icon and image asset, confirm correct asset identity, colour fill, and that it is not clipped.
+>
+> *Score 3 — Complex:* Everything in Score 2, plus: (a) treat each data visualisation element (bar, line, arc, label, axis tick, legend item) as a separate finding — do not summarise as "chart looks correct"; (b) for every overlapping or absolutely positioned element, verify it is not clipping its parent or siblings by inspecting each edge; (c) for every gradient, describe the exact direction and each stop colour from both images before deciding if they match; (d) use `/image-compare` to decompose the entire section into quadrants first, then recurse into any quadrant that shows any difference, no matter how minor, until you reach a 50×50px region or find the root cause; (e) if the section has small repeated elements (icon rows, stat blocks, tag lists), verify each instance individually — do not assume they are all identical.
+>
+> **Scale warning:** Scale errors are extremely common and easy to miss — elements that are too small, too large, or incorrectly proportioned relative to their surroundings. Do not trust that something "looks about right." Actively compare the relative size of every element (icons, images, text blocks, buttons, cards) against the Figma design. Ask yourself: does this element occupy the same proportion of the section as it does in the design?
+>
+> Go through every category below. For each one, describe what you see in both images and call out any discrepancy, no matter how small:
+> - **Layout**: flex direction, alignment (horizontal and vertical), gap, padding, margin — compare every axis
+> - **Typography**: font size, weight, line height, letter spacing, colour, text-transform, text-decoration — read each text node character by character
+> - **Copy**: every word, punctuation mark, and line break — do not skim
+> - **Colour**: backgrounds, borders, text, icon fills — flag anything that looks even slightly off
+> - **Spacing**: internal padding, gaps between elements, outer margins
+> - **Scale & sizing**: widths, heights, aspect ratios — this is a high-risk category. Elements are frequently too small or too large. Compare proportions carefully against the Figma design, not just against your expectations.
+> - **Borders & shadows**: radius, width, colour, box-shadow offsets and blur
+> - **Visual effects**: gradients (direction, stops, colours), opacity, blur, overlay effects — these are the most commonly wrong
+> - **Charts & data visualisations**: bar heights, line paths, colours, labels, axes, legends — treat every detail as suspect
+> - **Icons & images**: correct asset, correct size, correct colour/fill, correct orientation
+> - **Missing elements**: scan the Figma image for anything absent from the implementation — decorative lines, badges, indicators, overlays, subtle background patterns
+> - **Extra elements**: scan the implementation for anything not present in Figma
+> - **Responsive state**: confirm the viewport matches the intended breakpoint variant
+>
+> Classify each finding: **Layout deviation** / **Copy mismatch** / **Missing element** / **Wrong element** / **Correct**. Output a cumulative numbered diff list. An empty list is almost certainly wrong — if you find nothing, re-examine. Justify an empty list explicitly.
+
+Only fetch `get_design_context` for sections where the subagent identified layout deviations needing exact values.
+
+### 7d. Triage priority
+
+1. Wrong layout or structure — fix immediately
+2. Wrong copy — fix immediately
+3. Missing element — fix immediately
+4. Minor spacing deviation (1–4px) — fix if visible; skip if within Tailwind rounding
+5. Pixel-perfect padding differences with no visual impact — skip
+
+### 7e. Batch all edits, verify once
+
+Make all fixes, then run typecheck/build once. Update the relevant section file if any description or value was wrong.
+
+## Catalogue Files
+
+```
+.figma/
+ [fileKey].md # Index — always loaded
+ [fileKey]/
+ shared-styles.md # Shared tokens — loaded by every implementation agent
+ hero.md # Section detail — loaded only when implementing/auditing Hero
+ features.md
+ pricing.md
+```
+
+- Add `.figma/` to `.gitignore` — generated, may go stale
+- `mkdir -p .figma/[fileKey]` on first use in a project
+- Shared across agents — avoids re-fetching
+- When passing catalogue context to a subagent, paste the content inline — do not ask the subagent to read files itself
+
+## Hard Rules
+
+These are the non-obvious ones most commonly violated:
+
+- **Never fetch `0:1`** — always start from the URL's `node-id`
+- **Never call `get_metadata` in the main session** — always route through a subagent
+- **Screenshot first; `get_design_context` only to resolve discrepancies** — not upfront for every section
+- **Download assets immediately after `get_design_context`** — URLs expire in 7 days; never mix URLs from different fetches
+- **Connector lines, dividers, shapes → CSS only** — never download or use expiring URLs for structural elements
+- **Sub-component nodes: fetch the parent's metadata first** — labels and decorators are often siblings, not children
+- **Fidelity over DRY** — inline markup when a shared component can't express the Figma layout
+- **Check config/data layer for copy** — mismatches often live there, invisible in JSX
+- **Pass catalogue content inline to subagents** — do not ask subagents to read files themselves
+- **Every section gets a QA pass** — complexity score sets pass count, not whether QA runs
+- **Never QA your own implementation** — always spawn an independent subagent; you cannot see your own mistakes reliably
+- **Visual gate requires a blocking TodoWrite todo** — create it before taking screenshots; do not mark done until all passes complete and findings are resolved
diff --git a/skills/image-compare/SKILL.md b/skills/image-compare/SKILL.md
new file mode 100644
index 0000000..3710026
--- /dev/null
+++ b/skills/image-compare/SKILL.md
@@ -0,0 +1,218 @@
+---
+name: image-compare
+description: Use when comparing two images for visual differences — including visual QA after implementing UI from a Figma design or spec, when a section has been coded and you need to verify fidelity, or when a broad visual inspection has missed a difference. Mandatory during the figma-driven-development visual gate.
+---
+
+# Image Comparison
+
+## Overview
+
+Systematically locate subtle visual differences between two images using a binary spatial decomposition to guide the LLM's attention. Instead of scanning the whole image and relying on intuition, you build an average-colour tree for each image, diff the trees to find the most divergent region, then examine only that region with vision.
+
+## Core Pattern
+
+```
+Build colour tree (both images) → Diff trees → Zoom into most-divergent region → Examine with vision → Step out for context if needed
+```
+
+## Step 1: Build the Colour Tree
+
+For each image, recursively compute average colour per region. Absolute coordinates are
+tracked in `build_tree` itself — **do not** try to reconstruct them in `diff_trees`.
+
+```python
+from PIL import Image
+import numpy as np
+from skimage.metrics import structural_similarity as sk_ssim
+
+def avg_colour(img_array: np.ndarray) -> tuple[int, int, int]:
+ return tuple(img_array.mean(axis=(0, 1)).astype(int)[:3])
+
+def node_ssim(region_a: np.ndarray, region_b: np.ndarray) -> float | None:
+ h, w = region_a.shape[:2]
+ if min(h, w) < 11:
+ return None
+ gray_a = region_a.mean(axis=2).astype(np.float32) if region_a.ndim == 3 else region_a.astype(np.float32)
+ gray_b = region_b.mean(axis=2).astype(np.float32) if region_b.ndim == 3 else region_b.astype(np.float32)
+ return sk_ssim(gray_a, gray_b, data_range=255.0)
+
+def build_tree(img_a: np.ndarray, img_b: np.ndarray, max_depth: int = 6, depth: int = 0,
+ abs_x: int = 0, abs_y: int = 0,
+ parent_id: int | None = None,
+ _counter: list | None = None) -> dict:
+ if _counter is None:
+ _counter = [0]
+ h, w = img_a.shape[:2]
+ my_id = _counter[0]
+ _counter[0] += 1
+ node = {
+ "id": my_id,
+ "parent_id": parent_id,
+ "depth": depth,
+ "abs_region": (abs_x, abs_y, w, h),
+ "colour": avg_colour(img_a),
+ "colour_b": avg_colour(img_b),
+ "ssim_score": node_ssim(img_a, img_b),
+ "children": [],
+ }
+ if depth < max_depth and min(h, w) >= 11:
+ if w >= h:
+ mid = w // 2
+ halves = [
+ (img_a[:, :mid], img_b[:, :mid], abs_x, abs_y),
+ (img_a[:, mid:], img_b[:, mid:], abs_x + mid, abs_y),
+ ]
+ else:
+ mid = h // 2
+ halves = [
+ (img_a[:mid, :], img_b[:mid, :], abs_x, abs_y),
+ (img_a[mid:, :], img_b[mid:, :], abs_x, abs_y + mid),
+ ]
+ node["children"] = [
+ build_tree(ha, hb, max_depth, depth + 1, ax, ay, my_id, _counter)
+ for ha, hb, ax, ay in halves
+ ]
+ return node
+```
+
+**max_depth guidance:**
+- 4 → ~16 leaf regions (fast, good for large layout differences)
+- 6 → ~64 leaf regions (default, catches most UI changes)
+- 8 → ~256 leaf regions (use when colour shift is extremely subtle)
+
+## Step 2: Diff the Trees
+
+Walk both trees together and score each node by colour distance and structural similarity. Record absolute coordinates for every node — these are used both to crop the image and to map findings back to the page (e.g. for `elementFromPoint` or DevTools inspection).
+
+```python
+def colour_distance(c1, c2) -> float:
+ return sum((a - b) ** 2 for a, b in zip(c1, c2)) ** 0.5
+
+def normalise_and_score(diffs: list[dict]) -> list[dict]:
+ colour_vals = [n["colour_distance_raw"] for n in diffs]
+ ssim_diffs = [1 - n["ssim_score"] for n in diffs if n["ssim_score"] is not None]
+
+ max_c = max(colour_vals) if max(colour_vals) > 0 else 1.0
+ max_s = max(ssim_diffs) if ssim_diffs and max(ssim_diffs) > 0 else 1.0
+
+ for n in diffs:
+ n["norm_colour_dist"] = n["colour_distance_raw"] / max_c
+ n["norm_ssim_diff"] = (1 - n["ssim_score"]) / max_s if n["ssim_score"] is not None else 0.0
+ n["distance"] = max(n["norm_colour_dist"], n["norm_ssim_diff"])
+ return diffs
+
+def diff_trees(tree_a: dict, tree_b: dict) -> tuple[list[dict], dict[int, dict]]:
+ """Return (diffs sorted by distance, index mapping node_id → diff_node).
+ distance = max(norm_colour_dist, norm_ssim_diff) — either signal can surface a region.
+ abs_region is (x, y, w, h) in image pixels.
+ """
+ results = []
+ stack = [(tree_a, tree_b)]
+ while stack:
+ a, b = stack.pop()
+ x, y, w, h = a["abs_region"]
+ results.append({
+ "id": a["id"],
+ "parent_id": a["parent_id"],
+ "depth": a["depth"],
+ "abs_region": (x, y, w, h),
+ "centre": (x + w // 2, y + h // 2),
+ "colour_distance_raw": colour_distance(a["colour"], a["colour_b"]),
+ "colour_a": a["colour"],
+ "colour_b": a["colour_b"],
+ "ssim_score": a.get("ssim_score"), # None for small nodes
+ })
+ for child_a, child_b in zip(a["children"], b["children"]):
+ stack.append((child_a, child_b))
+
+ normalise_and_score(results)
+ sorted_results = sorted(results, key=lambda n: n["distance"], reverse=True)
+ index = {n["id"]: n for n in sorted_results}
+ return sorted_results, index
+```
+
+**Prefer leaf nodes when selecting candidates** — parent nodes accumulate divergence from all children. Filter to the deepest depth before picking top-N:
+
+```python
+img_a = np.array(Image.open(path_a).convert("RGB"))
+img_b = np.array(Image.open(path_b).convert("RGB"))
+
+tree = build_tree(img_a, img_b) # single call builds the combined tree
+diffs, index = diff_trees(tree, tree) # tree embeds both img_a and img_b
+
+max_d = max(n["depth"] for n in diffs)
+leaf_diffs = [n for n in diffs if n["depth"] == max_d]
+top_candidates = leaf_diffs[:3]
+```
+
+## Step 3: Zoom and Examine
+
+Take the top-N most-divergent **leaf** nodes (start N=3). For each, crop both images to that region and examine with vision:
+
+```python
+def crop_region(img_path: str, abs_region: tuple, padding: int = 20) -> Image.Image:
+ img = Image.open(img_path)
+ x, y, w, h = abs_region
+ x1 = max(0, x - padding)
+ y1 = max(0, y - padding)
+ x2 = min(img.width, x + w + padding)
+ y2 = min(img.height, y + h + padding)
+ return img.crop((x1, y1, x2, y2))
+```
+
+Save crops as temp files, then pass both to the Read tool (which renders images inline for vision).
+
+**Examination prompt to send yourself:**
+> "These are two crops of the same UI region. Identify every visual difference: text, colour, size, spacing, visibility, icon, state."
+
+## Step 4: Step Out for Context
+
+If you find a candidate difference but can't tell what UI element it belongs to, step out by walking **up the index** to the parent node — no re-cropping needed, the parent's `abs_region` is already computed:
+
+```python
+def step_out(node: dict, index: dict, img_a: Image.Image, img_b: Image.Image,
+ padding: int = 20) -> tuple[Image.Image, Image.Image, dict]:
+ """Return crops of the parent node and the parent dict itself."""
+ parent = index[node["parent_id"]]
+ crop_a = crop_region_from_img(img_a, parent["abs_region"], padding)
+ crop_b = crop_region_from_img(img_b, parent["abs_region"], padding)
+ return crop_a, crop_b, parent
+```
+
+Repeat — each call moves one level up the tree, doubling the region — until you can answer: "What changed and where in the UI?"
+
+**Stopping rule:** Stop when the cropped region contains enough surrounding UI to name the component (e.g. you can see the button label, the card boundary, the nav section).
+
+## Step 5: Report
+
+For each difference found, state:
+- **Location**: approximate position (e.g. "top-right, navigation bar")
+- **Pixel coordinates**: `abs_region` and `centre` from the diff node — e.g. `(412, 88, 64, 32)`, centre `(444, 104)`
+- **Signals**: `ssim_score: 0.71` (perceptual similarity, 1 = identical), `colour_distance_raw: 42.3`
+- **Page element** (if working from a live browser): run `document.elementFromPoint(cx, cy)` in DevTools using the centre coords to identify the DOM element
+- **What changed**: specific description (e.g. "button label changed from 'Save' to 'Update'")
+- **Severity**: cosmetic / functional / breaking
+
+If the diff trees show zero divergence above threshold (~0.05 distance), the images are likely identical or the change is sub-pixel. Raise max_depth or check that the images are the same dimensions first.
+
+## Quick Reference
+
+| Situation | Action |
+|-----------|--------|
+| Images different sizes | Resize to same dimensions before building tree |
+| No regions diverge | Increase max_depth or check colour space (RGBA vs RGB). Also check that both signals (`norm_colour_dist`, `norm_ssim_diff`) are near zero — if one is non-zero, re-examine that signal's raw values |
+| Difference found but context unclear | Step out: walk up the index to parent node |
+| Multiple high-distance nodes | Examine top 3–5, they may be the same logical region split across depth |
+| Very subtle colour shift (e.g. opacity) | Use max_depth=8 and check RGB channels individually |
+| Need to identify DOM element on live page | Use `centre` coords with `document.elementFromPoint(cx, cy)` in DevTools |
+| `ssim_score` is `None` for a candidate | Region was below 11px on one axis — ranking used colour distance only; zoom still valid |
+
+## Common Mistakes
+
+- **Examining the full image directly** — the LLM's attention disperses; the subtle difference gets missed. Always zoom first.
+- **Stopping at the first divergent node** — parent nodes accumulate child divergence; check that the children aren't pointing at a more specific sub-region.
+- **Forgetting padding** — a 4×4 pixel crop gives no context. Always add 20px padding minimum.
+- **Re-cropping to step out** — the parent node already has the right coordinates. Use `step_out()` with the index rather than computing a new crop size.
+- **Assuming one difference** — run the full top-N scan; there may be multiple independent changes.
+- **Reconstructing offsets in `diff_trees` instead of tracking them in `build_tree`** — every node slice stores its top-left as `(0, 0)` relative to itself, so `child["region"][:2]` is always `(0, 0)`. Any attempt to accumulate offsets during tree traversal produces wrong coordinates. Always pass `abs_x`/`abs_y` into `build_tree` and store `abs_region` there; `diff_trees` must read those values directly without modification.
+- **Calling `build_tree` twice (once per image)** — the updated `build_tree` takes both image arrays and computes SSIM in a single pass. Calling it separately on each image loses the SSIM signal entirely and leaves `ssim_score: None` on all nodes.