diff --git a/AGENTS.md b/AGENTS.md
index 9eb7be2..f9faf70 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -12,7 +12,7 @@ Pagerite is a CMS. See `docs` for the full design and implementation details. Ke
- `pagerite/` — Python backend package (hatchling build target).
- `app.py` — thin FastAPI assembly: lifespan, `FastAPI(...)`, router includes (route ordering: api/tracking/messages/files routers, then `frontend.route(app, "/")`, then the pages catch-all last).
- `state.py` — shared core, no routes: env-derived site constants, `data`/`kanta`, `analytics_store`, the fastapi-vue `frontend`, the render cache (`_html_response`, `_invalidate_pages`), the translator `dispatcher`, slug helpers, `@kanta.bootstrap` hooks.
- - `files.py` — `FileStore` (content-addressed, RAM-cached), image derivative helpers (`store_image`), file routes (`/_api/files`, `/_f/` — incl. `/_f/{name}/{filename}` forced-download attachments, `/_themes/`, `/_fonts/`, favicon settings).
+ - `files.py` — `FileStore` (content-addressed, RAM-cached), image derivative helpers (`store_image`; `store_attachment` keeps non-media uploads as inert `{hash}.file` blobs), file routes (`/_api/files`, `/_f/` — media by hash, attachments only via `/_f/{hash}/{filename}`, all forced-download + nosniff, `/_themes/`, `/_fonts/`, favicon settings).
- `api.py` — editor REST + WS: `/_api/pages`, `/_api/structure`, `/_api/settings`, `/_api/toggle-task`, `/_api/translations`, `/_api/ws/editor`, `/_translate/{key}`.
- `tracking.py` — visit analytics: GeoIP, client enrichment, favicon fetch, `/_ws`, `/_api/ws/analytics`, the `/_a` page (docs/analytics.md). Both sockets also carry the contact-message protocol (docs/messages.md): `/_ws` routes token-bearing visitor frames, the analytics socket the admin inbox.
- `messages.py` — visitor contact threads (docs/messages.md): strict message rendering (`html=False` markdown-it — threads never carry user markup), thread store ops, the visitor/admin socket protocols, and the public `/_contact/files` upload route.
diff --git a/docs/messages.md b/docs/messages.md
index 3239c85..388b77f 100644
--- a/docs/messages.md
+++ b/docs/messages.md
@@ -92,18 +92,25 @@ injection point, fed only by the strict renderer.
## Uploads
`PUT /_contact/files/{name}` (public, like the messaging socket) stores
-panel uploads in the content-addressed file store: raster images through
-the usual AVIF/WebP/JPEG derivative pipeline, **anything else as-is**,
-rendering as a download chip in the thread. Every file type is allowed
-(html, exe, svg…) — safety comes from enforced downloads rather than
-type filtering, verified twice: the chip anchor carries a `download`
-attribute, and the attachment link is `/_f/{name}/{original-filename}`,
-which the file server answers with `content-disposition: attachment`
-naming the original file (the name rides in the URL; there is no
-filename registry). The plain `/_f/{name}` form additionally forces a
-download whenever the stored type is a script carrier (html/xml/svg/
-js), so stripping the filename segment cannot turn an attachment into
-an in-origin page; all `/_f/` responses carry `nosniff`. Gates today: a
+panel uploads in the content-addressed file store. Images (raster and
+SVG) go through the usual AVIF/WebP/JPEG derivative pipeline and inline
+in the message — SVGs included: the extension-less link negotiates
+raster derivatives, and even an as-is-served `.svg` is script-inert
+inside an `
`. Everything else (videos, archives, html, exe…) is
+stored as an inert `{hash}.file` blob — the original extension is
+dropped on disk, so the store can never be double-click-executed — and
+linked as `/_f/{hash}/{original-name}`; the `.file` suffix exists only
+on the filesystem and is rejected in URLs. Serving is strictly
+separated: `/_f/{name}` serves media only (hash with or without
+extension), `.orig.` originals stay blocked, and the filename-carrying
+route serves only `{name}.file` blobs (never media, never by hash).
+Every `/_f/` response carries `content-disposition: attachment` (no
+filename needed — the URL's last segment names the download) and
+`x-content-type-options: nosniff`, so a store file can never be
+rendered or sniffed into something executable in the site's origin;
+subresource fetches (`
`, `