diff --git a/.gitignore b/.gitignore
index 77895d518..738067b7b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ test-results/
# Git worktrees for parallel agent work
.worktrees/
.env
+docs/.vitepress/cache/
diff --git a/docs/development/roadmap.md b/docs/development/roadmap.md
index d242aeafa..84cb4aba3 100644
--- a/docs/development/roadmap.md
+++ b/docs/development/roadmap.md
@@ -86,6 +86,9 @@ Components, instances, overrides, variables, collections, modes, image export.
- ColorInput component extraction, ColorPicker alpha slider checkerboard fix
- App identity: pencil icon, Cargo crate open_pencil, macOS Dock "OpenPencil"
- Splash loader during WASM initialization
+- Rich text style runs: per-selection ⌘B/I/U, StyleRun model, ParagraphBuilder pushStyle/pop, .fig roundtrip
+- B/I/U/S toggle buttons in TypographySection
+- Double-click (word), triple-click (select all) text selection
**Remaining (deferred to Phase 6):**
- Variant switching
@@ -98,7 +101,12 @@ Core extraction, CLI, MCP server, design guidelines, screenshot verification loo
**Delivered:**
- @open-pencil/core extracted to packages/core/ (zero DOM deps, Bun workspace)
-- @open-pencil/cli with headless .fig operations (info, tree, find, export), CanvasKit CPU rasterization, --json output
+- @open-pencil/cli with headless .fig operations (info, tree, find, export, analyze, node, pages, variables), CanvasKit CPU rasterization, --json output
+- JSX renderer: TreeNode builders (Frame, Text, Rectangle, etc.), renderTreeNode/renderJsx, Tailwind-like shorthand props, 27 tests
+- jscpd copy-paste detection (15.6% → 0.62%), kiwi-serialize.ts consolidation
+- .fig roundtrip tests with LFS fixtures (material3.fig 87K nodes, nuxtui.fig 314K nodes)
+- .fig import O(n²) → O(n) fix (37s → 535ms on 87K nodes), ByteBuffer optimization
+- test:coverage script
- npm publishing preparation for core and cli packages
**Planned:**
diff --git a/docs/guide/features.md b/docs/guide/features.md
index 632b60d8a..6cab6d426 100644
--- a/docs/guide/features.md
+++ b/docs/guide/features.md
@@ -33,10 +33,18 @@ Shift+A toggles auto-layout on a frame or wraps selected nodes.
Canvas-native text editing — no DOM textarea overlay on screen. A `TextEditor` class in `@open-pencil/core` handles cursor positioning, text selection, word boundary detection, and line navigation using the CanvasKit Paragraph API (`getGlyphPositionAtCoordinate`, `getRectsForRange`, `getLineMetrics`). A hidden phantom textarea captures keyboard input, IME composition, and clipboard events.
-Double-click a text node to enter edit mode. The canvas renders a blinking caret, translucent blue selection rectangles, and a blue outline around the node. Click and drag to select text, double-click a word to select it. Keyboard navigation with modifier support: ⌥←/→ for word movement, ⌘←/→ for line start/end, ⌥⌫ for word delete, ⌘⌫ for line delete. Shift extends selection. Esc or clicking outside commits the edit.
+Double-click a text node to enter edit mode. The canvas renders a blinking caret, translucent blue selection rectangles, and a blue outline around the node. Click and drag to select text, double-click a word to select it, triple-click to select all. Keyboard navigation with modifier support: ⌥←/→ for word movement, ⌘←/→ for line start/end, ⌥⌫ for word delete, ⌘⌫ for line delete. Shift extends selection. Esc or clicking outside commits the edit.
**Font picker** with virtual scroll (reka-ui ListboxVirtualizer), search filter, and CSS font preview — each font name renders in its own typeface. In Tauri, system fonts are enumerated via Rust `font-kit` crate (`list_system_fonts`/`load_system_font` commands) with OnceLock caching for instant picker access. In browser, the Local Font Access API is used when available.
+## Rich Text Formatting
+
+Per-character formatting within a single text node. Select text and press ⌘B for bold, ⌘I for italic, ⌘U for underline, or use the B/I/U/S buttons in the Typography section. With no selection, the shortcut toggles the whole-node style.
+
+Implemented via a StyleRun model — an array of `{start, length, style}` segments where style includes fontWeight, italic, and textDecoration. The renderer uses CanvasKit ParagraphBuilder.pushStyle/pop to render mixed formatting in a single paragraph. Style runs adjust automatically on insert and delete to preserve formatting boundaries.
+
+Rich text formatting is preserved during .fig import/export — `characterStyleIDs` and `styleOverrideTable` from Figma's TextData are imported as StyleRun arrays and exported back with a deduped style table.
+
## Undo/Redo
Every operation is undoable — node creation/deletion, moves, resizes, property changes, reparenting, layout changes, and all variable operations (create/delete/rename variables, create/rename collections, color and value changes). The system uses an inverse-command pattern — before applying any change, it snapshots affected fields. The snapshot becomes the inverse. ⌘Z undoes, ⇧⌘Z redoes.
@@ -163,5 +171,26 @@ The engine is extracted to `packages/core/` (@open-pencil/core) — scene-graph,
- `open-pencil tree ` — visual node tree
- `open-pencil find ` — search by name/type
- `open-pencil export ` — render to PNG/JPG/WEBP at any scale
+- `open-pencil analyze colors ` — color palette usage with clustering
+- `open-pencil analyze typography ` — font/size/weight distribution
+- `open-pencil analyze spacing ` — gap/padding values with grid check
+- `open-pencil analyze clusters ` — repeated patterns (potential components)
+- `open-pencil node ` — detailed properties of a node by ID
+- `open-pencil pages ` — list pages with node counts
+- `open-pencil variables ` — list design variables and collections
All commands support `--json` for machine-readable output. Runnable via `bun open-pencil` in the workspace. See [Project Structure](/development/contributing#project-structure) for the full monorepo layout.
+
+## JSX Renderer
+
+Programmatic design creation via TreeNode builder functions exported from `@open-pencil/core`: Frame, Text, Rectangle, Ellipse, and others. Supports Tailwind-like shorthand props — `w`, `h`, `bg`, `rounded`, `flex`, `gap`, `p`/`px`/`py`, `justify`, `items`, `shadow`, `blur`.
+
+Two rendering paths:
+- `renderTreeNode()` — tree → scene graph (any runtime, no external deps)
+- `renderJsx()` — JSX string → esbuild → tree → scene graph (CLI/headless)
+
+Covered by 27 tests for all node types, layout props, effects, and nesting.
+
+## Code Quality
+
+Copy-paste detection via jscpd — reduced project-wide duplication from 15.6% to 0.62%. Kiwi serialization consolidated into `kiwi-serialize.ts` (shared by clipboard, fig-export, and the CLI). The .fig import pipeline was optimized from O(n²) to O(n) by building a children index upfront — material3.fig (87K nodes) went from 37s to 535ms. ByteBuffer optimized with inline readVarUint and TextDecoder for strings.
diff --git a/docs/guide/figma-comparison.md b/docs/guide/figma-comparison.md
index 6da6a538f..f665ab75d 100644
--- a/docs/guide/figma-comparison.md
+++ b/docs/guide/figma-comparison.md
@@ -6,7 +6,7 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
✅ Supported — feature works end-to-end · 🟡 Partial — core behavior exists, some sub-features missing · 🔲 Not yet implemented
:::
-**Coverage:** 79 of 150 Figma feature items addressed — 64 ✅ fully supported, 15 🟡 partial, 71 🔲 not yet. Last updated: 2026-03-01 (notes updated for text editing, fonts, variables).
+**Coverage:** 80 of 150 Figma feature items addressed — 64 ✅ fully supported, 16 🟡 partial, 70 🔲 not yet. Last updated: 2026-03-01.
## Interface & Navigation
@@ -78,13 +78,13 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
| Feature | Status | Notes |
|---------|--------|-------|
-| Text tool & inline editing | ✅ | T to create, double-click to edit, canvas-native with phantom textarea, cursor/selection/word select, drag to select |
+| Text tool & inline editing | ✅ | Canvas-native editing, phantom textarea, cursor/selection/word select, drag to select, double/triple-click, rich text style runs (⌘B/I/U, S button) |
| Text rendering (Paragraph API) | ✅ | CanvasKit Paragraph for shaping, line-breaking, metrics |
| Font loading (system fonts) | ✅ | Inter default, font-kit in Tauri with OnceLock cache + preloading, queryLocalFonts in browser |
| Font family & weight | ✅ | FontPicker with virtual scroll, search, CSS preview; weight selection in properties panel |
| Font size & line height | ✅ | Editable in typography section |
| Text alignment | 🟡 | Basic alignment; Figma has vertical alignment and auto-width/height modes |
-| Text styles | 🔲 | Reusable named text style presets |
+| Text styles | 🟡 | Per-selection bold/italic/underline/strikethrough (⌘B/I/U, S button); not yet reusable named text style presets |
| Text resizing modes (auto, fixed, hug) | 🔲 | Figma's auto-width, auto-height, fixed-size text modes |
| Bulleted & numbered lists | 🔲 | List formatting in text |
| Links in text | 🔲 | Hyperlinks within text content |
@@ -209,7 +209,7 @@ Feature-by-feature comparison of Figma Design capabilities with Open Pencil's cu
| Code snippets | 🔲 | Auto-generated code from layers |
| Figma for VS Code | 🔲 | Editor plugin integration |
| MCP server | 🔲 | Planned for Phase 5; 117-tool MCP server from figma-use |
-| CLI tools | 🟡 | Headless CLI: info, tree, find, export commands; MCP server not yet |
+| CLI tools | 🟡 | Headless CLI: info, tree, find, export, analyze (colors/typography/spacing/clusters), node, pages, variables; MCP server not yet |
## Figma Draw
diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/.openspec.yaml b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/.openspec.yaml
new file mode 100644
index 000000000..0b4defe0e
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-03-01
diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/design.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/design.md
new file mode 100644
index 000000000..263628489
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/design.md
@@ -0,0 +1,22 @@
+# Design: Sync style runs, JSX renderer, CLI expansion, tests
+
+## Approach
+Documentation-only change. Update delta-specs for affected domains, then propagate to VitePress docs and comparison matrix.
+
+## Affected spec domains
+
+1. **text-editing** — rich text style runs (per-selection bold/italic/underline), double/triple-click, selectLine
+2. **canvas-rendering** — mixed-style text rendering with ParagraphBuilder.pushStyle/pop
+3. **editor-ui** — B/I/U/S toggle buttons in TypographySection
+4. **cli** — new commands: analyze (colors/typography/spacing/clusters), node, pages, variables
+5. **tooling** — jscpd, kiwi deduplication, kiwi-serialize.ts, test:coverage
+6. **testing** — .fig roundtrip tests, LFS fixtures, import performance fix, 27 JSX renderer tests
+7. **scene-graph** — JSX renderer (TreeNode builders, renderTreeNode, renderJsx), StyleRun model
+
+## VitePress docs updates
+- features.md: add rich text formatting (⌘B/I/U), JSX renderer, expanded CLI commands, double/triple-click
+- figma-comparison.md: update text editing notes (style runs, mixed formatting), mark new CLI capabilities
+- roadmap.md: add new items to Phase 4/5 delivered
+
+## Risks
+- None — documentation only
diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/proposal.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/proposal.md
new file mode 100644
index 000000000..95373f972
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/proposal.md
@@ -0,0 +1,50 @@
+# Proposal: Sync style runs, JSX renderer, CLI expansion, tests
+
+## Why
+22 commits merged from master (33d3def..6268d8b) introduce rich text formatting, a JSX renderer for programmatic design creation, expanded CLI analysis commands, .fig roundtrip tests, and major deduplication.
+
+## What changed
+
+### Rich text style runs (88bfacd)
+- StyleRun model: `{start, length, style}` with CharacterStyleOverride (fontWeight, italic, textDecoration)
+- Renderer: ParagraphBuilder.pushStyle/pop for mixed-style text within a single text node
+- ⌘B/I/U apply to selection range (toggle per-selection) or whole-node style
+- B/I/U/S toggle buttons in TypographySection
+- .fig roundtrip: import characterStyleIDs + styleOverrideTable from TextData, export with deduped style table
+
+### Text editing improvements (6268d8b)
+- Double-click: select word at position, Triple-click: select all
+- selectLine/selectLineAt added to TextEditor
+
+### JSX renderer in @open-pencil/core (13f562a, a32d80f)
+- TreeNode builder functions (Frame, Text, Rectangle, Ellipse, etc.)
+- renderTreeNode(): tree → scene graph (browser)
+- renderJsx(): JSX string → esbuild → tree → scene graph (CLI/headless)
+- Tailwind-like shorthand props: w/h, bg, rounded, flex, gap, p/px/py, justify, items, shadow, blur
+- 27 tests covering all node types, layout props, effects, nesting
+
+### CLI expansion (36e3f14, 7dcc582)
+- analyze colors: color palette usage with clustering
+- analyze typography: font/size/weight distribution
+- analyze spacing: gap/padding values with grid check
+- analyze clusters: repeated patterns (potential components)
+- node: detailed properties by ID
+- pages: list pages with node counts
+- variables: list design variables and collections
+
+### Deduplication (f92a883, e3ae61e, 20827a2, 46bb923)
+- kiwi-serialize.ts: shared sceneNodeToKiwi, buildFigKiwi, parseFigKiwiChunks
+- Vendored kiwi-schema removed from src/kiwi/ (re-exports from core)
+- jscpd added: 15.6% → 0.62% duplication
+- Shared font weight mapping, layout merge, style-runs expandRuns helper
+
+### .fig testing (f24f6e0, ca42214, f1806f6)
+- Git LFS fixtures: material3.fig (87K nodes), nuxtui.fig (314K nodes)
+- O(n²) getChildren() fix: 37s → 535ms on material3
+- Roundtrip tests: property invariants, encode/decode cycle
+- test:coverage script
+- ByteBuffer optimization: inline readVarUint, TextDecoder for strings
+
+### AI chat design (4fb9290, af6a98c, 017b0aa, cc5dda2)
+- Planning documents only (PLAN.md), no implementation
+- JSX-first rendering, OpenRouter from browser, Stronghold key storage
diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/canvas-rendering/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/canvas-rendering/spec.md
new file mode 100644
index 000000000..9ca11d5af
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/canvas-rendering/spec.md
@@ -0,0 +1,10 @@
+# canvas-rendering Specification (delta)
+
+## New Requirements
+
+### Requirement: Mixed-style text rendering
+The renderer SHALL render text nodes with per-character formatting using CanvasKit ParagraphBuilder.pushStyle/pop. Each StyleRun segment pushes its own TextStyle (weight, slant, decoration) before adding text, then pops to restore the default style.
+
+#### Scenario: Render mixed bold and regular text
+- **WHEN** a text node has "Hello " (regular) + "world" (bold)
+- **THEN** the renderer outputs a single Paragraph with two style segments, "world" visually bold
diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/cli/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/cli/spec.md
new file mode 100644
index 000000000..8be870fb2
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/cli/spec.md
@@ -0,0 +1,41 @@
+# cli Specification (delta)
+
+## New Requirements
+
+### Requirement: Analyze commands
+The CLI SHALL provide `open-pencil analyze ` subcommands for design file analysis:
+- `analyze colors` — color palette usage with clustering
+- `analyze typography` — font/size/weight distribution
+- `analyze spacing` — gap/padding values with grid alignment check
+- `analyze clusters` — repeated patterns that could be components
+
+All subcommands SHALL support `--json` for machine-readable output.
+
+#### Scenario: Analyze colors
+- **WHEN** `bun open-pencil analyze colors design.fig` is run
+- **THEN** a color palette summary with usage counts and clusters is printed
+
+#### Scenario: Analyze clusters
+- **WHEN** `bun open-pencil analyze clusters design.fig` is run
+- **THEN** repeated node patterns that could be components are listed
+
+### Requirement: Node command
+The CLI SHALL provide `open-pencil node ` to display detailed properties of a specific node by ID.
+
+#### Scenario: Node details
+- **WHEN** `bun open-pencil node design.fig abc123` is run
+- **THEN** the node's type, properties, children, and parent are displayed
+
+### Requirement: Pages command
+The CLI SHALL provide `open-pencil pages ` to list all pages with node counts.
+
+#### Scenario: List pages
+- **WHEN** `bun open-pencil pages design.fig` is run
+- **THEN** each page name and its node count are listed
+
+### Requirement: Variables command
+The CLI SHALL provide `open-pencil variables ` to list design variables and collections.
+
+#### Scenario: List variables
+- **WHEN** `bun open-pencil variables design.fig` is run
+- **THEN** all variable collections, modes, and variable values are listed
diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/editor-ui/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/editor-ui/spec.md
new file mode 100644
index 000000000..d873464ff
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/editor-ui/spec.md
@@ -0,0 +1,14 @@
+# editor-ui Specification (delta)
+
+## New Requirements
+
+### Requirement: Rich text formatting buttons
+The TypographySection SHALL include B/I/U/S toggle buttons for Bold, Italic, Underline, and Strikethrough. When text is selected during editing, the buttons apply formatting to the selection. When editing with no selection, they toggle the whole-node style. The active state reflects current selection formatting.
+
+#### Scenario: Bold button with selection
+- **WHEN** user selects text and clicks the Bold button (or presses ⌘B)
+- **THEN** the selection becomes bold and the B button shows active state
+
+#### Scenario: Underline button with no selection
+- **WHEN** user clicks the Underline button during editing with no selection
+- **THEN** the entire text node's underline style toggles
diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/scene-graph/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/scene-graph/spec.md
new file mode 100644
index 000000000..a15d45d11
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/scene-graph/spec.md
@@ -0,0 +1,21 @@
+# scene-graph Specification (delta)
+
+## New Requirements
+
+### Requirement: StyleRun model
+SceneNode SHALL support a `styleRuns` array of `{start, length, style}` where style is CharacterStyleOverride (fontWeight, italic, textDecoration). The scene graph SHALL provide helpers to apply/toggle styles on ranges and adjust runs on insert/delete.
+
+#### Scenario: Apply bold to range
+- **WHEN** applyStyleToRange is called with fontWeight 700 on characters 6–11
+- **THEN** the styleRuns array contains a run for that range with fontWeight 700
+
+### Requirement: JSX renderer
+@open-pencil/core SHALL export TreeNode builder functions (Frame, Text, Rectangle, Ellipse, etc.) and two rendering paths: renderTreeNode() (tree → scene graph, no deps) and renderJsx() (JSX string → esbuild → tree → scene graph, for CLI/headless). Builder functions SHALL accept Tailwind-like shorthand props: w, h, bg, rounded, flex, gap, p/px/py, justify, items, shadow, blur.
+
+#### Scenario: Build tree from functions
+- **WHEN** Frame({w: 200, h: 100, bg: '#f00'}, [Text({children: 'Hello'})]) is called
+- **THEN** a TreeNode with type FRAME, width 200, height 100, and red fill is created with a TEXT child
+
+#### Scenario: Render JSX string
+- **WHEN** renderJsx('Hi') is called
+- **THEN** a scene graph with a FRAME parent and TEXT child is produced
diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/testing/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/testing/spec.md
new file mode 100644
index 000000000..bb2fec182
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/testing/spec.md
@@ -0,0 +1,28 @@
+# testing Specification (delta)
+
+## New Requirements
+
+### Requirement: .fig roundtrip tests
+The test suite SHALL include roundtrip tests for real .fig files: parsing property invariants, encode/decode cycle fidelity. Test fixtures (material3.fig, nuxtui.fig) SHALL be tracked via Git LFS.
+
+#### Scenario: Roundtrip encode/decode
+- **WHEN** a .fig file is parsed and re-encoded
+- **THEN** the re-encoded data decodes to the same node properties
+
+#### Scenario: Property invariants
+- **WHEN** a real .fig file is parsed
+- **THEN** all nodes have valid types, dimensions ≥ 0, and required fields present
+
+### Requirement: .fig import performance
+The .fig import pipeline SHALL avoid O(n²) child resolution. A children index SHALL be built upfront for linear-time lookups.
+
+#### Scenario: Large file import speed
+- **WHEN** material3.fig (87K nodes) is imported
+- **THEN** parsing completes in under 2 seconds (was 37s before fix)
+
+### Requirement: JSX renderer tests
+The test suite SHALL include tests for the JSX renderer covering all node types (Frame, Text, Rectangle, Ellipse, etc.), layout props, effects, and nesting.
+
+#### Scenario: Render Frame with children
+- **WHEN** a JSX tree with Frame containing Rectangle and Text is rendered
+- **THEN** the scene graph contains correct parent-child relationships and properties
diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/text-editing/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/text-editing/spec.md
new file mode 100644
index 000000000..331428435
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/text-editing/spec.md
@@ -0,0 +1,45 @@
+# text-editing Specification (delta)
+
+## New Requirements
+
+### Requirement: Rich text style runs
+Text nodes SHALL support per-character formatting via StyleRun arrays: `{start, length, style}` where style includes fontWeight, italic (boolean), and textDecoration (UNDERLINE, STRIKETHROUGH). ⌘B toggles bold, ⌘I toggles italic, ⌘U toggles underline on the current selection. With no selection, the shortcut toggles the whole-node style. Style runs adjust on insert/delete to preserve formatting.
+
+#### Scenario: Bold selection
+- **WHEN** user selects "world" in "Hello world" and presses ⌘B
+- **THEN** the word "world" renders bold while "Hello " stays regular
+
+#### Scenario: Toggle italic on whole node
+- **WHEN** user presses ⌘I with no text selected in a text node
+- **THEN** the entire text node toggles italic
+
+#### Scenario: Style preservation on insert
+- **WHEN** user types "X" between a bold and regular segment
+- **THEN** the inserted character inherits the style of the preceding segment and run boundaries adjust
+
+### Requirement: .fig roundtrip for style runs
+The .fig import SHALL parse characterStyleIDs and styleOverrideTable from TextData into StyleRun arrays. The .fig export SHALL write back with a deduped style table.
+
+#### Scenario: Import rich text from .fig
+- **WHEN** a .fig file with mixed bold/italic text is imported
+- **THEN** the style runs are correctly reconstructed
+
+## Modified Requirements
+
+### Requirement: Text selection via mouse drag (MODIFIED)
+Double-click SHALL select the word at click position. Triple-click SHALL select the entire text. Single click positions cursor. Drag extends selection. Click outside commits.
+
+#### Scenario: Double-click selects word
+- **WHEN** user double-clicks on "world" in "Hello world" during editing
+- **THEN** "world" is selected
+
+#### Scenario: Triple-click selects all
+- **WHEN** user triple-clicks inside a text node during editing
+- **THEN** all text is selected
+
+### Requirement: TextEditor class in core (MODIFIED)
+TextEditor SHALL additionally support selectLine(pos) and selectLineAt(x, y) methods for line-level selection.
+
+#### Scenario: Select line
+- **WHEN** selectLineAt is called at a position within line 2
+- **THEN** the entire second line is selected
diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/tooling/spec.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/tooling/spec.md
new file mode 100644
index 000000000..9ed2ea06b
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/specs/tooling/spec.md
@@ -0,0 +1,24 @@
+# tooling Specification (delta)
+
+## New Requirements
+
+### Requirement: Copy-paste detection
+The project SHALL use jscpd for copy-paste detection. The `bun run jscpd` command SHALL scan for duplicated code blocks.
+
+#### Scenario: Detect duplicates
+- **WHEN** `bun run jscpd` is run
+- **THEN** duplicated code blocks are reported with locations and percentages
+
+### Requirement: Kiwi serialization consolidation
+Shared kiwi serialization logic (sceneNodeToKiwi, buildFigKiwi, parseFigKiwiChunks, decompressFigKiwiDataAsync) SHALL be extracted to packages/core/src/kiwi-serialize.ts. The app's src/kiwi/ SHALL re-export from core, eliminating the vendored kiwi-schema copy.
+
+#### Scenario: Clipboard and fig-export share serialization
+- **WHEN** clipboard.ts and fig-export.ts serialize nodes to kiwi
+- **THEN** both use the shared kiwi-serialize.ts functions
+
+### Requirement: Test coverage script
+The project SHALL include a `test:coverage` script for measuring code coverage.
+
+#### Scenario: Run coverage
+- **WHEN** `bun run test:coverage` is run
+- **THEN** test coverage metrics are reported
diff --git a/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/tasks.md b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/tasks.md
new file mode 100644
index 000000000..ffb307c72
--- /dev/null
+++ b/openspec/changes/archive/2026-03-01-sync-style-runs-jsx-cli-tests/tasks.md
@@ -0,0 +1,42 @@
+# Tasks: Sync style runs, JSX renderer, CLI expansion, tests
+
+Reference: 22 commits from master (33d3def..6268d8b).
+Delta-specs in `openspec/changes/sync-style-runs-jsx-cli-tests/specs/` (text-editing, canvas-rendering, editor-ui, cli, tooling, testing, scene-graph).
+Proposal: `proposal.md`, Design: `design.md` — validated and aligned with delta-specs.
+
+Section 1 (specs) MUST complete before Section 2 (docs) — docs reference merged spec content.
+
+## 1. Merge Delta-Specs into Main Specs
+
+Each task merges ADDED and MODIFIED requirements from the delta-spec into the baseline at `openspec/specs//spec.md`. Untouched baseline requirements are preserved. No conflicts between domains — each delta-spec targets a distinct section of its baseline.
+
+- [x] 1.1 Update `openspec/specs/text-editing/spec.md` — ADDED: "Rich text style runs" (⌘B/I/U per-selection, style preservation on insert/delete), ".fig roundtrip for style runs" (import characterStyleIDs, export deduped table). MODIFIED: "Text selection via mouse drag" (add double-click word, triple-click all), "TextEditor class in core" (add selectLine/selectLineAt)
+- [x] 1.2 Update `openspec/specs/canvas-rendering/spec.md` — ADDED: "Mixed-style text rendering" (ParagraphBuilder pushStyle/pop per StyleRun segment)
+- [x] 1.3 Update `openspec/specs/editor-ui/spec.md` — ADDED: "Rich text formatting buttons" (B/I/U/S toggles in TypographySection, selection-aware)
+- [x] 1.4 Update `openspec/specs/cli/spec.md` — ADDED: "Analyze commands" (colors/typography/spacing/clusters with --json), "Node command" (properties by ID), "Pages command" (list with counts), "Variables command" (collections and values)
+- [x] 1.5 Update `openspec/specs/tooling/spec.md` — ADDED: "Copy-paste detection" (jscpd, 15.6%→0.62%), "Kiwi serialization consolidation" (kiwi-serialize.ts shared by clipboard + fig-export), "Test coverage script" (test:coverage)
+- [x] 1.6 Update `openspec/specs/testing/spec.md` — ADDED: ".fig roundtrip tests" (LFS fixtures material3/nuxtui, property invariants, encode/decode cycle), ".fig import performance" (O(n²)→O(n), 37s→535ms), "JSX renderer tests" (27 tests)
+- [x] 1.7 Update `openspec/specs/scene-graph/spec.md` — ADDED: "StyleRun model" (per-character formatting array on SceneNode), "JSX renderer" (TreeNode builders + renderTreeNode + renderJsx, Tailwind-like shorthand props). These are two independent additions to scene-graph — StyleRun extends the text data model, JSX renderer adds a programmatic creation API
+
+## 2. Update VitePress Docs
+
+All three target files already exist — no sidebar/nav changes needed.
+
+- [x] 2.1 Update `docs/guide/features.md` — modify existing sections and add new ones:
+ - **Modify "Inline Text Editing"**: add double-click (word), triple-click (all) to existing paragraph
+ - **Add "Rich Text Formatting" section** (after "Inline Text Editing"): ⌘B/I/U toggle per-selection bold/italic/underline, ⌘U for underline, strikethrough via S button in TypographySection, StyleRun model, .fig roundtrip preserves formatting
+ - **Add "JSX Renderer" section** (after "@open-pencil/core & CLI"): TreeNode builders (Frame, Text, Rectangle, etc.), Tailwind-like shorthand props (w/h/bg/rounded/flex/gap/p), renderTreeNode() for browser, renderJsx() for CLI/headless via esbuild, 27 tests
+ - **Expand CLI commands** in "@open-pencil/core & CLI" section: add analyze (colors/typography/spacing/clusters), node, pages, variables to the list
+ - **Add "Code Quality"** section (after "CI/CD Builds"): jscpd copy-paste detection (15.6%→0.62%), kiwi-serialize.ts consolidation, .fig import O(n²) fix (37s→535ms on 87K nodes)
+- [x] 2.2 Update `docs/guide/figma-comparison.md` — specific row changes:
+ - "Text tool & inline editing" ✅: add "rich text style runs (⌘B/I/U), double/triple-click" to notes
+ - "Text styles" 🔲→🟡: "Per-selection bold/italic/underline/strikethrough via ⌘B/I/U and S button; not yet reusable named text styles"
+ - "CLI tools" 🟡: add "analyze colors/typography/spacing/clusters, node, pages, variables" to notes
+ - No other rows change status. After: 64✅, 16🟡, 70🔲 = 150 total. Update coverage line
+- [x] 2.3 Update `docs/development/roadmap.md` — specific phase assignments:
+ - **Phase 4 Delivered** (text/rendering): rich text style runs (⌘B/I/U per-selection, StyleRun model, .fig roundtrip), B/I/U/S buttons in TypographySection, double/triple-click text selection
+ - **Phase 5 Delivered** (tooling/infra): JSX renderer (TreeNode builders, renderTreeNode/renderJsx, 27 tests), expanded CLI (analyze/node/pages/variables), jscpd (15.6%→0.62%), kiwi-serialize.ts consolidation, .fig roundtrip tests (LFS fixtures), import O(n²) fix, test:coverage script
+
+## 3. Verify
+
+- [x] 3.1 Run `bun run docs:build` — verify build passes. Manually confirm: coverage count in figma-comparison.md matches actual ✅/🟡/🔲 tallies, no broken internal anchor links in output
diff --git a/openspec/specs/canvas-rendering/spec.md b/openspec/specs/canvas-rendering/spec.md
index 002e33330..0b3975d17 100644
--- a/openspec/specs/canvas-rendering/spec.md
+++ b/openspec/specs/canvas-rendering/spec.md
@@ -206,3 +206,10 @@ During text editing, the renderer SHALL draw on the canvas: a blue outline aroun
#### Scenario: Blue outline during editing
- **WHEN** user is editing a text node
- **THEN** a blue outline is drawn around the text node bounds (at selection color)
+
+### Requirement: Mixed-style text rendering
+The renderer SHALL render text nodes with per-character formatting using CanvasKit ParagraphBuilder.pushStyle/pop. Each StyleRun segment pushes its own TextStyle (weight, slant, decoration) before adding text, then pops to restore the default style.
+
+#### Scenario: Render mixed bold and regular text
+- **WHEN** a text node has "Hello " (regular) + "world" (bold)
+- **THEN** the renderer outputs a single Paragraph with two style segments, "world" visually bold
diff --git a/openspec/specs/cli/spec.md b/openspec/specs/cli/spec.md
index 31553fbbe..238c8c1c2 100644
--- a/openspec/specs/cli/spec.md
+++ b/openspec/specs/cli/spec.md
@@ -14,6 +14,13 @@ The CLI SHALL support the following commands:
- `open-pencil tree ` — visual node tree with formatted output
- `open-pencil find ` — search nodes by name or type
- `open-pencil export ` — render to PNG/JPG/WEBP at any scale
+- `open-pencil analyze colors ` — color palette usage with clustering
+- `open-pencil analyze typography ` — font/size/weight distribution
+- `open-pencil analyze spacing ` — gap/padding values with grid alignment check
+- `open-pencil analyze clusters ` — repeated patterns (potential components)
+- `open-pencil node ` — detailed properties of a specific node
+- `open-pencil pages ` — list pages with node counts
+- `open-pencil variables ` — list design variables and collections
All commands SHALL support `--json` for machine-readable output.
@@ -35,3 +42,35 @@ The CLI SHALL be runnable via `bun open-pencil` within the Bun workspace, withou
#### Scenario: Run from workspace root
- **WHEN** `bun open-pencil info design.fig` is run from the project root
- **THEN** the CLI executes using the workspace-linked binary
+
+### Requirement: Analyze commands
+The CLI SHALL provide `open-pencil analyze ` subcommands for design file analysis: colors (palette usage with clustering), typography (font/size/weight distribution), spacing (gap/padding values with grid check), clusters (repeated patterns that could be components).
+
+#### Scenario: Analyze colors
+- **WHEN** `bun open-pencil analyze colors design.fig` is run
+- **THEN** a color palette summary with usage counts and clusters is printed
+
+#### Scenario: Analyze clusters
+- **WHEN** `bun open-pencil analyze clusters design.fig` is run
+- **THEN** repeated node patterns that could be components are listed
+
+### Requirement: Node command
+The CLI SHALL provide `open-pencil node ` to display detailed properties of a specific node by ID.
+
+#### Scenario: Node details
+- **WHEN** `bun open-pencil node design.fig abc123` is run
+- **THEN** the node's type, properties, children, and parent are displayed
+
+### Requirement: Pages command
+The CLI SHALL provide `open-pencil pages ` to list all pages with node counts.
+
+#### Scenario: List pages
+- **WHEN** `bun open-pencil pages design.fig` is run
+- **THEN** each page name and its node count are listed
+
+### Requirement: Variables command
+The CLI SHALL provide `open-pencil variables ` to list design variables and collections.
+
+#### Scenario: List variables
+- **WHEN** `bun open-pencil variables design.fig` is run
+- **THEN** all variable collections, modes, and variable values are listed
diff --git a/openspec/specs/editor-ui/spec.md b/openspec/specs/editor-ui/spec.md
index e67d19190..56cc2a7d1 100644
--- a/openspec/specs/editor-ui/spec.md
+++ b/openspec/specs/editor-ui/spec.md
@@ -281,3 +281,14 @@ The demo document SHALL include three variable collections demonstrating multi-m
#### Scenario: Demo loads with variables
- **WHEN** the demo document is created
- **THEN** three collections with variables and bindings are present in the scene graph
+
+### Requirement: Rich text formatting buttons
+The TypographySection SHALL include B/I/U/S toggle buttons for Bold, Italic, Underline, and Strikethrough. When text is selected during editing, the buttons apply formatting to the selection. When editing with no selection, they toggle the whole-node style. The active state reflects current selection formatting.
+
+#### Scenario: Bold button with selection
+- **WHEN** user selects text and clicks the Bold button (or presses ⌘B)
+- **THEN** the selection becomes bold and the B button shows active state
+
+#### Scenario: Underline button with no selection
+- **WHEN** user clicks the Underline button during editing with no selection
+- **THEN** the entire text node's underline style toggles
diff --git a/openspec/specs/scene-graph/spec.md b/openspec/specs/scene-graph/spec.md
index fdf6968e1..c93338958 100644
--- a/openspec/specs/scene-graph/spec.md
+++ b/openspec/specs/scene-graph/spec.md
@@ -245,3 +245,21 @@ Removing a variable SHALL clean up all bindings referencing it across all nodes.
#### Scenario: Remove variable with bindings
- **WHEN** a variable bound to three nodes is removed
- **THEN** all three nodes' variableBindings no longer reference that variable
+
+### Requirement: StyleRun model
+SceneNode SHALL support a `styleRuns` array of `{start, length, style}` where style is CharacterStyleOverride (fontWeight, italic, textDecoration). The scene graph SHALL provide helpers to apply/toggle styles on ranges and adjust runs on insert/delete.
+
+#### Scenario: Apply bold to range
+- **WHEN** applyStyleToRange is called with fontWeight 700 on characters 6–11
+- **THEN** the styleRuns array contains a run for that range with fontWeight 700
+
+### Requirement: JSX renderer
+@open-pencil/core SHALL export TreeNode builder functions (Frame, Text, Rectangle, Ellipse, etc.) and two rendering paths: renderTreeNode() (tree → scene graph, no deps) and renderJsx() (JSX string → esbuild → tree → scene graph, for CLI/headless). Builder functions SHALL accept Tailwind-like shorthand props: w, h, bg, rounded, flex, gap, p/px/py, justify, items, shadow, blur.
+
+#### Scenario: Build tree from functions
+- **WHEN** Frame({w: 200, h: 100, bg: '#f00'}, [Text({children: 'Hello'})]) is called
+- **THEN** a TreeNode with type FRAME, width 200, height 100, and red fill is created with a TEXT child
+
+#### Scenario: Render JSX string
+- **WHEN** renderJsx('Hi') is called
+- **THEN** a scene graph with a FRAME parent and TEXT child is produced
diff --git a/openspec/specs/testing/spec.md b/openspec/specs/testing/spec.md
index 78d772b7b..530fae253 100644
--- a/openspec/specs/testing/spec.md
+++ b/openspec/specs/testing/spec.md
@@ -96,3 +96,28 @@ Unit tests SHALL cover the variables system: add and resolve color/number variab
- **WHEN** `bun test ./tests/engine` is run
- **THEN** all variable tests pass (add/resolve color, resolve number, alias chain, circular alias, mode switching, bind/unbind, removal cleanup)
+
+### Requirement: .fig roundtrip tests
+The test suite SHALL include roundtrip tests for real .fig files: parsing property invariants, encode/decode cycle fidelity. Test fixtures (material3.fig, nuxtui.fig) SHALL be tracked via Git LFS.
+
+#### Scenario: Roundtrip encode/decode
+- **WHEN** a .fig file is parsed and re-encoded
+- **THEN** the re-encoded data decodes to the same node properties
+
+#### Scenario: Property invariants
+- **WHEN** a real .fig file is parsed
+- **THEN** all nodes have valid types, dimensions ≥ 0, and required fields present
+
+### Requirement: .fig import performance
+The .fig import pipeline SHALL avoid O(n²) child resolution. A children index SHALL be built upfront for linear-time lookups.
+
+#### Scenario: Large file import speed
+- **WHEN** material3.fig (87K nodes) is imported
+- **THEN** parsing completes in under 2 seconds (was 37s before fix)
+
+### Requirement: JSX renderer tests
+The test suite SHALL include tests for the JSX renderer covering all node types (Frame, Text, Rectangle, Ellipse, etc.), layout props, effects, and nesting. Currently 27 tests.
+
+#### Scenario: Render Frame with children
+- **WHEN** a JSX tree with Frame containing Rectangle and Text is rendered
+- **THEN** the scene graph contains correct parent-child relationships and properties
diff --git a/openspec/specs/text-editing/spec.md b/openspec/specs/text-editing/spec.md
index 0d38d9bf1..a8033c5bc 100644
--- a/openspec/specs/text-editing/spec.md
+++ b/openspec/specs/text-editing/spec.md
@@ -24,7 +24,7 @@ Double-clicking a text node SHALL enter canvas-native inline editing mode. A hid
- **THEN** the phantom textarea handles compositionstart/compositionend events and the composed text is inserted into the TextEditor
### Requirement: TextEditor class in core
-A TextEditor class SHALL live in packages/core/src/text-editor.ts providing cursor positioning, text selection, word boundary detection, and line navigation using CanvasKit Paragraph API (getGlyphPositionAtCoordinate, getRectsForRange, getLineMetrics). It SHALL support: insert, backspace, delete, select all, select word, move left/right/up/down, move to line start/end, move word left/right, and extend selection with Shift.
+A TextEditor class SHALL live in packages/core/src/text-editor.ts providing cursor positioning, text selection, word boundary detection, and line navigation using CanvasKit Paragraph API (getGlyphPositionAtCoordinate, getRectsForRange, getLineMetrics). It SHALL support: insert, backspace, delete, select all, select word, selectLine, selectLineAt, move left/right/up/down, move to line start/end, move word left/right, and extend selection with Shift.
#### Scenario: Cursor positioning
- **WHEN** user clicks inside a text node during editing
@@ -34,6 +34,10 @@ A TextEditor class SHALL live in packages/core/src/text-editor.ts providing curs
- **WHEN** user double-clicks a word during text editing
- **THEN** the word is selected (from word boundary to word boundary)
+#### Scenario: Line selection
+- **WHEN** selectLineAt is called at a position within line 2
+- **THEN** the entire second line is selected
+
#### Scenario: Line navigation
- **WHEN** user presses ⌘← during text editing
- **THEN** the cursor moves to the start of the current line
@@ -50,12 +54,20 @@ The phantom textarea SHALL handle keyboard navigation with modifier support: ⌥
- **THEN** the selected text is copied to the clipboard
### Requirement: Text selection via mouse drag
-Clicking inside a text node during editing SHALL position the cursor. Dragging SHALL extend the selection. Clicking outside the text node bounds SHALL commit the edit and exit editing mode.
+Clicking inside a text node during editing SHALL position the cursor. Dragging SHALL extend the selection. Double-click SHALL select the word at click position. Triple-click SHALL select all text. Clicking outside the text node bounds SHALL commit the edit and exit editing mode.
#### Scenario: Drag to select
- **WHEN** user clicks and drags inside a text node during editing
- **THEN** the text between click start and current position is selected
+#### Scenario: Double-click selects word
+- **WHEN** user double-clicks on "world" in "Hello world" during editing
+- **THEN** "world" is selected
+
+#### Scenario: Triple-click selects all
+- **WHEN** user triple-clicks inside a text node during editing
+- **THEN** all text is selected
+
#### Scenario: Click outside commits
- **WHEN** user clicks outside the editing text node
- **THEN** the text edit is committed and editing mode exits
@@ -110,3 +122,25 @@ A blue outline SHALL appear around a text node during editing to indicate edit m
#### Scenario: Edit mode indicator
- **WHEN** user is editing a text node inline
- **THEN** a blue outline is visible around the text node bounds
+
+### Requirement: Rich text style runs
+Text nodes SHALL support per-character formatting via StyleRun arrays: `{start, length, style}` where style includes fontWeight, italic (boolean), and textDecoration (UNDERLINE, STRIKETHROUGH). ⌘B toggles bold, ⌘I toggles italic, ⌘U toggles underline on the current selection. With no selection, the shortcut toggles the whole-node style. Style runs adjust on insert/delete to preserve formatting.
+
+#### Scenario: Bold selection
+- **WHEN** user selects "world" in "Hello world" and presses ⌘B
+- **THEN** the word "world" renders bold while "Hello " stays regular
+
+#### Scenario: Toggle italic on whole node
+- **WHEN** user presses ⌘I with no text selected in a text node
+- **THEN** the entire text node toggles italic
+
+#### Scenario: Style preservation on insert
+- **WHEN** user types "X" between a bold and regular segment
+- **THEN** the inserted character inherits the style of the preceding segment and run boundaries adjust
+
+### Requirement: .fig roundtrip for style runs
+The .fig import SHALL parse characterStyleIDs and styleOverrideTable from TextData into StyleRun arrays. The .fig export SHALL write back with a deduped style table.
+
+#### Scenario: Import rich text from .fig
+- **WHEN** a .fig file with mixed bold/italic text is imported
+- **THEN** the style runs are correctly reconstructed
diff --git a/openspec/specs/tooling/spec.md b/openspec/specs/tooling/spec.md
index 1330fa3be..7e2d83c75 100644
--- a/openspec/specs/tooling/spec.md
+++ b/openspec/specs/tooling/spec.md
@@ -88,3 +88,24 @@ The project SHALL use Bun workspaces with packages: root (app), packages/core (@
### Requirement: npm publishing preparation
@open-pencil/core and @open-pencil/cli SHALL have proper package.json fields for npm publishing: name, version, description, exports, main, types, files, license, repository.
+
+### Requirement: Copy-paste detection
+The project SHALL use jscpd for copy-paste detection. The `bun run jscpd` command SHALL scan for duplicated code blocks.
+
+#### Scenario: Detect duplicates
+- **WHEN** `bun run jscpd` is run
+- **THEN** duplicated code blocks are reported with locations and percentages
+
+### Requirement: Kiwi serialization consolidation
+Shared kiwi serialization logic (sceneNodeToKiwi, buildFigKiwi, parseFigKiwiChunks, decompressFigKiwiDataAsync) SHALL be extracted to packages/core/src/kiwi-serialize.ts. The app's src/kiwi/ SHALL re-export from core, eliminating the vendored kiwi-schema copy.
+
+#### Scenario: Clipboard and fig-export share serialization
+- **WHEN** clipboard.ts and fig-export.ts serialize nodes to kiwi
+- **THEN** both use the shared kiwi-serialize.ts functions
+
+### Requirement: Test coverage script
+The project SHALL include a `test:coverage` script for measuring code coverage.
+
+#### Scenario: Run coverage
+- **WHEN** `bun run test:coverage` is run
+- **THEN** test coverage metrics are reported