# Marketing visuals — home & about Plan for adding visuals to the logged-out home page (`web/components/home/home.tsx`) and the about page (`web/pages/about.tsx`). Both are currently 100% type + Heroicons; `web/public/images/` holds only logos and `default-avatar.png`. Every differentiator ("keyword search any profile", "filter by values", "get notified about searches", "democratic") is asserted in prose and never shown. ## Principles - **Product visuals, not people visuals.** The home subtitle ends "— not just your photos". Stock imagery of smiling couples would contradict the positioning and read as the thing we're arguing against. - **Captured, not mocked.** Everything that _can_ be a screenshot of the live app should be, so it can be regenerated when the UI changes instead of rotting. `media-creator/scripts/capture-profile.mjs` already does this for the profile tour reel; the same approach extends to marketing stills. - **Never fabricate social proof.** See H3 below. - **Poster-first.** The LCP element stays a static image; video is lazy and decorative. ## Prerequisite: showcase seed data (W0) Screenshots are only as good as the database behind them. The current seed (`tests/e2e/utils/seedDatabase.ts` + `tests/e2e/backend/utils/userInformation.ts`) produces: - `bio` = 200–350 lorem-ipsum words, `headline` = lorem words, `keywords` = lorem words - `age` uniform in 18–100 - `pinned_url` = `/images/default-avatar.png` for _every_ medium/full profile - `city` / `country` / `university` / `links` / `photo_urls` / interests / causes / work never set Unusable for capture. W0 adds a curated set of hand-authored personas alongside the faker ones — real prose, coherent field combinations, real photos, real locations, populated interests/causes/work and social links. **Status: in progress.** See "Showcase profiles" below. ## Home page ### H1 — Hero search demo ✅ The page said "Don't Swipe. / Search." and then never showed a search. It now does, directly under the hero: one real mobile session, filter to women → type `meditation` → 10 people narrow to 2 → open Sofia's profile → scroll it → write to her. That covers both halves of the pitch (keyword search _and_ filtering) plus the payoff, which a static grid shot cannot. **Mobile, not desktop.** A desktop capture rendered into a phone-width column puts the app's 14px type at roughly 5px — unreadable for what is likely most of the traffic. The mobile capture is legible on a phone at natural scale (~11px effective) and on desktop the CSS phone frame explains the tall aspect ratio rather than it reading as an odd crop. Measured both before choosing; this was not a taste call. **Capture** — `media-creator/scripts/capture-search.mjs` (`npm run capture:search`). Signs in as the seeded viewer account headlessly, dismisses the growth-phase banner (timely copy would date the clip), kills CSS animations so no frame lands mid-transition, then walks the flow shooting 27 frames. Typing and scrolling are _captured_ step by step, not animated — so the result count updates on the app's own debounce schedule and the clip cannot drift from the product. Two things that bite when scripting this page: - Both filter UIs are always in the DOM, one hidden by `lg:` classes. Playwright's `.first()` is DOM order, not visibility, so on a phone viewport it returns the _desktop_ control and times out. Hence the `firstVisible()` helper — use it for anything that exists in both layouts. - Filter sections start collapsed, so "Any gender" must be expanded before "Woman" exists in the DOM. Each frame carries its own `hold` in the manifest, so pacing lives next to the interaction it describes and the Remotion scene stays a dumb sequencer. **Tap indicators.** Frames also carry a `tap` position (CSS px) and the scene draws an expanding ring plus a lingering dot there. Without it the clip is a series of UI states that mutate for no visible reason — you can see _that_ the filter sheet opened, not that someone pressed a button to open it. The marker is attached to the frame _before_ the click, and plays over the tail of its hold. Two reasons: clicking often reflows the page (choosing a gender inserts the "Woman ×" summary row and pushes everything down), so a position measured pre-click does not describe the post-click frame and the ripple lands on the wrong control; and press-then-result is the truthful order. **Smooth scrolling.** The profile scroll is 30 steps of 100px held 3 frames each, not 7 steps of 420px held 14. Total distance and duration are about the same, but a large jump followed by stillness reads as a stutter however long you hold it; ~33px per frame at 30fps is continuous motion. Smoothness here is about step size, not overall pace. Four capture traps, all fixed and all worth knowing: - Waiting a fixed 3s after opening the profile was hostage to however long the dev server took to compile the route. When it ran long, the first frames caught the results list mid-navigation with the card greyed out and the "scroll the profile" beat opened on the wrong page. Now waits on `waitForURL` + `networkidle` instead of guessing. - Playwright leaves the virtual pointer wherever it last clicked, so whatever sits under that point stays hovered for the rest of the run. After closing the filter sheet the pointer rested on a card's bookmark icon and its "Save Profile" tooltip hung over the results for the whole clip. `shoot()` now parks the pointer in the top-left corner before every frame. - The first anchor on the page is the visually hidden "Skip to main content" link. Clicking it still ends up navigating, so the flow looked fine — but it has no bounding box, so the tap silently went missing. Target the result card's name heading instead. - `scrollIntoViewIfNeeded()` stops as soon as an element is technically in the viewport, which parked the contact button flush against the bottom edge under the fixed nav. Use `scrollIntoView({block: 'center'})` for anything that gets tapped. **Loop seam.** The clip is a looping `