diff --git a/STYLED_SOURCE_DESIGN.md b/STYLED_SOURCE_DESIGN.md
new file mode 100644
index 0000000..00c783c
--- /dev/null
+++ b/STYLED_SOURCE_DESIGN.md
@@ -0,0 +1,161 @@
+# Styled Source Editor — Design Plan
+
+## Core Concept
+
+The editor is always a markdown text editor. There is no separate "WYSIWYG mode" —
+the user edits markdown directly, but the editor applies CSS styling that makes it
+look like rendered output. Delimiters (`**`, `*`, `` ` ``, etc.) are hidden when the
+cursor is outside the element and revealed when the cursor enters it.
+
+## Two CSS States (not modes)
+
+- **Editing**: `contentEditable="true"`, delimiters revealed on cursor focus
+- **Viewing**: `contentEditable="false"`, all delimiters hidden
+
+No content transformation on state switch. The DOM is identical in both states —
+only CSS changes. This eliminates all conversion-during-editing bugs.
+
+## DOM Structure
+
+The editor contains markdown text wrapped in styled spans:
+
+```html
+
+
+ ## Hello World
+
+
+ Some
+ **bold**
+ and
+ *italic*
+ text.
+
+
+ - First item
+
+
+ > Quoted text
+
+
+```
+
+CSS handles all visual rendering:
+
+```css
+.md-delim { display: none; color: #999; font-weight: normal; }
+.md-bold.editing .md-delim,
+.md-italic.editing .md-delim { display: inline; }
+.md-bold { font-weight: bold; }
+.md-italic { font-style: italic; }
+.md-heading[data-level="1"] { font-size: 2em; font-weight: bold; }
+.md-list-item { display: list-item; margin-left: 1.5em; }
+.md-blockquote { border-left: 3px solid #ccc; padding-left: 1em; }
+.md-code { font-family: monospace; background: #f5f5f5; }
+```
+
+## Per-Keystroke Pipeline
+
+1. User types a character → browser inserts it into the DOM (contentEditable)
+2. `input` event fires
+3. Parser scans the **current line only** (the block element containing the cursor)
+4. If the span structure needs updating (e.g. user just typed the closing `**`):
+ - Wrap/unwrap the affected text range using targeted DOM operations
+ - No innerHTML rebuild, no full-document re-parse
+5. If a block pattern is detected (e.g. `# ` at start of line):
+ - Update the block element's class and data attributes
+ - Move the delimiter text into a `.md-delim` span
+
+## Key Operations
+
+### Inline formatting detection
+When the user types a delimiter character, scan backward in the current
+text node for a matching opener. If found, wrap the range:
+
+```
+Before: hello **world**
+After: hello
+ **world**
+
+```
+
+Use `Range` and `surroundContents` for the wrap — no innerHTML.
+
+### Block detection
+When the user types a space after `#`, `>`, `-`, `1.`, etc. at the start
+of a line, update the block element:
+
+```
+Before: # Title
+After:
+ # Title
+
+```
+
+### Cursor focus tracking
+On `selectionchange`, find the nearest formatting span and add an
+`.editing` class so CSS reveals its delimiters. Remove `.editing`
+from the previous span.
+
+## getMarkdown()
+
+Read `textContent` from the editor element. The delimiter spans contain
+the actual delimiter characters, so `textContent` produces valid markdown.
+No conversion needed.
+
+## getHTML()
+
+Run the existing tokenizer + `toHTML` pipeline on the markdown string
+from `getMarkdown()`. This is only called on demand (export, save, API),
+never during editing.
+
+## Macros
+
+Macros are rendered as `contentEditable="false"` islands within the
+editable text. The macro source (`@user`) is stored in a `data-source`
+attribute. The rendered output is displayed inside the island. On focus,
+the island could expand to show the source for editing.
+
+For `toMarkdown`, macro islands emit their `data-source` value.
+
+## Initial Load
+
+Markdown → styled source DOM is a one-time conversion on editor init:
+
+1. Parse markdown using the existing tokenizer (produces token stream)
+2. Walk the token stream, creating the span structure described above
+3. Set the editor's innerHTML once
+
+This replaces the current `toHTML` → innerHTML path.
+
+## What This Eliminates
+
+- `transformInline` and its innerHTML rebuild
+- `blockToMarkdown` / `nodeToMarkdown` (DOM → markdown string → DOM)
+- The flatten-rebuild pipeline and all its escaping bugs
+- The `
` + ZWS cursor anchor workarounds
+- The sentinel marker system for preserved HTML elements
+- Mode switch conversions (WYSIWYG ↔ view ↔ edit)
+
+## What This Keeps
+
+- The tokenizer (for initial load and `getHTML()`)
+- The serializer (for `getHTML()` via `toMarkdown` → `toHTML`)
+- Tag definitions (for block pattern matching and toolbar buttons)
+- The `BaseTag` keyboard dispatch system
+- The collaboration transport layer
+- The macro system
+
+## Implementation Order
+
+1. Build the markdown → styled DOM renderer (replaces `toHTML` for editor init)
+2. Build the per-line parser that updates span structure on keystroke
+3. Build the inline delimiter detection (wrap/unwrap via Range)
+4. Wire up cursor focus tracking for delimiter reveal
+5. Implement `getMarkdown()` as `textContent` read
+6. Remove `transformInline`, `blockToMarkdown`, and the rebuild pipeline
+7. Update tests
+
+## Branch
+
+Work on the `styled-source` branch, branched from current `main`.