Launch Page

Landing page app · built by the Vibecode team

Launch Page is a studio-grade launch site for a fictional AI calendar called Halo: a huge serif hero, product screens built in HTML on photo cards, pricing with a monthly/yearly toggle, FAQ, a blog, a changelog, and Privacy and Terms pages. The waitlist really works: signups are stored and de-duplicated, everyone sees their place in line ("You're #214"), and each friend who joins with their referral link moves them up five spots. A private link lets people check their spot or delete their data, and the owner signs in at /admin (the login is shown on first run) to see real stats, search signups and export them to CSV. Every name, colour and line of copy lives in one config file, documented in the README.

  • Web app
  • Built in 8 prompts
  • Free to remix
Launch Page, landing page app built with Vibecode
Remix this template

Remix copies the working app into your account for free, exactly as we built it. It runs in your browser.

The prompt that builds it

We built Launch Page in 8 prompts. Here they are folded into one, so a single paste into a new app builds the finished version.

Prompt · 8,985 words

Build a complete, production-quality launch website for a fictional AI product called **Halo — "The calendar that schedules itself."** It is a template founders will remix to launch their own SaaS or mobile app, so it must look like a top design studio made it (Venice, who designed Poke and OpenAI Dev Day; Turbo; Off-Menu; Josh Comeau), and every flow must really work: a waitlist that stores signups in a database, de-dupes them, shows each person's place in line ("You're #214") with a referral link to move up, an owner admin at /admin with CSV export, pricing with a monthly/yearly toggle, FAQ, blog, changelog, Privacy and Terms pages, App Store / Google Play badges, SEO meta, an OG image and Halo's own favicon. Every name, colour and line of copy must be editable from ONE config file.

Design references (the original screenshots are not included; build from these descriptions):
- Reference: venice.inc hero: huge editorial serif on white, light-grey spaced commas between items, one small sans intro line above, small dark pill button. This is the typographic voice of the whole site.
- Reference: venice.inc: centred serif caption over a grey logo grid, then a serif section heading with sans body.
- Reference: venice.inc testimonials: black page, company wordmark, big serif quote, small avatar + name + role.
- Reference: venice.inc: centred serif CTA heading + dark pill, footer with two columns and a full-width rounded photo.
- Reference: turbodesign.co: big rounded showcase card with a product UI floating on a sky photo; mono uppercase "TRUSTED BY" label; live clock line.
- Reference: turbodesign.co: more showcase cards (laptop on a plaster wall, angled phones on light grey). Copy this "product UI on a photographic backdrop" card treatment.
- Reference: poke.com (a Venice client): "New" announcement chip, serif headline, dark + light buttons, phone floating over a landscape photo on warm paper.
- Reference: poke.com: numbered "(2)" feature row, serif title, sans body, "Learn more →", photo-backed UI card, 1px hairline dividers between rows.
- Reference: off-menu.com: a big paragraph whose words light up from dim to full as you scroll.
- Reference: code.storage: small mono labels in bordered tags, dotted connector lines and little ring nodes.

Build it fully, run it, and fix every error. Deliver the complete app, not a plan. When you've looked at the references, delete the refs/ folder: screenshots of other companies' sites must not ship in the template.

---

## 1. Stack and data

- Use the platform's standard stack: the React + Vite + Tailwind webapp and the Hono backend, with the database-auth skill (Prisma + SQLite + Better Auth). Read the frontend, backend and database-auth skills first.
- Better Auth email + password only, for ONE owner account. **Public sign-up is disabled** (`disableSignUp: true`); waitlist people are not user accounts. Give the owner a role (e.g. `role = "owner"`) and protect every `/api/admin/*` route with session + role.
- **Seed on boot, idempotently:** when the backend starts and the owner or the demo data is missing, create them. Remixes of this project must work on first run with no manual step.
  - Owner login: **owner@halo.example / halo-launch-2026**.
  - **213 demo waitlist signups** (`isDemo = true`) so the next real signup is "#214". Use 213 distinct, fictional, diverse name combinations (no first name more than twice) with emails on example domains only (example.com, example.org, example.net), e.g. "maya.lindqvist@example.com". About 60% have a first name.
    - Spread `createdAt` from Oct 4 to Nov 3, 2026, heavier in recent days, with a spike on Oct 20 (Product Hunt day).
    - Sources: x 28%, producthunt 18%, direct 22%, newsletter 14%, linkedin 10%, referral 8%.
    - About 40 of them were referred by about 12 referrers. The top referrer has 7 referrals.
- **Remixes must boot.** Remixing clears `BETTER_AUTH_SECRET` in backend/.env, and the standard env.ts would then exit with "BETTER_AUTH_SECRET is required". Add `backend/src/lib/authSecret.ts` exporting `ensureAuthSecret()`, and call it in backend/src/env.ts before the environment is validated (keep the validation). If `BETTER_AUTH_SECRET` is blank, it reuses a non-empty value already saved in backend/.env (Bun only loads the .env in the working directory); otherwise it generates `randomBytes(32).toString("base64")`, writes it to backend/.env (replacing a blank `BETTER_AUTH_SECRET=` line or appending one), sets `process.env.BETTER_AUTH_SECRET` and logs "Generated a new BETTER_AUTH_SECRET". If the file can't be written, warn and keep the in-memory secret.
- Prisma model (add to the auth models):
  ```
  model WaitlistSignup {
    id           String   @id @default(cuid())
    email        String   @unique      // normalised: trimmed, lowercased, "+tag" removed before @
    emailRaw     String                // as typed
    name         String?
    referralCode String   @unique      // 6 chars, A-Z and 2-9, no 0/O/1/I. PUBLIC: goes in every ?ref= link
    statusToken  String   @unique      // 24 random URL-safe chars. PRIVATE: the only key for the status page and erasure
    referredById String?
    referredBy   WaitlistSignup?  @relation("Referrals", fields: [referredById], references: [id], onDelete: SetNull)
    referrals    WaitlistSignup[] @relation("Referrals")
    source       String?
    consentAt    DateTime
    consentText  String
    isDemo       Boolean  @default(false)
    createdAt    DateTime @default(now())
  }
  ```
  Also a tiny `Setting` key/value table. Use it to remember whether the owner still uses the seeded password.
- **Place in line:** `joinIndex` = rank by `createdAt` (1-based). `score = joinIndex − spotsPerReferral × referralCount`, where `spotsPerReferral` is 5 and comes from the config file. Position = rank by score ascending, ties broken by `createdAt`. Compute it on the server for every response (fine for thousands of rows).
- **API** (Zod schemas in `backend/src/types.ts`; every route under `/api/`):
  - `POST /api/waitlist` with body `{ email, name?, consent: true, ref?, source?, company? }`.
    - `company` is a hidden honeypot: if it is filled, answer like success but save nothing.
    - Rate limit 8 requests per minute per IP, in memory.
    - Validate the email.
    - De-dupe on the normalised email. If it exists, return the existing record with `alreadyJoined: true`.
    - Ignore a `ref` that is unknown or belongs to the same email.
    - Store the consent time and `consentText`.
    - Do not store IP addresses.
    - Returns `{ code, token, position, total, referrals, alreadyJoined }`. `code` is the public referral code; `token` is the private status token.
    - If the request fails with a network error on the client, the client retries it automatically up to 2 times (after 1.5 s, then 3 s) before showing an error. That is safe because joins de-dupe on the normalised email.
  - `GET /api/waitlist/stats` returns `{ total }`.
  - `GET /api/waitlist/ref/:code` returns `{ valid }`, with no personal data.
  - `GET /api/waitlist/:token` (private status token only; a referral code here must 404) returns `{ position, total, referrals, spotsGained, joinedAt, maskedEmail }`, where maskedEmail looks like "m•••@example.com".
  - `DELETE /api/waitlist/:token` erases the record (GDPR right to erasure). It looks up by `statusToken` only, so nobody holding a shared invite link can delete someone's spot.
  - `GET /api/owner-hint` returns `{ show, email, password }`. `show` is true only while the seeded password is still in use; it turns false for good once the owner changes the password.
  - Admin, owner only:
    - `GET /api/admin/stats` returns `{ total, last7, viaReferralPct, topReferrer: { maskedEmail, count }, daily: [{ date, count }] × 30, demoCount }`.
    - `GET /api/admin/signups?search=&sort=line|newest|referrals&page=` returns 25 per page with position.
    - `GET /api/admin/signups.csv` returns ALL rows (never the status token). Columns: position, email, name, referral_code, referrals, referred_by_email, source, consent_at, joined_at, is_demo. Send it with `Content-Disposition: attachment; filename="halo-waitlist-YYYY-MM-DD.csv"`.
    - `DELETE /api/admin/signups/:id`.
    - `POST /api/admin/clear-demo`.
- Test every endpoint with cURL against $BACKEND_URL before wiring the UI.
- **Save before you notify:** the UI only shows success after the server has confirmed the write.

## 2. One config file

Create **`webapp/src/site.config.ts`**, typed and heavily commented. It holds every editable value:
- brand: name "Halo", tagline, description, company "Halo Labs, Inc.", url "https://halo.example", contactEmail "hello@halo.example", city "San Francisco", timeZone "America/Los_Angeles", twitter "@halo", social links;
- colours (below), fonts;
- nav links;
- ALL landing copy, section by section;
- features, pricing tiers (monthly and yearly prices), testimonials (each with `sample: true`), FAQ;
- store links `appStoreUrl: ""` and `playStoreUrl: ""` (empty means the badge shows "Coming soon");
- waitlist settings: `spotsPerReferral: 5`, share texts, consent text;
- legal placeholders: legalName "[COMPANY LEGAL NAME]", address "[REGISTERED ADDRESS]", jurisdiction "[GOVERNING LAW / COUNTRY]", effectiveDate "[EFFECTIVE DATE]", dpoEmail "[PRIVACY CONTACT EMAIL]";
- `showOwnerHint: true`;
- SEO defaults.

Rules for the config:
- Apply colours as CSS variables at startup. Tailwind tokens use `rgb(var(--accent) / <alpha-value>)` and so on.
- The backend reads `spotsPerReferral` from this same file through a relative import, so there is one source of truth. Make sure that import also works when the app is deployed.
- Never hard-code copy in components.
- Long-form content lives in `webapp/src/content/`: `posts/*.md`, `changelog.ts` and `legal/*.md`. Legal text renders config placeholders. Any value still in [BRACKETS] renders highlighted (accent-soft background, mono font) so the owner can't miss it.

## 3. Design system (follow exactly)

Mood: Venice/Poke editorial calm. That means warm paper, black ink, ONE warm accent, a huge serif, generous whitespace, and product UI shown on photographic backdrops like Turbo.

Banned:
- purple or blue gradients;
- glassmorphism or backdrop-blur panels;
- emoji used as icons;
- default shadcn look (grey cards with drop shadows everywhere);
- stock photos of real people;
- any Vibecode branding.

**Colours** (CSS variables, from config):
- paper #FBFAF7 (page)
- paper-2 #F3F1EC (subtle fills)
- white #FFFFFF (cards, inputs)
- ink #18181B (text, primary buttons)
- ink-2 #3F3F46 (body text)
- muted #71717A
- faint #A1A1AA
- hairline #E6E3DD (1px borders and dividers)
- comma #CFCBC4 (the Venice commas)
- **accent #F2641C ("Halo orange", the only accent)**
- accent-soft #FDEBDD
- night #0D0D0F (dark sections)
- night-2 #18181B
- night-line #2A2A2E
- night-text #F4F2EE
- error #B42318 (small text only)

**Fonts:** self-host with `@fontsource-variable/source-serif-4`, `@fontsource-variable/geist` and `@fontsource-variable/geist-mono`, imported in main.tsx. Do not load Google Fonts; that keeps visitors' IPs away from Google and is GDPR-friendly.
- Display = Source Serif 4, weight 400, optical size large (`font-variation-settings: "opsz" 60`), tight tracking.
- UI and body = Geist.
- Labels = Geist Mono.

| Style | Desktop | Phone (≤640px) |
|---|---|---|
| Hero H1 | Source Serif 4, 96px, line-height 0.98, letter-spacing −0.025em (80px at 1024px) | 48px, line-height 1.0 |
| Section H2 | 60px, line-height 1.04, letter-spacing −0.02em | 38px |
| Feature titles, quotes | 36–40px, line-height 1.12 | 28px |
| Venice list | 64px, line-height 1.1 | 36px |
| Body L | Geist 19px, line-height 1.55, ink-2 | 17px |
| Body | Geist 16px, line-height 1.6 | — |
| Small | Geist 14px, line-height 1.5 | — |
| Mono label | Geist Mono 12px, uppercase, letter-spacing 0.08em, muted | — |

Prices and positions use `tabular-nums`. Every H1, H2 and H3 uses `text-wrap: balance`, and body paragraphs use `text-wrap: pretty`, so no heading leaves one word alone on a line.

**Spacing:**
- Scale: 4, 8, 12, 16, 24, 32, 48, 64, 96, 128, 160.
- Section vertical padding: 160px on desktop, 96px on phone.
- Container: max-width 1240px, side padding 32px (20px on phone).

**Radius:** showcase cards 24px; app windows 14px; small cards and chips 10px; buttons, inputs and toggles fully pill (999px).

**Floating-UI shadow:** `0 1px 2px rgba(24,24,27,.06), 0 12px 32px -12px rgba(24,24,27,.22), 0 40px 80px -32px rgba(24,24,27,.28)` plus a 1px rgba(24,24,27,.08) border. Plain cards on paper get only the 1px hairline, no shadow.

**Buttons:**
- Primary: pill, ink bg, white Geist 15px/500, 52px tall in forms and 40px in the nav, 26px horizontal padding, arrow-right icon. Hover bg #27272A and the arrow "boops" 3px right with a spring. Press scale 0.97.
- Secondary: white bg, 1px hairline border, ink text.
- On night sections the primary is the accent bg with ink text.

**Links:** an underline that grows from the left on hover (200ms).

**Motion (Framer Motion, Josh Comeau-style springs):**
- UI springs: stiffness 380, damping 30.
- Showcase settle: stiffness 140, damping 18.
- Reveal on scroll: fade up 16px over 600ms with ease [0.22, 1, 0.36, 1], 60ms stagger, once only.
- Icons boop on hover: rotate or translate, then spring back.
- Honour `prefers-reduced-motion`: opacity only, no transforms, no count-ups.

**Image fallbacks:** every photo `<img>` (backdrops, blog covers, the chrome ring) hides itself on `onError`, so the card shows its paper-2 or night background instead of a broken-image icon. A failed avatar shows the person's initials in a circle.

**Accessibility:**
- Visible focus ring: 2px accent outline, 2px offset.
- Every input has a label (sr-only is fine).
- Accordion has `aria-expanded`; busy buttons have `aria-busy`.

**Logo:** the Halo mark is an accent ring (SVG circle, 2.5px stroke, 20px) followed by the wordmark "Halo" in Source Serif 4 at 24px. On hover the ring rotates 25° and springs back. The same ring is the favicon.

## 4. Landing page "/", top to bottom

One component per section in `src/components/landing/`. All copy comes from config. Section order:
1. Nav
2. Hero
3. Showcase
4. Logos
5. "What Halo handles"
6. Features ×3
7. Mobile app
8. How it works
9. Manifesto
10. Pricing
11. FAQ
12. the dark block (Testimonials → Final CTA)
13. Footer

### 4.1 Nav (sticky, 72px)
- Left: logo.
- Centre: Geist 14px ink-2 links, 32px apart: "Product" (#product), "How it works" (#how), "Pricing" (#pricing), "Changelog" (/changelog), "Blog" (/blog).
- Right: primary pill "Join the waitlist" (40px). It smooth-scrolls to the hero form and focuses the email input, or the success card if they already joined.
- After 8px of scroll: solid paper background at 96% opacity (no blur) and a hairline bottom border fades in.
- Phone (60px tall):
  - Logo, a small "Join" pill (34px), and a two-line menu button.
  - The menu button opens a full-screen paper sheet that slides down. It lists the links in Source Serif 36px with staggered fade-in, plus a close ×.

### 4.2 Hero (left-aligned, Venice style; top padding 152px)
1. **Announcement chip** linking to /changelog.
   - Chip: white, hairline border, pill, 32px tall.
   - Inner tag "New": Geist Mono 11px uppercase, ink bg, white text, pill.
   - Text: "Say it in plain English: natural-language scheduling is here", Geist 13.5px ink-2.
   - A chevron-right that boops on hover.
2. **H1**: "The calendar that" / "schedules itself" + a final period in accent. Max width 1000px. On load each word rises 24px and fades in, 45ms stagger.
3. **Sub** (Body L, max 600px, 28px below): "Halo plans your week around what matters, protects your focus time and moves meetings for you, always asking first. Join the waitlist for early access."
4. **WaitlistForm** (variant `light`, 40px below; one reusable component, used again in the final CTA):
   - Row:
     - email input: 360px wide, 52px tall, pill, white, hairline border, 22px left padding, placeholder "you@company.com". Focus: ink border plus a 4px accent ring at 18% opacity.
     - primary button "Join the waitlist" with arrow.
   - Hidden honeypot field `company`.
   - Consent row, 14px below:
     - a custom 16px checkbox, radius 4. Checked = accent fill with a white check that pops in with a spring.
     - text (13px muted): "Email me about Halo's launch. No spam, unsubscribe anytime. See our Privacy Policy." The Privacy Policy link goes to /privacy.
     - The box starts unticked and is required. If they submit unticked, the row shakes (3 frames, ±6px) and shows "Please tick the box so we can email you."
   - Live line, 18px below (mono 12px muted): a 6px accent dot that softly pulses + "213 PEOPLE ON THE WAITLIST". This is the real count from /api/waitlist/stats. Never invent numbers. When the total is 0, say "BE THE FIRST TO JOIN"; when it is 1, "1 PERSON ON THE WAITLIST".
   - **Referral chip:** if the URL has `?ref=CODE` and the code is valid, show above the form an accent-soft chip with the ring icon: "A friend invited you. Joining moves them up the list." Keep the ref in sessionStorage.
   - **source** = `utm_source`, else "referral" if a ref is set, else the referrer's hostname, else "direct".
   - Submitting: button shows a spinner + "Saving…", disabled, `aria-busy`.
   - Errors, inline under the row in error red 14px with an alert icon:
     - invalid email: "That email doesn't look quite right."
     - server or network: "We couldn't save your spot. Please try again."
   - Success: the form crossfades (AnimatePresence) into the **SuccessCard** (§5).
   - Remember `{ code, token }` in localStorage (the token is private and only used for "See your spot"). On return visits, show a compact "You're on the list: #214 · See your spot →" with a small "Not you? Use another email" link instead of the empty form.
5. **Store badges** (36px below), a reusable `StoreBadges` component:
   - Two black badges, 44px tall, radius 10, 1px #3F3F46 border. Build them as inline SVG/HTML:
     - App Store: Apple glyph + "Download on the" 10px / "App Store" 17px semibold.
     - Google Play: a four-colour play-triangle glyph + "GET IT ON" / "Google Play".
   - Links come from config. While a URL is empty, show a tiny mono "COMING SOON" caption under the badge and link to the waitlist form instead.

### 4.3 Showcase card (the money shot, 88px below the hero)
- Full container width × 720px, radius 24, overflow hidden, background photo `sky-morning.jpg` (cover).
- Centred inside: the **WeekView** app window, 1040×600 (spec in §6).
- Scroll-linked: the card scales 0.96 → 1 and the window rises 24px as it enters.
- **"Halo arranges your week" animation** (once, when 40% visible):
  - Every event starts scattered, with deterministic offsets per event: x ±20–80px, y ±30–120px, rotate ±2–8°, opacity 0.
  - Each springs into its slot (stiffness 140, damping 18, 35ms stagger).
  - The three "moved by Halo" events then flash an accent outline for 800ms.
  - Then the toast slides up.
  - Clicking "Undo" in the toast replays the whole animation.
- Phone: the card is 520px tall and shows the **DayView** (Tuesday as one column + toast, window title "Halo — Today") instead of the week. Never scale the week down.

### 4.4 Logos
- 120px top padding, centred.
- Serif caption, Source Serif 22px ink-2, max 520px: "People at these teams are already on the waitlist."
- Grid of 8 fictional wordmarks: 4×2 on desktop, 2×4 on phone, each about 140×48, colour faint, turning ink on hover (200ms). Build each as inline SVG with its own type treatment and a simple geometric mark:
  1. "Northwind": bold sans + three wind lines
  2. "fieldnote": lowercase serif italic
  3. "Parcel & Pine": small caps serif
  4. "LUMEN": wide-tracked caps + half circle
  5. "okto": heavy rounded sans + dot
  6. "Harbor/Co": mono
  7. "Juniper": medium sans + leaf
  8. "QUARRY": condensed bold
- Under the grid, mono 11px faint: "FICTIONAL COMPANIES · PLACEHOLDER LOGOS".

### 4.5 "What Halo handles" (the Venice list; 160px padding, left-aligned)
- Intro, Geist 17px ink-2: "Tell Halo what matters once. From then on it quietly takes care of your"
- The list, exactly like ref 01: Source Serif 64px ink, each item followed by a space and a comma in the comma colour.
  - Items: "Focus time , One-on-ones , Standups , Lunch , Travel , Time zones , Deadlines , The gym , Everything else".
- Hovering, focusing or tapping an item pops a small calendar chip just above it (scale 0.9 → 1 and rise 6px, spring).
  - Chip style: white, hairline border, radius 10, floating shadow, 8px 12px padding, Geist 13px, a 3px accent left bar.
  - The chip is clamped inside the viewport. For words on the first line of the list, show the chip BELOW the word when there is less than 64px between the word and the intro sentence, so it never covers the intro. Leave 16px more space than usual between the intro line and the list.
  - Chips:
    - Focus time → "Deep work · Tue 9:00–11:30 AM · Protected"
    - One-on-ones → "1:1 with Priya · moved to Thu 10:30 AM"
    - Standups → "Standup · 15 min, kept short"
    - Lunch → "Lunch · 12:30 PM every day, guarded"
    - Travel → "Flight SFO → JFK · 45 min buffer added"
    - Time zones → "Sync with Lisbon · 5:00 PM there, 9:00 AM here"
    - Deadlines → "Q4 plan due Fri · 3 work blocks booked"
    - The gym → "Gym · Mon, Wed, Fri at 5:00 PM"
    - Everything else → "This week · 14 changes, 0 surprises"
- Below, 48px down: a link "See how it works ↓" (to #how).

### 4.6 Features (#product)
Header:
- Mono label "WHAT IT DOES".
- H2 in two lines; the second line is in faint colour, like Poke's staggered headline: "A calendar with" / "a little judgment."

Then 3 rows separated by 1px hairlines, each with 96px vertical padding.
- Each row is 12 columns: text in 5, visual in 7. Sides alternate: visual right, left, right.
- **Text column:**
  - mono number "(1)" muted;
  - title in Source Serif 40px;
  - body (Body L, max 440px);
  - 3 detail lines (Geist 15px), each with a tiny accent ring bullet;
  - text link "Read how Halo decides →" to /blog/how-halo-picks-the-best-hour.
- **Visual column:**
  - A Turbo-style card, 640×480, radius 24, photo background, with a UI fragment floating in the middle.
  - Hover: the UI lifts 6px (spring) and the photo scales to 1.03 over 700ms.
  - On phone the card is full width with `height: auto`, `min-height: 360px` and 40px vertical padding, so it grows to fit its fragment. Scale each fragment by width only (ResizeObserver) to fit 340px. Nothing may ever be clipped; FindTimeCard must show all three options and its footer.

1. **Row 1, plaster-sun.jpg**
   - Title: "Finds the right hour, not just a free one."
   - Body: "Halo weighs everyone's focus time, time zones and meeting load, then suggests the slot that costs the least. One tap sends the invite."
   - Detail lines: "Ranks every open slot" · "Respects other people's focus time" · "Works across Google, Outlook and iCloud".
   - UI: **FindTimeCard**, 400px wide.
     - Header "Find a time" and a ×.
     - Field row: "Coffee chat with Priya Raman · 30 min".
     - Mono label "SUGGESTED".
     - Three radio rows:
       - "Thu, Nov 12 · 10:30 AM", with an accent pill "Best" and the sub-line "Both free · before Priya's focus block"
       - "Fri, Nov 13 · 2:00 PM", sub-line "Both free · after your design review"
       - "Mon, Nov 16 · 4:30 PM", sub-line "Free · late in Priya's day"
     - The first is selected: an accent ring with a springy dot. Clicking a row selects it.
     - Footer: primary pill "Send invite" and ghost "More times".
2. **Row 2, dusk-sea.jpg**
   - Title: "Your mornings stay yours."
   - Body: "Pick your focus hours once. Halo defends them, turns down conflicts politely and offers a better time, so nobody waits on a reply."
   - Detail lines: "Focus blocks that move, never vanish" · "Polite declines with a better time" · "Daily meeting limits".
   - UI: **FocusDayCard**, 420px wide.
     - A mini day column "Tuesday, Nov 10" covering 8 AM–1 PM:
       - "Deep work" 9:00–11:30: accent-soft fill, 3px accent left border, sub-line "Protected by Halo" with a shield icon.
       - "Standup" 11:30–11:45 and "Lunch" 12:30–1:15, both grey.
     - Overlapping on the right, a white request card:
       - initials avatar "MC";
       - "Marcus Chen wants "Quick sync" · Tue 10:00 AM";
       - a divider;
       - a ring-icon line "Halo suggested Tue 2:30 PM instead";
       - a chip "Marcus accepted ✓".
     - In view: the card slides in, then the suggestion types itself in (600ms), then the chip pops.
3. **Row 3, oak-desk.jpg**
   - Title: "Just say what you need."
   - Body: "Type a sentence the way you'd text an assistant. Halo shows every change before it makes one, and anything can be undone."
   - Detail lines: "Plain-English commands, ⌘K anywhere" · "A preview of every change" · "One-tap undo".
   - UI: **CommandCard**, 460px wide.
     - A command bar with the ring icon. The text types in when in view, with a blinking caret: "Move my 1:1s to Friday afternoon and keep lunch free".
     - Mono label "PREVIEW · 3 CHANGES", then three rows showing the old time struck through → the new time:
       - "1:1 with Priya": Wed 2:00 PM → Fri 1:30 PM
       - "1:1 with Dev": Thu 11:00 AM → Fri 2:15 PM
       - "1:1 with Sam": Thu 4:00 PM → Fri 3:00 PM
     - A check line: "Lunch 12:30–1:15 PM stays free".
     - Footer: primary pill "Apply 3 changes", ghost "Cancel" and a kbd chip "↵".

### 4.7 Mobile app (#app)
- Centred header:
  - mono "ON YOUR PHONE";
  - H2 "Halo goes where you go.";
  - sub "A calm Today view, one-tap approvals and your next meeting on the lock screen. On iPhone and Android.";
  - StoreBadges, centred.
- Below: a full-width showcase card, 640px tall, radius 24, `terracotta.jpg`. Two CSS phone frames float in it:
  - Frame: 300×620, radius 52, night bezel 10px, screen radius 42, paper screen, dynamic island 90×26, status bar "9:41" with SVG signal, wifi and battery.
  - The phones tilt −5° and +4° and drift ±6px on a 6s loop, out of phase.
- **Phone 1, "Today":**
  - Serif 32 "Tuesday" with "November 10" muted.
  - Accent-soft "Next up" card: "Design review · in 12 min · Room 4B".
  - Agenda rows: 9:00 Deep work (accent bar) · 11:30 Standup · 12:30 Lunch · 2:30 Quick sync with Marcus · 3:00 Design review · 5:00 Gym.
  - Bottom tab bar with lucide icons: Today, Week, Ask, Settings.
- **Phone 2, "Ask Halo":**
  - User bubble (ink): "Can you find 2 hours for the Q4 plan this week?"
  - Halo reply card with the ring icon: "Booked 3 blocks for the Q4 plan:" then rows "Wed 2:00–3:00 PM", "Thu 3:00–4:00 PM", "Fri 10:00–11:00 AM" and an "Undo" text button.
  - Input bar "Ask Halo…" with a mic icon.
- Phone: show only phone 1, centred, card 560px tall.

### 4.8 How it works (#how)
- Mono "HOW IT WORKS"; H2 "Three steps to a calmer week."
- Then 3 columns joined by one horizontal dotted line (1px dashed comma colour). On the line sit 12px ring nodes (hairline circles) above each column. This is the code.storage style, ref 10.
- Each column has:
  - a bordered mono tag like code.storage's labels: white, 1px ink border, 11px;
  - a serif title, 28px;
  - body text, 16px;
  - a mini UI.
- Steps:
  1. Tag "STEP 01 · CONNECT", title "Connect your calendars".
     - Body: "Google, Outlook and iCloud in two clicks. Halo reads free/busy and event details, never your email."
     - Mini UI: three rows with switches, "Google Calendar", "Outlook", "iCloud" (text only, no brand logos). The switches flip on one by one when in view.
  2. Tag "STEP 02 · TELL", title "Tell Halo what matters".
     - Body: "Focus hours, meeting limits, lunch, workouts. Write them in plain English."
     - Mini UI: pill tags "No meetings before 10 AM", "Lunch at 12:30", "Gym 3× a week", "Max 5 hours of meetings a day".
  3. Tag "STEP 03 · APPROVE", title "Approve your week".
     - Body: "Every Sunday evening Halo drafts next week. Approve it in one tap, or switch on autopilot."
     - Mini UI: a notification card with the ring icon, "Your week is ready · 14 changes", and a small ink "Review" pill.
- Phone: a vertical list with the dotted line running down the left through the nodes.

### 4.9 Manifesto (scroll reveal, ref 09)
- Container max 1100px.
- Mono "WHY WE BUILT HALO".
- Source Serif paragraph, 52px, line-height 1.18 (32px on phone): "Most calendars only record where your time went. Halo decides where it should go, asks before it moves a thing, and hands back the mornings you keep meaning to protect."
- Each word's colour interpolates from #D6D2CB to ink as the section scrolls through: `useScroll` with offset ["start 0.85", "end 0.45"], one word after another.
- Signature below: a 32px AI avatar of Lena Hart, then mono "LENA HART · CO-FOUNDER".

### 4.10 Pricing (#pricing)
**Header (centred):**
- mono "PRICING";
- H2 "Simple pricing, free during beta.";
- sub "Everyone on the waitlist gets three months of Pro free when we launch."

**Toggle:**
- A pill container: paper-2, hairline, 4px padding.
- Options "Monthly" and "Yearly". "Yearly" carries an accent mini tag "−20%".
- The active option is a white pill with a soft shadow that slides between them (Framer `layoutId` spring).

**Cards:** 3 columns, 16px gap, radius 24, 32px padding, min-height 560. Each card lifts 4px on hover.
- **Free** (white, hairline):
  - "Free" (Geist 15/600) · "For a calmer week."
  - "$0" in Source Serif 64 + "/month".
  - Secondary full-width pill "Join the waitlist".
  - Features with ink check icons: "1 connected calendar" · "Scheduling links" · "5 hours of protected focus a week" · "iPhone and Android apps".
- **Pro** (night-2 bg, night-text):
  - Accent pill "Most popular".
  - "Pro" · "For people who live in their calendar."
  - "$12" monthly / "$10" yearly. The digits roll vertically when the toggle changes (spring, tabular). Then "/month", with "Billed $120 yearly" or "Billed monthly" underneath.
  - White pill "Join the waitlist".
  - Accent checks: "Unlimited calendars" · "Unlimited focus time" · "Plain-English commands" · "Automatic rescheduling" · "Travel and time-zone smarts" · "Early access to new features".
- **Team** (white):
  - "Team" · "For teams that meet a lot."
  - "$20" monthly / "$16" yearly + "/user/month".
  - Secondary pill "Talk to us", a `mailto:hello@halo.example?subject=Halo%20for%20teams` link.
  - Features: "Everything in Pro" · "Shared team availability" · "Round-robin booking links" · "Meeting-load insights" · "SSO and admin controls".

**Behaviour:**
- Every "Join the waitlist" in pricing scrolls to the hero form and focuses it.
- Under the cards, centred 13px muted: "Prices in USD. Taxes may apply. Cancel anytime."
- Phone: the cards stack vertically.

### 4.11 FAQ (#faq)
**Layout:**
- Left 4 columns, sticky:
  - mono "FAQ";
  - H2 "Questions, answered.";
  - "Can't find yours? Write to hello@halo.example" (a mailto link).
- Right 8 columns: an accordion.
  - Question: Geist 19px/500, 24px vertical padding, hairline below each item.
  - A plus icon rotates 45° into × (spring).
  - The answer animates its height: Geist 16px, line-height 1.6, ink-2.
  - One item open at a time; the first is open by default.

**Questions and answers:**
1. "When do I get access?" — "We let people in every week, in waitlist order. Each friend who joins with your link moves you up five spots."
2. "Which calendars does Halo work with?" — "Google Calendar, Outlook and iCloud at launch. Halo can merge several calendars into one view."
3. "Does Halo read my email?" — "No. Halo only needs free/busy times and event details from the calendars you connect."
4. "Will Halo move meetings without asking?" — "Not by default. Halo proposes changes and shows a preview first. Autopilot is opt-in for each calendar, and every change can be undone."
5. "Is my data used to train AI models?" — "No. Your calendar data is only used to schedule for you, and you can export or delete it at any time."
6. "How much will Halo cost?" — "Halo is free during the beta. After launch, Free stays free and Pro is $12 a month, or $10 a month billed yearly."
7. "Is there an iPhone or Android app?" — "Both are on the way. Waitlist members get them first, and the store links will appear right here."
8. "How do I leave the waitlist?" — "Open your waitlist page from the link you got when you joined and tap "Remove me", or email us. We delete your details straight away."

Phone: a single column, and the heading is not sticky.

### 4.12 The dark block: Testimonials → Final CTA
**Block:** one full-bleed night section with 40px rounded top AND bottom corners (Off-Menu). It slides over the paper, with 16px side insets on desktop and 0 on phone.

**Testimonials (Venice, ref 03; centred column max 720px; 160px top padding):**
- Mono chip: 1px night-line border, pill, faint text, "SAMPLE QUOTES · FICTIONAL PEOPLE".
- H2 night-text: "Early access, early opinions."
- 3 quotes, stacked with 96px gaps. Each one has:
  - a fictional company wordmark in white at 20px;
  - the quote in Source Serif 30px, line-height 1.3, night-text, with curly quotes;
  - an author row: a 40px AI photo with radius 10, the name in Geist 14/500 white, the role in 14px faint.
  - Each reveals on scroll.
- Quotes:
  1. Northwind — "Sunday nights used to be calendar Tetris. Now Halo hands me a finished week and I just say yes." — Maya Lindqvist, Head of Product, Northwind
  2. fieldnote — "It turned down a 'quick sync' for me, offered a better time, and nobody even noticed. That's the dream." — Daniel Osei, Engineering Manager, Fieldnote
  3. Parcel & Pine — "Our team finally has afternoons without back-to-backs, and it always asks before it moves anything." — Sofia Marín, COO, Parcel & Pine
- End caption, mono 11px muted: "SAMPLE TESTIMONIALS FROM FICTIONAL PEOPLE. REPLACE BEFORE LAUNCH."

**Final CTA (centred, 160px padding):**
- The chrome ring `halo-ring.png` (transparent, 360px; 220px on phone).
  - It floats ±10px and rotates ±3° on a 6s loop.
  - It follows the mouse with 10px parallax.
- H2, Source Serif 88px (48px on phone), night-text: "Get your mornings back" + an accent period.
- Sub, 18px faint: "Halo opens to new people every week. Join now to hold your spot."
- WaitlistForm variant `dark`:
  - input: bg night-2, border #3F3F46, white text, placeholder muted;
  - button: accent bg with ink text;
  - consent text: faint;
  - live count line: faint.
- StoreBadges, centred.

### 4.13 Footer (paper; 96px top padding, 40px bottom)
**Columns:**
- Column 1 (5 cols):
  - logo;
  - tagline (muted): "The calendar that schedules itself.";
  - a Turbo-style live clock, mono 12px muted, ticking every second in config.timeZone, e.g. "SAN FRANCISCO · TUE, NOV 3 · 2:29:30 PM".
- "Product": Features, Pricing, Changelog, Mobile app.
- "Company": Blog, Contact (mailto), Owner admin (/admin).
- "Legal": Privacy Policy, Terms of Service.

**Bottom row (above a hairline):**
- Left: "© 2026 Halo Labs, Inc. Halo is a fictional product made for this template."
- Right: X and LinkedIn as simple SVG glyphs, linked from config.

### 4.14 First-run owner hint
- A fixed pill, bottom-left with 16px insets: ink bg, white 13px text, 10px 14px padding, radius 14, floating shadow, ring icon.
- Text: "Owner admin → /admin · owner@halo.example · halo-launch-2026", plus an "Open" link and a close ×.
- Show it only while `config.showOwnerHint` is true AND `/api/owner-hint` says `show` (so it disappears for good once the owner changes the password), and until it is dismissed (localStorage).
- Hide it with a 200ms fade while any WaitlistForm or SuccessCard is at least 20% in the viewport (IntersectionObserver), on every screen size, so it never covers a form or its consent checkbox.
- Phone (≤640px): full width with 12px insets, one line "Owner admin → /admin" plus a small "Show login" text button that expands to the credentials, and a 44px close target.

## 5. Waitlist success and status page

**SuccessCard**, replacing the form inline:
- Mono "YOU'RE ON THE LIST".
- The serif number "#214" at 72px counts up from #1 over 900ms, ease-out. 3–4 tiny accent four-point sparkles twinkle once around it (Josh Comeau sparkles; no confetti).
- Then: "of 214 people waiting. Each friend who joins with your link moves you up 5 spots."
- Referral link: a read-only pill field showing the link without "https://", e.g. `halo.example/?ref=K7M2QX` (copy and share the full URL), + a "Copy link" button that turns into "Copied ✓" for 2s.
- **ShareRow:** outline pills, 36px, with simple SVG icons. All texts come from config.
  - "Post on X" → `https://twitter.com/intent/tweet?text=…&url=…`
  - "WhatsApp" → `https://wa.me/?text=…`
  - "Text" → `sms:?&body=…`
  - "Email" → `mailto:?subject=…&body=…`
- Link "See your spot anytime →" to /waitlist/:token (the private token).
- De-duped email: the heading becomes "You're already on the list", showing the current spot.

**/waitlist/:token**, centred, max 640px (a referral code in this URL shows the "couldn't find" state):
- Mono "YOUR SPOT".
- Serif "#209" at 120px, counting up.
- "of 214 people on the Halo waitlist".
- A progress track: paper-2, with an ink fill of (total − position) / total. Left label "Joined Nov 3, 2026", right label "Front of the line".
- Two stats: "Friends joined: 1" and "Spots gained: 5".
- The referral link and the ShareRow.
- "How it works", 3 lines:
  - "We invite people in waitlist order every week."
  - "Each friend who joins with your link moves you up 5 spots."
  - "We'll email m•••@example.com when it's your turn."
- A small text button "Remove me from the waitlist". It opens a confirm dialog:
  - Title: "Remove your spot?"
  - Body: "This deletes your email and referral data. It can't be undone."
  - Buttons: Cancel / "Remove me".
  - After removal: "You're off the list. We've deleted your details." + a link home. Clear localStorage and remove the query from the React Query cache instead of refetching it, so no 404 is logged.
- Unknown code: "We couldn't find that spot." + "Join the waitlist" (home).
- Loading: a skeleton.

## 6. Product UI mockups (real HTML, fixtures in `src/components/mockups/data.ts`)

**WeekView window** (1040×600, white, radius 14, floating shadow):
- Top bar, 40px: three neutral dots (#E4E2DD, 10px) and the muted centred text "Halo — Week".
- **Sidebar** (216px, #FAF9F6, hairline on the right):
  - logo;
  - mini month "November 2026": S M T W T F S headers, days 1–30, the 10th selected (ink circle), today (the 3rd) marked with an accent dot;
  - mono "CALENDARS": Work (ink swatch), Personal (faint swatch), Focus (accent swatch);
  - mono "PRIORITIES": "Deep work · 10 h/wk" with a 70% accent progress bar, "Lunch · daily ✓", "Gym · 2 of 3".
- **Main header**, 56px:
  - "November 9 – 13" in serif 22 + "2026" muted;
  - right side: a segmented Day | Week | Month control with Week active, and an "Ask Halo ⌘K" pill (hairline, ring icon).
- Day headers: mono "MON 9 / TUE 10 / WED 11 / THU 12 / FRI 13". TUE is highlighted with an ink pill on its date.
- Hour gutter 52px, "8 AM"–"6 PM", 44px per hour.
- **Events:** absolutely positioned by time, radius 8, 6px 8px padding, title 12.5px/600, time 11px muted.
  - Meeting: #F1EFEA bg, 3px #A1A1AA left border.
  - Focus: accent-soft bg, 3px accent left border.
  - Personal: white bg, 1px dashed #D4D0C8.
  - Moved by Halo: a tiny ring icon in the top-right corner.
- **Fixtures:**
  - **MON:** Deep work 9:00–11:00 (focus) · Standup 11:30–11:45 · Lunch 12:30–1:15 (personal) · Hiring sync 2:00–2:45 · Design crit 3:30–4:30 · Gym 5:00–6:00 (personal).
  - **TUE:** Deep work 9:00–11:30 (focus, "Protected") · Standup 11:30–11:45 · Lunch 12:30–1:15 · Quick sync with Marcus 2:30–3:00 (moved) · Design review 3:00–4:00.
  - **WED:** Standup 9:30–9:45 · Roadmap 10:00–11:30 · Lunch 12:30–1:15 · Focus: Q4 plan 2:00–4:00 (focus) · Gym 5:00–6:00.
  - **THU:** Deep work 9:00–10:30 (focus) · Coffee with Priya 10:30–11:00 (moved) · Standup 11:30–11:45 · Lunch 12:30–1:15 · Customer call: Juniper 1:30–2:15.
  - **FRI:** Deep work 9:00–11:00 (focus) · Lunch 12:30–1:15 · 1:1 with Priya 1:30–2:00 (moved) · 1:1 with Dev 2:15–2:45 (moved) · 1:1 with Sam 3:00–3:30 (moved) · Gym 5:00–6:00.
- A now-line across Tuesday at 10:12 AM: a 1px accent line with a 6px dot.
- **Toast**, bottom-right of main, 360px: white, radius 12, floating shadow, ring icon.
  - Text: "Halo moved 3 meetings to keep your mornings free."
  - Buttons: "Undo" (ghost) and "Review" (small ink pill).

**DayView** (phone): Tuesday's events in one column with the same styles, plus the toast.

## 7. Other pages

- **/blog**:
  - H1 serif "Notes from the Halo team" and the intro "How we think about time, focus and building a calendar with judgment."
  - Two post cards:
    - cover 16:10, radius 16, image scales 1.03 on hover;
    - mono "NOV 3, 2026 · 6 MIN READ";
    - serif title 32px;
    - excerpt;
    - author avatar + name.
- **/blog/:slug**:
  - "← All posts".
  - Mono date line.
  - Serif H1 56px (36px on phone).
  - Author row.
  - Cover.
  - Body in Source Serif 20px, line-height 1.7, max 680px. Serif h2 at 30px. A pull quote with a 3px accent left border. A callout box. One small inline HTML figure, a table or a chip.
  - At the end: an inline waitlist CTA card, then a next-post link.
  - Markdown files in src/content/posts/, rendered with react-markdown.
- **Posts** (write each one in full, 600–800 words, in a warm, plain, specific voice):
  1. `why-we-built-a-calendar-that-says-no`
     - "Why we built a calendar that says no" · November 3, 2026 · Lena Hart, Co-founder & CEO · 6 min read.
     - Sections: "The tyranny of the empty slot", "A calendar with opinions", "Asking before acting", "What's next".
  2. `how-halo-picks-the-best-hour`
     - "How Halo picks the best hour" · October 21, 2026 · Tomás Reyes, Engineering · 8 min read.
     - Sections: "Free isn't the same as good", "Scoring every candidate slot" (include a small table: slot, cost, why), "Time zones are a people problem", "What we got wrong in the alpha".
- **/changelog**:
  - H1 serif "Changelog" and "New updates and improvements to Halo."
  - Entries: a sticky mono date + version tag on the left; on the right a serif title, a paragraph, and bullets with mono tags NEW / IMPROVED / FIXED.
  - Entries:
    - Nov 3, 2026 · v0.9: "Say it in plain English" (natural-language commands, preview before apply, one-tap undo)
    - Oct 20, 2026 · v0.8: "Focus time 2.0" (blocks that move instead of vanishing, polite declines)
    - Oct 6, 2026 · v0.7: "Team availability" (shared free/busy, round-robin links)
    - Sep 22, 2026 · v0.6: "Travel and time zones" (flight buffers, home time-zone overlay)
    - Sep 8, 2026 · v0.5: "The waitlist opens"
- **/privacy** and **/terms**: sensible, complete templates.
  - Page layout:
    - serif H1;
    - mono "LAST UPDATED: [EFFECTIVE DATE]";
    - a hairline notice box: "Template notice: this is a starting point, not legal advice. Replace every highlighted [PLACEHOLDER] and have it reviewed.";
    - a sticky table of contents on the left on desktop;
    - body in Geist 16px, line-height 1.7.
  - **Privacy sections:**
    1. Who we are
    2. What we collect (waitlist email, optional name, referral code, consent time, sign-up source; no IP addresses)
    3. How we use it
    4. Legal basis (consent, GDPR Art. 6(1)(a))
    5. Sharing and processors ([HOSTING PROVIDER], [EMAIL PROVIDER])
    6. International transfers
    7. Retention
    8. Your rights (access, rectification, erasure via "Remove me", portability, objection, withdrawing consent, complaint to a supervisory authority)
    9. Cookies and local storage (only essential local storage to remember your spot; no tracking cookies)
    10. Children
    11. Changes
    12. Contact
  - **Terms sections:** Acceptance; The waitlist and beta; Accounts; Acceptable use; Plans and billing (placeholder); Intellectual property; Feedback; Disclaimers; Limitation of liability; Termination; Governing law ([GOVERNING LAW / COUNTRY]); Changes; Contact.
- **404:** serif "This page took the day off." and a link "Back to Halo".

## 8. Owner admin (/admin)

**Signed out:** a centred login card on paper. No request to `/api/admin/*` may fire before a session exists: check the session first and gate the queries on it, so the console stays clean.
- Logo and serif "Owner sign in".
- Email and password pill inputs, and a primary "Sign in" button.
- Below it, a hairline note box with the mono label "FIRST RUN":
  - "Demo owner login. Change the password in Settings after you sign in." Then two mono rows, "EMAIL owner@halo.example" and "PASSWORD halo-launch-2026", each `whitespace-nowrap` with a tiny copy icon button.
  - A "Fill in" button that fills both fields.
  - Show the box only while /api/owner-hint says `show`.
- Wrong login: "Wrong email or password."

**Signed in:**
- Top bar: logo, mono tag "ADMIN", tabs "Signups" / "Settings", "View site ↗", "Sign out".

**Signups tab:**
- Header:
  - serif 40px "Waitlist" + "Everyone who joined, in line order.";
  - right side: primary pill "Export CSV" with a download icon. It downloads every row, not just the current page.
- Demo banner (while demo rows exist):
  - accent-soft bg: "213 of these signups are seeded demo data.";
  - button "Clear demo signups" → confirm dialog → POST clear-demo.
- 4 stat cards: hairline border, serif 40px numbers, mono labels. All computed from the DB.
  - "TOTAL SIGNUPS"
  - "LAST 7 DAYS"
  - "VIA REFERRAL" (%)
  - "TOP REFERRER" (masked email + count)
- Bar chart "Signups per day · last 30 days":
  - plain div bars in ink, today's bar in accent;
  - a hover tooltip "Oct 20 · 31 signups";
  - date ticks every 7 days.
- Table toolbar:
  - search "Search email or name" (debounced);
  - sort select: "Line order" / "Newest first" / "Most referrals".
- Table columns: "#", "Email" (+ a mono DEMO tag on demo rows), "Name", "Referrals", "Referred by", "Source", "Joined", and a delete icon.
  - Joined dates are human-readable, e.g. "Nov 3, 2026, 2:14 PM".
  - Delete opens a confirm dialog, then removes the row.
- Pagination: "Showing 1–25 of 213", Prev / Next.
- States:
  - Empty: serif "No signups yet." + "Share your page to get your first one." + a "Copy page link" button.
  - Loading: skeleton rows.
  - Error: "Couldn't load signups." + Retry.

**Settings tab:**
- "Signed in as owner@halo.example".
- Change password: current, new, confirm (Better Auth changePassword with `revokeOtherSessions: true`, so every other session is signed out; the current one stays).
- Success shows a toast "Password updated" and marks the seeded password as retired, so the hint disappears.

**Phone:** stat cards 2×2, the table turns into a list of row cards (the DEMO tag is `whitespace-nowrap shrink-0` and long emails truncate with an ellipsis), and the chart scrolls horizontally inside its card.

## 9. SEO, OG image, favicon

- index.html meta comes from site.config.ts at build time (a small Vite `transformIndexHtml` plugin):
  - title "Halo — The calendar that schedules itself";
  - meta description;
  - canonical;
  - og:title, og:description, og:type, og:url, og:image "/og.png" (1200×630);
  - twitter:card summary_large_image, plus twitter title, description and image;
  - twitter:site from config, i.e. remove @vibecodeapp;
  - theme-color #FBFAF7;
  - JSON-LD SoftwareApplication with Offers.
- A `<Seo>` component sets the title, description and og:url for each route:
  - "Blog — Halo", "<post title> — Halo", "Changelog — Halo", "Privacy Policy — Halo", "Terms of Service — Halo";
  - /admin and /waitlist/* get noindex.
- `public/robots.txt` disallows /admin and /waitlist/. Add `public/sitemap.xml` built from the config url.
- **OG image:** write `webapp/scripts/make-og.ts` using `satori` + `@resvg/resvg-js`.
  - Fonts: download static .woff files from the Fontsource CDN into `webapp/scripts/fonts/`, e.g. https://cdn.jsdelivr.net/fontsource/fonts/source-serif-4@latest/latin-400-normal.woff, …/geist@latest/latin-500-normal.woff and …/geist-mono@latest/latin-400-normal.woff.
  - Design (read from config):
    - paper bg;
    - top-left: ring mark + "Halo";
    - a big serif "The calendar that schedules itself" with an accent period;
    - bottom-left: mono "JOIN THE WAITLIST · HALO.EXAMPLE";
    - right side: an abstract week grid of rounded rects in accent-soft and #F1EFEA.
  - Run it, then open the PNG and check it.
- **Favicon:**
  - `public/favicon.svg`: the accent ring;
  - `favicon.ico` (16/32, via png-to-ico from resvg renders);
  - `apple-touch-icon.png` (180, the ring on paper).
  - **Delete the template's Vibecode `favicon.ico` and `og-base.png`.** No Vibecode branding anywhere.

## 10. Images to generate

- Write `webapp/scripts/gen-images.ts`. Run `env` first, then call `https://api.openai.com/v1/images/generations` with the project's proxied `OPENAI_API_KEY` and model **`gpt-image-2.5-flare`**.
- Save into `webapp/public/images/` as optimised JPG (PNG for the ring), about 200–400 KB each.
- Run the calls in parallel and retry a failure once. Never use stock photos; all people are AI-generated.
- **Landscape, 1536×1024:**
  1. `sky-morning.jpg`: "Soft early-morning sky with a few drifting cumulus clouds, pale powder blue at the top fading to warm peach near the bottom, medium-format film photo, gentle grain, calm and airy, lots of empty space, no sun disk, no birds, no text."
  2. `plaster-sun.jpg`: "A warm off-white lime-plaster wall in late-afternoon sun with the soft diagonal shadow of a window frame and a palm frond across it, minimalist editorial product-photography backdrop, cream and apricot tones, no objects, no text."
  3. `dusk-sea.jpg`: "Very soft-focus photograph of a calm sea horizon at dusk, apricot and dusty-rose sky fading into a muted blue-grey sea, dreamy, film grain, extremely minimal, no boats, no sun disk, no text."
  4. `oak-desk.jpg`: "Top-down photograph of a pale oak desk in soft morning window light with long gentle shadows, almost empty, one small cream ceramic cup at the far top-right edge, muted warm tones, editorial still life, no devices, no paper, no text."
  5. `terracotta.jpg`: "A sunlit terracotta-pink stucco wall with the curved shadow of an archway and the shadow of a small olive tree, Mediterranean afternoon light, warm and calm editorial backdrop, nothing in frame, no text."
- **Transparent PNG, 1024×1024:**
  6. `halo-ring.png` (`background: "transparent"`): "A single glossy polished-chrome torus ring floating at a slight three-quarter angle, liquid-metal reflections with warm orange sunrise highlights and soft white studio light, perfectly smooth, hyper-real 3D render, isolated, no shadow, no text."
- **Portraits, 1024×1024**, each: "Editorial portrait photograph of <person>, soft window light, plain warm-grey background, 35mm film look, natural skin texture, relaxed expression, shoulders up". People:
  7. `maya.jpg`: a Scandinavian woman in her mid-30s with a short dark bob, cream knit sweater
  8. `daniel.jpg`: a Ghanaian man in his early 40s with close-cropped hair, navy overshirt
  9. `sofia.jpg`: a Latina woman in her late 20s with long wavy brown hair, white linen shirt
  10. `lena.jpg`: a woman in her early 40s with shoulder-length blonde hair, black turtleneck
  11. `tomas.jpg`: a man in his early 30s with curly black hair and round glasses, grey t-shirt
- **Blog covers, 1536×1024:**
  12. `post-says-no.jpg`: "Still life of a paper desk calendar page on warm linen with one day circled by a hand-drawn orange ring, soft daylight, editorial and minimal, no legible words or numbers."
  13. `post-best-hour.jpg`: "Abstract macro photograph of overlapping translucent white and apricot-orange glass discs casting soft coloured shadows on a pale surface, editorial, minimal."

## 11. Phone layout (390px) — check every section

- No horizontal scrolling anywhere. Container padding is 20px.
- Hero:
  - 112px top padding; the chip wraps cleanly;
  - H1 48px;
  - the form stacks: input and button each full width and 52px tall, 10px gap;
  - the badges sit side by side.
- Showcase: DayView.
- Logos: 2 columns. Venice list: 36px.
- Features: text first, then the card.
- Mobile app: one phone.
- How it works: vertical.
- Manifesto: 32px.
- Pricing: cards stacked.
- FAQ: one column.
- Dark block: 0 insets, 24px corners.
- Footer: a 2-column link grid.
- The owner hint must not cover the hero button on load. Place it at the bottom; it is fine below the fold.
- Every tap target is at least 44px.

## 12. README.md

Document:
- every field in site.config.ts;
- how to change the name, colours, fonts and copy;
- where blog, changelog and legal content live;
- the owner login and how to change it;
- how positions and referrals work (spotsPerReferral);
- the CSV export;
- how to clear demo data;
- how to set the store links;
- how to regenerate the OG image, favicon and images (`bun run scripts/make-og.ts` etc.);
- the private status token versus the public referral code;
- a "Before you launch" checklist: change the owner password in /admin → Settings (this signs out every other session); put a new random `BETTER_AUTH_SECRET` in backend/.env (give the exact one-line command); set `url`, the store links and the legal placeholders in site.config.ts; set `showOwnerHint: false`; click "Clear demo signups".

## 13. Build order (small files, under ~200 lines each)

1. Config, theme and fonts: site.config.ts, CSS variables, Tailwind tokens, self-hosted fonts.
2. Backend:
   - Prisma schema and migrate;
   - auth with sign-up disabled;
   - seed-on-boot;
   - waitlist routes, then admin routes;
   - cURL-test them all.
3. UI primitives: Button, Input, Checkbox, MonoLabel, Logo, RingIcon, StoreBadges, ShareRow, Reveal.
4. Landing sections, one file each, in page order. Mockups go in `src/components/mockups/` (WeekView, DayView, FindTimeCard, FocusDayCard, CommandCard, PhoneFrame, TodayScreen, AskScreen).
5. Waitlist status page, then the admin pages.
6. Blog, changelog, legal and 404.
7. gen-images.ts, then run it. make-og.ts and the favicons, then run them.
8. SEO component, the index.html plugin, robots.txt and sitemap.xml.
9. README.

Get a first full version of the landing page running before polishing.

## 14. Before you finish, test it yourself in the browser

1. Join with a new email: it shows #214 and the count updates.
2. Join again with the same email, in different case or with a +tag: "You're already on the list".
3. Open `/?ref=<that code>` and join with another email: the first person moves up 5 spots on their /waitlist/:token page.
4. Copy link works.
5. Remove me works.
6. /admin:
   - log in with the seeded owner;
   - search, sort and paginate;
   - CSV downloads every row;
   - deleting a row works;
   - Clear demo works;
   - changing the password hides the hint.
7. Pricing toggle, FAQ, mobile menu and all routes work.
8. The console has zero errors.
9. 390px has no overflow.
10. The OG image and favicon are Halo's.
11. A referral code can't open or delete a spot (404); the owner hint hides over forms.

Last step, after all testing: delete your test signups and scratch files. The database must hold exactly the 213 demo signups, the owner on the seeded password (`owner.seededPassword = active`, `demo.seeded = yes`) and zero rows in Session and Verification, so no login tokens ship in the template. Then run `PRAGMA wal_checkpoint(TRUNCATE);` and `VACUUM;` on backend/prisma/dev.db, and do not sign in again afterwards (a sign-in writes a Session row into the database that ships).
All templates