/**
* block-content — pure content-shape helpers for text blocks.
*
* The editor's blank-area flow (page-view) needs three answers about a
* block, and all of them must be DOM-free + deterministic (unit-testable
* without a browser, per AGENTS.md "prefer deterministic behavior"):
*
* 1. `isAutoCleanableType(type)` — which block types the empty-block
* cleanup may touch (text-editable types only).
* 2. `isContentEmpty(content)` — is the stored content empty?
* 3. `isEmptyTextBlock(block)` — the full guard: text-editable type,
* NOT the page title, and empty content.
*
* WHY THE STRICT CONTENT CHECK (never guess "empty"):
* Deleting a block is irreversible. The stored `content` has exactly two
* shapes in the wild (see `InlineEditor.toHtml`):
*
* - `string` — web edits store Tiptap HTML. Empty ⇔ no tags AND no
* non-whitespace text (`""`, `"
"`, `"
"`).
* - `{}` — the API/Prisma default for a freshly-created block (nothing
* ever typed). Empty: an object with no keys provably carries no data,
* so deleting/reusing it can lose nothing.
* - `{ text?: string }` / `{ title?: string }` — the API/AI canonical
* envelope (page-title block, blocks created via API/import/AI).
* Empty ⇔ no displayable text.
*
* Any OTHER unrecognized object shape (tables, arrays, nested docs,
* non-empty objects) is treated as NON-EMPTY. A false negative just
* leaves a block around; a false positive deletes user data.
*
* WHY `isTitle` IS PROTECTED:
* Every page is seeded with a title block (a top-level `paragraph` with
* `properties.isTitle: true`, see page.service.ts `create`). It shares the
* top-level list with content blocks, so the type check alone cannot
* distinguish it — the flag must. Deleting it would orphan the page's
* title; the header already edits the title via `PATCH /pages/:id`.
*/
/**
* Block types edited through the inline text editor (Tiptap) that may be
* auto-cleaned. These are exactly the types rendered via `InlineEditor`,
* which is where the `onEmptyBlur` hook + the shared caret-mover
* (`focusBlockEditable`) attach.
*
* Deliberately EXCLUDED:
* - `toggle` — a toggle with no title text can still own CHILD blocks (a
* container — deleting it cascade-deletes its children), and its child
* rows are nested rows outside the top-level blank-area scope.
* - `code` — edited by `CodeBlock`'s own `