**Manual trigger only.** Merging to `main` does NOT auto-deploy. To ship: Gitea → **Actions → deploy → Run workflow** on `main`. The workflow runs tests, builds, then `rsync`s `dist/` to Hetzner over SSH (ed25519 key on port 222, host-key-pinned), and post-deploy curls the live site to verify security headers.
Rollback: trigger the deploy workflow on the previous `main` commit (Actions UI lets you pick a ref).
To put the site into "back shortly" mode, trigger Gitea → **Actions → deploy-placeholder → Run workflow**. To bring the real site back, trigger **Actions → deploy → Run workflow** on the desired `main` commit. Both workflows share the `deploy-production` concurrency group, so they can never run simultaneously.
The placeholder is hand-authored static HTML in `placeholder/` and does NOT depend on `npm`/`astro build` — it is deliberately decoupled from the main build so it can ship even when that build is broken.
**Scope note.** The placeholder serves HTTP 200 (not 503), so Cloudflare's edge will cache it normally. This is fine for short planned maintenance windows. For longer outages or incident fallback, purge Cloudflare's cache (or set a short-TTL Cache Rule for the maintenance window) before triggering recovery via `deploy.yml`, otherwise the edge may serve the placeholder past recovery until TTL expires.
**Security headers** (HSTS, CSP, X-Frame-Options, etc.) are owned by **Cloudflare Transform Rules**, not by anything in this repo. Hetzner Webhosting L ignores file-based `.htaccess` (`AllowOverride None`), so origin-side header config is impossible from code. See `OPERATOR-CHECKLIST.md` §2.