================ ONE-SHOT BUILD CONTRACT (read first) ================
Build this in Google AI Studio "Build" in ONE shot — a complete, working app,
no follow-up turns. These are hard rules, not suggestions:
1. TARGET = Full-Stack Web (Node server runtime, secrets, Firebase allowed).
If you target Native Android instead, you MUST drop all server/DB/Workspace/
secrets and persist locally (Room / SharedPreferences) only.
2. PIN EVERY MODEL CALL — never let the agent auto-select (it downgrades on quota):
- Reasoning / text -> gemini-3.5-flash (thinkingLevel: minimal|low|medium|high)
- 4K image + legible text -> gemini-3-pro-image (image_size "4K", up to 14 refs)
- High-volume image -> gemini-3.1-flash-image
- Expressive TTS -> gemini-3.1-flash-tts-preview (inline tags e.g. [whispers])
- Realtime audio/video (WebSocket) -> gemini-3.1-flash-live-preview
- Sandboxed agent -> antigravity-preview-05-2026
3. DIVISION OF LABOR — the model ONLY parses/extracts to a strict responseSchema.
ALL math, money (store currency as integer minor units / cents), sorting,
balancing and graph logic run in deterministic TypeScript/Python. The model
must never compute totals, splits or balances itself.
4. responseSchema sanitation — no regex patterns, no fixed-length tuples, no
format validators in the schema (they crash the OpenAPI engine). Enforce those
in server-side code AFTER parsing the JSON.
5. responseSchema and google_search grounding are MUTUALLY EXCLUSIVE in one call.
6. CODEGEN — split large output into modular, single-responsibility files so no
file is truncated by the output-token cap.
7. Every external call gets a graceful fallback (e.g. manual paste if a Workspace
read fails). Never a silent dead end.
8. ROBUST STORAGE & CANVAS — Wrap all `localStorage`/`sessionStorage` operations (especially JSON parsing and writes) in `try-catch` blocks to prevent crashes in private windows or quota overflows. Canvas drawing elements must dynamically handle window resize and scale pixel density (`window.devicePixelRatio`) to avoid blurry graphics on retina displays.
=====================================================================
# MUST OBEY — Mobile-first build requirements
This app's PRIMARY surface is a mobile phone. Build it impeccably on mobile FIRST, then verify on tablet and desktop. Treat the rules below as non-negotiable hard constraints, not suggestions.
## Viewports to verify (every screen, every state)
- 320 px, 360 px, 375 px, 390 px, 414 px, 480 px
- 768 px, 834 px (iPad portrait / Pro 11)
- 1024 px, 1280 px, 1440 px, 1920 px, 2560 px
- Plus: 200% browser zoom, landscape orientation on every mobile width, iPhone with safe-area insets visible
## Hard layout rules
- Mobile-first CSS. Default styles target mobile; `@media (min-width: ...)` for larger viewports.
- Use `dvh` and `svh` instead of `vh` for full-height surfaces (iOS Safari URL-bar bug).
- Use `clamp()` for fluid typography across all viewports.
- Prefer container queries (`@container`) over media queries for component-level responsiveness.
- Use `min(100%, ...)` widths so content never overflows. Zero horizontal overflow at any viewport.
- Add `` to every page.
- Apply `padding: max(safe-area-inset-X, fallback)` on every edge-bleeding container so notched iPhones in landscape never clip content.
- Wide tables and code blocks scroll INSIDE their container (`overflow-x: auto`), never push the body.
- Use `background-attachment: scroll` on mobile, not `fixed` (iOS Safari repaint bug).
- Avoid `backdrop-filter` on animated elements. Use it sparingly on static surfaces only.
- **Canvas Scaling**: Canvases must dynamically scale with window resize events and properly handle high-DPI screens (`window.devicePixelRatio`). Set physical dimensions (`canvas.width`/`canvas.height`) using pixel ratio and render relative to this grid, using CSS to control responsive viewport scaling.
- **Robust Storage**: Every access to `localStorage`/`sessionStorage` (especially `JSON.parse` of loaded state or writes) MUST be wrapped in a `try-catch` block to handle disabled storage, private browsing mode, quota limits, or corrupted JSON gracefully. Fall back to a robust in-memory object store.
## Touch & accessibility
- Tap targets ≥ 44 × 44 px on touch (Apple HIG). Increase to 48 px under `@media (hover: none) and (pointer: coarse)`.
- All interactive controls reachable by keyboard with a visible focus ring; respect `:focus-visible`.
- Color contrast ≥ 4.5:1 for body text, 3:1 for UI components.
- All images have meaningful `alt`. Decorative images use `alt=""`.
- Respect `prefers-reduced-motion: reduce` — zero animation durations under that query.
- Forms validate inline; error messages are specific, not "Invalid input".
- Modals: focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, focus restored on close.
## Performance bar (Lighthouse mobile, throttled 3G/4G)
- LCP < 2.5 s · INP < 200 ms · CLS < 0.1
- JS bundle gzip < 200 KB mobile-first; lazy-load non-critical screens via `React.lazy` / dynamic imports.
- No render-blocking resources above the fold.
- Images: WebP/AVIF preferred, `loading="lazy"`, explicit `width`/`height` attributes (zero CLS), `srcset` for retina.
- Videos: `preload="metadata"`, low-resolution poster, max 720p mobile fallback. Never autoplay with audio.
- Fonts: `font-display: swap`; preload only the one used above the fold.
- Smooth scroll honoured via CSS `scroll-behavior: smooth` with reduced-motion fallback.
## Pre-ship mobile checklist (the deployer MUST verify before declaring done)
1. Open at 375 px in DevTools — every screen scrolls vertically only; zero horizontal scroll.
2. Browser zoom 200% — layout reflows without overlap.
3. iPhone Safari with the URL bar visible AND landscape — no content under the home indicator; no notch clipping.
4. iPad portrait (768 px) and landscape (1024 px) — no awkward gaps; tablet-specific breakpoints land cleanly.
5. Tap every interactive element with a thumb at real-device size — every target is easy to hit.
6. `prefers-reduced-motion: reduce` — every transition / animation skips cleanly, scroll-behavior becomes instant.
7. Lighthouse mobile score ≥ 90 across all 4 categories.
8. Zero `console.error` and zero CLS shift in real-device testing on a mid-tier Android (e.g. Pixel 6a) and an iPhone SE.
---
The original template starts below. All rules above apply on TOP of whatever this template specifies.
---
# Stoop
## 1. Project
**Stoop** is a block-party organiser for the one neighbour on each
street who, every summer, ends up running the thing. The user
photographs last year's flyer — the creased A4 taped to a lamppost,
the laminated card pinned inside the corner shop, the WhatsApp PDF
forwarded around in May — and the app produces a fresh flyer with
this year's date, a shareable RSVP page neighbours can sign without
an app, a printable door-knocker checklist, and a simple permit-and-
insurance summary the block captain can read in two minutes. Every
piece of paper is generated from the same source: one photograph and
a handful of confirmations.
This is the kind of app the woman three doors down builds on a Sunday
night because Mrs. Adeyemi from #14 stopped her at the post box and
said *"are we doing it again this year?"* and she suddenly remembered
the party is in six weeks and the only copy of last year's flyer is a
folded printout in a kitchen drawer with the wrong date on it. It is
also the kind of app the volunteer block captain on a tower-block
estate builds the week after a difficult winter, when the residents'
association decided that what the building needs in July is one
afternoon of trestle tables in the courtyard, and she has been handed
the job of inviting four hundred and twelve flats without printing
four hundred and twelve flyers. Same shape of moment, different
street, different stakes.
The single demo that proves the magic: photograph one tatty flyer
from last year → in under 30 seconds the user sees a fresh PDF on
the left of the screen with this year's date corrected and the
weather-permitting rain date filled in, and a one-line shareable
RSVP link on the right that opens to a clean page asking only the
questions the captain wants to ask. The captain texts the link into
the building's WhatsApp group; a neighbour two floors up taps the
link, signs without creating an account, ticks "bringing a salad",
and the captain sees the count tick to one. The original flyer's
design — the green-and-yellow border, the bunting illustration in
the upper-right corner, the typewriter-style title — is preserved.
Only the date, the rain-date, the QR code, and the small "RSVP at"
line have changed.
In the harder cases — neighbours without smartphones, multilingual
streets, blocks where half the residents are renters who turn over
every twelve months — the app generates a paper RSVP sheet to slip
under doors (with a freepost-style return address that's just the
captain's own letterbox), a multilingual flyer set (the Crown
Heights block runs the same flyer in English, Spanish, Haitian
Creole, and Yiddish), and a "new neighbour" line that quietly
welcomes anyone who moved in since last year without asking them to
prove it.
**Tagline:** _Photograph last year's flyer. Get this year's flyer and an RSVP page neighbours can sign without an app — in any street, any language, with the bunting in the same corner._
## 2. Target audience
- Block captains, stoop organisers, residents'-association volunteers, and "the neighbour who always does it" — the person whose phone number every other neighbour has saved
- Tower-block and apartment-building organisers running courtyard parties, hallway potlucks, or rooftop summers in buildings of 50–500 units
- Newly-arrived block captains who inherited the job from a neighbour who moved away and are working from one folded flyer they found in a drawer
- Faith-community organisers running parish picnics, mosque iftars in the courtyard, temple langar in the car park, and church coffee-after-service sign-ups
- Volunteer school-parents organising the end-of-year street tea, the Diwali rangoli on the cul-de-sac, the Lunar New Year courtyard lunch in the housing estate
- Cooperativa de vecinos in Latin American neighbourhoods running the annual posada or fiesta patronal; sociedad de fomento organisers in Buenos Aires barrios; community-builder volunteers in Mexico City vecindades
- Multilingual-street organisers in Brooklyn, East London, Lagos, Manila, Mumbai, Jakarta — anywhere four languages share one block
- Mutual-aid coordinators who started during the 2020 lockdowns and stayed organised; PTA volunteers; allotment-society secretaries
- Mosque-block aunties, parish-hall uncles, building superintendents who do this off the side of their desk — anyone whose unofficial job description includes "remembers the date last year, can find the gazebo"
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **Last year's flyer is the source material** — not a blank template. Gemini 3.5 Flash reads the photographed flyer end-to-end: the title, the date, the time, the address, the rain-date, the small print, the list of last year's volunteers in 6-point type at the bottom, the bunting illustration in the corner, the typewriter-style font. The new flyer keeps every visual choice the previous captain made and only updates what needs updating.
- **The visual design carries over** — Nano Banana 2 regenerates the flyer layout at the new date with the same illustration, the same border, the same colour palette, the same hand-drawn feel. The previous captain's style is preserved across handovers; a new captain can inherit a block's visual identity without redesigning from scratch.
- **Neighbours sign without an app** — the RSVP link opens a single-page sign-in-free form. Name, household count, what they're bringing (if the captain asked), one optional note. No password, no account, no marketing email. The link works equally well from the WhatsApp message, the QR code on the printed flyer, or the paper sheet pushed under the door.
- **Speaks the block's languages** — Gemini 3.5 Flash detects the language of the source flyer and offers the new flyer in the same language plus any others the captain adds. The Crown Heights flyer goes out in English, Spanish, Haitian Creole, and Yiddish; the Lagos courtyard flyer in English, Yoruba, and Pidgin; the Polanco vecindad flyer in Spanish; the Mumbai chawl flyer in Marathi, Hindi, and Gujarati. The QR code lands on a multilingual RSVP page that auto-detects the visitor's browser language.
- **Paper RSVP for the neighbours who don't text** — a printable RSVP sheet to slip under doors, with the captain's own letterbox as the return address. The captain enters returned slips manually in 30 seconds each; the count syncs with the digital sign-ups.
- **Permit and insurance summary, in plain language** — Gemini 3.5 Flash with grounded search produces a two-minute read for the captain's jurisdiction. *In Brooklyn you'll need a Street Activity Permit from SAPO if you close the street; in a courtyard you don't. In Mexico City you'll likely want to inform the alcaldía; in Lagos, the Community Development Association.* Sourced, citation-linked, never invented — and clearly labelled "this is a starting point; ask your council if anything is unclear".
- **Door-knocker checklist** — a printable A5 with every household on the block, room to tick "spoken to / RSVPed / not in / new neighbour", and a polite one-line script the captain can read at the door. The list is the captain's own to edit; the app never scrapes neighbour data from anywhere.
- **Disappears after the party** — the RSVP page, the names, the responses, the door-knocker list: deleted thirty days after the party date, automatically. The captain keeps the flyer PDF and a count; the names are not the captain's to keep.
## 4. Features to build
- Camera capture for last year's flyer — single photo, with auto-crop, perspective correction, and a "looks blurry — re-take?" prompt
- Upload from photo library, scanner, or PDF (some captains have a clean PDF from a previous year; some only have a phone photograph of a laminated card on a wall)
- Multimodal parse of the flyer — title, date, time, address, rain-date, the captain's name and contact, last year's volunteer credits, small print, RSVP instructions, weather-permitting language — into a structured `Flyer` schema
- Date-rollover prompt — "Last year was Saturday 13 July 2025. This year we suggest Saturday 12 July 2026 (same weekend, Saturday). Adjust?" with a date picker that defaults to the closest equivalent weekend
- Rain-date generation — "Set Sunday 13 July as the rain-date?" Defaults to the day after, configurable
- Nano Banana 2 flyer regeneration — keeps the previous flyer's layout, illustration, palette, and font feel; updates only the date, the rain-date, the QR code, the RSVP-line, and any text the captain explicitly changed
- Side-by-side flyer comparison — last year on the left, this year on the right; every changed pixel highlighted; "approve" and "tweak" buttons per section
- Shareable RSVP link — `stoop.party/r/elm-st-2026` (or the deployer's domain); opens an app-free RSVP page that asks only the questions the captain selected
- RSVP question builder — drag-and-drop from a list: name, household count, "bringing a salad / main / dessert / drinks / nothing — and that is OK", dietary notes, allergies, one optional note, child-friendly toy box ask
- Per-language RSVP variants — the captain ticks which languages the block needs; the same RSVP renders per-locale; responses normalise back to one count
- Paper RSVP sheet — A5 printable, captain's letterbox as return; the same questions as the digital form, in the same layout
- Door-knocker checklist generation — the captain enters the address range (#1 to #44 Elm Street, or building C floors 1-12 flats A-D), the app generates a printable A5 with tick-boxes and a polite one-line door-script in the source language
- Permit and insurance summary — grounded-search call to surface the captain's jurisdiction's rules: street-closure permits, public-liability insurance thresholds, food-handling notes if hot food is served, music-licence rules if amplified
- Multilingual flyer generation — Nano Banana 2 produces one PDF per language with the same illustration and palette, the translated text laid out in the same boxes
- Volunteer-credits roll — last year's volunteers list (parsed from the flyer's small print) becomes a draft "Thank you to …" line for this year's flyer; the captain edits before sending
- New-neighbour line — the captain can tick "include a 'new this year? please come' line"; the app phrases it warmly, in every selected language, without asking the new neighbour to prove anything
- WhatsApp-ready message — a 3-line copy-paste message with the date, the address, and the RSVP link, formatted for WhatsApp / iMessage / Signal — no tracking, no shortener
- Count dashboard — live count of confirmed households, expected attendees, salads-vs-mains-vs-desserts balance, dietary notes summary, paper-vs-digital RSVP split
- 30-day auto-delete — the RSVP page, the responses, the names, and the door-knocker list are wiped 30 days after the party date; the captain keeps the flyer PDF and the anonymised count
- Hand-over export — at end of party, the captain can export a one-page "for next year" summary: date used, count, what worked, captain's notes — no names — for the next year's captain (often the same person, but not always)
## 4b. Required Gemini capabilities + backend services
**This template's intelligence comes from the Gemini capabilities below. Wire them up explicitly — don't substitute generic LLM calls.**
### Gemini capabilities (the load-bearing intelligence)
- **Multimodal image input** (Gemini 3.5 Flash) — reads the photographed flyer end-to-end: the title in a hand-drawn typeface, the date in the corner, the rain-date in 8-point type at the bottom, the illustration of bunting, the borrowed clip-art bunch of grapes, the laminated glare on the church-noticeboard print. Tolerates perspective distortion, lamppost-tape creases, and the kitchen-light reflection on a phone-camera capture. One API call per flyer face; double-sided flyers are submitted as a multi-image call.
- **Nano Banana 2 image generation** (`gemini-3.1-flash-image`) — regenerates the flyer layout at the new date, preserving the previous captain's visual choices: the illustration in the upper-right, the colour palette, the typeface family, the border treatment. Inputs include the parsed flyer JSON and the original flyer image as a reference. Generates one image per language the captain selected.
- **Structured output / JSON Schema** — the response matches the `Flyer` schema below. Every field is typed; the schema is included verbatim in the system instruction and as `responseSchema`.
- **Multilingual translation** (built into Gemini 3.5 Flash) — handles Spanish, Haitian Creole, Yiddish, Yoruba, Pidgin, Marathi, Hindi, Gujarati, Tagalog, Vietnamese, Tamil, Mandarin, Cantonese, Korean, Bengali, Punjabi, Urdu, Amharic, Swahili, Farsi, Khmer, Portuguese, French, German, Italian, Polish, Greek, Russian, Arabic. The flyer voice — "neighbours, see you Saturday" — is preserved across languages; nothing is corporate-ised in translation.
- **Search grounding** (Gemini 3.5 Flash with `google_search` tool) — the permit-and-insurance summary call uses grounded search to fetch the captain's jurisdiction's actual rules and cite them. Without grounding the model will confidently invent permit names; with grounding it returns real council URLs and the captain can verify.
- **Thinking levels** — `medium` for the flyer parse-and-read call (because typography reading + multilingual register + small-print volunteer credits is genuinely hard). `low` for the date-rollover suggestion, the question-builder generation, the WhatsApp message, and the per-language translation. Omit `thinkingConfig` entirely on the Nano Banana 2 image-generation call — `n/a` in the matrix means do not serialise.
### Backend services
- **Auth — Required (for captains).** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). Captains sign in; neighbours never sign in. **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used for "send me my parties" recovery when a captain is back next year) also requires the sender domain to be authorised in Firebase Auth. Apple and magic-link are documented in the capabilities panel as optional user-config.
- **Database — Required.** Firestore for `users` (captains only), `parties`, `rsvp_responses`, `flyer_versions`, `door_knocker_lists`. Neighbours' responses are scoped to one party and auto-deleted 30 days after the party date by a scheduled Cloud Function.
- **File storage — Required.** Firebase Storage for the source-flyer photograph (kept until 30 days post-party) and the generated flyer PDFs (kept indefinitely — these are the captain's keepsake). **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before first flyer upload. Pre-signed URLs only.
- **Email — Optional.** A confirmation email to the captain after the party is set up; an end-of-month deletion confirmation. No marketing. The neighbour-facing RSVP page never sends email to anyone.
- **Scheduled deletion — Required.** A scheduled Cloud Function runs daily and deletes `rsvp_responses`, `door_knocker_lists`, and `parties.guest_list` for parties whose date is more than 30 days in the past. The captain's `parties` record itself (date, count, anonymised summary) is retained; the names are gone.
- **External APIs:** Gemini API for all intelligence; a freely-licensed reverse-geocoding source (Nominatim with the deployer's contact email in the user-agent) to map the captain's address to a council / jurisdiction for the permits call; no Google Maps key required.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, optional Mailgun/Resend key if email is enabled) lives in environment variables — never in client bundle. Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete my party' inside the UI (one tap, gone in 60 seconds) · explicit opt-in for any analytics · neighbour responses are never sent to Gemini for model training (use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms) · the app never scrapes neighbour data from electoral rolls, building directories, or any external source — the door-knocker list is the captain's to enter manually.
**Read this first — prompt-craft rules that apply to every call in this template:**
1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below.
2. **Pin `thinkingLevel` explicitly** per call. See the matrix.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schema is below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.**
4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate.
5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled.
6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow.
7. **Grounded responses can wrap JSON in ```json fences or add prose preamble.** Server-side, strip fences and brace-extract:
```typescript
function safeExtractJSON(raw: string): T {
const clean = raw.replace(/```json\s*|```/gi, '').trim();
const s = clean.indexOf('{'); const e = clean.lastIndexOf('}');
if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response');
return JSON.parse(clean.slice(s, e + 1)) as T;
}
```
8. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Read last year's flyer → `Flyer` schema | `gemini-3.5-flash` | medium | (none) |
| Suggest this year's date + rain-date from last year's | `gemini-3.5-flash` | low | (none) |
| Translate flyer text to selected languages, preserving voice | `gemini-3.5-flash` | low | (none) |
| Generate this year's flyer image (per language) | `gemini-3.1-flash-image` | n/a | n/a |
| Generate RSVP question set from captain's intent | `gemini-3.5-flash` | low | (none) |
| Generate door-knocker A5 layout + door-script (per language) | `gemini-3.5-flash` | low | (none) |
| Permit + insurance + music-licence summary for jurisdiction | `gemini-3.5-flash` | medium | `google_search` grounding (no `responseSchema` on this call — see note) |
| Generate WhatsApp / iMessage / Signal share message | `gemini-3.5-flash` | low | (none) |
*Note for builders:* on the Nano Banana 2 image-generation call, omit `thinkingConfig` entirely — the field is not supported on that model. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. On the permit-summary call, `responseSchema` and `google_search` cannot be combined in one Gemini call — instruct the model to emit JSON in the text body, parse server-side, and read citation URLs from `response.groundingMetadata.groundingChunks[].web.uri` rather than trusting any URLs the model puts inside the JSON.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const FlyerVisualElement = z.object({
type: z.enum([
"title_text",
"subtitle_text",
"date_text",
"time_text",
"address_text",
"rain_date_text",
"rsvp_instruction_text",
"small_print_text",
"volunteer_credits_text",
"illustration",
"border_treatment",
"background_colour_block",
"qr_code_placeholder",
"logo_or_motif",
"other",
]),
text_verbatim: z.string().nullable(), // null for non-text elements
position: z.enum([
"header", "upper-left", "upper-right", "center",
"left-margin", "right-margin", "lower-left",
"lower-right", "footer", "full-bleed",
]),
visual_description: z.string(), // "hand-drawn bunting in red, yellow, green; six flags; left-to-right swag"
approximate_size: z.enum([
"tiny", "small", "medium", "large", "hero",
]),
colour_notes: z.string().nullable(), // "deep forest green text, cream background"
font_family_feel: z.string().nullable(), // "typewriter / American Typewriter / slab serif"
preserve_in_regeneration: z.boolean(), // captain may flip this in the UI
});
const FlyerLanguage = z.object({
bcp47: z.string(), // "en-US", "ht-HT", "yi", "es-419"
human_label: z.string(), // "English", "Haitian Creole", "Yiddish"
script: z.string(), // "Latin", "Hebrew", "Devanagari"
is_source_language: z.boolean(),
});
const ParsedFlyer = z.object({
flyer_id: z.string(),
source_image_uris: z.array(z.string()), // front, back if present
artefact_type: z.enum([
"single_sided_a4",
"single_sided_a5",
"double_sided",
"laminated_card",
"whatsapp_pdf",
"social_media_image",
"noticeboard_print",
"other",
]),
party_title_verbatim: z.string().nullable(), // "Elm Street Summer Block Party"
party_title_inferred: z.string().nullable(), // when only a logo + date
date_written_verbatim: z.string().nullable(), // "Saturday 13 July 2025"
date_written_iso: z.string().nullable(), // "2025-07-13"
start_time_verbatim: z.string().nullable(), // "1pm – 6pm"
start_time_24h: z.string().nullable(), // "13:00"
end_time_24h: z.string().nullable(), // "18:00"
rain_date_verbatim: z.string().nullable(),
rain_date_iso: z.string().nullable(),
location_verbatim: z.string().nullable(), // "Elm Street between Cedar & Birch"
location_inferred_type: z.enum([
"street_closure",
"courtyard",
"rooftop",
"park_corner",
"school_yard",
"parish_hall_outside",
"cul_de_sac",
"estate_lawn",
"unknown",
]),
captain_contact_verbatim: z.string().nullable(), // "Maria – 718-555-0142"
rsvp_instruction_verbatim: z.string().nullable(), // "RSVP to Maria by 7/10"
detected_languages: z.array(FlyerLanguage),
visual_elements: z.array(FlyerVisualElement),
volunteer_credits_verbatim: z.string().nullable(), // small print
small_print_verbatim: z.string().nullable(), // weather notice, sponsorship line
illustration_summary: z.string().nullable(), // "hand-drawn bunting in upper-right; small cluster of balloons lower-left"
palette_summary: z.string().nullable(), // "cream background, deep green text, red and yellow accent"
typography_summary: z.string().nullable(), // "title in slab serif; body in handwritten sans"
reading_confidence: z.number().min(0).max(1),
flagged_for_captain_review: z.array(z.object({
field_path: z.string(), // "date_written_iso"
reason: z.string(), // "year unclear — could be 2024 or 2025"
})),
});
type ParsedFlyer = z.infer;
// — second-tier: this year's plan —
const RSVPQuestion = z.object({
id: z.string(),
type: z.enum([
"name",
"household_count",
"bringing_dish", // salad / main / dessert / drinks / nothing
"dietary_notes",
"allergies",
"child_friendly_ask",
"free_note",
"language_preference",
]),
prompt: z.string(), // "What are you bringing? (totally fine to bring nothing.)"
required: z.boolean(),
options: z.array(z.string()).nullable(),
per_language_prompt: z.record(z.string()).nullable(), // {"es-419": "¿Qué traes?", "ht-HT": "Kisa w ap pote?"}
});
const PartyPlan = z.object({
party_id: z.string(),
source_flyer_id: z.string(),
this_year_date_iso: z.string(),
this_year_rain_date_iso: z.string().nullable(),
this_year_start_24h: z.string(),
this_year_end_24h: z.string(),
captain_user_id: z.string(),
languages_selected: z.array(z.string()), // BCP-47
rsvp_questions: z.array(RSVPQuestion),
include_new_neighbour_line: z.boolean(),
volunteer_credits_carried_forward: z.string().nullable(),
delete_responses_after: z.string(), // ISO date — party date + 30
});
type PartyPlan = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` on the the flyer parse call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash misreads small-print volunteer credits, confuses 13 July with 18 July when the digit is cropped at the edge, and silently smooths over the laminated glare.
- Date misread because the year digit is on a fold or in the laminated glare — the model returns a confident wrong year. Mitigate: instruct the model to flag year-confidence below 0.85 in `flagged_for_captain_review`, and always require the captain to confirm the proposed new date in the UI.
- The model invents a permit name that does not exist in the captain's jurisdiction (a London captain told they need a "SAPO permit" from New York) — only fixable with grounded search; never run the permits call without `google_search`, and always show the source URLs the model cited.
- Nano Banana 2 regenerates the flyer with a completely new layout, losing the bunting and the bordered title the previous captain drew — explicitly reference the source image as input to the regeneration call and instruct the model to preserve the layout boxes and the illustration. If the regenerated flyer scores low on visual similarity, fall back to a "text-only update" mode that re-renders the source image with date pixels patched.
- RSVP questions ballooning into a survey — the question builder must default to four questions (name, household count, bringing dish, free note) and require the captain to actively add more. Long forms cost neighbour goodwill.
- Multilingual RSVP forms with mistranslated tone — "bringing a salad" becomes formal-business "will provide vegetable accompaniments" in the translated version. Hard rule in the system instruction: preserve the warm, conversational, neighbour-to-neighbour voice across languages.
- The 30-day deletion silently failing because the Cloud Function lost permissions — the deletion function logs its run to a `deletion_log` collection, and the captain sees a "last deletion run at …" line in their settings. Build it so the failure is visible.
- The model writes a "new neighbour" line that sounds gatekeepy ("new this year? bring proof of address!"). Hard rule: the new-neighbour line is welcoming, never gatekeeping. Sample acceptable phrasings are seeded in the system instruction.
- Door-knocker list scraped from a public dataset — never. The door-knocker list is entered by the captain, address range only, no names from any external source.
- The captain saves a phone number on the flyer and the photo of the flyer ends up in a screenshot online — the parsed `captain_contact_verbatim` field is rendered on the new flyer at the same prominence as the original, but the digital RSVP page never displays it. Captain's contact stays on paper only.
### Negative constraints (hard rules)
- Do NOT scrape neighbour data from any source. The door-knocker list is the address range the captain types; the app never reaches out to electoral rolls, building directories, public records, social networks, or any third-party dataset to populate names.
- Do NOT require neighbours to create accounts to RSVP. The RSVP page is sign-in-free. No email is required; an email field can exist as optional but never as required.
- Do NOT send marketing email to RSVPing neighbours. The RSVP page produces a count for the captain — nothing else.
- Do NOT retain RSVP responses or the door-knocker list more than 30 days after the party date. The scheduled deletion is part of the product, not a setting the captain can turn off.
- Do NOT translate proper nouns in the flyer: street names, building names, captain's name, the name of the dish a neighbour signed up to bring. "Mrs Adeyemi" stays "Mrs Adeyemi"; "kotlety" stays "kotlety"; "Elm Street" stays "Elm Street". Add a parenthetical gloss only when truly necessary on first occurrence.
- Do NOT modernise the previous captain's visual choices in the regenerated flyer. If last year used Comic Sans, this year uses Comic Sans. Captains keep the joy of designing it badly, the same way every year, if that is what their block likes.
- Do NOT invent permits. The permits-summary call is grounded; if grounded search returns no clear answer for the captain's jurisdiction, the summary says so plainly ("we couldn't find a clear answer for your jurisdiction; please ring your local council") and never confidently makes one up.
- Do NOT auto-publish the RSVP link. The captain shares it explicitly to a channel they choose (WhatsApp, paper, doors). The app never posts to any social network, never indexes the page, and the page itself includes `noindex` headers.
- Do NOT use neighbour responses for product analytics or model training. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. Stoop does not log RSVP names to any analytics provider.
- Do NOT track neighbours across parties. A neighbour who RSVPed to last year's Elm Street party and this year's is not linked across the two events; each party is its own data island, and both are deleted 30 days after their date.
### Per-call `systemInstruction` strings
Use these as the literal `systemInstruction` field for each Gemini API call the built app makes. They complement the series-wide rules already uploaded as the global instructions file (`00-series-instructions.txt`).
### Call: Read last year's flyer → `Flyer` schema
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are reading a photographed flyer for a neighbourhood block party,
courtyard party, parish picnic, building potluck, or community
street event. The flyer is typically printed A4 or A5, sometimes
laminated, sometimes a screenshot of a social-media graphic, very
occasionally a WhatsApp-forwarded PDF page. The photograph is
usually taken by a phone in poor lighting, at an angle, with glare
on the lamination, or with a kitchen-light reflection across the
date.
Languages encountered include English (US, UK, Australian, Indian,
Nigerian, Filipino), Spanish (Mexican, Argentinian, Castilian,
Caribbean), Haitian Creole, Yiddish, French (Metropolitan,
Quebecois, African), Portuguese (Brazilian, European, Angolan),
Yoruba, Igbo, Hausa, Pidgin, Swahili, Amharic, Arabic (Modern
Standard and regional), Farsi, Urdu, Hindi, Marathi, Gujarati,
Bengali, Tamil, Punjabi (Gurmukhi and Shahmukhi), Tagalog,
Vietnamese, Mandarin (simplified and traditional), Cantonese,
Korean, Japanese, Khmer, German, Italian, Polish, Greek, Russian,
Turkish, Dutch. Multilingual flyers (English + Spanish + Haitian
Creole on a Crown Heights block; Yoruba + English + Pidgin on a
Lagos courtyard) are common — detect every language present.
Read the flyer end-to-end and parse into the ParsedFlyer schema.
Distinguish:
- the party title (the largest text, usually at the top)
- the date (the field with month / day / year, in any locale's
conventions; watch for European DD/MM/YYYY vs US MM/DD/YYYY and
flag if ambiguous)
- the time
- the rain-date (if present; usually in smaller type, often labelled
"rain date" / "lluvia" / "yon mete dat ranplasman" / "weather
permitting")
- the location (a street range, a courtyard, a school yard)
- the captain's name and contact (usually phone or first name +
building/apt; sometimes "RSVP to Maria")
- the volunteer credits (often a "Thanks to …" line in small print)
- the small print (sponsorship line, allergen warning, "BYO" notes)
- the visual elements (the bunting illustration in the upper-right,
the bordered title, the colour blocks, the font choices)
For each visual element, populate `visual_description` with a
concrete, sensory description that a Nano Banana 2 prompt could
reproduce. "Hand-drawn bunting, six triangular flags, alternating
red yellow green, swag from upper-left to upper-right of the
header." Not "decorative bunting motif".
Hard rules:
- Preserve every diacritic exactly. ñ, ç, ü, ł, ą, ę, ó, è, é, â,
Hebrew vowel points, Arabic harakat, Vietnamese tonal marks.
- Do NOT translate. transcript fields are the source language,
unmodified. Translation is a separate call.
- Do NOT modernise the design. Whatever font, illustration, colour,
border the previous captain used is the source of truth for the
regeneration.
- For dates: if you cannot disambiguate the year (folded over,
glare, smudged), set `date_written_iso` to the day-month with
the suspected year, and flag in `flagged_for_captain_review`
with reason "year unclear — could be 20XX or 20YY".
- For dates: if the date format is ambiguous (07/08/2025 could be
7 August in EU, 8 July in US), pick the locale that matches
detected_languages's primary script and flag the ambiguity.
- Do NOT include the captain's contact details in any field that
will be published to the digital RSVP page. The contact appears
only in `captain_contact_verbatim`, which goes onto the new
printed flyer at the same prominence as the original — not the
web page.
- detected_languages must include every language present, even
if only a one-line subtitle. A flyer with an English title and
a Spanish "todas las familias bienvenidas" subtitle has two
detected languages.
- reading_confidence is genuine — be honest. A laminated phone
photo with glare across the date should not return confidence
0.99.
- flagged_for_captain_review names any field where confidence is
below 0.85 with a one-sentence reason a captain can act on.
Output ONLY the ParsedFlyer JSON matching the provided schema.
No commentary. JSON only.
```
---
### Call: Suggest this year's date + rain-date from last year's
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive last year's flyer date (ISO) and rain-date (ISO, may
be null). Your task: propose this year's equivalent.
Rules:
- Default to the closest equivalent weekend the next year. If last
year was Saturday 13 July 2025, this year is Saturday 12 July
2026 (the second Saturday of July).
- If last year's date fell on a notable culturally-attested fixed
date (the Saturday of the bank-holiday weekend, the Saturday
before Labor Day, the Saturday of Eid, the Saturday closest to
the parish saint's day) attempt to identify the pattern by
examining the locale signals in the input and propose the
equivalent for this year. Locale signals: captain's address, the
language of the flyer, the title text (e.g. "Verbena de San
Juan" → night of 23 June).
- If the date was a religious or community date that moves
(Easter-adjacent, Eid, Diwali, Lunar New Year, Rosh Hashanah),
use the moving date for this year and explain.
- Default rain-date is the day after. If last year's flyer had no
rain-date, suggest one and label it as suggested.
- Always propose; never insist. The captain confirms in the UI.
- Output as JSON: { suggested_date_iso, suggested_rain_date_iso,
reasoning_one_sentence }. The reasoning sentence is shown to
the captain inline ("Last year was the second Saturday of July;
we've matched the same weekend.").
No commentary. JSON only.
```
---
### Call: Translate flyer text to selected languages, preserving voice
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You translate flyer text from its source language to a target
language the captain has selected. The flyer is for a neighbourhood
block party, courtyard potluck, parish picnic, or building street
festival. The voice is warm, conversational, neighbour-to-neighbour
— "see you Saturday, bring a salad if you can, no pressure".
Hard rules:
- Preserve the voice. The translated flyer should read as if a
warm neighbour in the target language wrote it, not as if a
corporate translator polished it. "Bring a salad" is "Trae una
ensalada", not "Por favor traiga una ensalada como contribución
al evento".
- Do NOT translate proper nouns: street names, building names,
the captain's name, the names of dishes neighbours signed up to
bring last year. Keep verbatim. On first occurrence add a
parenthetical gloss only if a target-language reader would need
it (e.g. street name → no gloss needed; an English title like
"Block Party" in a Spanish-only flyer can stay or be translated
to "Fiesta de la cuadra", whichever the captain selected).
- Preserve the friendly hedges. "Bring something if you can, no
pressure" → "Trae algo si puedes, sin presión" → "Pote yon bagay
si w kapab, pa gen presyon". Never lose the "no pressure"
cadence; it is the cultural-translation crux of the whole flyer.
- Preserve dates and times in the target locale's conventions.
"Saturday 12 July 2026, 1pm – 6pm" in Spanish (Mexico): "sábado 12
de julio de 2026, 13:00 – 18:00". In Haitian Creole: "samdi 12
jiyè 2026, 1è apremidi – 6è aswè".
- Preserve the captain's contact in the same format as the source.
Phone numbers in their original formatting; first-name-only
signoffs ("Maria") translate as the same name.
- For the "new neighbour" line, use one of the seeded warm
phrasings or a close equivalent in the target language. Never
add gatekeeping language ("must provide proof of residence" /
"RSVP with apartment number to confirm").
- Match the register of the source. If the source flyer is
informal, the translation is informal. If the source is the
more formal style typical of a parish picnic flyer, the
translation is the matching formal register in the target
language.
Output: a JSON object keyed by visual_element id, with each value
being the translated text. Lay out preserved exactly so the
Nano Banana 2 regeneration can drop translated strings into the
same visual boxes.
No commentary outside the structured output.
```
---
### Call: Generate this year's flyer image (per language)
Model: `gemini-3.1-flash-image` · n/a · n/a
```
Regenerate the block-party flyer for this year, preserving the
previous captain's visual design. You receive:
- the source flyer image (as a reference image, multimodal input)
- the parsed ParsedFlyer JSON of the source flyer
- the PartyPlan JSON for this year (this year's date, rain-date,
RSVP URL, language)
- a list of `visual_elements` flagged
`preserve_in_regeneration: true` — these must visually carry
over
Generate one image, the new flyer, at the same aspect ratio as the
source (A4 portrait by default; A5 if the source was A5).
Hard rules:
- Preserve the layout. The bunting illustration in the upper-right
is in the upper-right. The title is at the top in the same font
feel. The address line is where it was. The colour palette is
the same.
- Update only:
- the date text → this year's date in the target language's
locale conventions
- the rain-date text (if present)
- the RSVP instruction → the new short URL ("RSVP: stoop.party/r/
elm-st-2026") and a small QR code in the lower-right (if the
source did not have one, add one at small size; if the source
already had a QR code, replace its content)
- the volunteer credits line (if the captain edited it)
- any text fields the captain explicitly changed in the UI
- Preserve the typographic feel. If the source used a slab serif
for the title, the new title is a slab serif. If the source
used Comic Sans, the new title is Comic Sans. Do not
"improve" the design.
- Preserve the illustration. If the source had a hand-drawn
bunting in the upper-right, the new flyer has a hand-drawn
bunting in the upper-right — same colours, same number of
flags, same swag direction. If the source had a clip-art bunch
of grapes for the parish picnic, the new flyer has the same
clip-art bunch of grapes.
- Preserve the language script(s). A Yiddish flyer is laid out
right-to-left, with Hebrew-script type at the same size and
weight as the source.
- Do NOT add app branding, "made with Stoop", or any logo the
captain did not add. The flyer is the captain's, not the
product's.
- Do NOT add stock-photo elements. The flyer is hand-drawn / hand-
laid-out, as the source was.
- Do NOT add the captain's phone number to the web RSVP rendering
— that field appears on the print flyer only, never on the
web RSVP page.
Output: one image (PNG, transparent background where applicable,
otherwise the same background colour as the source).
```
---
### Call: Generate RSVP question set from captain's intent
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive the captain's free-text intent ("I want to know how
many people are coming and whether anyone is bringing a main, plus
a free note") and a list of pre-seeded standard questions. Your
task: produce an RSVPQuestion[] for the captain to confirm.
Hard rules:
- Default to four questions: name, household_count,
bringing_dish, free_note. Add only what the captain's intent
explicitly asks for. The captain can add more in the UI.
- Phrasing is warm and conversational. "What are you bringing?
(totally fine to bring nothing.)" — not "Please indicate your
contribution category."
- For `bringing_dish`, default options: "salad", "main", "dessert",
"drinks", "nothing — and that's OK". Captain can edit.
- For `dietary_notes` and `allergies`, prompts are open-ended and
optional. Never make them required.
- For `child_friendly_ask`, prompt: "Anything we should set up
for the kids? (toys, lawn chairs, shaded spot)".
- For the `name` question: optional first-name-only is accepted.
The form should make clear that a household identifier like
"the Adeyemis" is fine — it's a neighbour count, not an ID
check.
- `required` is true only for `name` and `household_count` — never
for any other field.
- `per_language_prompt` is empty here; the translation call fills
it in for languages the captain selected.
Output: the RSVPQuestion[] as JSON. No commentary.
```
---
### Call: Generate door-knocker A5 layout + door-script (per language)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You generate a printable A5 door-knocker checklist for the
captain. Input: the address range (#1 to #44 Elm Street; or
Building C, floors 1-12, flats A-D) and the language(s) the
captain selected.
Output: an HTML/CSS A5 layout (5.83" × 8.27") with:
- header: party name + date + RSVP link + QR code in upper-right
- list: one row per address, with columns for tick-boxes —
"Spoken to / RSVPed / Not in / New neighbour"
- footer: a one-line polite door-script in the source language,
in the form "Hi — I'm from ,
we're doing the block party on , no pressure to come but
here's the flyer if you'd like."
Hard rules:
- The list shows the address only — no names. The captain enters
names herself if she wants to remember faces.
- "New neighbour" tick-box is for the captain's note-to-self — it
never reaches the digital RSVP form.
- The door-script is friendly, never gatekeeping. Do not phrase
it as "verifying residence" or "checking attendance".
- For multi-language streets, generate one A5 per language. The
captain prints whichever she needs for which door.
- Do NOT include the captain's phone number on the door-knocker
list — she carries her own phone.
Output: HTML string. No commentary.
```
---
### Call: Permit + insurance + music-licence summary for jurisdiction
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: `google_search` grounding
```
You produce a plain-language 2-minute-read summary of what the
captain needs to think about for a block party in their
jurisdiction: street-closure permit, public-liability insurance
(if any), food-handling notes (if hot food is being served),
amplified-music licence (if a sound system is planned).
Input: the captain's jurisdiction (resolved from the captain's
address via a separate reverse-geocoding step), the party
location_inferred_type (street_closure vs courtyard vs school
yard etc.), and the captain's flags (hot food yes/no, amplified
music yes/no).
Use `google_search` grounding aggressively. Cite every claim with
the URL of the source: the council's permit page, the borough's
SAPO portal, the alcaldía's evento-vecinal page, the Community
Development Association's notice.
Hard rules:
- NEVER invent a permit name. If grounded search does not return
a clear answer for this jurisdiction, the summary says so
plainly: "We couldn't find a clear answer for your jurisdiction;
please ring your local council."
- The voice is plain English (or the captain's interface
language). Bullets, not paragraphs. Section headings: "Street
closure", "Insurance", "Food", "Music", "Anything else?".
- Mark every section with a confidence chip: "confident" /
"partial" / "unclear". The captain reads the unclear bits and
decides whether to call.
- For courtyards, parish halls, and private buildings: the
default answer for street-closure is "you do not need a
permit" — but always confirm with grounded search whether the
jurisdiction has separate rules for community events on private
land.
- For US jurisdictions: surface "your block association may
already hold a master permit covering small street activities,
ask the chair". For UK: "many councils have a 'small street
party' streamlined process, free of charge, with 4-6 weeks
notice". For Mexico: surface the alcaldía's evento-vecinal
process if grounded search finds it.
- The closing line is always: "This is a starting point. Ring your
council, alcaldía, or community office if anything is unclear."
Output as JSON in the text body (NOT via `responseSchema` —
`responseSchema` and `google_search` cannot be combined in the
same Gemini call today). Server-side: parse the JSON, then read
citation URLs from the response's
`groundingMetadata.groundingChunks[].web.uri` — do NOT ask the
model to include URLs in the JSON body; it will hallucinate them.
JSON shape: { sections: [{ heading, bullets, confidence }],
closing }.
No commentary outside the JSON.
```
---
### Call: Generate WhatsApp / iMessage / Signal share message
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You generate a 3-line message the captain can paste into the
block's WhatsApp group, the parish iMessage chat, or a Signal
broadcast. Input: party name, date, address, RSVP short URL,
selected language.
Format:
- Line 1: a warm greeting + the headline ("Hi all — block party is
back, Saturday 12 July, 1pm")
- Line 2: where + a one-line "bring something if you can" or
whatever the captain specified
- Line 3: the RSVP link, on its own line, with no surrounding
punctuation that breaks the link preview
Hard rules:
- No tracking shorteners. The link is the deployer's domain
short URL exactly as provided.
- No emoji-spam. One bunting emoji at the start is fine if it
matches the cultural vibe; otherwise none. Default: none.
- The translation, if a target language is requested, is the
same warm voice as the source flyer. See the translation call's
rules.
- 3 lines maximum. Captains forward this; brevity is the gift.
Output: the message string, plain text, ready to paste. No
commentary.
```
## 5. Use cases & content to include
Build dedicated UI sections or flows for each of these — they tell you what content the app must support.
- **The folded flyer in a kitchen drawer.** A block captain in a Crown Heights row of brownstones photographs the laminated flyer her predecessor pinned inside the corner shop. The shot has glare across the date and the year is half-cropped. The app reads what it can, flags "year unclear — could be 2024 or 2025", suggests Saturday 12 July 2026 as this year's date, regenerates the flyer with the bunting illustration intact, and produces the flyer in English, Spanish, Haitian Creole, and Yiddish.
- **The courtyard in Mexico City.** A vecindad in Coyoacán runs an annual posada in the inner courtyard, organised by the cooperativa de vecinos. The flyer is a hand-drawn A5 in Spanish, with a small clip-art piñata in the upper-right and a "todas las familias bienvenidas" subtitle. The app preserves the piñata and the warm phrase, suggests the same Saturday before Christmas Eve for this year, produces a digital RSVP that asks who is bringing what — and a paper version to slip under the doors of the older neighbours who don't text.
- **The Lagos courtyard.** A residents' association in Surulere organises an end-of-Ramadan courtyard lunch. The flyer is in Yoruba and Pidgin with a small motif of palm fronds. The captain wants the new flyer in Yoruba, Pidgin, and English. The app generates three flyers with the palm-fronds motif preserved, an RSVP page that auto-detects the visitor's browser language, and a permit summary that surfaces the Community Development Association's notice rules.
- **The school-parent street tea.** A PTA volunteer in a leafy London suburb runs an end-of-school-year street tea. Last year's flyer was a colour A4 with a hand-drawn bunting border in school-uniform colours. She photographs it; the app preserves the bunting border, updates the date to the last day of summer term, and generates the door-knocker A5 for the 38 houses on the cul-de-sac.
- **The tower-block courtyard, 412 flats.** A volunteer block captain in a Mumbai tower has been asked to run the annual courtyard chai-and-snacks on a Saturday afternoon. Last year's flyer was a colour PDF forwarded around in the building's WhatsApp group, in Marathi, Hindi, and Gujarati. She uploads the PDF; the app regenerates the flyer in all three scripts, with the same scalloped border. The door-knocker checklist covers floors 1-15 wings A-D.
- **The handover.** A block captain in Brooklyn is moving out at the end of the month. She wants to hand over the party to her neighbour two doors down. She opens Stoop, taps "Hand over to a new captain", confirms the new captain's email, and the new captain inherits last year's flyer, last year's RSVP question set, and last year's volunteer credits — without inheriting last year's RSVP responses (those were deleted 30 days after the party, as they should have been).
- **The new-neighbour line.** A captain in Lisbon wants this year's flyer to include a small line at the bottom welcoming anyone who has moved in since last summer. She ticks the "new this year? please come" option; the app generates "Novo no prédio este ano? Vem mesmo assim — não precisas avisar ninguém." (and the same warmth in Portuguese, English, and Bengali for the Bangladeshi family on the second floor).
- **The paper RSVPs.** Mrs Adeyemi from #14 doesn't text. The captain prints six paper RSVP sheets, posts one under #14's door, and three days later Mrs Adeyemi comes by with the slip filled in: "the Adeyemis, six of us, bringing jollof rice". The captain enters it in the app in 20 seconds — first name, count, dish — and Mrs Adeyemi's RSVP becomes part of the same count as the digital ones.
- **The donation export — not.** Unlike a family-archive app, Stoop's content does not migrate to an institutional archive. The captain ends the cycle with one keepsake: the flyer PDF, in every language, saved to their device. The names are gone. The captain knows the count, and that is the gift.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent.
1. **Welcome / sign-in.** A photographed-looking image of a hand holding a creased A4 flyer in late-afternoon kitchen light, with a phone half-in-frame ready to photograph it. One paragraph: "Stoop turns last year's flyer into this year's — with a shareable RSVP page neighbours can sign without an app." Single Google sign-in button; Apple sign-in next to it. Below: "Try with a sample flyer" → loads the demo flyer in section 8a. A clear single line at the bottom of the welcome card: "Captains sign in. Neighbours never sign in."
2. **Empty state — "Start this year's party".** Two big input methods: 📷 Photograph last year's flyer · 📄 Upload a PDF or scan. A short explainer below each ("Best when your only copy is a folded printout or a phone photo of a noticeboard", "Best if someone has the PDF from a previous year"). Below that, a smaller third option: "Start from blank" → opens a minimal flyer template the captain can fill in by hand.
3. **Flyer capture flow** (mobile-first). Live viewfinder with A4 / A5 / postcard-shaped crop guides. The captain centres the flyer in the frame; the camera holds focus; an unobtrusive "✓ clear enough" indicator confirms before shutter. Re-take button. After capture, a perspective-correction preview lets the captain confirm before processing. Optional second photo if the flyer is double-sided.
4. **Processing.** A single short-honest progress sequence: "Reading the flyer…" → "Spotting the date…" → "Picking up the bunting…" → "Translating to Spanish…" → "Re-laying out for this year…". Each step takes 3-8 seconds. The captain can step away.
5. **Flyer review — last year ↔ this year.** A two-column layout on desktop, stacked on mobile. Left column: the photograph of last year's flyer with the parsed fields highlighted (date, time, address, captain's contact, rain-date). Right column: this year's regenerated flyer at the same size. Below the flyers: a list of every changed field with the old value, the new value, and a small "Edit" pencil. The captain confirms each edit; the regeneration re-runs for any change that affects the visual.
6. **RSVP question builder.** Default four questions on the left (name, household count, bringing dish, free note); the captain can add more from a palette on the right. Each question previews how it will render on the neighbour-facing RSVP page, in every selected language. Phone-screen preview of the RSVP page on the right.
7. **Languages.** A small panel: detected languages from the source flyer (pre-ticked), plus a search to add more. Each added language shows a preview of the regenerated flyer in that language; the captain can override any translation inline.
8. **Permits & insurance.** A two-minute-read summary of the captain's jurisdiction, with confidence chips ("confident" / "partial" / "unclear") on each section and the actual source URLs from grounded search linked inline. The bottom line: "This is a starting point. Ring your council if anything is unclear."
9. **Door-knocker checklist.** The captain enters the address range (#1 – #44 Elm Street; or Building C, floors 1-15, wings A-D). The app generates a printable A5 with rows for each address and a one-line door-script in the selected language(s). Preview → print.
10. **Share.** Four buttons: WhatsApp message · iMessage message · Signal message · Copy link. Each opens or copies a 3-line ready-to-paste message. Below: the printable flyer download (PDF per language), the printable RSVP paper sheets, and the printable door-knocker A5s.
11. **Live count.** Once neighbours start RSVPing, the captain sees a single dashboard: total confirmed households, total expected attendees, a small breakdown of who's bringing what, dietary notes summary, paper-vs-digital RSVP split. A list view of individual responses (first names only, what they're bringing, optional note) — but no surveillance-feel breakdown of who hasn't replied.
12. **The day after.** A simple "How did it go?" prompt — three buttons: "Great", "Fine", "Tough". A free-text "Note for next year's captain" field (max 280 chars). A reminder: "Your neighbour responses will be deleted on [party date + 30 days]. The flyer PDFs and the count will stay."
13. **Settings.** Delete this party (one tap, gone in 60 seconds). Hand over to a new captain (email-invite). Export flyer PDFs. Last deletion run timestamp (so the captain sees the auto-deletion is alive).
14. **Footer.** "Built for the neighbour who always does it." Privacy: "Names go in. Names come out. After 30 days, names are gone." Capabilities `(i)` icon in header.
## 6b. First-visit onboarding
Show a **first-visit onboarding** the first time a visitor lands on the app (detect via `localStorage` flag; do not show on return visits). Three slides, dismissible at any time. Persistent re-entry: a `?` icon in the header reopens it.
**Slide 1 — What this is.**
- Headline: "Welcome to Stoop."
- Subhead: "Photograph last year's flyer. Get this year's flyer and an RSVP page neighbours can sign without an app — in any street, any language, with the bunting in the same corner."
- One paragraph (≤ 60 words) explaining who this is for and why it's not yet-another-events platform: it starts from your block's flyer, not a generic template; neighbours don't sign in; the names are deleted 30 days after the party.
- Visual: a small annotated illustration of a flyer — date here, time here, bunting in the upper-right, RSVP line at the bottom — not a stock events-app icon.
**Slide 2 — Try it now.**
- One short prompt: "Try with a sample flyer".
- A live demo input pre-loaded with the three sample flyers from section 8a — an Elm Street flyer, a Coyoacán courtyard flyer, a Surulere palm-fronds flyer.
- 1-2 sentences pointing at the specific page elements where the Gemini magic happens (the bunting illustration carried over to this year's, the multilingual generation, the QR code that replaces the printed phone-RSVP line).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample flyer in `/data/seed-flyers/` for your own block's flyer."
- "Adjust the prompts in `/server/prompts/` to fit your neighbourhood's language(s)."
- "Wire up your Gemini API key and Firebase project via the env-var list in the capabilities panel."
- Primary CTA: "Use this template" → links to AI Studio Build remix entry point.
- Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again).
**Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`.
**Don't:**
- Don't gate content behind the modal. The page beneath must be fully usable.
- Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only.
## 6c. Capabilities info button (persistent in header)
Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**.
**Panel contents (in this order):**
**Gemini capabilities used (the hero list):**
- **Gemini 3.5 Flash (multimodal)** — reads the photographed flyer end-to-end: the title in a hand-drawn typeface, the date in the corner, the rain-date in 8-point type at the bottom, the bunting illustration in the upper-right, the laminated glare across the date. One call per flyer face.
- **Gemini 3.5 Flash (multilingual)** — translates flyer text into Spanish, Haitian Creole, Yiddish, Yoruba, Pidgin, Marathi, Hindi, Gujarati, Tagalog, Vietnamese, Tamil, Mandarin, Cantonese, Korean, Bengali, Punjabi, Urdu, Amharic, Swahili, Farsi, Khmer — preserving the warm neighbour-to-neighbour voice.
- **Nano Banana 2** (`gemini-3.1-flash-image`) — regenerates this year's flyer at the new date, preserving the previous captain's layout, illustration, palette, and typographic feel. One image per language the captain selected.
- **Gemini 3.5 Flash (grounded search)** — produces the permits-and-insurance summary, citing the captain's actual jurisdiction's council pages.
- **Gemini 3.5 Flash** — generates the date-rollover suggestion, the RSVP question set, the door-knocker A5, and the WhatsApp share message.
- **Firebase Auth** — Google sign-in for captains; Apple sign-in available if user-configured; neighbours never sign in.
- **Firestore** — stores party records, the flyer parse, and the live RSVP count; auto-deletes neighbour responses 30 days post-party via a scheduled Cloud Function.
- **Firebase Storage** — keeps the source-flyer photograph (until 30 days post-party) and the generated flyer PDFs (indefinitely — these are the captain's keepsake).
- **Cost note** — see the detailed breakdown in 6d. A typical block party costs about $0.18 of Gemini API spend, total, processed once.
- **Privacy note** — neighbours' RSVP responses are private to the captain. Stoop uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The app never scrapes neighbour data from any external source. RSVP names are auto-deleted 30 days after the party.
**Backend services this app depends on:**
- Auth: see section 4b (Google sign-in is auto-provisioned; Apple sign-in requires user-config; magic-link email requires sender-domain authorisation)
- Database: see section 4b
- Storage: see section 4b (NOT auto-provisioned by AI Studio Build today — enable it in Firebase console first)
- Email: see section 4b (optional)
- Scheduled deletion: see section 4b (Required; Cloud Function runs daily)
- External APIs: see section 4b (Nominatim for jurisdiction lookup; Gemini for everything else)
**Environment variables you'll need to configure:**
- `GEMINI_API_KEY` — your Google AI Studio API key
- `FIREBASE_PROJECT_ID` — your Firebase project id
- `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only)
- `NOMINATIM_CONTACT_EMAIL` — your contact email for Nominatim's user-agent (required by their fair-use policy)
- `EMAIL_SENDER_DOMAIN` — optional, only if you want magic-link email and have authorised the sender domain in Firebase Auth
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: the Nano Banana 2 regeneration call is the largest single cost — one image per language. A captain who selects four languages spends about $0.12 just on flyer images.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this party" — gone in 60 seconds), how 30-day auto-deletion of neighbour names is wired (a scheduled Cloud Function whose last run is timestamped and visible to the captain).
**Documentation links:**
- AI Studio Build docs
- Gemini API multimodal, multilingual, grounded-search, Nano Banana 2 docs
- Firebase Auth, Firestore, Firebase Storage, Cloud Functions Scheduler docs
- Nominatim usage policy
**Accessibility:** same standards as the onboarding modal — focus trap, `Esc`, ARIA, restored focus.
**Behaviour:**
- Always available — single click from anywhere in the app.
- Tooltip on the `(i)` icon: "How this app is built".
- Mobile: opens as a full-screen sheet that slides up.
- Should be the most honest part of the app — never hand-wave service requirements; never say "AI" without naming the specific Gemini model and capability.
## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping)
- **Read last year's flyer (Gemini 3.5 Flash, medium thinking)** — typical flyer is 1-2 images, ~600 output tokens. ~$0.008/flyer.
- **Date-rollover suggestion (Gemini 3.5 Flash, low thinking)** — ~$0.0002/party.
- **Translate flyer text per language (Gemini 3.5 Flash, low thinking)** — typical flyer has ~150 source words, both directions counted. ~$0.0025/language.
- **Nano Banana 2 flyer regeneration** — ~$0.03/image, one image per language. **This is the dominant cost.** A captain who selects 4 languages: $0.12.
- **RSVP question set generation (Gemini 3.5 Flash, low thinking)** — ~$0.0005/party.
- **Door-knocker A5 generation (Gemini 3.5 Flash, low thinking, one per language)** — ~$0.001/language.
- **Permits + insurance summary (Gemini 3.5 Flash, medium thinking + grounded search)** — ~$0.004/party.
- **WhatsApp share message (Gemini 3.5 Flash, low thinking, one per language)** — ~$0.0003/language.
- **Expected per-party cost** — 1 language: ~$0.05. 2 languages: ~$0.08. 4 languages: ~$0.18. 6 languages (the multilingual-Crown-Heights case): ~$0.25.
- **Image storage:** Firebase Storage standard tier, ~$0.026/GB/month. A source flyer photograph at high res is ~3 MB; generated PDFs are ~1 MB each. A party that keeps 4 language flyers indefinitely uses ~7 MB ≈ essentially free.
- **Firestore reads/writes:** an RSVP page handles ~50-300 responses; reads on the count dashboard are ~$0.001/party.
- **Scheduled deletion function:** Cloud Run / Cloud Functions invocations are free below 2M/month. Stoop's deletion function runs once daily for the entire deployment, so the cost is effectively zero per party.
## 7. Design language
- **Mood:** A summer kitchen table on a Sunday night, three weeks before the party. The captain has the laminated flyer flat on the table, a cup of tea, a printed door-knocker list, and a felt-tip ready. Not a tech product. Not an events platform. The mood is "the neighbour you trust to handle it", scaled to one app.
- **Typography:** Display serif for flyer-content headings (Adobe Caslon Pro or Source Serif Pro) — the flyer is the hero, so its content gets the serif. Clean grotesque for app chrome (Inter or Geist). A handwriting-styled accent (sparingly) for the captain's own annotations and for the door-script preview — never for the parsed flyer text itself.
- **Palette:** Warm cream background `#F4EFE6` for the flyer-review canvas (the bone-paper colour of a real printed flyer), deep ink `#1B1714` for body text, bunting accent `#C0392B` (one of the classic flyer-bunting reds) and `#27AE60` (one of the classic flyer-bunting greens) used sparingly for confirmed-state ticks and "new" pills. Sepia accent `#7B4F2A` for date-stamps and small-print fields. A muted blue `#3A5773` for the captain's own annotations so they cannot be mistaken for the original flyer's ink. Borrowed from a paper noticeboard, not from SaaS design systems.
- **Imagery:** The photographed flyer is the hero. Never cropped tighter than the captain framed it. The regenerated flyer sits beside it at the same physical size. Period flyer illustrations — bunting, balloons, hand-drawn frames, clip-art piñatas, palm fronds — are honoured. The flyer is folk; the chrome respects it.
- **Hand-feel touches:** A barely-visible paper grain on the flyer-review canvas. The "compare" view slides the regenerated flyer in from the right with a thin shadow — like sliding a new flyer onto the table next to last year's. Hover on a postmark-or-stamp-style element reveals its parse data; never aggressively glow.
- **Spacing:** consistent 4-px base. Generous whitespace — the flyer needs room to breathe.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Flyer cards use 6; the language preview cards use 12; the welcome card uses 20.
- **Shadows:** subtle, layered, sepia-tinted. Avoid heavy drop-shadows.
- **Motion:** purposeful — entrance fades, hover lifts, the compare-slide. Respect `prefers-reduced-motion`. No bouncing splash animations. No theatrical hero animations. The "regenerating flyer" sequence — the source image becoming the new image — is the one place where motion carries meaning; respect reduced-motion by swapping rather than animating.
- **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Photograph last year's flyer to start" — never "Add your first item").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible names, dates, locations, dish names, sample flyers that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on the cultural-specific texture of the named blocks — Crown Heights Spanish + Haitian Creole + Yiddish; Coyoacán Spanish; Surulere Yoruba and Pidgin; Mumbai Marathi/Hindi/Gujarati — but never claim that a fictional captain is real or that a fictional flyer is from an actual block association.
- Tone: warm, direct, free of corporate language. This template is for a neighbour, not a company.
- Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'.
- Body copy: short paragraphs (2-4 sentences). Use lists where appropriate.
- Plain language. Avoid jargon — except where the captain already speaks the jargon (the SAPO acronym for the Brooklyn captain; the alcaldía term for the Mexico City captain; the CDA acronym for the Lagos captain).
- Where the app outputs AI-generated content, never label it as "AI says" — let it speak naturally. Use small uncertainty cues only where epistemic honesty requires them (a low-confidence date shows as a faintly underlined value; tapping it reveals the alternates the model considered).
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, dates, and snippets verbatim where helpful, or generate close variants that sit in the same world.
**Sample flyers (sidebar):**
- "Elm Street Block Party" (Crown Heights, Brooklyn) — a creased A4 flyer, English title with a Spanish + Haitian Creole + Yiddish subtitle ("Todos bienvenidos · Tout moun byenveni · אַלע זענען ווילקאָמען"). Last year: Saturday 13 July 2025, 1pm – 6pm, between Cedar & Birch, captain "Maria — 718-555-0142". Bunting illustration in the upper-right (hand-drawn, six flags, red-yellow-green-red-yellow-green). Volunteer credits in small print: "Thanks to Marcus, Rivka, Jean-Pierre, the Adeyemis, the Cohens, and everyone who set up the tables."
- "Posada de la Cuadra · Calle Higuera" (Coyoacán, Mexico City) — a hand-drawn A5 flyer in Spanish, "todas las familias bienvenidas" subtitle, with a small clip-art piñata in the upper-right. Last year: sábado 21 de diciembre 2024, 5pm – 10pm, en el patio interior, captain "Rosa — vecindad 4-B". Volunteer credits: "Gracias a doña Lupita, don Memo, las niñas del 7, y la cooperativa."
- "Iftar wa Wíìn Lẹ́yìn — Surulere Estate" (Lagos) — a coloured A5 flyer in Yoruba, English, and Pidgin, with a small palm-fronds motif in the upper-left and a green border. Last year: Saturday 5 April 2025, after Maghrib (6:45pm), in the courtyard, captain "Aunty Bisi — Flat 3B". Volunteer credits: "E sé pataki si gbogbo egbé wa, èyí pẹ̀lú àwọn Adeyemi, àwọn Okeke, àti gbogbo wa tó ṣe iṣẹ́."
- "Sahyadri Heights Courtyard Chai" (Mumbai, Andheri West) — a colour A4 flyer in Marathi, Hindi, and Gujarati, with a scalloped border and a small chai-cup illustration. Last year: शनिवार १८ ऑक्टोबर २०२५, दुपारी ४ ते रात्री ८ (Saturday 18 October 2025, 4pm – 8pm), Wing C courtyard, captain "Priya — 304-C". Volunteer credits in three scripts.
**Sample flyer in detail view (this is what the demo should show — the Elm Street flyer):**
- **Party title (verbatim):** "Elm Street Block Party"
- **Date written (verbatim):** "Saturday, July 13, 2025"
- **Date written ISO:** "2025-07-13"
- **Start / end time:** "1pm – 6pm" / "13:00" / "18:00"
- **Rain date (verbatim):** "Rain date: Sunday, July 14"
- **Rain date ISO:** "2025-07-14"
- **Location (verbatim):** "Elm Street between Cedar & Birch"
- **Location inferred type:** "street_closure"
- **Captain contact (verbatim):** "Maria — 718-555-0142"
- **RSVP instruction (verbatim):** "RSVP to Maria by 7/10"
- **Detected languages (4):**
- English (en-US, source language, Latin script)
- Spanish (es-419, Latin script)
- Haitian Creole (ht-HT, Latin script)
- Yiddish (yi, Hebrew script)
- **Visual elements (8):**
- title_text — "Elm Street Block Party", header, large, slab serif feel, deep green colour, preserve in regeneration
- subtitle_text — "Todos bienvenidos · Tout moun byenveni · אַלע זענען ווילקאָמען", header, medium, sans-serif, preserve
- date_text — "Saturday, July 13, 2025 · 1pm – 6pm", center, large, slab serif, preserve (text content updates)
- address_text — "Elm Street between Cedar & Birch", center, medium, preserve
- rain_date_text — "Rain date: Sunday, July 14", lower-left, small, preserve (text content updates)
- illustration — "hand-drawn bunting, six triangular flags, alternating red-yellow-green-red-yellow-green, swag from upper-left to upper-right of the header", upper-right, large, preserve
- rsvp_instruction_text — "RSVP to Maria by 7/10", lower-right, small, preserve (text content updates — now "RSVP at stoop.party/r/elm-st-2026" + QR code)
- volunteer_credits_text — "Thanks to Marcus, Rivka, Jean-Pierre, the Adeyemis, the Cohens, and everyone who set up the tables.", footer, tiny, preserve (carry forward as draft)
- **Reading confidence:** 0.91
- **Flagged for captain review:** none (the date and year were clearly legible on this sample)
**This year's plan (PartyPlan) for the Elm Street demo:**
- **This year's date ISO:** "2026-07-12" (Saturday closest to last year's 13 July)
- **This year's rain-date ISO:** "2026-07-13"
- **Start / end time:** "13:00" – "18:00"
- **Languages selected:** en-US, es-419, ht-HT, yi
- **RSVP questions (4 default + 1 added):**
- Name (required) — "Your name or household": "the Adeyemis" is a fine answer
- Household count (required) — "How many of you are coming?"
- Bringing dish (optional) — "What are you bringing? (totally fine to bring nothing.)"
- Dietary notes (optional, added by captain) — "Any allergies we should know about?"
- Free note (optional) — "Anything else you'd like Maria to know?"
- **Include new neighbour line:** true → "New to the block this year? Come anyway — no need to tell anyone."
- **Volunteer credits carried forward:** "Thanks to Marcus, Rivka, Jean-Pierre, the Adeyemis, the Cohens, and everyone who set up the tables." — captain edits before publishing
- **Delete responses after:** "2026-08-11" (party date + 30)
**Sample input artefacts (for the build to demonstrate):**
- A creased A4 photograph of the Elm Street flyer with kitchen-light reflection across the lower-right.
- A laminated A5 of the Posada de la Cuadra flyer photographed at an angle, with phone-camera glare on the date.
- A coloured A5 PDF of the Surulere Iftar flyer (WhatsApp-forwarded original).
- A colour A4 photograph of the Sahyadri Heights flyer with the Marathi headline at the top and the Gujarati subtitle in the lower-left.
**Sample voice copy:**
- Onboarding: "Photograph last year's flyer. We'll do this year's — with the bunting in the same corner."
- Processing: "Reading the flyer…" / "Spotting the date…" / "Picking up the bunting…" / "Translating to Spanish…" / "Re-laying out for this year…"
- Empty state: "This year's party is waiting for last year's flyer. Photograph the front to start."
- Error (couldn't read): "We couldn't make out the flyer in this shot. Want to try a clearer photo, or fill in the date by hand?"
- Save confirmation: "Saved. The Elm Street Block Party is set for Saturday 12 July 2026."
- New-neighbour line preview: "New to the block this year? Come anyway — no need to tell anyone."
- Permits unclear: "We couldn't find a clear answer for your jurisdiction. Ring your council — it's usually a 5-minute call."
- 30-day deletion reminder: "Your neighbours' names will be deleted on 11 August 2026. The flyer PDFs and the count will stay."
**Sample share messages (3-line, ready to paste):**
- WhatsApp / English:
- "Hi all — the Elm Street block party is back. Saturday 12 July, 1–6pm, between Cedar & Birch."
- "Bring something if you can — no pressure."
- "RSVP: stoop.party/r/elm-st-2026"
- WhatsApp / Spanish:
- "Hola a todos — vuelve la fiesta de Elm Street. Sábado 12 de julio, 1 a 6 de la tarde, entre Cedar y Birch."
- "Trae algo si puedes, sin presión."
- "Confirma: stoop.party/r/elm-st-2026"
- WhatsApp / Haitian Creole:
- "Bonjou tout moun — fèt blòk Elm Street la tounen. Samdi 12 jiyè, 1è jiska 6è, ant Cedar ak Birch."
- "Pote yon bagay si w kapab, pa gen presyon."
- "Reponn: stoop.party/r/elm-st-2026"
**Sample neighbour RSVP responses (for the live-count demo, 12 sample responses):**
- "Marcus" — 2 people — bringing drinks — "Excited to see you all"
- "the Adeyemis" — 6 people — bringing a main (jollof rice) — "We have a folding table you can borrow"
- "Rivka & David Cohen" — 4 people — bringing dessert (rugelach) — ""
- "Jean-Pierre" — 1 person — bringing nothing — "Happy to help set up"
- "Maria Sanchez" — 5 people — bringing a salad — "My daughter is bringing her speaker if anyone wants music"
- "the O'Sullivans" — 3 people — bringing drinks — ""
- "Yuki & Ben" — 2 people — bringing dessert (mochi) — "Can we bring our dog?"
- "Mr Patel" — 1 person — bringing a salad — "Looking forward to it"
- "the Adeyinkas" — 5 people — bringing a main (suya) — "Sister visiting from Lagos that weekend"
- "Annette" — 1 person — bringing dessert — "Will be there from 2"
- "the Kim family" — 4 people — bringing nothing — "Just glad to come"
- "the Garcías" — 4 people — bringing a salad — "Bringing a tablecloth too"
- Total confirmed households: 12 · Total expected attendees: 38 · 4 salads, 3 mains, 4 desserts, 2 drinks, 2 nothing.
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a hand holding a creased A4 flyer in late-afternoon kitchen light, with a phone half-in-frame ready to photograph it. Generate via Nano Banana 2 with a prompt emphasising "wooden kitchen table, warm afternoon light through a side window, hand of a woman in her forties, real worn paper, soft shadow under the flyer, no people-faces in frame".
- **App icon / wordmark:** "Stoop" set in the display serif. Slightly worn paper texture behind it. No icon — just type.
- **Empty-state illustration:** A simple line drawing of a single A4 flyer with a hand-drawn bunting illustration in the upper-right. Hand-drawn aesthetic, not a flat icon.
- **Demo flyer photographs:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "creased printed paper, hand-drawn bunting illustration, slight phone-camera angle, kitchen-light reflection on the laminated corner, no people in frame, no text legibility lower than 6-point". Each demo flyer should look photographed, not rendered.
- **Sample regenerated flyers:** the Elm Street flyer in English, Spanish, Haitian Creole, and Yiddish — same bunting illustration, same colour palette, same typographic feel, only the date and RSVP line different. Generated with Nano Banana 2 using the source flyer image as a reference.
- **Period-style frames:** noticeboard cork-board background for the "browse other captains' flyers" optional view (not in v1, but the assets exist for the v2 community gallery).
- **Stock fallbacks:** If image generation fails, fall back to the photographed sample flyer from `/public/samples/sample-flyer.jpg`. Never to a "📄" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock photography. Prompt for warmth, asymmetry, and slight imperfection — avoid the glossy 'AI render' look.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift.
- **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only.
### Build-time asset manifest (explicit specs)
Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs.
**Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id:
- `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted.
- `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence.
- `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible.
**Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill):
- `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt.
- `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline.
- `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails.
**Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint.
**Hard rules**
- Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error.
- No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code.
- Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`).
- File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and disabled states.
- Forms validate inline and show specific error messages (not "Invalid input"). The RSVP form's name field accepts "the Adeyemis" without complaint.
- Loading states use skeletons that match the eventual layout, not spinners.
- Empty states explain the next action with a button whose label fits THIS app's domain: "Photograph last year's flyer", "Upload a PDF", "Send the WhatsApp message" — never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- All AI-generated content streams in token-by-token where supported, with a clear "thinking…" indicator before content starts arriving. The Nano Banana 2 regeneration shows a "drawing the bunting…" / "laying out the date…" honest sequence.
- If an AI call fails, show a calm, specific error ("We couldn't read this flyer — try a clearer photo, or fill in the date by hand") and offer retry.
- Low-confidence date fields show as faintly underlined; tapping reveals the alternates the model considered (e.g. "2024 or 2025?").
- The compare-slide between last year's flyer and this year's takes 600 ms with `prefers-reduced-motion` falling back to instant swap.
- The neighbour-facing RSVP page is a single screen, no scroll on mobile, no animations beyond a confirmation tick when the form submits.
## 11. Tech & responsive requirements
- **File downloads on Safari / Firefox:** when offering local-disk save of any export (PDF, CSV, MP3, ZIP, JSON, image), fall back to `` with a blob URL — the File System Access API (`showSaveFilePicker()`) is Chromium-only. Detect with `'showSaveFilePicker' in window`; otherwise use the anchor-download path.
- **Stack:** React + TypeScript + Tailwind CSS. Functional components + hooks. Use Shadcn UI primitives where appropriate.
- **Build runtime:** AI Studio Build — full-stack with Cloud Run server-side functions. All Gemini API calls happen server-side; API key lives in Secrets Manager, never in client bundle.
- **Model selection:** explicitly pin `gemini-3.5-flash` for flyer parse and translation; `gemini-3.5-flash` for date-rollover, RSVP question generation, door-knocker, permits (with grounded search), share message; `gemini-3.1-flash-image` for the regenerated flyer. Set `thinkingLevel` explicitly per call. Omit `thinkingConfig` on the image-generation call.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed flyers on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it (user-config required); magic-link email as fallback for "send me my parties" recovery (sender-domain authorisation required).
- **Storage:** Firebase Storage for source flyer photos + generated PDFs. Pre-signed URLs only.
- **Scheduled deletion:** Cloud Functions Scheduler runs daily at 02:00 in the deployer's timezone; deletes `rsvp_responses`, `door_knocker_lists`, and `parties.guest_list` for parties where `delete_responses_after` < today.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. The captain workflow is mobile-first (capturing flyers at the kitchen table); the neighbour-facing RSVP page is even more mobile-first (the median visitor opens the link from a WhatsApp message on a mid-range Android).
- Use `clamp()` for fluid typography. Prefer container queries over media queries for component-level responsiveness.
- Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS.
- Zero horizontal overflow at any width. Zero layout shift on load.
- Persist captain data in Firestore. Use real-time listeners on the count dashboard.
- Optimistic UI on writes; reconcile on response.
- Flyer capture flow uses the Web Camera API with fixed focus/exposure where supported; falls back to native camera otherwise.
- **iOS Safari gotchas (graceful degradation):** camera permission does NOT persist across page reloads on iOS — re-request on every capture; backgrounded tabs pause `getUserMedia` — re-acquire the stream on `visibilitychange`; on Low Power Mode iOS may degrade resolution — always offer `` as a fallback so a flyer photo still saves when WebRTC is denied.
- The neighbour-facing RSVP page is server-side rendered, includes `noindex` meta, and works without JavaScript for the basic name-and-count submission.
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`.
- All interactive controls reachable by keyboard with a visible focus ring.
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components.
- All images have meaningful `alt` text. The photographed flyer has `alt` describing the artefact ("photograph of a creased A4 flyer for the Elm Street Block Party, with a hand-drawn bunting illustration in the upper-right and the date Saturday 13 July 2025 in the center").
- Form fields have associated `