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
| File | Action |
|---|---|
public/hero-book.png | Copy from design/refresh/surfc-ux/project/frames/ |
.gitignore | Add .superpowers/ |
src/App.jsx | Remove lines 25, 189–194, 214–219, 338–345 |
src/components/HowItWorksPage.jsx | Delete |
src/content/howItWorksContent.js | Delete |
src/test/how-it-works.test.jsx | Delete |
src/test/LandingPage.policyLinks.test.jsx | Update link text selectors |
src/test/landing-page.test.jsx | Create — new behavioural tests |
src/components/LandingPage.jsx | Complete rewrite |
src/styles.css | Add /* ─── 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/
cp "design/refresh/surfc-ux/project/frames/hero-book.png" public/hero-book.pngExpected: 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
git add public/hero-book.png .gitignoregit 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
npm run buildExpected: Build succeeds with zero errors. No references to HowItWorksPage or isHowItWorksRoute should remain.
- Step 6: Commit
git add src/App.jsxgit 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
git rm src/components/HowItWorksPage.jsx src/content/howItWorksContent.js src/test/how-it-works.test.jsx- Step 2: Verify no remaining imports
grep -r "HowItWorksPage\|howItWorksContent" src/Expected: no output.
- Step 3: Commit
git commit -m "chore: delete HowItWorksPage, howItWorksContent, and stale tests"Task 4: Update policyLinks test + write new failing 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
npx vitest run src/test/landing-page.test.jsxExpected: 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
git add src/test/landing-page.test.jsx src/test/LandingPage.policyLinks.test.jsxgit 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
npx vitest run src/test/landing-page.test.jsx src/test/LandingPage.policyLinks.test.jsxExpected: 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
npx vitest runExpected: 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
git add src/components/LandingPage.jsxgit 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
npm run buildExpected: Zero errors. If Vite warns about any CSS, check for typos in property names.
- Step 3: Commit
git add src/styles.cssgit 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
grep -n "LANDING PAGE\|\.lp-" src/styles.css | head -30Expected 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:
grep -n "\.lp-" src/styles.cssExpected: no output.
- Step 3: Build to confirm no regressions
npm run buildExpected: Zero errors.
- Step 4: Run the full test suite
npx vitest runExpected: All tests pass.
- Step 5: Commit
git add src/styles.cssgit commit -m "chore(styles): remove old landing page CSS"Task 8: Final verification
Files: None — verification only.
- Step 1: Run the complete test suite
npx vitest runExpected: 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
npm run buildExpected: 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
npm run devOpen 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-surfcsection -
Clicking “FAQ” scrolls to the
#faqsection -
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
git status# only commit if there are outstanding changesgit add -pgit commit -m "chore: post-implementation tidy-up"Future Work (out of scope for this plan)
- Replace
CapturePhoneMockupwith liveCaptureScreenonce 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.