diff --git a/docs/CONCEPT_HTML_VIEWER.md b/docs/CONCEPT_HTML_VIEWER.md
new file mode 100644
index 0000000..d843e34
--- /dev/null
+++ b/docs/CONCEPT_HTML_VIEWER.md
@@ -0,0 +1,278 @@
+# HTML Viewer Export Concept
+
+## Overview
+
+The HTML Viewer export produces a **self-contained offline ZIP** that is a read-only clone
+of the live EventSnap feed. Opening `index.html` in any modern browser shows the full event
+gallery — list view, grid view, search, filter, lightbox — with no internet connection or
+server required.
+
+It **replaces the current HTML export job type**. The old HTML export produced a raw
+minijinja-rendered template; the new viewer supersedes it entirely. The existing `html`
+job variant in the backend is repurposed to run this flow instead of being kept alongside
+it.
+
+It is distinct from the ZIP archive export (which is raw media files). The viewer is a
+polished, navigable web app bundled with the event's content.
+
+---
+
+## User-Facing Behavior
+
+- Download `event_name_viewer.zip`, unzip, open `index.html`
+- Full list view (chronological, newest first) and grid view with search/filter
+- Likes, comments, and reaction counts shown (static snapshot from export time)
+- Read-only: no uploads, no auth, no dashboards
+- Works offline, no CDN or external resources
+
+---
+
+## Architecture
+
+### Separate Static SvelteKit App
+
+A new mini SvelteKit project lives at `frontend/export-viewer/` within the same monorepo.
+It uses `adapter-static` and is kept completely independent of the main app.
+
+**Why separate rather than a shared route:**
+- The viewer must be distributable as a standalone static bundle; the main app uses
+ `adapter-node` and cannot be mixed
+- Keeping it separate avoids auth, store, and routing dependencies leaking in
+- Simpler to reason about: the viewer has exactly two concerns (list view, grid view)
+
+**Why same repo:**
+- Shares Tailwind config and design tokens → visual parity with the main app
+- Single `pnpm` workspace, no separate CI needed
+- Backend can reference the pre-built output by relative path
+
+### Pre-Built Output Committed to Repo
+
+The viewer is built once and its output committed to `backend/static/export-viewer/`.
+The backend export job **does not run a Node build** at runtime — it just copies the
+pre-built assets and injects event data alongside them.
+
+When the viewer source changes, a developer rebuilds it locally (`pnpm build` in
+`frontend/export-viewer/`) and commits the updated `backend/static/export-viewer/` output.
+
+---
+
+## ZIP Structure
+
+```
+event_name_viewer.zip/
+├── index.html ← entry point; open this in any browser
+├── _app/
+│ └── immutable/
+│ ├── viewer.[hash].js ← all Svelte/app logic, single bundle
+│ └── viewer.[hash].css ← all styles including Tailwind output
+├── data.json ← injected by backend at export time
+└── media/
+ ├── abc123_thumb.jpg ← ~400 px wide, used in grid cells
+ ├── abc123_full.jpg ← original or capped (see Media Strategy)
+ ├── def456_thumb.jpg
+ └── def456.mp4 ← videos included as-is
+```
+
+No external font CDN, no Google Fonts, no remote scripts. All assets are local.
+
+---
+
+## data.json Schema
+
+Generated by the backend export job. The viewer fetches this file on startup via
+`fetch('./data.json')` (relative path, works from filesystem).
+
+```json
+{
+ "event": {
+ "name": "Sommerfest 2025",
+ "exported_at": "2025-07-15T20:00:00Z"
+ },
+ "posts": [
+ {
+ "id": "abc123",
+ "uploader": "MaxMustermann",
+ "caption": "Tolle Stimmung! #party #spaß",
+ "tags": ["party", "spaß"],
+ "timestamp": "2025-07-15T18:30:00Z",
+ "likes": 12,
+ "comments": [
+ {
+ "author": "AnnaSchulz",
+ "text": "So schön!",
+ "timestamp": "2025-07-15T18:35:00Z"
+ }
+ ],
+ "media": [
+ {
+ "type": "image",
+ "thumb": "media/abc123_thumb.jpg",
+ "full": "media/abc123_full.jpg",
+ "width": 1920,
+ "height": 1080
+ }
+ ]
+ }
+ ]
+}
+```
+
+All post data (likes, comments, tags) reflects the state at export time. No live updates.
+
+---
+
+## Media Strategy
+
+### Images
+
+| Variant | Purpose | Max dimension | Format |
+|---------|---------|---------------|--------|
+| `_thumb` | Grid cells, list post thumbnail | 400 px wide | JPEG q75 |
+| `_full` | Lightbox / full-screen view | Original, or 2000 px cap if >5 MB | JPEG q85 |
+
+The backend applies compression only when the original exceeds a threshold (e.g. >5 MB for
+images). Below that threshold the original is used as `_full` unchanged.
+
+The full-resolution original is always available via the separate ZIP archive export.
+
+### Videos
+
+Included as-is (no server-side transcoding). The viewer uses a standard `