diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7b790f5..f13d30b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,19 @@ skip versions are the composition of each intervening adjacent
release's steps in order — no A-to-B path is pre-computed beyond
that.
+## 0.52.3 — 2026-06-09
+
+**Patch — unmatched routes render a 404 instead of a blank page or a
+silent redirect home (no operator action required).**
+
+- **The top-level catch-all redirected any unknown path to `/`**, and the
+ nested `/admin/*`, `/p/:projectId/*`, and `/docs/*` route groups had no
+ catch-all (so an invalid subpath rendered an empty pane). Added a shared
+ `NotFound` 404 page and wired it into all four route groups, so a bad or
+ typo'd URL — including a stale `/admin/users/allowlist/graduation`-style
+ path — now shows a clear "page not found" with a link back to the catalog.
+ (Client-side 404 UI; the SPA still serves over HTTP 200, as before.)
+
## 0.52.2 — 2026-06-09
**Patch — admin sidebar nav links no longer accumulate URL segments (no
diff --git a/VERSION b/VERSION
index fbb7b19..d572c9b 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.52.2
\ No newline at end of file
+0.52.3
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index 0a31e1c..7c77605 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "rfc-app-frontend",
"private": true,
- "version": "0.52.2",
+ "version": "0.52.3",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/App.css b/frontend/src/App.css
index b8901f5..3b09763 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -2687,3 +2687,19 @@ select:focus-visible,
align-items: flex-start;
gap: 12px;
}
+
+/* 404 / not-found page (client-side, for unmatched routes) */
+.not-found {
+ max-width: 32rem;
+ margin: 4rem auto;
+ padding: 0 1.5rem;
+ text-align: center;
+}
+.not-found-code {
+ font-size: 3.5rem;
+ line-height: 1;
+ margin: 0 0 0.5rem;
+ color: var(--c-gray-900, #111);
+}
+.not-found-message { color: var(--c-gray-700, #444); margin: 0 0 1.25rem; }
+.not-found-actions a { color: var(--c-gray-900, #111); text-decoration: underline; }
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index aabfa33..63e961d 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -7,6 +7,7 @@ import { brandTitle } from './lib/brand'
import { entryPath, proposalPath, DEFAULT_COLLECTION } from './lib/entryPaths'
import { useDeployment } from './context/DeploymentProvider'
import ProjectLayout from './components/ProjectLayout.jsx'
+import NotFound from './components/NotFound.jsx'
import Directory from './components/Directory.jsx'
import ProjectSwitcher from './components/ProjectSwitcher.jsx'
import Catalog from './components/Catalog.jsx'
@@ -381,13 +382,14 @@ export default function App() {
+ {message || "We couldn't find that page."} +
++ Return to the catalog +
+