Skip to content

Landing Page Redesign Implementation Plan

Landing Page Redesign 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: Replace LandingPage.jsx and HowItWorksPage.jsx with the new comprehensive marketing page from the design handoff, making it the single unauthenticated entry point at /.

Architecture: Rebuild LandingPage.jsx in-place with 11 sections (nav, hero, quote, what-surfc-is, capture, tag-ideas, supporting, testimonials, FAQ, closing, footer). Remove HowItWorksPage.jsx and the /how-it-works route from App.jsx. All new CSS goes in styles.css; old landing page CSS is removed.

Tech Stack: React 18, Vite, CSS custom properties (tokens in src/tokens.css), Vitest + React Testing Library.


File Map

FileAction
public/hero-book.pngCopy from design/refresh/surfc-ux/project/frames/
.gitignoreAdd .superpowers/
src/App.jsxRemove lines 25, 189–194, 214–219, 338–345
src/components/HowItWorksPage.jsxDelete
src/content/howItWorksContent.jsDelete
src/test/how-it-works.test.jsxDelete
src/test/LandingPage.policyLinks.test.jsxUpdate link text selectors
src/test/landing-page.test.jsxCreate — new behavioural tests
src/components/LandingPage.jsxComplete rewrite
src/styles.cssAdd /* ─── MARKETING PAGE v2 ─── */ section; remove old /* ─── LANDING PAGE ─── */ section

Task 1: Copy hero image asset and update .gitignore

Files:

  • Create: public/hero-book.png

  • Modify: .gitignore

  • Step 1: Copy the hero image into public/

Terminal window
cp "design/refresh/surfc-ux/project/frames/hero-book.png" public/hero-book.png

Expected: public/hero-book.png now exists (~200–500 KB image file).

  • Step 2: Add .superpowers/ to .gitignore

Open .gitignore and add this line at the end (keep existing entries):

.superpowers/
  • Step 3: Commit
Terminal window
git add public/hero-book.png .gitignore
git commit -m "chore: add hero-book.png asset and ignore .superpowers dir"

Task 2: Remove how-it-works routing from App.jsx

Files:

  • Modify: src/App.jsx

  • Step 1: Remove the HowItWorksPage import (line 25)

Delete this line:

import HowItWorksPage from './components/HowItWorksPage.jsx'
  • Step 2: Remove howItWorksSource state and isHowItWorksRoute (lines 189–194)

Delete these lines (they appear right after const policyRoute = matchPolicyRoute(pathname)):

const [howItWorksSource] = useState(() => {
if (window.location.pathname !== '/how-it-works') return null
const params = new URLSearchParams(window.location.search)
return params.get('source') || 'direct'
})
const isHowItWorksRoute = pathname === '/how-it-works'
  • Step 3: Remove the useEffect that strips ?source= from the URL (lines 214–219)

Delete this block (appears between the hashchange effect and navigatePublic):

useEffect(() => {
if (!isHowItWorksRoute || !window.location.search) return
const url = new URL(window.location.href)
url.search = ''
window.history.replaceState({}, '', url.toString())
}, [isHowItWorksRoute])
  • Step 4: Remove the HowItWorksPage early-return block (lines 338–345)

Delete this block (appears right before the if (policyRoute) block):

if (isHowItWorksRoute) {
return (
<HowItWorksPage
isAuthenticated={session === undefined ? null : Boolean(session)}
source={howItWorksSource || 'direct'}
/>
)
}
  • Step 5: Verify the build passes
Terminal window
npm run build

Expected: Build succeeds with zero errors. No references to HowItWorksPage or isHowItWorksRoute should remain.

  • Step 6: Commit
Terminal window
git add src/App.jsx
git commit -m "feat(routing): remove /how-it-works route and HowItWorksPage"

Task 3: Delete retired files

Files:

  • Delete: src/components/HowItWorksPage.jsx

  • Delete: src/content/howItWorksContent.js

  • Delete: src/test/how-it-works.test.jsx

  • Step 1: Delete the three files

Terminal window
git rm src/components/HowItWorksPage.jsx src/content/howItWorksContent.js src/test/how-it-works.test.jsx
  • Step 2: Verify no remaining imports
Terminal window
grep -r "HowItWorksPage\|howItWorksContent" src/

Expected: no output.

  • Step 3: Commit
Terminal window
git commit -m "chore: delete HowItWorksPage, howItWorksContent, and stale tests"

Files:

  • Modify: src/test/LandingPage.policyLinks.test.jsx

  • Create: src/test/landing-page.test.jsx

  • Step 1: Update the policy link selectors in LandingPage.policyLinks.test.jsx

The new footer uses “Privacy” and “Terms” (not “Privacy Policy” / “Terms of Use”). Update the two getByRole queries:

// Line 22 — change from:
const link = screen.getByRole('link', { name: /Privacy Policy/i })
// to:
const link = screen.getByRole('link', { name: /^Privacy$/i })
// Line 33 — change from:
const link = screen.getByRole('link', { name: /Terms of Use/i })
// to:
const link = screen.getByRole('link', { name: /^Terms$/i })
  • Step 2: Create src/test/landing-page.test.jsx with failing tests
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import LandingPage from '../components/LandingPage.jsx'
vi.mock('../hooks/useAnalytics.js', () => ({
useAnalytics: () => ({ capture: vi.fn() })
}))
const setup = () => render(
<LandingPage onJoinWaitlist={vi.fn()} onSignIn={vi.fn()} />
)
describe('LandingPage', () => {
it('calls onJoinWaitlist when any Request invitation button is clicked', () => {
const onJoinWaitlist = vi.fn()
render(<LandingPage onJoinWaitlist={onJoinWaitlist} onSignIn={vi.fn()} />)
fireEvent.click(screen.getAllByRole('button', { name: /request invitation/i })[0])
expect(onJoinWaitlist).toHaveBeenCalledTimes(1)
})
it('calls onSignIn when any Sign in button is clicked', () => {
const onSignIn = vi.fn()
render(<LandingPage onJoinWaitlist={vi.fn()} onSignIn={onSignIn} />)
fireEvent.click(screen.getAllByRole('button', { name: /sign in/i })[0])
expect(onSignIn).toHaveBeenCalledTimes(1)
})
it('renders all five FAQ questions', () => {
setup()
expect(screen.getByText('Who is Surfc for?')).toBeInTheDocument()
expect(screen.getByText('How do I capture printed passages?')).toBeInTheDocument()
expect(screen.getByText('Can I capture handwritten notes?')).toBeInTheDocument()
expect(screen.getByText('Will my notes sync across devices?')).toBeInTheDocument()
expect(screen.getByText('Is my data private?')).toBeInTheDocument()
})
it('opens the first FAQ answer by default', () => {
setup()
expect(screen.getByText(/Surfc is for people who annotate books/i)).toBeInTheDocument()
})
it('closes an open FAQ item when its button is clicked again', () => {
setup()
fireEvent.click(screen.getByRole('button', { name: /who is surfc for/i }))
expect(screen.queryByText(/Surfc is for people who annotate books/i)).not.toBeInTheDocument()
})
it('opens a different FAQ item when its button is clicked', () => {
setup()
fireEvent.click(screen.getByRole('button', { name: /is my data private/i }))
expect(screen.getByText(/encrypted at rest using AES-256-GCM/i)).toBeInTheDocument()
})
it('contains anchor hrefs for the How it works and FAQ nav links', () => {
setup()
expect(document.querySelector('a[href="#what-is-surfc"]')).not.toBeNull()
expect(document.querySelector('a[href="#faq"]')).not.toBeNull()
})
})
  • Step 3: Run the new tests — verify they fail
Terminal window
npx vitest run src/test/landing-page.test.jsx

Expected: All 7 tests FAIL. The current LandingPage.jsx has no FAQ, no #what-is-surfc anchor, and no “Request invitation” button. If any pass, re-check the test selectors.

  • Step 4: Commit the tests
Terminal window
git add src/test/landing-page.test.jsx src/test/LandingPage.policyLinks.test.jsx
git commit -m "test(landing): write failing tests for new landing page design"

Task 5: Implement LandingPage.jsx

Files:

  • Rewrite: src/components/LandingPage.jsx

  • Step 1: Replace the entire file with the new component

import { useState, useEffect } from 'react'
import PolicyLink from './PolicyLink.jsx'
const FAQ_ITEMS = [
{
q: 'Who is Surfc for?',
a: 'Surfc is for people who annotate books, build commonplace notebooks, or study recurring themes across disciplines.'
},
{
q: 'How do I capture printed passages?',
a: 'Use the camera inside Surfc to take a photo. The ingestion pipeline transcribes the marked text and preserves page context.'
},
{
q: 'Can I capture handwritten notes?',
a: 'Yes. Photograph handwritten pages; Surfc keeps the handwriting image and transcribes the text for search.'
},
{
q: 'Will my notes sync across devices?',
a: 'Notes live locally first and sync across your devices when online. Expect instant local saves and background conflict-aware sync.'
},
{
q: 'Is my data private?',
a: 'Yes. Notes are encrypted at rest using AES-256-GCM and stay offline unless you choose to sync.'
}
]
const INDEX_IDEAS = [
{ label: 'Virtue', count: 12 },
{ label: 'Habit', count: 9 },
{ label: 'Tranquility', count: 7 },
{ label: 'Freedom', count: 5 },
{ label: 'Self', count: 4 },
{ label: 'Anxiety', count: 3 },
]
const CANONICAL_CHIPS = ['Virtue', 'Habit', 'Freedom', 'Self', 'Time', 'Memory', 'Beauty', 'Justice', 'Change', 'Wisdom']
const CUSTOM_CHIPS = ['Daily Practice', 'The Self']
const SUPPORTING_CARDS = [
{ icon: '📖', label: 'Sources & notes', body: 'Capture by photo or jot insights directly. Every note keeps its page and book attribution.' },
{ icon: '', label: 'Sync across devices', body: 'Notes live locally first and sync to the cloud when online. Outbox sync keeps devices aligned.' },
{ icon: '', label: 'Offline-first & private', body: 'Everything works without a connection. End-to-end encryption at rest with AES-256-GCM.' },
]
const TESTIMONIALS = [
{
body: "I've been using Surfc for 6 weeks and my reading finally feels cumulative. I can see what Seneca and Aurelius actually disagreed about.",
name: 'Elena R.',
meta: 'beta reader · 240 notes captured'
},
{
body: "The photo-to-text pipeline is the first one I've trusted with handwriting. It caught a marginal note I'd forgotten I wrote four years ago.",
name: 'James O.',
meta: 'beta reader · 87 notes captured'
}
]
function CapturePhoneMockup() {
return (
<div className="hiw-phone-frame">
<div className="hiw-phone-island" aria-hidden="true" />
<div className="hiw-phone-status" aria-hidden="true">
<span className="hiw-phone-time">9:41</span>
<svg className="hiw-phone-signal" width="16" height="11" viewBox="0 0 19 12">
<rect x="0" y="7.5" width="3.2" height="4.5" rx="0.7" fill="currentColor"/>
<rect x="4.8" y="5" width="3.2" height="7" rx="0.7" fill="currentColor"/>
<rect x="9.6" y="2.5" width="3.2" height="9.5" rx="0.7" fill="currentColor"/>
<rect x="14.4" y="0" width="3.2" height="12" rx="0.7" fill="currentColor"/>
</svg>
</div>
<div className="hiw-phone-screen">
<div className="hiw-phone-header">
<div className="hiw-phone-eyebrow">Capture Mode</div>
<div className="hiw-phone-heading">
Record your highlights<br />and annotations
</div>
</div>
<div className="hiw-phone-viewfinder" aria-hidden="true">
<div className="hiw-phone-page">
<div className="hiw-phone-chapter">Chapter IV</div>
We are what we repeatedly do.{' '}
<span className="hiw-phone-highlight">
Excellence, then, is not an act, but a habit.
</span>{' '}
And what is habit but the practiced imitation of virtue, day upon day, until the shape of one's soul has conformed itself...
</div>
<span className="hiw-bracket hiw-bracket-tl" />
<span className="hiw-bracket hiw-bracket-tr" />
<span className="hiw-bracket hiw-bracket-bl" />
<span className="hiw-bracket hiw-bracket-br" />
<div className="hiw-phone-detected">
<span className="hiw-phone-detected-label">Passage detected</span>
<div className="hiw-phone-glow" />
</div>
</div>
<p className="hiw-phone-hint">Mark up the passage first. Capture will ignore unmarked text.</p>
<div className="hiw-phone-controls" aria-hidden="true">
<div className="hiw-phone-manual">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 20h9M16.5 3.5a2.121 2.121 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/>
</svg>
<span>Manual</span>
</div>
<div className="hiw-phone-shutter" />
<div className="hiw-phone-spacer" />
</div>
</div>
<div className="hiw-phone-nav" aria-hidden="true">
<span className="hiw-phone-nav-icon">🏠</span>
<span className="hiw-phone-nav-icon hiw-phone-nav-active">📷</span>
<span className="hiw-phone-nav-icon">📋</span>
<span className="hiw-phone-nav-icon">👤</span>
</div>
<div className="hiw-phone-indicator" aria-hidden="true" />
</div>
)
}
export default function LandingPage({ onJoinWaitlist, onSignIn }) {
const [scrolled, setScrolled] = useState(false)
const [faqOpen, setFaqOpen] = useState(0)
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8)
window.addEventListener('scroll', onScroll, { passive: true })
return () => window.removeEventListener('scroll', onScroll)
}, [])
return (
<div className="lp-page-v2">
{/* ── STICKY NAV ──────────────────────────────────────────── */}
<nav className={`hiw-nav${scrolled ? ' hiw-nav-scrolled' : ''}`} aria-label="Primary">
<div className="hiw-nav-inner">
<a href="/" className="hiw-wordmark" aria-label="Surfc home">Surfc</a>
<div className="hiw-nav-links">
<a href="#what-is-surfc" className="hiw-nav-link">How it works</a>
<a href="#faq" className="hiw-nav-link">FAQ</a>
<button type="button" className="hiw-nav-signin" onClick={onSignIn}>Sign in</button>
<button type="button" className="btn btn-accent hiw-nav-cta" onClick={onJoinWaitlist}>
Request invitation
</button>
</div>
</div>
</nav>
{/* ── HERO ────────────────────────────────────────────────── */}
<header className="hiw-hero-v2">
<div className="hiw-hero-grid">
<figure className="hiw-hero-photo-wrap">
<div className="hiw-hero-photo-frame">
<img
src="/hero-book.png"
alt="An annotated open book with multi-colored highlights and sticky note tabs in the margin."
className="hiw-hero-photo"
/>
</div>
<figcaption className="hiw-hero-caption">
<span className="hiw-caption-dot" aria-hidden="true" />
Read like you always do.
</figcaption>
</figure>
<div className="hiw-hero-copy">
<p className="hiw-eyebrow-v2">How it works</p>
<h1 className="hiw-title-v2">
Your ideas.<br />
<span className="hiw-title-ink">Indexed.</span>
</h1>
<p className="hiw-lede">
You've read hundreds of books. Highlighted and scribbled around
hundreds of passages. Where are they now?
</p>
<p className="hiw-lede-strong">
Surfc turns a lifetime of reading into something you can actually
think with.
</p>
<div className="hiw-cta-row-v2">
<button type="button" className="btn btn-accent hiw-cta-primary" onClick={onJoinWaitlist}>
Request invitation
</button>
<button type="button" className="hiw-cta-secondary" onClick={onSignIn}>
Sign in
</button>
</div>
<p className="hiw-cta-helper">
New to Surfc? <strong>Request access.</strong> Already invited?
<strong> Sign in.</strong>
</p>
</div>
</div>
</header>
{/* ── PULLED QUOTE ────────────────────────────────────────── */}
<section className="hiw-quote-section">
<div className="hiw-quote-wrap">
<span className="hiw-quote-mark" aria-hidden="true">"</span>
<blockquote className="hiw-quote-body">
A commonplace book, reborn as a living index —
<em> searchable, connected, and yours.</em>
</blockquote>
<div className="hiw-quote-rule" aria-hidden="true" />
<p className="hiw-quote-sub">An offline-first reading companion for people who read to think.</p>
</div>
</section>
{/* ── WHAT SURFC IS ───────────────────────────────────────── */}
<section className="hiw-section-v2" id="what-is-surfc">
<div className="hiw-two-col">
<div className="hiw-col-copy">
<p className="hiw-step-label">01 · The idea</p>
<h2 className="hiw-h2">What Surfc is</h2>
<p className="hiw-body-v2">
Surfc is an offline-first reading companion. Scan passages and
reflections, tag them to canonical or custom ideas, and resurface
them when those themes recur.
</p>
<ul className="hiw-inline-list">
<li><span className="idea-chip"><span className="idea-dot" />102 canonical ideas</span></li>
<li><span className="idea-chip custom"><span className="idea-dot" style={{ background: 'var(--color-custom)' }} />Your own concepts</span></li>
<li><span className="idea-chip"><span className="idea-dot" />Cross-source index</span></li>
</ul>
</div>
<div className="hiw-col-diagram">
<div className="hiw-diagram-card dotgrid">
<div className="hiw-diagram-title">Your index</div>
<div className="hiw-diagram-ideas">
{INDEX_IDEAS.map(idea => (
<div key={idea.label} className="hiw-diagram-row">
<span className="hiw-diagram-tag">
<span className="idea-dot" />{idea.label}
</span>
<span className="hiw-diagram-count">{idea.count} notes</span>
</div>
))}
</div>
<div className="hiw-diagram-footer">
<span className="hiw-stamp">cross-indexed</span>
</div>
</div>
</div>
</div>
</section>
{/* ── CAPTURE PASSAGES ────────────────────────────────────── */}
<section className="hiw-section-v2 hiw-alt-bg">
<div className="hiw-two-col hiw-reverse">
<div className="hiw-col-diagram hiw-col-diagram-phone">
<CapturePhoneMockup />
<div className="hiw-callout hiw-callout-a">
<span className="hiw-callout-num" aria-hidden="true">1</span>
<span>Mark passages first.<br /><small>Surfc ignores unmarked text.</small></span>
</div>
<div className="hiw-callout hiw-callout-b">
<span className="hiw-callout-num" aria-hidden="true">2</span>
<span>Tap the shutter.<br /><small>Printed or handwritten — both work.</small></span>
</div>
</div>
<div className="hiw-col-copy">
<p className="hiw-step-label hiw-step-hero">02 · Core feature</p>
<h2 className="hiw-h2 hiw-h2-hero">Capture passages</h2>
<p className="hiw-lede-small">
Surfc transcribes your highlights and annotations from a photo of
the page — no typing, no re-reading.
</p>
<div className="hiw-subpoints">
<div className="hiw-subpoint">
<h3>Printed passages</h3>
<p>Surfc transcribes highlighted or annotated text from a page photo.</p>
</div>
<div className="hiw-subpoint">
<h3>Handwritten notes</h3>
<p>Upload full notebook spreads and keep context-rich handwriting intact.</p>
</div>
</div>
</div>
</div>
</section>
{/* ── TAG IDEAS ───────────────────────────────────────────── */}
<section className="hiw-fullbleed">
<div className="hiw-fullbleed-inner">
<p className="hiw-step-label hiw-step-hero">03 · Core feature</p>
<h2 className="hiw-h2 hiw-h2-hero hiw-h2-center">Tag ideas</h2>
<p className="hiw-lede-small hiw-center">
Tag notes to the 102 built-in ideas — or create your own. AI tag
suggestions propose themes so your reading compounds across sources.
</p>
<div className="hiw-chipcloud">
{CANONICAL_CHIPS.map((label, i) => (
<span
key={label}
className="idea-chip chip-pop"
style={{ animationDelay: `${i * 60}ms`, fontSize: 13, padding: '6px 12px' }}
>
<span className="idea-dot" />{label}
</span>
))}
{CUSTOM_CHIPS.map((label, i) => (
<span
key={label}
className="idea-chip custom chip-pop"
style={{ animationDelay: `${(CANONICAL_CHIPS.length + i) * 60}ms`, fontSize: 13, padding: '6px 12px' }}
>
<span className="idea-dot" style={{ background: 'var(--color-custom)' }} />{label}
</span>
))}
</div>
<p className="hiw-fullbleed-foot">
<span className="hiw-accent-text">Amber</span> chips are canonical.{' '}
<span className="hiw-custom-text">Green</span> chips are your own.
</p>
</div>
</section>
{/* ── SUPPORTING 3-UP ─────────────────────────────────────── */}
<section className="hiw-section-v2">
<div className="hiw-threeup">
<p className="hiw-step-label hiw-step-center">04 · Plus</p>
<h2 className="hiw-h2 hiw-h2-center">Built for how you actually read</h2>
<div className="hiw-threeup-grid">
{SUPPORTING_CARDS.map(card => (
<article key={card.label} className="surfc-card hiw-mini-card">
<div className="hiw-mini-glyph" aria-hidden="true">{card.icon}</div>
<h3 className="hiw-mini-title">{card.label}</h3>
<p className="hiw-mini-body">{card.body}</p>
</article>
))}
</div>
</div>
</section>
{/* ── TESTIMONIALS ────────────────────────────────────────── */}
<section className="hiw-section-v2 hiw-alt-bg">
<div className="hiw-testimonials">
<p className="hiw-step-label hiw-step-center">From the beta</p>
<h2 className="hiw-h2 hiw-h2-center">Early readers</h2>
<div className="hiw-testimonial-grid">
{TESTIMONIALS.map(quote => (
<figure key={quote.name} className="note-dot-card hiw-testimonial">
<blockquote className="hiw-testimonial-body">{quote.body}</blockquote>
<figcaption className="hiw-testimonial-meta">
<strong>{quote.name}</strong>
<span>{quote.meta}</span>
</figcaption>
</figure>
))}
</div>
</div>
</section>
{/* ── FAQ ─────────────────────────────────────────────────── */}
<section className="hiw-section-v2" id="faq">
<div className="hiw-faq-wrap">
<p className="hiw-step-label">Common questions</p>
<h2 className="hiw-h2">FAQ</h2>
<dl className="hiw-faq-list">
{FAQ_ITEMS.map((item, i) => {
const isOpen = faqOpen === i
return (
<div key={item.q} className={`hiw-faq-item-v2${isOpen ? ' open' : ''}`}>
<dt>
<button
type="button"
className="hiw-faq-q"
aria-expanded={isOpen}
onClick={() => setFaqOpen(isOpen ? -1 : i)}
>
<span>{item.q}</span>
<span className="hiw-faq-chev" aria-hidden="true">{isOpen ? '' : '+'}</span>
</button>
</dt>
{isOpen && <dd className="hiw-faq-a">{item.a}</dd>}
</div>
)
})}
</dl>
</div>
</section>
{/* ── CLOSING CTA ─────────────────────────────────────────── */}
<section className="hiw-closing-v2">
<div className="hiw-closing-inner">
<p className="hiw-step-label hiw-step-center">Take the next step</p>
<h2 className="hiw-h2 hiw-h2-center">Build your reading index</h2>
<p className="hiw-lede-small hiw-center">
Surfc is rolling out through a waitlist. Access is approval-based —
we're onboarding readers gradually.
</p>
<div className="hiw-cta-row-v2 hiw-cta-center">
<button type="button" className="btn btn-accent hiw-cta-primary" onClick={onJoinWaitlist}>
Request invitation
</button>
<button type="button" className="hiw-cta-secondary" onClick={onSignIn}>
Sign in
</button>
</div>
</div>
</section>
{/* ── FOOTER ──────────────────────────────────────────────── */}
<footer className="hiw-footer-v2">
<div className="hiw-footer-inner">
<span className="hiw-footer-mark">Surfc</span>
<div className="hiw-footer-links">
<PolicyLink to="/policies/privacy">Privacy</PolicyLink>
<span aria-hidden="true">·</span>
<PolicyLink to="/policies/terms">Terms</PolicyLink>
<span aria-hidden="true">·</span>
<a href="#" className="termly-display-preferences">Consent preferences</a>
</div>
</div>
</footer>
</div>
)
}
  • Step 2: Run the new tests — verify they now pass
Terminal window
npx vitest run src/test/landing-page.test.jsx src/test/LandingPage.policyLinks.test.jsx

Expected: All 9 tests PASS. If any fail, check the selector text in the test against the component’s rendered output.

  • Step 3: Run the full test suite to catch regressions
Terminal window
npx vitest run

Expected: All tests pass. If App.behaviour.test.jsx or others fail, check for any remaining reference to HowItWorksPage or how-it-works.

  • Step 4: Commit
Terminal window
git add src/components/LandingPage.jsx
git commit -m "feat(landing): implement new marketing page design"

Task 6: Add marketing page CSS to styles.css

Files:

  • Modify: src/styles.css

  • Step 1: Add the marketing page CSS block

Append the following to the end of src/styles.css (before any existing closing comments):

/* ─── MARKETING PAGE v2 ─────────────────────────────────────────── */
.lp-page-v2 {
min-height: 100vh;
background: #EFE4D0;
color: var(--color-text-primary);
padding-top: 72px;
}
/* ── Sticky nav ── */
.hiw-nav {
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
background: rgba(255, 248, 242, 0.82);
backdrop-filter: saturate(1.2) blur(10px);
-webkit-backdrop-filter: saturate(1.2) blur(10px);
border-bottom: 1px solid transparent;
transition: border-color 200ms, background 200ms;
}
.hiw-nav-scrolled {
border-bottom-color: var(--color-border);
background: rgba(255, 248, 242, 0.94);
}
.hiw-nav-inner {
max-width: 1180px; margin: 0 auto; padding: 14px 28px;
display: flex; align-items: center; justify-content: space-between; gap: 20px;
}
.hiw-wordmark {
font-family: 'EB Garamond', Georgia, serif;
font-size: 26px; font-weight: 600;
color: var(--color-accent); letter-spacing: -0.02em; text-decoration: none;
}
.hiw-nav-links { display: flex; align-items: center; gap: 20px; }
.hiw-nav-link {
color: var(--color-text-secondary); text-decoration: none;
font-size: 14px; font-weight: 500;
}
.hiw-nav-link:hover { color: var(--color-accent-dark); }
.hiw-nav-signin {
background: none; border: none; cursor: pointer;
color: var(--color-accent-dark);
font: 600 14px 'Inter', sans-serif; font-family: inherit;
text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 3px;
padding: 6px 4px;
}
.hiw-nav-signin:hover { color: #3F2800; }
.hiw-nav-cta { min-height: 40px; padding: 8px 16px; font-size: 14px; }
/* ── btn-accent hover (shared button, also used outside marketing page) ── */
.btn-accent:hover {
transform: translateY(-1px);
box-shadow: 0 3px 10px rgba(200,134,10,.28), inset 0 -2px 0 rgba(92,58,0,.18);
}
.btn-accent:active { transform: translateY(1px); box-shadow: inset 0 1px 3px rgba(92,58,0,.25); }
/* ── Hero ── */
.hiw-hero-v2 {
max-width: 1180px; margin: 0 auto; padding: 56px 28px 72px;
}
.hiw-hero-grid {
display: grid; grid-template-columns: 1.05fr 0.95fr;
gap: 56px; align-items: center;
}
@media (max-width: 880px) { .hiw-hero-grid { grid-template-columns: 1fr; gap: 32px; } }
.hiw-hero-photo-wrap { position: relative; }
.hiw-hero-photo-frame {
position: relative;
background: var(--color-bg-card); border: 1px solid var(--color-border);
border-radius: 10px; padding: 10px;
box-shadow: 0 24px 60px rgba(92,58,0,.18), 0 2px 8px rgba(92,58,0,.08);
transform: rotate(-1.2deg);
transition: transform 300ms ease;
}
.hiw-hero-photo-frame:hover { transform: rotate(-0.4deg); }
.hiw-hero-photo {
display: block; width: 100%; height: auto;
border-radius: 4px; aspect-ratio: 3/2; object-fit: cover;
}
.hiw-hero-caption {
position: absolute; bottom: -18px; left: 24px;
display: inline-flex; align-items: center; gap: 8px;
background: var(--color-bg-card); border: 1px solid var(--color-border);
padding: 6px 14px; border-radius: 999px;
font-size: 12px; color: var(--color-text-secondary); font-style: italic;
box-shadow: var(--shadow-card); transform: rotate(1.5deg);
}
.hiw-caption-dot {
width: 7px; height: 7px; border-radius: 50%; background: var(--color-accent);
}
.hiw-hero-copy { max-width: 520px; }
.hiw-eyebrow-v2 {
font-size: 12px; text-transform: uppercase; letter-spacing: 0.18em;
color: var(--color-accent-dark); font-weight: 600; margin: 0 0 20px;
}
.hiw-title-v2 {
font-family: 'EB Garamond', Georgia, serif;
font-size: clamp(54px, 7vw, 86px); line-height: 0.98;
letter-spacing: -0.02em; margin: 0 0 28px;
color: var(--color-text-primary); font-weight: 500;
}
.hiw-title-ink {
color: var(--color-accent); font-style: italic; position: relative;
}
.hiw-title-ink::after {
content: ''; position: absolute; left: -2%; right: -2%; bottom: 6%;
height: 8px; background: rgba(200,134,10,.18); border-radius: 10px; z-index: -1;
}
.hiw-lede {
font-size: 18px; line-height: 1.6; color: var(--color-text-secondary); margin: 0 0 14px;
}
.hiw-lede-strong {
font-size: 20px; line-height: 1.55; color: var(--color-text-primary);
margin: 0 0 32px; font-weight: 500;
}
.hiw-cta-row-v2 {
display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 14px;
}
.hiw-cta-primary { padding: 12px 22px; font-size: 15px; min-height: 48px; }
.hiw-cta-secondary {
background: none; border: none; cursor: pointer;
color: var(--color-accent-dark);
font: 600 15px 'Inter', sans-serif; font-family: inherit;
text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 1.5px;
padding: 12px 10px; min-height: 48px;
}
.hiw-cta-secondary:hover { color: #3F2800; }
.hiw-cta-helper { font-size: 13px; color: var(--color-text-muted); margin: 0; line-height: 1.5; }
.hiw-cta-helper strong { color: var(--color-text-secondary); }
/* ── Quote band ── */
.hiw-quote-section {
background: var(--color-bg-subtle);
border-top: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
padding: 72px 28px;
}
.hiw-quote-wrap { max-width: 920px; margin: 0 auto; text-align: center; position: relative; }
.hiw-quote-mark {
font-family: 'EB Garamond', Georgia, serif; font-size: 100px; line-height: 0.7;
color: var(--color-accent); display: block; margin-bottom: 10px; opacity: 0.4;
}
.hiw-quote-body {
font-family: 'EB Garamond', Georgia, serif;
font-size: clamp(26px, 3.4vw, 38px); line-height: 1.3;
margin: 0 0 24px; color: var(--color-text-primary); font-weight: 500;
}
.hiw-quote-body em { color: var(--color-accent-dark); font-style: italic; }
.hiw-quote-rule { width: 60px; height: 1px; background: var(--color-accent); margin: 0 auto 16px; opacity: 0.5; }
.hiw-quote-sub { font-size: 14px; color: var(--color-text-muted); margin: 0; letter-spacing: 0.02em; }
/* ── Section scaffolding ── */
.hiw-section-v2 { max-width: 1180px; margin: 0 auto; padding: 88px 28px; }
.hiw-alt-bg {
max-width: none;
background: linear-gradient(180deg, transparent 0%, rgba(245,237,224,.55) 30%, rgba(245,237,224,.55) 70%, transparent 100%);
}
.hiw-alt-bg > * { max-width: 1180px; margin-left: auto; margin-right: auto; }
.hiw-two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 64px; align-items: center; }
.hiw-two-col.hiw-reverse .hiw-col-copy { order: 2; }
.hiw-two-col.hiw-reverse .hiw-col-diagram { order: 1; }
@media (max-width: 880px) {
.hiw-two-col { grid-template-columns: 1fr; gap: 40px; }
.hiw-two-col.hiw-reverse .hiw-col-copy { order: 2; }
.hiw-two-col.hiw-reverse .hiw-col-diagram { order: 1; }
}
.hiw-step-label {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.18em;
color: var(--color-accent-dark); font-weight: 600; margin: 0 0 14px;
}
.hiw-step-center { text-align: center; }
.hiw-step-hero { color: var(--color-accent); }
.hiw-h2 {
font-family: 'EB Garamond', Georgia, serif;
font-size: clamp(32px, 3.6vw, 44px); line-height: 1.05;
letter-spacing: -0.015em; color: var(--color-text-primary);
font-weight: 500; margin: 0 0 20px;
}
.hiw-h2-hero { font-size: clamp(40px, 4.4vw, 56px); }
.hiw-h2-center { text-align: center; }
.hiw-body-v2 {
font-size: 17px; line-height: 1.65; color: var(--color-text-secondary);
margin: 0 0 20px; max-width: 520px;
}
.hiw-lede-small {
font-size: 17px; line-height: 1.6; color: var(--color-text-secondary);
margin: 0 0 24px; max-width: 640px;
}
.hiw-center { text-align: center; margin-left: auto; margin-right: auto; }
.hiw-inline-list { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 8px; }
/* ── Index diagram ── */
.hiw-diagram-card {
background: var(--color-bg-card); border: 1px solid var(--color-border);
border-radius: var(--radius-card);
box-shadow: 0 10px 30px rgba(92,58,0,.08);
padding: 24px 22px; max-width: 420px; margin: 0 auto; position: relative;
}
.hiw-diagram-title {
font-family: 'EB Garamond', serif; font-size: 13px;
color: var(--color-accent-dark); letter-spacing: 0.1em; text-transform: uppercase;
font-weight: 600; margin-bottom: 14px;
border-bottom: 1px dashed var(--color-border-strong); padding-bottom: 10px;
}
.hiw-diagram-ideas {}
.hiw-diagram-row {
display: flex; justify-content: space-between; align-items: center;
padding: 9px 0; border-bottom: 1px solid rgba(232,217,197,.5); font-size: 14px;
}
.hiw-diagram-row:last-child { border-bottom: none; }
.hiw-diagram-tag {
display: inline-flex; align-items: center; gap: 8px;
font-family: 'EB Garamond', serif; font-size: 17px; color: var(--color-text-primary);
}
.hiw-diagram-count { font-size: 12px; color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.hiw-diagram-footer { margin-top: 12px; text-align: right; }
.hiw-stamp {
font-size: 10px; text-transform: uppercase; letter-spacing: 0.14em;
color: var(--color-accent-dark); font-weight: 600;
border: 1px solid var(--color-accent-dark); border-radius: 4px;
padding: 3px 8px; opacity: 0.7;
}
/* ── Capture section: phone column + callouts ── */
.hiw-col-diagram-phone {
position: relative; min-height: 560px;
display: flex; justify-content: center; align-items: center;
}
.hiw-callout {
position: absolute; background: var(--color-bg-card);
border: 1px solid var(--color-border); border-radius: 12px;
padding: 10px 14px 10px 12px;
display: flex; align-items: flex-start; gap: 10px;
font-size: 13px; line-height: 1.4; color: var(--color-text-primary);
box-shadow: 0 8px 20px rgba(92,58,0,.1); max-width: 200px;
}
.hiw-callout small { color: var(--color-text-muted); font-size: 11.5px; display: block; margin-top: 2px; }
.hiw-callout-num {
flex: 0 0 22px; width: 22px; height: 22px; border-radius: 50%;
background: var(--color-accent); color: #1A1410;
font-size: 11px; font-weight: 700;
display: flex; align-items: center; justify-content: center;
}
.hiw-callout-a { top: 20%; left: -10px; transform: rotate(-2deg); }
.hiw-callout-b { bottom: 18%; right: -10px; transform: rotate(2deg); }
/* ── Capture sub-points ── */
.hiw-subpoints { display: grid; gap: 20px; margin-top: 24px; max-width: 520px; }
.hiw-subpoint {
padding: 18px 20px; background: var(--color-bg-card);
border: 1px solid var(--color-border); border-radius: 12px; box-shadow: var(--shadow-card);
}
.hiw-subpoint h3 {
margin: 0 0 6px; font-family: 'Inter', sans-serif;
font-size: 15px; font-weight: 600; color: var(--color-accent-dark);
}
.hiw-subpoint p { margin: 0; font-size: 14.5px; line-height: 1.55; color: var(--color-text-secondary); }
/* ── Phone frame mockup ── */
.hiw-phone-frame {
width: 249px; height: 542px; border-radius: 30px;
background: #FFF8F2;
box-shadow: 0 25px 50px rgba(0,0,0,.18), 0 0 0 1px rgba(0,0,0,.12);
position: relative; display: flex; flex-direction: column; overflow: hidden;
font-family: -apple-system, 'Inter', sans-serif;
}
.hiw-phone-island {
position: absolute; top: 7px; left: 50%; transform: translateX(-50%);
width: 78px; height: 23px; border-radius: 15px; background: #000; z-index: 50;
}
.hiw-phone-status {
position: absolute; top: 0; left: 0; right: 0; z-index: 10;
padding: 14px 16px 0; display: flex; justify-content: space-between; align-items: center;
}
.hiw-phone-time { font-size: 10.5px; font-weight: 590; color: #1A1410; }
.hiw-phone-signal { color: #1A1410; }
.hiw-phone-screen { flex: 1; padding: 42px 14px 0; display: flex; flex-direction: column; }
.hiw-phone-header { margin-bottom: 10px; }
.hiw-phone-eyebrow {
font-size: 7px; text-transform: uppercase; letter-spacing: .14em;
color: var(--color-accent-text); font-weight: 600;
}
.hiw-phone-heading {
font-size: 13.5px; font-weight: 600; color: var(--color-text-primary);
margin-top: 2px; line-height: 1.3;
}
.hiw-phone-viewfinder {
position: relative; flex: 1; margin-top: 4px;
background: linear-gradient(135deg, #2b2218, #1a1410);
border-radius: 9px; overflow: hidden; min-height: 220px;
}
.hiw-phone-page {
position: absolute; inset: 12% 14%;
background:
repeating-linear-gradient(to bottom, transparent 0, transparent 11px, rgba(0,0,0,.08) 11px, rgba(0,0,0,.08) 12px),
#f6ebd5;
border-radius: 3px; box-shadow: 0 6px 18px rgba(0,0,0,.4);
padding: 8px; font-size: 5.8px; line-height: 12px;
color: #2b2218; font-family: 'EB Garamond', serif;
}
.hiw-phone-chapter {
font-size: 6px; letter-spacing: .14em; text-transform: uppercase;
font-family: 'Inter', sans-serif; color: #7A4F00; margin-bottom: 4px;
}
.hiw-phone-highlight {
background: rgba(200,134,10,.35);
box-shadow: inset 0 -2px 0 rgba(200,134,10,.6);
padding: 0 1px;
}
.hiw-bracket { position: absolute; width: 10px; height: 10px; }
.hiw-bracket-tl { top: 6px; left: 6px; border-top: 2px solid rgba(200,134,10,.8); border-left: 2px solid rgba(200,134,10,.8); }
.hiw-bracket-tr { top: 6px; right: 6px; border-top: 2px solid rgba(200,134,10,.8); border-right: 2px solid rgba(200,134,10,.8); }
.hiw-bracket-bl { bottom: 6px; left: 6px; border-bottom: 2px solid rgba(200,134,10,.8); border-left: 2px solid rgba(200,134,10,.8); }
.hiw-bracket-br { bottom: 6px; right: 6px; border-bottom: 2px solid rgba(200,134,10,.8); border-right: 2px solid rgba(200,134,10,.8); }
.hiw-phone-detected { position: absolute; left: 16%; right: 16%; top: 28%; }
.hiw-phone-detected-label {
display: block; font-size: 6px; color: var(--color-accent);
text-transform: uppercase; letter-spacing: .12em; font-weight: 600;
font-family: 'Inter', sans-serif; margin-bottom: 2px;
}
.hiw-phone-glow {
height: 25px; background: rgba(200,134,10,.18);
border: 1px solid rgba(200,134,10,.5); border-radius: 4px;
box-shadow: 0 0 14px rgba(200,134,10,.28);
}
.hiw-phone-hint {
text-align: center; font-size: 6px; color: var(--color-text-muted);
font-style: italic; margin: 6px 0 4px;
}
.hiw-phone-controls {
display: flex; align-items: center; justify-content: center; gap: 14px; padding: 8px 0 4px;
}
.hiw-phone-manual {
background: none; border: none; color: var(--color-text-secondary);
display: flex; flex-direction: column; align-items: center; gap: 2px;
font: 600 5px/1 'Inter', sans-serif; font-family: inherit;
}
.hiw-phone-shutter {
width: 47px; height: 47px; border-radius: 50%;
background: var(--color-accent);
border: 2.5px solid #FFF8F2;
outline: 1.5px solid var(--color-accent);
box-shadow: 0 2px 8px rgba(200,134,10,.4);
}
.hiw-phone-spacer { width: 37px; }
.hiw-phone-nav {
display: flex; justify-content: space-around; align-items: center;
padding: 8px 20px 4px;
border-top: 1px solid var(--color-border);
background: var(--color-bg-primary);
}
.hiw-phone-nav-icon { font-size: 16px; opacity: 0.4; }
.hiw-phone-nav-active { opacity: 1; }
.hiw-phone-indicator { height: 20px; display: flex; justify-content: center; align-items: flex-end; padding-bottom: 5px; }
.hiw-phone-indicator::after {
content: ''; width: 86px; height: 3px; border-radius: 100%; background: rgba(0,0,0,.25);
}
/* ── Tag ideas: full-bleed chip cloud ── */
.hiw-fullbleed {
background:
radial-gradient(circle at 1px 1px, rgba(200,134,10,.22) 1px, transparent 0) 0 0 / 22px 22px,
var(--color-bg-primary);
border-top: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
padding: 96px 28px 112px;
}
.hiw-fullbleed-inner { max-width: 960px; margin: 0 auto; text-align: center; }
.hiw-chipcloud {
margin: 40px auto 24px; max-width: 720px;
display: flex; flex-wrap: wrap; gap: 10px; justify-content: center;
}
.hiw-fullbleed-foot { margin-top: 16px; font-size: 13px; color: var(--color-text-muted); }
.hiw-accent-text { color: var(--color-accent-dark); font-weight: 600; }
.hiw-custom-text { color: var(--color-custom-text); font-weight: 600; }
@keyframes chipPop {
0% { opacity: 0; transform: scale(0.8); }
60% { opacity: 1; transform: scale(1.05); }
100% { opacity: 1; transform: scale(1); }
}
.chip-pop { animation: chipPop 400ms ease forwards; opacity: 0; }
/* ── Supporting 3-up ── */
.hiw-threeup { text-align: center; }
.hiw-threeup-grid {
margin-top: 40px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; text-align: left;
}
@media (max-width: 820px) { .hiw-threeup-grid { grid-template-columns: 1fr; } }
.hiw-mini-card { padding: 24px 22px; }
.hiw-mini-glyph {
font-size: 28px; line-height: 1; color: var(--color-accent); margin-bottom: 14px;
display: inline-flex; align-items: center; justify-content: center;
width: 44px; height: 44px; border-radius: 10px; background: var(--color-accent-light);
}
.hiw-mini-title { margin: 0 0 8px; font: 600 17px/1.3 'Inter', sans-serif; color: var(--color-text-primary); }
.hiw-mini-body { margin: 0; font-size: 14.5px; line-height: 1.6; color: var(--color-text-secondary); }
/* ── Testimonials ── */
.hiw-testimonials { text-align: center; }
.hiw-testimonial-grid {
margin-top: 40px; display: grid; grid-template-columns: 1fr 1fr; gap: 24px; text-align: left;
}
@media (max-width: 820px) { .hiw-testimonial-grid { grid-template-columns: 1fr; } }
.hiw-testimonial { padding: 24px 22px 20px; }
.hiw-testimonial-body {
font-family: 'EB Garamond', Georgia, serif;
font-size: 19px; line-height: 1.55; color: var(--color-text-primary); margin: 0 0 18px;
}
.hiw-testimonial-meta { display: flex; flex-direction: column; gap: 2px; font-size: 12.5px; }
.hiw-testimonial-meta strong { color: var(--color-accent-dark); font-weight: 600; font-size: 13.5px; }
.hiw-testimonial-meta span { color: var(--color-text-muted); }
/* ── FAQ ── */
.hiw-faq-wrap { max-width: 760px; margin: 0 auto; }
.hiw-faq-list { margin: 20px 0 0; padding: 0; border-top: 1px solid var(--color-border); }
.hiw-faq-item-v2 { border-bottom: 1px solid var(--color-border); }
.hiw-faq-q {
width: 100%; text-align: left; cursor: pointer;
background: none; border: none; padding: 20px 4px;
font: 600 17px 'Inter', sans-serif; font-family: inherit;
color: var(--color-text-primary);
display: flex; justify-content: space-between; align-items: center; gap: 16px;
}
.hiw-faq-q:hover { color: var(--color-accent-dark); }
.hiw-faq-chev {
width: 28px; height: 28px; border-radius: 50%;
background: var(--color-accent-light); color: var(--color-accent-dark);
font-size: 20px; font-weight: 400;
display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.hiw-faq-item-v2.open .hiw-faq-chev { background: var(--color-accent); color: #1A1410; }
.hiw-faq-a {
margin: 0; padding: 0 4px 22px;
font-size: 15.5px; line-height: 1.65; color: var(--color-text-secondary); max-width: 680px;
}
/* ── Closing CTA ── */
.hiw-closing-v2 { max-width: 780px; margin: 0 auto; padding: 96px 28px 72px; text-align: center; }
.hiw-closing-inner {}
.hiw-cta-center { justify-content: center; }
/* ── Footer ── */
.hiw-footer-v2 { border-top: 1px solid var(--color-border); padding: 28px; }
.hiw-footer-inner {
max-width: 1180px; margin: 0 auto;
display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px;
}
.hiw-footer-mark { font-family: 'EB Garamond', serif; font-size: 20px; color: var(--color-accent); font-weight: 600; }
.hiw-footer-links { display: flex; gap: 10px; align-items: center; font-size: 13px; color: var(--color-text-muted); }
.hiw-footer-links a { color: var(--color-text-secondary); text-decoration: none; }
.hiw-footer-links a:hover { color: var(--color-accent-dark); text-decoration: underline; }
  • Step 2: Verify the build still passes
Terminal window
npm run build

Expected: Zero errors. If Vite warns about any CSS, check for typos in property names.

  • Step 3: Commit
Terminal window
git add src/styles.css
git commit -m "feat(styles): add marketing page v2 CSS"

Task 7: Remove old landing page CSS from styles.css

Files:

  • Modify: src/styles.css

  • Step 1: Find the old landing page CSS section boundaries

Terminal window
grep -n "LANDING PAGE\|\.lp-" src/styles.css | head -30

Expected output will show the /* ─── LANDING PAGE ─── */ comment (around line 3103) and all lp-* rules below it. Note the first and last line numbers.

  • Step 2: Delete all lines in the landing page section

Open src/styles.css. Delete from the /* ─── LANDING PAGE ─── */ comment down through all .lp-* rules. The section runs from approximately line 3103 to the next unrelated section header. Use your editor’s search to find /* ─── LANDING PAGE and remove everything up to the next /* ─── comment block.

After deletion, verify no lp- rules remain:

Terminal window
grep -n "\.lp-" src/styles.css

Expected: no output.

  • Step 3: Build to confirm no regressions
Terminal window
npm run build

Expected: Zero errors.

  • Step 4: Run the full test suite
Terminal window
npx vitest run

Expected: All tests pass.

  • Step 5: Commit
Terminal window
git add src/styles.css
git commit -m "chore(styles): remove old landing page CSS"

Task 8: Final verification

Files: None — verification only.

  • Step 1: Run the complete test suite
Terminal window
npx vitest run

Expected: All tests PASS. Key tests to confirm:

  • src/test/landing-page.test.jsx — 7 tests pass

  • src/test/LandingPage.policyLinks.test.jsx — 2 tests pass

  • src/test/App.behaviour.test.jsx — all pass (no HowItWorksPage references)

  • Step 2: Production build

Terminal window
npm run build

Expected: Build succeeds with zero errors. Output bundle size should be smaller than before (HowItWorksPage and howItWorksContent removed).

  • Step 3: Visual smoke-check in dev server
Terminal window
npm run dev

Open http://localhost:5173 in a browser while unauthenticated (or clear your session). Verify:

  • Sticky nav appears, becomes opaque on scroll

  • Hero photo is tilted by default, straightens on hover

  • “Request invitation” buttons lift on hover (translateY(-1px) + shadow)

  • Clicking “How it works” scrolls to the #what-is-surfc section

  • Clicking “FAQ” scrolls to the #faq section

  • FAQ first item is open by default; clicking closes it; clicking another opens it

  • “Request invitation” → shows waitlist screen

  • “Sign in” → shows auth screen

  • Footer Privacy and Terms links navigate client-side

  • Step 4: Final commit if any clean-up edits were made

Terminal window
git status
# only commit if there are outstanding changes
git add -p
git commit -m "chore: post-implementation tidy-up"

Future Work (out of scope for this plan)

  • Replace CapturePhoneMockup with live CaptureScreen once its hook/context dependencies can be stubbed for a public page (no auth, no Dexie, no camera API).
  • Replace placeholder testimonials (Elena R., James O.) with real beta reader quotes.
  • Add a real capture screen screenshot once available.