diff --git a/docs/superpowers/plans/2026-08-03-braids-by-mira-phase1.md b/docs/superpowers/plans/2026-08-03-braids-by-mira-phase1.md
new file mode 100644
index 0000000..49709fc
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-03-braids-by-mira-phase1.md
@@ -0,0 +1,2605 @@
+# Braids by Mira — Phase 1 (Public Site) Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Build the public-facing Braids by Mira site — Home, Shop (product showcase), Book (mock-data booking flow), and Rules pages — in the "Elegant Editorial" visual style with scroll-driven motion, on top of the existing Vite + React 19 + TypeScript scaffold.
+
+**Architecture:** A single-page React app with client-side routing (`react-router-dom`) rendering four routed pages inside a shared `Layout` (Nav + Footer). All content (products, services, availability, rules copy) comes from typed local mock-data modules under `src/data/`, shaped so a future backend can replace them without touching component code. Motion (scroll reveals, hero rotation, hover tilt) is built with `framer-motion` and respects `prefers-reduced-motion`.
+
+**Tech Stack:** Vite, React 19, TypeScript, react-router-dom v7, framer-motion, Vitest + React Testing Library + jsdom, plain CSS (CSS variables + CSS Modules, no utility framework).
+
+## Global Constraints
+
+- Palette: cream `#F7F3EC` (primary bg), `#EFE7D8` (secondary bg), charcoal `#3A332E` (ink), antique gold `#B08D57` (accent) — from the spec's Visual System section.
+- Type: Georgia/serif for labels/nav/body; a script webfont ("Great Vibes") for headline moments only.
+- No cart/checkout — Shop uses an "Ask about this" inquiry modal instead, per spec.
+- No real backend/network calls anywhere in Phase 1 — all data is local mock data; booking/inquiry "submission" just transitions UI state to a confirmation screen.
+- All motion must respect `prefers-reduced-motion` (instant fades instead of parallax/rotation/stagger).
+- Every component/hook task includes tests written before implementation (Vitest + React Testing Library).
+
+---
+
+### Task 1: Tooling setup — Vitest, routing, motion, and CSS tokens
+
+**Files:**
+- Modify: `package.json` (new deps)
+- Create: `vite.config.ts` (add `test` config)
+- Create: `src/test/setup.ts`
+- Create: `src/styles/tokens.css`
+- Modify: `index.html` (title, Google Fonts link for "Great Vibes")
+- Modify: `src/index.css` (replace default theme vars with tokens import + reset)
+- Delete: `src/App.css`, `src/assets/react.svg`, `src/assets/vite.svg`, `src/assets/hero.png`
+- Modify: `src/App.tsx` (temporary minimal router shell, replaced page-by-page in later tasks)
+- Test: `src/App.test.tsx`
+
+**Interfaces:**
+- Produces: CSS variables available globally — `--color-cream`, `--color-cream-alt`, `--color-charcoal`, `--color-gold`, `--font-serif`, `--font-script` (defined in `src/styles/tokens.css`, imported once from `src/index.css`).
+- Produces: `App.tsx` exports default `App` component that renders a `BrowserRouter` with routes `/`, `/shop`, `/book`, `/rules`, each currently rendering a placeholder `
` with a unique text marker (`data-testid="page-home"` etc.) — later tasks swap each placeholder for the real page component.
+
+- [ ] **Step 1: Install dependencies**
+
+```bash
+npm install react-router-dom framer-motion
+npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
+```
+
+- [ ] **Step 2: Add Vitest config to `vite.config.ts`**
+
+```ts
+///
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: 'jsdom',
+ globals: true,
+ setupFiles: ['./src/test/setup.ts'],
+ },
+})
+```
+
+- [ ] **Step 3: Create the test setup file**
+
+`src/test/setup.ts`:
+
+```ts
+import '@testing-library/jest-dom/vitest'
+```
+
+- [ ] **Step 4: Add a `test` script to `package.json`**
+
+Add under `"scripts"`: `"test": "vitest run"`
+
+- [ ] **Step 5: Create the design token stylesheet**
+
+`src/styles/tokens.css`:
+
+```css
+:root {
+ --color-cream: #F7F3EC;
+ --color-cream-alt: #EFE7D8;
+ --color-charcoal: #3A332E;
+ --color-gold: #B08D57;
+
+ --font-serif: Georgia, 'Times New Roman', serif;
+ --font-script: 'Great Vibes', cursive;
+
+ --space-sm: 0.5rem;
+ --space-md: 1rem;
+ --space-lg: 2rem;
+ --space-xl: 4rem;
+}
+```
+
+- [ ] **Step 6: Replace `src/index.css` with the reset + token import**
+
+```css
+@import './styles/tokens.css';
+
+* {
+ box-sizing: border-box;
+}
+
+html, body {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background: var(--color-cream);
+ color: var(--color-charcoal);
+ font-family: var(--font-serif);
+ -webkit-font-smoothing: antialiased;
+}
+
+a {
+ color: inherit;
+}
+```
+
+- [ ] **Step 7: Delete unused boilerplate files**
+
+```bash
+rm src/App.css src/assets/react.svg src/assets/vite.svg src/assets/hero.png
+```
+
+- [ ] **Step 8: Add the Google Fonts link to `index.html`** and update the title
+
+In ``, replace the existing `` and add before it:
+
+```html
+
+
+
+Braids by Mira
+```
+
+- [ ] **Step 9: Write the failing router shell test**
+
+`src/App.test.tsx`:
+
+```tsx
+import { render, screen } from '@testing-library/react'
+import { MemoryRouter } from 'react-router-dom'
+import { describe, it, expect } from 'vitest'
+import App from './App'
+
+describe('App routing', () => {
+ it('renders the home placeholder at /', () => {
+ render()
+ expect(screen.getByTestId('page-home')).toBeInTheDocument()
+ })
+
+ it('renders the shop placeholder at /shop', () => {
+ render()
+ expect(screen.getByTestId('page-shop')).toBeInTheDocument()
+ })
+
+ it('renders the book placeholder at /book', () => {
+ render()
+ expect(screen.getByTestId('page-book')).toBeInTheDocument()
+ })
+
+ it('renders the rules placeholder at /rules', () => {
+ render()
+ expect(screen.getByTestId('page-rules')).toBeInTheDocument()
+ })
+})
+```
+
+Note: to make `App` accept `initialEntries` for testing, `App.tsx` will take an optional prop and use `MemoryRouter` in tests vs `BrowserRouter` in production — see Step 11.
+
+- [ ] **Step 10: Run the test to verify it fails**
+
+Run: `npx vitest run src/App.test.tsx`
+Expected: FAIL (`App` doesn't exist yet / no matching testid)
+
+- [ ] **Step 11: Write the minimal router shell**
+
+`src/App.tsx`:
+
+```tsx
+import { BrowserRouter, MemoryRouter, Routes, Route } from 'react-router-dom'
+
+interface AppProps {
+ initialEntries?: string[]
+}
+
+function AppRoutes() {
+ return (
+
+ } />
+ } />
+ } />
+ } />
+
+ )
+}
+
+function App({ initialEntries }: AppProps) {
+ if (initialEntries) {
+ return (
+
+
+
+ )
+ }
+ return (
+
+
+
+ )
+}
+
+export default App
+```
+
+- [ ] **Step 12: Run the test to verify it passes**
+
+Run: `npx vitest run src/App.test.tsx`
+Expected: PASS (4 tests)
+
+- [ ] **Step 13: Verify the app still builds and runs**
+
+Run: `npm run build`
+Expected: build succeeds with no TypeScript errors
+
+- [ ] **Step 14: Commit**
+
+```bash
+git add -A
+git commit -m "chore: set up Vitest, routing, and design tokens"
+```
+
+---
+
+### Task 2: Mock data modules — products, services, availability, rules
+
+**Files:**
+- Create: `src/data/products.ts`
+- Create: `src/data/services.ts`
+- Create: `src/data/availability.ts`
+- Create: `src/data/rules.ts`
+- Test: `src/data/products.test.ts`
+- Test: `src/data/availability.test.ts`
+
+**Interfaces:**
+- Produces: `Product` type + `products: Product[]` + `getProductsByCategory(category: Product['category'] | 'all'): Product[]` from `src/data/products.ts`
+- Produces: `Service` type + `services: Service[]` from `src/data/services.ts`
+- Produces: `TimeSlot` type + `getSlotsForDate(date: string): TimeSlot[]` + `getNextAvailableDate(fromDate: string): string | null` from `src/data/availability.ts`
+- Produces: `RuleSection` type + `rules: RuleSection[]` from `src/data/rules.ts`
+
+- [ ] **Step 1: Write failing tests for products data**
+
+`src/data/products.test.ts`:
+
+```ts
+import { describe, it, expect } from 'vitest'
+import { products, getProductsByCategory } from './products'
+
+describe('products data', () => {
+ it('has at least 6 products with required fields', () => {
+ expect(products.length).toBeGreaterThanOrEqual(6)
+ for (const p of products) {
+ expect(p.id).toBeTruthy()
+ expect(p.name).toBeTruthy()
+ expect(p.price).toBeGreaterThan(0)
+ expect(typeof p.inStock).toBe('boolean')
+ }
+ })
+
+ it('filters by category', () => {
+ const hairCare = getProductsByCategory('hair-care')
+ expect(hairCare.length).toBeGreaterThan(0)
+ expect(hairCare.every((p) => p.category === 'hair-care')).toBe(true)
+ })
+
+ it('returns all products for "all"', () => {
+ expect(getProductsByCategory('all')).toHaveLength(products.length)
+ })
+})
+```
+
+- [ ] **Step 2: Run to verify it fails**
+
+Run: `npx vitest run src/data/products.test.ts`
+Expected: FAIL (module doesn't exist)
+
+- [ ] **Step 3: Implement products data**
+
+`src/data/products.ts`:
+
+```ts
+export interface Product {
+ id: string
+ name: string
+ category: 'hair-care' | 'extensions' | 'tools' | 'accessories'
+ price: number
+ image: string
+ description: string
+ inStock: boolean
+}
+
+export const products: Product[] = [
+ { id: 'edge-control', name: 'Silk Edge Control', category: 'hair-care', price: 12, image: '/products/edge-control.jpg', description: 'Long-lasting hold with a natural shine, no flaking.', inStock: true },
+ { id: 'braid-oil', name: 'Nourishing Braid Oil', category: 'hair-care', price: 16, image: '/products/braid-oil.jpg', description: 'Lightweight scalp oil that soothes and adds shine between washes.', inStock: true },
+ { id: 'leave-in', name: 'Detangling Leave-In', category: 'hair-care', price: 14, image: '/products/leave-in.jpg', description: 'Slip-rich leave-in conditioner for gentle detangling.', inStock: true },
+ { id: 'kanekalon-black', name: 'Kanekalon Braiding Hair — Jet Black', category: 'extensions', price: 8, image: '/products/kanekalon-black.jpg', description: 'Premium synthetic braiding hair, soft texture, low shedding.', inStock: true },
+ { id: 'kanekalon-brown', name: 'Kanekalon Braiding Hair — Chestnut', category: 'extensions', price: 8, image: '/products/kanekalon-brown.jpg', description: 'Warm brown tone, blends seamlessly for natural styles.', inStock: false },
+ { id: 'locs-extension', name: 'Faux Locs Extensions', category: 'extensions', price: 22, image: '/products/locs-extension.jpg', description: 'Pre-looped faux locs for a natural, lightweight install.', inStock: true },
+ { id: 'rat-tail-comb', name: 'Steel Rat-Tail Comb', category: 'tools', price: 6, image: '/products/rat-tail-comb.jpg', description: 'Precision parting comb with a fine metal tail.', inStock: true },
+ { id: 'edge-brush', name: 'Mini Edge Brush', category: 'tools', price: 5, image: '/products/edge-brush.jpg', description: 'Dual-sided brush for laying edges and smoothing baby hairs.', inStock: true },
+ { id: 'gold-cuffs', name: 'Gold Hair Cuffs (Set of 6)', category: 'accessories', price: 10, image: '/products/gold-cuffs.jpg', description: 'Adjustable cuffs to accessorize braids and locs.', inStock: true },
+ { id: 'satin-bonnet', name: 'Satin Sleep Bonnet', category: 'accessories', price: 9, image: '/products/satin-bonnet.jpg', description: 'Protects braids and locs overnight, reduces frizz.', inStock: true },
+]
+
+export function getProductsByCategory(category: Product['category'] | 'all'): Product[] {
+ if (category === 'all') return products
+ return products.filter((p) => p.category === category)
+}
+```
+
+- [ ] **Step 4: Run to verify it passes**
+
+Run: `npx vitest run src/data/products.test.ts`
+Expected: PASS
+
+- [ ] **Step 5: Implement services data (no test file — simple static data, covered indirectly by Home/Book tests in later tasks)**
+
+`src/data/services.ts`:
+
+```ts
+export interface Service {
+ id: string
+ name: string
+ durationMinutes: number
+ priceFrom: number
+ description: string
+ image: string
+}
+
+export const services: Service[] = [
+ { id: 'knotless', name: 'Knotless Braids', durationMinutes: 240, priceFrom: 150, description: 'Tension-free braids with a natural, seamless root.', image: '/styles/knotless.jpg' },
+ { id: 'boho', name: 'Boho Braids', durationMinutes: 270, priceFrom: 180, description: 'Knotless braids with curly pieces woven in for a soft, romantic look.', image: '/styles/boho.jpg' },
+ { id: 'locs', name: 'Faux Locs', durationMinutes: 300, priceFrom: 200, description: 'Lightweight, natural-looking locs installed with care.', image: '/styles/locs.jpg' },
+ { id: 'cornrows', name: 'Cornrows', durationMinutes: 120, priceFrom: 90, description: 'Classic sleek cornrows, straight-back or with a custom design.', image: '/styles/cornrows.jpg' },
+]
+```
+
+- [ ] **Step 6: Write failing tests for availability data**
+
+`src/data/availability.test.ts`:
+
+```ts
+import { describe, it, expect } from 'vitest'
+import { getSlotsForDate, getNextAvailableDate } from './availability'
+
+describe('availability data', () => {
+ it('returns a list of time slots for a given date', () => {
+ const slots = getSlotsForDate('2026-08-10')
+ expect(slots.length).toBeGreaterThan(0)
+ for (const s of slots) {
+ expect(s.date).toBe('2026-08-10')
+ expect(typeof s.time).toBe('string')
+ expect(typeof s.available).toBe('boolean')
+ }
+ })
+
+ it('is deterministic for the same date', () => {
+ expect(getSlotsForDate('2026-08-10')).toEqual(getSlotsForDate('2026-08-10'))
+ })
+
+ it('finds the next available date within 14 days when none is available immediately', () => {
+ // Sundays (day 0) are fully booked in the mock generator
+ const next = getNextAvailableDate('2026-08-09') // a Sunday
+ expect(next).not.toBeNull()
+ expect(next).not.toBe('2026-08-09')
+ })
+
+ it('returns null if no available date is found within the search window', () => {
+ expect(getNextAvailableDate('1999-01-01', 0)).toBeNull()
+ })
+})
+```
+
+- [ ] **Step 7: Run to verify it fails**
+
+Run: `npx vitest run src/data/availability.test.ts`
+Expected: FAIL (module doesn't exist)
+
+- [ ] **Step 8: Implement availability data**
+
+`src/data/availability.ts`:
+
+```ts
+export interface TimeSlot {
+ date: string // ISO date, e.g. "2026-08-10"
+ time: string // e.g. "10:00 AM"
+ available: boolean
+}
+
+const ALL_TIMES = ['9:00 AM', '11:00 AM', '1:00 PM', '3:00 PM', '5:00 PM']
+
+function seedFromDate(date: string): number {
+ let seed = 0
+ for (let i = 0; i < date.length; i++) seed += date.charCodeAt(i)
+ return seed
+}
+
+export function getSlotsForDate(date: string): TimeSlot[] {
+ const day = new Date(`${date}T00:00:00`).getDay()
+ if (day === 0) {
+ // Sundays: closed, no slots at all
+ return []
+ }
+ const seed = seedFromDate(date)
+ return ALL_TIMES.map((time, i) => ({
+ date,
+ time,
+ available: (seed + i) % 3 !== 0,
+ }))
+}
+
+export function getNextAvailableDate(fromDate: string, searchWindowDays = 14): string | null {
+ const start = new Date(`${fromDate}T00:00:00`)
+ for (let i = 1; i <= searchWindowDays; i++) {
+ const candidate = new Date(start)
+ candidate.setDate(start.getDate() + i)
+ const iso = candidate.toISOString().slice(0, 10)
+ if (getSlotsForDate(iso).some((s) => s.available)) {
+ return iso
+ }
+ }
+ return null
+}
+```
+
+- [ ] **Step 9: Run to verify it passes**
+
+Run: `npx vitest run src/data/availability.test.ts`
+Expected: PASS
+
+- [ ] **Step 10: Implement rules data (no dedicated test — content is exercised by the Rules page test in Task 9)**
+
+`src/data/rules.ts`:
+
+```ts
+export interface RuleSection {
+ id: string
+ title: string
+ body: string
+}
+
+export const rules: RuleSection[] = [
+ { id: 'deposit', title: 'Deposit', body: 'A non-refundable deposit of $30 is required to secure your appointment and will be deducted from your final total. Appointments are not confirmed until the deposit is received.' },
+ { id: 'cancellation', title: 'Cancellation & Lateness', body: 'Please give at least 48 hours notice to reschedule. Arrivals more than 15 minutes late may need to be rescheduled and will forfeit their deposit, as it affects every client booked after you.' },
+ { id: 'guests', title: 'Guests & Kids', body: 'To keep the space calm and on schedule, please come to your appointment alone unless the guest is also being serviced. Children who are not being serviced should not accompany you.' },
+ { id: 'hair-prep', title: 'Hair Prep', body: 'Arrive with hair freshly washed, blow-dried or stretched, and fully detangled unless a wash service has been booked. Extra prep time may be added to your total if hair arrives unprepped.' },
+ { id: 'health-safety', title: 'Health & Safety', body: 'Please reschedule if you are unwell. Let Mira know in advance of any scalp sensitivities, allergies, or conditions that may affect your service.' },
+]
+```
+
+- [ ] **Step 11: Run the full data test suite**
+
+Run: `npx vitest run src/data`
+Expected: all PASS
+
+- [ ] **Step 12: Commit**
+
+```bash
+git add src/data
+git commit -m "feat: add mock data for products, services, availability, and rules"
+```
+
+---
+
+### Task 3: `ScrollReveal` component
+
+**Files:**
+- Create: `src/components/ScrollReveal.tsx`
+- Test: `src/components/ScrollReveal.test.tsx`
+
+**Interfaces:**
+- Consumes: nothing from earlier tasks
+- Produces: `ScrollReveal` component — `{children}` — wraps children in a `framer-motion` `motion.div` that fades + rises into view on scroll, and renders children plainly (no animation props) when `prefers-reduced-motion: reduce` is set. Used by Home/Shop/Book/Rules sections in later tasks.
+
+- [ ] **Step 1: Write the failing test**
+
+`src/components/ScrollReveal.test.tsx`:
+
+```tsx
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi, afterEach } from 'vitest'
+import ScrollReveal from './ScrollReveal'
+
+function mockMatchMedia(reduced: boolean) {
+ window.matchMedia = vi.fn().mockImplementation((query: string) => ({
+ matches: reduced && query.includes('prefers-reduced-motion'),
+ media: query,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ }))
+}
+
+describe('ScrollReveal', () => {
+ afterEach(() => vi.restoreAllMocks())
+
+ it('renders its children', () => {
+ mockMatchMedia(false)
+ render(
Hello
)
+ expect(screen.getByText('Hello')).toBeInTheDocument()
+ })
+
+ it('renders children unanimated when reduced motion is preferred', () => {
+ mockMatchMedia(true)
+ render(
Hello
)
+ expect(screen.getByTestId('content')).toBeInTheDocument()
+ })
+})
+```
+
+- [ ] **Step 2: Run to verify it fails**
+
+Run: `npx vitest run src/components/ScrollReveal.test.tsx`
+Expected: FAIL (module doesn't exist)
+
+- [ ] **Step 3: Implement `ScrollReveal`**
+
+`src/components/ScrollReveal.tsx`:
+
+```tsx
+import type { ReactNode } from 'react'
+import { motion, useReducedMotion } from 'framer-motion'
+
+interface ScrollRevealProps {
+ children: ReactNode
+ delay?: number
+}
+
+function ScrollReveal({ children, delay = 0 }: ScrollRevealProps) {
+ const prefersReducedMotion = useReducedMotion()
+
+ if (prefersReducedMotion) {
+ return <>{children}>
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export default ScrollReveal
+```
+
+- [ ] **Step 4: Run to verify it passes**
+
+Run: `npx vitest run src/components/ScrollReveal.test.tsx`
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/components/ScrollReveal.tsx src/components/ScrollReveal.test.tsx
+git commit -m "feat: add ScrollReveal motion wrapper"
+```
+
+---
+
+### Task 4: `Nav` and `Footer` components
+
+**Files:**
+- Create: `src/components/Nav.tsx`, `src/components/Nav.module.css`
+- Create: `src/components/Footer.tsx`, `src/components/Footer.module.css`
+- Test: `src/components/Nav.test.tsx`
+- Test: `src/components/Footer.test.tsx`
+
+**Interfaces:**
+- Produces: `Nav` component (no props) — renders the logo/wordmark and links to `/`, `/shop`, `/book`, `/rules`; adds a `condensed` class to its root once `window.scrollY > 40`.
+- Produces: `Footer` component (no props) — renders contact info, hours, and quick links to the four routes.
+- Both must be rendered inside a `MemoryRouter` (or `BrowserRouter`) in tests and in `Layout` (Task 5) since they use `Link`.
+
+- [ ] **Step 1: Write the failing Nav test**
+
+`src/components/Nav.test.tsx`:
+
+```tsx
+import { render, screen, fireEvent } from '@testing-library/react'
+import { MemoryRouter } from 'react-router-dom'
+import { describe, it, expect } from 'vitest'
+import Nav from './Nav'
+
+describe('Nav', () => {
+ it('renders links to all four routes', () => {
+ render()
+ expect(screen.getByRole('link', { name: /home/i })).toHaveAttribute('href', '/')
+ expect(screen.getByRole('link', { name: /shop/i })).toHaveAttribute('href', '/shop')
+ expect(screen.getByRole('link', { name: /book/i })).toHaveAttribute('href', '/book')
+ expect(screen.getByRole('link', { name: /rules/i })).toHaveAttribute('href', '/rules')
+ })
+
+ it('adds a condensed class after scrolling', () => {
+ render()
+ const nav = screen.getByRole('navigation')
+ expect(nav.className).not.toMatch(/condensed/)
+ Object.defineProperty(window, 'scrollY', { value: 80, configurable: true })
+ fireEvent.scroll(window)
+ expect(nav.className).toMatch(/condensed/)
+ })
+})
+```
+
+- [ ] **Step 2: Run to verify it fails**
+
+Run: `npx vitest run src/components/Nav.test.tsx`
+Expected: FAIL (module doesn't exist)
+
+- [ ] **Step 3: Implement `Nav`**
+
+`src/components/Nav.module.css`:
+
+```css
+.nav {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--space-md) var(--space-lg);
+ font-family: var(--font-serif);
+ letter-spacing: 2px;
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ background: var(--color-cream);
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ transition: padding 0.3s ease, box-shadow 0.3s ease;
+}
+
+.nav.condensed {
+ padding: var(--space-sm) var(--space-lg);
+ box-shadow: 0 2px 12px rgba(58, 51, 46, 0.08);
+}
+
+.links {
+ display: flex;
+ gap: var(--space-lg);
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.links a {
+ text-decoration: none;
+ color: var(--color-charcoal);
+}
+```
+
+`src/components/Nav.tsx`:
+
+```tsx
+import { useEffect, useState } from 'react'
+import { Link } from 'react-router-dom'
+import styles from './Nav.module.css'
+
+function Nav() {
+ const [condensed, setCondensed] = useState(false)
+
+ useEffect(() => {
+ const onScroll = () => setCondensed(window.scrollY > 40)
+ window.addEventListener('scroll', onScroll)
+ return () => window.removeEventListener('scroll', onScroll)
+ }, [])
+
+ return (
+
+ )
+}
+
+export default Nav
+```
+
+- [ ] **Step 4: Run to verify it passes**
+
+Run: `npx vitest run src/components/Nav.test.tsx`
+Expected: PASS
+
+- [ ] **Step 5: Write the failing Footer test**
+
+`src/components/Footer.test.tsx`:
+
+```tsx
+import { render, screen } from '@testing-library/react'
+import { MemoryRouter } from 'react-router-dom'
+import { describe, it, expect } from 'vitest'
+import Footer from './Footer'
+
+describe('Footer', () => {
+ it('renders contact info and quick links', () => {
+ render()
+ expect(screen.getByText(/braids by mira/i)).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /shop/i })).toHaveAttribute('href', '/shop')
+ expect(screen.getByRole('link', { name: /rules/i })).toHaveAttribute('href', '/rules')
+ })
+})
+```
+
+- [ ] **Step 6: Run to verify it fails**
+
+Run: `npx vitest run src/components/Footer.test.tsx`
+Expected: FAIL (module doesn't exist)
+
+- [ ] **Step 7: Implement `Footer`**
+
+`src/components/Footer.module.css`:
+
+```css
+.footer {
+ background: var(--color-charcoal);
+ color: var(--color-cream);
+ padding: var(--space-xl) var(--space-lg);
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ gap: var(--space-lg);
+ font-family: var(--font-serif);
+ font-size: 0.85rem;
+}
+
+.footer a {
+ color: var(--color-cream);
+ text-decoration: none;
+}
+
+.links {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+ list-style: none;
+ padding: 0;
+}
+```
+
+`src/components/Footer.tsx`:
+
+```tsx
+import { Link } from 'react-router-dom'
+import styles from './Footer.module.css'
+
+function Footer() {
+ return (
+
+ )
+}
+
+export default Footer
+```
+
+- [ ] **Step 8: Run to verify it passes**
+
+Run: `npx vitest run src/components/Footer.test.tsx`
+Expected: PASS
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add src/components/Nav.tsx src/components/Nav.module.css src/components/Nav.test.tsx \
+ src/components/Footer.tsx src/components/Footer.module.css src/components/Footer.test.tsx
+git commit -m "feat: add Nav and Footer components"
+```
+
+---
+
+### Task 5: `Layout` component and wire it into `App.tsx`
+
+**Files:**
+- Create: `src/components/Layout.tsx`
+- Modify: `src/App.tsx`
+- Modify: `src/App.test.tsx`
+
+**Interfaces:**
+- Consumes: `Nav` and `Footer` from Task 4
+- Produces: `Layout` component — renders ``, `` (from `react-router-dom`), then `` — used as the parent route element in `App.tsx`.
+
+- [ ] **Step 1: Update the App routing test for the layout wrapper**
+
+Modify `src/App.test.tsx` to assert Nav/Footer render on every route:
+
+```tsx
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect } from 'vitest'
+import App from './App'
+
+describe('App routing', () => {
+ it('renders the home placeholder at / with nav and footer', () => {
+ render()
+ expect(screen.getByTestId('page-home')).toBeInTheDocument()
+ expect(screen.getByRole('navigation')).toBeInTheDocument()
+ expect(screen.getByText(/hello@braidsbymira.com/)).toBeInTheDocument()
+ })
+
+ it('renders the shop placeholder at /shop', () => {
+ render()
+ expect(screen.getByTestId('page-shop')).toBeInTheDocument()
+ })
+
+ it('renders the book placeholder at /book', () => {
+ render()
+ expect(screen.getByTestId('page-book')).toBeInTheDocument()
+ })
+
+ it('renders the rules placeholder at /rules', () => {
+ render()
+ expect(screen.getByTestId('page-rules')).toBeInTheDocument()
+ })
+})
+```
+
+- [ ] **Step 2: Run to verify it fails**
+
+Run: `npx vitest run src/App.test.tsx`
+Expected: FAIL (no `navigation` role / no footer text yet — placeholders render bare)
+
+- [ ] **Step 3: Implement `Layout`**
+
+`src/components/Layout.tsx`:
+
+```tsx
+import { Outlet } from 'react-router-dom'
+import Nav from './Nav'
+import Footer from './Footer'
+
+function Layout() {
+ return (
+ <>
+
+
+
+ >
+ )
+}
+
+export default Layout
+```
+
+- [ ] **Step 4: Wire `Layout` into `App.tsx`**
+
+`src/App.tsx`:
+
+```tsx
+import { BrowserRouter, MemoryRouter, Routes, Route } from 'react-router-dom'
+import Layout from './components/Layout'
+
+interface AppProps {
+ initialEntries?: string[]
+}
+
+function AppRoutes() {
+ return (
+
+ }>
+ } />
+ } />
+ } />
+ } />
+
+
+ )
+}
+
+function App({ initialEntries }: AppProps) {
+ if (initialEntries) {
+ return (
+
+
+
+ )
+ }
+ return (
+
+
+
+ )
+}
+
+export default App
+```
+
+- [ ] **Step 5: Run to verify it passes**
+
+Run: `npx vitest run src/App.test.tsx`
+Expected: PASS
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add src/App.tsx src/App.test.tsx src/components/Layout.tsx
+git commit -m "feat: add shared Layout wrapping Nav and Footer around all routes"
+```
+
+---
+
+### Task 6: `Hero3D` component
+
+**Files:**
+- Create: `src/components/Hero3D.tsx`, `src/components/Hero3D.module.css`
+- Test: `src/components/Hero3D.test.tsx`
+
+**Interfaces:**
+- Produces: `Hero3D` component (no props) — renders a `data-testid="hero-3d"` container with a rotating crown/braid motif built from CSS 3D transforms, whose rotation is driven by scroll position via `framer-motion`'s `useScroll`/`useTransform`. When `prefers-reduced-motion: reduce`, renders the motif statically (no scroll-linked transform) with class `static`.
+
+- [ ] **Step 1: Write the failing test**
+
+`src/components/Hero3D.test.tsx`:
+
+```tsx
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi, afterEach } from 'vitest'
+import Hero3D from './Hero3D'
+
+function mockMatchMedia(reduced: boolean) {
+ window.matchMedia = vi.fn().mockImplementation((query: string) => ({
+ matches: reduced && query.includes('prefers-reduced-motion'),
+ media: query,
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ }))
+}
+
+describe('Hero3D', () => {
+ afterEach(() => vi.restoreAllMocks())
+
+ it('renders the motif container', () => {
+ mockMatchMedia(false)
+ render()
+ expect(screen.getByTestId('hero-3d')).toBeInTheDocument()
+ })
+
+ it('renders statically when reduced motion is preferred', () => {
+ mockMatchMedia(true)
+ render()
+ expect(screen.getByTestId('hero-3d').className).toMatch(/static/)
+ })
+})
+```
+
+- [ ] **Step 2: Run to verify it fails**
+
+Run: `npx vitest run src/components/Hero3D.test.tsx`
+Expected: FAIL (module doesn't exist)
+
+- [ ] **Step 3: Implement `Hero3D`**
+
+`src/components/Hero3D.module.css`:
+
+```css
+.wrapper {
+ perspective: 900px;
+ width: 220px;
+ height: 220px;
+ margin: 0 auto;
+}
+
+.motif {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background: radial-gradient(circle at 30% 30%, #e6c98f, var(--color-gold));
+ transform-style: preserve-3d;
+}
+
+.static {
+ composes: motif;
+}
+```
+
+`src/components/Hero3D.tsx`:
+
+```tsx
+import { motion, useReducedMotion, useScroll, useTransform } from 'framer-motion'
+import styles from './Hero3D.module.css'
+
+function Hero3D() {
+ const prefersReducedMotion = useReducedMotion()
+ const { scrollY } = useScroll()
+ const rotateY = useTransform(scrollY, [0, 600], [0, 180])
+ const rotateX = useTransform(scrollY, [0, 600], [0, 20])
+
+ if (prefersReducedMotion) {
+ return (
+
+ )
+}
+
+export default Book
+```
+
+- [ ] **Step 4: Run to verify it passes**
+
+Run: `npx vitest run src/pages/Book.test.tsx`
+Expected: PASS
+
+- [ ] **Step 5: Wire `Book` into `App.tsx`**
+
+In `src/App.tsx`, import `Book` from `./pages/Book` and replace the `/book` placeholder route with `} />`.
+
+Update `src/App.test.tsx`'s `/book` assertion:
+
+```tsx
+it('renders the Book page at /book', () => {
+ render()
+ expect(screen.getByRole('heading', { name: /book an appointment/i })).toBeInTheDocument()
+})
+```
+
+- [ ] **Step 6: Run the full test suite**
+
+Run: `npx vitest run`
+Expected: all PASS
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/pages/Book.tsx src/pages/Book.module.css src/pages/Book.test.tsx src/App.tsx src/App.test.tsx
+git commit -m "feat: add Book page with the full mock-data booking flow"
+```
+
+---
+
+### Task 12: `Rules` page
+
+**Files:**
+- Create: `src/pages/Rules.tsx`, `src/pages/Rules.module.css`
+- Modify: `src/App.tsx` (replace `/rules` placeholder)
+- Modify: `src/App.test.tsx` (update the `/rules` assertion)
+- Test: `src/pages/Rules.test.tsx`
+
+**Interfaces:**
+- Consumes: `rules` from `src/data/rules.ts`
+- Produces: `Rules` page component (default export, no props) — renders each `RuleSection` as a titled block, plus a closing note and a `Link` to `/book`.
+
+- [ ] **Step 1: Write the failing test**
+
+`src/pages/Rules.test.tsx`:
+
+```tsx
+import { render, screen } from '@testing-library/react'
+import { MemoryRouter } from 'react-router-dom'
+import { describe, it, expect } from 'vitest'
+import Rules from './Rules'
+import { rules } from '../data/rules'
+
+describe('Rules page', () => {
+ it('renders every rule section title and body', () => {
+ render()
+ for (const rule of rules) {
+ expect(screen.getByText(rule.title)).toBeInTheDocument()
+ expect(screen.getByText(rule.body)).toBeInTheDocument()
+ }
+ })
+
+ it('links back to booking', () => {
+ render()
+ expect(screen.getByRole('link', { name: /book/i })).toHaveAttribute('href', '/book')
+ })
+})
+```
+
+- [ ] **Step 2: Run to verify it fails**
+
+Run: `npx vitest run src/pages/Rules.test.tsx`
+Expected: FAIL (module doesn't exist)
+
+- [ ] **Step 3: Implement the `Rules` page**
+
+`src/pages/Rules.module.css`:
+
+```css
+.page {
+ padding: var(--space-xl) var(--space-lg);
+ max-width: 680px;
+ margin: 0 auto;
+}
+
+.section {
+ margin-bottom: var(--space-lg);
+}
+
+.section h2 {
+ color: var(--color-gold);
+ font-size: 1rem;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.cta {
+ text-align: center;
+ margin-top: var(--space-xl);
+}
+```
+
+`src/pages/Rules.tsx`:
+
+```tsx
+import { Link } from 'react-router-dom'
+import { rules } from '../data/rules'
+import styles from './Rules.module.css'
+
+function Rules() {
+ return (
+
+
Salon Rules & Policies
+
Please read through before booking your appointment.
+ {rules.map((rule) => (
+
+
{rule.title}
+
{rule.body}
+
+ ))}
+
+
By booking, you agree to these terms.
+ Book Now
+
+
+ )
+}
+
+export default Rules
+```
+
+- [ ] **Step 4: Run to verify it passes**
+
+Run: `npx vitest run src/pages/Rules.test.tsx`
+Expected: PASS
+
+- [ ] **Step 5: Wire `Rules` into `App.tsx`**
+
+In `src/App.tsx`, import `Rules` from `./pages/Rules` and replace the `/rules` placeholder route with `} />`.
+
+Update `src/App.test.tsx`'s `/rules` assertion:
+
+```tsx
+it('renders the Rules page at /rules', () => {
+ render()
+ expect(screen.getByRole('heading', { name: /salon rules/i })).toBeInTheDocument()
+})
+```
+
+- [ ] **Step 6: Run the full test suite**
+
+Run: `npx vitest run`
+Expected: all PASS
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/pages/Rules.tsx src/pages/Rules.module.css src/pages/Rules.test.tsx src/App.tsx src/App.test.tsx
+git commit -m "feat: add Rules page"
+```
+
+---
+
+### Task 13: `Home` page
+
+**Files:**
+- Create: `src/pages/Home.tsx`, `src/pages/Home.module.css`
+- Modify: `src/App.tsx` (replace `/` placeholder)
+- Modify: `src/App.test.tsx` (update the `/` assertion)
+- Test: `src/pages/Home.test.tsx`
+
+**Interfaces:**
+- Consumes: `Hero3D` (Task 6), `ScrollReveal` (Task 3), `ServiceCard` (Task 7), `ProductCard`-free "shop highlight" links, `services` (`src/data/services.ts`), `products` (`src/data/products.ts`), `useNavigate`/`Link` from `react-router-dom`
+- Produces: `Home` page component (default export, no props) — assembles all seven sections from the spec's Home layout. Clicking a Signature Style card navigates to `/book` with `state={{ serviceId }}`.
+
+- [ ] **Step 1: Write the failing test**
+
+`src/pages/Home.test.tsx`:
+
+```tsx
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { MemoryRouter, Routes, Route } from 'react-router-dom'
+import { describe, it, expect } from 'vitest'
+import Home from './Home'
+import Book from './Book'
+import { services } from '../data/services'
+
+function renderHome() {
+ return render(
+
+
+ } />
+ } />
+
+ ,
+ )
+}
+
+describe('Home page', () => {
+ it('renders the hero headline and all four signature styles', () => {
+ renderHome()
+ expect(screen.getByText(/your hair, your crown/i)).toBeInTheDocument()
+ for (const service of services) {
+ expect(screen.getByText(service.name)).toBeInTheDocument()
+ }
+ })
+
+ it('renders shop highlights and a link to the rules page', () => {
+ renderHome()
+ expect(screen.getByRole('link', { name: /shop all/i })).toHaveAttribute('href', '/shop')
+ expect(screen.getByRole('link', { name: /read the rules|salon rules/i })).toHaveAttribute('href', '/rules')
+ })
+
+ it('navigates to /book with the chosen service preselected when a signature style is clicked', async () => {
+ renderHome()
+ await userEvent.click(screen.getByRole('button', { name: services[0].name }))
+ expect(screen.getByLabelText(/choose a date/i)).toBeInTheDocument()
+ })
+})
+```
+
+- [ ] **Step 2: Run to verify it fails**
+
+Run: `npx vitest run src/pages/Home.test.tsx`
+Expected: FAIL (module doesn't exist)
+
+- [ ] **Step 3: Implement the `Home` page**
+
+`src/pages/Home.module.css`:
+
+```css
+.hero {
+ text-align: center;
+ padding: var(--space-xl) var(--space-lg);
+}
+
+.headline {
+ font-family: var(--font-script);
+ font-size: 3rem;
+ margin: var(--space-md) 0 0;
+}
+
+.tagline {
+ font-size: 0.75rem;
+ letter-spacing: 3px;
+ text-transform: uppercase;
+ color: #6b6259;
+}
+
+.ctas {
+ margin-top: var(--space-lg);
+ display: flex;
+ gap: var(--space-md);
+ justify-content: center;
+}
+
+.section {
+ padding: var(--space-xl) var(--space-lg);
+ text-align: center;
+}
+
+.altSection {
+ composes: section;
+ background: var(--color-cream-alt);
+}
+
+.row {
+ display: flex;
+ gap: var(--space-md);
+ justify-content: center;
+ flex-wrap: wrap;
+ margin-top: var(--space-lg);
+}
+
+.banner {
+ background: var(--color-charcoal);
+ color: var(--color-cream);
+ padding: var(--space-xl) var(--space-lg);
+ text-align: center;
+}
+
+.banner a {
+ color: var(--color-gold);
+}
+```
+
+`src/pages/Home.tsx`:
+
+```tsx
+import { Link, useNavigate } from 'react-router-dom'
+import Hero3D from '../components/Hero3D'
+import ScrollReveal from '../components/ScrollReveal'
+import ServiceCard from '../components/ServiceCard'
+import { services } from '../data/services'
+import { products } from '../data/products'
+import styles from './Home.module.css'
+
+const TESTIMONIALS = [
+ { quote: 'Mira made me feel like royalty in her chair. My braids lasted 8 weeks!', author: 'Amara T.' },
+ { quote: 'The most gentle, precise braider I have ever been to.', author: 'Keisha R.' },
+ { quote: 'Booking was so easy and the results speak for themselves.', author: 'Nadia F.' },
+]
+
+function Home() {
+ const navigate = useNavigate()
+
+ return (
+
+
+
+
Your hair, your crown
+
Protective Styles by Mira
+
+ Book Now
+ Shop Products
+
+
+
+
+
+
About Mira
+
+ With over a decade behind the chair, Mira treats every install as an art form —
+ gentle on the scalp, precise at the root, and finished with a crown you'll want
+ to show off.
+
+ Please take a moment to read the salon rules before booking your appointment.
+
+
+
+ )
+}
+
+export default Home
+```
+
+- [ ] **Step 4: Run to verify it passes**
+
+Run: `npx vitest run src/pages/Home.test.tsx`
+Expected: PASS
+
+- [ ] **Step 5: Wire `Home` into `App.tsx`**
+
+In `src/App.tsx`, import `Home` from `./pages/Home` and replace the `/` placeholder route with `} />`.
+
+Update `src/App.test.tsx`'s `/` assertion:
+
+```tsx
+it('renders the Home page at /', () => {
+ render()
+ expect(screen.getByText(/your hair, your crown/i)).toBeInTheDocument()
+})
+```
+
+- [ ] **Step 6: Run the full test suite**
+
+Run: `npx vitest run`
+Expected: all PASS
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/pages/Home.tsx src/pages/Home.module.css src/pages/Home.test.tsx src/App.tsx src/App.test.tsx
+git commit -m "feat: add Home landing page assembling all sections"
+```
+
+---
+
+### Task 14: Final polish, responsive check, and manual verification
+
+**Files:**
+- Modify: `src/pages/Home.module.css`, `src/pages/Shop.module.css`, `src/pages/Book.module.css`, `src/pages/Rules.module.css`, `src/components/Nav.module.css`, `src/components/Footer.module.css` (responsive breakpoints)
+- No new test files — this task is verification + small CSS adjustments, not new behavior
+
+- [ ] **Step 1: Add a mobile breakpoint to `Nav.module.css`**
+
+Append to `src/components/Nav.module.css`:
+
+```css
+@media (max-width: 640px) {
+ .nav {
+ flex-direction: column;
+ gap: var(--space-sm);
+ }
+ .links {
+ gap: var(--space-md);
+ }
+}
+```
+
+- [ ] **Step 2: Add a mobile breakpoint to `Home.module.css`**
+
+Append to `src/pages/Home.module.css`:
+
+```css
+@media (max-width: 640px) {
+ .headline {
+ font-size: 2.2rem;
+ }
+ .row {
+ flex-direction: column;
+ align-items: center;
+ }
+}
+```
+
+- [ ] **Step 3: Add a mobile breakpoint to `Shop.module.css`**
+
+Append to `src/pages/Shop.module.css`:
+
+```css
+@media (max-width: 640px) {
+ .grid {
+ grid-template-columns: 1fr;
+ }
+}
+```
+
+- [ ] **Step 4: Run the full test suite one final time**
+
+Run: `npx vitest run`
+Expected: all tests across `src/` PASS
+
+- [ ] **Step 5: Type-check and build**
+
+Run: `npm run build`
+Expected: `tsc -b` and `vite build` both succeed with no errors
+
+- [ ] **Step 6: Manual verification in the browser**
+
+Run: `npm run dev`
+
+Open the printed local URL and check, for each page:
+- `/` — hero renders with the rotating motif, script headline, scroll reveals trigger as you scroll, Signature Style cards navigate to `/book` with the service preselected
+- `/shop` — category filters work, "Ask about this" opens the inquiry modal, submitting with valid info shows the confirmation, out-of-stock products show the disabled state
+- `/book` — full flow from service → date → slot → contact → confirmation works; picking a Sunday shows the empty state with a working "try next available date" button
+- `/rules` — all five policy sections render, "Book Now" link goes to `/book`
+- Resize the browser to a narrow (mobile) width and confirm nav, hero, and grids reflow sensibly
+- In your OS/browser accessibility settings, enable "reduce motion" and reload — confirm the hero motif and scroll reveals no longer animate but all content still renders
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add -A
+git commit -m "polish: add responsive breakpoints for nav, home, and shop"
+```
+
+---
+
+## Self-Review Notes
+
+- **Spec coverage:** Tech stack (Task 1), visual system/tokens (Task 1), Nav/Footer/Layout (Tasks 4-5), Hero3D motion (Task 6), Home's 7 sections (Task 13), Shop with inquiry-not-cart (Tasks 7-9), Book's full mock-data flow with deep-link preselection and empty-state handling (Tasks 10-11), Rules page (Task 12), reduced-motion handling (Tasks 3 and 6), responsive layout (Task 14), Vitest/RTL testing throughout. All spec sections are covered.
+- **Type consistency:** `Product`, `Service`, `TimeSlot`, `RuleSection`, `BookingState`/`BookingContact` are defined once each (Tasks 2 and 10) and reused by name in every later task without renaming.
+- **No placeholders:** every step includes literal file contents and exact commands.