madelog18 Sep 2026

madelog, a CLI that records how a project came together: commits, AI sessions and one-sentence decisions, hash chained, rendered as a page a company reads in two minutes

Developers applying for jobs who want to show how they work with AI, and the companies reading the page

Built over 1 working day on 18 Sep 2026. Every commit, decision and AI session below was recorded as it happened; nothing was written after the fact.

27commits
9decisions
0AI sessions
1day active
Delivered
not yet published
Built with
claude-code
Record
Hash-chained log of 36 events, rendered on this machine. Badge

Timeline

Commits sit on the line, decisions above it, AI sessions below. Click a point to read the diff excerpt or the prompt behind it.

Timeline on 18 Sep 2026: 27 commits, 9 decisions, 0 AI sessions on one axis. decisions commits AI sessions commit, 18 Sep, 06:59: docs: add PRD, v1 design spec, implementation plan and event schema commit, 18 Sep, 06:59: docs: install modes (system-wide / per project), onboarding after install, English-only CLI decision, 18 Sep, 06:59: Two install modes: system-wide to ~/.local/bin, or per project to .pop/bin so the hook calls that path and PATH stays untouched. commit, 18 Sep, 06:59: docs: Git AI note format research, docs site section and task decision, 18 Sep, 06:59: Git AI carries capture; madelog starts at decisions, patterns and the page. No keylogger, no editor plugin. commit, 18 Sep, 06:59: docs: landing and docs site on Next.js + TypeScript (static export) decision, 18 Sep, 06:59: Landing moved to Next.js static export in TypeScript strict; the PRD's no-framework line is superseded. commit, 18 Sep, 06:59: docs: Railway hosting, monochromatic design with GitLens-style hero graph commit, 18 Sep, 06:59: docs: landing density and professional register commit, 18 Sep, 06:59: docs: hero reference image and GSAP scroll animation commit, 18 Sep, 06:59: feat(server): Go API for devices, pages, verify, delete and billing webhook decision, 18 Sep, 06:59: Server in Go, not Rust: three endpoints, one binary, faster iteration than sharing the CLI crate. commit, 18 Sep, 06:59: docs: hero decisions confirmed by the user commit, 18 Sep, 06:59: docs: hero mouse interaction decisions commit, 18 Sep, 06:59: docs: pnpm for the landing commit, 18 Sep, 06:59: feat(cli): foundation — events, hash chain, SQLite store, git hook, init/onboard/note/log/verify/export/hide/config decision, 18 Sep, 06:59: Hash chain input is prev_hash, ts, kind and canonical JSON, newline separated; pattern and summary events join the chain like everything else. commit, 18 Sep, 06:59: chore: README, MIT license, CI and release workflows, Homebrew and winget templates commit, 18 Sep, 06:59: docs: hero redirected to Three.js wireframe megastructure, pure neutral palette decision, 18 Sep, 06:59: Hero redirected from the organic tree to a Three.js wireframe megastructure; pure neutral palette, no accent colour. commit, 18 Sep, 06:59: docs: seamless section flow on the landing commit, 18 Sep, 06:59: docs: brand identity scope and decisions commit, 18 Sep, 06:59: docs: plan tasks for hero scene and brand identity commit, 18 Sep, 06:59: feat(cli): read Git AI notes into ai_session events, best-effort Claude Code prompt lookup commit, 18 Sep, 06:59: fix(cli): create the device key atomically so parallel init cannot read a partial key commit, 18 Sep, 06:59: feat(cli): pattern detection (revert, retry, fix after AI, stall), moments and pop analyze commit, 18 Sep, 06:59: feat(cli): weekly summaries via Ollama with heuristic fallback; help exits 0; page and preview deps decision, 18 Sep, 06:59: Weekly summaries come from Ollama with a heuristic fallback; a remote model is opt-in and the page marks generated text. commit, 18 Sep, 06:59: chore(cli): page module contract stub for the generator and publish tasks commit, 18 Sep, 06:59: feat(cli): Tera project page generator, pop publish with ratatui preview, signing and unpublish decision, 18 Sep, 06:59: Thin projects get a warning below 15 events, not a block. commit, 18 Sep, 06:59: docs: brand and hero decisions confirmed by the user decision, 18 Sep, 06:59: One sans family, Schibsted Grotesk, for display and text; Geist Mono for anything that is data. commit, 18 Sep, 06:59: fix(cli): re-upload the confirmed page so the public link carries the badge commit, 18 Sep, 06:59: docs: product name decided: madelog commit, 18 Sep, 06:59: docs: primary domain madelog.app

Scroll the axis sideways on a phone.

14 Sep – 20 Sep

In the week of 2026-09-14 you made 27 commits over 1 day. Most of the changes landed in docs/superpowers/specs/2026-09-17-pop-v1-design.md, cli/src/lib.rs and docs/superpowers/plans/2026-09-17-pop-v1-plan.md. You wrote down 9 decisions: Two install modes: system-wide to ~/.local/bin, or per project to .pop/bin so the hook calls that path and PATH stays untouched; Git AI carries capture; madelog starts at decisions, patterns and the page. No keylogger, no editor plugin; Landing moved to Next.js static export in TypeScript strict; the PRD's no-framework line is superseded; Server in Go, not Rust: three endpoints, one binary, faster iteration than sharing the CLI crate; Hash chain input is prev_hash, ts, kind and canonical JSON, newline separated; pattern and summary events join the chain like everything else; Hero redirected from the organic tree to a Three.js wireframe megastructure; pure neutral palette, no accent colour; Weekly summaries come from Ollama with a heuristic fallback; a remote model is opt-in and the page marks generated text; Thin projects get a warning below 15 events, not a block; One sans family, Schibsted Grotesk, for display and text; Geist Mono for anything that is data. generated from the log

Every record (36)
18 Sep, 06:59commit5a50941docs: add PRD, v1 design spec, implementation plan and event schema
@@ .gitignore
@@ -0,0 +1,16 @@
+# Rust
+/cli/target/
+/target/
+# Go
+/server/bin/
+# Node
+node_modules/
+# Local pop data (dogfooding this repo)
+.pop/
+# Env / secrets
+.env
+.env.*
+# macOS
+.DS_Store
+# Session memory
+.remember/
@@ docs/superpowers/plans/2026-09-17-pop-v1-plan.md
@@ -0,0 +1,77 @@
+# pop v1 — implementation plan
+
+Spec: `docs/superpowers/specs/2026-09-17-pop-v1-design.md`. Each task is owned by one agent; tasks in the same phase run in parallel and must not touch each other's directories/files.
+
+## Phase 1 (parallel)
+
+### A. CLI foundation (`cli/`)
+- Cargo crate `pop` (bin `pop`, lib `pop` for tests). Modules: `event`, `chain`, `store`, `gitrepo`, `config`, `keys`, `commands/{init,note,hook,log,verify,export,hide,config}`.
+- Event model matching `schema/event.schema.json` (serde structs, canonical JSON, hash). Test vector: hash of a known event is stable.
+- `store`: rusqlite, append-only, `events` table + indexes on `kind`, `commit_sha`, `ts`.
+- `gitrepo`: git2 wrapper — HEAD, commit metadata, per-file diff stats vs first parent, 20-line diff excerpt, ignored-path filtering.
+- Commands per spec §4 except `publish`, `analyze`, `summarize`. `pop hook post-commit` records commit events; leave a clearly marked hook point `gitai::sessions_for_commit(repo, sha) -> Vec<AiSession>` that returns empty for now (task B fills it).
+- Integration tests in `cli/tests/` using temp repos.
+
+### H. Server (`server/`)
+- Everything in spec §5. `sqlc.yaml`, `db/migrations/0001_init.sql`, `db/queries/*.sql`, generated `internal/db`. `cmd/popserver/main.go`, `internal/http/*`. Dockerfile, docker-compose.yml, Makefile (`make test`, `make run`).
+- Slug words list in `internal/slug/`.
+
+### F. Design: project page + landing shell (`design/`, `landing/`) — design agent
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -0,0 +1,177 @@
+# pop v1 — design spec
+
+Source: `PRD Proof of Process (radni naziv).pdf` (2026-09-17). This spec turns the PRD into buildable units. Where the PRD is silent, decisions are marked **(decision)**.
+
+## 1. What we are building
+
+A Rust CLI `pop` that runs alongside git in a project, records how the project came together (commits, AI sessions, one-sentence decisions, detected patterns, weekly summaries), and generates a verified project page the candidate sends to a company instead of a portfolio. A small Go server stores published pages and hash-chain roots. A static landing page whose only goal is that the visitor copies the install command.
+
+Scope of this build (PRD roadmap weeks 1–6, plus week 7's Ollama path):
+
+| Unit | PRD week | In this build |
+|---|---|---|
+| `pop init`, `pop note`, git hook, SQLite log, hash chain, `pop log` | 1 | yes |
+| Git AI integration (read Git Notes attribution) | 2 | yes, behind a reader with fixture tests |
+| Pattern analysis + ratatui preview | 3 | yes |
+| Page generator (Tera + SVG timeline), `pop publish --local` | 4 | yes |
+| Go server (chi, pgx, sqlc, Postgres), public link + badge | 5 | yes |
+| Landing page (EN/HR/DE), install scripts | 6 | yes |
+| Summaries via Ollama, heuristic fallback | 7 | yes (Ollama + heuristic) |
@@ schema/event.schema.json
@@ -0,0 +1,135 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://pop.dev/schema/event.schema.json",
+  "title": "pop event",
+  "description": "One recorded event in a pop project log. Every event enters the hash chain: hash = sha256(prev_hash + \"\\n\" + ts + \"\\n\" + kind + \"\\n\" + canonical_json(payload)), canonical JSON has sorted keys and no whitespace.",
+  "type": "object",
+  "required": ["id", "ts", "kind", "payload", "prev_hash", "hash"],
+  "additionalProperties": false,
+  "properties": {
+    "seq": { "type": "integer", "minimum": 1 },
+    "id": { "type": "string", "format": "uuid" },
+    "ts": { "type": "string", "format": "date-time" },
+    "kind": { "enum": ["intent", "commit", "ai_session", "decision", "pattern", "summary", "hide"] },
+    "commit_sha": { "type": ["string", "null"], "pattern": "^[0-9a-f]{40}$" },
+    "prev_hash": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
+    "hash": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
+    "payload": {
+      "oneOf": [
+        { "$ref": "#/$defs/intent" },
18 Sep, 06:59commita387812docs: install modes (system-wide / per project), onboarding after install, English-only CLI
@@ .claude/scheduled_tasks.lock
@@ -0,0 +1 @@
+{"sessionId":"f6a75ad1-60d0-4dbd-a43d-417a147b0171","pid":56618,"procStart":"Thu Sep 17 20:23:23 2026","acquiredAt":1789677046569}
\ No newline at end of file
@@ cli/Cargo.lock
@@ -0,0 +1,1618 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
@@ cli/Cargo.toml
@@ -0,0 +1,40 @@
+[package]
+name = "pop"
+version = "0.1.0"
+edition = "2021"
+rust-version = "1.75"
+description = "Record how your project came together, and prove it."
+license = "MIT"
+repository = "https://github.com/pop-cli/pop"
+
+# This crate is standalone; the repository root is not a cargo workspace.
+[workspace]
+
+[lib]
+name = "pop"
+path = "src/lib.rs"
+
+[[bin]]
+name = "pop"
+path = "src/main.rs"
@@ cli/src/main.rs
@@ -0,0 +1 @@
+fn main() {}
@@ cli/tests/commands.rs
@@ -0,0 +1,144 @@
+mod common;
+
+use common::*;
+use pop::event::{EventKind, HidePayload};
+
+#[test]
+fn config_get_and_set_round_trip() {
+    let repo = init_repo();
+    init_pop(&repo);
+
+    assert_eq!(
+        pop::run_capture(repo.root(), &["config", "get", "llm.provider"])
+            .unwrap()
+            .trim(),
+        "auto"
+    );
+
+    pop::run_in(repo.root(), &["config", "set", "llm.model", "qwen3:4b"]).unwrap();
+    pop::run_in(repo.root(), &["config", "set", "llm.remote", "true"]).unwrap();
@@ cli/tests/common/mod.rs
@@ -0,0 +1,118 @@
+//! Shared helpers for the integration tests: throwaway git repos and an
+//! isolated device-key directory so tests never touch `~/.config/pop`.
+
+#![allow(dead_code)]
+
+use std::path::{Path, PathBuf};
+use std::sync::OnceLock;
+
+use tempfile::TempDir;
+
+static KEY_DIR: OnceLock<TempDir> = OnceLock::new();
+
+/// Point `pop` at a throwaway config dir for the whole test binary.
+///
+/// Every test in a binary gets the same directory, so repeatedly setting the
+/// variable is a no-op rather than a race.
+pub fn isolate_device_key() {
+    let dir = KEY_DIR.get_or_init(|| TempDir::new().expect("temp key dir"));
+    std::env::set_var("POP_CONFIG_DIR", dir.path());
@@ cli/tests/export.rs
@@ -0,0 +1,62 @@
+mod common;
+
+use common::*;
+
+#[test]
+fn export_writes_valid_json_with_intent_config_and_events() {
+    let repo = init_repo();
+    init_pop(&repo);
+    repo.write("a.txt", "one\n");
+    repo.commit_all("first");
+    pop::run_in(repo.root(), &["hook", "post-commit"]).unwrap();
+    pop::run_in(repo.root(), &["note", "wrote it down"]).unwrap();
+
+    let out = repo.path("export.json");
+    pop::run_in(repo.root(), &["export", out.to_str().unwrap()]).unwrap();
+
+    let raw = std::fs::read_to_string(&out).unwrap();
+    let v: serde_json::Value = serde_json::from_str(&raw).expect("valid JSON");
+
@@ cli/tests/hash_vector.rs
@@ -0,0 +1,97 @@
+//! The hash chain is the product. These vectors are computed independently
+//! (sha256 of the documented byte string) and must never drift.
+
+use pop::event::{canonical_json, hash_for, EventKind, IntentPayload, Payload, GENESIS_PREV_HASH};
+
+#[test]
+fn genesis_prev_hash_is_sixty_four_zeros() {
+    assert_eq!(GENESIS_PREV_HASH, "0".repeat(64));
+}
+
+#[test]
+fn canonical_json_sorts_keys_and_drops_whitespace() {
+    let v = serde_json::json!({
+        "zed": 1,
+        "alpha": { "b": [1, 2, {"y": true, "x": null}], "a": "x" },
+        "mid": "m"
+    });
+    assert_eq!(
+        canonical_json(&v),
@@ cli/tests/hook.rs
@@ -0,0 +1,174 @@
+mod common;
+
+use common::*;
+use pop::event::{CommitPayload, EventKind, FileStatus};
+
+fn commit_payloads(repo: &TestRepo) -> Vec<CommitPayload> {
+    events(repo)
+        .into_iter()
+        .filter(|e| e.kind == EventKind::Commit)
+        .map(|e| serde_json::from_value(e.payload).unwrap())
+        .collect()
+}
+
+#[test]
+fn hook_records_a_commit_with_stats_excerpt_and_no_previous_gap() {
+    let repo = init_repo();
+    init_pop(&repo);
+    repo.write("src/main.rs", "fn main() {\n    println!(\"one\");\n}\n");
+    let sha = repo.commit_all("feat: first light");
@@ cli/tests/init.rs
@@ -0,0 +1,107 @@
+mod common;
+
+use common::*;
+use pop::event::{EventKind, IntentPayload};
+
+#[test]
+fn init_creates_files_and_intent_event() {
+    let repo = init_repo();
+    init_pop(&repo);
+
+    assert!(repo.path(".pop/intent.md").is_file(), "intent.md written");
+    assert!(repo.path(".pop/config.toml").is_file(), "config.toml written");
+    assert!(repo.path(".pop/log.db").is_file(), "log.db created");
+    assert!(
+        repo.path(".git/hooks/post-commit").is_file(),
+        "post-commit hook installed"
+    );
+
+    let intent_md = std::fs::read_to_string(repo.path(".pop/intent.md")).unwrap();
@@ cli/tests/note.rs
@@ -0,0 +1,57 @@
+mod common;
+
+use common::*;
+use pop::event::{DecisionPayload, EventKind};
+
+#[test]
+fn note_appends_a_decision_anchored_to_head() {
+    let repo = init_repo();
+    init_pop(&repo);
+    repo.write("src/main.rs", "fn main() {}\n");
+    let sha = repo.commit_all("first commit");
+
+    pop::run_in(repo.root(), &["note", "chose sqlite over a json file"]).unwrap();
+
+    let evs = events(&repo);
+    let last = evs.last().unwrap();
+    assert_eq!(last.kind, EventKind::Decision);
+    assert_eq!(last.commit_sha.as_deref(), Some(sha.as_str()));
+
@@ cli/tests/verify.rs
@@ -0,0 +1,71 @@
+mod common;
+
+use common::*;
+
+fn capture(repo: &TestRepo, args: &[&str]) -> anyhow::Result<String> {
+    pop::run_capture(repo.root(), args)
+}
+
+#[test]
+fn verify_passes_on_an_untouched_chain() {
+    let repo = init_repo();
+    init_pop(&repo);
+    repo.write("a.txt", "one\n");
+    repo.commit_all("first");
+    pop::run_in(repo.root(), &["hook", "post-commit"]).unwrap();
+    pop::run_in(repo.root(), &["note", "picked a hash chain"]).unwrap();
+
+    let out = capture(&repo, &["verify"]).expect("verify passes");
+    assert!(out.contains("root "), "{out}");
@@ design/tokens.css
@@ -0,0 +1,144 @@
+/* pop design tokens
+   Consumed by: design/project-page.html (inlined), landing/src/site.css (Tailwind v4 @theme inline).
+   Fonts (Google Fonts): Schibsted Grotesk (display), Instrument Sans (text), Geist Mono (terminal, hashes).
+   Colors are OKLCH. One accent hue (ochre-rust, hue 55). Neutrals are tinted toward the same hue. */
+
+:root {
+  color-scheme: light dark;
+
+  /* Type families */
+  --font-display: "Schibsted Grotesk", "Instrument Sans", system-ui, sans-serif;
+  --font-text: "Instrument Sans", system-ui, sans-serif;
+  --font-mono: "Geist Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
+
+  /* Type scale, fluid. Body stays 16-17px; display tops out well under 6rem. */
+  --text-xs: 0.75rem;                                   /* 12 */
+  --text-sm: 0.875rem;                                  /* 14 */
+  --text-base: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem); /* 16-17 */
+  --text-lg: clamp(1.125rem, 1.05rem + 0.35vw, 1.25rem);/* 18-20 */
+  --text-xl: clamp(1.375rem, 1.2rem + 0.8vw, 1.75rem);  /* 22-28 */
@@ docs/superpowers/plans/2026-09-17-pop-v1-plan.md
@@ -9,7 +9,7 @@ Spec: `docs/superpowers/specs/2026-09-17-pop-v1-design.md`. Each task is owned b
 - Event model matching `schema/event.schema.json` (serde structs, canonical JSON, hash). Test vector: hash of a known event is stable.
 - `store`: rusqlite, append-only, `events` table + indexes on `kind`, `commit_sha`, `ts`.
 - `gitrepo`: git2 wrapper — HEAD, commit metadata, per-file diff stats vs first parent, 20-line diff excerpt, ignored-path filtering.
-- Commands per spec §4 except `publish`, `analyze`, `summarize`. `pop hook post-commit` records commit events; leave a clearly marked hook point `gitai::sessions_for_commit(repo, sha) -> Vec<AiSession>` that returns empty for now (task B fills it).
+- Commands per spec §4 except `publish`, `analyze`, `summarize`. Plus `pop onboard` per spec §6.1 (flags `--project` to skip the question and run `init`, `--yes`). All CLI text in English. `pop hook post-commit` records commit events; leave a clearly marked hook point `gitai::sessions_for_commit(repo, sha) -> Vec<AiSession>` that returns empty for now (task B fills it).
 - Integration tests in `cli/tests/` using temp repos.
 
 ### H. Server (`server/`)
@@ -41,7 +41,7 @@ Spec: `docs/superpowers/specs/2026-09-17-pop-v1-design.md`. Each task is owned b
 - ratatui preview, signing, HTTP POST to server, `--local`, `--yes`, thin-project warning. End-to-end test against the Go server via httptest-like stub (Rust side uses a tiny local HTTP listener in the test).
 
 ### I. Landing final + install scripts (`landing/`) — design agent
-- Replace iframe mock with real `pop publish --local` output of this repo. `install.sh`, `install.ps1`, terminal screenshots (SVG rendered from real terminal output), Plausible event. Pre-flight and audit passes clean.
+- Replace iframe mock with real `pop publish --local` output of this repo. `install.sh`, `install.ps1` with system-wide (default) and `--project` modes, both ending in `pop onboard` (spec §6.1); terminal screenshots (SVG rendered from real terminal output), Plausible event. Pre-flight and audit passes clean.
 
 ### J. Release plumbing
 - `README.md` (root, cli, server), MIT LICENSE, GitHub Actions: cargo test/clippy/fmt, go test/vet, release workflow building macOS/Windows/Linux ZIPs with SHA-256. Homebrew tap formula and winget manifest templates.
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -153,7 +153,16 @@ One page, one goal: copy the install command. Static HTML + Tailwind (built with
 
 Design process is mandated by the PRD and applied in this order by the design agent: Impeccable → Taste Skill → Emil Kowalski motion → uncodixfy pass. Tone: short sentences, second person, no "revolutionary", no emoji. Typography and spacing at the level of Linear or Raycast, not a README.
 
-Install: `curl -fsSL https://<domain>/install.sh | sh` (macOS/Linux) and `irm https://<domain>/install.ps1 | iex` (Windows); scripts live in `landing/public/`. Both download a release ZIP from GitHub Releases, verify SHA-256 from the release, and put `pop` on PATH. `cargo install pop-cli` also works.
+### 6.1 Install and onboarding
+
+Two install modes, both scripts (`landing/public/install.sh`, `install.ps1`):
+
+- **System-wide (default)**: `curl -fsSL https://<domain>/install.sh | sh` / `irm https://<domain>/install.ps1 | iex`. Downloads the release ZIP from GitHub Releases, verifies SHA-256, installs to `~/.local/bin/pop` (or `%LOCALAPPDATA%\pop\bin`), adds it to PATH for the user's shell if missing.
+- **Per project**: same command with `-s -- --project` (PowerShell: `-Project`). Installs the binary to `<repo>/.pop/bin/pop`, excluded from git, and the hook calls that path. Nothing touches the user's PATH.
+
+Both modes end by running `pop onboard`, which is the onboarding: it explains in three lines what pop records and what never leaves the machine, checks for git, Git AI and Ollama and prints what is missing with install hints, and, if the current directory is a git repo, offers to run `pop init` right there (per-project mode runs it without asking). Outside a repo it prints the two lines to do next. `pop onboard` is idempotent and can be re-run. `cargo install pop-cli` works too and prints the same onboarding hint on first run.
+
+Language: every user-facing string in the CLI, the project page and the default landing is English. HR and DE exist only as landing translations.
 
 ## 7. Privacy and trust rules baked into code
 
@@ landing/package-lock.json
@@ -0,0 +1,1131 @@
+{
+  "name": "landing",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "landing",
+      "version": "1.0.0",
+      "license": "ISC",
+      "devDependencies": {
+        "@tailwindcss/cli": "^4.3.3",
+        "tailwindcss": "^4.3.3"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
@@ landing/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "landing",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "directories": {
+    "example": "example"
+  },
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "type": "commonjs",
+  "devDependencies": {
+    "@tailwindcss/cli": "^4.3.3",
+    "tailwindcss": "^4.3.3"
+  }
@@ server/go.mod
@@ -0,0 +1,12 @@
+module github.com/pop-dev/popserver
+
+go 1.27.1
+
+require (
+	github.com/go-chi/chi/v5 v5.3.2 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+	github.com/jackc/pgx/v5 v5.11.0 // indirect
+	golang.org/x/text v0.29.0 // indirect
+)
@@ server/go.sum
@@ -0,0 +1,19 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
+github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
+github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
+golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ server/internal/sign/sign.go
@@ -0,0 +1,101 @@
+// Package sign holds the exact byte strings the pop CLI signs with its device
+// ed25519 key, plus the verification helpers the HTTP handlers use.
+//
+// The byte strings are a wire contract shared with the CLI. They are defined
+// here, once, and documented in server/README.md. Do not change them without
+// changing the CLI.
+package sign
+
+import (
+	"crypto/ed25519"
+	"encoding/hex"
+	"errors"
+	"fmt"
+)
+
+// PublicKeySize and SignatureSize are the raw byte lengths; on the wire both
+// are lowercase hex, so twice as many characters.
+const (
+	PublicKeySize = ed25519.PublicKeySize // 32
@@ server/internal/sign/sign_test.go
@@ -0,0 +1,83 @@
+package sign
+
+import (
+	"crypto/ed25519"
+	"encoding/hex"
+	"strings"
+	"testing"
+)
+
+func TestPublishMessageByteString(t *testing.T) {
+	got := PublishMessage("abc123", "2026-09-17T14:02:00Z", "quiet-heron-k7m2")
+	want := "abc123\n2026-09-17T14:02:00Z\nquiet-heron-k7m2"
+	if string(got) != want {
+		t.Fatalf("PublishMessage = %q, want %q", got, want)
+	}
+}
+
+func TestPublishMessageEmptySlug(t *testing.T) {
+	got := PublishMessage("abc123", "2026-09-17T14:02:00Z", "")
@@ server/internal/slug/slug.go
@@ -0,0 +1,101 @@
+// Package slug generates and validates the public page identifiers.
+//
+// Shape: <adjective>-<noun>-<4 characters of lowercase base32, no padding>.
+// The suffix carries 20 bits of entropy, which is plenty because collisions
+// are resolved by retrying against the store, not by hoping.
+package slug
+
+import (
+	"crypto/rand"
+	"encoding/base32"
+	"fmt"
+	"strings"
+)
+
+// MaxLen is the longest slug the server will accept from a client.
+const MaxLen = 64
+
+// MinLen is the shortest slug the server will accept from a client.
+const MinLen = 3
@@ server/internal/slug/slug_test.go
@@ -0,0 +1,87 @@
+package slug
+
+import (
+	"regexp"
+	"testing"
+)
+
+var canonical = regexp.MustCompile(`^[a-z]{3,}-[a-z]{3,}-[a-z2-7]{4}$`)
+
+func TestGenerateShape(t *testing.T) {
+	for i := 0; i < 200; i++ {
+		s, err := Generate()
+		if err != nil {
+			t.Fatal(err)
+		}
+		if !canonical.MatchString(s) {
+			t.Fatalf("slug %q does not match <adjective>-<noun>-<4 base32 lower>", s)
+		}
+		if !Valid(s) {
@@ server/internal/slug/words.go
@@ -0,0 +1,43 @@
+package slug
+
+// Adjectives and Nouns are small curated lists of neutral English words.
+// Keep them lowercase ASCII, three letters or more, and free of anything that
+// could combine into something unfortunate: these end up in public URLs.
+
+// Adjectives holds the first component of a slug.
+var Adjectives = []string{
+	"amber", "ancient", "autumn", "brave", "brief", "bright", "brisk", "calm",
+	"candid", "clear", "clever", "cobalt", "cool", "copper", "crisp", "curious",
+	"daily", "damp", "dawn", "deep", "dry", "dusty", "eager", "early",
+	"easy", "even", "fair", "fierce", "flat", "fleet", "fond", "fresh",
+	"gentle", "glad", "golden", "grand", "green", "happy", "hidden", "humble",
+	"icy", "idle", "ivory", "jolly", "keen", "kind", "late", "level",
+	"light", "linen", "little", "lively", "lone", "loud", "lucky", "mellow",
+	"merry", "mild", "misty", "modest", "narrow", "neat", "nimble", "noble",
+	"north", "olive", "open", "patient", "plain", "polite", "proud", "quick",
+	"quiet", "rapid", "ready", "rough", "round", "royal", "rustic", "sandy",
+	"shady", "sharp", "silent", "silver", "simple", "slate", "small", "smooth",
@@ server/internal/store/memory.go
@@ -0,0 +1,157 @@
+package store
+
+import (
+	"context"
+	"sync"
+	"time"
+
+	"github.com/google/uuid"
+)
+
+// Memory is an in-memory Store used by handler tests and by `go run` smoke
+// checks without a database. It is safe for concurrent use.
+type Memory struct {
+	mu       sync.Mutex
+	devices  map[uuid.UUID]Device
+	byKey    map[string]uuid.UUID
+	pages    map[string]Page
+	roots    []RootRecord
+	billing  [][]byte
@@ server/internal/store/memory_test.go
@@ -0,0 +1,14 @@
+package store_test
+
+import (
+	"testing"
+
+	"github.com/pop-dev/popserver/internal/store"
+	"github.com/pop-dev/popserver/internal/store/storetest"
+)
+
+func TestMemoryStoreConformance(t *testing.T) {
+	storetest.Run(t, func(t *testing.T) store.Store {
+		return store.NewMemory()
+	})
+}
@@ server/internal/store/store.go
@@ -0,0 +1,95 @@
+// Package store is the persistence boundary. Handlers depend on the Store
+// interface only, so they can be tested against the in-memory fake in this
+// package while production runs the Postgres implementation.
+package store
+
+import (
+	"context"
+	"errors"
+	"time"
+
+	"github.com/google/uuid"
+)
+
+// ErrNotFound is returned when a row does not exist.
+var ErrNotFound = errors.New("store: not found")
+
+// Device is a published-from machine, identified by its ed25519 public key.
+type Device struct {
+	ID        uuid.UUID
@@ server/internal/store/storetest/storetest.go
@@ -0,0 +1,225 @@
+// Package storetest holds the behaviour every store.Store must have. The
+// in-memory fake and the Postgres implementation run the same suite, so the
+// fake used in handler tests cannot quietly drift from the real database.
+package storetest
+
+import (
+	"context"
+	"crypto/ed25519"
+	"errors"
+	"testing"
+	"time"
+
+	"github.com/google/uuid"
+
+	"github.com/pop-dev/popserver/internal/store"
+)
+
+// Run executes the conformance suite. newStore must return a store with no
+// rows in it; it may call t.Skip.
18 Sep, 06:59decisionTwo install modes: system-wide to ~/.local/bin, or per project to .pop/bin so the hook calls that path and PATH stays untouched.
18 Sep, 06:59commit41a88dddocs: Git AI note format research, docs site section and task
@@ cli/src/chain.rs
@@ -0,0 +1,173 @@
+//! Appending to and verifying the hash chain.
+//!
+//! The chain is the whole point: an event's hash covers the previous hash, so
+//! editing one row invalidates every row after it.
+
+use crate::event::{hash_for, Event, EventKind, GENESIS_PREV_HASH};
+
+/// Build the next event in a chain. Pure: no clock, no database.
+pub fn next_event(
+    prev: Option<&Event>,
+    id: String,
+    ts: String,
+    kind: EventKind,
+    commit_sha: Option<String>,
+    payload: serde_json::Value,
+) -> Event {
+    let (seq, prev_hash) = match prev {
+        Some(p) => (p.seq + 1, p.hash.clone()),
+        None => (1, GENESIS_PREV_HASH.to_string()),
@@ cli/src/event.rs
@@ -0,0 +1,479 @@
+//! The event model, mirroring `schema/event.schema.json`.
+//!
+//! Every recorded fact is an [`Event`]. Events are chained: each one hashes
+//! the previous hash together with its own timestamp, kind and canonical
+//! payload, so a single edited row breaks everything after it.
+
+use std::fmt;
+use std::str::FromStr;
+
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+
+/// `prev_hash` of the first event in a chain: 64 zeros.
+pub const GENESIS_PREV_HASH: &str =
+    "0000000000000000000000000000000000000000000000000000000000000000";
+
+/// At most this many lines of diff are kept per file.
+pub const MAX_DIFF_EXCERPT_LINES: usize = 20;
+
@@ design/mock/data.mjs
@@ -0,0 +1,376 @@
+// Sample data for the project-page mock: pop's own first two weeks.
+// Compact rows are expanded into the shared page context (see plan §"Shared page context").
+
+export const project = {
+  name: "pop",
+  what: "A Rust CLI that records how a project came together and turns it into one page a hiring manager reads in two minutes.",
+  for_whom: "Students and juniors applying for a first job, who work with Claude Code and have nothing but a finished repo to show.",
+  tools: ["claude-code"],
+  delivered_url: "https://github.com/vanjamodrinjak/pop",
+  screenshot: null,
+  first_ts: "2026-09-03T09:12:00Z",
+  last_ts: "2026-09-17T14:02:00Z",
+};
+
+// [ts, kind, title, detail, extra]
+// kind: c = commit, d = decision, a = ai_session, h = hidden ai_session
+// extra for commits: { sha, files: [[path, +, -]], excerpt }
+// extra for ai sessions: { model, files: [[path, ai_lines, modified]], accepted }
+export const rows = [
@@ docs/superpowers/plans/2026-09-17-pop-v1-plan.md
@@ -43,6 +43,9 @@ Spec: `docs/superpowers/specs/2026-09-17-pop-v1-design.md`. Each task is owned b
 ### I. Landing final + install scripts (`landing/`) — design agent
 - Replace iframe mock with real `pop publish --local` output of this repo. `install.sh`, `install.ps1` with system-wide (default) and `--project` modes, both ending in `pop onboard` (spec §6.1); terminal screenshots (SVG rendered from real terminal output), Plausible event. Pre-flight and audit passes clean.
 
+### K. Docs site (`landing/docs/`) — design agent, after G
+- Spec §6.2. Layout template (sidebar, content, prev/next, client-side search), eleven pages, command reference generated from real `pop --help` output via a small script (`landing/scripts/gen-commands.sh`) committed with its output. Same tokens as the landing. Verified in the browser at desktop and 360 px.
+
 ### J. Release plumbing
 - `README.md` (root, cli, server), MIT LICENSE, GitHub Actions: cargo test/clippy/fmt, go test/vet, release workflow building macOS/Windows/Linux ZIPs with SHA-256. Homebrew tap formula and winget manifest templates.
 
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -164,6 +164,24 @@ Both modes end by running `pop onboard`, which is the onboarding: it explains in
 
 Language: every user-facing string in the CLI, the project page and the default landing is English. HR and DE exist only as landing translations.
 
+### 6.2 Documentation site (`landing/docs/`)
+
+Static docs at `/docs/`, same design system as the landing, the kind of docs every CLI tool ships: left sidebar navigation, content column, previous/next links, copyable code blocks, a search box (client-side, small inline index, no service). English only in v1. Pages:
+
+1. Introduction — what pop is, what it records, what the company sees.
+2. Installation — system-wide, per project, `cargo install`, verifying the download, uninstall.
+3. Getting started — `pop onboard`, `pop init`, first `pop note`, first `pop publish --local`.
+4. Commands — reference for every command and flag, generated from the real `pop <cmd> --help` output so it cannot drift.
+5. Concepts — events, the hash chain, the verification badge, what "confirmed" means, moments and patterns.
+6. AI sessions — Git AI, which tools are supported, what is and is not captured (no prompt text in notes, best-effort transcript lookup for Claude Code), hiding a session.
+7. Summaries — Ollama, heuristic fallback, editing, the "generated" marker, `llm.remote`.
+8. Configuration — every key of `.pop/config.toml`, environment variables.
+9. Privacy — local-first, what leaves the machine, `pop export`, deleting a page.
+10. Self-hosting the server — Docker, environment, endpoints.
+11. FAQ — the five landing questions plus troubleshooting (hook not firing, notes missing, no Ollama).
+
@@ docs/superpowers/specs/gitai-format.md
@@ -0,0 +1,66 @@
+# Git AI note format (research note, 2026-09-17)
+
+Source: git-ai-project/git-ai `main` @ 0670e7ef (v1.7.6), spec `specs/git_ai_standard_v3.0.0.md`, serializer `src/authorship/authorship_log_serialization.rs`, plus real notes from the project's own published `refs/notes/ai`.
+
+## Where
+
+- Ref: **`refs/notes/ai`**, one note per commit SHA, standard 2/38 fanout. Never read `refs/notes/commits`. `refs/notes/ai-remote/*` and `refs/notes/ai-display` are internal, ignore.
+- Notes are not fetched by a normal clone: `git fetch origin '+refs/notes/ai:refs/notes/ai'`. Locally the daemon writes them at commit time, so for the author's own machine they are present.
+- Optional HTTP notes backend (enterprise) can leave `refs/notes/ai` empty. Out of scope; we just find nothing.
+
+## Shape
+
+Text blob: attestation lines, a line that is exactly `---`, then one pretty-printed JSON object. No trailing newline guaranteed. Split at the **first** `---` line.
+
+```
+src/daemon.rs
+  s_2c8a703bb1fd71::t_d92697dfcc1b35 6476-6479,6484-6488,6490-6492
+  s_2c8a703bb1fd71::t_cafe0d045c5634 6480-6482,6489
+  h_ea2010baa7a42b 100,116-118,122
@@ server/internal/httpapi/api_test.go
@@ -0,0 +1,601 @@
+package httpapi_test
+
+import (
+	"bytes"
+	"crypto/ed25519"
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"io"
+	"log/slog"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/pop-dev/popserver/internal/httpapi"
+	"github.com/pop-dev/popserver/internal/sign"
@@ server/internal/httpapi/devices.go
@@ -0,0 +1,42 @@
+package httpapi
+
+import (
+	"net/http"
+
+	"github.com/pop-dev/popserver/internal/sign"
+)
+
+type createDeviceRequest struct {
+	PublicKey string `json:"public_key"`
+}
+
+type createDeviceResponse struct {
+	DeviceID string `json:"device_id"`
+}
+
+// handleCreateDevice registers an ed25519 public key. Idempotent: the same key
+// always maps to the same device id, so `pop init` can call it on every run.
+func (a *api) handleCreateDevice(w http.ResponseWriter, r *http.Request) {
@@ server/internal/httpapi/health.go
@@ -0,0 +1,25 @@
+package httpapi
+
+import (
+	"context"
+	"net/http"
+	"time"
+)
+
+type healthResponse struct {
+	Status string `json:"status"`
+}
+
+// handleHealthz answers 200 only when the database answers too, so a rolling
+// deploy does not send traffic to an instance that cannot serve anything.
+func (a *api) handleHealthz(w http.ResponseWriter, r *http.Request) {
+	ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
+	defer cancel()
+
+	if err := a.store.Ping(ctx); err != nil {
@@ server/internal/httpapi/middleware.go
@@ -0,0 +1,51 @@
+package httpapi
+
+import (
+	"log/slog"
+	"net/http"
+	"time"
+
+	"github.com/go-chi/chi/v5/middleware"
+)
+
+// exposeRequestID copies chi's request id onto the response so a user can
+// quote it in a bug report.
+func exposeRequestID(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if id := middleware.GetReqID(r.Context()); id != "" {
+			w.Header().Set("X-Request-Id", id)
+		}
+		next.ServeHTTP(w, r)
+	})
@@ server/internal/httpapi/pages.go
@@ -0,0 +1,319 @@
+package httpapi
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/google/uuid"
+
+	"github.com/pop-dev/popserver/internal/sign"
+	"github.com/pop-dev/popserver/internal/slug"
+	"github.com/pop-dev/popserver/internal/store"
+)
+
+// slugAttempts is how many times we retry a random slug before giving up.
+// With 20 bits in the suffix on top of the word pair, one collision is already
+// unlikely; ten in a row means something is wrong.
@@ server/internal/httpapi/respond.go
@@ -0,0 +1,65 @@
+package httpapi
+
+import (
+	"encoding/json"
+	"errors"
+	"io"
+	"log/slog"
+	"net/http"
+
+	"github.com/go-chi/chi/v5/middleware"
+)
+
+type errorBody struct {
+	Error   string `json:"error"`
+	Message string `json:"message"`
+}
+
+func writeJSON(w http.ResponseWriter, r *http.Request, status int, v any) {
+	buf, err := json.Marshal(v)
@@ server/internal/httpapi/router.go
@@ -0,0 +1,105 @@
+// Package httpapi wires the chi router, the middleware stack and every handler
+// in spec section 5. It depends on store.Store, never on pgx, so the whole API
+// can be exercised with an in-memory fake.
+package httpapi
+
+import (
+	"errors"
+	"fmt"
+	"log/slog"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/go-chi/chi/v5/middleware"
+
+	"github.com/pop-dev/popserver/internal/store"
+)
@@ server/internal/httpapi/webhook.go
@@ -0,0 +1,63 @@
+package httpapi
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"io"
+	"net/http"
+)
+
+// maxWebhookBytes caps the billing payload we are willing to read.
+const maxWebhookBytes = 1 << 20 // 1 MiB
+
+// handleLemonSqueezyWebhook verifies the X-Signature header (hex HMAC-SHA256
+// over the raw body, keyed with LEMONSQUEEZY_SECRET) and files the raw payload
+// in billing_events. Nothing else happens in v1: no plan gating exists yet, so
+// the endpoint is a verified stub that keeps the audit trail.
18 Sep, 06:59decisionGit AI carries capture; madelog starts at decisions, patterns and the page. No keylogger, no editor plugin.
18 Sep, 06:59commit059004cdocs: landing and docs site on Next.js + TypeScript (static export)
@@ cli/src/cli.rs
@@ -0,0 +1,240 @@
+//! Argument parsing and dispatch. Every user-facing string here is English.
+
+use anyhow::Result;
+use clap::{Args, Parser, Subcommand};
+
+use crate::{commands, Ctx, ExitError};
+
+#[derive(Debug, Parser)]
+#[command(
+    name = "pop",
+    version,
+    about = "Record how your project came together, and prove it.",
+    long_about = "pop runs alongside git. It records your commits, your AI sessions and the \
+one-sentence decisions you write down, chains them with sha256, and turns them into a page you \
+can send to a company. Nothing leaves your machine until you run `pop publish`."
+)]
+pub struct Cli {
+    #[command(subcommand)]
+    pub command: Command,
@@ cli/src/commands/analyze.rs
@@ -0,0 +1,11 @@
+//! `pop analyze` — find patterns in the log and record them as events.
+//!
+//! Task C implements the pattern and moment detection behind this.
+
+use anyhow::Result;
+
+use crate::{Ctx, ExitError};
+
+pub fn run(_ctx: &mut Ctx<'_>) -> Result<()> {
+    Err(ExitError::not_implemented("pop analyze"))
+}
@@ cli/src/commands/ask.rs
@@ -0,0 +1,11 @@
+//! `pop ask` — question answering over the project history.
+//!
+//! Out of scope for v1; it needs the embedded model and an index.
+
+use anyhow::Result;
+
+use crate::{Ctx, ExitError};
+
+pub fn run(_ctx: &mut Ctx<'_>, _question: &str) -> Result<()> {
+    Err(ExitError::unavailable("pop ask"))
+}
@@ cli/src/commands/init.rs
@@ -0,0 +1,356 @@
+//! `pop init` — four questions, then pop is recording.
+
+use std::path::{Path, PathBuf};
+
+use anyhow::{Context, Result};
+
+use crate::cli::InitArgs;
+use crate::config::Config;
+use crate::event::{IntentPayload, Payload};
+use crate::gitrepo::Repo;
+use crate::keys::DeviceKey;
+use crate::store::Store;
+use crate::Ctx;
+
+/// Marks the lines pop owns inside `.git/hooks/post-commit`.
+pub const HOOK_MARKER: &str = "# pop-cli:post-commit";
+
+const HOOK_COMMENT: &str = "# pop-cli:post-commit - records this commit in .pop/log.db";
+
@@ cli/src/commands/mod.rs
@@ -0,0 +1,18 @@
+//! One file per command, per spec §4.
+
+pub mod config;
+pub mod export;
+pub mod hide;
+pub mod hook;
+pub mod init;
+pub mod log;
+pub mod note;
+pub mod onboard;
+pub mod verify;
+
+/// Commands that exist so later tasks have somewhere to land.
+pub mod analyze;
+pub mod ask;
+pub mod model;
+pub mod publish;
+pub mod summarize;
@@ cli/src/commands/model.rs
@@ -0,0 +1,11 @@
+//! `pop model pull` — download the embedded model.
+//!
+//! Out of scope for v1: pop runs on Ollama or on the heuristic summariser.
+
+use anyhow::Result;
+
+use crate::{Ctx, ExitError};
+
+pub fn pull(_ctx: &mut Ctx<'_>) -> Result<()> {
+    Err(ExitError::unavailable("pop model pull"))
+}
@@ cli/src/commands/publish.rs
@@ -0,0 +1,17 @@
+//! `pop publish` — render the project page, preview it, sign the root and send it.
+//!
+//! Task E renders the page and task G adds the preview, signing and upload.
+
+use anyhow::Result;
+
+use crate::{Ctx, ExitError};
+
+pub struct Options {
+    pub local: bool,
+    pub server: Option<String>,
+    pub yes: bool,
+}
+
+pub fn run(_ctx: &mut Ctx<'_>, _opts: Options) -> Result<()> {
+    Err(ExitError::not_implemented("pop publish"))
+}
@@ cli/src/commands/summarize.rs
@@ -0,0 +1,15 @@
+//! `pop summarize` and `pop summary edit` — weekly summaries.
+//!
+//! Task D adds the Ollama client and the heuristic fallback behind these.
+
+use anyhow::Result;
+
+use crate::{Ctx, ExitError};
+
+pub fn run(_ctx: &mut Ctx<'_>, _week: Option<u32>) -> Result<()> {
+    Err(ExitError::not_implemented("pop summarize"))
+}
+
+pub fn edit(_ctx: &mut Ctx<'_>, _event_id: &str) -> Result<()> {
+    Err(ExitError::not_implemented("pop summary edit"))
+}
@@ cli/src/config.rs
@@ -0,0 +1,236 @@
+//! `.pop/config.toml` — the only knobs pop has.
+
+use std::path::Path;
+
+use anyhow::{anyhow, Context, Result};
+use serde::{Deserialize, Serialize};
+
+/// Every key `pop config get|set` accepts.
+pub const KEYS: &[&str] = &[
+    "llm.provider",
+    "llm.model",
+    "llm.remote",
+    "server.url",
+    "privacy.redact",
+    "project.name",
+    "project.delivered_url",
+    "project.screenshot",
+];
+
@@ cli/src/gitai.rs
@@ -0,0 +1,42 @@
+//! Reader for Git AI attribution notes.
+//!
+//! Git AI is installed separately by the user and writes its attribution into
+//! a git notes ref. pop never parses editor or tool logs itself; this module
+//! is the only place that knows Git AI's format.
+//!
+//! **Task B implements this.** Until then it returns no sessions, so the
+//! post-commit hook records commits normally on a machine without Git AI.
+//! See `docs/superpowers/specs/gitai-format.md` and the fixtures that will
+//! live in `cli/tests/fixtures/gitai/`.
+
+use anyhow::Result;
+use git2::Repository;
+
+use crate::event::AiSessionPayload;
+
+/// AI sessions attributed to `sha`, newest first.
+///
+/// Returns an empty vector when Git AI is not installed, wrote no note for
@@ cli/src/gitrepo.rs
@@ -0,0 +1,282 @@
+//! A thin git2 wrapper: HEAD, commit metadata, per-file stats and short diff
+//! excerpts — with the privacy rules from spec §7 applied at the source.
+
+use std::path::{Path, PathBuf};
+
+use anyhow::{anyhow, Context, Result};
+use git2::{Delta, Diff, DiffFindOptions, DiffOptions, Oid, Patch, Repository};
+use regex::Regex;
+
+use crate::event::{CommitFile, FileStatus, MAX_DIFF_EXCERPT_LINES};
+
+pub struct Repo {
+    inner: Repository,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct CommitMeta {
+    pub sha: String,
+    pub message: String,
@@ cli/src/keys.rs
@@ -0,0 +1,180 @@
+//! The device key: one ed25519 keypair per machine, used to sign the chain
+//! root when you publish. It never leaves `~/.config/pop/device.key`.
+
+use std::path::PathBuf;
+
+use anyhow::{anyhow, Context, Result};
+use ed25519_dalek::{Signer, SigningKey, SECRET_KEY_LENGTH};
+
+/// Override the key directory (used by tests and by per-project installs).
+pub const CONFIG_DIR_ENV: &str = "POP_CONFIG_DIR";
+
+/// `~/.config/pop`, or `$POP_CONFIG_DIR`.
+pub fn config_dir() -> Result<PathBuf> {
+    if let Some(dir) = std::env::var_os(CONFIG_DIR_ENV) {
+        if !dir.is_empty() {
+            return Ok(PathBuf::from(dir));
+        }
+    }
+    let home = dirs::home_dir().ok_or_else(|| anyhow!("cannot find your home directory"))?;
@@ cli/src/lib.rs
@@ -0,0 +1,167 @@
+//! `pop` — record how your project came together, and prove it.
+//!
+//! The binary is a thin shell over this library so that integration tests can
+//! drive commands against a temporary repository without spawning a process.
+
+pub mod chain;
+pub mod cli;
+pub mod commands;
+pub mod config;
+pub mod event;
+pub mod gitai;
+pub mod gitrepo;
+pub mod keys;
+pub mod store;
+
+use std::io::{BufRead, BufReader, Read, Write};
+use std::path::{Path, PathBuf};
+
+use anyhow::{Context, Result};
@@ cli/src/store.rs
@@ -0,0 +1,327 @@
+//! The append-only event log, `.pop/log.db`.
+
+use std::path::{Path, PathBuf};
+
+use anyhow::{Context, Result};
+use rusqlite::{Connection, OptionalExtension};
+
+use crate::chain::{self, Verification};
+use crate::event::{canonical_json, Event, EventKind, Payload};
+
+const SCHEMA: &str = r#"
+CREATE TABLE IF NOT EXISTS events (
+    seq        INTEGER PRIMARY KEY,
+    id         TEXT NOT NULL UNIQUE,
+    ts         TEXT NOT NULL,
+    kind       TEXT NOT NULL,
+    commit_sha TEXT,
+    payload    TEXT NOT NULL,
+    prev_hash  TEXT NOT NULL,
@@ design/mock/build.mjs
@@ -0,0 +1,562 @@
+// Builds design/project-page.html (static mock) and design/mock/context.json
+// (the same data in the shared page-context shape from the plan).
+// Run: node design/mock/build.mjs
+import { readFileSync, writeFileSync } from "node:fs";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import { project, rows, patterns, moments, ratio, summaries, badge } from "./data.mjs";
+
+const here = dirname(fileURLToPath(import.meta.url));
+const tokens = readFileSync(join(here, "..", "tokens.css"), "utf8");
+
+// ---------- context -------------------------------------------------------
+
+const T0 = Date.parse(project.first_ts);
+const T1 = Date.parse(project.last_ts);
+const xOf = (ts) => (Date.parse(ts) - T0) / (T1 - T0);
+const uuid = (i) => `0199${(0x4c00 + i).toString(16)}-7a2e-7${String(i).padStart(3, "0")}-8b1c-${(0x9e0000000000 + i * 7919).toString(16).padStart(12, "0")}`;
+
+const kindOf = { c: "commit", d: "decision", a: "ai_session", h: "hidden" };
@@ design/mock/context.json
@@ -0,0 +1,1485 @@
+{
+  "project": {
+    "name": "pop",
+    "what": "A Rust CLI that records how a project came together and turns it into one page a hiring manager reads in two minutes.",
+    "for_whom": "Students and juniors applying for a first job, who work with Claude Code and have nothing but a finished repo to show.",
+    "tools": [
+      "claude-code"
+    ],
+    "delivered_url": "https://github.com/vanjamodrinjak/pop",
+    "screenshot": null,
+    "first_ts": "2026-09-03T09:12:00Z",
+    "last_ts": "2026-09-17T14:02:00Z"
+  },
+  "stats": {
+    "commits": 45,
+    "decisions": 9,
+    "ai_sessions": 17,
+    "days_active": 13
+  },
@@ design/project-page.html
@@ -0,0 +1,833 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<!-- ctx: project.name -->
+<title>pop · how it came together</title>
+<meta name="description" content="A Rust CLI that records how a project came together and turns it into one page a hiring manager reads in two minutes.">
+<meta name="robots" content="noindex">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@500;600;700&family=Instrument+Sans:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+/* pop design tokens
+   Consumed by: design/project-page.html (inlined), landing/src/site.css (Tailwind v4 @theme inline).
+   Fonts (Google Fonts): Schibsted Grotesk (display), Instrument Sans (text), Geist Mono (terminal, hashes).
+   Colors are OKLCH. One accent hue (ochre-rust, hue 55). Neutrals are tinted toward the same hue. */
+
+:root {
@@ docs/superpowers/plans/2026-09-17-pop-v1-plan.md
@@ -18,7 +18,7 @@ Spec: `docs/superpowers/specs/2026-09-17-pop-v1-design.md`. Each task is owned b
 
 ### F. Design: project page + landing shell (`design/`, `landing/`) — design agent
 - Read the PRD sections "Landing page" and "Što stranica projekta pokazuje firmi" and spec §4.4, §6.
-- Deliver `design/tokens.css` (OKLCH colors, type scale, spacing), `design/project-page.html` (a static, fully styled mock of the project page using the exact Tera context below, with realistic sample data from this very repo's history), and `landing/index.html` (EN) with `landing/hr/index.html`, `landing/de/index.html`, built CSS via Tailwind CLI to `landing/dist/`. The example-page slot in the landing is an `<iframe src="example/index.html">`; put the mock there for now — task I replaces it with real output.
+- Deliver `design/tokens.css` (OKLCH colors, type scale, spacing), `design/project-page.html` (a static, fully styled mock of the project page using the exact Tera context below, with realistic sample data from this very repo's history), and the landing as a Next.js app in `landing/` (TypeScript strict, App Router, Tailwind, `output: 'export'`, ESLint + Prettier, `npm run build` produces `landing/out/`), routes `/`, `/hr/`, `/de/`. The example-page slot is an `<iframe src="/example/index.html">` served from `landing/public/example/`; put the mock there for now — task I replaces it with real output.
 - Mandated skill order per PRD: impeccable → design-taste-frontend → emil-design-eng → uncodixfy. Then frontend-design for own hand. No emoji, no "revolutionary", second person, short sentences.
 
 ## Phase 2 (parallel, after A)
@@ -44,7 +44,7 @@ Spec: `docs/superpowers/specs/2026-09-17-pop-v1-design.md`. Each task is owned b
 - Replace iframe mock with real `pop publish --local` output of this repo. `install.sh`, `install.ps1` with system-wide (default) and `--project` modes, both ending in `pop onboard` (spec §6.1); terminal screenshots (SVG rendered from real terminal output), Plausible event. Pre-flight and audit passes clean.
 
 ### K. Docs site (`landing/docs/`) — design agent, after G
-- Spec §6.2. Layout template (sidebar, content, prev/next, client-side search), eleven pages, command reference generated from real `pop --help` output via a small script (`landing/scripts/gen-commands.sh`) committed with its output. Same tokens as the landing. Verified in the browser at desktop and 360 px.
+- Spec §6.2. Routes `/docs/[slug]` in the Next app with MDX under `landing/content/docs/`; layout (sidebar, content, prev/next, client-side search), eleven pages, command reference generated from real `pop --help` output via a small script (`landing/scripts/gen-commands.sh`) committed with its output. Same tokens as the landing. Verified in the browser at desktop and 360 px.
 
 ### J. Release plumbing
 - `README.md` (root, cli, server), MIT LICENSE, GitHub Actions: cargo test/clippy/fmt, go test/vet, release workflow building macOS/Windows/Linux ZIPs with SHA-256. Homebrew tap formula and winget manifest templates.
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -149,7 +149,7 @@ Tests: handler tests with `httptest`; DB tests run against `DATABASE_URL` if set
 
 ## 6. Landing (`landing/`)
 
-One page, one goal: copy the install command. Static HTML + Tailwind (built with the Tailwind CLI to a static CSS file, no runtime), hosted on Cloudflare Pages. EN at `/`, HR at `/hr/`, DE at `/de/` — same structure, translated copy. Sections in scroll order per PRD §Landing page: hero with install command (macOS / Windows tab, copy button, "see an example page"), the embedded real example page, three blocks with terminal screenshots, "for whom" (student / company), privacy in three lines on the front page, FAQ with the five listed questions, footer (GitHub, contact, HR / DE / EN). No email form, no waitlist. Plausible script tag (EU, cookieless) with a `copy-install` custom event.
+One page, one goal: copy the install command. **(decision, user instruction 2026-09-17)** Built with Next.js (App Router) in TypeScript strict mode with Tailwind, exported as a static site (`output: 'export'`) and hosted on Cloudflare Pages; no server runtime, no client data fetching. ESLint + Prettier. The PRD's "no framework" line is superseded by the user's instruction; the static-export constraint keeps the PRD's hosting and performance intent. EN at `/`, HR at `/hr/`, DE at `/de/` — one page component, three dictionaries, `generateStaticParams`. The project page (§4.4) is NOT part of the Next app: the CLI renders it locally from a Tera template, and the landing embeds a real rendered copy from `public/example/index.html`. Sections in scroll order per PRD §Landing page: hero with install command (macOS / Windows tab, copy button, "see an example page"), the embedded real example page, three blocks with terminal screenshots, "for whom" (student / company), privacy in three lines on the front page, FAQ with the five listed questions, footer (GitHub, contact, HR / DE / EN). No email form, no waitlist. Plausible script tag (EU, cookieless) with a `copy-install` custom event.
 
 Design process is mandated by the PRD and applied in this order by the design agent: Impeccable → Taste Skill → Emil Kowalski motion → uncodixfy pass. Tone: short sentences, second person, no "revolutionary", no emoji. Typography and spacing at the level of Linear or Raycast, not a README.
 
@@ -166,7 +166,7 @@ Language: every user-facing string in the CLI, the project page and the default 
 
 ### 6.2 Documentation site (`landing/docs/`)
 
-Static docs at `/docs/`, same design system as the landing, the kind of docs every CLI tool ships: left sidebar navigation, content column, previous/next links, copyable code blocks, a search box (client-side, small inline index, no service). English only in v1. Pages:
+Docs at `/docs/` inside the same Next.js app (MDX content under `content/docs/`, statically exported), same design system as the landing, the kind of docs every CLI tool ships: left sidebar navigation, content column, previous/next links, copyable code blocks, a search box (client-side, small inline index, no service). English only in v1. Pages:
 
 1. Introduction — what pop is, what it records, what the company sees.
 2. Installation — system-wide, per project, `cargo install`, verifying the download, uninstall.
@@ landing/example/index.html
@@ -0,0 +1,833 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<!-- ctx: project.name -->
+<title>pop · how it came together</title>
+<meta name="description" content="A Rust CLI that records how a project came together and turns it into one page a hiring manager reads in two minutes.">
+<meta name="robots" content="noindex">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@500;600;700&family=Instrument+Sans:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+/* pop design tokens
+   Consumed by: design/project-page.html (inlined), landing/src/site.css (Tailwind v4 @theme inline).
+   Fonts (Google Fonts): Schibsted Grotesk (display), Instrument Sans (text), Geist Mono (terminal, hashes).
+   Colors are OKLCH. One accent hue (ochre-rust, hue 55). Neutrals are tinted toward the same hue. */
+
+:root {
@@ server/.dockerignore
@@ -0,0 +1,4 @@
+.env
+.git
+.github
+*_test.go
@@ server/Dockerfile
@@ -0,0 +1,28 @@
+# Build the binary against the module's own Go version, then ship it on a
+# distroless base: no shell, no package manager, nothing to patch. The result
+# is a ~15 MB image that idles around 20 MB of RSS.
+FROM golang:1.27-alpine AS build
+
+WORKDIR /src
+
+# Dependencies first, so a source-only change reuses this layer.
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+
+# CGO is off, so the binary is static. The migrations are embedded via
+# db/embed.go, which is why nothing else needs to be copied into the runtime
+# image.
+ENV CGO_ENABLED=0
+RUN go build -trimpath -ldflags="-s -w" -o /out/popserver ./cmd/popserver
+
@@ server/Makefile
@@ -0,0 +1,78 @@
+# popserver
+#
+#   make run     start the server against DATABASE_URL (see .env.example)
+#   make test    go test ./... (DB tests skip unless DATABASE_URL is set)
+#   make sqlc    regenerate internal/db from db/queries and db/migrations
+#   make lint    gofmt check plus go vet
+
+GO      ?= go
+SQLC    ?= sqlc
+DB_DSN  ?= postgres://pop:pop@localhost:5432/pop?sslmode=disable
+
+.PHONY: run test test-db sqlc lint fmt build tidy db-up db-down up down clean help
+
+## run: start the server locally
+run:
+	DATABASE_URL="$(DATABASE_URL)" \
+	PUBLIC_BASE_URL="$${PUBLIC_BASE_URL:-http://localhost:8080}" \
+	LOG_FORMAT="$${LOG_FORMAT:-text}" \
+	$(GO) run ./cmd/popserver
@@ server/README.md
@@ -0,0 +1,202 @@
+# popserver
+
+The Go service behind `pop publish`. It stores published project pages, keeps a
+history of hash-chain roots, serves the public page and its verification record,
+and files Lemon Squeezy webhooks for later. Nothing else.
+
+Spec: `docs/superpowers/specs/2026-09-17-pop-v1-design.md` section 5.
+
+```
+cmd/popserver        main: config, migrations, HTTP server, graceful shutdown
+internal/config      environment, validated once at startup
+internal/httpapi     chi router, middleware, every handler
+internal/sign        the exact byte strings the CLI signs, plus verification
+internal/slug        <adjective>-<noun>-<4 base32> generation and validation
+internal/store       Store interface, Postgres implementation, in-memory fake
+internal/migrate     tiny embedded migrator (no external tool)
+internal/db          sqlc output - generated, committed, never edited by hand
+db/migrations        the schema, append-only
+db/queries           the SQL sqlc turns into internal/db
@@ server/cmd/popserver/main.go
@@ -0,0 +1,104 @@
+// Command popserver stores published pop pages and their hash-chain roots.
+//
+// It applies its embedded migrations at startup, serves the API in spec
+// section 5, and shuts down gracefully on SIGINT or SIGTERM.
+package main
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log/slog"
+	"net/http"
+	"os"
+	"os/signal"
+	"syscall"
+	"time"
+
+	"github.com/pop-dev/popserver/internal/config"
+	"github.com/pop-dev/popserver/internal/httpapi"
@@ server/db/embed.go
@@ -0,0 +1,16 @@
+// Package dbassets embeds the SQL migrations so the binary carries its own
+// schema and needs no migration tool at deploy time.
+//
+// The same directory is what sqlc.yaml reads as its schema, so the Go types and
+// the applied schema can never disagree.
+package dbassets
+
+import "embed"
+
+// Migrations holds db/migrations/*.sql, applied at startup by internal/migrate.
+//
+//go:embed migrations/*.sql
+var Migrations embed.FS
+
+// MigrationsDir is the path inside Migrations that holds the .sql files.
+const MigrationsDir = "migrations"
@@ server/db/migrations/0001_init.sql
@@ -0,0 +1,41 @@
+-- 0001_init: devices, pages, the root history and the billing event log.
+-- Applied at startup by internal/migrate. Migrations are append-only: to change
+-- something, add 0002_*.sql. Never edit a file that has already shipped.
+
+CREATE TABLE devices (
+    id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+    public_key bytea       NOT NULL UNIQUE,
+    email      text,
+    created_at timestamptz NOT NULL DEFAULT now()
+);
+
+CREATE TABLE pages (
+    slug         text PRIMARY KEY,
+    device_id    uuid        NOT NULL REFERENCES devices (id),
+    html         text        NOT NULL,
+    root         text        NOT NULL,
+    first_ts     timestamptz NOT NULL,
+    last_ts      timestamptz NOT NULL,
+    published_ts timestamptz NOT NULL,
@@ server/db/queries/billing.sql
@@ -0,0 +1,4 @@
+-- name: InsertBillingEvent :exec
+-- The raw, already HMAC-verified webhook body, stored verbatim.
+INSERT INTO billing_events (payload)
+VALUES ($1);
@@ server/db/queries/devices.sql
@@ -0,0 +1,12 @@
+-- name: UpsertDevice :one
+-- Idempotent on public_key: the DO UPDATE is a no-op write that lets us use
+-- RETURNING to get the existing row back in one round trip.
+INSERT INTO devices (public_key)
+VALUES ($1)
+ON CONFLICT (public_key) DO UPDATE SET public_key = EXCLUDED.public_key
+RETURNING id, public_key, email, created_at;
+
+-- name: GetDevice :one
+SELECT id, public_key, email, created_at
+FROM devices
+WHERE id = $1;
@@ server/db/queries/pages.sql
@@ -0,0 +1,38 @@
+-- name: GetPage :one
+-- Returns deleted pages too; the caller decides what that means.
+SELECT slug, device_id, html, root, first_ts, last_ts, published_ts, received_at, deleted_at
+FROM pages
+WHERE slug = $1;
+
+-- name: UpsertPage :one
+-- A re-publish overwrites html, root and the timestamps, stamps a fresh
+-- received_at, and clears deleted_at so a deleted page can come back.
+INSERT INTO pages (slug, device_id, html, root, first_ts, last_ts, published_ts, received_at, deleted_at)
+VALUES ($1, $2, $3, $4, $5, $6, $7, now(), NULL)
+ON CONFLICT (slug) DO UPDATE
+    SET html         = EXCLUDED.html,
+        root         = EXCLUDED.root,
+        first_ts     = EXCLUDED.first_ts,
+        last_ts      = EXCLUDED.last_ts,
+        published_ts = EXCLUDED.published_ts,
+        received_at  = now(),
+        deleted_at   = NULL
@@ server/docker-compose.yml
@@ -0,0 +1,38 @@
+# Local development: Postgres plus the server.
+#
+#   make db-up     just the database, for `make run` and the DB tests
+#   make up        database and server together
+services:
+  db:
+    image: postgres:16
+    environment:
+      POSTGRES_USER: pop
+      POSTGRES_PASSWORD: pop
+      POSTGRES_DB: pop
+    ports:
+      - "5432:5432"
+    volumes:
+      - pgdata:/var/lib/postgresql/data
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U pop -d pop"]
+      interval: 2s
+      timeout: 3s
@@ server/go.mod
@@ -1,12 +1,17 @@
 module github.com/pop-dev/popserver
 
-go 1.27.1
+go 1.27
+
+require (
+	github.com/go-chi/chi/v5 v5.3.2
+	github.com/google/uuid v1.6.0
+	github.com/jackc/pgx/v5 v5.11.0
+)
 
 require (
-	github.com/go-chi/chi/v5 v5.3.2 // indirect
-	github.com/google/uuid v1.6.0 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
-	github.com/jackc/pgx/v5 v5.11.0 // indirect
+	github.com/jackc/puddle/v2 v2.2.2 // indirect
+	golang.org/x/sync v0.17.0 // indirect
@@ server/go.sum
@@ -1,4 +1,6 @@
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
 github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -9,11 +11,20 @@ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7Ulw
 github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
 github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
 github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
@@ server/internal/config/config.go
@@ -0,0 +1,92 @@
+// Package config reads the server's environment. Everything the process needs
+// is here, validated once at startup, so no handler ever calls os.Getenv.
+package config
+
+import (
+	"fmt"
+	"log/slog"
+	"os"
+	"strings"
+	"time"
+)
+
+// Config is the validated environment.
+type Config struct {
+	// DatabaseURL is the pgx DSN. Required.
+	DatabaseURL string
+	// ListenAddr is the address the HTTP server binds. Default ":8080".
+	ListenAddr string
+	// PublicBaseURL is the origin used to build page URLs, without a trailing
@@ server/internal/config/config_test.go
@@ -0,0 +1,74 @@
+package config
+
+import "testing"
+
+func TestLoadDefaults(t *testing.T) {
+	env := map[string]string{
+		"DATABASE_URL":    "postgres://pop:pop@localhost:5432/pop",
+		"PUBLIC_BASE_URL": "https://pop.example/",
+	}
+	cfg, err := load(func(k string) string { return env[k] })
+	if err != nil {
+		t.Fatal(err)
+	}
+	if cfg.ListenAddr != ":8080" {
+		t.Errorf("ListenAddr = %q, want :8080", cfg.ListenAddr)
+	}
+	if cfg.PublicBaseURL != "https://pop.example" {
+		t.Errorf("trailing slash not trimmed: %q", cfg.PublicBaseURL)
+	}
@@ server/internal/db/billing.sql.go
@@ -0,0 +1,21 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.31.1
+// source: billing.sql
+
+package db
+
+import (
+	"context"
+)
+
+const insertBillingEvent = `-- name: InsertBillingEvent :exec
+INSERT INTO billing_events (payload)
+VALUES ($1)
+`
+
+// The raw, already HMAC-verified webhook body, stored verbatim.
+func (q *Queries) InsertBillingEvent(ctx context.Context, payload []byte) error {
+	_, err := q.db.Exec(ctx, insertBillingEvent, payload)
@@ server/internal/db/db.go
@@ -0,0 +1,32 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.31.1
+
+package db
+
+import (
+	"context"
+
+	"github.com/jackc/pgx/v5"
+	"github.com/jackc/pgx/v5/pgconn"
+)
+
+type DBTX interface {
+	Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
+	Query(context.Context, string, ...interface{}) (pgx.Rows, error)
+	QueryRow(context.Context, string, ...interface{}) pgx.Row
+}
+
@@ server/internal/db/devices.sql.go
@@ -0,0 +1,51 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.31.1
+// source: devices.sql
+
+package db
+
+import (
+	"context"
+
+	"github.com/google/uuid"
+)
+
+const getDevice = `-- name: GetDevice :one
+SELECT id, public_key, email, created_at
+FROM devices
+WHERE id = $1
+`
+
@@ server/internal/db/models.go
@@ -0,0 +1,43 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.31.1
+
+package db
+
+import (
+	"time"
+
+	"github.com/google/uuid"
+)
+
+type BillingEvent struct {
+	ID         int64
+	ReceivedAt time.Time
+	Payload    []byte
+}
+
+type Device struct {
@@ server/internal/db/pages.sql.go
@@ -0,0 +1,161 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.31.1
+// source: pages.sql
+
+package db
+
+import (
+	"context"
+	"time"
+
+	"github.com/google/uuid"
+)
+
+const getPage = `-- name: GetPage :one
+SELECT slug, device_id, html, root, first_ts, last_ts, published_ts, received_at, deleted_at
+FROM pages
+WHERE slug = $1
+`
@@ server/internal/migrate/migrate.go
@@ -0,0 +1,174 @@
+// Package migrate applies the embedded SQL migrations at startup.
+//
+// It is deliberately tiny: read db/migrations/*.sql in version order, run each
+// unapplied one inside a transaction, record it in schema_migrations. No
+// external tool, no down migrations. To change the schema, add a new file.
+package migrate
+
+import (
+	"context"
+	"fmt"
+	"io/fs"
+	"log/slog"
+	"path"
+	"sort"
+	"strings"
+
+	"github.com/jackc/pgx/v5/pgxpool"
+
+	dbassets "github.com/pop-dev/popserver/db"
@@ server/internal/migrate/migrate_test.go
@@ -0,0 +1,65 @@
+package migrate
+
+import (
+	"testing"
+	"testing/fstest"
+)
+
+func TestParseSortsByNumericVersion(t *testing.T) {
+	fsys := fstest.MapFS{
+		"migrations/0010_tenth.sql":  {Data: []byte("SELECT 10;")},
+		"migrations/0002_second.sql": {Data: []byte("SELECT 2;")},
+		"migrations/0001_init.sql":   {Data: []byte("SELECT 1;")},
+		"migrations/README.md":       {Data: []byte("not a migration")},
+	}
+	got, err := parse(fsys, "migrations")
+	if err != nil {
+		t.Fatal(err)
+	}
+	want := []string{"0001_init", "0002_second", "0010_tenth"}
@@ server/internal/store/postgres.go
@@ -0,0 +1,183 @@
+package store
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"github.com/google/uuid"
+	"github.com/jackc/pgx/v5"
+	"github.com/jackc/pgx/v5/pgxpool"
+
+	"github.com/pop-dev/popserver/internal/db"
+)
+
+// Postgres is the production Store, backed by a pgx connection pool and the
+// sqlc-generated queries in internal/db.
+type Postgres struct {
+	pool *pgxpool.Pool
+	q    *db.Queries
@@ server/internal/store/postgres_test.go
@@ -0,0 +1,84 @@
+package store_test
+
+import (
+	"context"
+	"log/slog"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/jackc/pgx/v5/pgxpool"
+
+	"github.com/pop-dev/popserver/internal/migrate"
+	"github.com/pop-dev/popserver/internal/store"
+	"github.com/pop-dev/popserver/internal/store/storetest"
+)
+
+// TestPostgresStoreConformance runs the same suite as the in-memory fake
+// against a real database. It only runs when DATABASE_URL is set:
+//
@@ server/sqlc.yaml
@@ -0,0 +1,23 @@
+version: "2"
+sql:
+  - engine: "postgresql"
+    schema: "db/migrations"
+    queries: "db/queries"
+    gen:
+      go:
+        package: "db"
+        out: "internal/db"
+        sql_package: "pgx/v5"
+        emit_empty_slices: true
+        emit_pointers_for_null_types: true
+        overrides:
+          - db_type: "uuid"
+            go_type: "github.com/google/uuid.UUID"
+          - db_type: "timestamptz"
+            go_type: "time.Time"
+          - db_type: "timestamptz"
+            nullable: true
18 Sep, 06:59decisionLanding moved to Next.js static export in TypeScript strict; the PRD's no-framework line is superseded.
18 Sep, 06:59commit90af160docs: Railway hosting, monochromatic design with GitLens-style hero graph
@@ cli/src/commands/config.rs
@@ -0,0 +1,30 @@
+//! `pop config get|set|list`.
+
+use anyhow::Result;
+
+use crate::cli::ConfigCommand;
+use crate::config::{Config, KEYS};
+use crate::Ctx;
+
+pub fn run(ctx: &mut Ctx<'_>, command: ConfigCommand) -> Result<()> {
+    let path = ctx.pop_dir()?.join("config.toml");
+    let mut config = Config::load(&path)?;
+
+    match command {
+        ConfigCommand::Get { key } => {
+            let value = config.get(&key)?;
+            ctx.say(&value)?;
+        }
+        ConfigCommand::Set { key, value } => {
+            config.set(&key, &value)?;
@@ cli/src/commands/export.rs
@@ -0,0 +1,66 @@
+//! `pop export` — everything pop holds about you, as JSON.
+
+use std::path::Path;
+
+use anyhow::{Context, Result};
+use serde::Serialize;
+
+use crate::chain;
+use crate::config::Config;
+use crate::event::{Event, EventKind, IntentPayload};
+use crate::Ctx;
+
+#[derive(Debug, Serialize)]
+pub struct Export {
+    pub version: u32,
+    pub exported_at: String,
+    pub intent: Option<IntentPayload>,
+    pub intent_md: Option<String>,
+    pub config: Config,
@@ cli/src/commands/hide.rs
@@ -0,0 +1,32 @@
+//! `pop hide` — keep one AI session off the page without removing it from the
+//! chain. The page says "1 session hidden".
+
+use anyhow::{bail, Result};
+
+use crate::event::{EventKind, HidePayload, Payload};
+use crate::Ctx;
+
+pub fn run(ctx: &mut Ctx<'_>, event_id: &str) -> Result<()> {
+    let store = ctx.open_store()?;
+    let Some(target) = store.by_id(event_id)? else {
+        bail!("no event with id `{event_id}` in this log");
+    };
+    if target.kind != EventKind::AiSession {
+        bail!(
+            "only ai_session events can be hidden; event `{event_id}` is a {}",
+            target.kind
+        );
+    }
@@ cli/src/commands/hook.rs
@@ -0,0 +1,97 @@
+//! `pop hook post-commit` — the only thing that runs without you asking.
+//!
+//! It must never fail a commit. Every error is written to stderr and the
+//! command still succeeds.
+
+use anyhow::{Context, Result};
+
+use crate::config::Config;
+use crate::event::{CommitPayload, EventKind, IntentPayload, Payload};
+use crate::gitrepo::{ExcerptFilter, Repo};
+use crate::store::Store;
+use crate::Ctx;
+
+pub fn post_commit(ctx: &mut Ctx<'_>) -> Result<()> {
+    if let Err(err) = record(ctx) {
+        // A broken hook must not stand between you and your commit.
+        eprintln!("pop: could not record this commit: {err:#}");
+    }
+    Ok(())
@@ cli/src/commands/log.rs
@@ -0,0 +1,37 @@
+//! `pop log` — what pop has recorded.
+
+use anyhow::{Context, Result};
+
+use crate::Ctx;
+
+pub fn run(ctx: &mut Ctx<'_>, json: bool) -> Result<()> {
+    let events = ctx.open_store()?.all()?;
+
+    if json {
+        let body = serde_json::to_string_pretty(&events).context("rendering events as JSON")?;
+        ctx.say(&body)?;
+        return Ok(());
+    }
+
+    if events.is_empty() {
+        ctx.say("nothing recorded yet")?;
+        return Ok(());
+    }
@@ cli/src/commands/note.rs
@@ -0,0 +1,31 @@
+//! `pop note` — one sentence, anchored to the commit you are on.
+
+use anyhow::{bail, Result};
+
+use crate::event::{DecisionPayload, Payload};
+use crate::gitrepo::Repo;
+use crate::Ctx;
+
+pub fn run(ctx: &mut Ctx<'_>, text: &str, private: bool) -> Result<()> {
+    let text = text.trim();
+    if text.is_empty() {
+        bail!("a note needs a sentence: pop note \"why you did it that way\"");
+    }
+    let store = ctx.open_store()?;
+    let head = Repo::discover(&ctx.cwd)?.head_sha()?;
+    let event = store.append_payload(
+        head.as_deref(),
+        &Payload::Decision(DecisionPayload {
+            text: text.to_string(),
@@ cli/src/commands/onboard.rs
@@ -0,0 +1,90 @@
+//! `pop onboard` — what the install script runs last, and what you can run
+//! again any time. Spec §6.1.
+
+use anyhow::Result;
+
+use crate::commands::init;
+use crate::gitrepo::Repo;
+use crate::Ctx;
+
+/// The tools pop cooperates with, and where to get them.
+const TOOLS: &[(&str, &str, &str)] = &[
+    ("git", "pop records your commits.", "https://git-scm.com/downloads"),
+    (
+        "git-ai",
+        "Without it, AI sessions stay empty.",
+        "https://github.com/oleander/git-ai",
+    ),
+    (
+        "ollama",
@@ cli/src/commands/verify.rs
@@ -0,0 +1,21 @@
+//! `pop verify` — recompute the chain from the first event.
+
+use anyhow::Result;
+
+use crate::chain::Verification;
+use crate::{Ctx, ExitError};
+
+pub fn run(ctx: &mut Ctx<'_>) -> Result<()> {
+    match ctx.open_store()?.verify()? {
+        Verification::Ok { root, count } => {
+            ctx.say("chain ok")?;
+            ctx.say(&format!("{count} events, root {root}"))?;
+            Ok(())
+        }
+        Verification::Broken { seq, reason, count } => Err(ExitError::new(
+            1,
+            format!("chain broken at seq {seq}: {reason} ({count} events read)"),
+        )
+        .into()),
@@ cli/src/main.rs
@@ -1 +1,32 @@
-fn main() {}
+use std::ffi::OsString;
+
+fn main() {
+    let cwd = match std::env::current_dir() {
+        Ok(dir) => dir,
+        Err(e) => {
+            eprintln!("pop: cannot read the current directory: {e}");
+            std::process::exit(1);
+        }
+    };
+
+    let args: Vec<OsString> = std::env::args_os().skip(1).collect();
+    let args: Vec<String> = args
+        .into_iter()
+        .map(|a| a.to_string_lossy().into_owned())
+        .collect();
+    let args: Vec<&str> = args.iter().map(String::as_str).collect();
+
@@ design/mock/build.mjs
@@ -76,7 +76,7 @@ function dayTicks() {
     const x = xOf(new Date(t).toISOString());
     if (x < 0) continue;
     const day = new Date(t);
-    const label = day.getUTCDay() === 1 || t === start.getTime() ? d(day.toISOString()) : String(day.getUTCDate());
+    const label = day.getUTCDay() === 1 || out.length === 0 ? d(day.toISOString()) : String(day.getUTCDate());
     out.push({ x: px(x), label, major: day.getUTCDay() === 1 });
   }
   return out;
@@ -266,7 +266,7 @@ section{padding-top:var(--space-16);scroll-margin-top:var(--space-6)}
 .records[open]>summary::before{transform:rotate(45deg)}
 .records>summary .count{color:var(--ink-3);font-weight:400}
 .rec{border-top:1px solid var(--line);scroll-margin-top:var(--space-8)}
-.rec:target{background:var(--accent-soft);box-shadow:0 0 0 var(--space-3) var(--accent-soft)}
+.rec:target{background:var(--accent-soft);box-shadow:0 0 0 var(--space-3) var(--accent-soft);border-top-color:transparent}
 .rec>summary,.rec-flat{display:grid;grid-template-columns:7.2rem 6.2rem 4.4rem minmax(0,1fr);align-items:baseline;gap:var(--space-3);padding:var(--space-2) 0;font-size:var(--text-sm);line-height:1.45}
 .rec>summary{cursor:pointer;list-style:none}
 .rec>summary::-webkit-details-marker{display:none}
@@ -276,7 +276,9 @@ section{padding-top:var(--space-16);scroll-margin-top:var(--space-6)}
 .rec-decision{color:var(--accent-ink);font-weight:500}
@@ design/project-page.html
@@ -110,7 +110,7 @@
     --ink-3: oklch(0.58 0.012 70);
     --accent: oklch(0.75 0.12 65);
     --accent-ink: oklch(0.82 0.11 70);
-    --accent-soft: oklch(0.30 0.05 60);
+    --accent-soft: oklch(0.265 0.035 60);
     --accent-line: oklch(0.48 0.09 60);
     --ok: oklch(0.72 0.11 150);
     --danger: oklch(0.70 0.14 25);
@@ -138,7 +138,7 @@
   --ink-3: oklch(0.58 0.012 70);
   --accent: oklch(0.75 0.12 65);
   --accent-ink: oklch(0.82 0.11 70);
-  --accent-soft: oklch(0.30 0.05 60);
+  --accent-soft: oklch(0.265 0.035 60);
   --accent-line: oklch(0.48 0.09 60);
   --ok: oklch(0.72 0.11 150);
   --danger: oklch(0.70 0.14 25);
@@ -235,7 +235,7 @@ section{padding-top:var(--space-16);scroll-margin-top:var(--space-6)}
 .records[open]>summary::before{transform:rotate(45deg)}
@@ design/tokens.css
@@ -97,7 +97,7 @@
     --ink-3: oklch(0.58 0.012 70);
     --accent: oklch(0.75 0.12 65);
     --accent-ink: oklch(0.82 0.11 70);
-    --accent-soft: oklch(0.30 0.05 60);
+    --accent-soft: oklch(0.265 0.035 60);
     --accent-line: oklch(0.48 0.09 60);
     --ok: oklch(0.72 0.11 150);
     --danger: oklch(0.70 0.14 25);
@@ -125,7 +125,7 @@
   --ink-3: oklch(0.58 0.012 70);
   --accent: oklch(0.75 0.12 65);
   --accent-ink: oklch(0.82 0.11 70);
-  --accent-soft: oklch(0.30 0.05 60);
+  --accent-soft: oklch(0.265 0.035 60);
   --accent-line: oklch(0.48 0.09 60);
   --ok: oklch(0.72 0.11 150);
   --danger: oklch(0.70 0.14 25);
@@ docs/superpowers/plans/2026-09-17-pop-v1-plan.md
@@ -46,6 +46,9 @@ Spec: `docs/superpowers/specs/2026-09-17-pop-v1-design.md`. Each task is owned b
 ### K. Docs site (`landing/docs/`) — design agent, after G
 - Spec §6.2. Routes `/docs/[slug]` in the Next app with MDX under `landing/content/docs/`; layout (sidebar, content, prev/next, client-side search), eleven pages, command reference generated from real `pop --help` output via a small script (`landing/scripts/gen-commands.sh`) committed with its output. Same tokens as the landing. Verified in the browser at desktop and 360 px.
 
+### L. Railway deployment — after H and I
+- Railway project `pop`: Postgres service, `popserver` from `server/Dockerfile` with `DATABASE_URL`, `PUBLIC_BASE_URL`, `LEMONSQUEEZY_SECRET` set as variables; landing as a static service from `landing/out/`. Generate domains, verify `/healthz` and a real `pop publish` round trip. Document in `docs/deploy.md`.
+
 ### J. Release plumbing
 - `README.md` (root, cli, server), MIT LICENSE, GitHub Actions: cargo test/clippy/fmt, go test/vet, release workflow building macOS/Windows/Linux ZIPs with SHA-256. Homebrew tap formula and winget manifest templates.
 
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -129,7 +129,9 @@ Single screen, three panes: timeline list (left), detail of selected item (right
 
 ## 5. Server (`server/`, Go 1.22+)
 
-chi router, pgx v5, sqlc-generated queries, one binary, Dockerfile, `docker-compose.yml` for local Postgres. Env: `DATABASE_URL`, `LISTEN_ADDR`, `PUBLIC_BASE_URL`, `LEMONSQUEEZY_SECRET`.
+chi router, pgx v5, sqlc-generated queries, one binary, Dockerfile, `docker-compose.yml` for local Postgres. Env: `DATABASE_URL`, `LISTEN_ADDR` (honours `PORT` when set), `PUBLIC_BASE_URL`, `LEMONSQUEEZY_SECRET`.
+
+**(decision, user instruction 2026-09-17)** Hosting is Railway, not Hetzner: one Railway project with a Postgres service, the Go server deployed from `server/Dockerfile` (EU region), and the landing deployed as a static site from `landing/out/`. Deployment is a plan task; secrets never enter the repo.
 
 Endpoints:
 
@@ -151,6 +153,8 @@ Tests: handler tests with `httptest`; DB tests run against `DATABASE_URL` if set
 
 One page, one goal: copy the install command. **(decision, user instruction 2026-09-17)** Built with Next.js (App Router) in TypeScript strict mode with Tailwind, exported as a static site (`output: 'export'`) and hosted on Cloudflare Pages; no server runtime, no client data fetching. ESLint + Prettier. The PRD's "no framework" line is superseded by the user's instruction; the static-export constraint keeps the PRD's hosting and performance intent. EN at `/`, HR at `/hr/`, DE at `/de/` — one page component, three dictionaries, `generateStaticParams`. The project page (§4.4) is NOT part of the Next app: the CLI renders it locally from a Tera template, and the landing embeds a real rendered copy from `public/example/index.html`. Sections in scroll order per PRD §Landing page: hero with install command (macOS / Windows tab, copy button, "see an example page"), the embedded real example page, three blocks with terminal screenshots, "for whom" (student / company), privacy in three lines on the front page, FAQ with the five listed questions, footer (GitHub, contact, HR / DE / EN). No email form, no waitlist. Plausible script tag (EU, cookieless) with a `copy-install` custom event.
 
+**Design direction (user instruction 2026-09-17):** monochromatic palette (one hue family in OKLCH, greys plus a single accent lightness step, light and dark), and a hero animation that resembles the GitLens commit graph: several lanes of commits with curved branch and merge paths drawn in as SVG on load, dots landing in sequence, a few nodes marked as decisions and AI sessions to foreshadow the product. Pure SVG + CSS/requestAnimationFrame, no animation library, under 60 KB, honours `prefers-reduced-motion` (static final frame).
+
 Design process is mandated by the PRD and applied in this order by the design agent: Impeccable → Taste Skill → Emil Kowalski motion → uncodixfy pass. Tone: short sentences, second person, no "revolutionary", no emoji. Typography and spacing at the level of Linear or Raycast, not a README.
 
 ### 6.1 Install and onboarding
@@ landing/example/index.html
@@ -110,7 +110,7 @@
     --ink-3: oklch(0.58 0.012 70);
     --accent: oklch(0.75 0.12 65);
     --accent-ink: oklch(0.82 0.11 70);
-    --accent-soft: oklch(0.30 0.05 60);
+    --accent-soft: oklch(0.265 0.035 60);
     --accent-line: oklch(0.48 0.09 60);
     --ok: oklch(0.72 0.11 150);
     --danger: oklch(0.70 0.14 25);
@@ -138,7 +138,7 @@
   --ink-3: oklch(0.58 0.012 70);
   --accent: oklch(0.75 0.12 65);
   --accent-ink: oklch(0.82 0.11 70);
-  --accent-soft: oklch(0.30 0.05 60);
+  --accent-soft: oklch(0.265 0.035 60);
   --accent-line: oklch(0.48 0.09 60);
   --ok: oklch(0.72 0.11 150);
   --danger: oklch(0.70 0.14 25);
@@ -235,7 +235,7 @@ section{padding-top:var(--space-16);scroll-margin-top:var(--space-6)}
 .records[open]>summary::before{transform:rotate(45deg)}
18 Sep, 06:59commitbbdd7e7docs: landing density and professional register
@@ cli/src/cli.rs
@@ -194,14 +194,21 @@ pub fn dispatch(cli: Cli, ctx: &mut Ctx<'_>) -> Result<()> {
         Command::Export(args) => commands::export::run(ctx, args.path.as_deref()),
         Command::Hide(args) => commands::hide::run(ctx, &args.event_id),
         Command::Config(args) => commands::config::run(ctx, args.command),
-        Command::Publish(_) => Err(ExitError::not_implemented("pop publish")),
-        Command::Analyze => Err(ExitError::not_implemented("pop analyze")),
-        Command::Summarize(_) => Err(ExitError::not_implemented("pop summarize")),
-        Command::Summary(SummaryCommand::Edit { .. }) => {
-            Err(ExitError::not_implemented("pop summary edit"))
+        Command::Publish(args) => commands::publish::run(
+            ctx,
+            commands::publish::Options {
+                local: args.local,
+                server: args.server,
+                yes: args.yes,
+            },
+        ),
+        Command::Analyze => commands::analyze::run(ctx),
+        Command::Summarize(args) => commands::summarize::run(ctx, args.week),
+        Command::Summary(SummaryCommand::Edit { event_id }) => {
@@ cli/src/gitrepo.rs
@@ -149,7 +149,7 @@ impl Repo {
             _ => FileStatus::Modified,
         };
 
-        let patch = Patch::from_diff(diff, idx).ok().flatten();
+        let mut patch = Patch::from_diff(diff, idx).ok().flatten();
         let (added, removed) = match &patch {
             Some(p) => {
                 let (_context, adds, dels) = p.line_stats().unwrap_or((0, 0, 0));
@@ -162,7 +162,7 @@ impl Repo {
         let excerpt = if binary || !filter.allows(&path) || self.is_ignored(&path) {
             None
         } else {
-            patch.as_ref().and_then(|p| excerpt_from_patch(p))
+            patch.as_mut().and_then(excerpt_from_patch)
         };
 
         Ok(Some(CommitFile {
@@ -223,7 +223,7 @@ impl ExcerptFilter {
 
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -155,6 +155,8 @@ One page, one goal: copy the install command. **(decision, user instruction 2026
 
 **Design direction (user instruction 2026-09-17):** monochromatic palette (one hue family in OKLCH, greys plus a single accent lightness step, light and dark), and a hero animation that resembles the GitLens commit graph: several lanes of commits with curved branch and merge paths drawn in as SVG on load, dots landing in sequence, a few nodes marked as decisions and AI sessions to foreshadow the product. Pure SVG + CSS/requestAnimationFrame, no animation library, under 60 KB, honours `prefers-reduced-motion` (static final frame).
 
+**Information density (user instruction):** the landing carries less, the docs carry more. Landing sections: hero, example page, three one-sentence blocks, short "for whom", privacy in three lines, footer with a single "Read the docs" link in place of the FAQ. Paragraphs are one or two sentences. Depth (FAQ, how verification works, configuration) lives in `/docs/`. Overall register: professional, restrained, no playful copy, no decorative flourishes beyond the hero graph.
+
 Design process is mandated by the PRD and applied in this order by the design agent: Impeccable → Taste Skill → Emil Kowalski motion → uncodixfy pass. Tone: short sentences, second person, no "revolutionary", no emoji. Typography and spacing at the level of Linear or Raycast, not a README.
 
 ### 6.1 Install and onboarding
18 Sep, 06:59commita1c72f8docs: hero reference image and GSAP scroll animation
@@ cli/src/commands/config.rs
@@ -18,7 +18,10 @@ pub fn run(ctx: &mut Ctx<'_>, command: ConfigCommand) -> Result<()> {
         ConfigCommand::Set { key, value } => {
             config.set(&key, &value)?;
             config.save(&path)?;
-            ctx.say(&format!("{key} = {}", config.get(&key)?.replace('\n', ", ")))?;
+            ctx.say(&format!(
+                "{key} = {}",
+                config.get(&key)?.replace('\n', ", ")
+            ))?;
         }
         ConfigCommand::List => {
             for key in KEYS {
@@ cli/src/commands/hide.rs
@@ -25,8 +25,6 @@ pub fn run(ctx: &mut Ctx<'_>, event_id: &str) -> Result<()> {
             target_id: target.id.clone(),
         }),
     )?;
-    ctx.say(&format!(
-        "hidden. The session stays in the chain; the page will say one session is hidden."
-    ))?;
+    ctx.say("hidden. The session stays in the chain; the page will say one session is hidden.")?;
     Ok(())
 }
@@ cli/src/commands/init.rs
@@ -78,8 +78,7 @@ pub fn run(ctx: &mut Ctx<'_>, opts: Options) -> Result<()> {
         None => ctx.ask_yes_no("Record AI sessions from Git AI?", true)?,
     };
 
-    std::fs::create_dir_all(&pop_dir)
-        .with_context(|| format!("creating {}", pop_dir.display()))?;
+    std::fs::create_dir_all(&pop_dir).with_context(|| format!("creating {}", pop_dir.display()))?;
 
     write_intent_md(&pop_dir, &what, &for_whom, &tools, record_ai_sessions)?;
 
@@ cli/src/commands/note.rs
@@ -25,7 +25,11 @@ pub fn run(ctx: &mut Ctx<'_>, text: &str, private: bool) -> Result<()> {
         Some(sha) => format!(" at {}", &sha[..7.min(sha.len())]),
         None => String::new(),
     };
-    let visibility = if private { " (private, never published)" } else { "" };
+    let visibility = if private {
+        " (private, never published)"
+    } else {
+        ""
+    };
     ctx.say(&format!("noted{anchor}{visibility}"))?;
     Ok(())
 }
@@ cli/src/commands/onboard.rs
@@ -9,7 +9,11 @@ use crate::Ctx;
 
 /// The tools pop cooperates with, and where to get them.
 const TOOLS: &[(&str, &str, &str)] = &[
-    ("git", "pop records your commits.", "https://git-scm.com/downloads"),
+    (
+        "git",
+        "pop records your commits.",
+        "https://git-scm.com/downloads",
+    ),
     (
         "git-ai",
         "Without it, AI sessions stay empty.",
@@ cli/src/config.rs
@@ -84,8 +84,8 @@ impl Config {
         if !path.exists() {
             return Ok(Config::default());
         }
-        let raw = std::fs::read_to_string(path)
-            .with_context(|| format!("reading {}", path.display()))?;
+        let raw =
+            std::fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
         toml::from_str(&raw).with_context(|| format!("parsing {}", path.display()))
     }
 
@@ -155,7 +155,10 @@ fn optional(value: &str) -> Option<String> {
 }
 
 fn unknown_key(key: &str) -> anyhow::Error {
-    anyhow!("unknown config key `{key}`. Known keys: {}", KEYS.join(", "))
+    anyhow!(
+        "unknown config key `{key}`. Known keys: {}",
+        KEYS.join(", ")
+    )
@@ cli/src/event.rs
@@ -396,12 +396,7 @@ fn escape_string(s: &str) -> String {
 
 /// `sha256(prev_hash + "\n" + ts + "\n" + kind + "\n" + canonical_json(payload))`,
 /// hex, lowercase.
-pub fn hash_for(
-    prev_hash: &str,
-    ts: &str,
-    kind: EventKind,
-    payload: &serde_json::Value,
-) -> String {
+pub fn hash_for(prev_hash: &str, ts: &str, kind: EventKind, payload: &serde_json::Value) -> String {
     let mut hasher = Sha256::new();
     hasher.update(prev_hash.as_bytes());
     hasher.update(b"\n");
@@ cli/src/gitrepo.rs
@@ -276,7 +276,7 @@ mod tests {
     #[test]
     fn discover_fails_outside_a_repository() {
         let dir = tempfile::TempDir::new().unwrap();
-        let err = Repo::discover(dir.path()).unwrap_err();
+        let err = Repo::discover(dir.path()).map(|_| ()).unwrap_err();
         assert!(format!("{err:#}").contains("git repository"), "{err:#}");
     }
 }
@@ cli/src/keys.rs
@@ -28,6 +28,15 @@ pub struct DeviceKey {
     signing: SigningKey,
 }
 
+/// Never print the secret, not even by accident in a panic message.
+impl std::fmt::Debug for DeviceKey {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("DeviceKey")
+            .field("public_key", &self.public_key_hex())
+            .finish_non_exhaustive()
+    }
+}
+
 impl DeviceKey {
     /// Load the device key, creating it on first use.
     pub fn load_or_create() -> Result<DeviceKey> {
@@ cli/src/lib.rs
@@ -137,12 +137,7 @@ pub fn run_capture(root: &Path, args: &[&str]) -> Result<String> {
     Ok(String::from_utf8_lossy(&out).into_owned())
 }
 
-fn run_ctx(
-    root: &Path,
-    args: &[&str],
-    input: &mut dyn BufRead,
-    out: &mut dyn Write,
-) -> Result<()> {
+fn run_ctx(root: &Path, args: &[&str], input: &mut dyn BufRead, out: &mut dyn Write) -> Result<()> {
     let parsed = cli::parse(args)?;
     let mut ctx = Ctx {
         cwd: root.to_path_buf(),
@@ cli/src/main.rs
@@ -22,7 +22,11 @@ fn main() {
             eprint!(
                 "{}{}",
                 exit.message,
-                if exit.message.ends_with('\n') { "" } else { "\n" }
+                if exit.message.ends_with('\n') {
+                    ""
+                } else {
+                    "\n"
+                }
             );
             std::process::exit(exit.code);
         }
@@ cli/src/store.rs
@@ -45,8 +45,7 @@ impl Store {
             std::fs::create_dir_all(parent)
                 .with_context(|| format!("creating {}", parent.display()))?;
         }
-        let conn = Connection::open(path)
-            .with_context(|| format!("opening {}", path.display()))?;
+        let conn = Connection::open(path).with_context(|| format!("opening {}", path.display()))?;
         conn.pragma_update(None, "journal_mode", "WAL").ok();
         conn.pragma_update(None, "foreign_keys", "ON").ok();
         conn.execute_batch(SCHEMA)
@@ -135,8 +134,7 @@ impl Store {
     }
 
     pub fn last_of_kind(&self, kind: EventKind) -> Result<Option<Event>> {
-        let sql =
-            format!("SELECT {COLUMNS} FROM events WHERE kind = ?1 ORDER BY seq DESC LIMIT 1");
+        let sql = format!("SELECT {COLUMNS} FROM events WHERE kind = ?1 ORDER BY seq DESC LIMIT 1");
         let found = self
             .conn
             .query_row(&sql, [kind.as_str()], row_to_event)
@@ cli/tests/commands.rs
@@ -27,7 +27,11 @@ fn config_get_and_set_round_trip() {
         &["config", "set", "privacy.redact", "^secret/, \\.env$"],
     )
     .unwrap();
-    pop::run_in(repo.root(), &["config", "set", "project.delivered_url", "https://a.test"]).unwrap();
+    pop::run_in(
+        repo.root(),
+        &["config", "set", "project.delivered_url", "https://a.test"],
+    )
+    .unwrap();
 
     assert_eq!(
         pop::run_capture(repo.root(), &["config", "get", "llm.model"])
@@ -131,7 +135,10 @@ fn deferred_commands_exit_with_code_two() {
 
     for (args, needle) in [
         (vec!["model", "pull"], "not available in this version"),
-        (vec!["ask", "what", "happened"], "not available in this version"),
+        (
+            vec!["ask", "what", "happened"],
@@ cli/tests/common/mod.rs
@@ -91,7 +91,7 @@ pub fn init_repo() -> TestRepo {
 /// A repo that already has `pop init` run against it, non-interactively.
 pub fn init_pop(repo: &TestRepo) {
     isolate_device_key();
-    pop::run_in(
+    pop::run_capture(
         repo.root(),
         &[
             "init",
@@ cli/tests/export.rs
@@ -20,7 +20,7 @@ fn export_writes_valid_json_with_intent_config_and_events() {
     assert_eq!(v["intent"]["what"], "a small cli that records process");
     assert_eq!(v["intent"]["for_whom"], "a studio hiring juniors");
     assert_eq!(v["config"]["llm"]["provider"], "auto");
-    assert_eq!(v["config"]["project"]["name"].as_str().is_some(), true);
+    assert!(v["config"]["project"]["name"].as_str().is_some());
 
     let events = v["events"].as_array().expect("events array");
     assert_eq!(events.len(), 3);
@@ cli/tests/hash_vector.rs
@@ -54,7 +54,9 @@ fn hash_for_matches_the_published_test_vector() {
         "acca5e4cbe2f6322f3db2ba4418cbcda1c923a02b8904acf9c6545afce20806e"
     );
     assert_eq!(hash.len(), 64);
-    assert!(hash.chars().all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()));
+    assert!(hash
+        .chars()
+        .all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()));
 }
 
 #[test]
@@ -78,10 +80,30 @@ fn every_event_kind_serialises_to_its_schema_name() {
 #[test]
 fn a_change_anywhere_in_the_input_changes_the_hash() {
     let payload = serde_json::json!({"private": false, "text": "a"});
-    let base = hash_for(GENESIS_PREV_HASH, "2026-09-17T09:00:00Z", EventKind::Decision, &payload);
-    let other_ts = hash_for(GENESIS_PREV_HASH, "2026-09-17T09:00:01Z", EventKind::Decision, &payload);
-    let other_kind = hash_for(GENESIS_PREV_HASH, "2026-09-17T09:00:00Z", EventKind::Summary, &payload);
-    let other_prev = hash_for(&"1".repeat(64), "2026-09-17T09:00:00Z", EventKind::Decision, &payload);
+    let base = hash_for(
@@ cli/tests/hook.rs
@@ -101,9 +101,17 @@ fn hook_omits_excerpts_for_ignored_and_redacted_paths() {
     pop::run_in(repo.root(), &["hook", "post-commit"]).unwrap();
 
     let c = commit_payloads(&repo).pop().unwrap();
-    let by = |p: &str| c.files.iter().find(|f| f.path == p).unwrap_or_else(|| panic!("{p} missing from {:?}", c.files));
+    let by = |p: &str| {
+        c.files
+            .iter()
+            .find(|f| f.path == p)
+            .unwrap_or_else(|| panic!("{p} missing from {:?}", c.files))
+    };
 
-    assert!(by("src/ok.rs").diff_excerpt.is_some(), "normal file keeps its excerpt");
+    assert!(
+        by("src/ok.rs").diff_excerpt.is_some(),
+        "normal file keeps its excerpt"
+    );
     assert!(
         by("src/secret.rs").diff_excerpt.is_none(),
         "redacted path has no excerpt"
@@ cli/tests/init.rs
@@ -9,7 +9,10 @@ fn init_creates_files_and_intent_event() {
     init_pop(&repo);
 
     assert!(repo.path(".pop/intent.md").is_file(), "intent.md written");
-    assert!(repo.path(".pop/config.toml").is_file(), "config.toml written");
+    assert!(
+        repo.path(".pop/config.toml").is_file(),
+        "config.toml written"
+    );
     assert!(repo.path(".pop/log.db").is_file(), "log.db created");
     assert!(
         repo.path(".git/hooks/post-commit").is_file(),
@@ cli/tests/onboard.rs
@@ -0,0 +1,101 @@
+mod common;
+
+use std::path::Path;
+
+use common::*;
+use pop::commands::init::hook_command_for;
+
+#[test]
+fn onboard_explains_itself_and_reports_on_the_tools() {
+    let repo = init_repo();
+    isolate_device_key();
+
+    let out = pop::run_capture(repo.root(), &["onboard", "--yes"]).unwrap();
+
+    assert!(out.contains("pop records three things"), "{out}");
+    assert!(out.contains("until you run `pop publish`"), "{out}");
+    assert!(out.contains("pop export"), "{out}");
+    for tool in ["git", "git-ai", "ollama"] {
+        let line = out
@@ cli/tests/verify.rs
@@ -36,7 +36,8 @@ fn verify_fails_after_a_payload_is_tampered_with_in_sqlite() {
     // Someone edits the database behind pop's back. The table is append-only,
     // so they have to remove the guard first.
     let conn = rusqlite::Connection::open(repo.path(".pop/log.db")).unwrap();
-    conn.execute_batch("DROP TRIGGER events_no_update;").unwrap();
+    conn.execute_batch("DROP TRIGGER events_no_update;")
+        .unwrap();
     conn.execute(
         "UPDATE events SET payload = ?1 WHERE seq = 3",
         rusqlite::params![r#"{"private":false,"text":"a flattering lie"}"#],
@@ -59,7 +60,9 @@ fn the_events_table_rejects_updates_and_deletes() {
     assert!(conn
         .execute("UPDATE events SET kind = 'commit' WHERE seq = 1", [])
         .is_err());
-    assert!(conn.execute("DELETE FROM events WHERE seq = 1", []).is_err());
+    assert!(conn
+        .execute("DELETE FROM events WHERE seq = 1", [])
+        .is_err());
 }
 
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -153,7 +153,7 @@ Tests: handler tests with `httptest`; DB tests run against `DATABASE_URL` if set
 
 One page, one goal: copy the install command. **(decision, user instruction 2026-09-17)** Built with Next.js (App Router) in TypeScript strict mode with Tailwind, exported as a static site (`output: 'export'`) and hosted on Cloudflare Pages; no server runtime, no client data fetching. ESLint + Prettier. The PRD's "no framework" line is superseded by the user's instruction; the static-export constraint keeps the PRD's hosting and performance intent. EN at `/`, HR at `/hr/`, DE at `/de/` — one page component, three dictionaries, `generateStaticParams`. The project page (§4.4) is NOT part of the Next app: the CLI renders it locally from a Tera template, and the landing embeds a real rendered copy from `public/example/index.html`. Sections in scroll order per PRD §Landing page: hero with install command (macOS / Windows tab, copy button, "see an example page"), the embedded real example page, three blocks with terminal screenshots, "for whom" (student / company), privacy in three lines on the front page, FAQ with the five listed questions, footer (GitHub, contact, HR / DE / EN). No email form, no waitlist. Plausible script tag (EU, cookieless) with a `copy-install` custom event.
 
-**Design direction (user instruction 2026-09-17):** monochromatic palette (one hue family in OKLCH, greys plus a single accent lightness step, light and dark), and a hero animation that resembles the GitLens commit graph: several lanes of commits with curved branch and merge paths drawn in as SVG on load, dots landing in sequence, a few nodes marked as decisions and AI sessions to foreshadow the product. Pure SVG + CSS/requestAnimationFrame, no animation library, under 60 KB, honours `prefers-reduced-motion` (static final frame).
+**Design direction (user instruction 2026-09-17):** monochromatic palette (one hue family in OKLCH, greys plus a single accent lightness step, light and dark), and a hero animation that resembles the GitLens commit graph: several lanes of commits with curved branch and merge paths drawn in as SVG on load, dots landing in sequence, a few nodes marked as decisions and AI sessions to foreshadow the product. Reference image: `design/inspo/hero-neurones.png` (a dense organic branching tree, thin filaments from one root, dots at the tips): the hero renders the project history as such a tree, procedurally generated with a fixed seed, inline SVG. Animation with GSAP + ScrollTrigger (user instruction): draw-in on load, growth and slight camera drift bound to scroll; headline and install command never move. Honours `prefers-reduced-motion` (static final frame, no scroll binding). GSAP is the only animation library; everything outside the hero is CSS.
 
 **Information density (user instruction):** the landing carries less, the docs carry more. Landing sections: hero, example page, three one-sentence blocks, short "for whom", privacy in three lines, footer with a single "Read the docs" link in place of the FAQ. Paragraphs are one or two sentences. Depth (FAQ, how verification works, configuration) lives in `/docs/`. Overall register: professional, restrained, no playful copy, no decorative flourishes beyond the hero graph.
 
@@ landing/.gitignore
@@ -0,0 +1,41 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
@@ landing/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
@@ landing/eslint.config.mjs
@@ -0,0 +1,18 @@
+import { defineConfig, globalIgnores } from "eslint/config";
+import nextVitals from "eslint-config-next/core-web-vitals";
+import nextTs from "eslint-config-next/typescript";
+
+const eslintConfig = defineConfig([
+  ...nextVitals,
+  ...nextTs,
+  // Override default ignores of eslint-config-next.
+  globalIgnores([
+    // Default ignores of eslint-config-next:
+    ".next/**",
+    "out/**",
+    "build/**",
+    "next-env.d.ts",
+  ]),
+]);
+
+export default eslintConfig;
@@ landing/example/index.html
@@ -1,835 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<!-- ctx: project.name -->
-<title>pop · how it came together</title>
-<meta name="description" content="A Rust CLI that records how a project came together and turns it into one page a hiring manager reads in two minutes.">
-<meta name="robots" content="noindex">
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
-<link href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@500;600;700&family=Instrument+Sans:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
-<style>
-/* pop design tokens
-   Consumed by: design/project-page.html (inlined), landing/src/site.css (Tailwind v4 @theme inline).
-   Fonts (Google Fonts): Schibsted Grotesk (display), Instrument Sans (text), Geist Mono (terminal, hashes).
-   Colors are OKLCH. One accent hue (ochre-rust, hue 55). Neutrals are tinted toward the same hue. */
-
-:root {
@@ landing/next.config.ts
@@ -0,0 +1,7 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+  /* config options here */
+};
+
+export default nextConfig;
@@ landing/package-lock.json
@@ -1,1131 +0,0 @@
-{
-  "name": "landing",
-  "version": "1.0.0",
-  "lockfileVersion": 3,
-  "requires": true,
-  "packages": {
-    "": {
-      "name": "landing",
-      "version": "1.0.0",
-      "license": "ISC",
-      "devDependencies": {
-        "@tailwindcss/cli": "^4.3.3",
-        "tailwindcss": "^4.3.3"
-      }
-    },
-    "node_modules/@jridgewell/gen-mapping": {
-      "version": "0.3.13",
-      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
-      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
@@ landing/package.json
@@ -1,20 +1,26 @@
 {
   "name": "landing",
-  "version": "1.0.0",
-  "description": "",
-  "main": "index.js",
-  "directories": {
-    "example": "example"
-  },
+  "version": "0.1.0",
+  "private": true,
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "dev": "next dev",
+    "build": "next build",
+    "start": "next start",
+    "lint": "eslint"
+  },
+  "dependencies": {
+    "next": "16.3.5",
@@ landing/postcss.config.mjs
@@ -0,0 +1,7 @@
+const config = {
+  plugins: {
+    "@tailwindcss/postcss": {},
+  },
+};
+
+export default config;
@@ landing/public/file.svg
@@ -0,0 +1 @@
+<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
\ No newline at end of file
@@ landing/public/globe.svg
@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
\ No newline at end of file
@@ landing/public/next.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
\ No newline at end of file
@@ landing/public/vercel.svg
@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
\ No newline at end of file
@@ landing/public/window.svg
@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
\ No newline at end of file
@@ landing/src/app/globals.css
@@ -0,0 +1,26 @@
+@import "tailwindcss";
+
+:root {
+  --background: #ffffff;
+  --foreground: #171717;
+}
+
+@theme inline {
+  --color-background: var(--background);
+  --color-foreground: var(--foreground);
+  --font-sans: var(--font-geist-sans);
+  --font-mono: var(--font-geist-mono);
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --background: #0a0a0a;
+    --foreground: #ededed;
+  }
@@ landing/src/app/layout.tsx
@@ -0,0 +1,29 @@
+import type { Metadata } from "next";
+import { Geist, Geist_Mono } from "next/font/google";
+import "./globals.css";
+
+const geistSans = Geist({
+  variable: "--font-geist-sans",
+  subsets: ["latin"],
+});
+
+const geistMono = Geist_Mono({
+  variable: "--font-geist-mono",
+  subsets: ["latin"],
+});
+
+export const metadata: Metadata = {
+  title: "Create Next App",
+  description: "Generated by create next app",
+};
+
@@ landing/src/app/page.tsx
@@ -0,0 +1,69 @@
+import Image from "next/image";
+
+export default function Home() {
+  return (
+    <div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
+      <main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
+        <Image
+          className="dark:invert h-5 w-[100px]"
+          src="/next.svg"
+          alt="Next.js logo"
+          width={100}
+          height={20}
+          priority
+        />
+        <div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
+          <h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
+            To get started, edit the{" "}
+            <code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
+              page.tsx
@@ landing/tsconfig.json
@@ -0,0 +1,34 @@
+{
+  "compilerOptions": {
+    "target": "ES2017",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "bundler",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "react-jsx",
+    "incremental": true,
+    "plugins": [
+      {
+        "name": "next"
+      }
@@ server/Makefile
@@ -11,11 +11,12 @@ DB_DSN  ?= postgres://pop:pop@localhost:5432/pop?sslmode=disable
 
 .PHONY: run test test-db sqlc lint fmt build tidy db-up db-down up down clean help
 
-## run: start the server locally
+## run: start the server locally, reading .env when it exists
 run:
-	DATABASE_URL="$(DATABASE_URL)" \
-	PUBLIC_BASE_URL="$${PUBLIC_BASE_URL:-http://localhost:8080}" \
-	LOG_FORMAT="$${LOG_FORMAT:-text}" \
+	@set -a; [ -f .env ] && . ./.env; set +a; \
+	export DATABASE_URL="$${DATABASE_URL:-$(DB_DSN)}"; \
+	export PUBLIC_BASE_URL="$${PUBLIC_BASE_URL:-http://localhost:8080}"; \
+	export LOG_FORMAT="$${LOG_FORMAT:-text}"; \
 	$(GO) run ./cmd/popserver
 
 ## test: unit and handler tests; database tests skip without DATABASE_URL
@@ server/README.md
@@ -117,6 +117,17 @@ Errors are JSON: `{"error": "<code>", "message": "<human readable>"}`.
   whole `roots` history survive. `/p/{slug}` and `/v1/verify/{slug}` then answer
   `404`. Deleting twice is fine. Re-publishing the slug brings the page back.
 
+- **Unknown fields are rejected.** A JSON body with a field the endpoint does
+  not know is a `400`, so a typo like `publishedTs` fails loudly instead of
+  being silently ignored and then failing signature verification.
+
+### Serving user html
+
+`/p/{slug}` returns exactly the bytes the client published, with
+`X-Content-Type-Options: nosniff`. The server sets no cookies and has no session
+of any kind, so a published page has nothing on this origin to steal. Pages are
+deliberately framable: the landing page embeds a real one in an `<iframe>`.
+
 ### Status codes for `POST /v1/pages`
 
 | code | when |
18 Sep, 06:59commit03b8d2bfeat(server): Go API for devices, pages, verify, delete and billing webhook
@@ server/README.md
@@ -147,7 +147,7 @@ Environment only; see `.env.example`.
 |---|---|---|---|
 | `DATABASE_URL` | yes | — | pgx DSN |
 | `PUBLIC_BASE_URL` | yes | — | origin used to build returned URLs; trailing slash trimmed |
-| `LISTEN_ADDR` | no | `:8080` | bind address |
+| `LISTEN_ADDR` | no | `:$PORT` or `:8080` | bind address; falls back to the `PORT` env var (Railway) |
 | `LEMONSQUEEZY_SECRET` | no | empty | empty makes the webhook answer 503 |
 | `LOG_LEVEL` | no | `info` | debug, info, warn, error |
 | `LOG_FORMAT` | no | `json` | json or text |
@@ server/internal/config/config.go
@@ -50,7 +50,12 @@ func load(get func(string) string) (Config, error) {
 		return Config{}, fmt.Errorf("config: PUBLIC_BASE_URL is required")
 	}
 	if cfg.ListenAddr == "" {
-		cfg.ListenAddr = ":8080"
+		// Railway and similar hosts inject PORT instead of a full address.
+		if port := strings.TrimSpace(get("PORT")); port != "" {
+			cfg.ListenAddr = ":" + port
+		} else {
+			cfg.ListenAddr = ":8080"
+		}
 	}
 	if cfg.LogFormat == "" {
 		cfg.LogFormat = "json"
@@ server/internal/config/config_test.go
@@ -72,3 +72,19 @@ func TestLoadRejectsBadLogSettings(t *testing.T) {
 		}
 	}
 }
+
+func TestLoadUsesPortWhenListenAddrUnset(t *testing.T) {
+	env := map[string]string{"DATABASE_URL": "postgres://x", "PUBLIC_BASE_URL": "https://p.example", "PORT": "9090"}
+	cfg, err := load(func(k string) string { return env[k] })
+	if err != nil {
+		t.Fatal(err)
+	}
+	if cfg.ListenAddr != ":9090" {
+		t.Fatalf("ListenAddr = %q, want :9090", cfg.ListenAddr)
+	}
+	env["LISTEN_ADDR"] = ":7070"
+	cfg, _ = load(func(k string) string { return env[k] })
+	if cfg.ListenAddr != ":7070" {
+		t.Fatalf("LISTEN_ADDR must win over PORT, got %q", cfg.ListenAddr)
+	}
+}
18 Sep, 06:59decisionServer in Go, not Rust: three endpoints, one binary, faster iteration than sharing the CLI crate.
18 Sep, 06:59commit2895815docs: hero decisions confirmed by the user
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -153,7 +153,7 @@ Tests: handler tests with `httptest`; DB tests run against `DATABASE_URL` if set
 
 One page, one goal: copy the install command. **(decision, user instruction 2026-09-17)** Built with Next.js (App Router) in TypeScript strict mode with Tailwind, exported as a static site (`output: 'export'`) and hosted on Cloudflare Pages; no server runtime, no client data fetching. ESLint + Prettier. The PRD's "no framework" line is superseded by the user's instruction; the static-export constraint keeps the PRD's hosting and performance intent. EN at `/`, HR at `/hr/`, DE at `/de/` — one page component, three dictionaries, `generateStaticParams`. The project page (§4.4) is NOT part of the Next app: the CLI renders it locally from a Tera template, and the landing embeds a real rendered copy from `public/example/index.html`. Sections in scroll order per PRD §Landing page: hero with install command (macOS / Windows tab, copy button, "see an example page"), the embedded real example page, three blocks with terminal screenshots, "for whom" (student / company), privacy in three lines on the front page, FAQ with the five listed questions, footer (GitHub, contact, HR / DE / EN). No email form, no waitlist. Plausible script tag (EU, cookieless) with a `copy-install` custom event.
 
-**Design direction (user instruction 2026-09-17):** monochromatic palette (one hue family in OKLCH, greys plus a single accent lightness step, light and dark), and a hero animation that resembles the GitLens commit graph: several lanes of commits with curved branch and merge paths drawn in as SVG on load, dots landing in sequence, a few nodes marked as decisions and AI sessions to foreshadow the product. Reference image: `design/inspo/hero-neurones.png` (a dense organic branching tree, thin filaments from one root, dots at the tips): the hero renders the project history as such a tree, procedurally generated with a fixed seed, inline SVG. Animation with GSAP + ScrollTrigger (user instruction): draw-in on load, growth and slight camera drift bound to scroll; headline and install command never move. Honours `prefers-reduced-motion` (static final frame, no scroll binding). GSAP is the only animation library; everything outside the hero is CSS.
+**Design direction (user instruction 2026-09-17):** monochromatic palette (one hue family in OKLCH, greys plus a single accent lightness step, light and dark), and a hero animation that resembles the GitLens commit graph: several lanes of commits with curved branch and merge paths drawn in as SVG on load, dots landing in sequence, a few nodes marked as decisions and AI sessions to foreshadow the product. Reference image: `design/inspo/hero-neurones.png` (a dense organic branching tree, thin filaments from one root, dots at the tips): the hero renders the project history as such a tree, procedurally generated with a fixed seed, inline SVG. Animation with GSAP + ScrollTrigger (user instruction): draw-in on load, growth and slight camera drift bound to scroll; headline and install command never move. Honours `prefers-reduced-motion` (static final frame, no scroll binding). GSAP is the only animation library; everything outside the hero is CSS. Hero decisions confirmed by the user on 2026-09-17: full-bleed background behind headline and install command with a radial fade for legibility; follows light/dark theme (not dark-only); grow on load for about 2 s, then scroll continues growth with slight camera drift; dense and abstract (thousands of filaments, commits as tiny dots, a handful of marked decision and AI-session nodes). Further hero-level questions go to the user, not to an agent's judgement.
 
 **Information density (user instruction):** the landing carries less, the docs carry more. Landing sections: hero, example page, three one-sentence blocks, short "for whom", privacy in three lines, footer with a single "Read the docs" link in place of the FAQ. Paragraphs are one or two sentences. Depth (FAQ, how verification works, configuration) lives in `/docs/`. Overall register: professional, restrained, no playful copy, no decorative flourishes beyond the hero graph.
 
18 Sep, 06:59commit0191614docs: hero mouse interaction decisions
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -153,7 +153,7 @@ Tests: handler tests with `httptest`; DB tests run against `DATABASE_URL` if set
 
 One page, one goal: copy the install command. **(decision, user instruction 2026-09-17)** Built with Next.js (App Router) in TypeScript strict mode with Tailwind, exported as a static site (`output: 'export'`) and hosted on Cloudflare Pages; no server runtime, no client data fetching. ESLint + Prettier. The PRD's "no framework" line is superseded by the user's instruction; the static-export constraint keeps the PRD's hosting and performance intent. EN at `/`, HR at `/hr/`, DE at `/de/` — one page component, three dictionaries, `generateStaticParams`. The project page (§4.4) is NOT part of the Next app: the CLI renders it locally from a Tera template, and the landing embeds a real rendered copy from `public/example/index.html`. Sections in scroll order per PRD §Landing page: hero with install command (macOS / Windows tab, copy button, "see an example page"), the embedded real example page, three blocks with terminal screenshots, "for whom" (student / company), privacy in three lines on the front page, FAQ with the five listed questions, footer (GitHub, contact, HR / DE / EN). No email form, no waitlist. Plausible script tag (EU, cookieless) with a `copy-install` custom event.
 
-**Design direction (user instruction 2026-09-17):** monochromatic palette (one hue family in OKLCH, greys plus a single accent lightness step, light and dark), and a hero animation that resembles the GitLens commit graph: several lanes of commits with curved branch and merge paths drawn in as SVG on load, dots landing in sequence, a few nodes marked as decisions and AI sessions to foreshadow the product. Reference image: `design/inspo/hero-neurones.png` (a dense organic branching tree, thin filaments from one root, dots at the tips): the hero renders the project history as such a tree, procedurally generated with a fixed seed, inline SVG. Animation with GSAP + ScrollTrigger (user instruction): draw-in on load, growth and slight camera drift bound to scroll; headline and install command never move. Honours `prefers-reduced-motion` (static final frame, no scroll binding). GSAP is the only animation library; everything outside the hero is CSS. Hero decisions confirmed by the user on 2026-09-17: full-bleed background behind headline and install command with a radial fade for legibility; follows light/dark theme (not dark-only); grow on load for about 2 s, then scroll continues growth with slight camera drift; dense and abstract (thousands of filaments, commits as tiny dots, a handful of marked decision and AI-session nodes). Further hero-level questions go to the user, not to an agent's judgement.
+**Design direction (user instruction 2026-09-17):** monochromatic palette (one hue family in OKLCH, greys plus a single accent lightness step, light and dark), and a hero animation that resembles the GitLens commit graph: several lanes of commits with curved branch and merge paths drawn in as SVG on load, dots landing in sequence, a few nodes marked as decisions and AI sessions to foreshadow the product. Reference image: `design/inspo/hero-neurones.png` (a dense organic branching tree, thin filaments from one root, dots at the tips): the hero renders the project history as such a tree, procedurally generated with a fixed seed, inline SVG. Animation with GSAP + ScrollTrigger (user instruction): draw-in on load, growth and slight camera drift bound to scroll; headline and install command never move. Honours `prefers-reduced-motion` (static final frame, no scroll binding). GSAP is the only animation library; everything outside the hero is CSS. Hero decisions confirmed by the user on 2026-09-17: full-bleed background behind headline and install command with a radial fade for legibility; follows light/dark theme (not dark-only); grow on load for about 2 s, then scroll continues growth with slight camera drift; dense and abstract (thousands of filaments, commits as tiny dots, a handful of marked decision and AI-session nodes). Mouse (user decision): parallax camera drift from pointer position, and hovering sprouts a few new filaments from the nearest node toward the pointer that draw in and fade; both off on touch and under reduced motion. Further hero-level questions go to the user, not to an agent's judgement.
 
 **Information density (user instruction):** the landing carries less, the docs carry more. Landing sections: hero, example page, three one-sentence blocks, short "for whom", privacy in three lines, footer with a single "Read the docs" link in place of the FAQ. Paragraphs are one or two sentences. Depth (FAQ, how verification works, configuration) lives in `/docs/`. Overall register: professional, restrained, no playful copy, no decorative flourishes beyond the hero graph.
 
18 Sep, 06:59commit6280c1adocs: pnpm for the landing
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -151,7 +151,7 @@ Tests: handler tests with `httptest`; DB tests run against `DATABASE_URL` if set
 
 ## 6. Landing (`landing/`)
 
-One page, one goal: copy the install command. **(decision, user instruction 2026-09-17)** Built with Next.js (App Router) in TypeScript strict mode with Tailwind, exported as a static site (`output: 'export'`) and hosted on Cloudflare Pages; no server runtime, no client data fetching. ESLint + Prettier. The PRD's "no framework" line is superseded by the user's instruction; the static-export constraint keeps the PRD's hosting and performance intent. EN at `/`, HR at `/hr/`, DE at `/de/` — one page component, three dictionaries, `generateStaticParams`. The project page (§4.4) is NOT part of the Next app: the CLI renders it locally from a Tera template, and the landing embeds a real rendered copy from `public/example/index.html`. Sections in scroll order per PRD §Landing page: hero with install command (macOS / Windows tab, copy button, "see an example page"), the embedded real example page, three blocks with terminal screenshots, "for whom" (student / company), privacy in three lines on the front page, FAQ with the five listed questions, footer (GitHub, contact, HR / DE / EN). No email form, no waitlist. Plausible script tag (EU, cookieless) with a `copy-install` custom event.
+One page, one goal: copy the install command. **(decision, user instruction 2026-09-17)** Built with Next.js (App Router) in TypeScript strict mode with Tailwind, exported as a static site (`output: 'export'`) and hosted on Cloudflare Pages; no server runtime, no client data fetching. ESLint + Prettier, pnpm as the package manager. The PRD's "no framework" line is superseded by the user's instruction; the static-export constraint keeps the PRD's hosting and performance intent. EN at `/`, HR at `/hr/`, DE at `/de/` — one page component, three dictionaries, `generateStaticParams`. The project page (§4.4) is NOT part of the Next app: the CLI renders it locally from a Tera template, and the landing embeds a real rendered copy from `public/example/index.html`. Sections in scroll order per PRD §Landing page: hero with install command (macOS / Windows tab, copy button, "see an example page"), the embedded real example page, three blocks with terminal screenshots, "for whom" (student / company), privacy in three lines on the front page, FAQ with the five listed questions, footer (GitHub, contact, HR / DE / EN). No email form, no waitlist. Plausible script tag (EU, cookieless) with a `copy-install` custom event.
 
 **Design direction (user instruction 2026-09-17):** monochromatic palette (one hue family in OKLCH, greys plus a single accent lightness step, light and dark), and a hero animation that resembles the GitLens commit graph: several lanes of commits with curved branch and merge paths drawn in as SVG on load, dots landing in sequence, a few nodes marked as decisions and AI sessions to foreshadow the product. Reference image: `design/inspo/hero-neurones.png` (a dense organic branching tree, thin filaments from one root, dots at the tips): the hero renders the project history as such a tree, procedurally generated with a fixed seed, inline SVG. Animation with GSAP + ScrollTrigger (user instruction): draw-in on load, growth and slight camera drift bound to scroll; headline and install command never move. Honours `prefers-reduced-motion` (static final frame, no scroll binding). GSAP is the only animation library; everything outside the hero is CSS. Hero decisions confirmed by the user on 2026-09-17: full-bleed background behind headline and install command with a radial fade for legibility; follows light/dark theme (not dark-only); grow on load for about 2 s, then scroll continues growth with slight camera drift; dense and abstract (thousands of filaments, commits as tiny dots, a handful of marked decision and AI-session nodes). Mouse (user decision): parallax camera drift from pointer position, and hovering sprouts a few new filaments from the nearest node toward the pointer that draw in and fade; both off on touch and under reduced motion. Further hero-level questions go to the user, not to an agent's judgement.
 
18 Sep, 06:59commit8aafa8dfeat(cli): foundation — events, hash chain, SQLite store, git hook, init/onboard/note/log/verify/export/hide/config
@@ cli/tests/schema.rs
@@ -0,0 +1,249 @@
+//! `schema/event.schema.json` is the single source of truth for the event
+//! model, shared with the Go server. The Rust structs are hand-written, so
+//! these tests hold them against it.
+
+use std::collections::BTreeSet;
+
+use pop::event::*;
+
+fn schema() -> serde_json::Value {
+    let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+        .join("..")
+        .join("schema")
+        .join("event.schema.json");
+    let raw = std::fs::read_to_string(&path)
+        .unwrap_or_else(|e| panic!("reading {}: {e}", path.display()));
+    serde_json::from_str(&raw).expect("the schema is valid JSON")
+}
+
+fn names(value: &serde_json::Value) -> BTreeSet<String> {
18 Sep, 06:59decisionHash chain input is prev_hash, ts, kind and canonical JSON, newline separated; pattern and summary events join the chain like everything else.
18 Sep, 06:59commit2c0ccb8chore: README, MIT license, CI and release workflows, Homebrew and winget templates
@@ .github/dependabot.yml
@@ -0,0 +1,57 @@
+version: 2
+
+updates:
+  - package-ecosystem: cargo
+    directory: /cli
+    schedule:
+      interval: weekly
+      day: monday
+    open-pull-requests-limit: 5
+    commit-message:
+      prefix: chore
+      include: scope
+    groups:
+      cargo:
+        patterns: ["*"]
+        update-types: [minor, patch]
+
+  - package-ecosystem: gomod
+    directory: /server
@@ .github/workflows/ci.yml
@@ -0,0 +1,142 @@
+name: CI
+
+on:
+  push:
+    branches: ["**"]
+  pull_request:
+
+permissions:
+  contents: read
+
+concurrency:
+  group: ci-${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+env:
+  CARGO_TERM_COLOR: always
+  RUST_BACKTRACE: 1
+
+jobs:
@@ .github/workflows/release.yml
@@ -0,0 +1,122 @@
+name: Release
+
+on:
+  push:
+    tags:
+      - "v*"
+
+permissions:
+  contents: read
+
+env:
+  CARGO_TERM_COLOR: always
+
+jobs:
+  build:
+    name: ${{ matrix.target }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
@@ CONTRIBUTING.md
@@ -0,0 +1,77 @@
+# Contributing
+
+Thanks for taking the time. This is a monorepo with three parts: the Rust CLI in `cli/`, the Go
+server in `server/`, and the Next.js landing and docs in `landing/`. The event model in
+`schema/event.schema.json` is shared, so a change there touches both the CLI and the server.
+
+## Branches
+
+Branch off `main` with one of three prefixes:
+
+- `feature/` for new behaviour
+- `fix/` for bug fixes
+- `chore/` for build, CI, dependencies and housekeeping
+
+Example: `feature/summary-editing`. Never force-push to `main`.
+
+## Commits
+
+Conventional commits, one concern per commit:
@@ LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Vanja Modrinjak
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
@@ README.md
@@ -0,0 +1,96 @@
+# pop
+
+pop runs alongside git and records how a project came together: the commits, the AI sessions,
+the one-sentence decisions you write down, and the patterns between them. Every record is
+hashed into an append-only chain in `.pop/log.db`, so the history can be verified instead of
+claimed. `pop publish` turns that log into one static page with a verification badge, which you
+send to a company in place of a portfolio.
+
+Documentation: <https://pop.dev/docs/>
+
+## Install
+
+System-wide, the default:
+
+```sh
+curl -fsSL https://pop.dev/install.sh | sh
+```
+
+```powershell
@@ cli/README.md
@@ -0,0 +1,28 @@
+# pop (CLI)
+
+The Rust crate behind the `pop` command. It records commits, AI sessions and decisions into an
+append-only hash chain in `.pop/log.db`, and renders the project page that `pop publish` sends
+to the server.
+
+Full documentation, including installation, every command and the configuration reference, is at
+<https://pop.dev/docs/>.
+
+```sh
+cargo install --path .     # build and install the binary
+pop onboard                # what pop records, what is missing, and set up this repo
+```
+
+Development:
+
+```sh
+cargo fmt --check
+cargo clippy --all-targets -- -D warnings
@@ packaging/homebrew/pop.rb
@@ -0,0 +1,70 @@
+# Homebrew formula template for pop.
+#
+# This file is a template. The published formula lives in the tap repository
+# `vanjamodrinjak/homebrew-pop` as `Formula/pop.rb`; nothing installs it from here.
+#
+# How the release workflow fills it in: after `.github/workflows/release.yml` has uploaded the
+# ZIPs and `SHA256SUMS`, a tap job downloads `SHA256SUMS` from the release and rewrites the five
+# placeholders below, one version and four checksums:
+#
+#   VERSION="${GITHUB_REF_NAME#v}"
+#   sum() { grep " pop-${VERSION}-$1.zip\$" SHA256SUMS | cut -d' ' -f1; }
+#   sed -e "s/__VERSION__/${VERSION}/g" \
+#       -e "s/__SHA256_AARCH64_APPLE_DARWIN__/$(sum aarch64-apple-darwin)/" \
+#       -e "s/__SHA256_X86_64_APPLE_DARWIN__/$(sum x86_64-apple-darwin)/" \
+#       -e "s/__SHA256_AARCH64_LINUX__/$(sum aarch64-unknown-linux-gnu)/" \
+#       -e "s/__SHA256_X86_64_LINUX__/$(sum x86_64-unknown-linux-gnu)/" \
+#       packaging/homebrew/pop.rb > Formula/pop.rb
+#
+# The result is committed to the tap with a conventional commit, for example
@@ packaging/winget/PopDev.Pop.installer.yaml
@@ -0,0 +1,24 @@
+# Template. See PopDev.Pop.yaml for the placeholders and the validation steps.
+#
+# The release ZIP holds the portable binary and LICENSE at its root, so winget installs it as a
+# nested portable package and puts `pop` on PATH through its links directory.
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json
+
+PackageIdentifier: PopDev.Pop
+PackageVersion: __VERSION__
+MinimumOSVersion: 10.0.17763.0
+InstallerType: zip
+NestedInstallerType: portable
+NestedInstallerFiles:
+  - RelativeFilePath: pop.exe
+    PortableCommandAlias: pop
+UpgradeBehavior: install
+Commands:
+  - pop
+ReleaseDate: __RELEASE_DATE__
+Installers:
@@ packaging/winget/PopDev.Pop.locale.en-US.yaml
@@ -0,0 +1,34 @@
+# Template. See PopDev.Pop.yaml for the placeholders and the validation steps.
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json
+
+PackageIdentifier: PopDev.Pop
+PackageVersion: __VERSION__
+PackageLocale: en-US
+Publisher: Vanja Modrinjak
+PublisherUrl: https://pop.dev
+PublisherSupportUrl: https://github.com/vanjamodrinjak/pop/issues
+Author: Vanja Modrinjak
+PackageName: pop
+PackageUrl: https://pop.dev
+License: MIT
+LicenseUrl: https://github.com/vanjamodrinjak/pop/blob/main/LICENSE
+Copyright: Copyright (c) 2026 Vanja Modrinjak
+ShortDescription: Record how your project came together, and prove it.
+Description: |-
+  pop runs alongside git and records the commits, the AI sessions and the one-sentence decisions
+  you write down. Every record is hashed into an append-only chain on your machine, so the
@@ packaging/winget/PopDev.Pop.yaml
@@ -0,0 +1,17 @@
+# Template. Copy the three PopDev.Pop*.yaml files in this directory into
+# microsoft/winget-pkgs under manifests/p/PopDev/Pop/<version>/ and replace the placeholders:
+#
+#   __VERSION__        the tag without the leading v, for example 1.2.3
+#   __RELEASE_DATE__   the release date, YYYY-MM-DD
+#   __SHA256_...__     the checksum from the release SHA256SUMS, upper case hex
+#
+# Validate before opening the pull request:
+#   winget validate --manifest manifests/p/PopDev/Pop/<version>
+#   winget install --manifest manifests/p/PopDev/Pop/<version>
+# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json
+
+PackageIdentifier: PopDev.Pop
+PackageVersion: __VERSION__
+DefaultLocale: en-US
+ManifestType: version
+ManifestVersion: 1.6.0
18 Sep, 06:59commitfa60ac8docs: hero redirected to Three.js wireframe megastructure, pure neutral palette
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -153,7 +153,7 @@ Tests: handler tests with `httptest`; DB tests run against `DATABASE_URL` if set
 
 One page, one goal: copy the install command. **(decision, user instruction 2026-09-17)** Built with Next.js (App Router) in TypeScript strict mode with Tailwind, exported as a static site (`output: 'export'`) and hosted on Cloudflare Pages; no server runtime, no client data fetching. ESLint + Prettier, pnpm as the package manager. The PRD's "no framework" line is superseded by the user's instruction; the static-export constraint keeps the PRD's hosting and performance intent. EN at `/`, HR at `/hr/`, DE at `/de/` — one page component, three dictionaries, `generateStaticParams`. The project page (§4.4) is NOT part of the Next app: the CLI renders it locally from a Tera template, and the landing embeds a real rendered copy from `public/example/index.html`. Sections in scroll order per PRD §Landing page: hero with install command (macOS / Windows tab, copy button, "see an example page"), the embedded real example page, three blocks with terminal screenshots, "for whom" (student / company), privacy in three lines on the front page, FAQ with the five listed questions, footer (GitHub, contact, HR / DE / EN). No email form, no waitlist. Plausible script tag (EU, cookieless) with a `copy-install` custom event.
 
-**Design direction (user instruction 2026-09-17):** monochromatic palette (one hue family in OKLCH, greys plus a single accent lightness step, light and dark), and a hero animation that resembles the GitLens commit graph: several lanes of commits with curved branch and merge paths drawn in as SVG on load, dots landing in sequence, a few nodes marked as decisions and AI sessions to foreshadow the product. Reference image: `design/inspo/hero-neurones.png` (a dense organic branching tree, thin filaments from one root, dots at the tips): the hero renders the project history as such a tree, procedurally generated with a fixed seed, inline SVG. Animation with GSAP + ScrollTrigger (user instruction): draw-in on load, growth and slight camera drift bound to scroll; headline and install command never move. Honours `prefers-reduced-motion` (static final frame, no scroll binding). GSAP is the only animation library; everything outside the hero is CSS. Hero decisions confirmed by the user on 2026-09-17: full-bleed background behind headline and install command with a radial fade for legibility; follows light/dark theme (not dark-only); grow on load for about 2 s, then scroll continues growth with slight camera drift; dense and abstract (thousands of filaments, commits as tiny dots, a handful of marked decision and AI-session nodes). Mouse (user decision): parallax camera drift from pointer position, and hovering sprouts a few new filaments from the nearest node toward the pointer that draw in and fade; both off on touch and under reduced motion. Further hero-level questions go to the user, not to an agent's judgement.
+**Design direction (user instructions 2026-09-17, latest wins):** palette is near black with white: neutral greys with zero chroma, no accent colour, no tint; status (verified, generated, hidden) is conveyed by weight, outline and type, never colour. Hero reference: `design/inspo/hero-array.png` (a dense 3D wireframe megastructure in steep upward perspective: layered translucent planes, thin white lines, trusses, HUD-like text fragments, near-black background). The earlier tree reference (`design/inspo/hero-neurones.png`) is superseded. The hero is a Three.js scene in a client component (`landing/src/components/hero/`), procedurally generated with a fixed seed (no imported models), rendered in white/grey line and translucent-plane materials on near black, with the red accents of the reference dropped. Motion (user decisions): the structure assembles on load over about 2 s; scrolling through the hero flies the camera further into it with GSAP ScrollTrigger scrub; pointer position gives parallax camera drift; hovering spawns a few new line fragments or panels near the pointer that build in and fade; touch and reduced motion get a static frame. Full-bleed behind headline and install command with a radial fade for legibility; follows light/dark theme (white structure on near black in dark mode, near-black structure on white in light mode). Budget: under 300 KB of JS for the scene including three, 60 fps on an M1 laptop, degrades to a static SVG frame without WebGL. Further hero-level questions go to the user, not to an agent's judgement.
 
 **Information density (user instruction):** the landing carries less, the docs carry more. Landing sections: hero, example page, three one-sentence blocks, short "for whom", privacy in three lines, footer with a single "Read the docs" link in place of the FAQ. Paragraphs are one or two sentences. Depth (FAQ, how verification works, configuration) lives in `/docs/`. Overall register: professional, restrained, no playful copy, no decorative flourishes beyond the hero graph.
 
@@ landing/src/components/hero/Hero.tsx
@@ -0,0 +1,10 @@
+"use client";
+
+/**
+ * Hero background scene. Owned by the dedicated hero task; the landing
+ * places it as a full-bleed layer behind the headline and install block.
+ * This placeholder renders nothing until the scene lands.
+ */
+export default function Hero() {
+  return <div aria-hidden className="absolute inset-0 -z-10" />;
+}
18 Sep, 06:59decisionHero redirected from the organic tree to a Three.js wireframe megastructure; pure neutral palette, no accent colour.
18 Sep, 06:59commit5ba6a7edocs: seamless section flow on the landing
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -155,7 +155,7 @@ One page, one goal: copy the install command. **(decision, user instruction 2026
 
 **Design direction (user instructions 2026-09-17, latest wins):** palette is near black with white: neutral greys with zero chroma, no accent colour, no tint; status (verified, generated, hidden) is conveyed by weight, outline and type, never colour. Hero reference: `design/inspo/hero-array.png` (a dense 3D wireframe megastructure in steep upward perspective: layered translucent planes, thin white lines, trusses, HUD-like text fragments, near-black background). The earlier tree reference (`design/inspo/hero-neurones.png`) is superseded. The hero is a Three.js scene in a client component (`landing/src/components/hero/`), procedurally generated with a fixed seed (no imported models), rendered in white/grey line and translucent-plane materials on near black, with the red accents of the reference dropped. Motion (user decisions): the structure assembles on load over about 2 s; scrolling through the hero flies the camera further into it with GSAP ScrollTrigger scrub; pointer position gives parallax camera drift; hovering spawns a few new line fragments or panels near the pointer that build in and fade; touch and reduced motion get a static frame. Full-bleed behind headline and install command with a radial fade for legibility; follows light/dark theme (white structure on near black in dark mode, near-black structure on white in light mode). Budget: under 300 KB of JS for the scene including three, 60 fps on an M1 laptop, degrades to a static SVG frame without WebGL. Further hero-level questions go to the user, not to an agent's judgement.
 
-**Information density (user instruction):** the landing carries less, the docs carry more. Landing sections: hero, example page, three one-sentence blocks, short "for whom", privacy in three lines, footer with a single "Read the docs" link in place of the FAQ. Paragraphs are one or two sentences. Depth (FAQ, how verification works, configuration) lives in `/docs/`. Overall register: professional, restrained, no playful copy, no decorative flourishes beyond the hero graph.
+**Information density (user instruction):** the landing carries less, the docs carry more. Landing sections: hero, example page, three one-sentence blocks, short "for whom", privacy in three lines, footer with a single "Read the docs" link in place of the FAQ. Paragraphs are one or two sentences. Depth (FAQ, how verification works, configuration) lives in `/docs/`. Overall register: professional, restrained, no playful copy, no decorative flourishes beyond the hero scene. Sections are seamlessly connected (user instruction): one continuous canvas, no horizontal rules or background jumps between sections, a thin vertical spine continuing from the hero through the page, gradual fade where the hero scene ends, consistent left alignment; the page reads as one scroll, not stacked cards.
 
 Design process is mandated by the PRD and applied in this order by the design agent: Impeccable → Taste Skill → Emil Kowalski motion → uncodixfy pass. Tone: short sentences, second person, no "revolutionary", no emoji. Typography and spacing at the level of Linear or Raycast, not a README.
 
18 Sep, 06:59commit8cd73efdocs: brand identity scope and decisions
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -170,6 +170,10 @@ Both modes end by running `pop onboard`, which is the onboarding: it explains in
 
 Language: every user-facing string in the CLI, the project page and the default landing is English. HR and DE exist only as landing translations.
 
+### 6.1a Brand (user instruction 2026-09-17)
+
+A full brand identity in `design/brand/`: name `pop` in lowercase with the descriptor "Proof of Process"; a geometric mark expressing a chained record (linked segments or nodes reading as an unbroken chain, the hash chain and the badge in one glyph) plus a lowercase wordmark; near black with white only; typography system, spacing and iconography rules, voice and tone (short, second person, professional, no superlatives, no emoji), usage do's and don'ts, favicon and app icon set, OG/social image, terminal banner for `pop onboard`, README badge, and a `DESIGN.md` other agents can read. Assets as SVG plus exported PNGs. The landing, project page and docs consume the brand; nothing there defines its own logo or colours.
+
 ### 6.2 Documentation site (`landing/docs/`)
 
 Docs at `/docs/` inside the same Next.js app (MDX content under `content/docs/`, statically exported), same design system as the landing, the kind of docs every CLI tool ships: left sidebar navigation, content column, previous/next links, copyable code blocks, a search box (client-side, small inline index, no service). English only in v1. Pages:
18 Sep, 06:59commit5a080bbdocs: plan tasks for hero scene and brand identity
@@ docs/superpowers/plans/2026-09-17-pop-v1-plan.md
@@ -46,6 +46,12 @@ Spec: `docs/superpowers/specs/2026-09-17-pop-v1-design.md`. Each task is owned b
 ### K. Docs site (`landing/docs/`) — design agent, after G
 - Spec §6.2. Routes `/docs/[slug]` in the Next app with MDX under `landing/content/docs/`; layout (sidebar, content, prev/next, client-side search), eleven pages, command reference generated from real `pop --help` output via a small script (`landing/scripts/gen-commands.sh`) committed with its output. Same tokens as the landing. Verified in the browser at desktop and 360 px.
 
+### F2. Hero scene (`landing/src/components/hero/`) — design agent, parallel with F
+- Spec §6 design direction: Three.js wireframe megastructure after `design/inspo/hero-array.png`, GSAP scroll fly-through, pointer parallax, hover-spawned fragments, reduced-motion and no-WebGL fallbacks. Screenshots in `design/hero-review/`.
+
+### M. Brand identity (`design/brand/`) — design agent, parallel with F
+- Spec §6.1a: mark + wordmark (chained record), favicon/app icon/OG set, typography, voice, terminal banner, badges, `DESIGN.md`, `guidelines.html`. Landing, project page, docs and `pop onboard` adopt it in tasks I, E, K, G.
+
 ### L. Railway deployment — after H and I
 - Railway project `pop`: Postgres service, `popserver` from `server/Dockerfile` with `DATABASE_URL`, `PUBLIC_BASE_URL`, `LEMONSQUEEZY_SECRET` set as variables; landing as a static service from `landing/out/`. Generate domains, verify `/healthz` and a real `pop publish` round trip. Document in `docs/deploy.md`.
 
18 Sep, 06:59commitcab9efbfeat(cli): read Git AI notes into ai_session events, best-effort Claude Code prompt lookup
@@ cli/src/gitai/mod.rs
@@ -0,0 +1,324 @@
+//! Reader for Git AI attribution notes.
+//!
+//! Git AI is installed separately by the user and writes its attribution into
+//! `refs/notes/ai`, one note per commit. pop never parses editor or tool logs
+//! itself; this module is the only place that knows Git AI's format.
+//!
+//! What a note can and cannot tell us is worked out in
+//! `docs/superpowers/specs/gitai-format.md`. The short version: the
+//! attestation is final authorship at that commit, so a line attributed to a
+//! session is AI-written and accepted unchanged, and human rework of AI lines
+//! shows up later as a pattern, not here.
+
+pub mod note;
+pub mod prompt;
+
+use std::path::Path;
+use std::sync::{Once, OnceLock};
+
+use anyhow::{Context, Result};
@@ cli/src/gitai/note.rs
@@ -0,0 +1,562 @@
+//! The Git AI note format.
+//!
+//! A note is a text blob: attestation lines, a line that is exactly `---`,
+//! then one JSON object. The attestations say which lines of which file each
+//! key wrote; the JSON says what each key is. See
+//! `docs/superpowers/specs/gitai-format.md`.
+//!
+//! Nothing here touches git or the filesystem, so every rule is testable
+//! against the fixtures in `cli/tests/fixtures/gitai/`.
+
+use std::collections::BTreeMap;
+
+use anyhow::{anyhow, bail, Context, Result};
+use serde::Deserialize;
+
+/// The separator between the attestations and the JSON metadata.
+const SEPARATOR: &str = "---";
+
+// ---------------------------------------------------------------------------
@@ cli/src/gitai/prompt.rs
@@ -0,0 +1,231 @@
+//! Best-effort prompt lookup for Claude Code sessions.
+//!
+//! Git AI notes carry no prompt text — by design. For `tool == "claude"` the
+//! agent id is the Claude Code session UUID, and Claude Code keeps that
+//! session's transcript at
+//! `~/.claude/projects/<encoded working directory>/<session id>.jsonl`.
+//!
+//! Everything here is best effort: a missing file, an unreadable line, a
+//! renamed directory, a prompt matching `privacy.redact` — all of them mean
+//! "no prompt", never an error.
+
+use std::path::{Path, PathBuf};
+
+use serde_json::Value;
+
+use crate::gitrepo::ExcerptFilter;
+
+/// Overrides `~/.claude/projects`, so tests never read a real transcript.
+pub const PROJECTS_DIR_ENV: &str = "POP_CLAUDE_PROJECTS_DIR";
@@ cli/tests/fixtures/gitai/malformed-json.note
@@ -0,0 +1,6 @@
+src/main.rs
+  s_2c8a703bb1fd71::t_d92697dfcc1b35 1-4
+---
+{
+  "schema_version": "authorship/3.0.0",
+  "sessions": {
@@ cli/tests/fixtures/gitai/mixed-ai-human-legacy.note
@@ -0,0 +1,47 @@
+cli/src/gitai.rs
+  s_9f1c22ab445566::t_11aa22bb33cc44 1-40,55-60
+  h_ea2010baa7a42b 41-54
+docs/spec.md
+  s_9f1c22ab445566::t_77dd88ee99ff00 1-12
+  4f3a9b1 20-29
+README.md
+  4f3a9b1 1-5
+---
+{
+  "schema_version": "authorship/3.0.0",
+  "git_ai_version": "1.7.6",
+  "base_commit_sha": "0670e7efb1c5f8d3a94c2e1f6b7d80a5c3e9d412",
+  "prompts": {
+    "4f3a9b1": {
+      "agent_id": {
+        "tool": "cursor",
+        "id": "cur-7781",
+        "model": "gpt-5-codex"
@@ cli/tests/fixtures/gitai/no-sessions-map.note
@@ -0,0 +1,22 @@
+src/main.rs
+  s_ff00ff00ff00ff::t_99aa88bb77cc66 1-4
+  0a1b2c3 10-19
+---
+{
+  "schema_version": "authorship/3.0.0",
+  "base_commit_sha": "2222222222222222222222222222222222222222",
+  "prompts": {
+    "0a1b2c3": {
+      "agent_id": {
+        "tool": "windsurf",
+        "id": "ws-42",
+        "model": "swe-1"
+      },
+      "human_author": null,
+      "total_additions": 10,
+      "total_deletions": 0,
+      "accepted_lines": 10,
+      "overriden_lines": 0
@@ cli/tests/fixtures/gitai/overlapping-ranges.note
@@ -0,0 +1,34 @@
+src/lib.rs
+  s_1111111111aaaa::t_0000000000000a 1-10
+  s_2222222222bbbb::t_0000000000000b 5-8
+  h_3333333333cccc 9
+---
+{
+  "schema_version": "authorship/3.0.0",
+  "git_ai_version": "1.7.6",
+  "base_commit_sha": "4444444444444444444444444444444444444444",
+  "prompts": {},
+  "humans": {
+    "h_3333333333cccc": {
+      "author": "Test Person <test@example.com>"
+    }
+  },
+  "sessions": {
+    "s_1111111111aaaa": {
+      "agent_id": {
+        "tool": "claude",
@@ cli/tests/fixtures/gitai/quoted-path.note
@@ -0,0 +1,22 @@
+"docs/design notes.md"
+  s_aaa111bbb222cc::t_deadbeefcafe01 1-3
+plain/file.txt
+  s_aaa111bbb222cc::t_deadbeefcafe01 7
+---
+{
+  "schema_version": "authorship/3.0.0",
+  "git_ai_version": null,
+  "base_commit_sha": "1111111111111111111111111111111111111111",
+  "prompts": {},
+  "humans": {},
+  "sessions": {
+    "s_aaa111bbb222cc": {
+      "agent_id": {
+        "tool": "codex",
+        "id": "01J9ZZTESTSESSIONCODEX",
+        "model": "gpt-5"
+      },
+      "human_author": null
@@ cli/tests/fixtures/gitai/single-claude-session.note
@@ -0,0 +1,26 @@
+src/daemon.rs
+  s_2c8a703bb1fd71::t_d92697dfcc1b35 6476-6479,6484-6488,6490-6492
+  s_2c8a703bb1fd71::t_cafe0d045c5634 6480-6482,6489
+  h_ea2010baa7a42b 100,116-118,122
+---
+{
+  "schema_version": "authorship/3.0.0",
+  "git_ai_version": "development:1.6.17",
+  "base_commit_sha": "ff11ab3fe24d01149f4be8bfcc84b4ae6fb58b38",
+  "prompts": {},
+  "humans": {
+    "h_ea2010baa7a42b": {
+      "author": "Test Person <test@example.com>"
+    }
+  },
+  "sessions": {
+    "s_2c8a703bb1fd71": {
+      "agent_id": {
+        "tool": "claude",
@@ cli/tests/fixtures/gitai/unknown-key.note
@@ -0,0 +1,26 @@
+src/known.rs
+  s_1234567890abcd::t_0000000000000a 1-6
+src/foreign.rs
+  s_cherrypickedaa::t_0000000000000b 1-3
+  h_notinthismapxx 4-5
+  9998887 6
+---
+{
+  "schema_version": "authorship/3.0.0",
+  "git_ai_version": "1.7.6",
+  "base_commit_sha": "3333333333333333333333333333333333333333",
+  "prompts": {},
+  "humans": {},
+  "sessions": {
+    "s_1234567890abcd": {
+      "agent_id": {
+        "tool": "claude",
+        "id": "0f0f0f0f-1111-2222-3333-444444444444",
+        "model": "claude-fable-5"
@@ cli/tests/gitai.rs
@@ -0,0 +1,593 @@
+//! The Git AI reader: parsing real note bodies and mapping them to events.
+//!
+//! The fixtures in `tests/fixtures/gitai/` are note blobs of the shape
+//! `git ai show <rev>` prints, described in
+//! `docs/superpowers/specs/gitai-format.md`.
+
+mod common;
+
+use std::path::{Path, PathBuf};
+use std::sync::OnceLock;
+
+use common::*;
+use pop::event::{AiSessionPayload, EventKind};
+use pop::gitai::note::{self, Route};
+use pop::gitai::{self, prompt};
+use pop::gitrepo::ExcerptFilter;
+use tempfile::TempDir;
+
+// ---------------------------------------------------------------------------
18 Sep, 06:59commit1759bb7fix(cli): create the device key atomically so parallel init cannot read a partial key
@@ cli/src/keys.rs
@@ -53,9 +53,20 @@ impl DeviceKey {
         let signing = SigningKey::generate(&mut rand::rngs::OsRng);
         let mut body = hex::encode(signing.to_bytes());
         body.push('\n');
-        std::fs::write(&path, body).with_context(|| format!("writing {}", path.display()))?;
-        restrict_file(&path)?;
-        Ok(DeviceKey { signing })
+        // Write the full key to a private temp file first, then link it into
+        // place. `hard_link` fails if the target already exists, so two
+        // concurrent `pop init` runs can never observe a half-written key:
+        // the loser simply loads the winner's key.
+        let tmp = dir.join(format!(".device.key.{}.tmp", std::process::id()));
+        std::fs::write(&tmp, body).with_context(|| format!("writing {}", tmp.display()))?;
+        restrict_file(&tmp)?;
+        let linked = std::fs::hard_link(&tmp, &path);
+        let _ = std::fs::remove_file(&tmp);
+        match linked {
+            Ok(()) => Ok(DeviceKey { signing }),
+            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => DeviceKey::load(),
+            Err(e) => Err(e).with_context(|| format!("creating {}", path.display())),
@@ docs/superpowers/specs/gitai-format.md
@@ -45,7 +45,7 @@ Tool names as written by Git AI: `claude` (Claude Code), `cursor`, `codex`, `git
 
 ## What is NOT in the note (and what we do about it)
 
-- **Prompt and response text.** By design. Transcripts live in the daemon's local db, not in git. For `tool == "claude"` the `id` is the Claude Code session UUID and the transcript is at `~/.claude/projects/<encoded cwd>/<id>.jsonl` (cwd encoded by replacing `/` with `-`). Best effort: read the first `user` message of that file as `prompt` (truncated to 500 chars, redact patterns applied); on any failure `prompt = null`. Other tools: `prompt = null`. `response_excerpt` is always null in v1.
+- **Prompt and response text.** By design. Transcripts live in the daemon's local db, not in git. For `tool == "claude"` the `id` is the Claude Code session UUID and the transcript is at `~/.claude/projects/<encoded cwd>/<id>.jsonl`. Verified encoding rule (checked against 23 real project dirs): every character that is not an ASCII letter or digit becomes `-` (so `/Users/x/My Proj` → `-Users-x-My-Proj`); the reader also tries the slash-only variant and the canonicalised workdir. Entries with `isMeta: true` are Claude Code's own injections, not prompts, and are skipped. Best effort: read the first `user` message of that file as `prompt` (truncated to 500 chars, redact patterns applied); on any failure `prompt = null`. Other tools: `prompt = null`. `response_excerpt` is always null in v1.
 - **Per-line "AI wrote it, human modified it".** The attestation is final authorship at that commit: an `s_` line is AI-written and accepted unchanged; an `h_` line is human-written (needs the IDE extension); untracked lines include AI lines a human rewrote. So from one note: `ai_lines` = count of `s_` lines per file, `ai_lines_modified_by_human` = 0. Human rework of AI lines is derived later by the pattern analysis (a following commit touching lines attributed to AI in a previous commit).
 - `accepted` = true whenever the session has at least one attributed line in the commit.
 
18 Sep, 06:59commit41f082efeat(cli): pattern detection (revert, retry, fix after AI, stall), moments and pop analyze
@@ cli/src/cli.rs
@@ -43,7 +43,7 @@ pub enum Command {
     /// Render the project page and publish it.
     Publish(PublishArgs),
     /// Look for patterns in the log and record them.
-    Analyze,
+    Analyze(AnalyzeArgs),
     /// Write weekly summaries.
     Summarize(SummarizeArgs),
     /// Work with existing summaries.
@@ -153,10 +153,20 @@ pub struct PublishArgs {
 }
 
 #[derive(Debug, Args)]
+pub struct AnalyzeArgs {
+    /// Print the patterns and moments as JSON.
+    #[arg(long)]
+    pub json: bool,
+}
+
+#[derive(Debug, Args)]
@@ cli/src/commands/analyze.rs
@@ -1,11 +1,137 @@
 //! `pop analyze` — find patterns in the log and record them as events.
 //!
-//! Task C implements the pattern and moment detection behind this.
+//! Analysis is append-only like everything else: a pattern that was already
+//! recorded is not recorded again, so running `pop analyze` twice leaves the
+//! log exactly as it was. Moments are derived on the spot and printed, never
+//! stored — the page rebuilds them from the events.
 
-use anyhow::Result;
+use std::collections::BTreeSet;
 
-use crate::{Ctx, ExitError};
+use anyhow::{Context, Result};
 
-pub fn run(_ctx: &mut Ctx<'_>) -> Result<()> {
-    Err(ExitError::not_implemented("pop analyze"))
+use crate::event::{Event, EventKind, PatternPayload, Payload};
+use crate::moments::{self, Moment};
+use crate::patterns;
@@ cli/src/keys.rs
@@ -57,7 +57,11 @@ impl DeviceKey {
         // place. `hard_link` fails if the target already exists, so two
         // concurrent `pop init` runs can never observe a half-written key:
         // the loser simply loads the winner's key.
-        let tmp = dir.join(format!(".device.key.{}.tmp", std::process::id()));
+        let tmp = dir.join(format!(
+            ".device.key.{}.{}.tmp",
+            std::process::id(),
+            hex::encode(&signing.verifying_key().to_bytes()[..4])
+        ));
         std::fs::write(&tmp, body).with_context(|| format!("writing {}", tmp.display()))?;
         restrict_file(&tmp)?;
         let linked = std::fs::hard_link(&tmp, &path);
@@ cli/src/lib.rs
@@ -11,7 +11,10 @@ pub mod event;
 pub mod gitai;
 pub mod gitrepo;
 pub mod keys;
+pub mod moments;
+pub mod patterns;
 pub mod store;
+pub mod summarize;
 
 use std::io::{BufRead, BufReader, Read, Write};
 use std::path::{Path, PathBuf};
@@ cli/src/moments.rs
@@ -0,0 +1,734 @@
+//! Moments: the three to five things worth telling a stranger about a
+//! project (spec §4.2).
+//!
+//! A moment is built from patterns and decisions, never from raw diffs. The
+//! ranking is the spec's: a fix after AI first, then a decision that precedes
+//! a change of directory structure, then the longest stall and how it
+//! resolved. Titles and bodies come from template strings, so a page renders
+//! the same with or without a language model; [`MomentCandidate`] is the shape
+//! a later Ollama step gets to choose and retitle from.
+
+use std::collections::BTreeSet;
+
+use serde::{Deserialize, Serialize};
+
+use crate::event::{DecisionPayload, Event, EventKind, PatternKind, PatternPayload};
+use crate::patterns::{
+    self, capitalize, files_phrase, humanize_gap, path_phrase, quotable, Commit,
+};
+
@@ cli/src/patterns.rs
@@ -0,0 +1,1121 @@
+//! Pattern detection over the recorded log (spec §4.2).
+//!
+//! Four rules run over the ordered `commit` and `ai_session` events and
+//! produce [`PatternPayload`]s. Everything here is pure and deterministic:
+//! the same events always give the same patterns in the same order, which is
+//! what lets `pop analyze` run twice without recording anything new.
+
+use std::collections::BTreeSet;
+
+use chrono::{DateTime, Utc};
+
+use crate::event::{
+    AiSessionPayload, CommitFile, CommitPayload, Event, EventKind, PatternKind, PatternPayload,
+};
+
+// ---------------------------------------------------------------------------
+// thresholds — spec §4.2
+// ---------------------------------------------------------------------------
+
@@ cli/tests/analyze.rs
@@ -0,0 +1,236 @@
+//! `pop analyze` over a real repository: the hook records the commits, the
+//! rules read them back, and running twice records nothing new.
+
+mod common;
+
+use common::*;
+use pop::event::{AiSessionFile, AiSessionPayload, EventKind, PatternPayload, Payload};
+
+fn hook(repo: &TestRepo) {
+    pop::run_in(repo.root(), &["hook", "post-commit"]).expect("post-commit hook");
+}
+
+fn analyze(repo: &TestRepo) -> String {
+    pop::run_capture(repo.root(), &["analyze"]).expect("pop analyze")
+}
+
+fn analyze_json(repo: &TestRepo) -> String {
+    pop::run_capture(repo.root(), &["analyze", "--json"]).expect("pop analyze --json")
+}
@@ cli/tests/commands.rs
@@ -140,9 +140,6 @@ fn deferred_commands_exit_with_code_two() {
             "not available in this version",
         ),
         (vec!["publish"], "not implemented yet"),
-        (vec!["analyze"], "not implemented yet"),
-        (vec!["summarize"], "not implemented yet"),
-        (vec!["summary", "edit", "some-id"], "not implemented yet"),
     ] {
         let err = pop::run_in(repo.root(), &args).unwrap_err();
         assert_eq!(exit_code(&err), Some(2), "{args:?}: {err:#}");
18 Sep, 06:59commitd3be33dfeat(cli): weekly summaries via Ollama with heuristic fallback; help exits 0; page and preview deps
@@ cli/Cargo.lock
@@ -3,6 +3,12 @@
 version = 3
 
 [[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
 name = "ahash"
 version = "0.8.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -24,6 +30,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
@@ cli/Cargo.toml
@@ -5,7 +5,7 @@ edition = "2021"
 rust-version = "1.75"
 description = "Record how your project came together, and prove it."
 license = "MIT"
-repository = "https://github.com/pop-cli/pop"
+repository = "https://github.com/vanjamodrinjak/pop"
 
 # This crate is standalone; the repository root is not a cargo workspace.
 [workspace]
@@ -34,7 +34,11 @@ serde_json = "1"
 sha2 = "0.10"
 thiserror = "1"
 toml = "0.8"
+ureq = { version = "3", features = ["json"] }
 uuid = { version = "1", features = ["v7", "serde"] }
+tera = { version = "1", default-features = false }
+ratatui = "0.29"
+crossterm = "0.28"
 
 [dev-dependencies]
@@ cli/src/cli.rs
@@ -190,7 +190,14 @@ pub struct AskArgs {
 /// Parse arguments, without the leading program name.
 pub fn parse(args: &[&str]) -> Result<Cli> {
     let argv = std::iter::once("pop").chain(args.iter().copied());
-    Cli::try_parse_from(argv).map_err(|e| ExitError::new(2, e.render().to_string()).into())
+    Cli::try_parse_from(argv).map_err(|e| {
+        // `--help` and `--version` are not errors: print and exit 0.
+        let code = match e.kind() {
+            clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => 0,
+            _ => 2,
+        };
+        ExitError::new(code, e.render().to_string()).into()
+    })
 }
 
 pub fn dispatch(cli: Cli, ctx: &mut Ctx<'_>) -> Result<()> {
@@ cli/src/commands/summarize.rs
@@ -1,15 +1,183 @@
-//! `pop summarize` and `pop summary edit` — weekly summaries.
+//! `pop summarize` and `pop summary edit` — weekly summaries (spec §4.3).
 //!
-//! Task D adds the Ollama client and the heuristic fallback behind these.
+//! Summaries are events like everything else: they are appended, never
+//! rewritten. Regenerating a week adds a new summary event, editing one adds
+//! another, and the page renders the last one per week.
 
-use anyhow::Result;
+use std::collections::BTreeSet;
+use std::process::Command;
 
-use crate::{Ctx, ExitError};
+use anyhow::{bail, Context, Result};
 
-pub fn run(_ctx: &mut Ctx<'_>, _week: Option<u32>) -> Result<()> {
-    Err(ExitError::not_implemented("pop summarize"))
+use crate::config::Config;
+use crate::event::{EventKind, Payload, SummaryPayload};
@@ cli/src/gitai.rs
@@ -1,42 +0,0 @@
-//! Reader for Git AI attribution notes.
-//!
-//! Git AI is installed separately by the user and writes its attribution into
-//! a git notes ref. pop never parses editor or tool logs itself; this module
-//! is the only place that knows Git AI's format.
-//!
-//! **Task B implements this.** Until then it returns no sessions, so the
-//! post-commit hook records commits normally on a machine without Git AI.
-//! See `docs/superpowers/specs/gitai-format.md` and the fixtures that will
-//! live in `cli/tests/fixtures/gitai/`.
-
-use anyhow::Result;
-use git2::Repository;
-
-use crate::event::AiSessionPayload;
-
-/// AI sessions attributed to `sha`, newest first.
-///
-/// Returns an empty vector when Git AI is not installed, wrote no note for
@@ cli/src/main.rs
@@ -19,6 +19,11 @@ fn main() {
     if let Err(err) = pop::run_in(&cwd, &args) {
         if let Some(exit) = err.downcast_ref::<pop::ExitError>() {
             // clap already renders its own help and usage text.
+            // Help and version (exit 0) belong on stdout; errors on stderr.
+            if exit.code == 0 {
+                print!("{}", exit.message);
+                std::process::exit(0);
+            }
             eprint!(
                 "{}{}",
                 exit.message,
@@ cli/src/summarize/heuristic.rs
@@ -0,0 +1,218 @@
+//! The summary pop writes when no model is available.
+//!
+//! Deterministic, plain, and incapable of inventing anything: every sentence
+//! is assembled from counts and from text you wrote yourself. The same week
+//! always produces the same paragraphs, which is what makes it safe to publish.
+
+use super::weeks::{count, Week};
+
+/// Two or three short paragraphs: what happened, what was decided, what stood
+/// out.
+pub fn summarize(week: &Week) -> String {
+    let mut paragraphs = vec![what_happened(week)];
+    let decided = what_was_decided(week);
+    let stood_out = what_stood_out(week);
+    match (decided, stood_out) {
+        (None, None) => paragraphs.push(
+            "No decisions were written down that week, and the analysis found no patterns.".into(),
+        ),
+        (decided, stood_out) => paragraphs.extend(decided.into_iter().chain(stood_out)),
@@ cli/src/summarize/mod.rs
@@ -0,0 +1,349 @@
+//! Weekly summaries (spec §4.3).
+//!
+//! The week is bucketed in [`weeks`], turned into structured text there, and
+//! handed either to a local model ([`ollama`]) or to the template
+//! ([`heuristic`]). Nothing here ever sees a diff or a prompt, and nothing
+//! here ever calls a remote API: `llm.remote` is out of scope in this version.
+
+pub mod heuristic;
+pub mod ollama;
+pub mod weeks;
+
+use std::collections::BTreeMap;
+
+use anyhow::{bail, Result};
+
+use crate::config::Config;
+use crate::event::{Event, EventKind, SummaryPayload};
+
+pub use ollama::{Ollama, DEFAULT_MODEL};
@@ cli/src/summarize/ollama.rs
@@ -0,0 +1,439 @@
+//! A small client for a local Ollama, and nothing else.
+//!
+//! pop talks to `http://localhost:11434` if something is listening there. It
+//! asks for JSON, validates what comes back, and treats anything unexpected as
+//! "no model available" rather than as text worth publishing.
+
+use std::time::Duration;
+
+use anyhow::{anyhow, bail, Context, Result};
+use serde::Deserialize;
+
+/// Where Ollama listens unless you say otherwise.
+pub const DEFAULT_HOST: &str = "http://localhost:11434";
+
+/// The model pop asks for when the config does not name one.
+pub const DEFAULT_MODEL: &str = "qwen3:4b";
+
+/// Is it alive? One second is long enough to answer on localhost.
+const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
@@ cli/src/summarize/weeks.rs
@@ -0,0 +1,597 @@
+//! Weekly buckets: Monday to Sunday, in local time, counted from the first
+//! commit.
+//!
+//! A bucket carries the *structured* facts of a week — commit messages with
+//! file counts, the decisions you wrote down, the patterns analysis found.
+//! Never a diff excerpt, never a prompt: whatever a model sees, it sees from
+//! here, and this is the only place that decides what that is.
+
+use std::collections::BTreeMap;
+use std::fmt::Write as _;
+
+use chrono::{DateTime, Datelike, Duration, Local, NaiveDate};
+
+use crate::event::{CommitPayload, DecisionPayload, Event, EventKind, PatternPayload};
+
+/// At most this many commit lines go into a week's text; the rest are counted.
+const MAX_COMMIT_LINES: usize = 40;
+
+/// How many files a week names as the ones most of the work landed in.
@@ cli/tests/summarize.rs
@@ -0,0 +1,317 @@
+//! `pop summarize` and `pop summary edit`, end to end, with `llm.provider =
+//! none` so the tests never look for a model.
+
+mod common;
+
+use common::*;
+use pop::event::{
+    CommitFile, CommitPayload, DecisionPayload, EventKind, FileStatus, Payload, SummaryPayload,
+};
+use pop::store::Store;
+
+/// Midday UTC on a Wednesday: no timezone on earth moves that out of its week.
+const WEEK_ONE: &str = "2026-01-07T12:00:00.000Z";
+const WEEK_THREE: &str = "2026-01-21T12:00:00.000Z";
+
+fn store(repo: &TestRepo) -> Store {
+    Store::open(&repo.path(".pop/log.db")).unwrap()
+}
+
@@ design/README.md
@@ -0,0 +1,46 @@
+# Design notes: project page and landing
+
+## What is here
+
+- `tokens.css`: the single source of tokens. OKLCH with zero chroma (near black, white, neutral greys), light and dark via `prefers-color-scheme` and `[data-theme]`. Fluid type scale, 4 px spacing scale, small radii, two easing curves. Fonts from Google: Schibsted Grotesk (display), Instrument Sans (text), Geist Mono (terminal, hashes, dates).
+- `project-page.html`: static mock of the page a company reads. One file, CSS inline, no dependencies besides the fonts. Every field of the shared page context is marked with `<!-- ctx: ... -->` for the Tera conversion (task E). `mock/context.json` holds the same sample data in the plan's context shape. The generator that produced both was removed after the last build; edit the HTML directly, it is now the source.
+- The landing lives in `landing/` (Next.js, static export) and imports `tokens.css` through `src/app/globals.css`. `landing/public/example/index.html` is a copy of the mock until task I replaces it with real `pop publish --local` output.
+
+## Decisions
+
+- Zero-chroma palette (user decision). Status is never colour: verified = shield outline plus the word, generated = mono label in a hairline box, remote model = the same label with a double rule, hidden session = dashed ring and italic, decisions = filled diamond, AI sessions = ring, commits = dot.
+- Project page reads top to bottom in the PRD order: intent and delivered, timeline (inline SVG, three lanes on one axis, `<details>` records underneath, click opens the record with or without JS), week summaries, three to five moments, attribution bar, badge. Print stylesheet targets one A4 page with the badge visible; records and summaries are dropped on paper.
+- Landing: one continuous canvas, no horizontal rules between sections. A 1 px spine at the content edge starts inside the hero, continues to the footer and fades out; each section hangs a short tick off it. The hero scene fades into the canvas with a gradient. Copy is one or two sentences per paragraph; the FAQ moved to `/docs/faq/` and the landing links to it once. Content is `hero, example, three blocks with terminal mocks, for whom, privacy, footer`.
+- The hero scene is owned by a separate task (`landing/src/components/hero/`). The landing mounts `<Hero />` as a full-bleed layer and keeps the headline, subheadline, install block and example link readable on top with a radial fade.
+- Install block: system-wide command by default, one quiet line for `--project` that reveals the per-project command. The copy button fires the Plausible `copy-install` tagged event (props `os`, `mode`).
+- Motion: only where it carries meaning. Timeline draws its axis and points arrive in order (CSS, 1.2 s, off under reduced motion). Copy button crossfades its label through a 3 px blur, 160 ms, `scale(0.97)` on press. Nothing else moves outside the hero.
+
+## Skill passes
+
@@ design/brand/logo/explore/concept-1-rings-row.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<defs><clipPath id="mac"><circle cx="187.0" cy="195.1" r="54.4"/></clipPath><mask id="ma1" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#ma1c)" d="M26 256A92 92 0 1 0 210 256A92 92 0 1 0 26 256Z" stroke="#000" stroke-width="78"/></mask><clipPath id="mbc"><circle cx="187.0" cy="316.9" r="54.4"/></clipPath><mask id="mb1" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mb1c)" d="M164 256A92 92 0 1 0 348 256A92 92 0 1 0 164 256Z" stroke="#000" stroke-width="78"/></mask></defs>
+<path d="M26 256A92 92 0 1 0 210 256A92 92 0 1 0 26 256Z" stroke-width="34" mask="url(#mb1)"/>
+<path d="M164 256A92 92 0 1 0 348 256A92 92 0 1 0 164 256Z" stroke-width="34" mask="url(#ma1)"/>
+<defs><clipPath id="mac"><circle cx="325.0" cy="316.9" r="54.4"/></clipPath><mask id="ma2" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#ma2c)" d="M164 256A92 92 0 1 0 348 256A92 92 0 1 0 164 256Z" stroke="#000" stroke-width="78"/></mask><clipPath id="mbc"><circle cx="325.0" cy="195.1" r="54.4"/></clipPath><mask id="mb2" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mb2c)" d="M302 256A92 92 0 1 0 486 256A92 92 0 1 0 302 256Z" stroke="#000" stroke-width="78"/></mask></defs>
+<path d="M302 256A92 92 0 1 0 486 256A92 92 0 1 0 302 256Z" stroke-width="34" mask="url(#ma2)"/>
+</svg>
@@ design/brand/logo/explore/concept-2-arc-seal.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<path d="M282.3 90.1A168 168 0 0 1 412.8 316.2" stroke-width="40"/>
+<path d="M386.6 361.7A168 168 0 0 1 125.4 361.7" stroke-width="40"/>
+<path d="M99.2 316.2A168 168 0 0 1 229.7 90.1" stroke-width="40"/>
+<circle cx="256.0" cy="88.0" r="22.0" fill="currentColor" stroke="none"/>
+<circle cx="401.5" cy="340.0" r="22.0" fill="currentColor" stroke="none"/>
+<circle cx="110.5" cy="340.0" r="22.0" fill="currentColor" stroke="none"/>
+</svg>
@@ design/brand/logo/explore/concept-3-square-links.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<defs><clipPath id="mac"><circle cx="306.0" cy="206.0" r="64.0"/></clipPath><mask id="ma" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mac)" d="M126 78H258A48 48 0 0 1 306 126V258A48 48 0 0 1 258 306H126A48 48 0 0 1 78 258V126A48 48 0 0 1 126 78Z" stroke="#000" stroke-width="84"/></mask><clipPath id="mbc"><circle cx="206.0" cy="306.0" r="64.0"/></clipPath><mask id="mb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mbc)" d="M254 206H386A48 48 0 0 1 434 254V386A48 48 0 0 1 386 434H254A48 48 0 0 1 206 386V254A48 48 0 0 1 254 206Z" stroke="#000" stroke-width="84"/></mask></defs>
+<path d="M126 78H258A48 48 0 0 1 306 126V258A48 48 0 0 1 258 306H126A48 48 0 0 1 78 258V126A48 48 0 0 1 126 78Z" stroke-width="40" mask="url(#mb)"/>
+<path d="M254 206H386A48 48 0 0 1 434 254V386A48 48 0 0 1 386 434H254A48 48 0 0 1 206 386V254A48 48 0 0 1 254 206Z" stroke-width="40" mask="url(#ma)"/>
+</svg>
@@ design/brand/logo/explore/concept-4-seal-record.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<defs><clipPath id="mac"><circle cx="266.0" cy="136.0" r="64.0"/></clipPath><mask id="ma" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mac)" d="M26 256A150 150 0 1 0 326 256A150 150 0 1 0 26 256Z" stroke="#000" stroke-width="84"/></mask><clipPath id="mbc"><circle cx="266.0" cy="376.0" r="64.0"/></clipPath><mask id="mb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mbc)" d="M204 136H356A44 44 0 0 1 400 180V332A44 44 0 0 1 356 376H204A44 44 0 0 1 160 332V180A44 44 0 0 1 204 136Z" stroke="#000" stroke-width="84"/></mask></defs>
+<path d="M26 256A150 150 0 1 0 326 256A150 150 0 1 0 26 256Z" stroke-width="40" mask="url(#mb)"/>
+<path d="M204 136H356A44 44 0 0 1 400 180V332A44 44 0 0 1 356 376H204A44 44 0 0 1 160 332V180A44 44 0 0 1 204 136Z" stroke-width="40" mask="url(#ma)"/>
+</svg>
@@ design/brand/logo/explore/concept-5-ledger.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<path d="M256 84V428" stroke-width="36"/>
+<path d="M170 96H342A24 24 0 0 1 366 120V152A24 24 0 0 1 342 176H170A24 24 0 0 1 146 152V120A24 24 0 0 1 170 96Z" stroke-width="36"/>
+<path d="M170 216H342A24 24 0 0 1 366 240V272A24 24 0 0 1 342 296H170A24 24 0 0 1 146 272V240A24 24 0 0 1 170 216Z" stroke-width="36"/>
+<path d="M170 336H342A24 24 0 0 1 366 360V392A24 24 0 0 1 342 416H170A24 24 0 0 1 146 392V360A24 24 0 0 1 170 336Z" stroke-width="36" fill="currentColor"/>
+</svg>
@@ design/brand/logo/explore/concept-6-timeline.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<path d="M92 256H340" stroke-width="36"/>
+<circle cx="112" cy="256" r="38" fill="currentColor" stroke="none"/>
+<circle cx="256" cy="256" r="38" fill="currentColor" stroke="none"/>
+<circle cx="400" cy="256" r="60" stroke-width="36" fill="none"/>
+<circle cx="400" cy="256" r="22" fill="currentColor" stroke="none"/>
+</svg>
@@ design/brand/logo/explore/concept-7-p-link.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<path d="M132 176V452" stroke-width="44"/>
+<defs><clipPath id="mac"><circle cx="286.0" cy="183.6" r="70.4"/></clipPath><mask id="ma" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mac)" d="M132 256A94 94 0 1 0 320 256A94 94 0 1 0 132 256Z" stroke="#000" stroke-width="88"/></mask><clipPath id="mbc"><circle cx="286.0" cy="328.4" r="70.4"/></clipPath><mask id="mb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mbc)" d="M252 256A94 94 0 1 0 440 256A94 94 0 1 0 252 256Z" stroke="#000" stroke-width="88"/></mask></defs>
+<path d="M132 256A94 94 0 1 0 320 256A94 94 0 1 0 132 256Z" stroke-width="44" mask="url(#mb)"/>
+<path d="M252 256A94 94 0 1 0 440 256A94 94 0 1 0 252 256Z" stroke-width="44" mask="url(#ma)"/>
+</svg>
@@ design/brand/logo/explore/concept-8-segments.svg
@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<path d="M82 196H130A26 26 0 0 1 156 222V290A26 26 0 0 1 130 316H82A26 26 0 0 1 56 290V222A26 26 0 0 1 82 196Z" stroke-width="30"/>
+<path d="M202 196H250A26 26 0 0 1 276 222V290A26 26 0 0 1 250 316H202A26 26 0 0 1 176 290V222A26 26 0 0 1 202 196Z" stroke-width="30"/>
+<path d="M322 196H370A26 26 0 0 1 396 222V290A26 26 0 0 1 370 316H322A26 26 0 0 1 296 290V222A26 26 0 0 1 322 196Z" stroke-width="30"/>
+<path d="M430 196H442A14 14 0 0 1 456 210V302A14 14 0 0 1 442 316H430A14 14 0 0 1 416 302V210A14 14 0 0 1 430 196Z" stroke-width="30" fill="currentColor"/>
+<path d="M156 256H176" stroke-width="30"/>
+<path d="M276 256H296" stroke-width="30"/>
+<path d="M396 256H416" stroke-width="30"/>
+</svg>
@@ design/brand/logo/explore/refine/r3-a-primary.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<defs><clipPath id="mac"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="ma" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mac)" d="M110.0 146.0H250.0A40 40 0 0 1 290.0 186.0V326.0A40 40 0 0 1 250.0 366.0H110.0A40 40 0 0 1 70.0 326.0V186.0A40 40 0 0 1 110.0 146.0Z" stroke="#000" stroke-width="88"/></mask><clipPath id="mbc"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="mb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mbc)" d="M142.0 256A150 150 0 1 0 442.0 256A150 150 0 1 0 142.0 256Z" stroke="#000" stroke-width="88"/></mask></defs>
+<path d="M110.0 146.0H250.0A40 40 0 0 1 290.0 186.0V326.0A40 40 0 0 1 250.0 366.0H110.0A40 40 0 0 1 70.0 326.0V186.0A40 40 0 0 1 110.0 146.0Z" stroke-width="44" mask="url(#mb)"/>
+<path d="M142.0 256A150 150 0 1 0 442.0 256A150 150 0 1 0 142.0 256Z" stroke-width="44" mask="url(#ma)"/>
+</svg>
@@ design/brand/logo/explore/refine/r3-b-primary-g30.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<defs><clipPath id="mac"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="ma" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mac)" d="M110.0 146.0H250.0A40 40 0 0 1 290.0 186.0V326.0A40 40 0 0 1 250.0 366.0H110.0A40 40 0 0 1 70.0 326.0V186.0A40 40 0 0 1 110.0 146.0Z" stroke="#000" stroke-width="88"/></mask><clipPath id="mbc"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="mb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mbc)" d="M142.0 256A150 150 0 1 0 442.0 256A150 150 0 1 0 142.0 256Z" stroke="#000" stroke-width="88"/></mask></defs>
+<path d="M110.0 146.0H250.0A40 40 0 0 1 290.0 186.0V326.0A40 40 0 0 1 250.0 366.0H110.0A40 40 0 0 1 70.0 326.0V186.0A40 40 0 0 1 110.0 146.0Z" stroke-width="44" mask="url(#mb)"/>
+<path d="M142.0 256A150 150 0 1 0 442.0 256A150 150 0 1 0 142.0 256Z" stroke-width="44" mask="url(#ma)"/>
+</svg>
@@ design/brand/logo/explore/refine/r3-c-compact-64.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<path d="M114.0 146.0H246.0A44 44 0 0 1 290.0 190.0V322.0A44 44 0 0 1 246.0 366.0H114.0A44 44 0 0 1 70.0 322.0V190.0A44 44 0 0 1 114.0 146.0Z" stroke-width="64"/>
+<path d="M142.0 256A150 150 0 1 0 442.0 256A150 150 0 1 0 142.0 256Z" stroke-width="64"/>
+</svg>
@@ design/brand/logo/explore/refine/r3-d-compact-76.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<path d="M118.0 146.0H242.0A48 48 0 0 1 290.0 194.0V318.0A48 48 0 0 1 242.0 366.0H118.0A48 48 0 0 1 70.0 318.0V194.0A48 48 0 0 1 118.0 146.0Z" stroke-width="76"/>
+<path d="M142.0 256A150 150 0 1 0 442.0 256A150 150 0 1 0 142.0 256Z" stroke-width="76"/>
+</svg>
@@ design/brand/logo/explore/refine/r3-e-compact-cut64.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<defs><clipPath id="mac"><circle cx="190.0" cy="366.0" r="102.4"/></clipPath><mask id="ma" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mac)" d="M114.0 146.0H246.0A44 44 0 0 1 290.0 190.0V322.0A44 44 0 0 1 246.0 366.0H114.0A44 44 0 0 1 70.0 322.0V190.0A44 44 0 0 1 114.0 146.0Z" stroke="#000" stroke-width="108"/></mask><clipPath id="mbc"><circle cx="190.0" cy="146.0" r="102.4"/></clipPath><mask id="mb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mbc)" d="M142.0 256A150 150 0 1 0 442.0 256A150 150 0 1 0 142.0 256Z" stroke="#000" stroke-width="108"/></mask></defs>
+<path d="M114.0 146.0H246.0A44 44 0 0 1 290.0 190.0V322.0A44 44 0 0 1 246.0 366.0H114.0A44 44 0 0 1 70.0 322.0V190.0A44 44 0 0 1 114.0 146.0Z" stroke-width="64" mask="url(#mb)"/>
+<path d="M142.0 256A150 150 0 1 0 442.0 256A150 150 0 1 0 142.0 256Z" stroke-width="64" mask="url(#ma)"/>
+</svg>
@@ design/mock/build.mjs
@@ -1,564 +0,0 @@
-// Builds design/project-page.html (static mock) and design/mock/context.json
-// (the same data in the shared page-context shape from the plan).
-// Run: node design/mock/build.mjs
-import { readFileSync, writeFileSync } from "node:fs";
-import { dirname, join } from "node:path";
-import { fileURLToPath } from "node:url";
-import { project, rows, patterns, moments, ratio, summaries, badge } from "./data.mjs";
-
-const here = dirname(fileURLToPath(import.meta.url));
-const tokens = readFileSync(join(here, "..", "tokens.css"), "utf8");
-
-// ---------- context -------------------------------------------------------
-
-const T0 = Date.parse(project.first_ts);
-const T1 = Date.parse(project.last_ts);
-const xOf = (ts) => (Date.parse(ts) - T0) / (T1 - T0);
-const uuid = (i) => `0199${(0x4c00 + i).toString(16)}-7a2e-7${String(i).padStart(3, "0")}-8b1c-${(0x9e0000000000 + i * 7919).toString(16).padStart(12, "0")}`;
-
-const kindOf = { c: "commit", d: "decision", a: "ai_session", h: "hidden" };
@@ design/mock/context.json
@@ -1001,7 +1001,7 @@
       "id": "01994c33-7a2e-7051-8b1c-9e000006299d",
       "ts": "2026-09-15T14:30:00Z",
       "kind": "decision",
-      "title": "Timeline is inline SVG with a <details> fallback. No JS framework. The page must open from a USB stick in 2030.",
+      "title": "Timeline is inline SVG with a details fallback. No JS framework: the page must still open, without a build step, years from now.",
       "detail": null,
       "sha": null,
       "x": 0.8605
@@ -1432,7 +1432,7 @@
     {
       "title": "The preview hid the wrong row",
       "kind": "fix_after_ai",
-      "body": "Claude wrote the ratatui list with a cursor index relative to the visible window. Pressing h after scrolling hid a different session than the one under the cursor. Fixed four hours later by adding the scroll offset. Small bug, but it is exactly the kind a hiring manager wants to see you catch.",
+      "body": "Claude wrote the ratatui list with a cursor index relative to the visible window. Pressing h after scrolling hid a different session than the one under the cursor. Fixed four hours later by adding the scroll offset. A small bug, caught the same afternoon.",
       "refs": [
         "01994c37-7a2e-7055-8b1c-9e000006a559",
         "01994c38-7a2e-7056-8b1c-9e000006c448",
@@ design/mock/data.mjs
@@ -1,376 +0,0 @@
-// Sample data for the project-page mock: pop's own first two weeks.
-// Compact rows are expanded into the shared page context (see plan §"Shared page context").
-
-export const project = {
-  name: "pop",
-  what: "A Rust CLI that records how a project came together and turns it into one page a hiring manager reads in two minutes.",
-  for_whom: "Students and juniors applying for a first job, who work with Claude Code and have nothing but a finished repo to show.",
-  tools: ["claude-code"],
-  delivered_url: "https://github.com/vanjamodrinjak/pop",
-  screenshot: null,
-  first_ts: "2026-09-03T09:12:00Z",
-  last_ts: "2026-09-17T14:02:00Z",
-};
-
-// [ts, kind, title, detail, extra]
-// kind: c = commit, d = decision, a = ai_session, h = hidden ai_session
-// extra for commits: { sha, files: [[path, +, -]], excerpt }
-// extra for ai sessions: { model, files: [[path, ai_lines, modified]], accepted }
-export const rows = [
@@ design/project-page.html
@@ -14,7 +14,8 @@
 /* pop design tokens
    Consumed by: design/project-page.html (inlined), landing/src/site.css (Tailwind v4 @theme inline).
    Fonts (Google Fonts): Schibsted Grotesk (display), Instrument Sans (text), Geist Mono (terminal, hashes).
-   Colors are OKLCH. One accent hue (ochre-rust, hue 55). Neutrals are tinted toward the same hue. */
+   Colors are OKLCH with zero chroma: near black, white and neutral greys. No accent hue.
+   Status (verified, generated, hidden) is carried by weight, outline and type, never by colour. */
 
 :root {
   color-scheme: light dark;
@@ -36,7 +37,7 @@
   --leading-tight: 1.05;
   --leading-snug: 1.25;
   --leading-body: 1.55;
-  --tracking-display: -0.025em;
+  --tracking-display: -0.03em;
   --tracking-tight: -0.012em;
   --measure: 62ch;
 
@@ -70,86 +71,77 @@
@@ design/tokens.css
@@ -1,7 +1,8 @@
 /* pop design tokens
    Consumed by: design/project-page.html (inlined), landing/src/site.css (Tailwind v4 @theme inline).
    Fonts (Google Fonts): Schibsted Grotesk (display), Instrument Sans (text), Geist Mono (terminal, hashes).
-   Colors are OKLCH. One accent hue (ochre-rust, hue 55). Neutrals are tinted toward the same hue. */
+   Colors are OKLCH with zero chroma: near black, white and neutral greys. No accent hue.
+   Status (verified, generated, hidden) is carried by weight, outline and type, never by colour. */
 
 :root {
   color-scheme: light dark;
@@ -23,7 +24,7 @@
   --leading-tight: 1.05;
   --leading-snug: 1.25;
   --leading-body: 1.55;
-  --tracking-display: -0.025em;
+  --tracking-display: -0.03em;
   --tracking-tight: -0.012em;
   --measure: 62ch;
 
@@ -57,86 +58,77 @@
@@ landing/.gitignore
@@ -39,3 +39,6 @@ yarn-error.log*
 # typescript
 *.tsbuildinfo
 next-env.d.ts
+
+# static export
+/out
@@ landing/.prettierignore
@@ -0,0 +1,6 @@
+.next
+out
+node_modules
+public/example
+package-lock.json
+pnpm-lock.yaml
@@ landing/.prettierrc
@@ -0,0 +1,6 @@
+{
+  "printWidth": 100,
+  "singleQuote": false,
+  "trailingComma": "all",
+  "semi": true
+}
@@ landing/AGENTS.md
@@ -0,0 +1,9 @@
+<!-- BEGIN:nextjs-agent-rules -->
+
+# This is NOT the Next.js you know
+
+This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
+
+This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
+
+<!-- END:nextjs-agent-rules -->
@@ landing/CLAUDE.md
@@ -0,0 +1 @@
+@AGENTS.md
@@ landing/eslint.config.mjs
@@ -1,18 +1,13 @@
 import { defineConfig, globalIgnores } from "eslint/config";
 import nextVitals from "eslint-config-next/core-web-vitals";
 import nextTs from "eslint-config-next/typescript";
+import prettier from "eslint-config-prettier/flat";
 
 const eslintConfig = defineConfig([
   ...nextVitals,
   ...nextTs,
-  // Override default ignores of eslint-config-next.
-  globalIgnores([
-    // Default ignores of eslint-config-next:
-    ".next/**",
-    "out/**",
-    "build/**",
-    "next-env.d.ts",
-  ]),
+  prettier,
+  globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts", "public/**"]),
 ]);
@@ landing/next.config.ts
@@ -1,7 +1,12 @@
+import path from "node:path";
 import type { NextConfig } from "next";
 
 const nextConfig: NextConfig = {
-  /* config options here */
+  output: "export",
+  trailingSlash: true,
+  images: { unoptimized: true },
+  // design/tokens.css lives one level up; Turbopack must see the monorepo root.
+  turbopack: { root: path.join(__dirname, "..") },
 };
 
 export default nextConfig;
@@ landing/package.json
@@ -5,13 +5,20 @@
   "scripts": {
     "dev": "next dev",
     "build": "next build",
-    "start": "next start",
-    "lint": "eslint"
+    "start": "pnpm dlx serve out",
+    "lint": "eslint",
+    "typecheck": "tsc --noEmit",
+    "format": "prettier --write .",
+    "format:check": "prettier --check ."
   },
   "dependencies": {
+    "@gsap/react": "^2.1.2",
+    "@types/three": "^0.186.0",
+    "gsap": "^3.15.0",
     "next": "16.3.5",
     "react": "19.2.8",
-    "react-dom": "19.2.8"
+    "react-dom": "19.2.8",
@@ landing/pnpm-lock.yaml
@@ -0,0 +1,4281 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    dependencies:
+      '@gsap/react':
+        specifier: ^2.1.2
+        version: 2.1.2(gsap@3.15.0)(react@19.2.8)
+      '@types/three':
+        specifier: ^0.186.0
+        version: 0.186.0
+      gsap:
+        specifier: ^3.15.0
+        version: 3.15.0
@@ landing/public/example/index.html
@@ -0,0 +1,827 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<!-- ctx: project.name -->
+<title>pop · how it came together</title>
+<meta name="description" content="A Rust CLI that records how a project came together and turns it into one page a hiring manager reads in two minutes.">
+<meta name="robots" content="noindex">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@500;600;700&family=Instrument+Sans:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+/* pop design tokens
+   Consumed by: design/project-page.html (inlined), landing/src/site.css (Tailwind v4 @theme inline).
+   Fonts (Google Fonts): Schibsted Grotesk (display), Instrument Sans (text), Geist Mono (terminal, hashes).
+   Colors are OKLCH with zero chroma: near black, white and neutral greys. No accent hue.
+   Status (verified, generated, hidden) is carried by weight, outline and type, never by colour. */
+
@@ landing/public/file.svg
@@ -1 +0,0 @@
-<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
\ No newline at end of file
@@ landing/public/globe.svg
@@ -1 +0,0 @@
-<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
\ No newline at end of file
@@ landing/public/next.svg
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
\ No newline at end of file
@@ landing/public/vercel.svg
@@ -1 +0,0 @@
-<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
\ No newline at end of file
@@ landing/public/window.svg
@@ -1 +0,0 @@
-<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
\ No newline at end of file
@@ landing/src/app/(en)/layout.tsx
@@ -0,0 +1,14 @@
+import type { Metadata } from "next";
+import type { ReactNode } from "react";
+import { Shell } from "@/components/Shell";
+import { en } from "@/i18n/en";
+
+export const metadata: Metadata = {
+  title: en.title,
+  description: en.description,
+  alternates: { languages: { en: "/", hr: "/hr/", de: "/de/" } },
+};
+
+export default function EnLayout({ children }: { children: ReactNode }) {
+  return <Shell lang="en">{children}</Shell>;
+}
@@ landing/src/app/(en)/page.tsx
@@ -0,0 +1,6 @@
+import { LandingPage } from "@/components/LandingPage";
+import { en } from "@/i18n/en";
+
+export default function Page() {
+  return <LandingPage dict={en} />;
+}
@@ landing/src/app/[lang]/layout.tsx
@@ -0,0 +1,30 @@
+import type { Metadata } from "next";
+import type { ReactNode } from "react";
+import { notFound } from "next/navigation";
+import { dicts, isLang } from "@/i18n";
+import { Shell } from "@/components/Shell";
+
+export const dynamicParams = false;
+
+export function generateStaticParams() {
+  return [{ lang: "hr" }, { lang: "de" }];
+}
+
+type Params = { params: Promise<{ lang: string }> };
+
+export async function generateMetadata({ params }: Params): Promise<Metadata> {
+  const { lang } = await params;
+  if (!isLang(lang)) return {};
+  const d = dicts[lang];
+  return {
@@ landing/src/app/[lang]/page.tsx
@@ -0,0 +1,9 @@
+import { notFound } from "next/navigation";
+import { LandingPage } from "@/components/LandingPage";
+import { dicts, isLang } from "@/i18n";
+
+export default async function Page({ params }: { params: Promise<{ lang: string }> }) {
+  const { lang } = await params;
+  if (!isLang(lang) || lang === "en") notFound();
+  return <LandingPage dict={dicts[lang]} />;
+}
@@ landing/src/app/fonts.ts
@@ -0,0 +1,24 @@
+import { Geist_Mono, Instrument_Sans, Schibsted_Grotesk } from "next/font/google";
+
+export const display = Schibsted_Grotesk({
+  subsets: ["latin", "latin-ext"],
+  weight: ["500", "600", "700"],
+  variable: "--font-display",
+  display: "swap",
+});
+
+export const text = Instrument_Sans({
+  subsets: ["latin", "latin-ext"],
+  weight: ["400", "500", "600"],
+  variable: "--font-text",
+  display: "swap",
+});
+
+export const mono = Geist_Mono({
+  subsets: ["latin"],
+  weight: ["400", "500"],
@@ landing/src/app/globals.css
@@ -1,26 +1,565 @@
+/* pop landing. Tokens come from design/tokens.css; Tailwind v4 maps them below. */
 @import "tailwindcss";
+@import "../../../design/tokens.css";
 
-:root {
-  --background: #ffffff;
-  --foreground: #171717;
+@theme inline {
+  --font-sans: var(--font-text);
+  --font-head: var(--font-display);
+  --font-code: var(--font-mono);
+
+  --color-bg: var(--bg);
+  --color-bg-sunk: var(--bg-sunk);
+  --color-surface: var(--surface);
+  --color-line: var(--line);
+  --color-line-strong: var(--line-strong);
+  --color-ink: var(--ink);
+  --color-ink-2: var(--ink-2);
@@ landing/src/app/layout.tsx
@@ -1,29 +0,0 @@
-import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
-import "./globals.css";
-
-const geistSans = Geist({
-  variable: "--font-geist-sans",
-  subsets: ["latin"],
-});
-
-const geistMono = Geist_Mono({
-  variable: "--font-geist-mono",
-  subsets: ["latin"],
-});
-
-export const metadata: Metadata = {
-  title: "Create Next App",
-  description: "Generated by create next app",
-};
-
@@ landing/src/app/page.tsx
@@ -1,69 +0,0 @@
-import Image from "next/image";
-
-export default function Home() {
-  return (
-    <div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
-      <main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
-        <Image
-          className="dark:invert h-5 w-[100px]"
-          src="/next.svg"
-          alt="Next.js logo"
-          width={100}
-          height={20}
-          priority
-        />
-        <div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
-          <h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
-            To get started, edit the{" "}
-            <code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
-              page.tsx
@@ landing/src/components/InstallBlock.tsx
@@ -0,0 +1,140 @@
+"use client";
+
+import { useState } from "react";
+import type { Dict } from "@/i18n";
+
+type Os = "mac" | "win";
+
+const CMD: Record<Os, { system: string; project: string }> = {
+  mac: {
+    system: "curl -fsSL https://pop.dev/install.sh | sh",
+    project: "curl -fsSL https://pop.dev/install.sh | sh -s -- --project",
+  },
+  win: {
+    system: "irm https://pop.dev/install.ps1 | iex",
+    project: "& ([scriptblock]::Create((irm https://pop.dev/install.ps1))) -Project",
+  },
+};
+
+const PROMPT: Record<Os, string> = { mac: "$", win: ">" };
@@ landing/src/components/LandingPage.tsx
@@ -0,0 +1,173 @@
+import type { Dict } from "@/i18n";
+import Hero from "./hero/Hero";
+import { InstallBlock } from "./InstallBlock";
+import { LangSwitch } from "./LangSwitch";
+import { Terminal } from "./Terminal";
+import { termInstall, termNote, termPublish } from "./terminals";
+
+const GITHUB = "https://github.com/vanjamodrinjak/pop";
+const DOCS = "/docs/";
+
+function Header({ dict }: { dict: Dict }) {
+  return (
+    <header className="wrap flex items-center justify-between gap-6 py-5 text-sm">
+      <a href="#top" className="font-head text-base font-bold text-ink no-underline">
+        pop
+      </a>
+      <nav className="hidden items-center gap-6 sm:flex" aria-label="Site">
+        <a className="nav-link" href="#example">
+          {dict.nav.example}
@@ landing/src/components/LangSwitch.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { LANGS, LANG_PATH, type Lang } from "@/i18n";
+
+const NAMES: Record<Lang, string> = { en: "EN", hr: "HR", de: "DE" };
+
+// Lists all three languages everywhere; each link carries the current section hash
+// so the reader lands on the same place in the other language.
+export function LangSwitch({ current }: { current: Lang }) {
+  const [hash, setHash] = useState("");
+  useEffect(() => {
+    const read = () => setHash(window.location.hash);
+    read();
+    window.addEventListener("hashchange", read);
+    return () => window.removeEventListener("hashchange", read);
+  }, []);
+
+  return (
@@ landing/src/components/Shell.tsx
@@ -0,0 +1,23 @@
+import Script from "next/script";
+import type { ReactNode } from "react";
+import { fontClass } from "@/app/fonts";
+import type { Lang } from "@/i18n";
+import "@/app/globals.css";
+
+// Shared <html>/<body> for every locale. Plausible is EU-hosted and cookieless;
+// data-domain is a placeholder until the domain is final.
+export function Shell({ lang, children }: { lang: Lang; children: ReactNode }) {
+  return (
+    <html lang={lang} className={fontClass}>
+      <body>
+        {children}
+        <Script
+          defer
+          data-domain="pop.dev"
+          src="https://plausible.io/js/script.tagged-events.js"
+          strategy="afterInteractive"
+        />
@@ landing/src/components/Terminal.tsx
@@ -0,0 +1,56 @@
+import type { TermLine, TermMock } from "./terminals";
+
+function Line({ line }: { line: TermLine }) {
+  switch (line.kind) {
+    case "blank":
+      return "\n";
+    case "cmd":
+      return (
+        <>
+          <span className="c">{line.text}</span>
+          {"\n"}
+        </>
+      );
+    case "dim":
+      return (
+        <>
+          <span className="d">{line.text}</span>
+          {"\n"}
+        </>
@@ landing/src/components/terminals.ts
@@ -0,0 +1,62 @@
+// Terminal mock content. The CLI speaks English in every locale (spec §6.1),
+// so these lines are shared by all three landing pages.
+
+export type TermLine =
+  | { kind: "cmd"; text: string }
+  | { kind: "out"; text: string }
+  | { kind: "dim"; text: string }
+  | { kind: "accent"; text: string }
+  | { kind: "prompt"; text: string }
+  | { kind: "blank" };
+
+export interface TermMock {
+  title: string;
+  lines: TermLine[];
+}
+
+export const termInstall: TermMock = {
+  title: "zsh — pop",
+  lines: [
@@ landing/src/i18n/de.ts
@@ -0,0 +1,64 @@
+import type { Dict } from "./types";
+
+export const de: Dict = {
+  lang: "de",
+  title: "pop — Zeig ihnen, wie du mit KI arbeitest",
+  description:
+    "pop zeichnet auf, wie dein Projekt tatsächlich entstanden ist: Entscheidungen, KI-Sitzungen, die Fehler, die du gefunden hast, und macht daraus eine Seite, die ein Hiring Manager in zwei Minuten liest.",
+  nav: { example: "Beispiel", how: "So funktioniert es", docs: "Dokumentation", github: "GitHub" },
+  hero: {
+    headline: "Zeig ihnen, wie du mit KI arbeitest. Nicht nur, was dabei entstanden ist.",
+    sub: "pop zeichnet auf, wie dein Projekt tatsächlich entstanden ist: Entscheidungen, KI-Sitzungen, die Fehler, die du gefunden hast, und macht daraus eine Seite, die ein Hiring Manager in zwei Minuten liest.",
+    tabs: { mac: "macOS / Linux", win: "Windows" },
+    copy: "Kopieren",
+    copied: "Kopiert",
+    projectNote: "Nur für ein Projekt? Führe den Befehl im Repository aus mit",
+    projectShow: "Befehl anzeigen",
+    projectHide: "Befehl ausblenden",
+    example: "Beispielseite ansehen",
+  },
@@ landing/src/i18n/en.ts
@@ -0,0 +1,64 @@
+import type { Dict } from "./types";
+
+export const en: Dict = {
+  lang: "en",
+  title: "pop — Show them how you work with AI",
+  description:
+    "pop records how your project actually came together, decisions, AI sessions, the bugs you caught, and turns it into a page a hiring manager reads in two minutes.",
+  nav: { example: "Example", how: "How it works", docs: "Docs", github: "GitHub" },
+  hero: {
+    headline: "Show them how you work with AI. Not just what it built.",
+    sub: "pop records how your project actually came together, decisions, AI sessions, the bugs you caught, and turns it into a page a hiring manager reads in two minutes.",
+    tabs: { mac: "macOS / Linux", win: "Windows" },
+    copy: "Copy",
+    copied: "Copied",
+    projectNote: "Only for one project? Run it inside the repo with",
+    projectShow: "show command",
+    projectHide: "hide command",
+    example: "See an example page",
+  },
@@ landing/src/i18n/hr.ts
@@ -0,0 +1,64 @@
+import type { Dict } from "./types";
+
+export const hr: Dict = {
+  lang: "hr",
+  title: "pop — Pokaži im kako radiš s AI-em",
+  description:
+    "pop bilježi kako je tvoj projekt stvarno nastao: odluke, AI sesije, greške koje si uhvatio, i od toga slaže stranicu koju voditelj zapošljavanja pročita u dvije minute.",
+  nav: { example: "Primjer", how: "Kako radi", docs: "Dokumentacija", github: "GitHub" },
+  hero: {
+    headline: "Pokaži im kako radiš s AI-em. Ne samo što je AI napravio.",
+    sub: "pop bilježi kako je tvoj projekt stvarno nastao: odluke, AI sesije, greške koje si uhvatio, i od toga slaže stranicu koju voditelj zapošljavanja pročita u dvije minute.",
+    tabs: { mac: "macOS / Linux", win: "Windows" },
+    copy: "Kopiraj",
+    copied: "Kopirano",
+    projectNote: "Samo za jedan projekt? Pokreni naredbu unutar repozitorija s",
+    projectShow: "prikaži naredbu",
+    projectHide: "sakrij naredbu",
+    example: "Pogledaj primjer stranice",
+  },
@@ landing/src/i18n/index.ts
@@ -0,0 +1,13 @@
+import { de } from "./de";
+import { en } from "./en";
+import { hr } from "./hr";
+import type { Dict, Lang } from "./types";
+
+export const dicts: Record<Lang, Dict> = { en, hr, de };
+
+export function isLang(x: string): x is Lang {
+  return x === "en" || x === "hr" || x === "de";
+}
+
+export type { Dict, Lang } from "./types";
+export { LANGS, LANG_PATH } from "./types";
@@ landing/src/i18n/types.ts
@@ -0,0 +1,32 @@
+export type Lang = "en" | "hr" | "de";
+
+export const LANGS: readonly Lang[] = ["en", "hr", "de"] as const;
+
+export const LANG_PATH: Record<Lang, string> = { en: "/", hr: "/hr/", de: "/de/" };
+
+export interface Dict {
+  lang: Lang;
+  title: string;
+  description: string;
+  nav: { example: string; how: string; docs: string; github: string };
+  hero: {
+    headline: string;
+    sub: string;
+    tabs: { mac: string; win: string };
+    copy: string;
+    copied: string;
+    projectNote: string;
+    projectShow: string;
18 Sep, 06:59decisionWeekly summaries come from Ollama with a heuristic fallback; a remote model is opt-in and the page marks generated text.
18 Sep, 06:59commita4ab0bachore(cli): page module contract stub for the generator and publish tasks
@@ cli/src/lib.rs
@@ -12,6 +12,7 @@ pub mod gitai;
 pub mod gitrepo;
 pub mod keys;
 pub mod moments;
+pub mod page;
 pub mod patterns;
 pub mod store;
 pub mod summarize;
@@ cli/src/page/mod.rs
@@ -0,0 +1,64 @@
+//! Project page generator (task E fills this in; task G consumes it).
+//!
+//! Contract shared by both tasks — keep these signatures stable:
+//! - [`PageContext`] mirrors the "Shared page context" JSON in the plan.
+//! - [`build_context`] derives it from the event log and config.
+//! - [`render`] renders the Tera template to a single self-contained HTML string.
+//! - [`render_local`] writes `.pop/site/index.html` and returns its path.
+
+use std::path::{Path, PathBuf};
+
+use anyhow::{Context, Result};
+use serde::{Deserialize, Serialize};
+
+/// Server-side publication facts shown in the badge; `None` for local renders.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct Published {
+    pub slug: String,
+    pub url: String,
+    pub received_at: String,
18 Sep, 06:59commita1cd361feat(cli): Tera project page generator, pop publish with ratatui preview, signing and unpublish
@@ cli/src/cli.rs
@@ -42,6 +42,8 @@ pub enum Command {
     Config(ConfigArgs),
     /// Render the project page and publish it.
     Publish(PublishArgs),
+    /// Take the published page down again.
+    Unpublish(UnpublishArgs),
     /// Look for patterns in the log and record them.
     Analyze(AnalyzeArgs),
     /// Write weekly summaries.
@@ -153,6 +155,13 @@ pub struct PublishArgs {
 }
 
 #[derive(Debug, Args)]
+pub struct UnpublishArgs {
+    /// The server the page is on.
+    #[arg(long)]
+    pub server: Option<String>,
+}
+
+#[derive(Debug, Args)]
@@ cli/src/commands/publish.rs
@@ -1,10 +1,30 @@
-//! `pop publish` — render the project page, preview it, sign the root and send it.
+//! `pop publish` and `pop unpublish` — the only two commands that talk to a
+//! network, and the only moment anything leaves this machine (spec §7).
 //!
-//! Task E renders the page and task G adds the preview, signing and upload.
+//! The order is fixed, and it matters:
+//!
+//! 1. refuse outside a repository pop knows,
+//! 2. bring patterns and summaries up to date, so the page is about today,
+//! 3. show the preview and let it change the log (hide, edit),
+//! 4. render `.pop/site/index.html` — always, so `--local` has produced
+//!    something even when the network is down,
+//! 5. sign the chain root and POST the page, then render it once more with the
+//!    server's confirmation in the badge.
+//!
+//! Nothing between step 3 and step 5 appends to the log, so the root the page
+//! shows is the root that gets signed.
 
-use anyhow::Result;
@@ cli/src/config.rs
@@ -11,6 +11,8 @@ pub const KEYS: &[&str] = &[
     "llm.model",
     "llm.remote",
     "server.url",
+    "server.device_id",
+    "server.slug",
     "privacy.redact",
     "project.name",
     "project.delivered_url",
@@ -50,12 +52,22 @@ impl Default for Llm {
 #[serde(default, deny_unknown_fields)]
 pub struct Server {
     pub url: String,
+    /// What `POST /v1/devices` called this machine. Written by `pop publish`,
+    /// not by you; it is a public identifier, never a secret.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub device_id: Option<String>,
+    /// The page this repository already owns, so re-publishing replaces it
+    /// instead of minting a second link.
+    #[serde(skip_serializing_if = "Option::is_none")]
@@ cli/src/lib.rs
@@ -14,6 +14,8 @@ pub mod keys;
 pub mod moments;
 pub mod page;
 pub mod patterns;
+pub mod preview;
+pub mod publish;
 pub mod store;
 pub mod summarize;
 
@@ cli/src/page/mod.rs
@@ -5,12 +5,28 @@
 //! - [`build_context`] derives it from the event log and config.
 //! - [`render`] renders the Tera template to a single self-contained HTML string.
 //! - [`render_local`] writes `.pop/site/index.html` and returns its path.
+//!
+//! Everything the page can show has to be in [`PageContext`] first. The
+//! template has no access to the log, so a private decision or a redacted
+//! prompt cannot reach the page by accident: it is dropped here, once.
+
+mod tmpl;
 
+use std::collections::{BTreeMap, BTreeSet};
 use std::path::{Path, PathBuf};
 
 use anyhow::{Context, Result};
+use chrono::{DateTime, Local, Utc};
 use serde::{Deserialize, Serialize};
 
+use crate::config::Config;
+use crate::event::{
@@ cli/src/page/tmpl.rs
@@ -0,0 +1,472 @@
+//! The Tera side of the page: one template, compiled once, plus the handful
+//! of filters it needs.
+//!
+//! `tera` is pulled in without its default features, so there is no `date`
+//! filter and no `now()`. Every derived value is therefore either a filter
+//! registered here or a key added to the context in [`render`]; the template
+//! itself does no arithmetic it cannot be trusted with.
+
+use std::collections::HashMap;
+use std::sync::OnceLock;
+
+use anyhow::{anyhow, Result};
+use chrono::{DateTime, Datelike, NaiveDate, Utc, Weekday};
+use serde::Serialize;
+use tera::{Context, Tera, Value};
+
+use super::PageContext;
+
+const TEMPLATE: &str = include_str!("../../templates/project.html.tera");
@@ cli/src/preview/mod.rs
@@ -0,0 +1,868 @@
+//! The publish preview (spec §4.5): one screen, three panes, four keys.
+//!
+//! The terminal lives in [`tui`]. Everything else — what the screen shows and
+//! what the keys are allowed to change — is plain data behind two traits, so
+//! the confirm and abort paths can be tested without a terminal:
+//!
+//! - [`Model`] is the screen's contents, rebuilt from the log.
+//! - [`Host`] is the only way the preview may touch the log (hide a session,
+//!   edit a summary, reload).
+//! - [`Preview`] is the screen itself; [`Scripted`] is one that answers from a
+//!   list of key presses.
+
+pub mod tui;
+
+use std::path::{Path, PathBuf};
+
+use anyhow::Result;
+
+use crate::event::{Event, EventKind, SummaryPayload};
@@ cli/src/preview/tui.rs
@@ -0,0 +1,377 @@
+//! The terminal half of the preview: ratatui draws, crossterm reads keys.
+//!
+//! Nothing here decides anything. Keys are turned into [`super::Key`] and
+//! handed to [`super::apply`], which is the same code the tests drive, so the
+//! screen cannot grow behaviour the tests do not see.
+//!
+//! Palette: none. The brand is near black and white, and status is carried by
+//! weight and by words, never by colour (spec §6).
+
+use std::io::IsTerminal;
+
+use anyhow::{Context, Result};
+use ratatui::crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
+use ratatui::layout::{Constraint, Direction, Layout, Rect};
+use ratatui::style::{Modifier, Style};
+use ratatui::text::{Line, Span};
+use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap};
+use ratatui::{DefaultTerminal, Frame};
+
@@ cli/src/publish/mod.rs
@@ -0,0 +1,543 @@
+//! Talking to the pop server: the exact bytes we sign, and the three requests
+//! `pop publish` and `pop unpublish` make.
+//!
+//! The signature scheme is `server/README.md`, and it is deliberately dull:
+//! the server verifies over the strings it received, so nothing here may
+//! re-serialise, re-case or re-format a field between signing and sending.
+//!
+//! - publish: `root + "\n" + published_ts + "\n" + slug`, the slug empty on a
+//!   first publish (so the message ends with a newline).
+//! - delete:  `"delete" + "\n" + slug`.
+//!
+//! Everything on the wire is lowercase hex.
+
+use std::time::Duration;
+
+use serde::{Deserialize, Serialize};
+
+/// The server refuses html larger than this, so we refuse it first and say why.
+pub const MAX_HTML_BYTES: usize = 2 * 1024 * 1024;
@@ cli/templates/project.html.tera
@@ -0,0 +1,552 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>{{ project.name }} · how it came together</title>
+<meta name="description" content="{{ project.what }}">
+<meta name="robots" content="noindex">
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@500;600;700&family=Instrument+Sans:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
+<style>
+/* pop design tokens
+   Consumed by: design/project-page.html (inlined), landing/src/site.css (Tailwind v4 @theme inline).
+   Fonts (Google Fonts): Schibsted Grotesk (display), Instrument Sans (text), Geist Mono (terminal, hashes).
+   Colors are OKLCH with zero chroma: near black, white and neutral greys. No accent hue.
+   Status (verified, generated, hidden) is carried by weight, outline and type, never by colour. */
+
+:root {
@@ cli/tests/commands.rs
@@ -139,7 +139,6 @@ fn deferred_commands_exit_with_code_two() {
             vec!["ask", "what", "happened"],
             "not available in this version",
         ),
-        (vec!["publish"], "not implemented yet"),
     ] {
         let err = pop::run_in(repo.root(), &args).unwrap_err();
         assert_eq!(exit_code(&err), Some(2), "{args:?}: {err:#}");
@@ cli/tests/page.rs
@@ -0,0 +1,355 @@
+//! The project page: the design mock's context renders, and a real repo's log
+//! renders the same template without leaking anything it must not show.
+
+mod common;
+
+use std::path::{Path, PathBuf};
+
+use pop::event::{AiSessionFile, AiSessionPayload, Payload};
+use pop::page::{self, PageContext, Published};
+use pop::store::Store;
+
+fn repo_root() -> PathBuf {
+    Path::new(env!("CARGO_MANIFEST_DIR"))
+        .parent()
+        .expect("cli/ has a parent")
+        .to_path_buf()
+}
+
+fn mock_context() -> PageContext {
@@ cli/tests/publish.rs
@@ -0,0 +1,759 @@
+//! `pop publish` and `pop unpublish` against a real socket.
+//!
+//! The server here is deliberately small and deliberately strict: it parses
+//! the HTTP by hand, rebuilds the byte string from `server/README.md` out of
+//! the fields it received, and verifies the ed25519 signature against the
+//! public key the device registered. If pop ever signs the wrong bytes, these
+//! tests fail rather than the user finding out at publish time.
+
+mod common;
+
+use std::collections::BTreeMap;
+use std::io::{BufRead, BufReader, Read, Write};
+use std::net::{TcpListener, TcpStream};
+use std::sync::{Arc, Mutex};
+
+use common::*;
+use ed25519_dalek::{Signature, Verifier, VerifyingKey};
+use pop::preview::{Key, Scripted};
+
@@ design/README.md
@@ -40,6 +40,8 @@ Detector findings that remain are false positives: it pairs dark-theme token val
 - Domain: `pop.dev` is a placeholder in the install commands, the Plausible `data-domain` and the badge URL.
 - Contact address in the footer (`hello@pop.dev`) and the GitHub URL are placeholders.
 - Print was not visually verified: no headless Chromium produced a PDF on this machine. The print stylesheet is written for one A4 page; check it in the browser's print preview.
+- Hero (scene task): the brief lists four motions (assembly, scroll flight, pointer drift, hover fragments) and nothing for idle time, so the structure holds still once assembled and the pointer is at rest. If a slow idle drift is wanted, say so; it is a one-line change in `landing/src/components/hero/scene.ts` (`placeCamera`).
+- Hero (scene task): plane fills read a little heavier in light mode (dark on white) than in dark mode. Left as is; a per-theme fill multiplier is trivial if the light frame should be airier.
 
 ## Verification
 
@@ design/brand/DESIGN.md
@@ -0,0 +1,234 @@
+# Design System Inspired by pop
+
+Read this before building anything that carries the brand: the landing, the docs, the project page,
+the CLI output, a badge. Tokens live in `design/tokens.css` and are restated here with hex
+equivalents so an agent without a CSS parser can use them. The name `pop` is a placeholder until the
+naming study closes; nothing in this document depends on it except the wordmark files.
+
+## 1. Visual Theme & Atmosphere
+
+The product is a record that cannot be rewritten after the fact. The design is built to feel like
+that record: one ink, one paper, and every distinction carried by weight, outline and type rather
+than by colour. There is no accent colour at all. Where another developer tool would put a green
+"verified" pill, this brand sets the word in weight 600 with a 1 px outline and lets the word do the
+work. That refusal is the signature move. Remove it and the page becomes any other dark-mode CLI
+landing.
+
+The atmosphere is **drafted**: the calm of a technical drawing, white line on near-black paper.
+The hero reference (`design/inspo/hero-array.png`) is a dense wireframe megastructure; the rest of
+the page takes its line weight (1 px), its restraint and its palette, but not its density. Below the
@@ design/brand/README.md
@@ -0,0 +1,49 @@
+# Brand: pop, Proof of Process
+
+## The mark
+
+A rounded square passes through a ring, interlocked like two chain links. The square is the record
+(a commit, a note, a session); the ring is the seal (the verification badge). The interlock is the
+hash chain: each link only holds because of the one it passes through. One glyph carries all three
+ideas, which was the brief. It is stroke-only at 44/512, so in a lockup its line matches the stem of
+the wordmark, and it is name-agnostic: nothing in it spells a letter, so it survives the naming
+study. Two variants ship: `mark.svg` with the over/under crossings for 40 px and up, and
+`mark-compact.svg` (heavier stroke, no crossings) for 16–32 px, favicons and the badge. The
+terminal glyph in `terminal/banner.txt` is the same idea in box-drawing characters: two records
+linked, crossings drawn with `┼`.
+
+All assets come from `logo/build.py`. The wordmark is Schibsted Grotesk 600, lowercase, tracked
+−0.025em, outlined with HarfBuzz shaping so the kerning is the font's own. A rename is
+`python3 design/brand/logo/build.py --name <new> --descriptor "<new descriptor>"`.
+
+## Rejected directions
@@ design/brand/badge/process-recorded-with-pop.svg
@@ -0,0 +1,33 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 179.1 20" width="179" height="20" role="img" aria-label="process recorded with pop" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<title>process recorded with pop</title>
+<rect width="179.1" height="20" rx="3" fill="#090909" stroke="none"/>
+<rect x="0.5" y="0.5" width="178.1" height="19" rx="2.5" fill="none" stroke="#424242" stroke-width="1"/>
+<g stroke="#eeeeee" transform="translate(5.5 4) scale(0.02344)">
+<path d="M114 146H246A44 44 0 0 1 290 190V322A44 44 0 0 1 246 366H114A44 44 0 0 1 70 322V190A44 44 0 0 1 114 146Z" stroke-width="76"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="76"/>
+</g>
+<g fill="#eeeeee" stroke="none" transform="translate(28 12.90)">
+<path d="M0.84365314245224 2.0947265625V-5.80078125H1.8735013604164124V-4.604529440402985L1.8490947484970093 -4.689650654792786Q2.1025648713111877 -5.324256002902985 2.6377640068531036 -5.6286046504974365Q3.1729631423950195 -5.932952642440796 3.7922714948654175 -5.932952642440796Q4.507786154747009 -5.932952642440796 5.04407924413681 -5.564387559890747Q5.580372333526611 -5.195823132991791 5.878296256065369 -4.513286083936691Q6.1762208342552185 -3.830749034881592 6.1762208342552185 -2.893386960029602Q6.1762208342552185 -1.9576574563980103 5.875610709190369 -1.2782140970230103Q5.575001239776611 -0.5987707376480103 5.037075579166412 -0.23329967260360718Q4.499149918556213 0.1321713924407959 3.7849249243736267 0.1321713924407959Q3.1959981322288513 0.1321713924407959 2.652913808822632 -0.1561698615550995Q2.1098294854164124 -0.4445111155509949 1.8178149461746216 -1.119636356830597L1.917155921459198 -1.1544422507286072V2.0947265625H0.84365314245224ZM3.4505120515823364 -0.7446734309196472Q4.133505761623383 -0.7446734309196472 4.564809113740921 -1.3100174367427826Q4.9961124658584595 -1.875361442565918 4.9961124658584595 -2.918136477470398Q4.9961124658584595 -3.9335826635360718 4.579289823770523 -4.495253384113312Q4.162467181682587 -5.056924104690552 3.4612542390823364 -5.056924104690552Q3.0321220755577087 -5.056924104690552 2.6750915348529816 -4.830799549818039Q2.3180609941482544 -4.604674994945526 2.1067600548267365 -4.130876272916794Q1.8954591155052185 -3.6570775508880615 1.8954591155052185 -2.914871335029602Q1.8954591155052185 -1.817095696926117 2.349879413843155 -1.280884563922882Q2.8042997121810913 -0.7446734309196472 3.4505120515823364 -0.7446734309196472Z"/>
+<path d="M7.56546954870224 0.0V-5.80078125H8.603480622768402V-4.550393640995026L8.562274918556213 -4.698204934597015Q8.824985132217407 -5.31201171875 9.337989822626113 -5.622482180595398Q9.85099451303482 -5.932952642440796 10.3990585064888 -5.932952642440796Q10.558085367679595 -5.932952642440796 10.712320730686187 -5.906097173690796Q10.866556093692779 -5.879241704940796 11.006074056625366 -5.814788579940796L10.887093708515167 -4.825754642486572Q10.576990411281585 -4.91869580745697 10.277972207069396 -4.91869580745697Q10.073315308094024 -4.91869580745697 9.79799364566803 -4.841288328170776Q9.522671983242034 -4.7638808488845825 9.262516841888427 -4.543086409568787Q9.00236170053482 -4.322291970252991 8.82977663040161 -3.8966858983039856Q8.657191560268402 -3.4710798263549805 8.657191560268402 -2.775498926639557V0.0H7.56546954870224Z"/>
+<path d="M14.393356860876084 0.1321713924407959Q13.560742588043214 0.1321713924407959 12.925326363444329 -0.25244986824691296Q12.289910220801831 -0.6370711289346218 11.933857993483544 -1.3202445283532143Q11.577805848121644 -2.00341796875 11.577805848121644 -2.9007987678050995Q11.577805848121644 -3.798179566860199 11.933843077421189 -4.4810919761657715Q12.28988038867712 -5.164004057645798 12.925269977450371 -5.548478186130524Q13.560659648180009 -5.932952642440796 14.393239171504975 -5.932952642440796Q15.226787419319153 -5.932952642440796 15.862078578472138 -5.548511624336243Q16.49736973762512 -5.164069950580597 16.85383740186691 -4.481235891580582Q17.210305066108702 -3.7984018325805664 17.210305066108702 -2.9005017578601837Q17.210305066108702 -2.002601683139801 16.854001970291137 -1.3196894377470016Q16.49769887447357 -0.6367771103978157 15.861835004091263 -0.2523028589785099Q15.225971133708954 0.1321713924407959 14.393356860876084 0.1321713924407959ZM14.39415675520897 -0.7356942892074585Q15.15542766571045 -0.7356942892074585 15.595669181346894 -1.317711666226387Q16.035910696983336 -1.8997290432453156 16.035910696983336 -2.900390625Q16.035910696983336 -3.9054375290870667 15.595567555427552 -4.485262244939804Q15.155224741697312 -5.0650869607925415 14.393954159021378 -5.0650869607925415Q13.632683248519898 -5.0650869607925415 13.192441732883454 -4.485262244939804Q12.75220021724701 -3.9054375290870667 12.75220021724701 -2.900390625Q12.75220021724701 -1.8990822434425354 13.19254303097725 -1.317388266324997Q13.632886008620263 -0.7356942892074585 14.39415675520897 -0.7356942892074585Z"/>
+<path d="M21.226640837192537 0.1321713924407959Q20.38329716205597 0.1321713924407959 19.767923043966295 -0.2384340763092041Q19.15254892587662 -0.6090395450592041 18.819671587944033 -1.2897073328495026Q18.486794250011446 -1.970375120639801 18.486794250011446 -2.901206910610199Q18.486794250011446 -3.832854986190796 18.819263445138933 -4.512706488370895Q19.15173264026642 -5.192557990550995 19.766698615550997 -5.562754988670349Q20.381664590835573 -5.932952642440796 21.22419198036194 -5.932952642440796Q21.85621799707413 -5.932952642440796 22.367263929843904 -5.702640771865845Q22.87830986261368 -5.472328245639801 23.23447559952736 -5.035122722387314Q23.590641336441042 -4.597917199134827 23.746876435279848 -3.9726824164390564L22.665978567600252 -3.708943486213684Q22.52538336992264 -4.370191872119904 22.157202498912813 -4.7176394164562225Q21.789021627902986 -5.0650869607925415 21.222559409141542 -5.0650869607925415Q20.462264435291292 -5.0650869607925415 20.061726527214052 -4.492804527282715Q19.661188619136812 -3.920522093772888 19.661188619136812 -2.896309196949005Q19.661188619136812 -1.86835777759552 20.061318384408953 -1.3020260334014893Q20.461448149681093 -0.7356942892074585 21.22337569475174 -0.7356942892074585Q21.78201796293259 -0.7356942892074585 22.148737715482714 -1.0666856467723846Q22.51545746803284 -1.3976770043373108 22.667954044342043 -2.0375714898109436L23.732052819728853 -1.7756775617599487Q23.608044283390047 -1.3115755915641785 23.37266291856766 -0.9572138786315918Q23.13728155374527 -0.6028521656990051 22.81252609372139 -0.35992851853370667Q22.48777063369751 -0.1170048713684082 22.086938338279726 0.007583260536193848Q21.68610604286194 0.1321713924407959 21.226640837192537 0.1321713924407959Z"/>
+<path d="M27.524614217281343 0.1321713924407959Q26.69093549489975 0.1321713924407959 26.066280307769777 -0.2388422191143036Q25.441625120639802 -0.6098558306694031 25.095213832855226 -1.2905236184597015Q24.74880254507065 -1.97119140625 24.74880254507065 -2.898284673690796Q24.74880254507065 -3.827483892440796 25.09317311882973 -4.509204655885696Q25.437543692588807 -5.190925419330597 26.05536666750908 -5.561939358711243Q26.673189642429353 -5.932952642440796 27.494363131523134 -5.932952642440796Q27.961174907684327 -5.932952642440796 28.433121087551118 -5.785655379295349Q28.90506726741791 -5.638358771800995 29.2968551838398 -5.274177372455597Q29.68864310026169 -4.909995973110199 29.927183392047883 -4.271954029798508Q30.165723683834077 -3.6339120864868164 30.165723683834077 -2.656976878643036H25.450507225990297V-3.4364227652549744H29.207675161361696L28.973534944057466 -3.112736999988556Q28.94259804725647 -3.8256388902664185 28.735851916074754 -4.258574813604355Q28.529105784893037 -4.691510736942291 28.200971426963807 -4.886053562164307Q27.872837069034578 -5.080596387386322 27.470903279781343 -5.080596387386322Q26.983505370616914 -5.080596387386322 26.636572184562684 -4.821257531642914Q26.289638998508455 -4.561918675899506 26.106417956352235 -4.072888195514679Q25.923196914196016 -3.5838577151298523 25.923196914196016 -2.892097294330597Q25.923196914196016 -1.86835777759552 26.334068866968156 -1.3024341762065887Q26.744940819740297 -0.7365105748176575 27.521349074840547 -0.7365105748176575Q28.066800298690797 -0.7365105748176575 28.443682643175126 -1.0127380192279816Q28.820564987659456 -1.2889654636383057 29.002839269638063 -1.8214380741119385L30.04171514749527 -1.501882255077362Q29.83305877685547 -0.9716945886611938 29.46679568529129 -0.607986569404602Q29.100532593727113 -0.24427855014801025 28.60512656211853 -0.05605357885360718Q28.10972053050995 0.1321713924407959 27.524614217281343 0.1321713924407959Z"/>
+<path d="M33.63949702978134 0.1321713924407959Q33.00373249053955 0.1321713924407959 32.47242661118507 -0.07012614607810974Q31.941120731830598 -0.2724236845970154 31.587232398986817 -0.6891568303108215Q31.233344066143037 -1.1058899760246277 31.121367383003236 -1.7549760341644287L32.20626472234726 -1.9661632180213928Q32.26289789676666 -1.3362765908241272 32.639437335729596 -1.0261247754096985Q33.01597677469253 -0.7159729599952698 33.611139464378354 -0.7159729599952698Q34.19766591787338 -0.7159729599952698 34.530183631181714 -0.9964123070240021Q34.86270134449005 -1.2768516540527344 34.86270134449005 -1.6756507754325867Q34.86270134449005 -2.0173433423042297 34.62185938954353 -2.227762758731842Q34.38101743459701 -2.4381821751594543 33.895986425876615 -2.5245930552482605L33.239717710018155 -2.645809829235077Q32.94851945638656 -2.697888195514679 32.62971468567848 -2.791302740573883Q32.310909914970395 -2.884717285633087 32.03238834738731 -3.055776000022888Q31.75386677980423 -3.226834714412689 31.58106927871704 -3.5114374458789825Q31.408271777629853 -3.796040177345276 31.408271777629853 -4.2342334389686584Q31.408271777629853 -4.805201947689056 31.70881633758545 -5.1804032027721405Q32.00936089754104 -5.555604457855225 32.515917259454724 -5.74427855014801Q33.022473621368405 -5.932952642440796 33.62935869693756 -5.932952642440796Q34.25975214242935 -5.932952642440796 34.74207334518432 -5.740409553050995Q35.2243945479393 -5.547866463661194 35.533616322278974 -5.185554325580597Q35.84283809661865 -4.8232421875 35.942782926559445 -4.304009139537811L34.822606313228604 -4.077524960041046Q34.785008656978604 -4.376591682434082 34.656616765260694 -4.605230331420898Q34.52822487354278 -4.833868980407715 34.28037925362587 -4.962195634841919Q34.03253363370895 -5.090522289276123 33.631203699111936 -5.090522289276123Q33.155364263057706 -5.090522289276123 32.826927977800366 -4.894110202789307Q32.49849169254303 -4.69769811630249 32.49849169254303 -4.31337970495224Q32.49849169254303 -4.086030721664429 32.62383087277412 -3.9293222427368164Q32.749170053005216 -3.772613763809204 32.99192224740982 -3.6710773408412933Q33.23467444181442 -3.5695409178733826 33.58650534152984 -3.4963210821151733L34.27065824270248 -3.358991026878357Q34.72427897453308 -3.272580146789551 35.115013915300366 -3.0914240777492523Q35.505748856067655 -2.910268008708954 35.745276886224744 -2.586026906967163Q35.98480491638183 -2.2617858052253723 35.98480491638183 -1.7301779985427856Q35.98480491638183 -1.14404296875 35.68439083099365 -0.7253343462944031Q35.383976745605466 -0.30662572383880615 34.85640938878059 -0.08722716569900513Q34.328842031955716 0.1321713924407959 33.63949702978134 0.1321713924407959Z"/>
+<path d="M39.534164998531345 0.1321713924407959Q38.898400459289554 0.1321713924407959 38.36709457993508 -0.07012614607810974Q37.8357887005806 -0.2724236845970154 37.48190036773682 -0.6891568303108215Q37.12801203489304 -1.1058899760246277 37.01603535175324 -1.7549760341644287L38.10093269109726 -1.9661632180213928Q38.157565865516666 -1.3362765908241272 38.5341053044796 -1.0261247754096985Q38.91064474344254 -0.7159729599952698 39.50580743312836 -0.7159729599952698Q40.092333886623386 -0.7159729599952698 40.42485159993172 -0.9964123070240021Q40.757369313240055 -1.2768516540527344 40.757369313240055 -1.6756507754325867Q40.757369313240055 -2.0173433423042297 40.51652735829354 -2.227762758731842Q40.27568540334702 -2.4381821751594543 39.79065439462662 -2.5245930552482605L39.13438567876816 -2.645809829235077Q38.84318742513657 -2.697888195514679 38.524382654428486 -2.791302740573883Q38.2055778837204 -2.884717285633087 37.92705631613732 -3.055776000022888Q37.64853474855423 -3.226834714412689 37.475737247467045 -3.5114374458789825Q37.302939746379856 -3.796040177345276 37.302939746379856 -4.2342334389686584Q37.302939746379856 -4.805201947689056 37.60348430633545 -5.1804032027721405Q37.90402886629105 -5.555604457855225 38.41058522820473 -5.74427855014801Q38.91714159011841 -5.932952642440796 39.524026665687565 -5.932952642440796Q40.154420111179356 -5.932952642440796 40.63674131393433 -5.740409553050995Q41.119062516689304 -5.547866463661194 41.42828429102898 -5.185554325580597Q41.737506065368656 -4.8232421875 41.83745089530945 -4.304009139537811L40.71727428197861 -4.077524960041046Q40.67967662572861 -4.376591682434082 40.5512847340107 -4.605230331420898Q40.42289284229279 -4.833868980407715 40.17504722237587 -4.962195634841919Q39.92720160245896 -5.090522289276123 39.52587166786194 -5.090522289276123Q39.05003223180771 -5.090522289276123 38.72159594655037 -4.894110202789307Q38.39315966129303 -4.69769811630249 38.39315966129303 -4.31337970495224Q38.39315966129303 -4.086030721664429 38.51849884152413 -3.9293222427368164Q38.64383802175522 -3.772613763809204 38.886590216159824 -3.6710773408412933Q39.129342410564426 -3.5695409178733826 39.48117331027985 -3.4963210821151733L40.16532621145249 -3.358991026878357Q40.618946943283085 -3.272580146789551 41.00968188405037 -3.0914240777492523Q41.40041682481766 -2.910268008708954 41.63994485497475 -2.586026906967163Q41.87947288513184 -2.2617858052253723 41.87947288513184 -1.7301779985427856Q41.87947288513184 -1.14404296875 41.579058799743656 -0.7253343462944031Q41.27864471435547 -0.30662572383880615 40.7510773575306 -0.08722716569900513Q40.22351000070572 0.1321713924407959 39.534164998531345 0.1321713924407959Z"/>
+<path d="M46.09425861120224 0.0V-5.80078125H47.132269685268405V-4.550393640995026L47.091063981056216 -4.698204934597015Q47.35377419471741 -5.31201171875 47.86677888512612 -5.622482180595398Q48.37978357553482 -5.932952642440796 48.9278475689888 -5.932952642440796Q49.0868744301796 -5.932952642440796 49.24110979318619 -5.906097173690796Q49.39534515619278 -5.879241704940796 49.53486311912537 -5.814788579940796L49.41588277101517 -4.825754642486572Q49.10577947378159 -4.91869580745697 48.8067612695694 -4.91869580745697Q48.60210437059403 -4.91869580745697 48.32678270816803 -4.841288328170776Q48.05146104574204 -4.7638808488845825 47.79130590438843 -4.543086409568787Q47.53115076303482 -4.322291970252991 47.358565692901614 -3.8966858983039856Q47.185980622768405 -3.4710798263549805 47.185980622768405 -2.775498926639557V0.0H46.09425861120224Z"/>
+<path d="M52.89980952978134 0.1321713924407959Q52.06613080739975 0.1321713924407959 51.44147562026978 -0.2388422191143036Q50.8168204331398 -0.6098558306694031 50.47040914535523 -1.2905236184597015Q50.12399785757065 -1.97119140625 50.12399785757065 -2.898284673690796Q50.12399785757065 -3.827483892440796 50.46836843132973 -4.509204655885696Q50.81273900508881 -5.190925419330597 51.43056198000908 -5.561939358711243Q52.048384954929354 -5.932952642440796 52.869558444023134 -5.932952642440796Q53.33637022018433 -5.932952642440796 53.80831640005112 -5.785655379295349Q54.28026257991791 -5.638358771800995 54.6720504963398 -5.274177372455597Q55.06383841276169 -4.909995973110199 55.302378704547884 -4.271954029798508Q55.54091899633408 -3.6339120864868164 55.54091899633408 -2.656976878643036H50.8257025384903V-3.4364227652549744H54.582870473861696L54.34873025655747 -3.112736999988556Q54.31779335975647 -3.8256388902664185 54.111047228574755 -4.258574813604355Q53.90430109739304 -4.691510736942291 53.57616673946381 -4.886053562164307Q53.24803238153458 -5.080596387386322 52.84609859228134 -5.080596387386322Q52.358700683116915 -5.080596387386322 52.011767497062685 -4.821257531642914Q51.664834311008455 -4.561918675899506 51.481613268852236 -4.072888195514679Q51.298392226696016 -3.5838577151298523 51.298392226696016 -2.892097294330597Q51.298392226696016 -1.86835777759552 51.70926417946816 -1.3024341762065887Q52.1201361322403 -0.7365105748176575 52.89654438734055 -0.7365105748176575Q53.4419956111908 -0.7365105748176575 53.81887795567513 -1.0127380192279816Q54.195760300159456 -1.2889654636383057 54.37803458213806 -1.8214380741119385L55.41691045999527 -1.501882255077362Q55.20825408935547 -0.9716945886611938 54.84199099779129 -0.607986569404602Q54.475727906227114 -0.24427855014801025 53.98032187461853 -0.05605357885360718Q53.48491584300995 0.1321713924407959 52.89980952978134 0.1321713924407959Z"/>
+<path d="M59.48687521219254 0.1321713924407959Q58.64353153705597 0.1321713924407959 58.028157418966295 -0.2384340763092041Q57.41278330087662 -0.6090395450592041 57.07990596294403 -1.2897073328495026Q56.747028625011446 -1.970375120639801 56.747028625011446 -2.901206910610199Q56.747028625011446 -3.832854986190796 57.07949782013893 -4.512706488370895Q57.41196701526642 -5.192557990550995 58.026932990550996 -5.562754988670349Q58.64189896583557 -5.932952642440796 59.48442635536194 -5.932952642440796Q60.11645237207413 -5.932952642440796 60.627498304843904 -5.702640771865845Q61.13854423761368 -5.472328245639801 61.49470997452736 -5.035122722387314Q61.85087571144104 -4.597917199134827 62.00711081027985 -3.9726824164390564L60.92621294260025 -3.708943486213684Q60.78561774492264 -4.370191872119904 60.41743687391281 -4.7176394164562225Q60.049256002902986 -5.0650869607925415 59.48279378414154 -5.0650869607925415Q58.72249881029129 -5.0650869607925415 58.32196090221405 -4.492804527282715Q57.92142299413681 -3.920522093772888 57.92142299413681 -2.896309196949005Q57.92142299413681 -1.86835777759552 58.32155275940895 -1.3020260334014893Q58.72168252468109 -0.7356942892074585 59.48361006975174 -0.7356942892074585Q60.04225233793259 -0.7356942892074585 60.40897209048271 -1.0666856467723846Q60.77569184303284 -1.3976770043373108 60.92818841934204 -2.0375714898109436L61.99228719472885 -1.7756775617599487Q61.86827865839005 -1.3115755915641785 61.63289729356766 -0.9572138786315918Q61.39751592874527 -0.6028521656990051 61.07276046872139 -0.35992851853370667Q60.74800500869751 -0.1170048713684082 60.347172713279726 0.007583260536193848Q59.94634041786194 0.1321713924407959 59.48687521219254 0.1321713924407959Z"/>
@@ design/brand/badge/verified-by-pop.svg
@@ -0,0 +1,23 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 115.9 20" width="116" height="20" role="img" aria-label="verified by pop" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<title>verified by pop</title>
+<rect width="115.9" height="20" rx="3" fill="#090909" stroke="none"/>
+<rect x="0.5" y="0.5" width="114.9" height="19" rx="2.5" fill="none" stroke="#424242" stroke-width="1"/>
+<g stroke="#eeeeee" transform="translate(5.5 4) scale(0.02344)">
+<path d="M114 146H246A44 44 0 0 1 290 190V322A44 44 0 0 1 246 366H114A44 44 0 0 1 70 322V190A44 44 0 0 1 114 146Z" stroke-width="76"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="76"/>
+</g>
+<g fill="#eeeeee" stroke="none" transform="translate(28 12.90)">
+<path d="M2.448320508003235 0.0 0.26357501745224 -5.80078125H1.4764652848243713L3.2085518836975098 -0.6808575987815857H3.0432071685791016L4.765367865562439 -5.80078125H5.97825813293457L3.8026222586631775 0.0H2.448320508003235Z"/>
+<path d="M9.421664998531341 0.1321713924407959Q8.58798627614975 0.1321713924407959 7.963331089019776 -0.2388422191143036Q7.338675901889801 -0.6098558306694031 6.992264614105225 -1.2905236184597015Q6.6458533263206485 -1.97119140625 6.6458533263206485 -2.898284673690796Q6.6458533263206485 -3.827483892440796 6.9902239000797275 -4.509204655885696Q7.3345944738388065 -5.190925419330597 7.952417448759079 -5.561939358711243Q8.570240423679351 -5.932952642440796 9.391413912773132 -5.932952642440796Q9.858225688934326 -5.932952642440796 10.330171868801116 -5.785655379295349Q10.802118048667907 -5.638358771800995 11.193905965089797 -5.274177372455597Q11.585693881511688 -4.909995973110199 11.824234173297882 -4.271954029798508Q12.062774465084075 -3.6339120864868164 12.062774465084075 -2.656976878643036H7.347558007240296V-3.4364227652549744H11.104725942611694L10.870585725307464 -3.112736999988556Q10.83964882850647 -3.8256388902664185 10.632902697324752 -4.258574813604355Q10.426156566143035 -4.691510736942291 10.098022208213806 -4.886053562164307Q9.769887850284576 -5.080596387386322 9.367954061031341 -5.080596387386322Q8.880556151866912 -5.080596387386322 8.533622965812683 -4.821257531642914Q8.186689779758453 -4.561918675899506 8.003468737602233 -4.072888195514679Q7.820247695446015 -3.5838577151298523 7.820247695446015 -2.892097294330597Q7.820247695446015 -1.86835777759552 8.231119648218154 -1.3024341762065887Q8.641991600990295 -0.7365105748176575 9.418399856090545 -0.7365105748176575Q9.963851079940795 -0.7365105748176575 10.340733424425125 -1.0127380192279816Q10.717615768909454 -1.2889654636383057 10.899890050888061 -1.8214380741119385L11.93876592874527 -1.501882255077362Q11.730109558105468 -0.9716945886611938 11.36384646654129 -0.607986569404602Q10.997583374977111 -0.24427855014801025 10.50217734336853 -0.05605357885360718Q10.006771311759948 0.1321713924407959 9.421664998531341 0.1321713924407959Z"/>
+<path d="M13.47625079870224 0.0V-5.80078125H14.514261872768403V-4.550393640995026L14.473056168556214 -4.698204934597015Q14.735766382217408 -5.31201171875 15.248771072626115 -5.622482180595398Q15.761775763034821 -5.932952642440796 16.3098397564888 -5.932952642440796Q16.468866617679595 -5.932952642440796 16.623101980686187 -5.906097173690796Q16.77733734369278 -5.879241704940796 16.916855306625365 -5.814788579940796L16.797874958515166 -4.825754642486572Q16.487771661281585 -4.91869580745697 16.188753457069396 -4.91869580745697Q15.984096558094025 -4.91869580745697 15.70877489566803 -4.841288328170776Q15.433453233242036 -4.7638808488845825 15.173298091888428 -4.543086409568787Q14.913142950534821 -4.322291970252991 14.740557880401612 -3.8966858983039856Q14.567972810268403 -3.4710798263549805 14.567972810268403 -2.775498926639557V0.0H13.47625079870224Z"/>
+<path d="M18.065742986202242 0.0V-5.80078125H19.157464997768404V0.0H18.065742986202242ZM18.611603991985323 -6.697034657001495Q18.25436856031418 -6.697034657001495 18.015379147529604 -6.936024069786072Q17.776389734745027 -7.175013482570648 17.776389734745027 -7.532248914241791Q17.776389734745027 -7.894039154052734 18.015379147529604 -8.130751490592957Q18.25436856031418 -8.367463171482086 18.611603991985323 -8.367463171482086Q18.973394231796266 -8.367463171482086 19.210106240510942 -8.130751490592957Q19.44681824922562 -7.894039154052734 19.44681824922562 -7.532248914241791Q19.44681824922562 -7.175013482570648 19.210106240510942 -6.936024069786072Q18.973394231796266 -6.697034657001495 18.611603991985323 -6.697034657001495Z"/>
+<path d="M21.306878211498262 0.0V-6.802089631557465Q21.306878211498262 -7.47158282995224 21.62935626745224 -7.764111399650574Q21.95183432340622 -8.056640625 22.69686573982239 -8.056640625H23.537059667110444V-7.1904075145721436H22.69448375940323Q22.53253466129303 -7.1904075145721436 22.465567442178727 -7.1222158670425415Q22.398600223064424 -7.0540242195129395 22.398600223064424 -6.8896262645721436V0.0H21.306878211498262ZM20.46825325727463 -4.9345481395721436V-5.80078125H23.641264917850496V-4.9345481395721436H20.46825325727463ZM24.754631938934327 0.0V-5.80078125H25.84635395050049V0.0H24.754631938934327ZM25.30049294471741 -6.697034657001495Q24.943257513046266 -6.697034657001495 24.70426810026169 -6.936024069786072Q24.465278687477113 -7.175013482570648 24.465278687477113 -7.532248914241791Q24.465278687477113 -7.894039154052734 24.70426810026169 -8.130751490592957Q24.943257513046266 -8.367463171482086 25.30049294471741 -8.367463171482086Q25.662283184528352 -8.367463171482086 25.89899486541748 -8.130751490592957Q26.135707201957704 -7.894039154052734 26.135707201957704 -7.532248914241791Q26.135707201957704 -7.175013482570648 25.89899486541748 -6.936024069786072Q25.662283184528352 -6.697034657001495 25.30049294471741 -6.697034657001495Z"/>
+<path d="M30.046235311031342 0.1321713924407959Q29.21255658864975 0.1321713924407959 28.587901401519776 -0.2388422191143036Q27.9632462143898 -0.6098558306694031 27.616834926605225 -1.2905236184597015Q27.27042363882065 -1.97119140625 27.27042363882065 -2.898284673690796Q27.27042363882065 -3.827483892440796 27.614794212579728 -4.509204655885696Q27.959164786338807 -5.190925419330597 28.57698776125908 -5.561939358711243Q29.194810736179353 -5.932952642440796 30.015984225273133 -5.932952642440796Q30.482796001434327 -5.932952642440796 30.954742181301118 -5.785655379295349Q31.42668836116791 -5.638358771800995 31.8184762775898 -5.274177372455597Q32.210264194011685 -4.909995973110199 32.44880448579788 -4.271954029798508Q32.68734477758407 -3.6339120864868164 32.68734477758407 -2.656976878643036H27.972128319740296V-3.4364227652549744H31.729296255111695L31.495156037807465 -3.112736999988556Q31.46421914100647 -3.8256388902664185 31.257473009824754 -4.258574813604355Q31.050726878643037 -4.691510736942291 30.722592520713807 -4.886053562164307Q30.394458162784577 -5.080596387386322 29.992524373531342 -5.080596387386322Q29.505126464366914 -5.080596387386322 29.158193278312684 -4.821257531642914Q28.811260092258454 -4.561918675899506 28.628039050102235 -4.072888195514679Q28.444818007946015 -3.5838577151298523 28.444818007946015 -2.892097294330597Q28.444818007946015 -1.86835777759552 28.855689960718156 -1.3024341762065887Q29.266561913490296 -0.7365105748176575 30.042970168590546 -0.7365105748176575Q30.588421392440797 -0.7365105748176575 30.965303736925126 -1.0127380192279816Q31.342186081409455 -1.2889654636383057 31.524460363388062 -1.8214380741119385L32.56333624124527 -1.501882255077362Q32.354679870605466 -0.9716945886611938 31.98841677904129 -0.607986569404602Q31.622153687477113 -0.24427855014801025 31.12674765586853 -0.05605357885360718Q30.63134162425995 0.1321713924407959 30.046235311031342 0.1321713924407959Z"/>
+<path d="M36.28848883867264 0.1321713924407959Q35.57834527254105 0.1321713924407959 35.03936663627625 -0.23680150508880615Q34.50038800001145 -0.6057744026184082 34.202463749647144 -1.2883114516735077Q33.90453949928284 -1.9708485007286072 33.90453949928284 -2.907394289970398Q33.90453949928284 -3.8439400792121887 34.205149296522144 -4.522975295782089Q34.50575909376145 -5.20201051235199 35.04327661156655 -5.567481577396393Q35.58079412937165 -5.932952642440796 36.29501912355423 -5.932952642440796Q36.88394591569901 -5.932952642440796 37.42743838191033 -5.64501953125Q37.97093084812165 -5.357086420059204 38.26294538736344 -4.681144893169403L38.14456889390946 -4.647155284881592V-8.056640625H39.23629090547562V0.0H38.20644268751145V-1.1970680952072144L38.23084929943085 -1.1119468808174133Q37.97982803344727 -0.4797903895378113 37.44462889790535 -0.1738094985485077Q36.90942976236344 0.1321713924407959 36.28848883867264 0.1321713924407959ZM36.618689808845524 -0.7446734309196472Q37.05319306612015 -0.7446734309196472 37.40794620275498 -0.97079798579216Q37.762699339389805 -1.1969225406646729 37.97400027871132 -1.6729986667633057Q38.18530121803284 -2.1490747928619385 38.18530121803284 -2.885909914970398Q38.18530121803284 -3.988240361213684 37.730880919694904 -4.522582232952118Q37.27646062135697 -5.056924104690552 36.63024828195572 -5.056924104690552Q35.948070857524876 -5.056924104690552 35.51635936260224 -4.491580098867416Q35.0846478676796 -3.926236093044281 35.0846478676796 -2.882644772529602Q35.0846478676796 -1.8688311576843262 35.501470509767536 -1.3067522943019867Q35.91829315185547 -0.7446734309196472 36.618689808845524 -0.7446734309196472Z"/>
+<path d="M46.74744445562363 0.1321713924407959Q46.158517663478854 0.1321713924407959 45.615433340072634 -0.1561698615550995Q45.072349016666415 -0.4445111155509949 44.780334477424624 -1.119636356830597L44.836837177276614 -1.1544422507286072V0.0H43.80698895931244V-8.056640625H44.898710970878604V-4.604529440402985L44.81161427974701 -4.689650654792786Q45.06508440256119 -5.324256002902985 45.600283538103106 -5.6286046504974365Q46.13548267364502 -5.932952642440796 46.75479102611542 -5.932952642440796Q47.47030568599701 -5.932952642440796 48.00659877538681 -5.564387559890747Q48.542891864776614 -5.195823132991791 48.84081578731537 -4.513286083936691Q49.13874036550522 -3.830749034881592 49.13874036550522 -2.893386960029602Q49.13874036550522 -1.9576574563980103 48.83813024044037 -1.2782140970230103Q48.537520771026614 -0.5987707376480103 47.999595110416415 -0.23329967260360718Q47.461669449806216 0.1321713924407959 46.74744445562363 0.1321713924407959ZM46.41303158283234 -0.7446734309196472Q47.096025292873385 -0.7446734309196472 47.527328644990924 -1.3100174367427826Q47.95863199710846 -1.875361442565918 47.95863199710846 -2.918136477470398Q47.95863199710846 -3.9335826635360718 47.541809355020526 -4.495253384113312Q47.12498671293259 -5.056924104690552 46.42377377033234 -5.056924104690552Q45.99464160680771 -5.056924104690552 45.637611066102984 -4.830799549818039Q45.28058052539826 -4.604674994945526 45.06927958607674 -4.130876272916794Q44.85797864675522 -3.6570775508880615 44.85797864675522 -2.914871335029602Q44.85797864675522 -1.817095696926117 45.31239894509316 -1.280884563922882Q45.766819243431094 -0.7446734309196472 46.41303158283234 -0.7446734309196472Z"/>
+<path d="M50.726067175865175 2.0947265625V1.2284934520721436H51.63304769039154Q51.734625091552736 1.2284934520721436 51.789152314662935 1.170399159193039Q51.843679537773134 1.1123048663139343 51.892019381523134 0.9804763793945312L52.277496984004976 -0.11279296875L52.186188390254976 0.3464932441711426L49.80207513809204 -5.80078125H51.024075021743776L52.777042140960695 -0.6353095173835754L52.58892338514328 -0.6763027906417847L54.3365194106102 -5.80078125H55.54940967798233L52.80207096576691 1.3615630865097046Q52.66315685749054 1.7222760915756226 52.36521588802338 1.9085013270378113Q52.067274918556215 2.0947265625 51.71927957773209 2.0947265625H50.726067175865175Z"/>
+<path d="M59.50824298620224 2.0947265625V-5.80078125H60.53809120416641V-4.604529440402985L60.51368459224701 -4.689650654792786Q60.76715471506119 -5.324256002902985 61.302353850603104 -5.6286046504974365Q61.83755298614502 -5.932952642440796 62.45686133861542 -5.932952642440796Q63.17237599849701 -5.932952642440796 63.70866908788681 -5.564387559890747Q64.24496217727662 -5.195823132991791 64.54288609981538 -4.513286083936691Q64.84081067800523 -3.830749034881592 64.84081067800523 -2.893386960029602Q64.84081067800523 -1.9576574563980103 64.54020055294038 -1.2782140970230103Q64.23959108352662 -0.5987707376480103 63.70166542291641 -0.23329967260360718Q63.163739762306214 0.1321713924407959 62.44951476812363 0.1321713924407959Q61.86058797597885 0.1321713924407959 61.31750365257263 -0.1561698615550995Q60.77441932916641 -0.4445111155509949 60.48240478992462 -1.119636356830597L60.5817457652092 -1.1544422507286072V2.0947265625H59.50824298620224ZM62.11510189533234 -0.7446734309196472Q62.79809560537338 -0.7446734309196472 63.22939895749092 -1.3100174367427826Q63.66070230960846 -1.875361442565918 63.66070230960846 -2.918136477470398Q63.66070230960846 -3.9335826635360718 63.243879667520524 -4.495253384113312Q62.82705702543259 -5.056924104690552 62.12584408283234 -5.056924104690552Q61.69671191930771 -5.056924104690552 61.33968137860298 -4.830799549818039Q60.982650837898255 -4.604674994945526 60.77134989857674 -4.130876272916794Q60.56004895925522 -3.6570775508880615 60.56004895925522 -2.914871335029602Q60.56004895925522 -1.817095696926117 61.014469257593156 -1.280884563922882Q61.46888955593109 -0.7446734309196472 62.11510189533234 -0.7446734309196472Z"/>
@@ design/brand/fonts/OFL-GeistMono.txt
@@ -0,0 +1,93 @@
+Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
@@ design/brand/fonts/OFL-InstrumentSans.txt
@@ -0,0 +1,93 @@
+Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
@@ design/brand/fonts/OFL-SchibstedGrotesk.txt
@@ -0,0 +1,93 @@
+Copyright 2023 The Schibsted-Grotesk Project Authors (https://github.com/schibsted/schibsted-grotesk)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
@@ design/brand/guidelines.html
@@ -0,0 +1,630 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>pop brand guidelines</title>
+<meta name="description" content="Mark, wordmark, palette, type, voice and usage rules for pop, Proof of Process.">
+<link rel="icon" href="logo/favicon.svg" type="image/svg+xml">
+<style>
+@font-face { font-family: "Schibsted Grotesk"; src: url("fonts/SchibstedGrotesk[wght].ttf") format("truetype"); font-weight: 400 900; font-display: swap; }
+@font-face { font-family: "Geist Mono"; src: url("fonts/GeistMono[wght].ttf") format("truetype"); font-weight: 100 900; font-display: swap; }
+
+:root {
+  color-scheme: light dark;
+  --font-text: "Schibsted Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
+  --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
+
+  --text-xs: 0.75rem; --text-sm: 0.875rem;
+  --text-base: clamp(1rem, 0.96rem + 0.2vw, 1.0625rem);
@@ design/brand/logo/build.py
@@ -0,0 +1,289 @@
+#!/usr/bin/env python3
+"""Build every logo, favicon, OG and badge asset for the brand.
+
+    python3 design/brand/logo/build.py [--name pop] [--descriptor "Proof of Process"]
+
+Dependencies: fonttools, uharfbuzz (pip), resvg (~/.cargo/bin/resvg), Pillow.
+The mark is name-agnostic. Only the wordmark, lockups, OG image and badges use NAME,
+so a rename is one flag and a re-run.
+"""
+import argparse, math, os, subprocess, sys, tempfile
+
+from fontTools.pens.svgPathPen import SVGPathPen
+from fontTools.pens.transformPen import TransformPen
+import uharfbuzz as hb
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+BRAND = os.path.dirname(HERE)
+FONTS = os.path.join(BRAND, "fonts")
+BADGE = os.path.join(BRAND, "badge")
@@ design/brand/logo/favicon.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<style>svg{color:#0f0f0f}@media(prefers-color-scheme:dark){svg{color:#eeeeee}}</style>
+<path d="M114 146H246A44 44 0 0 1 290 190V322A44 44 0 0 1 246 366H114A44 44 0 0 1 70 322V190A44 44 0 0 1 114 146Z" stroke-width="64"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="64"/>
+</svg>
@@ design/brand/logo/lockup-horizontal-dark.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3690 1285" color="#eeeeee" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<rect x="0" y="0" width="3689.78" height="1285.16" fill="#090909" stroke="none"/>
+<g transform="translate(139.6 -75.0) scale(2.2995)">
+<defs><clipPath id="lhac"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="lha" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lhac)" d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" fill="none" stroke="#000" stroke-width="92"/></mask><clipPath id="lhbc"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="lhb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lhbc)" d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" fill="none" stroke="#000" stroke-width="92"/></mask></defs>
+<path d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" stroke-width="44" mask="url(#lha)"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="44" mask="url(#lhb)"/>
+</g>
+<g fill="currentColor" stroke="none" transform="translate(1641.6 777.3)">
+<path d="M73.03917407989502 190.4296875V-527.34375H187.4760389328003V-419.99995708465576L185.53853034973145 -427.6444911956787Q210.5501890182495 -485.42964458465576 257.7514052391052 -512.582004070282Q304.95262145996094 -539.7343635559082 361.44089698791504 -539.7343635559082Q427.23774909973145 -539.7343635559082 475.8979082107544 -506.46287202835083Q524.5580673217773 -473.19138050079346 551.3139367103577 -411.09567880630493Q578.069806098938 -348.9999771118164 578.069806098938 -262.8476619720459Q578.069806098938 -177.0312786102295 551.0697960853577 -115.26370048522949Q524.0697860717773 -53.49612236022949 475.0736951828003 -20.552754402160645Q426.07760429382324 12.390613555908203 359.9292039871216 12.390613555908203Q304.14021015167236 12.390613555908203 256.5503120422363 -14.244139194488525Q208.9604139328003 -40.878891944885254 181.56981468200684 -102.06639766693115L192.757248878479 -104.29298877716064V190.4296875H73.03917407989502ZM319.49591636657715 -81.66754245758057Q377.36713886260986 -81.66754245758057 413.06060552597046 -129.82779741287231Q448.75407218933105 -177.98805236816406 448.75407218933105 -265.4726505279541Q448.75407218933105 -351.03535652160645 414.18951749801636 -398.43976497650146Q379.62496280670166 -445.8441734313965 320.47247886657715 -445.8441734313965Q283.9919328689575 -445.8441734313965 254.01920080184937 -426.4593720436096Q224.0464687347412 -407.07457065582275 206.4780592918396 -366.95533990859985Q188.909649848938 -326.83610916137695 188.909649848938 -264.8007869720459Q188.909649848938 -172.7849245071411 226.89217329025269 -127.22623348236084Q264.8746967315674 -81.66754245758057 319.49591636657715 -81.66754245758057Z"/>
+<path d="M910.8639717102051 12.390613555908203Q833.3101332187653 12.390613555908203 774.1243004798889 -22.642498835921288Q714.9384528398514 -57.67561122775078 681.7739740014076 -119.90226060152054Q648.6094951629639 -182.12890625 648.6094951629639 -263.7558579444885Q648.6094951629639 -345.38280963897705 681.7709118127823 -407.5557291507721Q714.9323284626007 -469.72864866256714 774.1127073764801 -504.73153591156006Q833.2930713891983 -539.7343635559082 910.8397722244263 -539.7343635559082Q988.5858058929443 -539.7343635559082 1047.7459192276 -504.7382712364197Q1106.9060325622559 -469.74217891693115 1140.1560068130493 -407.58535265922546Q1173.4059810638428 -345.4285264015198 1173.4059810638428 -263.69473338127136Q1173.4059810638428 -181.96094036102295 1140.1898622512817 -119.7880208492279Q1106.973773241043 -57.61510878801346 1047.695791721344 -22.612247616052628Q988.4178400039673 12.390613555908203 910.8639717102051 12.390613555908203ZM911.0285997390747 -79.81991767883301Q973.2852935791016 -79.81991767883301 1009.2756509780884 -129.25180792808533Q1045.2660083770752 -178.68371307849884 1045.2660083770752 -263.671875Q1045.2660083770752 -348.9454984664917 1009.254789352417 -398.23466539382935Q973.2435703277588 -447.523832321167 910.9868764877319 -447.523832321167Q848.7301826477051 -447.523832321167 812.7398252487183 -398.23466539382935Q776.7494678497314 -348.9454984664917 776.7494678497314 -263.671875Q776.7494678497314 -178.5506010055542 812.7606868743896 -129.1852593421936Q848.7719058990479 -79.81991767883301 911.0285997390747 -79.81991767883301Z"/>
+<path d="M1257.414174079895 190.4296875V-527.34375H1371.8510389328003V-419.99995708465576L1369.9135303497314 -427.6444911956787Q1394.9251890182495 -485.42964458465576 1442.1264052391052 -512.582004070282Q1489.327621459961 -539.7343635559082 1545.815896987915 -539.7343635559082Q1611.6127490997314 -539.7343635559082 1660.2729082107544 -506.46287202835083Q1708.9330673217773 -473.19138050079346 1735.6889367103577 -411.09567880630493Q1762.444806098938 -348.9999771118164 1762.444806098938 -262.8476619720459Q1762.444806098938 -177.0312786102295 1735.4447960853577 -115.26370048522949Q1708.4447860717773 -53.49612236022949 1659.4486951828003 -20.552754402160645Q1610.4526042938232 12.390613555908203 1544.3042039871216 12.390613555908203Q1488.5152101516724 12.390613555908203 1440.9253120422363 -14.244139194488525Q1393.3354139328003 -40.878891944885254 1365.9448146820068 -102.06639766693115L1377.132248878479 -104.29298877716064V190.4296875H1257.414174079895ZM1503.8709163665771 -81.66754245758057Q1561.7421388626099 -81.66754245758057 1597.4356055259705 -129.82779741287231Q1633.129072189331 -177.98805236816406 1633.129072189331 -265.4726505279541Q1633.129072189331 -351.03535652160645 1598.5645174980164 -398.43976497650146Q1563.9999628067017 -445.8441734313965 1504.8474788665771 -445.8441734313965Q1468.3669328689575 -445.8441734313965 1438.3942008018494 -426.4593720436096Q1408.4214687347412 -407.07457065582275 1390.8530592918396 -366.95533990859985Q1373.284649848938 -326.83610916137695 1373.284649848938 -264.8007869720459Q1373.284649848938 -172.7849245071411 1411.2671732902527 -127.22623348236084Q1449.2496967315674 -81.66754245758057 1503.8709163665771 -81.66754245758057Z"/>
+</g>
+</svg>
@@ design/brand/logo/lockup-horizontal-light.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3690 1285" color="#0f0f0f" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<rect x="0" y="0" width="3689.78" height="1285.16" fill="#fcfcfc" stroke="none"/>
+<g transform="translate(139.6 -75.0) scale(2.2995)">
+<defs><clipPath id="lhac"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="lha" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lhac)" d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" fill="none" stroke="#000" stroke-width="92"/></mask><clipPath id="lhbc"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="lhb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lhbc)" d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" fill="none" stroke="#000" stroke-width="92"/></mask></defs>
+<path d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" stroke-width="44" mask="url(#lha)"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="44" mask="url(#lhb)"/>
+</g>
+<g fill="currentColor" stroke="none" transform="translate(1641.6 777.3)">
+<path d="M73.03917407989502 190.4296875V-527.34375H187.4760389328003V-419.99995708465576L185.53853034973145 -427.6444911956787Q210.5501890182495 -485.42964458465576 257.7514052391052 -512.582004070282Q304.95262145996094 -539.7343635559082 361.44089698791504 -539.7343635559082Q427.23774909973145 -539.7343635559082 475.8979082107544 -506.46287202835083Q524.5580673217773 -473.19138050079346 551.3139367103577 -411.09567880630493Q578.069806098938 -348.9999771118164 578.069806098938 -262.8476619720459Q578.069806098938 -177.0312786102295 551.0697960853577 -115.26370048522949Q524.0697860717773 -53.49612236022949 475.0736951828003 -20.552754402160645Q426.07760429382324 12.390613555908203 359.9292039871216 12.390613555908203Q304.14021015167236 12.390613555908203 256.5503120422363 -14.244139194488525Q208.9604139328003 -40.878891944885254 181.56981468200684 -102.06639766693115L192.757248878479 -104.29298877716064V190.4296875H73.03917407989502ZM319.49591636657715 -81.66754245758057Q377.36713886260986 -81.66754245758057 413.06060552597046 -129.82779741287231Q448.75407218933105 -177.98805236816406 448.75407218933105 -265.4726505279541Q448.75407218933105 -351.03535652160645 414.18951749801636 -398.43976497650146Q379.62496280670166 -445.8441734313965 320.47247886657715 -445.8441734313965Q283.9919328689575 -445.8441734313965 254.01920080184937 -426.4593720436096Q224.0464687347412 -407.07457065582275 206.4780592918396 -366.95533990859985Q188.909649848938 -326.83610916137695 188.909649848938 -264.8007869720459Q188.909649848938 -172.7849245071411 226.89217329025269 -127.22623348236084Q264.8746967315674 -81.66754245758057 319.49591636657715 -81.66754245758057Z"/>
+<path d="M910.8639717102051 12.390613555908203Q833.3101332187653 12.390613555908203 774.1243004798889 -22.642498835921288Q714.9384528398514 -57.67561122775078 681.7739740014076 -119.90226060152054Q648.6094951629639 -182.12890625 648.6094951629639 -263.7558579444885Q648.6094951629639 -345.38280963897705 681.7709118127823 -407.5557291507721Q714.9323284626007 -469.72864866256714 774.1127073764801 -504.73153591156006Q833.2930713891983 -539.7343635559082 910.8397722244263 -539.7343635559082Q988.5858058929443 -539.7343635559082 1047.7459192276 -504.7382712364197Q1106.9060325622559 -469.74217891693115 1140.1560068130493 -407.58535265922546Q1173.4059810638428 -345.4285264015198 1173.4059810638428 -263.69473338127136Q1173.4059810638428 -181.96094036102295 1140.1898622512817 -119.7880208492279Q1106.973773241043 -57.61510878801346 1047.695791721344 -22.612247616052628Q988.4178400039673 12.390613555908203 910.8639717102051 12.390613555908203ZM911.0285997390747 -79.81991767883301Q973.2852935791016 -79.81991767883301 1009.2756509780884 -129.25180792808533Q1045.2660083770752 -178.68371307849884 1045.2660083770752 -263.671875Q1045.2660083770752 -348.9454984664917 1009.254789352417 -398.23466539382935Q973.2435703277588 -447.523832321167 910.9868764877319 -447.523832321167Q848.7301826477051 -447.523832321167 812.7398252487183 -398.23466539382935Q776.7494678497314 -348.9454984664917 776.7494678497314 -263.671875Q776.7494678497314 -178.5506010055542 812.7606868743896 -129.1852593421936Q848.7719058990479 -79.81991767883301 911.0285997390747 -79.81991767883301Z"/>
+<path d="M1257.414174079895 190.4296875V-527.34375H1371.8510389328003V-419.99995708465576L1369.9135303497314 -427.6444911956787Q1394.9251890182495 -485.42964458465576 1442.1264052391052 -512.582004070282Q1489.327621459961 -539.7343635559082 1545.815896987915 -539.7343635559082Q1611.6127490997314 -539.7343635559082 1660.2729082107544 -506.46287202835083Q1708.9330673217773 -473.19138050079346 1735.6889367103577 -411.09567880630493Q1762.444806098938 -348.9999771118164 1762.444806098938 -262.8476619720459Q1762.444806098938 -177.0312786102295 1735.4447960853577 -115.26370048522949Q1708.4447860717773 -53.49612236022949 1659.4486951828003 -20.552754402160645Q1610.4526042938232 12.390613555908203 1544.3042039871216 12.390613555908203Q1488.5152101516724 12.390613555908203 1440.9253120422363 -14.244139194488525Q1393.3354139328003 -40.878891944885254 1365.9448146820068 -102.06639766693115L1377.132248878479 -104.29298877716064V190.4296875H1257.414174079895ZM1503.8709163665771 -81.66754245758057Q1561.7421388626099 -81.66754245758057 1597.4356055259705 -129.82779741287231Q1633.129072189331 -177.98805236816406 1633.129072189331 -265.4726505279541Q1633.129072189331 -351.03535652160645 1598.5645174980164 -398.43976497650146Q1563.9999628067017 -445.8441734313965 1504.8474788665771 -445.8441734313965Q1468.3669328689575 -445.8441734313965 1438.3942008018494 -426.4593720436096Q1408.4214687347412 -407.07457065582275 1390.8530592918396 -366.95533990859985Q1373.284649848938 -326.83610916137695 1373.284649848938 -264.8007869720459Q1373.284649848938 -172.7849245071411 1411.2671732902527 -127.22623348236084Q1449.2496967315674 -81.66754245758057 1503.8709163665771 -81.66754245758057Z"/>
+</g>
+</svg>
@@ design/brand/logo/lockup-horizontal.svg
@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3690 1285" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<g transform="translate(139.6 -75.0) scale(2.2995)">
+<defs><clipPath id="lhac"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="lha" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lhac)" d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" fill="none" stroke="#000" stroke-width="92"/></mask><clipPath id="lhbc"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="lhb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lhbc)" d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" fill="none" stroke="#000" stroke-width="92"/></mask></defs>
+<path d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" stroke-width="44" mask="url(#lha)"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="44" mask="url(#lhb)"/>
+</g>
+<g fill="currentColor" stroke="none" transform="translate(1641.6 777.3)">
+<path d="M73.03917407989502 190.4296875V-527.34375H187.4760389328003V-419.99995708465576L185.53853034973145 -427.6444911956787Q210.5501890182495 -485.42964458465576 257.7514052391052 -512.582004070282Q304.95262145996094 -539.7343635559082 361.44089698791504 -539.7343635559082Q427.23774909973145 -539.7343635559082 475.8979082107544 -506.46287202835083Q524.5580673217773 -473.19138050079346 551.3139367103577 -411.09567880630493Q578.069806098938 -348.9999771118164 578.069806098938 -262.8476619720459Q578.069806098938 -177.0312786102295 551.0697960853577 -115.26370048522949Q524.0697860717773 -53.49612236022949 475.0736951828003 -20.552754402160645Q426.07760429382324 12.390613555908203 359.9292039871216 12.390613555908203Q304.14021015167236 12.390613555908203 256.5503120422363 -14.244139194488525Q208.9604139328003 -40.878891944885254 181.56981468200684 -102.06639766693115L192.757248878479 -104.29298877716064V190.4296875H73.03917407989502ZM319.49591636657715 -81.66754245758057Q377.36713886260986 -81.66754245758057 413.06060552597046 -129.82779741287231Q448.75407218933105 -177.98805236816406 448.75407218933105 -265.4726505279541Q448.75407218933105 -351.03535652160645 414.18951749801636 -398.43976497650146Q379.62496280670166 -445.8441734313965 320.47247886657715 -445.8441734313965Q283.9919328689575 -445.8441734313965 254.01920080184937 -426.4593720436096Q224.0464687347412 -407.07457065582275 206.4780592918396 -366.95533990859985Q188.909649848938 -326.83610916137695 188.909649848938 -264.8007869720459Q188.909649848938 -172.7849245071411 226.89217329025269 -127.22623348236084Q264.8746967315674 -81.66754245758057 319.49591636657715 -81.66754245758057Z"/>
+<path d="M910.8639717102051 12.390613555908203Q833.3101332187653 12.390613555908203 774.1243004798889 -22.642498835921288Q714.9384528398514 -57.67561122775078 681.7739740014076 -119.90226060152054Q648.6094951629639 -182.12890625 648.6094951629639 -263.7558579444885Q648.6094951629639 -345.38280963897705 681.7709118127823 -407.5557291507721Q714.9323284626007 -469.72864866256714 774.1127073764801 -504.73153591156006Q833.2930713891983 -539.7343635559082 910.8397722244263 -539.7343635559082Q988.5858058929443 -539.7343635559082 1047.7459192276 -504.7382712364197Q1106.9060325622559 -469.74217891693115 1140.1560068130493 -407.58535265922546Q1173.4059810638428 -345.4285264015198 1173.4059810638428 -263.69473338127136Q1173.4059810638428 -181.96094036102295 1140.1898622512817 -119.7880208492279Q1106.973773241043 -57.61510878801346 1047.695791721344 -22.612247616052628Q988.4178400039673 12.390613555908203 910.8639717102051 12.390613555908203ZM911.0285997390747 -79.81991767883301Q973.2852935791016 -79.81991767883301 1009.2756509780884 -129.25180792808533Q1045.2660083770752 -178.68371307849884 1045.2660083770752 -263.671875Q1045.2660083770752 -348.9454984664917 1009.254789352417 -398.23466539382935Q973.2435703277588 -447.523832321167 910.9868764877319 -447.523832321167Q848.7301826477051 -447.523832321167 812.7398252487183 -398.23466539382935Q776.7494678497314 -348.9454984664917 776.7494678497314 -263.671875Q776.7494678497314 -178.5506010055542 812.7606868743896 -129.1852593421936Q848.7719058990479 -79.81991767883301 911.0285997390747 -79.81991767883301Z"/>
+<path d="M1257.414174079895 190.4296875V-527.34375H1371.8510389328003V-419.99995708465576L1369.9135303497314 -427.6444911956787Q1394.9251890182495 -485.42964458465576 1442.1264052391052 -512.582004070282Q1489.327621459961 -539.7343635559082 1545.815896987915 -539.7343635559082Q1611.6127490997314 -539.7343635559082 1660.2729082107544 -506.46287202835083Q1708.9330673217773 -473.19138050079346 1735.6889367103577 -411.09567880630493Q1762.444806098938 -348.9999771118164 1762.444806098938 -262.8476619720459Q1762.444806098938 -177.0312786102295 1735.4447960853577 -115.26370048522949Q1708.4447860717773 -53.49612236022949 1659.4486951828003 -20.552754402160645Q1610.4526042938232 12.390613555908203 1544.3042039871216 12.390613555908203Q1488.5152101516724 12.390613555908203 1440.9253120422363 -14.244139194488525Q1393.3354139328003 -40.878891944885254 1365.9448146820068 -102.06639766693115L1377.132248878479 -104.29298877716064V190.4296875H1257.414174079895ZM1503.8709163665771 -81.66754245758057Q1561.7421388626099 -81.66754245758057 1597.4356055259705 -129.82779741287231Q1633.129072189331 -177.98805236816406 1633.129072189331 -265.4726505279541Q1633.129072189331 -351.03535652160645 1598.5645174980164 -398.43976497650146Q1563.9999628067017 -445.8441734313965 1504.8474788665771 -445.8441734313965Q1468.3669328689575 -445.8441734313965 1438.3942008018494 -426.4593720436096Q1408.4214687347412 -407.07457065582275 1390.8530592918396 -366.95533990859985Q1373.284649848938 -326.83610916137695 1373.284649848938 -264.8007869720459Q1373.284649848938 -172.7849245071411 1411.2671732902527 -127.22623348236084Q1449.2496967315674 -81.66754245758057 1503.8709163665771 -81.66754245758057Z"/>
+</g>
+</svg>
@@ design/brand/logo/lockup-stacked-dark.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2298 2814" color="#eeeeee" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<rect x="0" y="0" width="2298.14" height="2814.45" fill="#090909" stroke="none"/>
+<g transform="translate(285.7 -33.3) scale(3.3725)">
+<defs><clipPath id="lsac"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="lsa" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lsac)" d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" fill="none" stroke="#000" stroke-width="92"/></mask><clipPath id="lsbc"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="lsb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lsbc)" d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" fill="none" stroke="#000" stroke-width="92"/></mask></defs>
+<path d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" stroke-width="44" mask="url(#lsa)"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="44" mask="url(#lsb)"/>
+</g>
+<g fill="currentColor" stroke="none" transform="translate(250.0 2306.6)">
+<path d="M73.03917407989502 190.4296875V-527.34375H187.4760389328003V-419.99995708465576L185.53853034973145 -427.6444911956787Q210.5501890182495 -485.42964458465576 257.7514052391052 -512.582004070282Q304.95262145996094 -539.7343635559082 361.44089698791504 -539.7343635559082Q427.23774909973145 -539.7343635559082 475.8979082107544 -506.46287202835083Q524.5580673217773 -473.19138050079346 551.3139367103577 -411.09567880630493Q578.069806098938 -348.9999771118164 578.069806098938 -262.8476619720459Q578.069806098938 -177.0312786102295 551.0697960853577 -115.26370048522949Q524.0697860717773 -53.49612236022949 475.0736951828003 -20.552754402160645Q426.07760429382324 12.390613555908203 359.9292039871216 12.390613555908203Q304.14021015167236 12.390613555908203 256.5503120422363 -14.244139194488525Q208.9604139328003 -40.878891944885254 181.56981468200684 -102.06639766693115L192.757248878479 -104.29298877716064V190.4296875H73.03917407989502ZM319.49591636657715 -81.66754245758057Q377.36713886260986 -81.66754245758057 413.06060552597046 -129.82779741287231Q448.75407218933105 -177.98805236816406 448.75407218933105 -265.4726505279541Q448.75407218933105 -351.03535652160645 414.18951749801636 -398.43976497650146Q379.62496280670166 -445.8441734313965 320.47247886657715 -445.8441734313965Q283.9919328689575 -445.8441734313965 254.01920080184937 -426.4593720436096Q224.0464687347412 -407.07457065582275 206.4780592918396 -366.95533990859985Q188.909649848938 -326.83610916137695 188.909649848938 -264.8007869720459Q188.909649848938 -172.7849245071411 226.89217329025269 -127.22623348236084Q264.8746967315674 -81.66754245758057 319.49591636657715 -81.66754245758057Z"/>
+<path d="M910.8639717102051 12.390613555908203Q833.3101332187653 12.390613555908203 774.1243004798889 -22.642498835921288Q714.9384528398514 -57.67561122775078 681.7739740014076 -119.90226060152054Q648.6094951629639 -182.12890625 648.6094951629639 -263.7558579444885Q648.6094951629639 -345.38280963897705 681.7709118127823 -407.5557291507721Q714.9323284626007 -469.72864866256714 774.1127073764801 -504.73153591156006Q833.2930713891983 -539.7343635559082 910.8397722244263 -539.7343635559082Q988.5858058929443 -539.7343635559082 1047.7459192276 -504.7382712364197Q1106.9060325622559 -469.74217891693115 1140.1560068130493 -407.58535265922546Q1173.4059810638428 -345.4285264015198 1173.4059810638428 -263.69473338127136Q1173.4059810638428 -181.96094036102295 1140.1898622512817 -119.7880208492279Q1106.973773241043 -57.61510878801346 1047.695791721344 -22.612247616052628Q988.4178400039673 12.390613555908203 910.8639717102051 12.390613555908203ZM911.0285997390747 -79.81991767883301Q973.2852935791016 -79.81991767883301 1009.2756509780884 -129.25180792808533Q1045.2660083770752 -178.68371307849884 1045.2660083770752 -263.671875Q1045.2660083770752 -348.9454984664917 1009.254789352417 -398.23466539382935Q973.2435703277588 -447.523832321167 910.9868764877319 -447.523832321167Q848.7301826477051 -447.523832321167 812.7398252487183 -398.23466539382935Q776.7494678497314 -348.9454984664917 776.7494678497314 -263.671875Q776.7494678497314 -178.5506010055542 812.7606868743896 -129.1852593421936Q848.7719058990479 -79.81991767883301 911.0285997390747 -79.81991767883301Z"/>
+<path d="M1257.414174079895 190.4296875V-527.34375H1371.8510389328003V-419.99995708465576L1369.9135303497314 -427.6444911956787Q1394.9251890182495 -485.42964458465576 1442.1264052391052 -512.582004070282Q1489.327621459961 -539.7343635559082 1545.815896987915 -539.7343635559082Q1611.6127490997314 -539.7343635559082 1660.2729082107544 -506.46287202835083Q1708.9330673217773 -473.19138050079346 1735.6889367103577 -411.09567880630493Q1762.444806098938 -348.9999771118164 1762.444806098938 -262.8476619720459Q1762.444806098938 -177.0312786102295 1735.4447960853577 -115.26370048522949Q1708.4447860717773 -53.49612236022949 1659.4486951828003 -20.552754402160645Q1610.4526042938232 12.390613555908203 1544.3042039871216 12.390613555908203Q1488.5152101516724 12.390613555908203 1440.9253120422363 -14.244139194488525Q1393.3354139328003 -40.878891944885254 1365.9448146820068 -102.06639766693115L1377.132248878479 -104.29298877716064V190.4296875H1257.414174079895ZM1503.8709163665771 -81.66754245758057Q1561.7421388626099 -81.66754245758057 1597.4356055259705 -129.82779741287231Q1633.129072189331 -177.98805236816406 1633.129072189331 -265.4726505279541Q1633.129072189331 -351.03535652160645 1598.5645174980164 -398.43976497650146Q1563.9999628067017 -445.8441734313965 1504.8474788665771 -445.8441734313965Q1468.3669328689575 -445.8441734313965 1438.3942008018494 -426.4593720436096Q1408.4214687347412 -407.07457065582275 1390.8530592918396 -366.95533990859985Q1373.284649848938 -326.83610916137695 1373.284649848938 -264.8007869720459Q1373.284649848938 -172.7849245071411 1411.2671732902527 -127.22623348236084Q1449.2496967315674 -81.66754245758057 1503.8709163665771 -81.66754245758057Z"/>
+</g>
+</svg>
@@ design/brand/logo/lockup-stacked-light.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2298 2814" color="#0f0f0f" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<rect x="0" y="0" width="2298.14" height="2814.45" fill="#fcfcfc" stroke="none"/>
+<g transform="translate(285.7 -33.3) scale(3.3725)">
+<defs><clipPath id="lsac"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="lsa" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lsac)" d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" fill="none" stroke="#000" stroke-width="92"/></mask><clipPath id="lsbc"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="lsb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lsbc)" d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" fill="none" stroke="#000" stroke-width="92"/></mask></defs>
+<path d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" stroke-width="44" mask="url(#lsa)"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="44" mask="url(#lsb)"/>
+</g>
+<g fill="currentColor" stroke="none" transform="translate(250.0 2306.6)">
+<path d="M73.03917407989502 190.4296875V-527.34375H187.4760389328003V-419.99995708465576L185.53853034973145 -427.6444911956787Q210.5501890182495 -485.42964458465576 257.7514052391052 -512.582004070282Q304.95262145996094 -539.7343635559082 361.44089698791504 -539.7343635559082Q427.23774909973145 -539.7343635559082 475.8979082107544 -506.46287202835083Q524.5580673217773 -473.19138050079346 551.3139367103577 -411.09567880630493Q578.069806098938 -348.9999771118164 578.069806098938 -262.8476619720459Q578.069806098938 -177.0312786102295 551.0697960853577 -115.26370048522949Q524.0697860717773 -53.49612236022949 475.0736951828003 -20.552754402160645Q426.07760429382324 12.390613555908203 359.9292039871216 12.390613555908203Q304.14021015167236 12.390613555908203 256.5503120422363 -14.244139194488525Q208.9604139328003 -40.878891944885254 181.56981468200684 -102.06639766693115L192.757248878479 -104.29298877716064V190.4296875H73.03917407989502ZM319.49591636657715 -81.66754245758057Q377.36713886260986 -81.66754245758057 413.06060552597046 -129.82779741287231Q448.75407218933105 -177.98805236816406 448.75407218933105 -265.4726505279541Q448.75407218933105 -351.03535652160645 414.18951749801636 -398.43976497650146Q379.62496280670166 -445.8441734313965 320.47247886657715 -445.8441734313965Q283.9919328689575 -445.8441734313965 254.01920080184937 -426.4593720436096Q224.0464687347412 -407.07457065582275 206.4780592918396 -366.95533990859985Q188.909649848938 -326.83610916137695 188.909649848938 -264.8007869720459Q188.909649848938 -172.7849245071411 226.89217329025269 -127.22623348236084Q264.8746967315674 -81.66754245758057 319.49591636657715 -81.66754245758057Z"/>
+<path d="M910.8639717102051 12.390613555908203Q833.3101332187653 12.390613555908203 774.1243004798889 -22.642498835921288Q714.9384528398514 -57.67561122775078 681.7739740014076 -119.90226060152054Q648.6094951629639 -182.12890625 648.6094951629639 -263.7558579444885Q648.6094951629639 -345.38280963897705 681.7709118127823 -407.5557291507721Q714.9323284626007 -469.72864866256714 774.1127073764801 -504.73153591156006Q833.2930713891983 -539.7343635559082 910.8397722244263 -539.7343635559082Q988.5858058929443 -539.7343635559082 1047.7459192276 -504.7382712364197Q1106.9060325622559 -469.74217891693115 1140.1560068130493 -407.58535265922546Q1173.4059810638428 -345.4285264015198 1173.4059810638428 -263.69473338127136Q1173.4059810638428 -181.96094036102295 1140.1898622512817 -119.7880208492279Q1106.973773241043 -57.61510878801346 1047.695791721344 -22.612247616052628Q988.4178400039673 12.390613555908203 910.8639717102051 12.390613555908203ZM911.0285997390747 -79.81991767883301Q973.2852935791016 -79.81991767883301 1009.2756509780884 -129.25180792808533Q1045.2660083770752 -178.68371307849884 1045.2660083770752 -263.671875Q1045.2660083770752 -348.9454984664917 1009.254789352417 -398.23466539382935Q973.2435703277588 -447.523832321167 910.9868764877319 -447.523832321167Q848.7301826477051 -447.523832321167 812.7398252487183 -398.23466539382935Q776.7494678497314 -348.9454984664917 776.7494678497314 -263.671875Q776.7494678497314 -178.5506010055542 812.7606868743896 -129.1852593421936Q848.7719058990479 -79.81991767883301 911.0285997390747 -79.81991767883301Z"/>
+<path d="M1257.414174079895 190.4296875V-527.34375H1371.8510389328003V-419.99995708465576L1369.9135303497314 -427.6444911956787Q1394.9251890182495 -485.42964458465576 1442.1264052391052 -512.582004070282Q1489.327621459961 -539.7343635559082 1545.815896987915 -539.7343635559082Q1611.6127490997314 -539.7343635559082 1660.2729082107544 -506.46287202835083Q1708.9330673217773 -473.19138050079346 1735.6889367103577 -411.09567880630493Q1762.444806098938 -348.9999771118164 1762.444806098938 -262.8476619720459Q1762.444806098938 -177.0312786102295 1735.4447960853577 -115.26370048522949Q1708.4447860717773 -53.49612236022949 1659.4486951828003 -20.552754402160645Q1610.4526042938232 12.390613555908203 1544.3042039871216 12.390613555908203Q1488.5152101516724 12.390613555908203 1440.9253120422363 -14.244139194488525Q1393.3354139328003 -40.878891944885254 1365.9448146820068 -102.06639766693115L1377.132248878479 -104.29298877716064V190.4296875H1257.414174079895ZM1503.8709163665771 -81.66754245758057Q1561.7421388626099 -81.66754245758057 1597.4356055259705 -129.82779741287231Q1633.129072189331 -177.98805236816406 1633.129072189331 -265.4726505279541Q1633.129072189331 -351.03535652160645 1598.5645174980164 -398.43976497650146Q1563.9999628067017 -445.8441734313965 1504.8474788665771 -445.8441734313965Q1468.3669328689575 -445.8441734313965 1438.3942008018494 -426.4593720436096Q1408.4214687347412 -407.07457065582275 1390.8530592918396 -366.95533990859985Q1373.284649848938 -326.83610916137695 1373.284649848938 -264.8007869720459Q1373.284649848938 -172.7849245071411 1411.2671732902527 -127.22623348236084Q1449.2496967315674 -81.66754245758057 1503.8709163665771 -81.66754245758057Z"/>
+</g>
+</svg>
@@ design/brand/logo/lockup-stacked.svg
@@ -0,0 +1,12 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2298 2814" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<g transform="translate(285.7 -33.3) scale(3.3725)">
+<defs><clipPath id="lsac"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="lsa" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lsac)" d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" fill="none" stroke="#000" stroke-width="92"/></mask><clipPath id="lsbc"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="lsb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lsbc)" d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" fill="none" stroke="#000" stroke-width="92"/></mask></defs>
+<path d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" stroke-width="44" mask="url(#lsa)"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="44" mask="url(#lsb)"/>
+</g>
+<g fill="currentColor" stroke="none" transform="translate(250.0 2306.6)">
+<path d="M73.03917407989502 190.4296875V-527.34375H187.4760389328003V-419.99995708465576L185.53853034973145 -427.6444911956787Q210.5501890182495 -485.42964458465576 257.7514052391052 -512.582004070282Q304.95262145996094 -539.7343635559082 361.44089698791504 -539.7343635559082Q427.23774909973145 -539.7343635559082 475.8979082107544 -506.46287202835083Q524.5580673217773 -473.19138050079346 551.3139367103577 -411.09567880630493Q578.069806098938 -348.9999771118164 578.069806098938 -262.8476619720459Q578.069806098938 -177.0312786102295 551.0697960853577 -115.26370048522949Q524.0697860717773 -53.49612236022949 475.0736951828003 -20.552754402160645Q426.07760429382324 12.390613555908203 359.9292039871216 12.390613555908203Q304.14021015167236 12.390613555908203 256.5503120422363 -14.244139194488525Q208.9604139328003 -40.878891944885254 181.56981468200684 -102.06639766693115L192.757248878479 -104.29298877716064V190.4296875H73.03917407989502ZM319.49591636657715 -81.66754245758057Q377.36713886260986 -81.66754245758057 413.06060552597046 -129.82779741287231Q448.75407218933105 -177.98805236816406 448.75407218933105 -265.4726505279541Q448.75407218933105 -351.03535652160645 414.18951749801636 -398.43976497650146Q379.62496280670166 -445.8441734313965 320.47247886657715 -445.8441734313965Q283.9919328689575 -445.8441734313965 254.01920080184937 -426.4593720436096Q224.0464687347412 -407.07457065582275 206.4780592918396 -366.95533990859985Q188.909649848938 -326.83610916137695 188.909649848938 -264.8007869720459Q188.909649848938 -172.7849245071411 226.89217329025269 -127.22623348236084Q264.8746967315674 -81.66754245758057 319.49591636657715 -81.66754245758057Z"/>
+<path d="M910.8639717102051 12.390613555908203Q833.3101332187653 12.390613555908203 774.1243004798889 -22.642498835921288Q714.9384528398514 -57.67561122775078 681.7739740014076 -119.90226060152054Q648.6094951629639 -182.12890625 648.6094951629639 -263.7558579444885Q648.6094951629639 -345.38280963897705 681.7709118127823 -407.5557291507721Q714.9323284626007 -469.72864866256714 774.1127073764801 -504.73153591156006Q833.2930713891983 -539.7343635559082 910.8397722244263 -539.7343635559082Q988.5858058929443 -539.7343635559082 1047.7459192276 -504.7382712364197Q1106.9060325622559 -469.74217891693115 1140.1560068130493 -407.58535265922546Q1173.4059810638428 -345.4285264015198 1173.4059810638428 -263.69473338127136Q1173.4059810638428 -181.96094036102295 1140.1898622512817 -119.7880208492279Q1106.973773241043 -57.61510878801346 1047.695791721344 -22.612247616052628Q988.4178400039673 12.390613555908203 910.8639717102051 12.390613555908203ZM911.0285997390747 -79.81991767883301Q973.2852935791016 -79.81991767883301 1009.2756509780884 -129.25180792808533Q1045.2660083770752 -178.68371307849884 1045.2660083770752 -263.671875Q1045.2660083770752 -348.9454984664917 1009.254789352417 -398.23466539382935Q973.2435703277588 -447.523832321167 910.9868764877319 -447.523832321167Q848.7301826477051 -447.523832321167 812.7398252487183 -398.23466539382935Q776.7494678497314 -348.9454984664917 776.7494678497314 -263.671875Q776.7494678497314 -178.5506010055542 812.7606868743896 -129.1852593421936Q848.7719058990479 -79.81991767883301 911.0285997390747 -79.81991767883301Z"/>
+<path d="M1257.414174079895 190.4296875V-527.34375H1371.8510389328003V-419.99995708465576L1369.9135303497314 -427.6444911956787Q1394.9251890182495 -485.42964458465576 1442.1264052391052 -512.582004070282Q1489.327621459961 -539.7343635559082 1545.815896987915 -539.7343635559082Q1611.6127490997314 -539.7343635559082 1660.2729082107544 -506.46287202835083Q1708.9330673217773 -473.19138050079346 1735.6889367103577 -411.09567880630493Q1762.444806098938 -348.9999771118164 1762.444806098938 -262.8476619720459Q1762.444806098938 -177.0312786102295 1735.4447960853577 -115.26370048522949Q1708.4447860717773 -53.49612236022949 1659.4486951828003 -20.552754402160645Q1610.4526042938232 12.390613555908203 1544.3042039871216 12.390613555908203Q1488.5152101516724 12.390613555908203 1440.9253120422363 -14.244139194488525Q1393.3354139328003 -40.878891944885254 1365.9448146820068 -102.06639766693115L1377.132248878479 -104.29298877716064V190.4296875H1257.414174079895ZM1503.8709163665771 -81.66754245758057Q1561.7421388626099 -81.66754245758057 1597.4356055259705 -129.82779741287231Q1633.129072189331 -177.98805236816406 1633.129072189331 -265.4726505279541Q1633.129072189331 -351.03535652160645 1598.5645174980164 -398.43976497650146Q1563.9999628067017 -445.8441734313965 1504.8474788665771 -445.8441734313965Q1468.3669328689575 -445.8441734313965 1438.3942008018494 -426.4593720436096Q1408.4214687347412 -407.07457065582275 1390.8530592918396 -366.95533990859985Q1373.284649848938 -326.83610916137695 1373.284649848938 -264.8007869720459Q1373.284649848938 -172.7849245071411 1411.2671732902527 -127.22623348236084Q1449.2496967315674 -81.66754245758057 1503.8709163665771 -81.66754245758057Z"/>
+</g>
+</svg>
@@ design/brand/logo/mark-compact.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<path d="M114 146H246A44 44 0 0 1 290 190V322A44 44 0 0 1 246 366H114A44 44 0 0 1 70 322V190A44 44 0 0 1 114 146Z" stroke-width="64"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="64"/>
+</svg>
@@ design/brand/logo/mark-dark.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#eeeeee" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<rect x="0" y="0" width="512" height="512" fill="#090909" stroke="none"/>
+<defs><clipPath id="mac"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="ma" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mac)" d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" fill="none" stroke="#000" stroke-width="92"/></mask><clipPath id="mbc"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="mb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mbc)" d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" fill="none" stroke="#000" stroke-width="92"/></mask></defs>
+<path d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" stroke-width="44" mask="url(#ma)"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="44" mask="url(#mb)"/>
+</svg>
@@ design/brand/logo/mark-light.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#0f0f0f" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<rect x="0" y="0" width="512" height="512" fill="#fcfcfc" stroke="none"/>
+<defs><clipPath id="mac"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="ma" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mac)" d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" fill="none" stroke="#000" stroke-width="92"/></mask><clipPath id="mbc"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="mb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mbc)" d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" fill="none" stroke="#000" stroke-width="92"/></mask></defs>
+<path d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" stroke-width="44" mask="url(#ma)"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="44" mask="url(#mb)"/>
+</svg>
@@ design/brand/logo/mark.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<defs><clipPath id="mac"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="ma" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mac)" d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" fill="none" stroke="#000" stroke-width="92"/></mask><clipPath id="mbc"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="mb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#mbc)" d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" fill="none" stroke="#000" stroke-width="92"/></mask></defs>
+<path d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" stroke-width="44" mask="url(#ma)"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="44" mask="url(#mb)"/>
+</svg>
@@ design/brand/logo/og-image-1200x630.svg
@@ -0,0 +1,32 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" color="#eeeeee" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<rect width="1200" height="630" fill="#090909" stroke="none"/>
+<g transform="translate(52.4 166.4) scale(0.1745)">
+<g transform="translate(139.6 -75.0) scale(2.2995)">
+<defs><clipPath id="lhac"><circle cx="190.0" cy="146.0" r="70.4"/></clipPath><mask id="lha" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lhac)" d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" fill="none" stroke="#000" stroke-width="92"/></mask><clipPath id="lhbc"><circle cx="190.0" cy="366.0" r="70.4"/></clipPath><mask id="lhb" maskUnits="userSpaceOnUse" x="0" y="0" width="512" height="512"><rect width="512" height="512" fill="#fff"/><path clip-path="url(#lhbc)" d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" fill="none" stroke="#000" stroke-width="92"/></mask></defs>
+<path d="M110 146H250A40 40 0 0 1 290 186V326A40 40 0 0 1 250 366H110A40 40 0 0 1 70 326V186A40 40 0 0 1 110 146Z" stroke-width="44" mask="url(#lha)"/>
+<path d="M142 256A150 150 0 1 0 442 256A150 150 0 1 0 142 256Z" stroke-width="44" mask="url(#lhb)"/>
+</g>
+<g fill="currentColor" stroke="none" transform="translate(1641.6 777.3)">
+<path d="M73.03917407989502 190.4296875V-527.34375H187.4760389328003V-419.99995708465576L185.53853034973145 -427.6444911956787Q210.5501890182495 -485.42964458465576 257.7514052391052 -512.582004070282Q304.95262145996094 -539.7343635559082 361.44089698791504 -539.7343635559082Q427.23774909973145 -539.7343635559082 475.8979082107544 -506.46287202835083Q524.5580673217773 -473.19138050079346 551.3139367103577 -411.09567880630493Q578.069806098938 -348.9999771118164 578.069806098938 -262.8476619720459Q578.069806098938 -177.0312786102295 551.0697960853577 -115.26370048522949Q524.0697860717773 -53.49612236022949 475.0736951828003 -20.552754402160645Q426.07760429382324 12.390613555908203 359.9292039871216 12.390613555908203Q304.14021015167236 12.390613555908203 256.5503120422363 -14.244139194488525Q208.9604139328003 -40.878891944885254 181.56981468200684 -102.06639766693115L192.757248878479 -104.29298877716064V190.4296875H73.03917407989502ZM319.49591636657715 -81.66754245758057Q377.36713886260986 -81.66754245758057 413.06060552597046 -129.82779741287231Q448.75407218933105 -177.98805236816406 448.75407218933105 -265.4726505279541Q448.75407218933105 -351.03535652160645 414.18951749801636 -398.43976497650146Q379.62496280670166 -445.8441734313965 320.47247886657715 -445.8441734313965Q283.9919328689575 -445.8441734313965 254.01920080184937 -426.4593720436096Q224.0464687347412 -407.07457065582275 206.4780592918396 -366.95533990859985Q188.909649848938 -326.83610916137695 188.909649848938 -264.8007869720459Q188.909649848938 -172.7849245071411 226.89217329025269 -127.22623348236084Q264.8746967315674 -81.66754245758057 319.49591636657715 -81.66754245758057Z"/>
+<path d="M910.8639717102051 12.390613555908203Q833.3101332187653 12.390613555908203 774.1243004798889 -22.642498835921288Q714.9384528398514 -57.67561122775078 681.7739740014076 -119.90226060152054Q648.6094951629639 -182.12890625 648.6094951629639 -263.7558579444885Q648.6094951629639 -345.38280963897705 681.7709118127823 -407.5557291507721Q714.9323284626007 -469.72864866256714 774.1127073764801 -504.73153591156006Q833.2930713891983 -539.7343635559082 910.8397722244263 -539.7343635559082Q988.5858058929443 -539.7343635559082 1047.7459192276 -504.7382712364197Q1106.9060325622559 -469.74217891693115 1140.1560068130493 -407.58535265922546Q1173.4059810638428 -345.4285264015198 1173.4059810638428 -263.69473338127136Q1173.4059810638428 -181.96094036102295 1140.1898622512817 -119.7880208492279Q1106.973773241043 -57.61510878801346 1047.695791721344 -22.612247616052628Q988.4178400039673 12.390613555908203 910.8639717102051 12.390613555908203ZM911.0285997390747 -79.81991767883301Q973.2852935791016 -79.81991767883301 1009.2756509780884 -129.25180792808533Q1045.2660083770752 -178.68371307849884 1045.2660083770752 -263.671875Q1045.2660083770752 -348.9454984664917 1009.254789352417 -398.23466539382935Q973.2435703277588 -447.523832321167 910.9868764877319 -447.523832321167Q848.7301826477051 -447.523832321167 812.7398252487183 -398.23466539382935Q776.7494678497314 -348.9454984664917 776.7494678497314 -263.671875Q776.7494678497314 -178.5506010055542 812.7606868743896 -129.1852593421936Q848.7719058990479 -79.81991767883301 911.0285997390747 -79.81991767883301Z"/>
+<path d="M1257.414174079895 190.4296875V-527.34375H1371.8510389328003V-419.99995708465576L1369.9135303497314 -427.6444911956787Q1394.9251890182495 -485.42964458465576 1442.1264052391052 -512.582004070282Q1489.327621459961 -539.7343635559082 1545.815896987915 -539.7343635559082Q1611.6127490997314 -539.7343635559082 1660.2729082107544 -506.46287202835083Q1708.9330673217773 -473.19138050079346 1735.6889367103577 -411.09567880630493Q1762.444806098938 -348.9999771118164 1762.444806098938 -262.8476619720459Q1762.444806098938 -177.0312786102295 1735.4447960853577 -115.26370048522949Q1708.4447860717773 -53.49612236022949 1659.4486951828003 -20.552754402160645Q1610.4526042938232 12.390613555908203 1544.3042039871216 12.390613555908203Q1488.5152101516724 12.390613555908203 1440.9253120422363 -14.244139194488525Q1393.3354139328003 -40.878891944885254 1365.9448146820068 -102.06639766693115L1377.132248878479 -104.29298877716064V190.4296875H1257.414174079895ZM1503.8709163665771 -81.66754245758057Q1561.7421388626099 -81.66754245758057 1597.4356055259705 -129.82779741287231Q1633.129072189331 -177.98805236816406 1633.129072189331 -265.4726505279541Q1633.129072189331 -351.03535652160645 1598.5645174980164 -398.43976497650146Q1563.9999628067017 -445.8441734313965 1504.8474788665771 -445.8441734313965Q1468.3669328689575 -445.8441734313965 1438.3942008018494 -426.4593720436096Q1408.4214687347412 -407.07457065582275 1390.8530592918396 -366.95533990859985Q1373.284649848938 -326.83610916137695 1373.284649848938 -264.8007869720459Q1373.284649848938 -172.7849245071411 1411.2671732902527 -127.22623348236084Q1449.2496967315674 -81.66754245758057 1503.8709163665771 -81.66754245758057Z"/>
+</g>
+</g>
+<g fill="#a4a4a4" stroke="none" transform="translate(96 436.6)">
+<path d="M3.0873608589172363 0.0V-28.125H14.001777172088623Q18.939740657806396 -28.125 21.41551971435547 -26.08656644821167Q23.89129877090454 -24.04813289642334 23.89129877090454 -20.144553184509277Q23.89129877090454 -16.31019353866577 21.377792358398438 -14.23863410949707Q18.864285945892334 -12.16707468032837 13.765084743499756 -12.16707468032837H7.2856974601745605V0.0H3.0873608589172363ZM7.2856974601745605 -15.54450273513794H13.900854587554932Q16.704328060150146 -15.54450273513794 18.172940015792847 -16.759594678878784Q19.641551971435547 -17.97468662261963 19.641551971435547 -20.09486436843872Q19.641551971435547 -22.016286849975586 18.307788372039795 -23.381929397583008Q16.974024772644043 -24.74757194519043 13.874139785766602 -24.74757194519043H7.2856974601745605V-15.54450273513794Z"/>
+<path d="M27.750642108917237 0.0V-21.09375H31.52522783279419V-16.54688596725464L31.37538890838623 -17.084381580352783Q32.330698776245114 -19.31640625 34.19617037773132 -20.44538974761963Q36.06164197921753 -21.574373245239258 38.054601955413816 -21.574373245239258Q38.63288145065307 -21.574373245239258 39.19373731613159 -21.476716995239258Q39.754593181610105 -21.379060745239258 40.26193122863769 -21.144685745239258L39.82927541732788 -17.548198699951172Q38.70162706375122 -17.8861665725708 37.61428813934326 -17.8861665725708Q36.87008123397827 -17.8861665725708 35.868911552429196 -17.604684829711914Q34.867741870880124 -17.323203086853027 33.921723175048825 -16.52031421661377Q32.97570447921753 -15.717425346374512 32.34812240600586 -14.169766902923584Q31.72054033279419 -12.622108459472656 31.72054033279419 -10.092723369598389V0.0H27.750642108917237Z"/>
+<path d="M51.97932324409485 0.4806232452392578Q48.95163497924805 0.4806232452392578 46.641030526161195 -0.9179995208978653Q44.330426371097566 -2.3166222870349884 43.03569099903107 -4.800889194011688Q41.740955924987794 -7.28515625 41.740955924987794 -10.548359155654907Q41.740955924987794 -13.811562061309814 43.03563675880432 -16.294879913330078Q44.330317890644075 -18.778196573257446 46.64082548618317 -20.176284313201904Q48.951333379745485 -21.574373245239258 51.97889528274536 -21.574373245239258Q55.009979820251466 -21.574373245239258 57.32012948989868 -20.176405906677246Q59.6302791595459 -18.778436183929443 60.92652521133423 -16.295403242111206Q62.22277126312256 -13.812370300292969 62.22277126312256 -10.547279119491577Q62.22277126312256 -7.2821879386901855 60.927123641967775 -4.79887068271637Q59.63147602081299 -2.31555312871933 57.31924376487732 -0.917464941740036Q55.00701150894165 0.4806232452392578 51.97932324409485 0.4806232452392578ZM51.98223195075989 -2.6752519607543945Q54.75048980712891 -2.6752519607543945 56.351368045806886 -4.791678786277771Q57.952246284484865 -6.9081056118011475 57.952246284484865 -10.546875Q57.952246284484865 -14.20159101486206 56.35099849700928 -16.310044527053833Q54.74975190162659 -18.418498039245605 51.981495237350465 -18.418498039245605Q49.21323738098145 -18.418498039245605 47.61235914230347 -16.310044527053833Q46.01148090362549 -14.20159101486206 46.01148090362549 -10.546875Q46.01148090362549 -6.9057536125183105 47.61272749900818 -4.7905027866363525Q49.21397469043732 -2.6752519607543945 51.98223195075989 -2.6752519607543945Z"/>
+<path d="M76.50588574409485 0.4806232452392578Q73.47819747924805 0.4806232452392578 71.1675930261612 -0.9179995208978653Q68.85698887109757 -2.3166222870349884 67.56225349903107 -4.800889194011688Q66.2675184249878 -7.28515625 66.2675184249878 -10.548359155654907Q66.2675184249878 -13.811562061309814 67.56219925880433 -16.294879913330078Q68.85688039064408 -18.778196573257446 71.16738798618317 -20.176284313201904Q73.47789587974549 -21.574373245239258 76.50545778274537 -21.574373245239258Q79.53654232025147 -21.574373245239258 81.84669198989869 -20.176405906677246Q84.1568416595459 -18.778436183929443 85.45308771133423 -16.295403242111206Q86.74933376312256 -13.812370300292969 86.74933376312256 -10.547279119491577Q86.74933376312256 -7.2821879386901855 85.45368614196778 -4.79887068271637Q84.158038520813 -2.31555312871933 81.84580626487733 -0.917464941740036Q79.53357400894166 0.4806232452392578 76.50588574409485 0.4806232452392578ZM76.5087944507599 -2.6752519607543945Q79.27705230712891 -2.6752519607543945 80.87793054580689 -4.791678786277771Q82.47880878448487 -6.9081056118011475 82.47880878448487 -10.546875Q82.47880878448487 -14.20159101486206 80.87756099700928 -16.310044527053833Q79.27631440162659 -18.418498039245605 76.50805773735047 -18.418498039245605Q73.73979988098145 -18.418498039245605 72.13892164230347 -16.310044527053833Q70.5380434036255 -14.20159101486206 70.5380434036255 -10.546875Q70.5380434036255 -6.9057536125183105 72.13928999900818 -4.7905027866363525Q73.74053719043732 -2.6752519607543945 76.5087944507599 -2.6752519607543945Z"/>
@@ design/brand/logo/wordmark-dark.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2298 1285" color="#eeeeee" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<rect x="0" y="0" width="2298.14" height="1285.16" fill="#090909" stroke="none"/>
+<g fill="currentColor" stroke="none" transform="translate(250.0 777.3)">
+<path d="M73.03917407989502 190.4296875V-527.34375H187.4760389328003V-419.99995708465576L185.53853034973145 -427.6444911956787Q210.5501890182495 -485.42964458465576 257.7514052391052 -512.582004070282Q304.95262145996094 -539.7343635559082 361.44089698791504 -539.7343635559082Q427.23774909973145 -539.7343635559082 475.8979082107544 -506.46287202835083Q524.5580673217773 -473.19138050079346 551.3139367103577 -411.09567880630493Q578.069806098938 -348.9999771118164 578.069806098938 -262.8476619720459Q578.069806098938 -177.0312786102295 551.0697960853577 -115.26370048522949Q524.0697860717773 -53.49612236022949 475.0736951828003 -20.552754402160645Q426.07760429382324 12.390613555908203 359.9292039871216 12.390613555908203Q304.14021015167236 12.390613555908203 256.5503120422363 -14.244139194488525Q208.9604139328003 -40.878891944885254 181.56981468200684 -102.06639766693115L192.757248878479 -104.29298877716064V190.4296875H73.03917407989502ZM319.49591636657715 -81.66754245758057Q377.36713886260986 -81.66754245758057 413.06060552597046 -129.82779741287231Q448.75407218933105 -177.98805236816406 448.75407218933105 -265.4726505279541Q448.75407218933105 -351.03535652160645 414.18951749801636 -398.43976497650146Q379.62496280670166 -445.8441734313965 320.47247886657715 -445.8441734313965Q283.9919328689575 -445.8441734313965 254.01920080184937 -426.4593720436096Q224.0464687347412 -407.07457065582275 206.4780592918396 -366.95533990859985Q188.909649848938 -326.83610916137695 188.909649848938 -264.8007869720459Q188.909649848938 -172.7849245071411 226.89217329025269 -127.22623348236084Q264.8746967315674 -81.66754245758057 319.49591636657715 -81.66754245758057Z"/>
+<path d="M910.8639717102051 12.390613555908203Q833.3101332187653 12.390613555908203 774.1243004798889 -22.642498835921288Q714.9384528398514 -57.67561122775078 681.7739740014076 -119.90226060152054Q648.6094951629639 -182.12890625 648.6094951629639 -263.7558579444885Q648.6094951629639 -345.38280963897705 681.7709118127823 -407.5557291507721Q714.9323284626007 -469.72864866256714 774.1127073764801 -504.73153591156006Q833.2930713891983 -539.7343635559082 910.8397722244263 -539.7343635559082Q988.5858058929443 -539.7343635559082 1047.7459192276 -504.7382712364197Q1106.9060325622559 -469.74217891693115 1140.1560068130493 -407.58535265922546Q1173.4059810638428 -345.4285264015198 1173.4059810638428 -263.69473338127136Q1173.4059810638428 -181.96094036102295 1140.1898622512817 -119.7880208492279Q1106.973773241043 -57.61510878801346 1047.695791721344 -22.612247616052628Q988.4178400039673 12.390613555908203 910.8639717102051 12.390613555908203ZM911.0285997390747 -79.81991767883301Q973.2852935791016 -79.81991767883301 1009.2756509780884 -129.25180792808533Q1045.2660083770752 -178.68371307849884 1045.2660083770752 -263.671875Q1045.2660083770752 -348.9454984664917 1009.254789352417 -398.23466539382935Q973.2435703277588 -447.523832321167 910.9868764877319 -447.523832321167Q848.7301826477051 -447.523832321167 812.7398252487183 -398.23466539382935Q776.7494678497314 -348.9454984664917 776.7494678497314 -263.671875Q776.7494678497314 -178.5506010055542 812.7606868743896 -129.1852593421936Q848.7719058990479 -79.81991767883301 911.0285997390747 -79.81991767883301Z"/>
+<path d="M1257.414174079895 190.4296875V-527.34375H1371.8510389328003V-419.99995708465576L1369.9135303497314 -427.6444911956787Q1394.9251890182495 -485.42964458465576 1442.1264052391052 -512.582004070282Q1489.327621459961 -539.7343635559082 1545.815896987915 -539.7343635559082Q1611.6127490997314 -539.7343635559082 1660.2729082107544 -506.46287202835083Q1708.9330673217773 -473.19138050079346 1735.6889367103577 -411.09567880630493Q1762.444806098938 -348.9999771118164 1762.444806098938 -262.8476619720459Q1762.444806098938 -177.0312786102295 1735.4447960853577 -115.26370048522949Q1708.4447860717773 -53.49612236022949 1659.4486951828003 -20.552754402160645Q1610.4526042938232 12.390613555908203 1544.3042039871216 12.390613555908203Q1488.5152101516724 12.390613555908203 1440.9253120422363 -14.244139194488525Q1393.3354139328003 -40.878891944885254 1365.9448146820068 -102.06639766693115L1377.132248878479 -104.29298877716064V190.4296875H1257.414174079895ZM1503.8709163665771 -81.66754245758057Q1561.7421388626099 -81.66754245758057 1597.4356055259705 -129.82779741287231Q1633.129072189331 -177.98805236816406 1633.129072189331 -265.4726505279541Q1633.129072189331 -351.03535652160645 1598.5645174980164 -398.43976497650146Q1563.9999628067017 -445.8441734313965 1504.8474788665771 -445.8441734313965Q1468.3669328689575 -445.8441734313965 1438.3942008018494 -426.4593720436096Q1408.4214687347412 -407.07457065582275 1390.8530592918396 -366.95533990859985Q1373.284649848938 -326.83610916137695 1373.284649848938 -264.8007869720459Q1373.284649848938 -172.7849245071411 1411.2671732902527 -127.22623348236084Q1449.2496967315674 -81.66754245758057 1503.8709163665771 -81.66754245758057Z"/>
+</g>
+</svg>
@@ design/brand/logo/wordmark-light.svg
@@ -0,0 +1,8 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2298 1285" color="#0f0f0f" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<rect x="0" y="0" width="2298.14" height="1285.16" fill="#fcfcfc" stroke="none"/>
+<g fill="currentColor" stroke="none" transform="translate(250.0 777.3)">
+<path d="M73.03917407989502 190.4296875V-527.34375H187.4760389328003V-419.99995708465576L185.53853034973145 -427.6444911956787Q210.5501890182495 -485.42964458465576 257.7514052391052 -512.582004070282Q304.95262145996094 -539.7343635559082 361.44089698791504 -539.7343635559082Q427.23774909973145 -539.7343635559082 475.8979082107544 -506.46287202835083Q524.5580673217773 -473.19138050079346 551.3139367103577 -411.09567880630493Q578.069806098938 -348.9999771118164 578.069806098938 -262.8476619720459Q578.069806098938 -177.0312786102295 551.0697960853577 -115.26370048522949Q524.0697860717773 -53.49612236022949 475.0736951828003 -20.552754402160645Q426.07760429382324 12.390613555908203 359.9292039871216 12.390613555908203Q304.14021015167236 12.390613555908203 256.5503120422363 -14.244139194488525Q208.9604139328003 -40.878891944885254 181.56981468200684 -102.06639766693115L192.757248878479 -104.29298877716064V190.4296875H73.03917407989502ZM319.49591636657715 -81.66754245758057Q377.36713886260986 -81.66754245758057 413.06060552597046 -129.82779741287231Q448.75407218933105 -177.98805236816406 448.75407218933105 -265.4726505279541Q448.75407218933105 -351.03535652160645 414.18951749801636 -398.43976497650146Q379.62496280670166 -445.8441734313965 320.47247886657715 -445.8441734313965Q283.9919328689575 -445.8441734313965 254.01920080184937 -426.4593720436096Q224.0464687347412 -407.07457065582275 206.4780592918396 -366.95533990859985Q188.909649848938 -326.83610916137695 188.909649848938 -264.8007869720459Q188.909649848938 -172.7849245071411 226.89217329025269 -127.22623348236084Q264.8746967315674 -81.66754245758057 319.49591636657715 -81.66754245758057Z"/>
+<path d="M910.8639717102051 12.390613555908203Q833.3101332187653 12.390613555908203 774.1243004798889 -22.642498835921288Q714.9384528398514 -57.67561122775078 681.7739740014076 -119.90226060152054Q648.6094951629639 -182.12890625 648.6094951629639 -263.7558579444885Q648.6094951629639 -345.38280963897705 681.7709118127823 -407.5557291507721Q714.9323284626007 -469.72864866256714 774.1127073764801 -504.73153591156006Q833.2930713891983 -539.7343635559082 910.8397722244263 -539.7343635559082Q988.5858058929443 -539.7343635559082 1047.7459192276 -504.7382712364197Q1106.9060325622559 -469.74217891693115 1140.1560068130493 -407.58535265922546Q1173.4059810638428 -345.4285264015198 1173.4059810638428 -263.69473338127136Q1173.4059810638428 -181.96094036102295 1140.1898622512817 -119.7880208492279Q1106.973773241043 -57.61510878801346 1047.695791721344 -22.612247616052628Q988.4178400039673 12.390613555908203 910.8639717102051 12.390613555908203ZM911.0285997390747 -79.81991767883301Q973.2852935791016 -79.81991767883301 1009.2756509780884 -129.25180792808533Q1045.2660083770752 -178.68371307849884 1045.2660083770752 -263.671875Q1045.2660083770752 -348.9454984664917 1009.254789352417 -398.23466539382935Q973.2435703277588 -447.523832321167 910.9868764877319 -447.523832321167Q848.7301826477051 -447.523832321167 812.7398252487183 -398.23466539382935Q776.7494678497314 -348.9454984664917 776.7494678497314 -263.671875Q776.7494678497314 -178.5506010055542 812.7606868743896 -129.1852593421936Q848.7719058990479 -79.81991767883301 911.0285997390747 -79.81991767883301Z"/>
+<path d="M1257.414174079895 190.4296875V-527.34375H1371.8510389328003V-419.99995708465576L1369.9135303497314 -427.6444911956787Q1394.9251890182495 -485.42964458465576 1442.1264052391052 -512.582004070282Q1489.327621459961 -539.7343635559082 1545.815896987915 -539.7343635559082Q1611.6127490997314 -539.7343635559082 1660.2729082107544 -506.46287202835083Q1708.9330673217773 -473.19138050079346 1735.6889367103577 -411.09567880630493Q1762.444806098938 -348.9999771118164 1762.444806098938 -262.8476619720459Q1762.444806098938 -177.0312786102295 1735.4447960853577 -115.26370048522949Q1708.4447860717773 -53.49612236022949 1659.4486951828003 -20.552754402160645Q1610.4526042938232 12.390613555908203 1544.3042039871216 12.390613555908203Q1488.5152101516724 12.390613555908203 1440.9253120422363 -14.244139194488525Q1393.3354139328003 -40.878891944885254 1365.9448146820068 -102.06639766693115L1377.132248878479 -104.29298877716064V190.4296875H1257.414174079895ZM1503.8709163665771 -81.66754245758057Q1561.7421388626099 -81.66754245758057 1597.4356055259705 -129.82779741287231Q1633.129072189331 -177.98805236816406 1633.129072189331 -265.4726505279541Q1633.129072189331 -351.03535652160645 1598.5645174980164 -398.43976497650146Q1563.9999628067017 -445.8441734313965 1504.8474788665771 -445.8441734313965Q1468.3669328689575 -445.8441734313965 1438.3942008018494 -426.4593720436096Q1408.4214687347412 -407.07457065582275 1390.8530592918396 -366.95533990859985Q1373.284649848938 -326.83610916137695 1373.284649848938 -264.8007869720459Q1373.284649848938 -172.7849245071411 1411.2671732902527 -127.22623348236084Q1449.2496967315674 -81.66754245758057 1503.8709163665771 -81.66754245758057Z"/>
+</g>
+</svg>
@@ design/brand/logo/wordmark.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2298 1285" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
+<g fill="currentColor" stroke="none" transform="translate(250.0 777.3)">
+<path d="M73.03917407989502 190.4296875V-527.34375H187.4760389328003V-419.99995708465576L185.53853034973145 -427.6444911956787Q210.5501890182495 -485.42964458465576 257.7514052391052 -512.582004070282Q304.95262145996094 -539.7343635559082 361.44089698791504 -539.7343635559082Q427.23774909973145 -539.7343635559082 475.8979082107544 -506.46287202835083Q524.5580673217773 -473.19138050079346 551.3139367103577 -411.09567880630493Q578.069806098938 -348.9999771118164 578.069806098938 -262.8476619720459Q578.069806098938 -177.0312786102295 551.0697960853577 -115.26370048522949Q524.0697860717773 -53.49612236022949 475.0736951828003 -20.552754402160645Q426.07760429382324 12.390613555908203 359.9292039871216 12.390613555908203Q304.14021015167236 12.390613555908203 256.5503120422363 -14.244139194488525Q208.9604139328003 -40.878891944885254 181.56981468200684 -102.06639766693115L192.757248878479 -104.29298877716064V190.4296875H73.03917407989502ZM319.49591636657715 -81.66754245758057Q377.36713886260986 -81.66754245758057 413.06060552597046 -129.82779741287231Q448.75407218933105 -177.98805236816406 448.75407218933105 -265.4726505279541Q448.75407218933105 -351.03535652160645 414.18951749801636 -398.43976497650146Q379.62496280670166 -445.8441734313965 320.47247886657715 -445.8441734313965Q283.9919328689575 -445.8441734313965 254.01920080184937 -426.4593720436096Q224.0464687347412 -407.07457065582275 206.4780592918396 -366.95533990859985Q188.909649848938 -326.83610916137695 188.909649848938 -264.8007869720459Q188.909649848938 -172.7849245071411 226.89217329025269 -127.22623348236084Q264.8746967315674 -81.66754245758057 319.49591636657715 -81.66754245758057Z"/>
+<path d="M910.8639717102051 12.390613555908203Q833.3101332187653 12.390613555908203 774.1243004798889 -22.642498835921288Q714.9384528398514 -57.67561122775078 681.7739740014076 -119.90226060152054Q648.6094951629639 -182.12890625 648.6094951629639 -263.7558579444885Q648.6094951629639 -345.38280963897705 681.7709118127823 -407.5557291507721Q714.9323284626007 -469.72864866256714 774.1127073764801 -504.73153591156006Q833.2930713891983 -539.7343635559082 910.8397722244263 -539.7343635559082Q988.5858058929443 -539.7343635559082 1047.7459192276 -504.7382712364197Q1106.9060325622559 -469.74217891693115 1140.1560068130493 -407.58535265922546Q1173.4059810638428 -345.4285264015198 1173.4059810638428 -263.69473338127136Q1173.4059810638428 -181.96094036102295 1140.1898622512817 -119.7880208492279Q1106.973773241043 -57.61510878801346 1047.695791721344 -22.612247616052628Q988.4178400039673 12.390613555908203 910.8639717102051 12.390613555908203ZM911.0285997390747 -79.81991767883301Q973.2852935791016 -79.81991767883301 1009.2756509780884 -129.25180792808533Q1045.2660083770752 -178.68371307849884 1045.2660083770752 -263.671875Q1045.2660083770752 -348.9454984664917 1009.254789352417 -398.23466539382935Q973.2435703277588 -447.523832321167 910.9868764877319 -447.523832321167Q848.7301826477051 -447.523832321167 812.7398252487183 -398.23466539382935Q776.7494678497314 -348.9454984664917 776.7494678497314 -263.671875Q776.7494678497314 -178.5506010055542 812.7606868743896 -129.1852593421936Q848.7719058990479 -79.81991767883301 911.0285997390747 -79.81991767883301Z"/>
+<path d="M1257.414174079895 190.4296875V-527.34375H1371.8510389328003V-419.99995708465576L1369.9135303497314 -427.6444911956787Q1394.9251890182495 -485.42964458465576 1442.1264052391052 -512.582004070282Q1489.327621459961 -539.7343635559082 1545.815896987915 -539.7343635559082Q1611.6127490997314 -539.7343635559082 1660.2729082107544 -506.46287202835083Q1708.9330673217773 -473.19138050079346 1735.6889367103577 -411.09567880630493Q1762.444806098938 -348.9999771118164 1762.444806098938 -262.8476619720459Q1762.444806098938 -177.0312786102295 1735.4447960853577 -115.26370048522949Q1708.4447860717773 -53.49612236022949 1659.4486951828003 -20.552754402160645Q1610.4526042938232 12.390613555908203 1544.3042039871216 12.390613555908203Q1488.5152101516724 12.390613555908203 1440.9253120422363 -14.244139194488525Q1393.3354139328003 -40.878891944885254 1365.9448146820068 -102.06639766693115L1377.132248878479 -104.29298877716064V190.4296875H1257.414174079895ZM1503.8709163665771 -81.66754245758057Q1561.7421388626099 -81.66754245758057 1597.4356055259705 -129.82779741287231Q1633.129072189331 -177.98805236816406 1633.129072189331 -265.4726505279541Q1633.129072189331 -351.03535652160645 1598.5645174980164 -398.43976497650146Q1563.9999628067017 -445.8441734313965 1504.8474788665771 -445.8441734313965Q1468.3669328689575 -445.8441734313965 1438.3942008018494 -426.4593720436096Q1408.4214687347412 -407.07457065582275 1390.8530592918396 -366.95533990859985Q1373.284649848938 -326.83610916137695 1373.284649848938 -264.8007869720459Q1373.284649848938 -172.7849245071411 1411.2671732902527 -127.22623348236084Q1449.2496967315674 -81.66754245758057 1503.8709163665771 -81.66754245758057Z"/>
+</g>
+</svg>
@@ design/brand/terminal/README.md
@@ -0,0 +1,18 @@
+# Terminal banner
+
+`pop onboard` prints `banner.txt` once, before the three lines that explain what is recorded.
+Four lines, 52 columns, one colour. The glyph is the terminal abstraction of the mark: two
+records linked through each other, the crossings drawn with `┼`.
+
+Rules
+
+- Print it exactly as stored; do not pad to the terminal width and do not centre it.
+- Use the default foreground colour only. No bold, no dim, no colour codes in the glyph.
+  The name may be bold; nothing else.
+- Fall back to `banner-ascii.txt` when the terminal does not report UTF-8
+  (`LANG`/`LC_ALL` without `UTF-8`, or Windows console code page other than 65001).
+  Both files use only characters that Menlo, SF Mono, Consolas and Cascadia Code carry;
+  rounded corners (`╭ ╮`) were deliberately avoided because Consolas lacks them.
+- `--quiet` and non-interactive runs (`!isatty`) skip the banner.
+- The name and descriptor are the only name-dependent characters. When the name changes,
+  replace the text after the glyph; the glyph stays.
@@ design/brand/terminal/banner-ascii.txt
@@ -0,0 +1,4 @@
+  +---+
+  | +-+-+   pop - Proof of Process
+  +-+-+ |   Records how your project came together.
+    +---+
@@ design/brand/terminal/banner.txt
@@ -0,0 +1,4 @@
+  ┌───┐
+  │ ┌─┼─┐   pop · Proof of Process
+  └─┼─┘ │   Records how your project came together.
+    └───┘
@@ design/brand/typography.md
@@ -0,0 +1,90 @@
+# Typography
+
+Two families. Nothing else is loaded, anywhere.
+
+| Role | Family | Axis / weights used | License | Source |
+|---|---|---|---|---|
+| Display, text, wordmark, UI | **Schibsted Grotesk** | variable `wght` 400–900; we use 400, 500, 600, 700 | SIL OFL 1.1 | Google Fonts, vendored in `design/brand/fonts/SchibstedGrotesk[wght].ttf` |
+| Code, hashes, timestamps, terminal | **Geist Mono** | variable `wght` 100–900; we use 400 and 500 | SIL OFL 1.1 | Google Fonts, vendored in `design/brand/fonts/GeistMono[wght].ttf` |
+
+Both are self-hostable (the OFL texts sit next to the files). Load them with `font-display: swap`
+and subset to Latin plus Latin Extended-A; Croatian needs č ć đ š ž, German needs ä ö ü ß.
+Fallback stack: `"Schibsted Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif` and
+`"Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace`.
+
+Note for `design/tokens.css`: it currently pairs Schibsted Grotesk (display) with Instrument Sans
+(text). The brand uses Schibsted Grotesk for both. Instrument Sans stays vendored for a side-by-side
+comparison until the user decides; see `README.md`, open question 3.
+
+## Why these two
@@ design/brand/voice.md
@@ -0,0 +1,85 @@
+# Voice and tone
+
+The product records how you work. The voice does the same thing: it states what happened, what
+is true, and what to do next. It does not sell, cheer or apologise.
+
+## Voice in five lines
+
+1. **Second person, present tense.** "You record", "the page shows". Never "we help you".
+2. **Short sentences.** One idea each. Two sentences per paragraph on the landing, three in docs.
+3. **Concrete nouns.** Commit, session, note, hash, page. Not "insights", "journey", "story".
+4. **Nothing is amazing.** No superlatives, no exclamation marks, no emoji, no "🎉" in a CLI.
+5. **Verification is stated, not promised.** The page is "verified" only when the chain checks.
+   Say what was checked and against what. Never "trusted", "guaranteed", "tamper-proof".
+
+## Tone by surface
+
+| Surface | Tone | Example |
+|---|---|---|
+| Landing | Plain, direct, one claim per block | "Show a company how you work with AI, on a project you actually built." |
@@ design/mock/context.json
@@ -1,4 +1,5 @@
 {
+  "_note": "Task E trimmed this fixture to the plan's \"Shared page context\" shape (docs/superpowers/plans/2026-09-17-pop-v1-plan.md). Removed: top-level \"patterns\" (the SVG bracket data) and the per-timeline-entry \"files\", \"model\" and \"accepted\" keys. The plan's timeline entry is {id, ts, kind, title, detail, sha, x} and cli/src/page/mod.rs::PageContext follows the plan, so those keys had no renderer. design/project-page.html still shows them; see the task E report.",
   "project": {
     "name": "pop",
     "what": "A Rust CLI that records how a project came together and turns it into one page a hiring manager reads in two minutes.",
@@ -25,21 +26,7 @@
       "title": "Set up a Rust CLI crate with clap derive and subcommands init, note, log",
       "detail": "Set up a Rust CLI crate named pop with clap derive. Subcommands: init, note, log. Keep main.rs thin, put each command in commands/<name>.rs. No async.",
       "sha": null,
-      "x": 0.0011,
-      "files": [
-        {
-          "path": "cli/src/main.rs",
-          "ai_lines": 38,
-          "ai_lines_modified_by_human": 4
-        },
-        {
-          "path": "cli/src/commands/mod.rs",
-          "ai_lines": 21,
@@ docs/deploy.md
@@ -0,0 +1,279 @@
+# Deploying popserver
+
+`popserver` runs on Railway, in one project, in Amsterdam. This page is the
+whole operational surface: what exists, what it is configured with, how to ship
+a change, how to rotate a secret, and how to prove the thing actually works.
+
+Hosting decision and the shape of the service: spec
+`docs/superpowers/specs/2026-09-17-pop-v1-design.md` section 5. Server details:
+`server/README.md`.
+
+## What exists
+
+| | name | id |
+|---|---|---|
+| workspace | vanjamodrinjak21's Projects | `35355c97-936d-42bd-8c52-62e23a98b1fb` |
+| project | `pop` | `a26eb01f-a99e-4195-860a-5f732a3fb613` |
+| environment | `production` | `6a886fad-f714-4b17-8d57-5bcf3006feef` |
+| service | `popserver` | `400cd214-31c7-4e3f-b010-5b54e7b1b97c` |
+| service | `Postgres` | `bcc3ff90-21fa-483f-9902-27561a90d4ad` |
@@ landing/src/components/hero/Hero.tsx
@@ -1,10 +1,34 @@
 "use client";
 
+import dynamic from "next/dynamic";
+import type { RefObject } from "react";
+
 /**
- * Hero background scene. Owned by the dedicated hero task; the landing
- * places it as a full-bleed layer behind the headline and install block.
- * This placeholder renders nothing until the scene lands.
+ * Hero background: a procedural wireframe megastructure (three.js) that
+ * assembles on load, flies up on scroll and drifts with the pointer. The
+ * layer is decorative, sits behind the copy and never takes pointer events.
  */
-export default function Hero() {
-  return <div aria-hidden className="absolute inset-0 -z-10" />;
+const HeroScene = dynamic(() => import("./HeroScene"), { ssr: false });
+
+export interface HeroProps {
+  /** Section whose height drives the scroll flight. Defaults to the parent element. */
@@ landing/src/components/hero/HeroScene.tsx
@@ -0,0 +1,207 @@
+"use client";
+
+import { useEffect, useRef, type RefObject } from "react";
+import gsap from "gsap";
+import { ScrollTrigger } from "gsap/ScrollTrigger";
+import { createHeroScene, mulberry32 } from "./scene";
+import { fallbackSvg } from "./fallback";
+import { readTheme, watchTheme } from "./theme";
+
+gsap.registerPlugin(ScrollTrigger);
+
+const ASSEMBLE_SECONDS = 2;
+const SPAWN_DISTANCE = 26; // px the pointer must travel between spawns
+const SPAWN_INTERVAL = 90; // ms
+
+export interface HeroSceneProps {
+  /** Element whose height the scroll flight is bound to. Defaults to the parent section. */
+  scrollTargetRef?: RefObject<HTMLElement | null>;
+}
@@ landing/src/components/hero/fallback.ts
@@ -0,0 +1,89 @@
+/**
+ * Static SVG frame for browsers without WebGL: the same generated structure,
+ * projected with a plain pinhole camera. Strokes use currentColor so the
+ * frame follows the theme through CSS alone.
+ */
+import { buildStructure, type Vec3 } from "./structure";
+
+const W = 1440;
+const H = 640;
+
+type Mat = number[]; // 3x3 row-major rotation
+
+function rotY(a: number): Mat {
+  const c = Math.cos(a);
+  const s = Math.sin(a);
+  return [c, 0, s, 0, 1, 0, -s, 0, c];
+}
+function rotX(a: number): Mat {
+  const c = Math.cos(a);
@@ landing/src/components/hero/scene.ts
@@ -0,0 +1,456 @@
+import {
+  BufferGeometry,
+  Color,
+  DoubleSide,
+  DynamicDrawUsage,
+  Float32BufferAttribute,
+  Group,
+  LineSegments,
+  Mesh,
+  PerspectiveCamera,
+  Scene,
+  ShaderMaterial,
+  SRGBColorSpace,
+  Vector3,
+  WebGLRenderer,
+} from "three";
+import { buildStructure, Builder, mulberry32, STRUCTURE_HEIGHT, type Vec3 } from "./structure";
+import type { Theme } from "./theme";
+
@@ landing/src/components/hero/structure.ts
@@ -0,0 +1,381 @@
+/**
+ * Deterministic megastructure: stacked decks, vertical sheets, screens,
+ * rails, box trusses, HUD fragments and debris along a vertical axis (+Y).
+ * Output is a pair of flat vertex buffers (line segments and quads) with the
+ * attributes the shaders need to assemble and shade the structure. No three.js
+ * here so the same data feeds the WebGL scene and the SVG fallback.
+ */
+
+export const STRUCTURE_HEIGHT = 76;
+
+export interface LineBuffers {
+  pos: number[]; // xyz per vertex
+  other: number[]; // xyz of the segment's other endpoint
+  t: number[]; // 0 = anchored endpoint, 1 = endpoint that draws in
+  key: number[]; // assembly stagger key, 0 = near camera, 1 = far up the axis
+  alpha: number[];
+  shade: number[]; // 0 = full ink, 1 = grey
+}
+
@@ landing/src/components/hero/theme.ts
@@ -0,0 +1,60 @@
+/** Reads --bg and --ink from the page and resolves them to sRGB triples. */
+
+export type Rgb = [number, number, number];
+
+export interface Theme {
+  bg: Rgb;
+  ink: Rgb;
+  dark: boolean;
+}
+
+let ctx: CanvasRenderingContext2D | null | undefined;
+
+function parseCss(color: string): Rgb | null {
+  if (ctx === undefined) {
+    const c = document.createElement("canvas");
+    c.width = c.height = 1;
+    ctx = c.getContext("2d", { willReadFrequently: true });
+  }
+  if (!ctx) return null;
18 Sep, 06:59decisionThin projects get a warning below 15 events, not a block.
18 Sep, 06:59commit4b669e9docs: brand and hero decisions confirmed by the user
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -172,7 +172,7 @@ Language: every user-facing string in the CLI, the project page and the default 
 
 ### 6.1a Brand (user instruction 2026-09-17)
 
-A full brand identity in `design/brand/`: name `pop` in lowercase with the descriptor "Proof of Process"; a geometric mark expressing a chained record (linked segments or nodes reading as an unbroken chain, the hash chain and the badge in one glyph) plus a lowercase wordmark; near black with white only; typography system, spacing and iconography rules, voice and tone (short, second person, professional, no superlatives, no emoji), usage do's and don'ts, favicon and app icon set, OG/social image, terminal banner for `pop onboard`, README badge, and a `DESIGN.md` other agents can read. Assets as SVG plus exported PNGs. The landing, project page and docs consume the brand; nothing there defines its own logo or colours.
+A full brand identity in `design/brand/`: name `pop` in lowercase with the descriptor "Proof of Process"; a geometric mark expressing a chained record (linked segments or nodes reading as an unbroken chain, the hash chain and the badge in one glyph) plus a lowercase wordmark; near black with white only; typography system, spacing and iconography rules, voice and tone (short, second person, professional, no superlatives, no emoji), usage do's and don'ts, favicon and app icon set, OG/social image, terminal banner for `pop onboard`, README badge, and a `DESIGN.md` other agents can read. Assets as SVG plus exported PNGs. The landing, project page and docs consume the brand; nothing there defines its own logo or colours. Decisions confirmed by the user on 2026-09-18: the square-through-ring mark stays; one sans family (Schibsted Grotesk) for display and text with Geist Mono for code, so `design/tokens.css` drops Instrument Sans and adopts the brand's `--ink-3` contrast values; favicons and app icons on a near-black tile with a white mark; descriptor stays title case "Proof of Process"; both dark and light badge variants ship. The hero scene in `landing/src/components/hero/` is accepted as the direction; polish (density, idle drift, light-mode fill weight) happens in the final landing pass. Name: an English name is pending from the naming study; `pop` remains the placeholder and every name-bearing asset must be regenerable (`design/brand/logo/build.py --name`).
 
 ### 6.2 Documentation site (`landing/docs/`)
 
18 Sep, 06:59decisionOne sans family, Schibsted Grotesk, for display and text; Geist Mono for anything that is data.
18 Sep, 06:59commitb6eedfffix(cli): re-upload the confirmed page so the public link carries the badge
@@ cli/src/commands/publish.rs
@@ -9,10 +9,13 @@
 //! 4. render `.pop/site/index.html` — always, so `--local` has produced
 //!    something even when the network is down,
 //! 5. sign the chain root and POST the page, then render it once more with the
-//!    server's confirmation in the badge.
+//!    server's confirmation in the badge and POST that to the same slug, so
+//!    the page the company opens carries the badge rather than a draft.
 //!
 //! Nothing between step 3 and step 5 appends to the log, so the root the page
-//! shows is the root that gets signed.
+//! shows is the root that gets signed — both times. The second upload is the
+//! only part of this that is allowed to fail quietly: by then the page is
+//! already public, and a warning beats a non-zero exit.
 
 use std::path::{Path, PathBuf};
 
@@ -217,6 +220,11 @@ fn upload(ctx: &mut Ctx<'_>, root: &Path, page_path: &Path, server: Option<&str>
         }),
     )?;
 
@@ cli/tests/publish.rs
@@ -39,6 +39,9 @@ struct State {
     /// Answer the next `POST /v1/pages` with 404, as a server with a fresh
     /// database would.
     forget_device_once: bool,
+    /// Fall over on the next upload that carries a slug — that is, on the
+    /// confirmation that follows a successful first publish.
+    break_the_confirmation: bool,
 }
 
 struct FakeServer {
@@ -75,6 +78,10 @@ impl FakeServer {
         self.state.lock().unwrap().forget_device_once = true;
     }
 
+    fn break_the_confirmation(&self) {
+        self.state.lock().unwrap().break_the_confirmation = true;
+    }
+
     fn requests_to(&self, path: &str) -> Vec<Seen> {
         self.requests()
18 Sep, 06:59commitcb02835docs: product name decided: madelog
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -172,7 +172,7 @@ Language: every user-facing string in the CLI, the project page and the default 
 
 ### 6.1a Brand (user instruction 2026-09-17)
 
-A full brand identity in `design/brand/`: name `pop` in lowercase with the descriptor "Proof of Process"; a geometric mark expressing a chained record (linked segments or nodes reading as an unbroken chain, the hash chain and the badge in one glyph) plus a lowercase wordmark; near black with white only; typography system, spacing and iconography rules, voice and tone (short, second person, professional, no superlatives, no emoji), usage do's and don'ts, favicon and app icon set, OG/social image, terminal banner for `pop onboard`, README badge, and a `DESIGN.md` other agents can read. Assets as SVG plus exported PNGs. The landing, project page and docs consume the brand; nothing there defines its own logo or colours. Decisions confirmed by the user on 2026-09-18: the square-through-ring mark stays; one sans family (Schibsted Grotesk) for display and text with Geist Mono for code, so `design/tokens.css` drops Instrument Sans and adopts the brand's `--ink-3` contrast values; favicons and app icons on a near-black tile with a white mark; descriptor stays title case "Proof of Process"; both dark and light badge variants ship. The hero scene in `landing/src/components/hero/` is accepted as the direction; polish (density, idle drift, light-mode fill weight) happens in the final landing pass. Name: an English name is pending from the naming study; `pop` remains the placeholder and every name-bearing asset must be regenerable (`design/brand/logo/build.py --name`).
+A full brand identity in `design/brand/`: name `pop` in lowercase with the descriptor "Proof of Process"; a geometric mark expressing a chained record (linked segments or nodes reading as an unbroken chain, the hash chain and the badge in one glyph) plus a lowercase wordmark; near black with white only; typography system, spacing and iconography rules, voice and tone (short, second person, professional, no superlatives, no emoji), usage do's and don'ts, favicon and app icon set, OG/social image, terminal banner for `pop onboard`, README badge, and a `DESIGN.md` other agents can read. Assets as SVG plus exported PNGs. The landing, project page and docs consume the brand; nothing there defines its own logo or colours. Decisions confirmed by the user on 2026-09-18: the square-through-ring mark stays; one sans family (Schibsted Grotesk) for display and text with Geist Mono for code, so `design/tokens.css` drops Instrument Sans and adopts the brand's `--ink-3` contrast values; favicons and app icons on a near-black tile with a white mark; descriptor stays title case "Proof of Process"; both dark and light badge variants ship. The hero scene in `landing/src/components/hero/` is accepted as the direction; polish (density, idle drift, light-mode fill weight) happens in the final landing pass. Name (user decision 2026-09-18): **madelog**. Verified free on madelog.dev, .app, .sh, .io, crates.io, npm, Homebrew, PyPI and the GitHub handle `madelog`. Domains to register by the user: `madelog.dev` (site, docs, install) and `madelog.sh` (install one-liner alias). Rename plan (one coordinated task after the landing and docs land): crate and binary `madelog`; project dir `.madelog/`; config dir `~/.config/madelog`; env vars `MADELOG_*` (former `POP_*`); hook line `madelog hook post-commit`; all CLI text, docs, landing copy and terminal mocks; brand wordmark and lockups regenerated with `design/brand/logo/build.py --name madelog`; Go module `github.com/madelog/server`, Railway service renamed `madelog-server`; repo `github.com/vanjamodrinjak21/madelog`; install URLs `https://madelog.dev/install.sh` and `install.ps1`. Descriptor stays "Proof of Process".
 
 ### 6.2 Documentation site (`landing/docs/`)
 
18 Sep, 06:59commita5a4d13docs: primary domain madelog.app
@@ docs/superpowers/specs/2026-09-17-pop-v1-design.md
@@ -172,7 +172,7 @@ Language: every user-facing string in the CLI, the project page and the default 
 
 ### 6.1a Brand (user instruction 2026-09-17)
 
-A full brand identity in `design/brand/`: name `pop` in lowercase with the descriptor "Proof of Process"; a geometric mark expressing a chained record (linked segments or nodes reading as an unbroken chain, the hash chain and the badge in one glyph) plus a lowercase wordmark; near black with white only; typography system, spacing and iconography rules, voice and tone (short, second person, professional, no superlatives, no emoji), usage do's and don'ts, favicon and app icon set, OG/social image, terminal banner for `pop onboard`, README badge, and a `DESIGN.md` other agents can read. Assets as SVG plus exported PNGs. The landing, project page and docs consume the brand; nothing there defines its own logo or colours. Decisions confirmed by the user on 2026-09-18: the square-through-ring mark stays; one sans family (Schibsted Grotesk) for display and text with Geist Mono for code, so `design/tokens.css` drops Instrument Sans and adopts the brand's `--ink-3` contrast values; favicons and app icons on a near-black tile with a white mark; descriptor stays title case "Proof of Process"; both dark and light badge variants ship. The hero scene in `landing/src/components/hero/` is accepted as the direction; polish (density, idle drift, light-mode fill weight) happens in the final landing pass. Name (user decision 2026-09-18): **madelog**. Verified free on madelog.dev, .app, .sh, .io, crates.io, npm, Homebrew, PyPI and the GitHub handle `madelog`. Domains to register by the user: `madelog.dev` (site, docs, install) and `madelog.sh` (install one-liner alias). Rename plan (one coordinated task after the landing and docs land): crate and binary `madelog`; project dir `.madelog/`; config dir `~/.config/madelog`; env vars `MADELOG_*` (former `POP_*`); hook line `madelog hook post-commit`; all CLI text, docs, landing copy and terminal mocks; brand wordmark and lockups regenerated with `design/brand/logo/build.py --name madelog`; Go module `github.com/madelog/server`, Railway service renamed `madelog-server`; repo `github.com/vanjamodrinjak21/madelog`; install URLs `https://madelog.dev/install.sh` and `install.ps1`. Descriptor stays "Proof of Process".
+A full brand identity in `design/brand/`: name `pop` in lowercase with the descriptor "Proof of Process"; a geometric mark expressing a chained record (linked segments or nodes reading as an unbroken chain, the hash chain and the badge in one glyph) plus a lowercase wordmark; near black with white only; typography system, spacing and iconography rules, voice and tone (short, second person, professional, no superlatives, no emoji), usage do's and don'ts, favicon and app icon set, OG/social image, terminal banner for `pop onboard`, README badge, and a `DESIGN.md` other agents can read. Assets as SVG plus exported PNGs. The landing, project page and docs consume the brand; nothing there defines its own logo or colours. Decisions confirmed by the user on 2026-09-18: the square-through-ring mark stays; one sans family (Schibsted Grotesk) for display and text with Geist Mono for code, so `design/tokens.css` drops Instrument Sans and adopts the brand's `--ink-3` contrast values; favicons and app icons on a near-black tile with a white mark; descriptor stays title case "Proof of Process"; both dark and light badge variants ship. The hero scene in `landing/src/components/hero/` is accepted as the direction; polish (density, idle drift, light-mode fill weight) happens in the final landing pass. Name (user decision 2026-09-18): **madelog**. Verified free on madelog.dev, .app, .sh, .io, crates.io, npm, Homebrew, PyPI and the GitHub handle `madelog`. Primary domain (user is registering it on 2026-09-18): `madelog.app` for site, docs and install; `madelog.dev` and `madelog.sh` were also free and are optional extras. Rename plan (one coordinated task after the landing and docs land): crate and binary `madelog`; project dir `.madelog/`; config dir `~/.config/madelog`; env vars `MADELOG_*` (former `POP_*`); hook line `madelog hook post-commit`; all CLI text, docs, landing copy and terminal mocks; brand wordmark and lockups regenerated with `design/brand/logo/build.py --name madelog`; Go module `github.com/madelog/server`, Railway service renamed `madelog-server`; repo `github.com/vanjamodrinjak21/madelog`; install URLs `https://madelog.app/install.sh` and `install.ps1`. Descriptor stays "Proof of Process".
 
 ### 6.2 Documentation site (`landing/docs/`)
 

Four moments

A decision, then a new design/mock/

You wrote down a decision: "Two install modes: system-wide to ~/.local/bin, or per project to .pop/bin so the hook calls that path and PATH stays un...". The next commit, 41a88dd, added 1 file under design/mock/. It was called "docs: Git AI note format research, docs site section and tas...".

18 Sep decision · 18 Sep commit

A decision, then a new cli/src/commands/

You wrote down a decision: "Git AI carries capture; madelog starts at decisions, patterns and the page. No keylogger, no editor plugin". The next commit, 059004c, added 7 files under cli/src/commands/. It was called "docs: landing and docs site on Next.js + TypeScript (static...".

18 Sep decision · 18 Sep commit

A decision, then a new .github/

You wrote down a decision: "Hash chain input is prev_hash, ts, kind and canonical JSON, newline separated; pattern and summary events join the chain...". The next commit, 2c0ccb8, added 1 file under .github/. It was called "chore: README, MIT license, CI and release workflows, Homebr...".

18 Sep decision · 18 Sep commit

A decision, then a new cli/src/page/

You wrote down a decision: "Weekly summaries come from Ollama with a heuristic fallback; a remote model is opt-in and the page marks generated text". The next commit, a4ab0ba, added 1 file under cli/src/page/. It was called "chore(cli): page module contract stub for the generator and...".

18 Sep decision · 18 Sep commit

Code attribution

No AI attribution was recorded for this project. Without Git AI notes there is no per-line record of what a model proposed, so this page does not guess.

Not yet published

Records from 18 Sep 2026, 06:59 UTC to 18 Sep 2026, 06:59 UTC, root 4a1d5919f5b5. This page was rendered locally with madelog publish --local. The root has not been confirmed by a server, so the record cannot be checked by a third party yet.

4a1d5919f5b5d6edc830d97dbdd6660ef5c25dd21e7bb8ad906bf87b64369371

Generated by madelog from a local hash-chained log. Paragraphs marked generated were written by a model and reviewed by the author. Raw prompts and full diffs never left the author's machine.