================ 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.
---
# New to Town
## 1. Project
**New to Town** is a first-weekend city cheat sheet for families who
have just arrived. The user pastes the name of the city they have
landed in, the country they came from, the languages they speak, and
the ten urgent practical questions they have not been able to answer
in their phrasebook — when does rubbish get collected at their new
address, where is the nearest pharmacy that will fill their child's
prescription, how does one register with the local authorities, where
to get a SIM card without a local bank account, which supermarket is
open on Sundays — and the app produces a single printable A4 cheat
sheet in two languages, with addresses, opening hours, the bus route
to the registration office, and the exact words to say at the counter
when the user does not yet speak the language.
This is the kind of app a Brazilian mother builds on her second
morning in Lisbon, jet-lagged with two children at the kitchen table
of an unfamiliar rental, after the host has left and the wifi password
is the only thing she is sure of — because the kids need to be enrolled
on Monday, the asthma inhaler runs out on Wednesday, and the
rubbish-truck schedule taped inside the kitchen cabinet is in Portuguese
she can read but not yet trust. It is also the kind of app a Syrian
father builds on his first Saturday in Berlin, after eighteen months
of waiting, with a paper folder of documents in Arabic, three children
asleep on a single mattress, and a list of ten things his caseworker
told him to do "this week" that he has not been able to start because
he does not know where the buildings are. Same shape of moment,
different continent, different paperwork.
The single demo that proves the magic: the user types the city ("Lisbon"),
their country of origin ("Brazil"), the languages they read ("Portuguese,
some English"), the address of the place they are staying, and pastes
their ten questions in a single block — one sentence each, in any of
the languages they speak. In under sixty seconds the app produces a
one-page printable cheat sheet: ten answers grouped into Today / This
Week / This Month, each answer pinned to a specific address with
opening hours, the bus or metro line that gets there, the exact local
phrase to say at the counter, a "what to bring" list ("passport, lease,
your child's prescription in its original box"), and at the bottom of
each answer, a citation URL and a date stamp ("as of 28 May 2026 — confirm
on arrival; civic info can change").
And in the harder cases — asylum claims, missing residency status,
prescription medicine that is regulated differently in the new country,
a child with a condition that requires immediate continuity of care —
the app still answers the questions it can answer, names clearly the
questions only a caseworker should answer, and prints alongside the
cheat sheet a short directory of the local free legal-aid clinics,
the migrant-support NGO with after-hours staff, the public hospital's
foreign-patient desk, and the consulate of the user's country of
origin — with their opening hours, their working languages, and the
bus route from where the family is staying.
**Tagline:** _Turn ten urgent questions into one printable cheat sheet — in any city, in any language, with every address, opening hour, and local phrase you need to walk in the door._
## 2. Target audience
- Newly-arrived migrant families in their first 72 hours in a city — Brazilian families in Lisbon and Porto, Filipino families in Toronto and Vancouver, Syrian and Afghan families in Berlin and Hamburg, Venezuelan families in Madrid and Bogotá, Ukrainian families in Warsaw and Prague
- International students at the start of their first semester abroad — Indian, Nigerian, Vietnamese, Chinese, Mexican students arriving in cities they have never visited
- Refugee-resettlement caseworkers preparing onboarding packets for incoming families before the family lands, with one cheat sheet per case household
- Volunteers at parish-based and NGO-based welcome desks who run the same conversation with new arrivals every week and want a printable take-home for each family
- Returning citizens — Greeks who grew up in Australia and have just moved back, Iranian-Americans returning to Tehran for a family year, Filipinos returning to a province they left as children, second-generation diaspora moving "back" to a city they only know from holidays
- Expatriate professionals on a corporate relocation whose company HR covers the visa but not the school enrolment, the doctor registration, or the rubbish-day schedule
- Older parents joining adult children abroad on family-reunification visas — the user is often the adult child, building the sheet on behalf of a parent who does not yet read the local script
- Domestic-violence survivors relocating to a new city within their own country who need an unfamiliar neighbourhood's basics quickly and discreetly
- City-government integration offices and public libraries who maintain printable welcome packets and want a template they can adapt per neighbourhood
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **Grounded answers, not guesses** — every fact in the cheat sheet comes from a cited source: the municipal website, the public-transit authority, the national health-service page, the consulate, the registration-office page. Gemini 3.5 Flash with `google_search` grounding does the heavy lifting; the citation URL appears on the printed sheet itself, not buried in a tooltip. No citation, no answer.
- **Date-stamped, with a freshness warning** — every fact carries an "as of {date}" stamp. The printed sheet says explicitly that civic information changes and asks the user to confirm at the counter. The app never pretends a six-month-old opening hour is today's.
- **Two languages, side by side** — the cheat sheet is bilingual by default: the local language of the city on one side, the user's strongest reading language on the other. Same paragraph, same row. The user can hand the printed sheet to a bus driver and point.
- **The exact phrase to say at the counter** — under each answer, three short scripted sentences in the local language with phonetic respelling: how to ask for the form, how to ask what document to bring, how to ask for help in English (or in your strongest language). Composed for a beginner; reviewed by a fluent speaker.
- **Today / This Week / This Month** — questions are sorted by urgency, not by topic. The asthma inhaler is Today. The residence registration is This Week. The driving-licence transfer is This Month. The user does not have to triage a checklist on day one.
- **Honest about what we cannot answer** — visa categories, asylum status, prescription-substitution decisions, and anything that requires a regulated professional are flagged with a clear "this is a caseworker / lawyer / pharmacist question" and routed to a named local service with a citation. The app does not pretend to be a lawyer.
- **One sheet for the fridge, one for the wallet** — the export is two formats: an A4 (or US Letter) printable for the fridge, and a smaller four-fold pocket card for the parent who will be at the counter. Same content, different layout.
- **Works offline once printed** — the printed sheet is the artefact. The family does not need wifi at the bus stop, at the school gate, at the pharmacy counter. The phone may not even have a working SIM yet.
## 4. Features to build
- A single primary input — a long textarea where the user pastes any number of questions in any language, one per line or in a paragraph; the app extracts and reorders them
- Smart context capture — city, neighbourhood / postcode, country of origin, languages the user reads, ages of children (if any), any specific health needs the user wants to flag (asthma, diabetes, mobility, mental-health continuity)
- Question classifier — each pasted question is classified into a topic (housing, registration, transport, health, education, money, food, communication, legal, safety, culture) and assigned an urgency tier (Today / This Week / This Month) using thinking-level low
- Grounded search per question — Gemini 3.5 Flash with `google_search` resolves the question against current municipal, transit, and national-service pages; citations are captured from `groundingMetadata.groundingChunks[].web.uri`
- Address resolution — every answer that points to a physical place captures the verbatim street address, opening hours as quoted on the source, and the transit route from the user's address
- Bilingual rendering — the cheat sheet is generated in the city's official language and in the user's strongest reading language, paragraph for paragraph
- "Exact words to say" block — three short scripted sentences per answer in the local language, with a beginner-friendly phonetic respelling (not IPA — "shay-gah-dush" style), composed in the appropriate register (polite, simple, common)
- "What to bring" checklist — concrete documents and items required at each destination, listed as a tick-able checkbox row
- Date stamp on every fact — the response object carries `as_of_iso` (today's date in ISO) and the source's last-updated date when extractable
- Freshness flag — answers older than a year on their source page, or sources that are blogs / forums rather than official pages, are visually marked "verify on arrival" and never used as the sole source
- Caseworker / lawyer routing — questions classified as `requires_professional` route to a local-services directory and do not receive a confident answer
- Printable A4 cheat sheet — bilingual two-column layout, ten answers, citations and date stamps in the footer, designed to print clean in black-and-white
- Pocket card — four-fold pocket-sized layout with the four most urgent items, the local words to ask for help, and the user's home address printed for show-the-bus-driver use
- Offline-friendly — once generated, the cheat sheet is downloadable as a print-ready PDF and as a self-contained HTML file the user can keep on a phone with no signal
- Save and revise — the user can edit any answer (add a note from a neighbour, correct an address) and re-export; the citation and date stamp on edited answers carry a "user-corrected" badge
- Share with family / caseworker — invite-link share so the spouse, adult child, or caseworker can add questions and confirm answers
- Multiple cities, multiple households — a single user can keep cheat sheets for "Lisbon (us)" and "Lisbon (my parents joining in August)" with different needs
- Voice input — the parent who is fluent in their own language but not at a keyboard speaks the ten questions; speech-to-text drops them into the textarea
- Print-from-a-cybercafé fallback — explicit instructions on the export screen for the case where the user has no printer at home and will print at a public library or photocopy shop ("save the PDF to your phone, open at the counter, ask for one-sided A4")
## 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)
- **Search grounding** (`google_search`, Gemini 3.5 Flash) — the load-bearing capability. Every civic fact in the cheat sheet (an address, an opening hour, a bus line, a registration requirement) comes from a current grounded source, with the citation captured from `response.groundingMetadata.groundingChunks[].web.uri`. Without grounded search, this template would be dangerous — stale civic information sends families to closed offices and wrong addresses.
- **Long context (1M tokens)** (Gemini 3.5 Flash) — the model reads up to 30+ fetched municipal pages, transit timetables, and national-service pages in a single call to compose one cheat sheet. **Guardrail**: cap the per-cheat-sheet context at ~600k input tokens (typical: 30 questions × 10 pages × ~2k tokens/page ≈ 600k). For larger contexts (a city directory generation, an NGO building a 50-question sheet), chunk by urgency tier — process Today / This Week / This Month as three separate grounded calls and merge.
- **Multilingual output** (Gemini 3.5 Flash) — the cheat sheet is rendered in two languages simultaneously, paragraph by paragraph, in the city's official language and the user's strongest reading language. The model handles Portuguese, Spanish, French, German, Italian, English, Polish, Ukrainian, Russian, Czech, Greek, Turkish, Arabic, Persian, Urdu, Hindi, Bengali, Tamil, Punjabi (Gurmukhi and Shahmukhi), Tagalog, Vietnamese, Khmer, Thai, Mandarin and Cantonese, Korean, Japanese, Swahili, Amharic, Tigrinya, Somali, Yoruba, Igbo, and Hausa.
- **Multilingual phonetic respelling** (Gemini 3.5 Flash) — the "exact words to say at the counter" block is composed in the local language with a beginner-friendly respelling tuned to the user's source language's phonology. Brazilian Portuguese speaker arriving in Lisbon gets European-Portuguese pronunciation respelled in Brazilian-Portuguese conventions ("desculpe" → "desh-KOOL-puh", not the Brazilian "djees-COOL-pee"). A Tamil-speaking family arriving in Toronto gets English respelled in Tamil-phonology conventions.
- **Structured output / JSON Schema** — the response matches the `CheatSheet` schema below for the non-grounded composition pass. Note: the *grounded* per-question call below uses `google_search` and therefore cannot use `responseSchema` in the same call (the two are mutually exclusive). For that call, instruct the model to emit JSON in the text body and parse server-side, and read citation URLs from `groundingMetadata.groundingChunks[].web.uri` rather than the JSON body.
- **Thinking levels** — `medium` for the per-question grounded answer (the model must judge source authority, reconcile contradictory pages, and decide when to flag a question as "requires_professional"). `low` for question classification, urgency tiering, phonetic respelling, and the bilingual rendering pass. Surface `thoughtSummary` only on the small "(i) how we found this" link beside each answer.
- **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — optional. The user can tap any "exact words to say" block and hear the local language phrase spoken at counter-pace, in the voice whose `languageCode` matches the local language. Useful at the bus stop when the user wants to rehearse before walking in.
### Backend services
- **Auth — Optional for v1, recommended.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). The user can generate one cheat sheet without signing in (stored in `localStorage`); signing in unlocks save, share, and multi-household. **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 invite-link share) also requires the sender domain to be authorised in Firebase Auth. Anonymous use is supported deliberately because some users (asylum seekers, survivors of domestic violence) have good reasons not to sign in.
- **Database — Required.** Firestore for `users`, `households`, `cheat_sheets`, `questions`, `answers`, `citations`, `user_corrections`, `household_members`. Anonymous-mode data lives only in `localStorage` and is never sent to Firestore.
- **File storage — Optional.** Firebase Storage for the rendered PDF / HTML exports if the user wants them cloud-saved. **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console before first export-to-cloud. Pre-signed URLs only.
- **Email — Required (transactional).** Invite-link emails for share-with-family / share-with-caseworker (Firebase Auth magic links). Sender domain must be authorised in Firebase Auth.
- **Payments — Not needed for v1.** Free for personal and caseworker use. A future "city directory" tier for NGOs that want to maintain a region-wide template would be priced separately.
- **External APIs:** Gemini API for all intelligence (grounding included — no separate search key required). Optional: a public-transit routing API (Google Maps Routes, Citymapper, or the city's open-data transit feed) for the bus-route segment; the template ships with a fallback that names the bus line and stop but does not compute door-to-door routing.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, optional Maps Routes key) lives in environment variables — never in the 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 cheat sheet' inside the UI · explicit opt-in for any analytics · the user's questions about their move 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) · anonymous-mode data lives only on the device and is never persisted to any backend.
**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 for non-grounded calls. 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 calls cannot use `responseSchema`** — the two are mutually exclusive in a single Gemini call. On grounded calls, ask the model to emit JSON in the text body, parse server-side, and read citation URLs from `groundingMetadata.groundingChunks[].web.uri`, never from the JSON.
8. **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;
}
```
9. **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 |
|------|-------|---------------|---------------|
| Classify + urgency-tier pasted questions | `gemini-3.5-flash` | low | (none) |
| Answer one question with grounded civic search | `gemini-3.5-flash` | medium | `google_search` grounding (no `responseSchema` on this call — see note) |
| Compose bilingual cheat-sheet output → `CheatSheet` schema | `gemini-3.5-flash` | low | (none) |
| Generate "exact words to say" + phonetic respelling | `gemini-3.5-flash` | low | (none) |
| Flag a question as `requires_professional` + route | `gemini-3.5-flash` | low | (none) |
| Speak the local-language phrase | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Compose pocket-card 4-fold layout text | `gemini-3.5-flash` | low | (none) |
*Note for builders:* on TTS calls, 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 grounded answer call, do NOT pass `responseSchema` — the API rejects calls that combine grounding with structured output. Parse the JSON from the response text body, and read citation URLs from `groundingMetadata.groundingChunks[].web.uri`.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const UrgencyTier = z.enum([
"today", // first 24-48 hours: prescriptions, food, basic safety
"this_week", // days 3-7: registration, school, SIM, bank
"this_month", // weeks 2-4: driving licence, insurance, professional registration
"later", // beyond month 1: longer-term integration, recognition of credentials
]);
const Topic = z.enum([
"housing",
"registration", // residence permit, town hall registration
"transport",
"health",
"education", // school enrolment, language classes
"money", // banking, currency, remittance
"food", // halal/kosher/vegetarian shopping, prices, market days
"communication", // SIM, internet, postal
"legal",
"safety", // emergency numbers, neighbourhood info
"culture", // greetings, holidays, what to bring to a host
"work", // tax number, recognition of qualifications
"childcare",
"religion", // nearest place of worship in the user's faith
"other",
]);
const Citation = z.object({
url: z.string().url(),
source_label: z.string(), // "Câmara Municipal de Lisboa", "Berliner Bürgeramt"
source_type: z.enum([
"municipal_official",
"national_government",
"national_health_service",
"transit_authority",
"consulate",
"ngo_recognised",
"community_forum", // lower trust — never sole source
"blog", // lower trust — never sole source
"other",
]),
source_language: z.string(), // BCP-47, "pt-PT"
source_last_updated_iso: z.string().nullable(), // when extractable from the page
fetched_at_iso: z.string(), // when WE fetched it
excerpt_quoted: z.string(), // the verbatim sentence we leaned on
});
const PhraseToSay = z.object({
intent: z.string(), // "ask for the registration form"
local_language_text: z.string(), // "Posso pedir o impresso de registo, por favor?"
phonetic_respelling: z.string(), // beginner-friendly, NOT IPA
user_language_translation: z.string(), // "May I ask for the registration form, please?"
register: z.enum(["polite_formal", "polite_neutral", "casual"]),
});
const WhatToBring = z.object({
item: z.string(), // "child's prescription in its original box"
why: z.string().nullable(), // "so the pharmacist can confirm the active ingredient"
optional: z.boolean(),
});
const TransitDirection = z.object({
mode: z.enum(["walk", "bus", "tram", "metro", "train", "ferry", "bike", "car_share", "taxi", "other"]),
line_or_route_label: z.string(), // "Bus 706", "Metro Linha Amarela"
from_stop: z.string(), // "Estação do Oriente"
to_stop: z.string(), // "Loja do Cidadão das Laranjeiras"
approx_minutes: z.number().min(0).max(240),
approx_cost_local_currency: z.string().nullable(), // "€1.85"
notes: z.string().nullable(),
});
const Address = z.object({
name_local: z.string(), // "Loja do Cidadão das Laranjeiras"
street_verbatim: z.string(), // as written on the source page
postcode: z.string().nullable(),
city: z.string(),
country: z.string(),
opening_hours_verbatim: z.string(), // quoted from the source page
opening_hours_machine: z.string().nullable(), // ISO weekday spec if confidently extractable
phone: z.string().nullable(),
notes: z.string().nullable(), // "appointment-only — book online first"
});
const Answer = z.object({
question_verbatim: z.string(), // the user's question as pasted
question_language: z.string(), // BCP-47
topic: Topic,
urgency: UrgencyTier,
short_answer_local_lang: z.string(), // 1-2 sentences, in the city's language
short_answer_user_lang: z.string(), // 1-2 sentences, in the user's strongest reading language
long_explanation_local_lang: z.string().nullable(),
long_explanation_user_lang: z.string().nullable(),
primary_address: Address.nullable(),
alternative_addresses: z.array(Address),
transit_from_user_address: z.array(TransitDirection),
what_to_bring: z.array(WhatToBring),
phrases_to_say: z.array(PhraseToSay),
citations: z.array(Citation),
as_of_iso: z.string(), // YYYY-MM-DD — when this answer was composed
freshness_warning: z.boolean(), // true if any source is older than 12 months or low-trust
requires_professional: z.boolean(), // if true, short_answer routes to caseworker / lawyer
professional_routing: z.object({
kind: z.enum([
"free_legal_aid",
"migrant_support_ngo",
"consulate",
"public_hospital_foreign_patient_desk",
"school_enrolment_office",
"social_services",
"other_professional",
]),
name: z.string(),
address: Address.nullable(),
languages_supported: z.array(z.string()),
notes: z.string().nullable(),
}).nullable(),
confidence: z.number().min(0).max(1),
flagged_for_user_review: z.array(z.object({
field_path: z.string(),
reason: z.string(),
})),
});
const CheatSheet = z.object({
cheat_sheet_id: z.string(),
generated_at_iso: z.string(),
city: z.string(),
city_country: z.string(),
user_country_of_origin: z.string(),
user_languages: z.array(z.string()), // BCP-47 list, strongest first
city_official_language: z.string(), // BCP-47
household_address_at_time: z.string().nullable(),
household_members_summary: z.string().nullable(), // "two adults, two children ages 6 and 9"
specific_health_notes: z.array(z.string()), // "child uses asthma inhaler (salbutamol)"
answers: z.array(Answer),
general_freshness_warning: z.string(), // standard line about confirming on arrival
emergency_numbers_local: z.array(z.object({
label: z.string(), // "Ambulance / fire / police"
number: z.string(), // "112"
citation: Citation,
})),
consulate_of_origin: z.object({
country: z.string(),
city_of_consulate: z.string(),
address: Address.nullable(),
languages_supported: z.array(z.string()),
citation: Citation,
}).nullable(),
});
type CheatSheet = z.infer;
```
### Common failure modes (and how to avoid them)
- The model invents an address it remembers from training data. Pin: every address MUST appear in a citation excerpt. If `primary_address.street_verbatim` does not appear verbatim in at least one `citations[i].excerpt_quoted`, reject the answer server-side and re-prompt with "this address was not in any cited source — re-search".
- The model omits the date stamp. Pin: `as_of_iso` is required for every answer. Reject responses where it is missing.
- The model uses a blog or forum as the sole source for a critical answer (registration deadlines, prescription policy). Pin: for `requires_professional` topics and for any answer where `freshness_warning` is true, at least one citation must be `source_type: municipal_official | national_government | national_health_service | transit_authority | consulate | ngo_recognised`.
- The model confidently answers an asylum-status question. Pin: the system instruction enumerates topics that ALWAYS set `requires_professional: true` (asylum claims, visa-category disputes, prescription substitution, custody arrangements, tax residence status). The model is instructed to route, not answer.
- The model translates a proper noun (the name of an office, a metro line, a hospital). Pin: official names stay verbatim in the local language. The bilingual rendering shows the local name on both sides, with the user-language column adding a parenthetical gloss on first occurrence ("Loja do Cidadão (Citizen's Service Centre)").
- The phonetic respelling uses IPA. Pin: "use respelling conventions familiar to a beginner speaker of the user's strongest language; never IPA, never strict linguistic notation".
- The model uses Brazilian Portuguese pronunciation respelling for a user arriving in Lisbon. Pin: respelling must reflect the city's *local* pronunciation (European Portuguese in Lisbon, Quebec French in Montréal, Egyptian Arabic in Cairo, Mandarin in Taipei).
- The model gives a six-month-old opening hour without flagging it. Pin: if `source_last_updated_iso` is older than 12 months OR cannot be extracted, set `freshness_warning: true` and prepend the local-language line that translates to "confirm on arrival — civic info can change".
- The model batches all ten questions into one grounded call and the citations get scrambled. Pin: ONE grounded call per question. The orchestration layer runs the per-question grounded calls in parallel (with a concurrency cap) and assembles into the `CheatSheet` schema in a final non-grounded composition pass.
- The model misclassifies "where do I register the children for school?" as `housing`. Pin: the classifier system instruction enumerates 30+ examples per topic.
- The model writes the cheat sheet only in English when the user reads Tigrinya. Pin: `user_languages[0]` is the user's strongest reading language; the bilingual rendering uses it as the second column. English is never assumed.
### Negative constraints (hard rules)
- Do NOT answer asylum, visa-category, custody, tax-residence, or prescription-substitution questions with a confident factual answer. These are `requires_professional: true` by enumeration. Route, do not advise.
- Do NOT cite a blog, forum, or social-media post as the sole source for a critical answer. Community forums and blogs may appear as *supporting* citations, never sole.
- Do NOT invent addresses, phone numbers, opening hours, or transit routes. If a field cannot be confirmed from a citation, leave it null and add a `flagged_for_user_review` entry.
- Do NOT modernise or translate official place names ("Loja do Cidadão" stays "Loja do Cidadão"; "Bürgeramt" stays "Bürgeramt"). Add a parenthetical gloss on first occurrence only.
- Do NOT use IPA in the phonetic respelling. Beginner-friendly only.
- Do NOT omit the date stamp. Every answer carries `as_of_iso`.
- Do NOT use the user's pasted questions to train or fine-tune any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms.
- Do NOT publish or share the user's cheat sheet. Cheat sheets are private to the user and explicitly invited household / caseworker members. Anonymous-mode cheat sheets never leave the device.
- Do NOT assume English. The bilingual default is local-language + user's strongest reading language; English appears only if the user lists it.
- Do NOT collapse the freshness warning into a single global line and hide the per-answer warnings — every answer carries its own freshness state, visible on the printed sheet.
- Do NOT auto-share with a caseworker without explicit consent — share is opt-in, per cheat sheet.
### 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: Classify + urgency-tier pasted questions
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a block of text containing one or more questions from a
newly-arrived family in a city they have just moved to. The questions
may be in any language — Portuguese, Spanish, French, German, English,
Polish, Ukrainian, Russian, Arabic, Persian, Urdu, Hindi, Bengali,
Tamil, Tagalog, Vietnamese, Mandarin, Cantonese, Korean, Amharic,
Tigrinya, Somali, Swahili, Yoruba, Hausa, and others. The questions
may be mixed languages within a single block.
Your task: for each distinct question, return:
- question_verbatim (the user's wording, exactly)
- question_language (BCP-47)
- topic (from the closed Topic enum)
- urgency (today | this_week | this_month | later)
- one-sentence rationale for the urgency call
Hard rules:
- Preserve the user's verbatim phrasing in `question_verbatim`. Do not
paraphrase. Do not correct typos. Do not "polish".
- Split compound questions only when they ask about clearly different
topics. "Where do I register and where do I get a SIM?" is two.
"Where do I register and what do I bring?" is ONE registration
question with a what-to-bring sub-component.
- "Today" is the first 24-48 hours and includes anything safety-,
food-, or active-prescription-related, plus anything required for
the family to sleep safely tonight.
- "This week" is days 3-7 and includes residence registration, school
enrolment, SIM card, opening a basic bank account, finding the
nearest pharmacy and supermarket, rubbish-and-recycling schedule.
- "This month" is weeks 2-4 and includes driving-licence transfer,
full health-insurance enrolment, professional-qualification
recognition, longer-term housing.
- "Later" is everything beyond month one — credential recognition
appeals, longer-term integration.
- Health continuity (an existing prescription, an ongoing condition,
a pregnancy due-date within the month) is ALWAYS at least
this_week and frequently today.
- Anything involving a child's safety, schooling, or healthcare
trends one tier MORE urgent than the same question would for an
adult.
- If you are unsure between two adjacent urgency tiers, pick the
more urgent one. Erring conservative is correct here.
Output ONLY a JSON array of classified questions, one object per
question, matching the schema above. No commentary.
```
---
### Call: Answer one question with grounded civic search
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: `google_search` grounding
```
You are answering ONE question from a newly-arrived family in a city
they have just moved to. The family pastes their address, their
country of origin, their languages, and one question. Your task: use
grounded search to compose ONE Answer object (as JSON in the text
body — do NOT use responseSchema on this call) that they can rely
on to walk into the right office tomorrow morning.
Today's date is {{TODAY_ISO}}. Every fact you cite must be confirmed
against a source you searched today.
Prioritise sources in this order:
1. municipal official sites (Câmara Municipal, Bürgeramt, City Hall)
2. national government sites (.gov.pt, .gov.de, gc.ca, .gov.br)
3. national health services (SNS in Portugal, NHS in UK, AOK in Germany)
4. transit authorities (Carris, BVG, TTC, Metro de Madrid)
5. consulates of the user's country of origin
6. recognised NGOs serving migrants in the city (Caritas, IOM, Refugee
Council, JIAS)
7. community forums and blogs — supporting citations only, NEVER sole
source for a critical answer
For each answer:
- Quote a verbatim sentence from the source in `excerpt_quoted`.
- Capture the source's last-updated date in `source_last_updated_iso`
if visible on the page; null otherwise.
- Capture today's date in `fetched_at_iso` and propagate to
`as_of_iso` on the parent Answer.
- If any source you used is older than 12 months, OR if you could
not find a municipal/national/transit/consulate/NGO source and had
to fall back to a blog or forum, set `freshness_warning: true` and
prepend the standard freshness sentence in BOTH the local and the
user's language in `short_answer_*`.
For the address: capture the street EXACTLY as the source page
prints it (with abbreviations, with diacritics, with the postcode).
Do NOT clean it up. Do NOT translate it. The family will copy it to
a taxi driver.
For opening hours: capture `opening_hours_verbatim` from the source
page exactly. Only populate `opening_hours_machine` if you are
confident the schedule is unambiguous.
For transit: use the city's transit authority site (the user's address
is provided). Name the LINE and the STOP, not turn-by-turn directions.
If you cannot confidently produce a route, leave
`transit_from_user_address` empty and add a `flagged_for_user_review`
entry.
For phrases_to_say: compose THREE phrases in the city's local
language, register = polite_formal, register-appropriate for a
counter conversation. Do NOT translate them in this call —
respelling and translation happen in the next call.
Hard rules — `requires_professional: true` for any of:
- asylum claims, refugee-status determinations
- visa-category disputes, status changes
- custody arrangements, child-protection
- tax-residence determinations
- prescription substitutions (only a local pharmacist can decide if
drug X in country A is equivalent to drug Y in country B)
- school placement appeals
- domestic-violence safety planning
For these, set `requires_professional: true`, leave the factual
answer fields concise and route-focused, and populate
`professional_routing` with a named local service from your grounded
search.
Hard rules — NEVER invent:
- If a field cannot be confirmed from a citation, leave it null and
add a `flagged_for_user_review` entry naming the field and the
reason ("opening hours not visible on linked page; recommend the
user call the listed number before visiting").
- Never paste an address you remember from training. Every address
must appear verbatim in at least one citation excerpt.
- Never invent a phone number.
Output: a single JSON object matching the Answer schema, embedded
in the text body. Citations URLs will be read by the server from
`groundingMetadata.groundingChunks[].web.uri` and merged into the
`citations[]` array — do NOT paste URLs in the JSON body; you will
hallucinate them. Cite which grounding chunk supported which field
by using its numeric index in your `excerpt_quoted` references.
No commentary outside the JSON.
```
---
### Call: Compose bilingual cheat-sheet output → `CheatSheet` schema
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a JSON object containing the household context and an
array of fully-answered Answer objects (each one already grounded
and cited from the previous call). Your task: produce a final
CheatSheet object, with both the local language and the user's
strongest reading language populated for every short_answer and
long_explanation field, ready to render.
Today's date is {{TODAY_ISO}}.
Hard rules:
- For each Answer: produce `short_answer_local_lang` in the city's
official language and `short_answer_user_lang` in
user_languages[0]. Each is 1-2 sentences. Same content, same
facts, paragraph for paragraph.
- Do NOT translate proper nouns. Office names, metro lines,
hospital names, neighbourhood names stay verbatim in the city's
language on both sides. Add a parenthetical gloss in the
user-language column on first occurrence only.
- Do NOT translate the verbatim address. Both columns show the
street the same way (the user shows it to a taxi driver).
- Do NOT polish the citation excerpts. They are quoted evidence.
- For freshness-warned answers: prepend the standard freshness
sentence in both languages. Local-language sentence first; user
language sentence second. Example PT: "Confirme à chegada — a
informação cívica pode mudar." EN: "Please confirm on arrival —
civic info can change."
- Sort `answers[]` by urgency tier: today first, then this_week,
then this_month, then later. Within a tier, sort by topic
alphabetically.
- Compose `general_freshness_warning` in both the local language
and user_languages[0], joined by " — " and printed once at the
top of the sheet.
- Populate `emergency_numbers_local` from the per-question
citations where the local emergency number was mentioned, or as
a separate confirmed entry. Always include the equivalent of
ambulance, fire, police (often the single Europe-wide 112).
- Populate `consulate_of_origin` if a citation was found; null
otherwise — do NOT guess the consulate address.
- Carry through every `flagged_for_user_review` from upstream
Answer objects.
Output: the full CheatSheet JSON object matching the schema. No
commentary.
```
---
### Call: Generate "exact words to say" + phonetic respelling
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive: (a) a phrase in the city's local language composed in
the previous step, (b) the user's strongest reading language. Your
task: produce a beginner-friendly phonetic respelling that a
non-speaker of the local language can read aloud and be understood
at a counter.
Hard rules:
- Use the city's *local* pronunciation, not the standard / textbook
pronunciation. Lisbon Portuguese has reduced unstressed vowels
("desculpe" ≈ "desh-KOOL-puh") that differ markedly from
Brazilian Portuguese ("djees-COOL-pee"). Quebec French is not
Parisian French. Egyptian Arabic is not MSA. Mandarin in Taipei
has tone realisations that differ from Beijing. Use what the
user will actually hear at the counter.
- Use respelling conventions familiar to a beginner reader of
user_languages[0]. A Brazilian-Portuguese reader arriving in
Lisbon should see Brazilian-Portuguese-style respelling of
European-Portuguese phonemes. A Tamil reader arriving in Toronto
should see English phrases respelled in Tamil-phonology
conventions.
- NEVER use IPA. NEVER use strict linguistic notation. NEVER use
diacritics the beginner reader will not recognise.
- Mark primary stress with a capitalised syllable (Posso-SAY-bair).
Mark a secondary stress with bolding if your output supports it,
otherwise omit.
- Translate `user_language_translation` in the user's strongest
reading language. Match the register of the local-language
source ("polite_formal" stays polite_formal in translation).
- If the local language has a fundamentally different writing
system, ALSO include the local-language text in its native
script — the respelling is for speech, the script is for
pointing.
Output: a single PhraseToSay object. No commentary.
```
---
### Call: Flag a question as `requires_professional` + route
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a single classified question (verbatim text + topic +
urgency tier) and the user's city, neighbourhood, and country of
origin. Your task: decide whether this question requires a
professional answer (lawyer, caseworker, doctor, accredited
pharmacist, school placement officer) and, if so, name the kind of
service and the routing intent.
Set `requires_professional: true` for any of:
- asylum claims, refugee status, family-reunification appeals
- any visa category dispute or status change
- custody, child protection, family-court matters
- domestic-violence safety planning
- tax-residence and double-taxation questions
- prescription substitution decisions (drug X in country A vs drug
Y in country B — only a local pharmacist may decide)
- school placement appeals or special-education provision
- recognition of foreign professional qualifications when the
question is whether the user can practise (vs. just where to file)
Set `requires_professional: false` for fact-of-the-matter civic
questions (rubbish day, nearest pharmacy, SIM purchase, opening
hours, transit, school enrolment paperwork, registration office
address, finding a halal/kosher/vegetarian shop). These get a
grounded answer.
When `true`: produce a `professional_routing.kind` value from the
enum and a one-sentence rationale. The actual routing details
(the named service, its address, its working languages) come from
the grounded answer call.
Output: a single object {requires_professional, kind?, rationale}.
No commentary.
```
---
### Call: Speak the local-language phrase
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, calm, unhurried. Pick the Gemini 2.5 Flash TTS voice
whose `languageCode` matches the city's local language — pronunciation
will follow that locale automatically. Prefer a voice in the gender
the user prefers when published for the locale; otherwise pick
whichever is available rather than blocking.
The input is a single phrase the user will say at a counter. Read it
at counter-pace, with a slight pause between words. The descendant
of the writer is not the listener here — the LEARNER is, and they
need to be able to repeat the phrase after hearing it.
Pre-process the text before sending it to TTS:
- Read from `local_language_text`. Do NOT read the phonetic
respelling — that is for the user's eyes.
- Insert a single ellipsis (`…`) between major clauses to produce
a natural pause; insert a blank line plus em-dash (`—`) between
the three phrases of a multi-phrase block. Gemini 2.5 TTS does
not support SSML `` — these textual cues are how
you signal pace.
- Skip parenthetical glosses, citations, and metadata.
- Mid-call voice switching is not supported. One phrase, one
voice. If the user needs to hear the same phrase in their own
language for comparison, that is a separate TTS call.
- Target rate: ~90 words per minute — slow enough to imitate.
Style direction: prepend ONE short directive sentence to the
text input, exactly like: "Read slowly and clearly, as a phrase
a beginner will repeat at a counter. Pause briefly between major
words. …". There is no separate `style` API field on Gemini
2.5 TTS; the directive sentence inside the input is how style
is conveyed.
Phoneme overrides (Portuguese nasal vowels, German ü, Arabic
emphatic consonants, Mandarin tones) are NOT exposed by Gemini
2.5 TTS — no SSML `` tag. Pronunciation comes from the
chosen voice's native locale.
```
---
### Call: Compose pocket-card 4-fold layout text
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive the full CheatSheet object. Your task: produce a
condensed four-panel pocket-card layout that fits on a single A4
sheet folded in quarters. Each panel is ~400 characters of dense
content max.
Panels:
1. Home + Emergency — the user's home address (verbatim, in the
local language script — for showing a taxi driver), the local
emergency number, the local-language phrase for "I need help
in [user_languages[0]]".
2. Today — the most urgent two items (often: prescription pharmacy
address + the local-language phrase to ask for the medicine;
tonight's meal address + opening hours).
3. This Week — the four most actionable this_week items as
one-line addresses with one local-language phrase each.
4. Who to ask — the named professional routing services (legal
aid, migrant NGO, consulate), each with one line: name,
address, working languages.
Each panel is bilingual: local language at top, user_languages[0]
at the bottom of the same panel. Identical content, same row.
Hard rules:
- Addresses stay verbatim, NEVER translated.
- Local-language phrases stay verbatim, NEVER translated (a
one-line user-language gloss can sit below in smaller type).
- Date stamp "as of {today}" printed at the bottom of panel 4.
- No marketing copy. No "you've got this". The card is a tool.
Output: a JSON object with four panel strings + the date stamp.
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 Brazilian family in Lisbon, second morning.** A mother of two children (6 and 9) lands in Lisbon on a Friday night. Saturday morning she pastes ten questions into the app: where to register the family at the Junta de Freguesia, where to enrol the kids at the nearest public school (Agrupamento de Escolas), where to buy an MEO SIM without a Portuguese bank account, where the SNS (Serviço Nacional de Saúde) signs up new arrivals, where the closest farmácia stocks salbutamol, what day the rubbish truck comes to her street in Anjos, where the nearest Pingo Doce or Continente is open Sunday, the bus line from her rental to the Loja do Cidadão das Laranjeiras, what to say at the school office, and where the Brazilian consulate is. The app produces an A4 bilingual cheat sheet (Portuguese + Portuguese — same language, but European/Brazilian register-tuned), and a pocket card. The salbutamol question is flagged as a pharmacist-question on substitution, but the address of the closest farmácia open today is answered confidently with a citation.
- **The Filipino family in Toronto, on a permanent-residency landing.** Two parents, three children (4, 11, 14), arriving from Quezon City with their first Canadian winter ahead of them. Questions span OHIP enrolment (three-month wait, what to do meanwhile), school enrolment at the TDSB, where to get a Fido SIM at a kiosk that accepts overseas ID, the closest Filipino grocery, the nearest church with a Tagalog mass, where to find winter coats this week (Salvation Army, New Circles), how to register for the Toronto Public Library card, the bus and streetcar map from their address in Scarborough, the closest walk-in clinic that takes uninsured patients, and where to file for their SIN. The cheat sheet renders in English and Tagalog. The OHIP-during-the-wait question routes to the named community health centre with sliding-scale fees.
- **The Syrian family in Berlin, after eighteen months waiting.** A father with three children (8, 10, 13), settling into a flat in Neukölln. Questions span Bürgeramt registration appointment, Aufenthaltstitel application status, where the nearest Apotheke is open on Sunday for the children's medications, the Schulamt enrolment paperwork, where to find a halal butcher, the nearest mosque, the BVG U-Bahn line from his flat to the Jobcenter, where to get a Vodafone SIM, the Caritas migrant-support office, and the Arabic-speaking GP his caseworker mentioned but he could not find. The cheat sheet renders in German and Arabic, right-to-left for the Arabic column. The Aufenthaltstitel question routes to the named immigration lawyer at the local migrant-rights NGO; the pharmacy and food questions are answered confidently with addresses.
- **The Indian student in Manchester, week one.** A 22-year-old MSc student in Manchester from Hyderabad. Questions span NHS GP registration (yes, students need to register), Council Tax exemption letter from the university, where to open a student bank account without a UK credit history, the closest Indian grocery on the Curry Mile, the nearest Hindu temple, the closest 24-hour Sainsbury's, how to top up the Stagecoach bus pass, where the laundrette is, how to register for the BRP (Biometric Residence Permit) at the post office, and the contact for the university's international-students office. The cheat sheet renders in English and Hindi. No professional routing required — everything is fact-of-the-matter civic.
- **The Vietnamese-Canadian family returning to Hanoi.** A second-generation Vietnamese-Canadian family — both parents grew up in Vancouver, the kids are 7 and 11 — moving to Hanoi for a parent's three-year work contract. Questions span apartment-registration with the công an (local police), enrolment at the United Nations International School and at a Vietnamese public school for comparison, where to get a Viettel SIM at the airport, the closest pharmacy that stocks Western-branded medicines, the closest international hospital (Vinmec, Family Medical Practice), where to find the kids' familiar foods, the bus and Grab options from their compound to the school, and what to say at the police station for the temporary-residence stamp. The cheat sheet renders in English and Vietnamese.
- **The Venezuelan family in Madrid.** Two parents and a teenage daughter on a Spanish reunification visa to join a sister already in Vallecas. Questions span empadronamiento at the Junta Municipal de Distrito, NIE renewal timeline, where to register at the local centro de salud, the closest Mercadona open Sunday, where to find arepas and reina pepiada, the Metro line from Vallecas to the Oficina de Extranjería, how to register the daughter at the nearest IES, where to get a Movistar SIM, the Venezuelan consulate's address, and where to access free Spanish-as-a-second-language classes. The cheat sheet renders in Spanish and Spanish — same language, but explicitly using Madrid-Spanish register for the family that grew up with Venezuelan Spanish.
- **The Ukrainian family in Prague.** A mother and two children (5 and 12), arriving in Prague under the Temporary Protection scheme after eighteen months in a refugee centre in Lviv. Questions span the OAMP registration appointment, the temporary-protection visa stamping, where to enrol the children at the Czech-Ukrainian bilingual school programme, the closest pharmacy (lékárna) with Ukrainian-speaking staff, where to top up a Vodafone SIM with Ukrainian ID, the closest Albert supermarket, the closest Orthodox church, the tram line from her temporary housing to the OAMP office, and where the Caritas Czech Republic migrant office is. The cheat sheet renders in Czech and Ukrainian.
- **The caseworker building five sheets in one afternoon.** A volunteer at a parish-based migrant-welcome desk in Lisbon receives the next month's incoming families on Monday — three Brazilian, one Cape Verdean, one São Toméan, all arriving at different addresses across Lisbon. She makes five cheat sheets in a row, sharing each with the family's WhatsApp by the link the app emits.
- **The corporate-relocation family in Singapore.** A senior engineer and partner with two children (3 and 6) moving from Bengaluru to Singapore on a Tech.Pass. Questions span MOM IPA card collection, Singpass setup for the partner, the nearest polyclinic, where to enrol the kids at the Indian International School and at the nearest neighbourhood Singaporean school for comparison, where to get a Singtel SIM, where to find South Indian groceries in Little India, the MRT line from their HDB rental to the MOM office, the closest temple, and the recognition pathway for the partner's nursing qualification (this last one routes to the SNB — Singapore Nursing Board — as a professional question).
## 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 kitchen table in an unfamiliar rental: a laptop, a paper notebook, a passport on the table, an unmade mattress visible in the background through an open door. One paragraph: "New to Town turns ten urgent first-weekend questions into one printable cheat sheet — with every address, opening hour, and the words to say at the counter, in your language and the city's." Single Google sign-in button; Apple sign-in next to it; "Continue without signing in" below — for the families who have good reasons to stay anonymous.
2. **First-run input — "Tell us where you are".** Three short fields: City + country (autocomplete from a list of supported metros). Home address (free-text; "this is where the bus routes start from"). Country you came from. Two more fields: Languages you read (multi-select, ordered by strength — strongest first). Household summary ("how many adults, how many children, ages") with a clear "skip" link.
3. **Questions input — "Paste your ten questions".** A single large textarea labelled "Type or paste your questions, one per line or in a paragraph — any language is fine." A small example block underneath showing three sample lines in mixed Portuguese / English / Tagalog. A voice-input mic button to the right of the textarea. A "specific health notes (optional)" smaller textarea below ("e.g. my daughter uses an asthma inhaler") with a privacy reassurance line. Primary CTA: "Generate cheat sheet". Tertiary link: "Try with the sample family" → loads the Brazilian-in-Lisbon seed scenario.
4. **Processing screen.** A single-pane honest progress display: "Reading your questions…" → "Sorting Today / This Week / This Month…" → "Looking up the registration office…" → "Finding the pharmacies open Sunday…" → "Checking opening hours…" → "Writing the local phrases…" → "Composing the cheat sheet…". Each step takes 5-15 seconds; the queue is visible so the user can see what the app is grounding. The user can close the tab and come back; results are saved.
5. **Cheat sheet preview.** The full sheet rendered as it will print: bilingual two-column layout, ten answers grouped Today / This Week / This Month / Later, with addresses, transit lines, "what to bring" checkboxes, the local-language phrases under each answer with the phonetic respelling, citation links discreetly at the end of each answer, date stamp on every answer, freshness warnings clearly marked. Sticky right-rail action panel: Export PDF · Export HTML offline file · Export pocket card · Share with family · Save to your sheets.
6. **Per-answer detail expansion.** Tap any answer to expand: long-form explanation, the full citation list with source labels and source types, the verbatim quoted excerpt that anchors the answer, the "(i) how we found this" link that surfaces the model's `thoughtSummary`, an edit button ("correct or add a note") that lets the user add a personal note that prints on the sheet alongside the AI answer with a clear "Your note" header.
7. **Pocket card preview.** Four-panel A4 folded-in-quarters layout, bilingual, with the user's home address as the first panel, the most urgent two items as the second, the four this-week items as the third, and the professional-routing directory as the fourth. Print preview shows the fold lines. A toggle: A4 · US Letter.
8. **Cheat sheets index.** Lists every cheat sheet the user has generated: "Lisbon (us, May 2026)", "Lisbon (my parents joining August)", "Cascais (the move next year)". Each card shows city, date generated, number of answers, freshness state (green if all answers ≤30 days, amber if some are 30-90 days, red if any are >90 days). A primary CTA: "New cheat sheet" → returns to questions input.
9. **Share modal.** Two share modes. (a) "Share with a family member" — magic-link email; the recipient lands directly on the cheat sheet with comment-and-edit rights. (b) "Share with my caseworker" — same magic link, plus a one-line note. Privacy reminder visible: "anyone with the link can read this sheet — don't paste sensitive details into it".
10. **Settings.** Account, default language pair, default print format (A4 / US Letter), data export ("Download all my cheat sheets as JSON"), delete-archive ("Delete every sheet I've ever made — gone in 60 seconds"). Below: capabilities `(i)` info button.
11. **Caseworker workspace** (visible only to users who switch their mode to "caseworker"). A small dashboard of households the caseworker is supporting, with the cheat sheet for each, the last-generated date, and a "regenerate with today's freshness" button. The caseworker can label households privately ("Family A — landed 17 May, prefers Tigrinya").
12. **Footer.** "Built for the families nobody handed a folder to." Privacy: "Your questions are yours. We never train on them." Capabilities `(i)` icon in header.
13. **Offline page.** When the user opens the saved HTML on a phone with no signal, an offline banner appears: "You are offline. This cheat sheet was generated on 28 May 2026. Civic info can change — confirm at the counter." The page is fully functional offline (the HTML export is self-contained — no external CSS, no fonts, no JS dependencies, no images apart from inline SVG).
## 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 New to Town."
- Subhead: "Turn ten urgent first-weekend questions into one printable cheat sheet — with every address, opening hour, and the words to say at the counter, in your language and the city's."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic search engine: every fact is cited from a current municipal or national source, every fact carries a date stamp, every answer comes with the exact words to say at the counter, the sheet is bilingual side-by-side, and the artefact is a printable page — the family can use it without a SIM or wifi.
- Visual: a small annotated illustration of an A4 cheat sheet with the relevant elements labelled (date stamp, citation, bilingual columns, "exact words to say" block, "what to bring" checklist) — not a generic search-engine icon.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample family — Beatriz, two kids, just landed in Lisbon."
- A live demo input pre-loaded with ten questions from the seed content in section 8a.
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the grounded municipal citation, the European-Portuguese phonetic respelling tuned for a Brazilian reader, the asthma-pharmacy answer that flags substitution-decisions for a pharmacist while still giving the address).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample household in `/data/seed-households/` for your own."
- "Adjust the prompts in `/server/prompts/` to fit your city's specific services."
- "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 + `google_search` grounding** — every civic fact in the cheat sheet (an address, an opening hour, a bus line, a registration requirement) comes from a current grounded source. The citation URL appears on the printed sheet itself. Note: a Gemini call cannot combine `google_search` grounding with `responseSchema` — the grounded answer call emits JSON in the text body and citations come from `groundingMetadata.groundingChunks[].web.uri`, not from the JSON body.
- **Gemini 3.5 Flash (long context, 1M tokens)** — the model reads up to 30+ municipal pages and transit timetables in a single call to compose one cheat sheet. For very large jobs (city-wide directories), the orchestration chunks by urgency tier.
- **Gemini 3.5 Flash (multilingual)** — Portuguese, Spanish, French, German, Italian, English, Polish, Ukrainian, Russian, Czech, Greek, Turkish, Arabic, Persian, Urdu, Hindi, Bengali, Tamil, Punjabi (Gurmukhi & Shahmukhi), Tagalog, Vietnamese, Khmer, Thai, Mandarin, Cantonese, Korean, Japanese, Swahili, Amharic, Tigrinya, Somali, Yoruba, Igbo, Hausa.
- **Gemini 3.5 Flash** — question classification, urgency tiering, the "requires_professional" check, the pocket-card composition.
- **Gemini TTS** — optional. Speak the local-language phrase at counter-pace. Voice locale is picked via `languageCode`; pronunciation follows that voice's native locale. Note: Gemini 2.5 TTS does not support SSML `` or `` — pauses are encoded textually with `…` and blank-line `—`, and the style directive is prepended as a sentence to the input text. No separate `style` API field.
- **Firebase Auth** — Google and Apple sign-in (Apple requires user-side configuration of an Apple Developer account); family invitations via magic links (requires sender-domain authorisation in Firebase Auth).
- **Firestore** — stores your cheat sheets, syncs across devices in real time. Anonymous-mode sheets never reach Firestore — they live only on the device.
- **Firebase Storage** — optional. Cloud-saves the rendered PDFs / HTML if you want them; not auto-provisioned by AI Studio Build — enable in the Firebase console before first cloud-save.
- **Cost note** — see the detailed breakdown in 6d. A typical ten-question cheat sheet costs about $0.11 of Gemini API spend; a caseworker generating ten sheets per week costs about $1.10/week.
- **Privacy note** — your questions are private to you and the family / caseworker you invite. This app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. Anonymous-mode data lives only on your device and is never sent to any backend.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b
- Email: see section 4b
- Payments: see section 4b (not used in v1)
- External APIs: see section 4b
**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)
- `MAPS_ROUTES_KEY` — optional, only if you want door-to-door routing on top of the named line + stop
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: grounded per-question calls are billed per token of input + output — a typical ten-question sheet costs ~$0.07 in grounded calls plus ~$0.03 in the composition pass.
- One short paragraph on privacy: where the data lives (your Firebase project, or your local device in anonymous mode), how to delete it (Settings → "Delete every sheet I've ever made — gone in 60 seconds"), what is never sent for training.
**Documentation links:**
- AI Studio Build docs
- Gemini API multilingual, long-context, grounding, TTS docs
- Firebase Auth, Firestore, Firebase Storage docs
**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)
- **Classify + urgency-tier (Gemini 3.5 Flash, low thinking)** — typical 10-question paste, ~1k input + ~1k output ≈ ~$0.0004 per sheet.
- **Answer one question with grounded search (Gemini 3.5 Flash, medium thinking, grounded)** — per question: ~20k input tokens (the user context + several fetched pages of grounded snippets) and ~1.5k output tokens. ~$0.033 per question × 10 questions = ~$0.33 worst case. In practice the grounded sub-pages are cached by Google's grounding pipeline; realistic per-question cost is ~$0.006-$0.010, putting a typical ten-question sheet at ~$0.06-$0.10.
- **Compose bilingual cheat sheet (Gemini 3.5 Flash, low thinking)** — typical ~15k input + ~5k output ≈ ~$0.044 per sheet (this is the main composition cost).
- **Phonetic respelling pass (Gemini 3.5 Flash, low thinking)** — typical ~30 phrases × 200 tokens each ≈ ~$0.012 per sheet.
- **Flag `requires_professional` (Gemini 3.5 Flash, low thinking)** — typical ~10 questions ≈ ~$0.0001 per sheet.
- **TTS narration (Gemini 2.5 Flash TTS)** — optional, on-demand. Billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A typical 60-character phrase ≈ $0.0002 per phrase. A user who taps every phrase: ~$0.006 per sheet.
- **Pocket card composition (Gemini 3.5 Flash, low thinking)** — ~$0.001 per sheet.
- **Expected per-sheet cost on first generation:** ~$0.10-$0.13. **Caseworker generating ten sheets per week:** ~$1.00-$1.30/week. **City library bulk-generating 100 region sheets:** ~$10-$13 one-off.
- **Cheat-sheet storage:** Firestore standard tier; a typical CheatSheet document is ~25 kB. 1000 sheets ≈ ~$0.02/month.
## 7. Design language
- **Mood:** A real document, made for a real Monday morning. Not a tech product. Not a kiosk. The kitchen table at 9pm on the second night in the new city, the unfamiliar window in the background, the paper notebook open, the family WhatsApp group ringing on the phone — and the printer at the cybercafé waiting.
- **Typography:** Humanist sans-serif for the cheat-sheet body (Inter, Source Sans, or Noto Sans — Noto is preferable because the app must render every script reliably). A subdued serif (Source Serif Pro) for the running heads on the printed page only — never for the screen. A monospaced font (JetBrains Mono or IBM Plex Mono) for the phonetic respelling blocks so the syllable boundaries are clearly visible.
- **Palette:** Paper-cream background `#FAF7F2` for the cheat-sheet page, ink `#1A1A1A` for body text, ink-muted `#5A5A5A` for citations and date stamps, a single accent — civic blue `#2C5282` — for office and address callouts. A muted amber `#A07A1F` for freshness warnings ("verify on arrival"). A muted red `#9B2C2C` only for `requires_professional` routing badges, never for general alerts. White-card sidebar for app chrome (`#FFFFFF`), with civic-blue `4-px` rule above the user's home address. The palette borrows from municipal stationery — a Câmara Municipal form, a Bürgeramt slip — not from SaaS design systems.
- **Imagery:** No stock photos in the chrome. Hero is photograph-style only on the welcome screen. Inside the app, the cheat sheet itself is the hero — the design honours the act of printing it. Icons are line-only at 1.5 stroke width; never filled, never decorative. The "what to bring" checkboxes are square, not pill-shaped — they should look like the checkbox on a real form.
- **Hand-feel touches:** A faint dot-grid on the cheat-sheet background that matches a 4-mm civic-form grid. The print preview honours `@page { margin: 12mm; }` and shows the fold lines for the pocket card. A small "as of {date}" date stamp at the foot of every printed page, rendered in the muted ink colour, like a notary stamp.
- **Spacing:** consistent 4-px base. The bilingual columns sit on a shared baseline grid so the two languages align row-for-row.
- **Radius:** consistent token set (e.g. 4 / 8 / 16 px). Sheet cards use 4; the share modal uses 8; the welcome card uses 16.
- **Shadows:** subtle, almost absent. The cheat sheet sits flat on the page — like a printed document, not a 3D card.
- **Motion:** purposeful — entrance fades, the processing screen's step transitions, the print-preview slide-in. Respect `prefers-reduced-motion`. No bouncing splash animations. No theatrical hero animations. The "share link copied" confirmation is the one place where motion is allowed to flex — a brief check-mark draw at 240 ms with reduced-motion falling back to instant.
- **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible — the processing screen's step queue is the canonical "thinking" indicator. Empty states have helpful next-action guidance ("Paste your first question to start").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible household scenarios — Beatriz Souza arriving in Lisbon, Maricel Reyes arriving in Toronto, Yusuf Haddad arriving in Berlin — that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on real municipal terminology — "Junta de Freguesia" in Lisbon, "Bürgeramt" in Berlin, "TDSB" in Toronto, "Empadronamiento" in Madrid — but never claim that a fictional family is a real one or that a fictional address is a real address.
- Tone: warm, direct, free of corporate language. This template is for a person at a kitchen table on a stressful weekend, not for an enterprise procurement officer.
- Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'. The headline of the cheat sheet itself is the bilingual phrase that translates to "First-weekend cheat sheet for [family name] — [city], [date]".
- Body copy: short paragraphs (2-4 sentences). Use lists where appropriate.
- Plain language. Avoid jargon — except where the user already speaks the jargon (the caseworker user wants to see "OAMP" in the routing block; the migrant family wants to see the official office name in the local language, with a parenthetical gloss on first occurrence).
- 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 opening hour shows as a faintly amber-tinted row; tapping it reveals the freshness reason and the citation excerpt).
- Date stamps appear plain — "as of 28 May 2026", not "AI-checked 28 May 2026". The date stamp is a fact about the page, not a marketing claim.
## 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 households (sidebar):**
- "Souza family — Lisbon, May 2026" (Brazilian mother, two children aged 6 and 9, lives in a short-let in Anjos; questions span school, SNS, SIM, asthma inhaler, rubbish day, Sunday supermarket, Brazilian consulate)
- "Reyes family — Toronto, March 2026" (Filipino parents, three children aged 4, 11, 14, settling in Scarborough; questions span OHIP, TDSB, Fido SIM, winter coats, Tagalog mass, walk-in clinic)
- "Haddad family — Berlin, May 2026" (Syrian father, three children aged 8, 10, 13, in Neukölln; questions span Bürgeramt appointment, Aufenthaltstitel status, Sunday Apotheke, halal butcher, Arabic-speaking GP, Caritas)
- "Kumar — Manchester, September 2026" (MSc student from Hyderabad; questions span NHS GP, Council Tax exemption, student bank account, Curry Mile groceries, BRP collection)
- "Nguyen-Tran family — Hanoi, June 2026" (Vietnamese-Canadian family returning, two children aged 7 and 11, on a three-year contract; questions span công an registration, school comparison, Viettel SIM, Vinmec hospital)
- "Marrero family — Madrid, June 2026" (Venezuelan family on reunification visa, teenage daughter, in Vallecas; questions span empadronamiento, NIE renewal, centro de salud, arepas, IES enrolment)
**Sample question paste (this is what the demo should accept verbatim):**
- "Em qual junta de freguesia eu registo a família?"
- "Onde fica a escola pública mais próxima de Anjos para crianças de 6 e 9 anos?"
- "Where can I get a MEO SIM card without a Portuguese bank account?"
- "Qual farmácia perto de Anjos vende salbutamol e está aberta amanhã (sábado)?"
- "Que dia recolhem o lixo na Rua Cidade de Manchester, perto do Largo do Intendente?"
- "Where can I sign my family up at the SNS as a new arrival?"
- "Qual é o Pingo Doce mais próximo aberto no domingo?"
- "Como vou de Anjos até a Loja do Cidadão das Laranjeiras de transporte público?"
- "Onde fica o consulado do Brasil em Lisboa?"
- "Como pergunto na escola sobre matrícula, em português de Portugal?"
**Sample cheat-sheet excerpt in detail view (this is what the demo should show):**
- **Sheet title (bilingual):** "Cheat sheet de chegada — Família Souza, Lisboa, 28 de maio de 2026" / "Arrival cheat sheet — Souza family, Lisbon, 28 May 2026" (the Brazilian family reads Portuguese on both sides, but the second column uses Brazilian register and notes when European-Portuguese terms differ)
- **General freshness warning:** "Confirme à chegada — a informação cívica pode mudar. As fontes e a data de cada resposta aparecem por baixo."
- **Sample answer — "Onde sign-up no SNS?" (this_week, registration topic):**
- `short_answer_local_lang`: "Para se inscrever no SNS como recém-chegada, dirija-se ao Centro de Saúde da freguesia onde mora (USF Sétima Colina, mais próximo de Anjos), com o passaporte e o atestado de residência (pedido na Junta de Freguesia). É gratuito."
- `short_answer_user_lang` (Brazilian register): "Para se inscrever no SNS como recém-chegada, vá ao Centro de Saúde da freguesia onde você mora (USF Sétima Colina, mais perto de Anjos), com o passaporte e o atestado de residência (pedido na Junta de Freguesia). É gratuito."
- `primary_address`: { name_local: "USF Sétima Colina", street_verbatim: "Travessa de Santa Quitéria, 22, 1250-247 Lisboa", opening_hours_verbatim: "Segunda a sexta, 08h00 – 20h00; sábado, 09h00 – 13h00", phone: "+351 213 803 080", notes: "marcação prévia pelo SNS 24 (808 24 24 24) recomendada" }
- `transit_from_user_address`: [{ mode: "metro", line_or_route_label: "Linha Verde", from_stop: "Intendente", to_stop: "Rato", approx_minutes: 12 }, { mode: "walk", line_or_route_label: "a pé", from_stop: "Rato", to_stop: "Travessa de Santa Quitéria", approx_minutes: 6 }]
- `what_to_bring`: [{ item: "Passaporte de cada membro da família", why: "documento de identificação obrigatório", optional: false }, { item: "Atestado de residência da Junta de Freguesia", why: "comprovativo de morada", optional: false }, { item: "Número de Identificação Fiscal (NIF)", why: "necessário para registo", optional: false }]
- `phrases_to_say`: [{ intent: "ask to register at the SNS as a new arrival", local_language_text: "Bom dia. Cheguei recentemente a Portugal e queria inscrever a minha família no SNS, se faz favor.", phonetic_respelling: "Bohn DEE-ah. Sheh-GAY reh-sen-teh-MEN-tch ah Por-too-GAHL ee keh-REE-ah ins-kreh-VAYR ah MEE-nyah fah-MEE-lyah noo es-eh-EHN-ess, see fahz fah-VOR.", user_language_translation: "Good morning. I recently arrived in Portugal and I'd like to register my family at the SNS, please.", register: "polite_formal" }]
- `citations`: [{ url: "https://www.sns.gov.pt/cidadao/recem-chegados/", source_label: "Serviço Nacional de Saúde", source_type: "national_health_service", source_language: "pt-PT", source_last_updated_iso: "2026-03-14", fetched_at_iso: "2026-05-28", excerpt_quoted: "Os recém-chegados ao território nacional podem inscrever-se gratuitamente no SNS no Centro de Saúde da área de residência, mediante apresentação de documento de identificação e atestado de residência." }]
- `as_of_iso`: "2026-05-28"
- `freshness_warning`: false
- `requires_professional`: false
- `confidence`: 0.93
- **Sample answer — "Onde compro um SIM MEO sem conta bancária portuguesa?" (this_week, communication topic):**
- `short_answer_local_lang`: "Pode comprar um SIM MEO pré-pago em qualquer loja MEO ou quiosque autorizado com o passaporte — a conta bancária portuguesa não é exigida para o pré-pago. A loja MEO mais próxima de Anjos é na Rua da Palma."
- `phrases_to_say`: [{ intent: "ask for a prepaid SIM with passport only", local_language_text: "Bom dia. Queria um cartão SIM pré-pago, por favor. Só tenho o passaporte — não tenho ainda conta bancária portuguesa.", phonetic_respelling: "Bohn DEE-ah. Keh-REE-ah oon kar-TOWNGH SIM preh-PAH-goo, por fah-VOR. Saw TEH-nyoo oo pah-sah-POR-tch — nowngh TEH-nyoo eye-EEN-dah KOHN-tah ban-KAH-ree-ah por-too-GAY-zah.", user_language_translation: "Good morning. I'd like a prepaid SIM card, please. I only have my passport — I don't have a Portuguese bank account yet.", register: "polite_formal" }]
- `citations`: [{ url: "https://www.meo.pt/telemovel/cartao-sim", source_label: "MEO", source_type: "other", source_language: "pt-PT", source_last_updated_iso: null, fetched_at_iso: "2026-05-28", excerpt_quoted: "O cartão SIM pré-pago da MEO está disponível em todas as lojas MEO e quiosques autorizados com apresentação de documento de identificação." }]
- `freshness_warning`: true (because `source_last_updated_iso` is null)
- `confidence`: 0.81
**Sample input artefacts (for the build to demonstrate):**
- A Brazilian-Portuguese paste of ten questions, mixing Portuguese and English, with one health-continuity note ("minha filha de 9 anos usa salbutamol").
- A Tagalog-English paste from the Filipino-in-Toronto seed, with three OHIP-related questions and one mention of a chronic condition.
- An Arabic paste from the Syrian-in-Berlin seed, written right-to-left, mixing classical and Levantine vocabulary.
- A Hindi-English paste from the student-in-Manchester seed, asking about NHS GP, council tax, and the closest Hindu temple.
**Sample voice copy:**
- Onboarding: "Paste your first ten questions. We'll write the cheat sheet."
- Processing: "Reading your questions…" / "Sorting Today / This Week / This Month…" / "Looking up the Junta de Freguesia…" / "Finding the pharmacies open Sunday…" / "Writing the phrases to say at the counter…"
- Empty state: "Paste your first question to start. Any language — we'll sort them by urgency."
- Error (low confidence): "We couldn't confirm the opening hours for this office from a current source. We've left the time blank and added a note to call ahead — the phone number is still in the address block."
- Save confirmation: "Saved to Souza family — Lisbon, 28 May 2026."
- Freshness warning toast: "One of these answers is based on a source we couldn't date. We've marked it 'verify on arrival' on the printed sheet."
- Requires-professional banner: "This question needs a specialist — we've left the directory entry for the nearest free legal-aid clinic instead of guessing."
**Sample family invitation email subject + body:**
- Subject: "André — your cheat sheet for our Lisbon weekend is ready."
- Body: "Hi André — I've made the cheat sheet for the weekend, with the rubbish day for Rua Cidade de Manchester, the SNS sign-up office, the pharmacy that has Maria's inhaler, and the Brazilian consulate. Open it before you land — and bring the printed pocket card. Tap to open." [Open Cheat Sheet]
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a kitchen table in an unfamiliar rental at evening: a laptop open, a paper notebook, two passports, an enamel mug of tea, a child's drawing of a house on the corner of the table. No people in frame. Generate via Nano Banana 2 with a prompt emphasising "wooden kitchen table, warm desk-lamp light, late evening, gentle out-of-focus mug of tea, two passports visible, real notebook paper, soft shadow, no people, no logos, no signage in the frame".
- **App icon / wordmark:** Set in the humanist sans (Inter or Noto Sans), with a small civic-blue square the size of a stamp. No icon — just type + the square.
- **Empty-state illustration:** A simple line drawing of an A4 page with the cheat-sheet skeleton — a heading rule, two columns, a footer date stamp — at 1.5 stroke width. Hand-drawn aesthetic, not a flat icon.
- **Demo cheat-sheet visuals:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "an A4 printed cheat sheet on a wooden kitchen table, civic-blue accent, bilingual two-column layout, real folded fold lines, no people in frame, no logos, no brand names". Each demo sheet should look photographed, not rendered.
- **Period maps / city maps:** A static SVG basemap per supported city in `/data/city-maps/` that shows the city's neighbourhoods at a low-detail level. No image-based tiles. The user's home address is plotted as a civic-blue dot and the offices, schools, and pharmacies in their cheat sheet are plotted as smaller hollow dots. The map appears in the printable sheet's footer at one-eighth height.
- **Stock fallbacks:** If image generation fails, fall back to the photographed sample cheat sheet from `/public/samples/sample-cheat-sheet.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"). "Please paste at least one question" / "Please add the city you're in — we use it to find the right municipal office".
- 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: "Paste your first question", "Try with the sample family", "Print to PDF" — 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 step-by-step "thinking…" indicator before content starts arriving. The processing screen names the actual step ("Looking up the Junta de Freguesia…") rather than a generic spinner.
- If an AI call fails, show a calm, specific error ("We couldn't confirm the SNS sign-up office from the official site. The page may be temporarily unavailable. Want to retry, or skip this question and continue with the others?") and offer retry.
- Low-confidence rows on the cheat sheet are faintly amber-tinted; tapping reveals the freshness reason and the citation excerpt.
- The pocket-card preview transition between A4 and US Letter takes 220 ms with `prefers-reduced-motion` falling back to instant.
- The share-link copied confirmation is a 240-ms check-mark draw with reduced-motion falling back to instant.
- Anonymous-mode cheat sheets show a persistent badge in the header ("Anonymous — saved on this device only") so the user knows their data lifecycle. A one-click "save to my account" prompt invites them to convert without losing the sheet.
## 11. Tech & responsive requirements
- **Code-generation calls — pin `maxOutputTokens: 8192`** on any Gemini call whose `systemInstruction` asks the model to emit code (HTML, TypeScript, Python, Kotlin, Swift, SQL, etc.). Without an explicit cap the generation can truncate mid-function on long files. 8192 is the current Gemini 3.5 Flash output ceiling.
- **TTS markdown-stripping preprocessor:** before sending any user-authored markdown to `gemini-3.1-flash-tts-preview`, strip non-spoken markdown: `#`/`##`/`###` headings (keep the title text), `**bold**` (keep the inner text), `[label](url)` (keep `label`, drop URL), `` ``` `` fenced code blocks (skip entirely), `>` block-quote markers (keep the text), and `|` table pipes (read row-by-row as sentences). Insert `…` between sentences for a short pause and a blank line plus `—` between paragraphs for a long pause. The model does not understand markdown; raw markdown will be read aloud as literal characters ("asterisk asterisk").
- **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 grounded answers, bilingual composition, and phonetic respelling; `gemini-3.5-flash` for classification, urgency tiering, and pocket-card composition; `gemini-3.1-flash-tts-preview` for TTS. Set `thinkingLevel` explicitly per call. On TTS calls, omit `thinkingConfig` entirely.
- **Grounding:** Each grounded answer is a separate Gemini 3.5 Flash call with `google_search` enabled and NO `responseSchema`. Parse JSON from the response text body; read citation URLs from `groundingMetadata.groundingChunks[].web.uri`. Orchestrate per-question grounded calls in parallel with a concurrency cap of 4.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Anonymous-mode cheat sheets live in `localStorage` only.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it (Apple sign-in requires user-configured Apple Developer credentials); magic-link email for share-with-family / share-with-caseworker (requires sender-domain authorisation).
- **Storage:** Firebase Storage for optional cloud-saved PDF / HTML exports — NOT auto-provisioned by AI Studio Build; the user must enable in the Firebase console before first cloud-save. Pre-signed URLs only.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+.
- 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 user data in Firestore. Use real-time listeners on the cheat sheet preview so a caseworker editing a sheet on desktop and a parent reading it on phone stay in sync.
- Optimistic UI on writes; reconcile on response.
- The HTML offline export is fully self-contained — inline CSS, inline SVG, no external font, no external image, no JS dependency. It must open and render on a phone with no signal, in any browser, including Safari iOS 14+.
## 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 (3px solid civic-blue, 2px offset).
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components.
- All images have meaningful `alt` text. The hero image's alt: "An evening kitchen table with a notebook, two passports, and a mug of tea — the user's first weekend in a new city".
- Form fields have associated `