diff --git a/docs/KONTRA-GUIDE.md b/docs/KONTRA-GUIDE.md
new file mode 100644
index 0000000..db5e414
--- /dev/null
+++ b/docs/KONTRA-GUIDE.md
@@ -0,0 +1,157 @@
+# Kontra — Editing, Media, Admin & Ops
+
+Single source of truth for working with the Kontra site. Covers: how content is
+structured, how to edit it (Obsidian or Decap), how media works, how the admin is
+configured, and how the whole thing runs + deploys.
+
+**Live site:** https://kontra.lab.audasmedia.com.au
+**Admin (Decap):** https://admin.kontra.lab.audasmedia.com.au/web/admin/
+**Architecture map:** https://maps.lab.audasmedia.com.au/kontra_day/docs/kontra-architecture-map.html
+
+```mermaid
+flowchart LR
+ subgraph editors["Content Editors"]
+ O[Obsidian vault /obsidian/kontra_content] -->|git push| G
+ N[Neovim / CLI] -->|git push| G
+ D[Decap CMS admin.kontra.lab] -->|OAuth + git commit| G
+ end
+ subgraph gitea["Gitea (.35 Docker)"]
+ G[(sam/kontra-content markdown + images = origin)]
+ end
+ subgraph prod["Kontra container (.13 :8600)"]
+ P[git pull loop every 30s] -->|restart on change| B
+ B[Go binary: templ + htmx + Goldmark]
+ end
+ subgraph media["Media (git files)"]
+ F[(Images beside articles in the content repo)]
+ end
+ G -->|"SSH git"| P
+ B -->|"{{media:...}} shortcode"| F
+ B -->|"HTML"| CB[Caddy master (.35 :443)]
+ CB --> WWW[browsers]
+ D -->|login| G
+```
+
+## 1. Content structure (the rules)
+
+```
+content/
+├── config/site.yaml # site name, tagline, nav (lowercase keys)
+├── subjects/
+│ ├── world/
+│ │ ├── index.md # SUBJECT index — Decap folder marker (not an article)
+│ │ ├── _subject.yaml # subject config (name, template, color, ALL lowercase keys)
+│ │ ├── chinese-chips/
+│ │ │ ├── index.md # THE ARTICLE (file is always index.md inside its folder)
+│ │ │ ├── hero.jpg # images live beside their article
+│ │ │ └── figure-2.jpg
+│ │ └── quantum-ai/…
+│ └── tech/…
+├── pages/ # standalone pages (about, 404…)
+│ └── about.md
+└── _templates/
+ └── article.md # Obsidian insert-template (⌘⇧T)
+```
+
+**Hard rules**
+- Article file = **`index.md` inside its own folder** named exactly like the slug
+ (`subjects///index.md`). The folder name = the URL slug (lowercased).
+- Filenames in a folder: **lowercase, no spaces** (`quantum-ai/`, not `Quantum-ai/`).
+- Images go in the **same folder as the article** — never the repo root, never a shared folder.
+- `_`-prefixed files are skipped by the site (`_subject.yaml`, `_templates/`, `.trash/`).
+
+## 2. Editing content
+
+### A. Obsidian (recommended daily editor)
+Open **`/home/sam/obsidian/kontra_content`** (a symlink to the content repo) as a vault.
+The **GitHub Sync** plugin auto-pushes on save (~15s).
+
+- **New article:** insert the template via **⌘⇧T → "article"** — this substitutes `{{title}}`/`{{date}}`.
+ (If you hand-copy the template file, those variables stay literal and break front-matter.)
+- **New subject:** create a folder under `subjects/` with an `index.md` (used as the subject index)
+ and `_subject.yaml`.
+- **Publish/hide:** `published: true` → live; `published: false` → hidden everywhere (404 + removed from all lists). Omit = published.
+- **Featured:** `featured: true` puts it in the hero lead slot.
+
+### B. Decap (browser admin)
+Login with **Gitea** → you get a nested tree: `Articles → world → chinese-chips …`.
+Click an article to edit. Same file → same result. **No manual push needed** — Decap commits to Gitea, autosync publishes.
+
+## 3. Media (images)
+
+Images are **git files beside their article** — no S3/object store.
+
+```
+subjects/world/chinese-chips/
+ index.md
+ hero.jpg ← dragged into Obsidian (or placed in the folder)
+```
+
+**Referencing**
+- Body: ``
+- Front-matter hero: `image: "{{media:chinese-chips/hero.jpg}}"` ← **must be quoted**
+- Rule: `{{media:/}}` — slug lowercase, **exact filename incl. extension** (`.jpg` ≠ `.jpeg`), **no leading slash**, **no backticks** around a real image markdown line.
+
+The app serves `/media/` by mapping the slug→subject folder (safe-path guarded).
+
+## 4. The admin (Decap) config
+
+- Lives at **`app/src/web/admin/config.yml`** (embedded into the binary at build).
+- **Nested collections** — this is how it lists the 2-level tree. Do NOT "simplify" it:
+```yaml
+collections:
+ - name: articles
+ label: Articles
+ folder: subjects
+ create: true
+ slug: "{{slug}}"
+ nested: { depth: 100, subfolders: false } # subfolders:false is REQUIRED
+ meta: { path: { widget: string, label: 'Path', index_file: 'index' } }
+```
+- **Decap gotchas (do not repeat):**
+ - `index_file: 'index'` (not `_index`), `subfolders: false` (not true)
+ - Labels in single quotes: NO apostrophes inside (breaks YAML — use double quotes)
+ - The media library is disabled (`media_folder: ""`) — images are git files, not uploaded through Decap.
+- `subjects` + `pages` collections also exist. Auth is **Gitea OAuth** (app `decap-kontra`, PKCE).
+
+## 5. Ops — how it runs
+
+**Machines**
+| Machine | IP | Role |
+|---|---|---|
+| Gitea (origin) | .35 :3001 | `sam/kontra-content` (content+images), `sam/kontra` (app) |
+| Kontra container | .13 :8600 | Go binary; pulls content every 30s + restarts on change |
+| Caddy master | .35 :443 | TLS; `kontra.lab…` + `admin.kontra…` → `.13:8600` |
+| Dev (.27) | .27 | Obsidian vault + app source + build |
+
+**Build** (after source changes, on .27)
+```bash
+cd app && ./build.sh # templ generate → go build → app/kontra-bin
+```
+The container builds inside Docker (golang stage) — update `config.yml`/assets then redeploy.
+
+**Deploy to .13** (the container runs from `/home/sam/Docker/Containers/kontra/`)
+```bash
+# source (needed bits)
+scp app/go.mod app/go.sum sam@192.168.20.13:/home/sam/Docker/Containers/kontra/src/
+rsync -az --delete --exclude bin --exclude kontra-bin --exclude .git \
+ app/src/ sam@192.168.20.13:/home/sam/Docker/Containers/kontra/src/src/
+ssh sam@192.168.20.13 'cd /home/sam/Docker/Containers/kontra && docker compose up -d --build'
+```
+> `.go.sum` must move with `go.mod`; and use `--delete` or stale files (like old `media.go`) break the build.
+
+**Content autosync** — pushes to `sam/kontra-content` are pulled by the container every 30s; server restarts on change. No CI/CD needed.
+
+**Secrets** — the Gitea deploy token lives in `.env` (repo root, **gitignored**, chmod 600). Never commit.
+
+## 6. Backups
+- Content + images are in the git repo → backed up by Gitea (mirrors + clients) and the production clone on `.13`.
+- The app source lives in git on Gitea.
+- House-wide offsite backup covers these hosts per the Backup Architecture plan.
+
+## 7. For AI agents resuming this project
+1. Read this file first.
+2. The site is **live** and the pipeline works — prefer the smallest change + `rsync --delete` deploy + verify on `https://kontra.lab.audasmedia.com.au`.
+3. Decap config: treat `config.yml` as sacred (nested `subfolders:false` + `index_file:index`).
+4. Media = git files. `{{media:/}}`, quoted in front-matter, exact filename.
+5. Don't reintroduce S3/Garage for media (dropped 2026-09-10).
\ No newline at end of file
diff --git a/docs/archify/kontra-architecture.architecture.json b/docs/archify/kontra-architecture.architecture.json
index 4616f99..0537866 100644
--- a/docs/archify/kontra-architecture.architecture.json
+++ b/docs/archify/kontra-architecture.architecture.json
@@ -123,8 +123,8 @@
{
"id": "garage",
"type": "cloud",
- "label": "Garage S3",
- "sublabel": "kontra-day bucket",
+ "label": "Git media",
+ "sublabel": "images beside articles",
"pos": [
680,
160
@@ -310,7 +310,7 @@
"title": "Auth & media",
"items": [
"Decap CMS logs in with Gitea OAuth (PKCE)",
- "Media resolves from Garage S3 via {{media:}} shortcodes"
+ "Media = git files beside articles; {{media:slug/file}}"
]
}
]
diff --git a/docs/diagrams/kontra-architecture.png b/docs/diagrams/kontra-architecture.png
index e41d814..0081fef 100644
Binary files a/docs/diagrams/kontra-architecture.png and b/docs/diagrams/kontra-architecture.png differ
diff --git a/docs/diagrams/kontra-architecture.svg b/docs/diagrams/kontra-architecture.svg
index 3b5cb5e..5f7d435 100644
--- a/docs/diagrams/kontra-architecture.svg
+++ b/docs/diagrams/kontra-architecture.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/docs/kontra-architecture-map.html b/docs/kontra-architecture-map.html
index 36d1456..8c962ce 100644
--- a/docs/kontra-architecture-map.html
+++ b/docs/kontra-architecture-map.html
@@ -5007,15 +5007,15 @@
:8600 Docker
-
- Garage S3 · kontra-day bucket · Kontra container (.13 :8600)
+
+ Git media · images beside articles · Kontra container (.13 :8600)
- Garage S3
- kontra-day bucket
+ Git media
+ images beside articles
@@ -5317,7 +5317,7 @@
• Decap CMS logs in with Gitea OAuth (PKCE)
-
• Media resolves from Garage S3 via {{media:}} shortcodes
+
• Media = git files beside articles; {{media:slug/file}}
diff --git a/docs/kontra-architecture.mmd b/docs/kontra-architecture.mmd
index 78cfbed..4f2e4d5 100644
--- a/docs/kontra-architecture.mmd
+++ b/docs/kontra-architecture.mmd
@@ -15,12 +15,12 @@ flowchart LR
B --> MD[(Markdown render front-matter + shortcodes)]
end
- subgraph media["Media (Garage S3 on .13)"]
- S3[(Garage S3 kontra-day bucket)]
+ subgraph media["Media (git files)"]
+ F[(Images beside articles in the content repo)]
end
G -->|"SSH git"| P
- B -->|"shortcode: media"| S3
+ B -->|"{{media:slug/file}}"| F
B -->|"emits HTML"| CB[Caddy master .35 :80/:443]
CB -->|"reverse_proxy :8600"| B