diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb806f7..ef96c20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
- **The Contents button is the first item of the sticky review bar**, so it stays in reach while scrolling a long page.
- **The page thumbnail is the Review button.** In the thumbnail grid, clicking the (big) page image opens the page in review; the separate *Review* / *Review this page* buttons are gone. A page shared by two or more articles keeps one labelled *Review* button per article, so it stays clear which is meant.
+- **Click a flagged word to correct it yourself.** In the spelling list, clicking the underlined word (or *Edit…*) turns the row into a small editor prefilled with the word: type the correction — one word or several, e.g. `dasich` → `das ich` — and press Enter (Escape cancels). Until now only the listed suggestions could be applied. The server accepts any short single-line text without markup for a correction you type; model answers still go through the strict check.
+
### Fixed
- **The new-box popup is no longer cut off.** Drawing a box near the bottom of a page put the *Region type / Add region* panel below the window edge. It is now placed in the window: below the box when it fits, above it otherwise, always fully inside, and it follows the box when the page scrolls.
- **Changed scripts and stylesheets now reach the browser immediately.** They were requested under a version that only moved at release time (`…js?v=1.53.4`) and cached as immutable for a year, so a browser or proxy could keep running old code after an update (a button added to a script simply did not appear). The app shell now requests every asset under `-`, where the build id follows the newest script/stylesheet/manifest change.
diff --git a/routes/magazine.py b/routes/magazine.py
index 4df2870..ca319a4 100644
--- a/routes/magazine.py
+++ b/routes/magazine.py
@@ -2505,8 +2505,13 @@ async def magazine_spelling_fix(doc_id: str, request: Request):
token, replacement = str(body["token"]), str(body["replacement"]).strip()
except (KeyError, TypeError, ValueError):
raise HTTPException(400, "block_id, start, end, token and replacement are required")
- if not re.fullmatch(r"[A-Za-zÄÖÜäöüß-]{1,60}(?:- (?:und|oder))?", replacement):
- raise HTTPException(400, "A replacement is a single word")
+ # Typed by the reviewer (a suggestion, or their own correction such as "das ich" for "dasich"):
+ # any short single-line text, but no markup or control characters. The agent's answers are held
+ # to the much stricter spelling.accept_agent_fix instead.
+ if not replacement or len(replacement) > 120 or re.search(r"[\x00-\x1f<>]", replacement):
+ raise HTTPException(400, "A correction is 1-120 characters on one line, without markup")
+ if replacement == token:
+ raise HTTPException(400, "That is what is already there")
layout = mag.load_layout(d)
block = next((b for b in layout.get("blocks", []) if b["id"] == block_id), None)
if block is None:
diff --git a/static/js/magazine.js b/static/js/magazine.js
index 5bfc7ab..7a63c8c 100644
--- a/static/js/magazine.js
+++ b/static/js/magazine.js
@@ -3640,11 +3640,13 @@ function magSpellPanelHtml(issues, scopeLabel) {
' that no German or English dictionary knows. Names and technical terms are normal — choose Keep for those. ' + agent + '