+ );
+}
+```
+
+- [x] **Step 9: Typecheck + build to verify the foundation stands**
+
+```bash
+cd frontend && npm run build
+```
+Expected: clean build (the kit is not yet imported anywhere — that's fine; tsc checks it).
+
+- [x] **Step 10: Commit**
+
+```bash
+git add frontend/public frontend/src/styles frontend/src/ui frontend/src/main.tsx frontend/index.html
+git commit -m "feat(slice-3): design-system foundation — Wiggleverse tokens, fonts, brand marks, UI kit (per ui/designs export)"
+```
+
+### Task 6: Landing rebuild (per hf-landing)
+
+**Files:**
+- Modify: `frontend/src/screens/Landing.tsx`
+
+- [x] **Step 1: Rebuild the screen**
+
+```tsx
+// Landing (SD-0001 §5.1) — a Visitor learns what ecomm is and picks a door. Honest voice,
+// no trial/pricing/fee (corpus 14.01.0011). Both doors open the same sign-in flow (§5.2);
+// they differ only in framing. Visuals per the ui/designs export (hf-landing, Direction A).
+import { Footer, PrimaryButton, Screen, TopBar, Wordmark } from "../ui/kit";
+
+interface Props {
+ onGetStarted: () => void;
+ onLogIn: () => void;
+}
+
+const PROMISES: Array<[string, string]> = [
+ ["No trial clock", "Your storefront never expires or locks."],
+ ["No plan wall", "We take only what it takes to run."],
+ ["Your data, yours", "Nothing collected you didn't agree to give."],
+];
+
+export default function Landing({ onGetStarted, onLogIn }: Props) {
+ return (
+
+ }
+ right={
+
+ }
+ />
+
+
+ One storefront, fully yours
+
+ Sell online,
+
+ honestly.
+
+
+ Claim the one storefront that's yours on a platform that takes only what it takes
+ to run — no trial countdown, no plan wall, no data you didn't agree to give.
+
+
+
+ Create your storefront →
+
+
+
+
+ {PROMISES.map(([title, copy]) => (
+
+
{title}
+
{copy}
+
+ ))}
+
+
+
+
+
+ );
+}
+```
+
+(add `Eyebrow` to the kit import list: `import { Eyebrow, Footer, PrimaryButton, Screen, TopBar, Wordmark } from "../ui/kit";`. The CTA renders auto-width inside `.hero__actions` — add `btn-primary--auto` via a wrapper if it stretches; check visually at Task 10.)
+
+- [x] **Step 2: Build + tests**
+
+```bash
+cd frontend && npm run build && npm test
+```
+Expected: clean build, routing tests still green.
+
+- [x] **Step 3: Commit**
+
+```bash
+git add frontend/src/screens/Landing.tsx
+git commit -m "feat(slice-3): Landing re-skinned to the Claude Design export (§5.1)"
+```
+
+### Task 7: Sign-in rebuild (per hf-signin) — cooldown countdown, code cells, honest states
+
+**Files:**
+- Modify: `frontend/src/screens/SignIn.tsx`
+
+- [x] **Step 1: Rebuild the screen**
+
+States per §5.2 + the design's states stack: invalid email → inline field error; sending → busy button; resend cooldown (PUC-2c) → countdown from `retry_after_s`; delivery failure (INV-9) → attn banner; wrong code (PUC-2a) → inline error with attempts remaining; expired (PUC-2b) → inline + resend offer; exhausted → attn banner; resend link cooldown-aware.
+
+```tsx
+// Sign in (SD-0001 §5.2) — the single email + one-time-code flow behind both doors. Step 1
+// requests a code; step 2 verifies it. Honest copy for every §5.2 state; storefront routing
+// is handled by App on success. Visuals per the ui/designs export (hf-signin).
+import { useEffect, useState } from "react";
+import { requestCode, verifyCode, type ApiError, type VerifyResult } from "../api";
+import CodeInput from "../ui/CodeInput";
+import { AuthCard, Banner, Field, Footer, PrimaryButton, Screen, TopBar, Wordmark } from "../ui/kit";
+
+type Door = "signup" | "login";
+
+interface Props {
+ door: Door;
+ onAuthed: (result: VerifyResult) => void;
+ onBack: () => void;
+}
+
+function useCooldown(): [number, (s: number) => void] {
+ const [left, setLeft] = useState(0);
+ useEffect(() => {
+ if (left <= 0) return;
+ const t = setInterval(() => setLeft((v) => Math.max(0, v - 1)), 1000);
+ return () => clearInterval(t);
+ }, [left > 0]);
+ return [left, setLeft];
+}
+
+export default function SignIn({ door, onAuthed, onBack }: Props) {
+ const [step, setStep] = useState<"email" | "code">("email");
+ const [email, setEmail] = useState("");
+ const [code, setCode] = useState("");
+ const [busy, setBusy] = useState(false);
+ const [fieldError, setFieldError] = useState(null);
+ const [codeError, setCodeError] = useState(null);
+ const [bannerError, setBannerError] = useState(null);
+ const [cooldown, setCooldown] = useCooldown();
+
+ function applyError(err: ApiError) {
+ setFieldError(null);
+ setCodeError(null);
+ setBannerError(null);
+ if (err.code === "invalid_email") setFieldError(err.message);
+ else if (err.code === "resend_cooldown") setCooldown(err.retry_after_s ?? 60);
+ else if (err.code === "code_mismatch" || err.code === "code_expired") setCodeError(err.message);
+ else setBannerError(err); // delivery_failed, code_exhausted, unexpected
+ }
+
+ async function sendCode(): Promise {
+ setBusy(true);
+ setFieldError(null);
+ setBannerError(null);
+ const err = await requestCode(email);
+ setBusy(false);
+ if (err) {
+ applyError(err);
+ return err.code === "resend_cooldown"; // a cooldown still means a code is out there
+ }
+ setCooldown(60);
+ return true;
+ }
+
+ async function submitEmail(e: React.FormEvent) {
+ e.preventDefault();
+ if (await sendCode()) {
+ setCode("");
+ setCodeError(null);
+ setStep("code");
+ }
+ }
+
+ async function resend() {
+ setCodeError(null);
+ setCode("");
+ await sendCode();
+ }
+
+ async function submitCode(e: React.FormEvent) {
+ e.preventDefault();
+ setBusy(true);
+ setCodeError(null);
+ setBannerError(null);
+ const res = await verifyCode(email, code);
+ setBusy(false);
+ if (!res.ok) {
+ applyError(res.error);
+ return;
+ }
+ onAuthed(res.result);
+ }
+
+ const heading = door === "signup" ? "Create your storefront" : "Log in";
+ const sub =
+ door === "signup"
+ ? "Enter your email to begin. There's no password to create."
+ : "Enter your email and we'll send a one-time code to sign in.";
+
+ return (
+
+ }
+ right={
+
+ }
+ />
+
+
+ {step === "email" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+}
+```
+
+- [x] **Step 2: Build + tests**
+
+```bash
+cd frontend && npm run build && npm test
+```
+Expected: green.
+
+- [x] **Step 3: Commit**
+
+```bash
+git add frontend/src/screens/SignIn.tsx
+git commit -m "feat(slice-3): Sign-in re-skinned — code cells, cooldown countdown, honest §5.2 states"
+```
+
+### Task 8: Create-storefront screen + API helper
+
+**Files:**
+- Modify: `frontend/src/api.ts`
+- Create: `frontend/src/screens/CreateStorefront.tsx`
+
+- [x] **Step 1: Add the API helper** (append to `frontend/src/api.ts`)
+
+```ts
+export interface StorefrontResult {
+ id: number;
+ name: string;
+}
+
+export async function createStorefront(
+ name: string,
+): Promise<{ ok: true; storefront: StorefrontResult } | { ok: false; error: ApiError }> {
+ const resp = await fetch("/api/storefronts", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify(name.trim() ? { name: name.trim() } : {}),
+ });
+ if (resp.ok) return { ok: true, storefront: (await resp.json()) as StorefrontResult };
+ return { ok: false, error: await errorOf(resp) };
+}
+```
+
+- [x] **Step 2: Write the screen** (`frontend/src/screens/CreateStorefront.tsx`; per §5.3 + hf-storefront — name optional with helper, busy state, already-owns banner with admin link, honest error banner with retry; welcome banner carries PUC-3's honest copy after verify)
+
+```tsx
+// Create storefront (SD-0001 §5.3) — a storefront-less Merchant establishes their one
+// storefront (PUC-4/5; PUC-7 defense-in-depth on 409). Visuals per the ui/designs export
+// (hf-storefront). The welcome banner carries PUC-3's honest copy from the verify step.
+import { useState } from "react";
+import { createStorefront, logout, type StorefrontResult } from "../api";
+import { AuthCard, AccountChip, Banner, Eyebrow, Field, Footer, PrimaryButton, Screen, TopBar, Wordmark } from "../ui/kit";
+
+interface Props {
+ email: string;
+ welcome: "new" | "back" | null;
+ onCreated: (storefront: StorefrontResult) => void;
+ onAlreadyOwns: () => void;
+ onSignedOut: () => void;
+}
+
+export default function CreateStorefront({ email, welcome, onCreated, onAlreadyOwns, onSignedOut }: Props) {
+ const [name, setName] = useState("");
+ const [busy, setBusy] = useState(false);
+ const [alreadyOwns, setAlreadyOwns] = useState(false);
+ const [error, setError] = useState(null);
+
+ async function signOut() {
+ await logout();
+ onSignedOut();
+ }
+
+ async function submit(e: React.FormEvent) {
+ e.preventDefault();
+ setBusy(true);
+ setError(null);
+ const res = await createStorefront(name);
+ setBusy(false);
+ if (res.ok) {
+ onCreated(res.storefront);
+ return;
+ }
+ if (res.error.code === "already_owns_storefront") setAlreadyOwns(true);
+ else setError(res.error.message);
+ }
+
+ return (
+
+ } right={} />
+
+
+
+
+
+
+
+ );
+}
+```
+
+- [x] **Step 3: Build**
+
+```bash
+cd frontend && npm run build
+```
+Expected: clean (screen not yet routed; tsc checks it).
+
+- [x] **Step 4: Commit**
+
+```bash
+git add frontend/src/api.ts frontend/src/screens/CreateStorefront.tsx
+git commit -m "feat(slice-3): Create-storefront screen + API helper (§5.3, PUC-4/5/7)"
+```
+
+### Task 9: Admin shell + full entry routing in App
+
+**Files:**
+- Create: `frontend/src/screens/Admin.tsx`
+- Modify: `frontend/src/App.tsx`
+- Delete: `frontend/src/screens/SignedIn.tsx`
+
+- [x] **Step 1: Write the Admin screen** (per §5.4 + hf-admin — storefront name anchors the header, account chip + sign out right, honestly-empty centerpiece, no fabricated anything)
+
+```tsx
+// Admin shell (SD-0001 §5.4) — the storefront's stable home; honestly empty this release
+// (PUC-8; PUC-9 sign-out). Renders from /me alone: storefront name + signed-in email. No
+// zeroed metric tiles, no locked-feature teasers (OHM: Agency & Anti-Manipulation).
+// Visuals per the ui/designs export (hf-admin).
+import { logout } from "../api";
+import { AccountChip, Banner, Eyebrow, Screen, TopBar } from "../ui/kit";
+
+interface Props {
+ storefrontName: string;
+ email: string;
+ welcome: "new" | "back" | null;
+ onSignedOut: () => void;
+}
+
+export default function Admin({ storefrontName, email, welcome, onSignedOut }: Props) {
+ async function signOut() {
+ await logout();
+ onSignedOut();
+ }
+
+ return (
+
+
+
+
+ {storefrontName}
+ ecomm storefront
+
+
+ }
+ right={}
+ />
+
+
+ {welcome && (
+
+
+ {welcome === "new"
+ ? "A new account was created for this email."
+ : "Signed in to your existing account."}
+
+
+ )}
+
+
+
+ Your storefront
+
{storefrontName}
+
+ There's nothing to manage yet — and that's a finished state, not a missing one.
+ Catalog, orders, and settings will appear here as ecomm grows.
+
+
+
+
+ );
+}
+```
+
+- [x] **Step 2: Complete the routing in `frontend/src/App.tsx`** (full §6.5 table: the verify response feeds the session directly — it carries the same shape as /me — and `welcome` carries the honest copy to the destination screen)
+
+```tsx
+// App shell — loads the session, applies the entry-routing rule (SD-0001 §6.5), and renders
+// the matching screen. The rule is exhaustive: no state lands nowhere (BUC-4). The verify
+// response carries the same shape as /me, so onAuthed feeds the session directly.
+import { useEffect, useState } from "react";
+import { getMe, type StorefrontResult, type VerifyResult } from "./api";
+import { routeFor, type SessionState } from "./routing";
+import Admin from "./screens/Admin";
+import CreateStorefront from "./screens/CreateStorefront";
+import Landing from "./screens/Landing";
+import SignIn from "./screens/SignIn";
+
+type Door = "signup" | "login";
+type Welcome = "new" | "back" | null;
+
+export default function App() {
+ const [session, setSession] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [door, setDoor] = useState(null);
+ const [welcome, setWelcome] = useState(null);
+
+ async function reload() {
+ setLoading(true);
+ setSession(await getMe());
+ setLoading(false);
+ }
+
+ useEffect(() => {
+ void reload();
+ }, []);
+
+ if (loading) {
+ return (
+
+
+
Loading…
+
+
+ );
+ }
+
+ const route = routeFor(session ?? { account: null, storefront: null });
+
+ if (route === "landing") {
+ if (door) {
+ return (
+ setDoor(null)}
+ onAuthed={(result: VerifyResult) => {
+ setWelcome(result.created ? "new" : "back");
+ setDoor(null);
+ setSession({ account: result.account, storefront: result.storefront });
+ }}
+ />
+ );
+ }
+ return setDoor("signup")} onLogIn={() => setDoor("login")} />;
+ }
+
+ const email = session!.account!.email;
+
+ function signedOut() {
+ setWelcome(null);
+ setDoor(null);
+ setSession(null);
+ }
+
+ if (route === "create-storefront") {
+ return (
+ {
+ setWelcome(null);
+ setSession({ account: { email }, storefront: sf });
+ }}
+ onAlreadyOwns={() => void reload()}
+ onSignedOut={signedOut}
+ />
+ );
+ }
+
+ return (
+
+ );
+}
+```
+
+- [x] **Step 3: Delete the placeholder**
+
+```bash
+git rm frontend/src/screens/SignedIn.tsx
+```
+
+- [x] **Step 4: Build + tests**
+
+```bash
+cd frontend && npm run build && npm test
+```
+Expected: green — routing tests already assert all three routes (§6.5 table is unchanged; "admin" merely became reachable).
+
+- [x] **Step 5: Commit**
+
+```bash
+git add frontend/src/App.tsx frontend/src/screens/Admin.tsx
+git commit -m "feat(slice-3): Admin shell + complete entry routing (§5.4/§6.5, PUC-6/8/9)"
+```
+
+### Task 10: Full gate + localhost walkthrough (PUC-10) + housekeeping
+
+**Files:**
+- Modify: `README.md`, `frontend/package.json`
+
+- [x] **Step 1: Version bumps** — `frontend/package.json` `"version": "0.2.0"` → `"0.3.0"` (backend FastAPI version already bumped in Task 3).
+
+- [x] **Step 2: README status** — update the status line/section that says the identity slice is in place to say SLICE-3 (storefront) is in place: storefronts domain, create + admin screens, full entry routing, INV-1 whole-flow green; SLICE-4 (deployed environments) is next.
+
+- [x] **Step 3: Run the whole gate**
+
+```bash
+./scripts/check.sh
+```
+Expected: import contract kept; full backend suite green (SLICE-2's 43+3 plus the new storefronts/bootstrap tests); frontend typecheck + build + vitest green.
+
+- [x] **Step 4: Localhost walkthrough (PUC-10, §7.2 definition of done)** — bring the app up and walk sign-up → create storefront → admin end to end against the real stack:
+
+```bash
+./scripts/dev.sh
+```
+
+Then drive the flow (browser or curl): request a code for a fresh email, read the 6-digit code from the backend log (`ecomm.mailer` INFO line), verify, create a storefront, confirm the admin answer. Screenshot or curl-transcript the result into the session transcript. Stop the dev stack after.
+
+- [x] **Step 5: Commit + push + PR**
+
+```bash
+git add README.md frontend/package.json docs/superpowers/plans/2026-06-10-slice-3-storefront.md
+git commit -m "chore(slice-3): version 0.3, README status — storefront slice in place"
+git push -u origin slice-3-storefront
+```
+
+Open the PR against `main` titled `SLICE-3: storefront — create + INV-4 guard, Create/Admin screens, full entry routing (SD-0001 §7.2)`, body citing SD-0001 §7.2 SLICE-3 definition of done; merge per autonomous posture once green.
+
+---
+
+## Self-review notes
+
+- **Spec coverage:** PUC-4 (Task 1/3/8), PUC-5 (Task 3 test + routing), PUC-6 (Task 3 test `14_01_0027` + Task 9), PUC-7 (Tasks 1–3 guard/409 + no second-create affordance anywhere in the UI), PUC-8 (Task 3 `puc_08` test + Task 9 Admin), corpus 0013/0015/0016/0025/0026/0027 (Task 3), INV-4 concurrent refusal (Task 2), INV-1 whole-flow (Task 4), §5.1–5.4 visuals incl. slices 1–2 re-skin (Tasks 5–9), localhost PUC-10 walkable (Task 10).
+- **Deliberate divergences** (logged to Deferred decisions): no dead `#` links from the design; welcome banner moves to the destination screen; Fraunces not shipped (unused); E2E browser tests deferred per §6.8.
+- **Type consistency:** `StorefrontResult {id, name}` (api.ts) = the 201 body = `SessionState.storefront` shape; `VerifyResult` already carries `storefront | null`; `welcome: "new" | "back" | null` threaded App → CreateStorefront/Admin.
diff --git a/frontend/package.json b/frontend/package.json
index 8fc2c37..11eda0d 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "wiggleverse-ecomm-frontend",
"private": true,
- "version": "0.2.0",
+ "version": "0.3.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/ui/kit.tsx b/frontend/src/ui/kit.tsx
index d666764..bbda4ec 100644
--- a/frontend/src/ui/kit.tsx
+++ b/frontend/src/ui/kit.tsx
@@ -1,6 +1,7 @@
// The ecomm UI kit — React faces of the app.css primitives, matching the Claude Design
// export (ui/designs/ecomm-login-and-create-storefront-designs, hf-kit). Presentation
// only: no business rules live here (§6.2 — the SPA renders what the BFF returns).
+import { useId } from "react";
import type { ReactNode } from "react";
export function Screen({
@@ -110,13 +111,15 @@ export function Field({
helper?: string;
inputProps: React.InputHTMLAttributes;
}) {
+ const id = useId();
return (