diff --git a/CHANGELOG.md b/CHANGELOG.md
index b7cb52a..2aa9e29 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,18 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
+## [1.12.97] — 2026-07-05
+
+### Fixed
+- **German umlauts mojibake'd during live casting** ("Häfen" → "Häfen") — the streaming attribution endpoint decoded the LLM's SSE response with `requests`' guessed encoding, which falls back to Latin-1 when the upstream doesn't declare a charset. Forced UTF-8 explicitly.
+- **"LLM Thinking" pane duplicated the passage text** for models that ignore the reasoning-preamble instruction and stream straight into JSON. It now only shows genuine `...` content when present, and otherwise labels the pane honestly ("Live Output (raw — no reasoning exposed)") instead of passing off raw JSON as thinking.
+- **A−/A+ font-size buttons had no visible effect** — `.ab-cv-row` had a hardcoded `font-size: 13.5px` that always overrode the CSS variable the buttons set on an ancestor. Now scales with it.
+- **Typing a new name + Enter in the "Assign to" popup silently did nothing** (after the first cast/recast run in a session) — the popup is a page-lifetime singleton, but its input's Enter/typing handlers closed over whichever run's `assignName` existed the *first* time the popup was created. Every later run's popup opens now repoint those handlers at the current run.
+- **Click-and-drag assign stopped working after the first run**, for the same reason as above (drag pre-fills the popup, then confirming it hit the same stale closure).
+- **"Split text to Unknown Speaker" split at the wrong spot** when the selected phrase (or a whitespace-trimmed variant) occurred earlier in the same paragraph — it searched for the text with `indexOf` instead of using the actual selection position. Now computes the exact DOM-range character offset, so it always splits where you dragged, regardless of repeated text elsewhere in the passage.
+
+---
+
## [1.12.96] — 2026-07-05
### Fixed
diff --git a/VERSION b/VERSION
index 2fbc2df..3940254 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.12.96
+1.12.97
diff --git a/routes/conversation.py b/routes/conversation.py
index 4fdb02b..844d79e 100644
--- a/routes/conversation.py
+++ b/routes/conversation.py
@@ -1228,6 +1228,11 @@ async def attribute_dialogue_stream(request: Request):
yield f'data: {json.dumps({"error": f"HTTP {upstream.status_code}: {_response_error_text(upstream)[:300]}"})}\n\n'
return
raw_parts = []
+ # The LLM's SSE response usually omits a charset on its Content-Type,
+ # so requests falls back to Latin-1 (the old HTTP default) instead of
+ # UTF-8 when decode_unicode=True guesses the encoding — every umlaut
+ # then comes out mojibake'd ("Häfen" -> "Häfen"). Force UTF-8.
+ upstream.encoding = "utf-8"
for line in upstream.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
diff --git a/static/index.html b/static/index.html
index ce34f1d..128b89a 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) {
-
+