Three things that are currently wrong in how a document renders. Opening this before the first commit.
1. Paragraph spacing does not match a word processor
Paul pasted a list from Google Docs — four lines made with plain returns, one blank line before the last — and it came across as four widely-spaced paragraphs with a hole in the middle.
The structure was faithful; the spacing was not. Our CSS is [docs-text] p { margin-block: 0 1em } with line-height: 1.6. Google Docs' "Normal text" is 0pt before, 0pt after, 1.15 line spacing. So in Docs consecutive paragraphs stack like lines and a visible gap requires an empty paragraph; here every paragraph carries a 12pt gap, and an empty one carries two.
This is worth stating precisely because the obvious diagnosis is wrong: Docs has paragraphs, its Enter makes one, and ours already matches. The difference is entirely spacing.
Changing to:
p { margin-block: 0 }line-height: 1.6 → 1.4 — Docs is 1.15 at Arial 11pt; 1.4 suits 12pt on a serif and still tightens things considerablyp:empty { min-block-size: 1lh }
That last one is the part that would otherwise ship broken. An empty paragraph is currently visible in the server-rendered read view only because it contributes that 1em margin. Remove the margin and <p></p> collapses to zero height — so a blank line would still show while editing (ProseMirror puts a <br> in an empty paragraph) and silently vanish from the read view, the PDF and the Markdown export. Same document, two different shapes.
2. Shift+Enter does nothing
hard_break is in the editor's schema — it comes with prosemirror-schema-basic — but baseKeymap binds only Enter, so nothing inserts one. And core/src/fragment.rs has no case for the node, so even if the browser produced one, the read view and both exports would drop it silently.
With paragraph spacing fixed this becomes the thing you reach for when you want a break inside a paragraph, so it belongs in the same change. Three layers: the keymap, the Rust renderer's HTML and text output, and its JSON, which is what the PDF and Markdown exporters read.
3. Every PDF has no left margin
internal/export/pdf.go:523 draws text at x := ctx.indent, and indent starts at zero — blockContext{scale: 1} at lines 218 and 222. It is only ever added to, for blockquote and list indentation. The vertical cursor starts correctly at MarginPt (line 267); the horizontal never does.
So body text starts at the page edge, and because wrapping still uses the correct 6.5in content width, every page also has a two-inch gutter on the right. MarginPt = 72 was right all along; nothing applied it horizontally.
The fix keeps indent meaning "extra indent beyond the margin" and adds the margin at draw time, which touches the three places that compute an x: the text draw, the blockquote rule, and the list marker.
No test asserts where text starts on a page, which is why a whole-page offset survived a review and shipped. Adding one is part of this.
Not in this branch
Paragraph alignment and the sans-serif default. Both are real and both are agreed; each establishes a pattern that wants its own review rather than riding along with a spacing fix. They follow immediately.