diff --git a/.planning/phases/08-gitea-ci/08-CONTEXT.md b/.planning/phases/08-gitea-ci/08-CONTEXT.md
new file mode 100644
index 0000000..185283c
--- /dev/null
+++ b/.planning/phases/08-gitea-ci/08-CONTEXT.md
@@ -0,0 +1,112 @@
+# Phase 8: Gitea CI - Context
+
+**Gathered:** 2026-06-11
+**Status:** Ready for planning
+
+
+## Phase Boundary
+
+Phase 8 adds CI on the existing self-hosted **Gitea Actions** runner. Two outcomes:
+
+1. **PR regression gate** — every PR targeting `main` runs lint, typecheck (both apps), unit tests, API-integration tests against a MariaDB service container, **and the Phase 7 mobile Playwright harness** (against a CI-brought-up dev stack with `DEV_AUTH_BYPASS=true`). Any failure blocks the merge.
+2. **Publish on merge** — a push to `main` builds and publishes the API Docker image to the Gitea container registry.
+
+This phase owns only the CI plumbing: workflow files, dev-stack bring-up + readiness waits, image build/push. It does **not** modify the Phase 7 harness specs (CI reuses them unchanged), the Dockerfile (already multi-stage, builds API + PWA), or application code. Requirements: **CI-01, CI-02**.
+
+
+
+
+## Implementation Decisions
+
+### Dev-stack bring-up in CI (for the harness step)
+- **D-01:** Bring up the stack with **bare background processes + a MariaDB service container** — NOT docker compose, NOT a production image.
+ - MariaDB runs as a Gitea **service container** (the same one the API-integration job needs; `DB_HOST=127.0.0.1`, service creds).
+ - The **API** runs as a background process via `pnpm dev:api` (or equivalent) with `DEV_AUTH_BYPASS=true` and `DB_HOST=127.0.0.1`, listening on `:3000`.
+ - The **PWA** Vite dev server is started by **Playwright's own `webServer`** config (already present; `reuseExistingServer: !process.env.CI`), on `:5173`. Vite proxies `/api`, `/health`, `/callback` → `:3000`.
+ - Rationale: no docker-in-docker on the self-hosted runner; matches the Phase 7 dev-server harness contract exactly; reuses the MariaDB service container already required for integration tests.
+- **D-02:** The harness step MUST wait for **both** the API (`:3000`) and the PWA Vite server (`:5173`) to accept connections before Playwright launches. The harness already polls `baseURL/health` (proxied to the API) in `global-setup.ts`; CI must additionally ensure the API process is up first. This is on top of the MariaDB-11 readiness loop (Pitfall 11 — `healthcheck.sh --connect --innodb_initialized`, never `mysqladmin ping`).
+
+### Workflow topology & jobs
+- **D-03:** **One workflow file with parallel, event-gated jobs.**
+ - `pull_request` → `main`: fast-checks job (lint + typecheck both apps + unit tests) runs **in parallel** with the heavier API-integration job and the harness job. Fast feedback — a lint failure does not wait behind the harness.
+ - `push` → `main` (merge): build-and-publish job runs.
+ - Single file so the whole regression + publish story lives in one place; accept the minor setup duplication (checkout, pnpm cache, Node-22 pin) across jobs.
+
+### Docker image tag strategy (CI-02)
+- **D-04:** On merge to `main`, publish the API image with **two tags**: `:latest` (moving pointer) **and** `:-` (immutable, e.g. `v1.1-4303a1b`).
+ - The milestone string (e.g. `v1.1`) is read from PROJECT.md / ROADMAP.md, not hardcoded inline if avoidable.
+ - `` is the short commit SHA of the merge commit.
+ - Rationale: `:latest` for easy pulls; the milestone-prefixed SHA tag groups builds by release line and stays immutable for rollback/traceability.
+
+### Failure artifacts & browser matrix
+- **D-05:** Run **both** device profiles in CI — iPhone 14/WebKit **and** Pixel 7/Chromium (the full Phase 7 matrix). Install whatever system deps WebKit needs on the runner (probe in the runner-probe step).
+- **D-06:** On harness **failure**, upload Playwright **traces / screenshots / videos** as CI artifacts for debugging. The config already emits `trace`/`video` `on-first-retry` and `screenshot: only-on-failure`; CI must upload the `test-results/` output. Note the config's `reporter: 'github'` may not render natively in Gitea Actions — verify during the runner probe and fall back to `list`/`html` if annotations don't surface.
+
+### Claude's Discretion
+- Exact job names, step ordering within a job, pnpm store cache key strategy, and whether fast-checks is one job or split — planner/executor decide.
+- Whether the API background process is launched with `pnpm dev:api` vs a built `node dist` — pick whatever gives reliable `:3000` readiness under `DEV_AUTH_BYPASS`; the harness only needs the authed PWA reachable (Dev User 1 has no CalDAV creds, so verify layout/flows, not live event-create).
+- Registry hostname / image repository path under the Gitea registry.
+
+
+
+
+## Canonical References
+
+**Downstream agents MUST read these before planning or implementing.**
+
+### Phase scope & requirements
+- `.planning/ROADMAP.md` §"Phase 8: Gitea CI" — goal, 6 success criteria, pitfalls this phase owns.
+- `.planning/REQUIREMENTS.md` — CI-01 (PR regression incl. harness), CI-02 (publish image on merge).
+- `.planning/PITFALLS.md` — Pitfalls 11 (MariaDB-11 readiness), 12 (runner-probe first), 13 (`--password-stdin`), 15 (SW block, harness side).
+
+### Harness the CI step runs (reused unchanged from Phase 7)
+- `apps/pwa/playwright.config.ts` — device matrix, `serviceWorkers: 'block'`, `webServer` (Vite-only, `reuseExistingServer: !CI`), `retries`/`workers`/`reporter` under `CI`, env-driven `PLAYWRIGHT_BASE_URL`.
+- `apps/pwa/e2e/global-setup.ts` — `/health` readiness poll, `/api/me` DEV_AUTH_BYPASS reachability gate, fail-closed env guard (refuses `NODE_ENV=production` or missing `DEV_AUTH_BYPASS`), mysql2 truncate-and-seed (calendar id 10, lists/items for user 1).
+- `.planning/phases/07-mobile-test-harness/07-CONTEXT.md` — Phase 7 decisions D-01..D-10 (auth strategy, SW block, env baseURL, compose-managed backend).
+
+### Infra the CI builds/runs against
+- `apps/api/Dockerfile` — multi-stage: `builder` (API), `pwa-builder` (PWA dist → `./public`), `production` target. CI publishes the `production` target.
+- `docker-compose.yml` / `docker-compose.dev.yml` — service shape, MariaDB 11 healthcheck (`healthcheck.sh --connect --innodb_initialized`), dev override exposing 3306, API `dev` build target.
+- `apps/pwa/vite.config.ts` — dev proxy (`/api`, `/health`, `/callback` → `:3000`) the harness depends on.
+- `package.json` (root) — scripts: `dev:api`, `dev:pwa`, `test`, `test:e2e`, `lint`, `typecheck`.
+
+
+
+
+## Existing Code Insights
+
+### Reusable Assets
+- **Playwright config + global-setup (Phase 7):** ready to run headlessly in CI. `retries: 2`, `workers: 1`, `reporter: 'github'` already gated on `process.env.CI`. CI sets `CI=true` and `PLAYWRIGHT_BASE_URL` and the harness behaves correctly. No spec changes.
+- **MariaDB service-container pattern:** API-integration tests already require a real MariaDB with `DB_HOST=127.0.0.1` + service creds + Drizzle `generate`+`migrate` for schema. The harness's `global-setup` seeds the same DB directly via mysql2. One MariaDB service container can back both the integration job and the harness job.
+- **Multi-stage Dockerfile:** `production` target already builds API + PWA and serves both on `:3000`. CI build/push is a thin wrapper (`docker build --target production` + `docker login --password-stdin` + `docker push`).
+
+### Established Patterns
+- **Gitea, not GitHub:** origin is self-hosted Gitea; `main` is protected (PRs only). Gitea Actions is GitHub-Actions-syntax-compatible but **do not assume `actions/setup-node` behaves identically** — runner-probe first (Pitfall 12), pin Node 22 explicitly.
+- **node-cron lesson (long-running process):** not directly relevant to CI, but the API in CI is short-lived/background — no scheduler concerns.
+
+### Integration Points
+- CI orchestrates, in order, for the harness job: MariaDB service container (readiness loop) → Drizzle generate+migrate → API background process (`DEV_AUTH_BYPASS=true`, `:3000`, readiness wait) → Playwright (`webServer` starts Vite `:5173`, `global-setup` polls `/health` + `/api/me`) → specs → upload artifacts on failure.
+- Publish job depends on `apps/api/Dockerfile` `production` target + Gitea registry credentials (PAT with `write:package`, piped via `--password-stdin`).
+
+
+
+
+## Specific Ideas
+
+- Image tag format locked to `:latest` + `:v1.1-` (milestone prefix + short SHA). Example: `v1.1-4303a1b`.
+- Start the very first CI iteration as a **runner-probe** only: `node --version` / `pnpm --version` / Docker access / WebKit dep availability on the `self-hosted` runner — before any real test/build steps are designed.
+
+
+
+
+## Deferred Ideas
+
+- **ROADMAP status fix:** ROADMAP.md line 29 marks Phase 8 "completed 2026-06-11" while line 204 says "Not started" and no Phase 8 artifacts exist. This is a bookkeeping error to correct (Phase 8 is being started now) — a docs/roadmap cleanup, not Phase 8 scope.
+- None other — discussion stayed within phase scope.
+
+
+
+---
+
+*Phase: 8-Gitea CI*
+*Context gathered: 2026-06-11*
diff --git a/.planning/phases/08-gitea-ci/08-DISCUSSION-LOG.md b/.planning/phases/08-gitea-ci/08-DISCUSSION-LOG.md
new file mode 100644
index 0000000..507b340
--- /dev/null
+++ b/.planning/phases/08-gitea-ci/08-DISCUSSION-LOG.md
@@ -0,0 +1,74 @@
+# Phase 8: Gitea CI - Discussion Log
+
+> **Audit trail only.** Do not use as input to planning, research, or execution agents.
+> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
+
+**Date:** 2026-06-11
+**Phase:** 8-Gitea CI
+**Areas discussed:** Dev-stack bring-up in CI, Workflow topology & jobs, Docker image tag strategy, Failure artifacts & browser matrix
+
+---
+
+## Dev-stack bring-up in CI
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Bare processes + service MariaDB | MariaDB as a Gitea service container; API as background `pnpm dev:api` with DEV_AUTH_BYPASS; Playwright webServer starts Vite. No docker-in-docker. | ✓ |
+| docker compose dev override | Run docker-compose.yml + dev override inside the runner; adds image-build time + docker-in-docker. | |
+| Production image for harness | Build prod image, run harness against API-served PWA on :3000; diverges from Phase 7 dev-server contract. | |
+
+**User's choice:** Bare processes + service MariaDB
+**Notes:** Reuses the MariaDB service container already needed by the API-integration tests; matches the Phase 7 harness contract exactly.
+
+---
+
+## Workflow topology & jobs
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| One file, parallel jobs | Single workflow; PR→main runs fast-checks in parallel with integration + harness; push→main publishes. | ✓ |
+| Two files | Separate ci.yml + publish.yml; cleaner split, duplicated setup. | |
+| One file, linear job | Single sequential job; simplest, slowest feedback. | |
+
+**User's choice:** One file, parallel jobs
+**Notes:** Fast feedback prioritized; minor setup duplication across jobs accepted.
+
+---
+
+## Docker image tag strategy
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| latest + short SHA | :latest + : | |
+| Short SHA only | Immutable per-commit only | |
+| latest only | Single moving tag | |
+| semver from package.json | Version field + latest | |
+| **latest + milestone short (custom)** | :latest + :-, e.g. v1.1-4303a1b | ✓ |
+
+**User's choice:** latest + `v1.1-4303a1b` (milestone prefix + short SHA), confirmed in follow-up over a 3-tag variant and a no-milestone variant.
+**Notes:** Milestone string read from PROJECT.md/ROADMAP, not hardcoded; SHA tag immutable for rollback.
+
+---
+
+## Failure artifacts & browser matrix
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Upload on failure + both profiles | iPhone/WebKit + Pixel/Chromium; upload traces/screenshots/videos on failure. | ✓ |
+| Upload on failure + Chromium only | Pixel/Chromium only; faster, loses iOS-engine coverage. | |
+| Both profiles, no artifacts | Full matrix, log-only failures. | |
+
+**User's choice:** Upload on failure + both profiles
+**Notes:** Full mobile coverage + debuggable failures. `reporter: 'github'` may not render in Gitea — verify in runner probe, fall back if needed.
+
+---
+
+## Claude's Discretion
+
+- Exact job names/step ordering, pnpm cache key, fast-checks split.
+- API launch mechanism (`pnpm dev:api` vs built `node dist`) as long as `:3000` is reliably ready under DEV_AUTH_BYPASS.
+- Registry hostname / image repo path.
+
+## Deferred Ideas
+
+- ROADMAP status conflict (line 29 "completed" vs line 204 "Not started", no artifacts) — bookkeeping fix, not Phase 8 scope.