diff --git a/.convoys/pick-a-name.md b/.convoys/pick-a-name.md
index 478eb39..eca28c2 100644
--- a/.convoys/pick-a-name.md
+++ b/.convoys/pick-a-name.md
@@ -1,11 +1,12 @@
# pick-a-name (P1 — pre-launch brand consistency)
-**Status:** in-progress
+**Status:** shipped
**Priority:** P1 (pre-launch; not a security blocker, but a brand-consistency
blocker that touches every user-facing surface)
**Convoy owner:** parent (orchestration); `role-architect` then
`role-implementer(s)` then `role-doc-writer`
**Opened:** 2026-05-24
+**Shipped:** 2026-05-24
**Operator decision (gate-0, already ratified):** **Deck Hearth wins.**
## Problem
@@ -593,4 +594,316 @@ Two NEW follow-ups surfaced by architect inventory:
### As-shipped
-(To be appended post-merge by `role-doc-writer`.)
+Shipped 2026-05-24 as squash commit `9abbab6` (PR #21, architect-commit
+`50ce9ab`, Brief 1 commit `ac8c998`, Brief 2 commit `1c18d21`). **This is
+the first post-P0 convoy** — `add-rate-limiting` (squash `708ef45`, PR
+#20, 2026-05-24) closed the LAST P0 ship-blocker, flipping
+`.convoys/ship-readiness.md`'s § Status summary to "8 of 8 RESOLVED";
+this convoy opens the post-P0 P1 quality-bar phase by closing the
+brand-consistency P1 (#12 in the same file) that AGENTS.md line 5 has
+flagged since project setup. Two briefs in the convoy: B1
+(display + comment sweep) and B2 (infrastructure + email migration)
+shipped without scope expansions and with all five D-decisions ratified
+verbatim at gate-1 plus Risk 4 PRESERVE applied to the negative
+regression-lock test.
+
+#### Decisions ratified at gate-1
+
+All five canonical-string choices required operator gate-1 ratification
+(naming choices are not architect-self-ratifiable per the convoy file's
+§ Architect-proposed decisions header). Operator ratified all five
+verbatim:
+
+- **D1 — Display string:** **"Deck Hearth"** (two words, internal cap,
+ space-separated). Avoids re-sweeping the seven user-facing surfaces
+ that already render the two-word form (`components/Layout.js`,
+ `components/AnimatedFireLogo.js`, `pages/index.js`, `pages/login.js`
+ welcome copy, `pages/signup.js`, `components/ShareModal.js`,
+ `styles/globals.css`).
+- **D2 — Kebab string:** **`deck-hearth`**. Matches the existing
+ `deck-hearth-logo-container` CSS class + Node-ecosystem convention for
+ `package.json` `name` fields. Used in `package.json` rename + the
+ three `User-Agent` HTTP product tokens in `pages/api/cards/import-*.js`
+ + `scripts/import-lorcana.js` (`Deck-Hearth/1.0`, hyphenated lowercase
+ per HTTP UA convention).
+- **D3 — Camel/lower for IDs:** **`deckhearth`** (single token, no
+ separators). Used in the five Redis key prefixes
+ (`deckhearth:auth/search/upload/generate/import`) — full word, NOT the
+ abbreviated `dh` (the ~800KB Redis-memory savings at peak load is
+ negligible against an Upstash plan that bills by request count, and
+ the self-documenting prefix is materially easier to debug in Redis
+ console output).
+- **D4 — Admin email:** **`admin@deckhearth.com`** (placeholder TLD
+ pending the queued `point-domain-at-deckhearth` convoy). Sweep
+ applied to all three `@tcgvault.com` accounts uniformly via
+ `REPLACE(email, '@tcgvault.com', '@deckhearth.com')`: admin + alice
+ + bob (the test users created by `scripts/create-test-users.js`).
+- **D5 — Redis prefix:** **full `deckhearth`**. Pattern:
+ `deckhearth:auth`, `deckhearth:search`, `deckhearth:upload`,
+ `deckhearth:generate`, `deckhearth:import`. The five-prefixes-must-be-
+ distinct invariant from `add-rate-limiting` Brief 1 is preserved.
+
+**Risk 4 PRESERVE applied:** the architect's recommendation
+(`.convoys/pick-a-name.md` § Risk list § 4) was to preserve the
+historical `admin@tcgvault.com` literal in
+`test/lib/permission-middleware.test.js` line 87's negative regression
+assertion as the EXACT pre-`fix-auth-bypass` synthetic-admin shape this
+test locks against. The implementer kept the literal and added a
+7-line comment block above the `.not.toEqual({...})` call documenting
+why future doc-writers / refactorers must NOT update the literal to the
+new `@deckhearth.com` email — doing so would weaken the regression-lock
+to a shape that never actually existed. **This matches the project's
+established pattern** of treating historical-shape locks as audit-trail
+artifacts rather than self-consistency markers (Gotchas #2, #3, #4, #5,
+#8 in `AGENTS.md` all note "Entry kept (not renumbered) to preserve
+cross-references" for the same reason). The post-merge state has
+exactly **1** `@tcgvault.com` literal outside of `.convoys/**` and
+`*.md` files — the preserved historical regression-lock.
+
+#### Brief structure (file-disjoint; serial-in-practice)
+
+The architect designed B1 (7 files: `AGENTS.md` line 5, two
+`.cursor/rules/*.mdc` description / branding paragraphs, `scripts/README.md`
+title, three `User-Agent` strings) and B2 (10 modified + 1 new
+migration script: `lib/rate-limit.js` 5 prefix renames, `package.json`
+`name` rename, `package-lock.json` regen, `README.md` + `TESTING_GUIDE.md`
+title + email + operator caveat, three seed/reset/test-user scripts'
+admin/alice/bob email rename, `pages/login.js` quick-login fixtures,
+`test/lib/permission-middleware.test.js` Risk 4 PRESERVE comment block,
+new `scripts/migrations/2026-05-24-rename-admin-email.js`) so that the
+two `files:` sets are DISJOINT and the conductor could dispatch them in
+parallel via `/multitask`. **In practice the two briefs landed
+serially** (B1 commit `ac8c998` first, B2 commit `1c18d21` second on
+the same branch) for the lockfile-regen + migration-script-author
+caution recommended by Brief 2 § Risk 3 (lockfile STOP-on-churn
+verification is easier to audit if no other diff is in flight). The
+parallel-via-multitask path stayed open as the architect's intended
+fallback if either brief had hit a blocker.
+
+#### As-shipped diff
+
+Per `git show --stat 9abbab6`:
+
+- **B1 (`ac8c998`): 7+/7-** across 7 files (the 7 `files:` listed in
+ `.convoys/pick-a-name/brief-1-display-and-comment-sweep.md`). Pure
+ string-literal edits; no new code, no new imports, zero ESLint
+ surface added.
+- **B2 (`1c18d21`): 133+/30-** across 10 modified files + 1 new
+ migration script. The +/- imbalance is dominated by the new
+ ~85-line migration script (`scripts/migrations/2026-05-24-rename-
+ admin-email.js`) + the new operator-caveat blockquote in `README.md`
+ (~10 lines added) + the 7-line Risk 4 PRESERVE comment block in
+ `test/lib/permission-middleware.test.js`. Source-code edits in
+ `lib/rate-limit.js` (5 prefix lines) + `pages/login.js` (2 quick-
+ login fixtures) + `scripts/setup-neon-db.js` / `reset-db.js` /
+ `create-test-users.js` (admin/alice/bob email INSERTs + console
+ logs) are pure string-literal swaps.
+- **Lockfile regen:** the `package-lock.json` diff was confirmed
+ before B2 commit to touch ONLY the two `name` lines (top-level +
+ `packages[""]` entry); the brief's STOP-on-churn rule (Risk 3) was
+ exercised by the implementer and confirmed clean — no opportunistic
+ dependency-metadata refresh, no integrity-hash churn, no `version`
+ bumps. `npm ci` succeeded against the regenerated lockfile.
+
+#### As-shipped CI metrics
+
+Post-merge run 26387909625 + subsequent runs on `main`:
+
+- **`Playwright smoke` — PASS in 1m4s, 3/3 tests** against the rebranded
+ Vercel preview. Same three checks (`home redirects or renders without
+ 5xx` ✓ / `sign-in page renders` ✓ / `public health endpoint responds`
+ ✓) — all green. The new brand strings render correctly on `/` +
+ `/login`; the auth surface still works (smoke test 2 still asserts
+ the `Sign in` CTA wording, which is
+ brand-agnostic per architect-time verification of the smoke spec's
+ text matchers); no false 429s from the renamed Redis prefixes (the
+ post-rename `deckhearth:*` keys were brand-fresh in Upstash, but the
+ smoke spec's `/api/health` hit is anonymous and unrate-limited and
+ the home + sign-in routes don't touch any of the 6 newly-gated
+ rate-limit endpoints).
+- **`Aggregate gate` — PASS in 10s.** All upstream gate dependencies
+ green.
+- **`Lint` — PASS in 45s, 128 problems** (lint baseline preserved, no
+ regression from B1 or B2). Zero new lint problems; `|| true` wrapper
+ in `.github/workflows/ci.yml` was a no-op for this convoy.
+- **`forbidden-endpoints` (from `fix-auth-bypass` Brief 3) — PASS in
+ 4s.** No new `pages/api/test-*.js` shapes reintroduced.
+- **`forbidden-cors-headers` (from `cors-tighten`) — PASS in 4s.** Pure
+ brand-rename diff; no `Access-Control-Allow-*` headers reintroduced.
+- **`Unit tests (vitest)` — PASS, 21/21 in 35s.** The Risk 4 PRESERVE
+ comment-block addition kept the negative regression-lock test green
+ byte-identically — the assertion semantics are unchanged; only the
+ comment block above the `.not.toEqual({...})` call is new. Decision
+ 6 (no new vitest specs in this convoy) honored per the
+ `add-rate-limiting` D6 + `cors-tighten` D4 precedent.
+- **`Screenshot diff` — PASS in 1m5s** (`continue-on-error: true`
+ swallow per `adopt-playwright-smoke` Decision 4 still active; the
+ rebrand introduced no visual regression because no baseline is
+ committed yet — the queued `seed-visual-baselines-on-linux` convoy
+ generates the FIRST baseline and that baseline will capture the
+ Deck Hearth brand strings, per the architect's ordering note in
+ `.convoys/pick-a-name.md` § Test plan + the updated entry in
+ `.convoys/ship-readiness.md` § Queued convoys).
+- **`Schema map up to date`** — skipping (only runs on Prisma changes;
+ this convoy made an INSERT-semantics change but no DDL change).
+- **Vercel deployment** — completed cleanly. First production-tier
+ Vercel deploy serving the `deck-hearth` `package.json` `name`.
+
+#### Cross-validation finding: smoke spec defends the brand-rename surface (organically)
+
+This convoy is the **fourth in a row** where the same 3-test Playwright
+smoke spec authored by `adopt-playwright-smoke` (PR #18, 2026-05-24)
+defended the auth surface through a sweeping change without anyone
+having to write a dedicated test. The lineage now reads:
+
+- **PR #15** (`fix-layout-default-user`, `ca302a8`) — introduced the
+ `Sign in` CTA that smoke test 2 asserts on.
+- **PR #19** (`cors-tighten`, `da50d78`) — removed wildcard CORS from
+ 24 handlers; smoke test 2 still passed against the post-CORS preview.
+- **PR #20** (`add-rate-limiting`, `708ef45`) — wired 6 new route gates
+ (search/upload/generate/import) and refactored `lib/rate-limit.js`
+ end-to-end; smoke test 2 still passed (the new 60/min search ceiling
+ doesn't 429 the smoke spec because home + `/login` don't touch any
+ gated route).
+- **PR #21** (`pick-a-name`, `9abbab6`) — this convoy — renamed the
+ product brand end-to-end across 17 files + 1 new migration script,
+ including all 5 Redis key prefixes; smoke test 2 still passes.
+
+The smoke spec's text matchers (`'sign in'` regex, `/api/health`,
+HTTP status < 500) are brand-agnostic by architect-time design — but
+**organically**, smoke is doing real work: it has now defended the
+auth surface against four sweeping changes spanning the full P0 #5
++ P0 #6 + P0 #7 closures + the P1 #12 brand-consistency closure.
+That's a "trust the smoke spec" data point worth preserving. Future
+sweeping changes (single-auth-provider, single-sql-client, the
+god-component-split convoys) should expect the smoke spec to keep
+catching auth-surface regressions without dedicated coverage —
+unless and until a feature change deliberately rewords the
+`Sign in` CTA, in which case the smoke spec needs a
+matcher update in the same PR.
+
+#### Implementer-vs-architect calibration note
+
+The architect's B2 forecast paragraph (`.convoys/pick-a-name/brief-2-
+infrastructure-and-email-migration.md` line 662, the post-B2
+repo-wide grep cross-check) reads *"Expected: at most 1 match —
+`test/lib/permission-middleware.test.js` line 87's preserved historical
+regression literal"*. Architect-verified — the implementer's actual
+post-B2 state showed exactly 1 non-`.convoys/`-non-`*.md` match. **But
+the architect's authoritative § Full surface inventory § Convoy
+artifacts + manifest list (line 344 of the convoy file)** had already
+correctly enumerated `scripts/seed-collections-with-cards.js` and
+`scripts/seed-collections-alice-bob.js` (3 hits each) as DO-NOT-TOUCH
+historical seed jobs per `.cursor/rules/no-go-zones.mdc` § Append-only
+/ historical, with the explicit reasoning: *"the email migration in B2
+covers the in-DB rename, so these scripts' WHERE-clause literals would
+simply match zero rows post-migration if re-run, which is a harmless
+no-op."* So the forecast paragraph and the inventory are both correct
+— the forecast counted "in-scope source files" while the inventory
+documented why the seed-collections matches stay. Worth noting for the
+next architect: when forecasting post-brief grep-cleanliness, cite the
+inventory's exact carve-out rationale to avoid a reader thinking the
+two numbers contradict each other. Not a process gap — both ends were
+right; the cross-reference between them could be tighter.
+
+#### Operator action required going forward
+
+**Run `node scripts/migrations/2026-05-24-rename-admin-email.js`
+against the prod Neon DB before any admin login attempt with the new
+`admin@deckhearth.com` email.** The migration is ESM, idempotent,
+UNIQUE-collision-safe — re-running on an already-migrated DB prints
+"Nothing to migrate." and exits 0; running on a DB where the seed
+script already created a duplicate `admin@deckhearth.com` row fails
+loud on the `users.email` UNIQUE constraint (which is the correct
+fail-loud behavior — manually delete one of the duplicate admin rows
+before retrying). **Order: migration FIRST, then any subsequent
+`npm run setup-db`** — the migration sweeps the existing
+`@tcgvault.com` row to `@deckhearth.com`; a subsequent `setup-db`
+sees the row exists with the new email and no-ops via `ON CONFLICT
+(email) DO NOTHING`.
+
+Documented in three places for redundancy:
+
+- `AGENTS.md` Gotcha #4 (extended in this doc-writer pass with the
+ email-rename + migration-script + ordering paragraph).
+- `README.md` operator caveat blockquote (added by B2 alongside the
+ existing `admin123`-rotation caveat from `drop-public-setup`).
+- The migration script's own header comment + stdout messages.
+
+Two downstream convoys are queued in `.convoys/ship-readiness.md`
+§ Queued convoys after this one:
+
+- **`rename-repo-and-vercel-project`** (P2 polish) — GitHub repo +
+ Vercel project rename to `deck-hearth`; auto-redirects make it
+ low-urgency. One-line update to local git remotes + Vercel link
+ + the 8 architect-verified `tcg-vault` literal-repo references
+ (README cd / tree, AGENTS.md line 1, three workflow YAML comments,
+ `.agent-context-manifest.yml` source tags).
+- **`point-domain-at-deckhearth`** (blocked on domain acquisition)
+ — DNS pointer + Vercel domain claim + admin email TLD swap if the
+ operator buys a TLD other than `.com`.
+
+#### What did NOT change
+
+Audit trail of files explicitly NOT touched by this convoy, despite
+sitting near the brand-rename surface:
+
+- **All `.convoys/**` historical artifacts.** ~25 lines across the
+ drop-public-setup / fix-auth-bypass / cors-tighten / add-rate-limiting
+ / fix-layout-default-user / bump-next-js / fix-vercel-deployment-
+ protection convoy files reference `tcgvault` / `tcg-vault` /
+ `TCG Vault` as the historical state at write-time — per
+ `.cursor/rules/no-go-zones.mdc` § Append-only / historical, these
+ stay byte-identical. The doc-writer pass at THIS convoy's close
+ rewrites only `.convoys/pick-a-name.md` (this file) + Gotcha #4 +
+ Gotcha #12 of `AGENTS.md` + the launch-sequence step in
+ `.convoys/ship-readiness.md` + the lineage attribution in
+ `.cursor/rules/api-routes.mdc`. No retroactive historical rewrite.
+- **`scripts/seed-collections-with-cards.js`,
+ `scripts/seed-collections-alice-bob.js`,
+ `scripts/add-*.js`, `scripts/fix-*.js`** — historical seed /
+ migration jobs per the no-go-zones rule. The 6 hits (3 each in the
+ two seed-collections scripts) of `@tcgvault.com` literals in their
+ WHERE-clauses are now stale — the migration sweeps the in-DB rows,
+ so any re-run of these scripts would match zero rows and no-op
+ harmlessly.
+- **Repo-name references** — `README.md` lines 30 + 105, `AGENTS.md`
+ line 1, `.github/workflows/ci.yml` lines 12 + 123,
+ `.github/workflows/visual-diff.yml` line 5,
+ `.agent-context-manifest.yml` source tags. All literally describe
+ the GitHub repo + Vercel project, both of which still ARE named
+ `tcg-vault` until the queued `rename-repo-and-vercel-project` convoy
+ ships. Per the convoy seed § *"Keep the repo `tcg-vault` for now"*.
+- **`docs/agent-context/README.md` line 78** (`"we'll measure
+ tcg-vault's number after a few convoys"`) and
+ **`.cursor/skills/add-api-route/SKILL.md` line 7**
+ (`"…for the tcg-vault stack"`) — both reference the literal repo
+ name, not the product brand. Stay byte-identical; they sweep
+ together with `rename-repo-and-vercel-project`.
+- **`scripts/reset-db.js` line 142** (CJS-in-ESM bug) and **lines
+ 143 + 156** (`admin123` weak password literals) — out of scope
+ per `convert-reset-db-to-esm` (now queued, surfaced 2026-05-24
+ during this convoy's architect inventory) + `purge-weak-creds-
+ from-helpers`. B2 only renamed the email literals on lines 147 +
+ 155; the CJS bug + weak-password literals stay byte-identical for
+ the dedicated follow-up convoys to fix together.
+- **`tests/smoke/**`, `tests/visual/**`, `playwright.config.js`**
+ — smoke spec is brand-agnostic (architect-verified at brief time);
+ visual baseline doesn't exist yet (per
+ `adopt-playwright-smoke` Decision 4). The queued
+ `seed-visual-baselines-on-linux` convoy generates the first
+ baseline AFTER `pick-a-name` lands so the captured strings are
+ Deck Hearth (now satisfied — `9abbab6` is on `main`).
+- **`.agent-context-manifest.yml` `source: "tcg-vault-local"` tags**
+ (6 entries) — internal `sync-agent-context` skill source identifier
+ for drift tracking. Renaming could break the skill's behavior;
+ flagged for re-evaluation in `rename-repo-and-vercel-project` when
+ the actual repo name changes (Risk 5 of the convoy file).
+- **`pages/api/cards/import-mtg.js`'s `User-Agent`** — the file does
+ NOT set a custom UA (Scryfall is fine with Node's default per
+ Brief 1 Finding 2). Only the Lorcana + Pokémon TCG import handlers
+ + the standalone `scripts/import-lorcana.js` CLI variant set
+ `Deck-Hearth/1.0`.
+- **No new vitest or playwright specs** — Decision 6 honored. The
+ Risk 4 PRESERVE comment-block addition is the only test-file edit;
+ assertion semantics are unchanged.
diff --git a/.convoys/ship-readiness.md b/.convoys/ship-readiness.md
index 9d30c63..7f419db 100644
--- a/.convoys/ship-readiness.md
+++ b/.convoys/ship-readiness.md
@@ -36,6 +36,11 @@ single auth provider, lint baseline cleanup, brand decision, test
coverage expansion) plus the P2 / P3 polish lanes in this file's
Queued convoys section. None of those are P0 ship-blockers.
+**P1 quality-bar work in progress** (pick-a-name shipped 2026-05-24,
+PR #21 squash `9abbab6`, closing P1 #12 brand-consistency — the
+inconsistency `AGENTS.md` line 5 had flagged since project setup;
+single-auth-provider, single-sql-client, migration-tool queued).
+
## P0 — ship-blockers (security)
These MUST land before any anonymous traffic touches the production URL.
@@ -338,7 +343,7 @@ Each phase is one Conductor-created convoy. Don't run more than two in parallel
3. **`fix-layout-default-user`** (P0 #7). One PR. Trivial.
3.5. **`fix-lint-baseline`** (P1 #11.5). 2-4 PRs via multitask. Closes the lint gate (drops `continue-on-error`).
4. **`add-rate-limiting`** (P0 #6 full). One PR. Adds @upstash/ratelimit + applies to listed routes. **RESOLVED 2026-05-24** — PR #20 squash `708ef45`; closes P0 #6 (last open P0), flipping the ship-blocker set to **8/8 RESOLVED**. 5 named limiters (auth/search/upload/generate/import), 6 routes newly gated + the 3 import routes auth-gated atomically with a `pages/admin/card-import.js` Bearer-header fix. Smoke 3/3 green in 3.8s post-merge — confirms the new 60/min search limiter doesn't 429 the smoke spec. See § Queued convoys and P0 #6 above for the full as-shipped block.
-5. **`pick-a-name`** (P1 #12). Human decision first, then one or two PRs.
+5. **`pick-a-name`** (P1 #12). Human decision first, then one or two PRs. **RESOLVED 2026-05-24** — PR #21 squash `9abbab6`; closes P1 #12 (brand-consistency, the inconsistency `AGENTS.md` line 5 had flagged since project setup). Two file-disjoint briefs landed serially (B1 commit `ac8c998` display + comment sweep across 7 files; B2 commit `1c18d21` infrastructure + email migration across 10 modified + 1 new migration script). Five canonical-string D-decisions ratified verbatim at gate-1 (Deck Hearth / `deck-hearth` / `deckhearth` / `admin@deckhearth.com` / full `deckhearth` Redis prefix) plus Risk 4 PRESERVE on `test/lib/permission-middleware.test.js` line 87's negative regression-lock literal. Smoke 3/3 green in 1m4s post-merge — fourth convoy in a row (PR #15 → #19 → #20 → #21) where the same 3-test smoke spec defends the auth surface through a sweeping change. **Operator action required:** run `node scripts/migrations/2026-05-24-rename-admin-email.js` against prod Neon BEFORE the next admin login attempt with the new email (idempotent, UNIQUE-collision-safe). See § Queued convoys for the downstream `rename-repo-and-vercel-project` + `point-domain-at-deckhearth` + `regenerate-brand-assets` follow-ups, and `.convoys/pick-a-name.md` § As-shipped for the full record.
6. **`adopt-vitest`** (P1 #10 step 1). One PR. Enables testing every future change.
7. **`migration-tool`** (P1 #11). One PR. Backfill + first new migration.
8. **`single-sql-client`** (P1 #8). 2-3 PRs, fanned out via multitask once per-file briefs are written.
@@ -357,14 +362,18 @@ Follow-ups surfaced mid-convoy or mid-PR that didn't fit the original launch seq
- **`rotate-default-admin`** (priority: P2 hygiene). Operator-rotation script for envs that ran `setup-neon-db.js` before `drop-public-setup` and still carry the weak `admin123` bcrypt hash. Surfaced in P0 #3 § Operator caveat. Optional: do nothing if no audit finds a deployed env with the weak hash.
- **`delete-dead-lorcana-import`** (priority: P3 polish). Delete `pages/api/cards/import-lorcana.js` (and possibly `scripts/import-lorcana.js`) if Lorcana stays out of the admin UI's `