diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6190cf3..c7e26db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,25 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
+## [1.12.85] — 2026-07-03
+
+### Performance
+- **Heading OCR renders only the top band, not the whole page** — the OCR pass rasterized every full page at 2.5× scale (~6M pixels for A4) and then cropped ~10% of it; the canvas is now sized to the band itself so the remaining ~90% is never rendered or allocated. Also: the Tesseract worker (tens of MB of WASM/language data) is now terminated after extraction instead of living for the whole session, and the top-gap scan no longer allocates a throwaway array per page.
+- **Casting feed name-underlining no longer rebuilds its name index per segment** — the list of character names/aliases and their compiled regexes was recomputed for every rendered segment (O(segments × records) over a full book); it's now cached and invalidated only when the roster or character records actually change. The dialogue-splitting fallback also compiled a constant regex once per sentence-ending character; hoisted to a module constant.
+- **Old casting drafts migrate their page numbers once at load** — drafts saved before segments carried a `.page` field were re-deriving page boundaries via text search on every feed redraw; they're now stamped once when the draft is applied, and the renderer is single-path.
+
+### Changed
+- **`[hidden]` now always hides, globally** — one root rule (`[hidden]{display:none!important}`) replaces the per-component patches this bug class kept requiring (`.ab-char-bar`, `.wf-stepper`, `.reader-extract-banner`, `.ab-castpanel-inline`, and ~20 others individually). New components can no longer reintroduce the empty-box-while-hidden bug.
+- **Removed the racy cast-panel view override** — navigating to Read Aloud restored an active casting session via a deferred `setTimeout` that overrode whatever view the caller had just chosen, which needed a global suppress flag (`_readerSuppressCastRestore`) set from two unrelated places to defeat. The restore is now synchronous and respects an explicit view request, so the flag is gone and callers simply win by calling `showReaderView()` after `navTo()`. This also fixes the sidebar "Reader" item landing in the casting view instead of the reader while a cast was active.
+- **Cards can declare their collapse default in markup** — `data-collapse-default="closed"` on a card is now read by the generic collapse mechanism, replacing reader.js writing another module's localStorage key derived from the card's heading text (which would have silently broken on any heading rename).
+- **Deduplicated helpers** — casting's segment-join, colour (hue/hex/normalize), and alias-splitting logic now delegate to the canonical implementations (`audiobookJoinSegmentText`, `clNormalizeColor`/`clHslToHex`/`clNameHue`, `clSplitIdentityTokens`) instead of maintaining byte-identical private copies; the status bar's LLM endpoint resolution is now a single shared `statusLlmTarget()` instead of two identical inline copies.
+- **Removed dead state** — `readerState.pdfParsing`/`pdfParsedPages`/`pdfParseTotal` (never set, only reset — including a toast suffix that could never appear) and the write-only `readerState.textExtracted` flag.
+
+### Fixed
+- **Pipeline stepper never hid on a fresh session** — the "show only when a document is loaded" guard still checked for the old `'pdf'` step key after the PDF/Text merge into "Source", so the always-available Source stop kept the strip permanently visible.
+
+---
+
## [1.12.84] — 2026-07-03
### Fixed
diff --git a/VERSION b/VERSION
index aad4e4c..60fe64e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.12.84
+1.12.85
diff --git a/static/index.html b/static/index.html
index 75e0fbb..6f4c296 100644
--- a/static/index.html
+++ b/static/index.html
@@ -10,7 +10,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
@@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
-
+