================ 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. --- # Embassy Appointment ## 1. Project **Embassy Appointment** is a personal preparation tool for the person about to walk into a consulate or high commission in a country that is not yet home. The user picks their nationality, their destination mission, and the specific service they need — child passport renewal, adult passport renewal, birth registration of a child born abroad, power-of-attorney legalisation, police-clearance certificate, marriage certificate apostille, statutory declaration, NRI/OCI card, dual- nationality decision form — and the app produces a single, dated, cited brief: the exact documents to bring (in the order the desk will ask for them), the fees and what each one is for, the queue-and-window flowchart for appointment day, and a short script in the user's first language for the moments at the counter where wording matters. This is the kind of app a Filipino mother in Toronto pulls up on the streetcar to North York the morning she renews her seven-year-old's passport at the Consulate General of the Philippines, because the consulate's website is three layers of PDFs deep, the most recent fee-schedule announcement is from a Facebook post the consul shared in March, and the only voice in the room who has done this before is a stranger on a forum who did it the year the form changed. And it is the kind of app a Tamil father in Sydney builds the night before his appointment at the Indian High Commission, because his daughter holds dual citizenship, the Indian government renounced birthright dual nationality in 1955, and the form OCI vs. surrender-of-Indian- passport question has consequences he does not have a second chance to undo. Same shape of moment, different paperwork, different stakes. The single demo that proves the magic: the user picks **Argentine Consulate General in London → child passport renewal → dual citizen, born in the UK**. In under twenty seconds the app produces a one-page brief: the four documents she must bring (her DNI, her daughter's expired Argentine passport, the original UK birth certificate, and a recent photograph that meets the Argentine specification, not the British one), the consular fee in pounds with the exact reason each line item is charged, a five-step flowchart for the appointment day (security desk → ticket machine → window 3 for biometrics → window 1 for payment → return for collection or postal delivery), and a short Spanish-Castellano script for the two moments at the counter where the wrong phrasing costs her the appointment: confirming her own identity as the parent of record, and declaring on her daughter's behalf that she has not renounced Argentine citizenship. Every fact is cited to a source the user can open. Every fact is stamped with the date the app last verified it. Anything older than thirty days shows a faint amber dot and reruns the moment she opens the brief. **Tagline:** _The exact documents, the exact fees, the exact words — for any consulate, in any city, in your first language._ ## 2. Target audience - Newly-arrived parents renewing their child's passport at a consulate in a new city — Filipino in Toronto, Argentine in London, Brazilian in Lisbon, Mexican in Houston, Vietnamese in Berlin, Eritrean in Frankfurt - Dual-citizen families navigating one government's rules at another government's mission — Tamil/Australian, Korean/American, Iranian/Canadian, Lebanese/Brazilian, Polish/British - International students applying for or renewing visas, residence permits, work-authorisation letters — Indian, Nigerian, Chinese, Pakistani, Bangladeshi - Asylum-seekers and refugees navigating appointments at their host country's immigration authority, with first-language scaffolding the host has not provided - OFW (Overseas Filipino Worker) families and other long-distance-worker families coordinating documents across the worker's host country and their home country's consulate - Grandparents flying to meet a grandchild born abroad who need a child's emergency travel document on short notice - Returning citizens renewing a long-expired passport after a decade away, navigating a new form they have never seen - Diaspora elders applying for survivor pensions, military-service certifications, or birth-registration corrections at their home country's mission - Volunteer caseworkers at community centres and diaspora-association legal clinics who walk people through embassy paperwork as a weekly part of the work ## 3. Core value propositions Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app. - **The exact documents for your exact case** — not a generic checklist scraped from a 2019 forum post. The brief is generated per the specific combination of nationality, destination mission, service, and the user's situation (dual citizen, child under 18, expired more than five years, married name change, lost not stolen). Each item names what counts as valid (original vs. notarised copy, certified translation by whom, photograph specification by millimetres, validity window from issue date). - **Every fact dated and cited** — the app shows when each piece of information was last verified and the URL it came from. The user clicks any sentence and sees the source: the consulate's own page, the home-country government portal, the most recent diaspora-community forum thread that confirmed the fact for someone going the same day. Stale information is the failure mode; the app never trusts a fact older than thirty days without re-checking it. - **The walk-through for the appointment day** — a five-to-eight-step flowchart of what happens in the building: arrival window, security, ticket, biometrics, payment, collection. With the floor numbers, the queue rules, the bag-and-phone policy, the cash-or-card answer, the estimated time at each step. Built from the consulate's own guidance plus the most recent diaspora-forum posts of people who went last week. - **A short script for the desk in the user's first language** — the two or three sentences at the counter where the wrong wording costs the appointment. Confirming identity, declaring relationship to the applicant, answering the dual-citizenship question, asking for the postal-return option. The script is in the official language of the mission, with a phonetic guide and an English (or user-first-language) parallel translation underneath. - **Multilingual end to end** — the user reads in Tagalog, Spanish, Portuguese, Tamil, Hindi, Urdu, Bengali, Punjabi, Mandarin, Cantonese, Korean, Vietnamese, Khmer, Amharic, Tigrinya, Swahili, Farsi, Pashto, Arabic, French, Russian, or English. The consulate's source page is shown verbatim in its original language; the brief is rendered in the user's first language; the script is rendered in the language the desk will use. - **The fees, line-itemed, in the right currency** — what each line is actually for (the consular act, the local-currency surcharge, the postal-return service, the same-day-collection premium), in the currency the mission accepts at the window. With a one-line note on the payment method the consulate actually takes that day, because half the missions still take only money order or bank-cheque. - **A "what if it's wrong" panel** — every section of the brief shows the path to override. The user has a friend who went last Tuesday and the fee was different; the user types what their friend said; the app notes the discrepancy, re-checks, and shows both versions side-by-side with timestamps. - **Offline once prepared** — the final brief is a printable PDF and an offline-readable web page on the phone, because consulates have basement-level signal and the user will not have data inside the building. - **No personal data leaves the device by default** — the user's nationality and destination and service are needed for the query; their passport number, full name, child's date of birth, and any photographs of documents are processed locally or in the user's own Firestore and never sent to third-party search engines. ## 4. Features to build - Pick-a-trip flow — nationality, destination mission (city + country + which mission, since several countries run more than one in a city), service (from a curated list per mission), applicant relationship to the user (self, child under 18, spouse, parent, grandparent), specific situational toggles (dual citizen, name change, lost not stolen, expired more than five years, born abroad never registered) - Long-context grounded research call — Gemini 3.5 Flash with `google_search` grounding reads the consulate's own pages, the home-country government portal, the most recent diaspora forum threads, and the official fee announcements; produces a single structured brief - Document checklist with per-item validity — original or copy, certified translation requirement, validity window (e.g. "issued within the last six months"), photograph specification (size in millimetres, head-size percentage, background colour), how to obtain each if missing - Per-line fee breakdown — consular act fee, currency-conversion surcharge, postal-return surcharge, expedited-collection premium, biometric fee where separate, with the payment methods the mission actually accepts at the window (money order, debit-only, cash with exact change, bank-cheque to a specific payee name) - Appointment-day flowchart — a five-to-eight-step visual walkthrough: arrival, security, ticket, queue, biometrics, payment, collection. With floor and window numbers where known, bag-and-phone policy, children-allowed-in policy, estimated time per step - Counter script in the official language of the mission — short paragraphs for the moments at the desk that matter; phonetic guide; first-language parallel translation underneath; tap to hear it read by TTS at the cadence of the desk worker; one-tap "loop this sentence" for rehearsal - Source pane — every fact in the brief tagged with a citation chip; tap to see the source URL, the publication date, and the snippet the fact was extracted from - Freshness indicator — a small dot next to every fact: green (verified within 7 days), amber (verified within 30 days), red (older than 30 days, re-check on next open). The app's default behaviour is to re-run the grounded research call when amber or red dots are present - Forum-corroboration panel — the brief surfaces 1-3 recent diaspora-community posts (with month/year) that confirm a fact someone there found surprising. These are listed as "what other people who went last month said", not as authoritative — they corroborate or contradict the consulate's official answer - "What if it's wrong" override — the user types a discrepancy ("my cousin paid £45, not £42"); the app stores the user's note, re-runs the relevant grounded check, and shows both numbers with their sources - Photograph-specification helper — the per-mission passport-photo specification (millimetres, head-size, background, expression, glasses, head covering) plus a checklist for the photo shop ("ask for 'Argentine consular photo, 4 × 4 cm, white background, neutral expression'") - Document-uploader (private) — the user can photograph each required document into their own private Firestore so they have a checklist they can tick off the night before; nothing is sent to Gemini unless the user opts in for OCR-and-fill on a specific document - "Bring a backup" suggestions — for items where missions vary mission-by-mission ("some Brazilian consulates accept a UK utility bill as proof of residence, others only a council-tax letter; bring both"), the brief recommends a backup with a one-sentence reason - Printable brief — A4 / Letter / phone-PDF, offline-readable, one page where possible, with QR codes back to the live brief if any fact is amended - Multilingual at the top of every screen — the user sets reading language once; the consulate's source page is shown verbatim in its original language; the brief is rendered in the user's first language; the script is rendered in the language the desk will use - Calendar block — one tap creates an iCal event titled "Embassy appointment — Consulate General of the Philippines, Toronto", with the document checklist in the description and an alarm at 06:30 the morning of - Multi-trip mode — for users running an OFW family, a community caseworker, or a diaspora-association volunteer: stash multiple briefs (one per family member, one per client) under labels and reuse the template - TTS playback of the script — the desk sentences read aloud at counter cadence, with one-tap "loop this sentence" to rehearse - "Bring a friend" mode — share the brief read-only with a partner or sibling who is going with the user; they see the same flowchart and script - Donation export of completed briefs — for community caseworkers, an anonymised export of the completed-brief structure (no personal data) to seed the next family doing the same appointment ## 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) - **Long-context grounded search** (Gemini 3.5 Flash + `google_search`) — the hero call. Reads the consulate's own pages (in their original language), the home-country government portal, the destination-country resident-permit authority, and the three or four most recent diaspora forum threads that mention this mission and this service. Produces a structured brief. Grounding citations are what make the brief trustable; without them the app is just an LLM monologue. - **Multilingual translation + register awareness** (built into Gemini 3.5 Flash) — the consulate's source page may be in Spanish, Portuguese, Italian, French, Tagalog, Tamil, Hindi, Urdu, Bengali, Mandarin, Cantonese, Korean, Vietnamese, Khmer, Amharic, Tigrinya, Swahili, Farsi, Arabic. The user's first language is selected once and respected everywhere. The counter script is rendered in the official language of the mission with phonetic guidance and a parallel first-language translation. Register is "polite-formal-official", not casual. - **Structured output / JSON Schema** — the response matches the `EmbassyBrief` schema below. The structured output and the grounded search are NOT in the same call: a single Gemini call cannot use `responseSchema` and `google_search` at once. The grounded call instructs the model to emit JSON in the text body, and the server parses it. Grounding citations come from `response.groundingMetadata.groundingChunks[].web.uri`. - **Long context (1M tokens)** — the brief is built from the consulate's own page (sometimes 6,000 words of layered FAQs), the home-country portal page (3,000 words), the destination-country relevant immigration page (2,000 words), and several diaspora forum threads (5,000-15,000 words). Combined that is comfortably 30k-60k tokens on the first call. **Guardrail**: cap the combined source set at ~250k tokens; if the search returns more, drop the lowest-relevance source first (older diaspora threads beyond the most-recent-three, then the destination-country resident-permit page if it is not the gating fact). - **Multimodal image input** (Gemini 3.5 Flash) — optional, for the document-uploader. The user can photograph an existing document (their child's UK birth certificate, their own DNI, their expired passport) and the app reads the dates, names, and document numbers to verify they meet the validity window of the brief. Documents are processed only when the user explicitly opts in for a single document. - **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — narrates the counter script in the language of the desk worker, at the cadence of an official greeting at a window: clear, polite, slightly slower than conversational. Voice locale is picked via `languageCode` to match the consulate's official language. - **Thinking levels** — `high` for the primary grounded-research call (the model has to reconcile sources that disagree, infer the most recent fee, surface ambiguities for the user). `low` for translation of the script, the per-document validity check, and the freshness re-run on a known brief. ### Backend services - **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). **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 "bring a friend" share and for community-caseworker multi-trip mode) requires the sender domain to be authorised in Firebase Auth. Briefs are private to the user; "bring a friend" share is explicit and per-brief. - **Database — Required.** Firestore for `users`, `briefs`, `documents` (the user's own document metadata, never the photographs themselves), `freshness_log`, `script_audio_cache`, `forum_corroborations`, `user_overrides`. - **File storage — Required.** Firebase Storage for the user's private document photographs (when they opt in for the OCR-verify-a-document feature), and for the printable brief PDFs. **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before the first document upload. Pre-signed URLs only; document photographs are never publicly addressable, never shared with "bring a friend" by default. - **Email — Required (transactional).** "Bring a friend" share invitation; the printable brief delivered to the user's own inbox the morning of the appointment; the calendar-block iCal as an attachment. - **Payments — Not needed for v1.** Free for personal use. A future paid tier could be for community-caseworker organisations running multi-trip mode at scale. - **External APIs:** Gemini API for all intelligence; Google Search via the Gemini `google_search` grounding tool; the app does not call third-party scrapers directly. Calendar block via `iCalendar`-format text file delivered as a download or email attachment (no Google Calendar API integration in v1; that would require Google Calendar scopes the AIS Build harness does not auto-provision). **Environment variables:** every secret (Gemini API key, Firebase service-account JSON) lives in environment variables — never in client bundle. Include a `.env.example`. **Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete my account' inside the UI · explicit opt-in for any analytics · the user's documents are never sent to Gemini for model training (use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms) · the user's passport number, full name, child's date of birth, and any document photographs are processed only on explicit per-document opt-in and never leave the user's own Firestore by default. **Read this first — prompt-craft rules that apply to every call in this template:** 1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below. 2. **Pin `thinkingLevel` explicitly** per call. See the matrix. 3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schema is below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.** Note that the primary research call CANNOT use `responseSchema` — see below. 4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate. 5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled. 6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow. 7. **Grounded responses can wrap JSON in ```json fences or add prose preamble.** Server-side, strip fences and brace-extract: ```typescript function safeExtractJSON(raw: string): T { const clean = raw.replace(/```json\s*|```/gi, '').trim(); const s = clean.indexOf('{'); const e = clean.lastIndexOf('}'); if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response'); return JSON.parse(clean.slice(s, e + 1)) as T; } ``` 8. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing. ### Per-call model + tools matrix | Call | Model | thinkingLevel | Tools enabled | |------|-------|---------------|---------------| | Grounded research → embassy brief (primary) | `gemini-3.5-flash` | high | `google_search` grounding (no `responseSchema` — emit JSON in text body, parse server-side) | | Translate counter script + brief into user's first language | `gemini-3.5-flash` | low | (none) | | Verify a single user-uploaded document against the brief's validity window | `gemini-3.5-flash` | low | (none, multimodal image input) | | Re-check a single fact (the amber-dot freshness re-run) | `gemini-3.5-flash` | low | `google_search` grounding | | Forum-corroboration extraction (most-recent diaspora threads on this mission + service) | `gemini-3.5-flash` | low | `google_search` grounding | | Generate TTS narration of counter script | `gemini-3.1-flash-tts-preview` | n/a | n/a | *Note for builders:* on TTS calls, omit `thinkingConfig` entirely — the field is not supported on those models. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. *Critical note on grounded JSON:* `responseSchema` and `google_search` grounding are mutually exclusive in a single Gemini call. For the primary research call and the freshness re-run, instruct the model to emit JSON inside the text body, parse it server-side with the Zod schema, and validate. The grounding citations live in `response.groundingMetadata.groundingChunks[].web.uri` — do NOT ask the model to include URLs in the JSON body; it will hallucinate them. ### Primary structured-output schema (seed this verbatim in the prompt) ```typescript import { z } from "zod"; const Currency = z.enum([ "GBP", "EUR", "USD", "CAD", "AUD", "NZD", "JPY", "KRW", "INR", "PKR", "BDT", "PHP", "SGD", "HKD", "TWD", "CNY", "ARS", "BRL", "MXN", "CLP", "COP", "PEN", "UYU", "ZAR", "NGN", "KES", "ETB", "ERN", "AED", "SAR", "EGP", "MAD", "TND", "RUB", "TRY", "ILS", "JOD", "VND", "THB", "IDR", "MYR", "KHR", "CHF", "NOK", "SEK", "DKK", "PLN", "CZK", "HUF", "RON", ]); const PaymentMethod = z.enum([ "cash_exact_change", "cash_any", "card_debit_only", "card_credit_or_debit", "bank_money_order", "bank_cheque_to_specified_payee", "online_pre_payment_only", "mobile_payment_local", "other", ]); const FreshnessDot = z.enum(["green", "amber", "red", "unknown"]); const Citation = z.object({ source_label: z.string(), // "Consulate General of the Philippines, Toronto — passport renewal page" source_url: z.string(), // populated SERVER-SIDE from groundingMetadata, NOT by the model source_kind: z.enum([ "consulate_official", "home_country_government", "destination_country_government", "diaspora_forum", "diaspora_association", "news_or_media", "other", ]), source_language: z.string(), // BCP-47, "es-AR", "tl-PH", "pt-PT" verified_on: z.string(), // ISO date the server recorded the check snippet_verbatim: z.string(), // the sentence the fact came from, verbatim from the source freshness: FreshnessDot, }); const DocumentRequirement = z.object({ item_name: z.string(), // "child's expired Argentine passport" required_form: z.enum([ "original", "certified_copy", "notarised_copy", "apostilled_copy", "uncertified_copy", "either_original_or_certified_copy", ]), validity_window_note: z.string().nullable(), // "issued within the last six months" translation_requirement: z.string().nullable(), // "certified translation into Spanish by a public translator registered with the consulate; UK sworn translators not accepted" how_to_obtain_if_missing: z.string().nullable(), specification_detail: z.string().nullable(), // "passport photo: 4 × 4 cm, white background, neutral expression, no glasses" backup_suggestion: z.string().nullable(), // "some consulates accept a council-tax letter; bring both" why_it_matters_short: z.string(), // one sentence the user will remember citations: z.array(Citation), }); const FeeLine = z.object({ line_label: z.string(), // "consular act — child passport renewal" amount: z.number(), // 42 (in the currency below) currency: Currency, payment_method_at_window: PaymentMethod, what_this_is_for: z.string(), // one sentence refundable_if_no_show: z.boolean(), citations: z.array(Citation), }); const AppointmentDayStep = z.object({ step_number: z.number(), step_label: z.string(), // "biometrics — window 3" what_happens_here: z.string(), // one paragraph estimated_minutes: z.number().nullable(), bring_to_this_step: z.array(z.string()), // ["child's expired passport", "photo printout"] watch_out_for: z.string().nullable(), // "they will photograph the child; have her ready" citations: z.array(Citation), }); const CounterScriptLine = z.object({ moment_at_counter: z.string(), // "confirming you are the parent of record" desk_language_phrase: z.string(), // "Soy la madre de la solicitante, [nombre completo]." desk_language_locale: z.string(), // "es-AR" phonetic_guide: z.string().nullable(), user_first_language_translation: z.string(), notes_for_the_user: z.string().nullable(), // "say it once; do not over-explain. the desk worker will ask follow-ups if they need them." }); const ForumCorroboration = z.object({ source_label: z.string(), source_url: z.string(), // populated server-side from grounding metadata posted_on: z.string().nullable(), // ISO date if extractable corroborates_what: z.string(), // "fee paid in £, not in pesos" agrees_with_consulate_official: z.boolean(), one_sentence_summary: z.string(), }); const UserSituation = z.object({ user_nationality: z.string(), // BCP-47 country, "PH" user_first_language: z.string(), // BCP-47, "tl-PH" mission_country: z.string(), // "CA" (the country the mission is IN) mission_city: z.string(), // "Toronto" mission_kind: z.enum([ "embassy", "consulate_general", "consulate", "high_commission", "trade_office", "interest_section", "honorary_consulate", "other", ]), service: z.string(), // "child passport renewal (under 18)" applicant_relationship_to_user: z.enum([ "self", "child_under_18", "child_over_18", "spouse", "parent", "grandparent", "ward_or_dependent", "client_or_caseload", "other", ]), situational_flags: z.array(z.enum([ "dual_citizen", "name_change_since_last_passport", "lost_not_stolen", "stolen_with_police_report", "expired_more_than_5_years", "born_abroad_never_registered", "first_time_applicant", "applicant_minor_under_5", "applicant_minor_5_to_17", "applicant_in_a_wheelchair", "applicant_does_not_speak_desk_language", "user_does_not_speak_desk_language", ])), }); const EmbassyBrief = z.object({ brief_id: z.string(), generated_on: z.string(), // ISO datetime last_freshness_check: z.string(), // ISO datetime user_situation: UserSituation, one_paragraph_summary: z.string(), // the brief boiled down to ~80 words documents: z.array(DocumentRequirement), fees: z.array(FeeLine), fees_total_in_window_currency: z.number(), appointment_day_flow: z.array(AppointmentDayStep), counter_script: z.array(CounterScriptLine), forum_corroborations: z.array(ForumCorroboration), user_overrides: z.array(z.object({ field_path: z.string(), // "fees[0].amount" user_typed_value: z.string(), user_note: z.string().nullable(), recorded_on: z.string(), })), mission_metadata: z.object({ official_name: z.string(), // "Consulate General of the Philippines in Toronto" address: z.string().nullable(), phone: z.string().nullable(), email: z.string().nullable(), appointment_booking_url: z.string().nullable(), desk_language: z.string(), // BCP-47, "en-CA" or "es-AR" estimated_total_minutes_in_building: z.number().nullable(), bag_and_phone_policy: z.string().nullable(), children_allowed_in_policy: z.string().nullable(), citations: z.array(Citation), }), confidence_overall: z.number().min(0).max(1), flagged_for_user_review: z.array(z.object({ field_path: z.string(), reason: z.string(), })), }); type EmbassyBrief = z.infer; ``` ### Common failure modes (and how to avoid them) - Agent silently downgrades `thinkingLevel` for the primary research call to save cost — pin `gemini-3.5-flash` at `thinkingLevel: high` explicitly. Flash with grounding misses the reconciliation step where two sources disagree and a third confirms which is current; the brief is only as good as that reconciliation. - Agent uses `responseSchema` and `google_search` in the same call — they are mutually exclusive. The model will either ignore one or the call will be rejected. For the grounded calls, instruct the model to emit JSON in the text body and parse server-side. - Citation URLs hallucinated — never ask the model to include URLs inside the JSON body. They come from `response.groundingMetadata.groundingChunks[].web.uri` server-side. Wire each Citation object's `source_url` to a grounding chunk by matching the `snippet_verbatim` to the chunk content. - Stale fees presented as current — the model finds a 2021 fee schedule and prints it. Pin in the system instruction: "the verified_on date on every fee citation must be within the last 90 days of generated_on; if no fee citation younger than 90 days exists, set freshness: red and flag the fee in flagged_for_user_review". - Generic checklist printed because the model conflated services — "passport renewal" alone is not specific enough. The system instruction requires the model to refuse and ask for clarification if the user_situation.service is ambiguous (adult vs. child, first-time vs. renewal, dual-citizen renunciation question vs. not). - Counter script in the wrong register — the model writes casual sentences for a polite-formal moment. Pin in the system instruction: "register is polite-formal-official; phrases the desk worker would expect from a citizen at a window. Do not write casual sentences. Do not write sentences that volunteer information not asked." - Diaspora forum posts cited as authoritative — they are not. They are surfaced as forum_corroborations, never as the source for a fee, a document requirement, or a procedure. Pin: "official sources are consulate_official, home_country_government, and destination_country_government; everything else is corroboration." - Multilingual translation flattening register — the script asked for in Castilian Spanish ("Soy la madre de la solicitante") comes back in a casual register. The translation call is given a register hint per call. - TTS reads the script in the wrong locale — the script is in Castilian Spanish but the voice picked is Latin-American Spanish, or vice versa. Pin TTS voice via `languageCode` to match the mission's `desk_language` exactly (es-AR for Argentine missions, es-ES for Spanish missions, pt-BR for Brazilian, pt-PT for Portuguese, etc.). - The model writes an answer the consulate website explicitly contradicts — the model is "helpful" beyond the source. Hard rule in the system instruction: if a fact is not in a cited source, do not assert it; surface the gap as a flagged_for_user_review entry. - The model invents a fee amount, currency, or payment method — pin the model to refuse if no cited source confirms; surface the gap as a flagged_for_user_review entry. ### Negative constraints (hard rules) - Do NOT print a fact without a citation. If the source set does not confirm a fact, the brief surfaces the gap with `flagged_for_user_review` and tells the user explicitly which fact is unverified. The user goes into the building with a known-unknown, not a confident-falsehood. - Do NOT mix sources of differing authority. Consulate-official, home-country-government, and destination-country-government are authoritative; everything else (diaspora forums, news media, blog posts, association pages) is corroboration only. - Do NOT translate official documents' names. "DNI", "Aadhaar", "OCI", "NIE", "Tarjeta Sanitaria", "Carte de séjour", "Aufenthaltstitel" stay verbatim. Add a parenthetical English gloss on first occurrence only. - Do NOT modernise government-form vocabulary. If the consulate's site uses "DS-160" or "Form-4567A" or "Formulario 02-R", that is the name in the brief. - Do NOT print a fee unless a cited source from the last 90 days confirms the amount AND the currency AND the payment method at the window. - Do NOT invent a counter-script phrase. The script is composed only of polite-formal sentences a citizen would say at a window in the desk's official language; no creative phrasing; no marketing-style framing. - Do NOT assert legal consequences ("you will lose your citizenship if you tick the wrong box"). Surface the cited rule and the consulate's own guidance; do not interpret legal consequences on behalf of the user. - Do NOT auto-publish, auto-share, or auto-export. Briefs are private by default; "bring a friend" share is explicit per brief, read-only. - Do NOT keep document photographs after the user clicks "I have packed this document". Pre-signed Firebase Storage URLs for documents expire on opt-out; the storage object is deleted within 24 hours unless the user explicitly opts to keep it for their own checklist. - Do NOT use the user's documents, situation, or queries 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. The capabilities-info panel says this in plain English. - Do NOT extrapolate to immigration outcomes. If the user is renewing a child passport at a consulate, do not opine on whether the child should choose dual citizenship at 18, naturalise in the destination country, or take any other downstream action. ### 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: Grounded research → embassy brief (primary) Model: `gemini-3.5-flash` · thinkingLevel: high · Tools: `google_search` grounding (no `responseSchema`) ``` You are preparing a single dated, cited brief for a person about to attend a consular or diplomatic mission appointment. The user_situation provided names: the user's nationality, the country and city of the mission they are attending, the kind of mission (embassy, consulate general, consulate, high commission, honorary consulate, trade office, interest section), the specific service (child passport renewal, adult passport renewal, birth registration of a child born abroad, OCI card, NRI services, power-of-attorney legalisation, police-clearance certificate, marriage certificate apostille, statutory declaration, dual- nationality decision form, and so on), the applicant's relationship to the user (self, child under 18, spouse, parent, ward), and situational flags (dual citizen, name change since last passport, lost not stolen, expired more than five years, born abroad never registered, applicant a minor under 5). Use `google_search` grounding to read: 1. The consulate's own official page for this service (highest authority — required source). 2. The home-country government's national portal page for this service if it exists (e.g. the Philippine DFA passport-renewal page; the Argentine RENAPER page; the Indian Ministry of External Affairs page; the Brazilian Itamaraty page; the Mexican SRE page; the Pakistani NADRA page; the Bangladeshi Department of Immigration page). 3. The destination country's relevant resident-immigration page if the appointment depends on it (e.g. UK Home Office page on foreign nationals' obligations during a passport renewal). 4. The three most recent diaspora-community forum threads (within the last 18 months, preferably within the last 6) that mention this mission and this service. Use these as corroboration only; they are never the primary source for a fee or a document requirement. Reconcile what the sources say. If two sources disagree (a 2023 consulate page lists a £40 fee, a 2025 Facebook post from the consul lists £45), prefer the more recent source AND surface the discrepancy as a flagged_for_user_review entry. Output JSON ONLY, matching the EmbassyBrief schema given to you in the user message. Do NOT wrap in markdown. Do NOT include any sentence outside the JSON. Citation handling: - The JSON's Citation objects have a `source_url` field. Leave it as an empty string in your output. The server will populate `source_url` from `response.groundingMetadata.groundingChunks[].web.uri` by matching the `snippet_verbatim` to the chunk's content. - Do NOT invent URLs. Do NOT include URLs anywhere in the JSON besides the (empty) source_url field. The server checks for any http(s):// substring in the JSON body and rejects the response. - Every fact in the brief — every document, every fee line, every flowchart step, every metadata field — carries at least one Citation. If a fact has zero citations, do NOT print the fact; instead, add a flagged_for_user_review entry naming the missing fact. - Every Citation has a `snippet_verbatim` — the exact sentence the fact was extracted from, copied verbatim from the source (no paraphrasing). The server uses this to match grounding chunks. Hard rules: - Documents: name each item the consulate explicitly requires. For each, name the required_form (original, certified copy, notarised copy, apostilled copy), the validity_window_note (e.g. "issued within the last six months"), and the translation_requirement (which translator authority, into which language). If the consulate explicitly accepts more than one form of a document, name them all. - Fees: name each line item separately. Do NOT print a fee unless a cited source from the last 90 days confirms the amount, the currency, and the payment method at the window. Set freshness on the citation accordingly. If no recent source exists, omit the fee and add a flagged_for_user_review entry. - Appointment day flow: 5 to 8 steps. Each step has a step_label (one line), a what_happens_here paragraph (two to four sentences), estimated_minutes if a cited source mentions it, bring_to_this_step (the items the user hands over at this step), and watch_out_for (a one-sentence specific tip from a recent source). - Counter script: 2 to 5 short paragraphs covering the moments at the counter where wording matters. Register is polite-formal- official, in the desk's official language. Phrases a citizen would say at a window — never casual, never confessional, never volunteer information not asked. Each line has the desk_language_phrase verbatim, a phonetic_guide if the desk's language differs from the user's first language (use IPA where helpful), a user_first_language_translation, and a one-sentence notes_for_the_user. Do NOT compose sentences that propose action the desk worker has not requested. - Forum corroborations: 1 to 3 entries from the last 18 months, each with corroborates_what (the fact it confirms or contradicts), agrees_with_consulate_official (true / false), and a one_sentence_summary. - Mission metadata: official_name verbatim from the consulate's page (preserve "Consulate General of the X in Y" exactly); the desk_language is the language the user will hear at the window (the consulate's official language, e.g. es-AR at the Argentine consulate in London, even though that consulate is in London). - confidence_overall: 0.85 or higher if every fact has at least one consulate_official citation from the last 90 days; lower otherwise. Be honest. The user reads this number and decides whether to phone the consulate before the appointment. - flagged_for_user_review: name every fact that is missing a recent citation, every place where two sources disagree, every situational flag the consulate's page does not explicitly address. No commentary outside the JSON. ``` --- ### Call: Translate counter script + brief into user's first language Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You translate an embassy preparation brief from a working language into the user's first language. You translate to be read by a person who is about to walk into a government building in a country that is not yet home. The priority is clarity, then warmth. The translation should make the reader feel that an experienced friend is explaining what will happen — not that a corporate help page is being recited. Hard rules: - Preserve official-document names verbatim. "DNI", "Aadhaar", "OCI", "NIE", "Tarjeta Sanitaria", "Carte de séjour", "Aufenthaltstitel", "DS-160", "Form-4567A", "Formulario 02-R" stay in the source language. Add a parenthetical first-language gloss on first occurrence only and never thereafter. - Preserve currency symbols and amounts verbatim. £42, ARS 38,000, ₱1,200 stay as written. - Preserve consulate official names verbatim. "Consulate General of the Philippines in Toronto" stays in English even when the rest of the brief is in Tagalog. - Preserve the counter script's desk_language_phrase verbatim. Do NOT translate the desk phrase into the user's first language — the user needs to say it AT the window in the desk's language. Translate only the user_first_language_translation field, the notes_for_the_user, and surrounding brief copy. - Preserve register. The consulate's official tone is polite- formal-official; the brief tone is plain, warm, direct. Do not flatten into corporate help-page voice. - Preserve hedges. If the source brief says "the consulate's page does not explicitly confirm this fee for dual-citizen renewals; phone ahead to confirm", that hedge stays. - Preserve numbers, dates, and document validity windows exactly. Do not round, do not paraphrase. Output: the brief in the user's first language as a single JSON object with the same shape as the input, with the translatable string fields replaced. No commentary outside the JSON. ``` --- ### Call: Verify a single user-uploaded document against the brief's validity window Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none, multimodal image input) ``` You receive a single photograph of a personal document the user is preparing to bring to their consulate appointment. Photographs may be: a national identity card, a passport biodata page, an expired passport, a birth certificate (original, certified copy, or apostilled), a marriage certificate, a residence permit, a council- tax letter, a utility bill, a passport photo printout. The user has opted in for this specific document. Read the document carefully. Extract: - document_kind (best-fit label) - document_number (verbatim if visible) - issue_date and expiry_date (ISO if extractable; null if not) - holder_name (verbatim) - issuing_authority (verbatim) - any visible expiration / validity stamps - any redactions the user appears to have applied (do not extract the redacted content; record only that redaction is present) Then compare against the brief's DocumentRequirement for this item: - Does the issue_date fall within the validity_window_note? (e.g. "issued within the last six months") - Does the expiry_date fall after the appointment date? - Does the holder_name match the expected applicant? - Does the document appear to be the required_form (original vs. certified copy vs. apostilled)? Output JSON ONLY: { "extracted": { ... }, "verdict": "meets_requirement" | "does_not_meet" | "ambiguous", "reasons": [ "..." ], "what_to_do_next": "...", "reading_confidence": 0.0 - 1.0 } Hard rules: - Do NOT extract or echo the document number to any caller other than the user's own session. The server enforces this; your output is consumed only by the user's own client. - Do NOT speculate about whether the document is "real" or "fraudulent". Read what is visible; surface what you cannot read; never accuse. - Do NOT translate the holder_name or issuing_authority. They stay verbatim. - If the photograph is too blurry to read a critical field, set reading_confidence below 0.7 and name the field in reasons. No commentary outside the JSON. ``` --- ### Call: Re-check a single fact (the amber-dot freshness re-run) Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding ``` You re-verify one fact from a previously-generated embassy brief. The user has opened the brief; the freshness dot for this fact is amber (verified between 7 and 30 days ago) or red (older than 30 days). Re-check the fact against the same authoritative sources the original brief drew on. You receive: the fact (e.g. "Consulate General of the Philippines in Toronto charges CAD 70 for a child passport renewal, payable by money order to 'Consulate General of the Philippines'"), the original Citation set, and the user_situation context. Output JSON ONLY: { "still_accurate": true | false | "ambiguous", "new_value_if_changed": "...", "new_citation": { source_label, source_kind, source_language, verified_on, snippet_verbatim, freshness }, "old_citation_status": "still_valid" | "out_of_date" | "removed", "one_sentence_explanation": "..." } Hard rules: - Use `google_search` grounding. Leave `source_url` empty in the JSON; the server will populate it from grounding metadata. - If the new source disagrees with the old, surface both — do not silently overwrite. - If no source confirms the fact within the last 90 days, set still_accurate to "ambiguous" and explain. - Do NOT invent a value. If the consulate's page no longer lists the fact, say so explicitly in one_sentence_explanation. No commentary outside the JSON. ``` --- ### Call: Forum-corroboration extraction Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding ``` You find recent diaspora-community forum posts that corroborate or contradict a specific fact in an embassy brief. You receive: the user_situation context, the fact to corroborate (e.g. "the Consulate General of the Philippines in Toronto requires the child's previous passport, even if expired"), and the authoritative source citation already gathered. Search for posts on diaspora community forums (Reddit's r/PhilippinesUK, r/IndiansInUK, Facebook groups for Filipino families in Toronto, the OFW community pages, the Argentine-in- London forum, the Brazilians-in-Lisbon forum, the Tamil-in-Sydney forum, equivalent communities for any diaspora). Limit to posts from the last 18 months. Output JSON ONLY: an array of 1 to 3 ForumCorroboration objects. Each one names the source_label (e.g. "Filipino Parents in Toronto, Facebook group, public post"), source_url (leave empty — server populates from grounding metadata), posted_on if a date is visible, corroborates_what (one phrase), agrees_with_consulate_official (true / false), and a one_sentence_summary in the working language. Hard rules: - Do NOT cite a forum post older than 18 months unless no recent post exists; if you must, set posted_on accurately. - Do NOT cite a private group or paywalled source; only publicly readable posts. - Do NOT name the original poster. Refer to "a recent post" or "a poster from June 2025"; never the poster's username. - Do NOT extract the original poster's personal data even when visible. No commentary outside the JSON. ``` --- ### Call: Generate TTS narration of counter script Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` Voice: clear, polite, at the cadence of an official greeting at a window — slightly slower than conversational, never sing-song. Pick the Gemini 2.5 Flash TTS voice whose `languageCode` matches the counter script's `desk_language_locale` exactly: `es-AR` for Argentine missions, `es-ES` for Spanish missions, `pt-BR` for Brazilian missions, `pt-PT` for Portuguese missions, `tl-PH` for Philippine missions, `ta-IN` for Indian missions where the user selected Tamil as the desk script (rare), `hi-IN` for Indian missions generally, `ko-KR` for Korean, `vi-VN` for Vietnamese, `zh-HK` for Hong Kong (Cantonese), `zh-CN` for Mandarin, `ar-EG` or `ar-SA` for Arabic depending on the mission, `fa-IR` for Iran, `fr-FR` for France, `de-DE` for Germany. The locale is set on the voice; pronunciation follows the voice's native locale and is not overridable. Pre-process the text before sending it to TTS: - Read from the counter_script's `desk_language_phrase` (the source-language version the user will speak at the window). - Insert a single ellipsis (`…`) after each complete sentence so the TTS model produces a natural pause for the user to repeat. - Between counter-script lines, insert a blank line plus an em- dash (`—`). Gemini 2.5 TTS does not support SSML `` — these textual cues are how you signal pace. - Skip phonetic guides, parenthetical glosses, and the user- first-language translation. They are subtitle content, not speech. - Target rate: ~95 words per minute — desk cadence, not podcast cadence. Style direction: prepend ONE short directive sentence to the text input, exactly like: "Read clearly and politely, at the cadence of an official greeting at a consular window. …". There is no separate `style` API field on Gemini 2.5 TTS; the directive sentence inside the input is how style is conveyed. Do NOT use SSML `` or `` — they are unsupported. Mid-call voice switching is not supported. The whole script is read in one voice. If the user's situation calls for a second language at the counter (e.g. a Quebec mission where the desk is bilingual French / English), produce a separate TTS call for each language and let the client stitch them. ``` ## 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 OFW grandmother in Toronto.** A Filipina mother in North York is renewing her seven-year-old's passport at the Consulate General of the Philippines on Bay Street. She is fluent in Tagalog and conversational in English. The consulate's official page lists three different fees in three places and her cousin's friend paid a fourth amount last March. The app rebuilds the brief in Tagalog with the Tagalog consulate-page sentences shown verbatim, the fee reconciled to the most recent authoritative source, and the counter script in Filipino English (the language she will hear at the window in Toronto). - **The Tamil father in Sydney.** A father with dual Indian / Australian citizenship is at the Indian High Commission on Cricket Place. His twelve-year-old daughter holds dual citizenship; India's renunciation-of-Indian-citizenship rule for adult dual nationals is a fact with consequences he cannot reverse. The app surfaces the rule with the cited High Commission page, explains it neutrally without opining, and produces the OCI-application script in Tamil with the desk language in English. - **The Brazilian mother in Lisbon.** A mother newly arrived in Lisbon is registering her child's birth at the Brazilian Consulate General. The app reads the Itamaraty page (in Portuguese), reconciles with the Consulado-Geral-do-Brasil-em-Lisboa announcements, and produces the brief in Brazilian Portuguese with notes on the differences from European Portuguese where the desk worker may use them (vocês vs. vocês, fila vs. bicha). - **The Argentine mother in London.** The hero demo. A mother is renewing her dual-citizen daughter's Argentine passport at the Consulate General of Argentina on Brook Street. The brief is in Castilian Spanish with a side-by-side English translation; the counter script is in Castellano-rioplatense (es-AR); the fees are in pounds with a note that the consulate has, historically, accepted only postal money order until April 2024 and now accepts card payment. - **The Vietnamese international student in Berlin.** A second-year student is renewing his Vietnamese passport at the Vietnamese Embassy in Berlin. The embassy publishes its fee schedule only on a Word document last updated in 2023; the app surfaces a more recent fee from a community Facebook post (June 2025) and explicitly flags the discrepancy. - **The Eritrean mother in Frankfurt.** A mother is renewing her child's Eritrean passport at the Eritrean Embassy. The 2% diaspora-tax question is a politically loaded one; the app cites the embassy's own page verbatim, cites the German federal foreign office's public guidance, and refuses to interpret the political question on the user's behalf. - **The community caseworker in Houston.** A volunteer at a Mexican-American community centre runs five briefs a week for clients at the Consulado de México. The app's multi-trip mode stores per-client briefs under labels (no client personal data, just situation tags) and exports an anonymised template for the next family doing the same case. - **The grandfather renewing after fifteen years.** A grandfather is renewing a long-expired Pakistani passport at NADRA's London centre. The form has changed twice since he last applied; the app's brief explains what is different from 2009, cites the NADRA-London page, and produces a polite-formal counter script in Urdu. - **The asylum-seeker without diaspora forum corroboration.** A new arrival from a country with a small diaspora in the UK has zero recent forum posts to corroborate the consulate's page. The app surfaces this gap explicitly: "no recent community-forum corroboration available; the brief is based solely on the consulate's official page and may not reflect day-of-appointment variation." The user is told to phone ahead. - **The grandparent on a five-day trip to meet a grandchild born abroad.** A Pakistani grandmother needs an emergency travel document at the High Commission to fly home next Tuesday. The brief flags the same-day-emergency service explicitly and produces the script for asking for expedited collection. - **The dual-citizen child's first appointment.** A four-year-old's first passport appointment at the Brazilian Consulate-General in São Paulo (the family is on a return trip). The app handles the "minor under 5" situational flag with the consulate's specific requirements: both parents' presence, the child's UK birth certificate apostilled in the UK, the photograph spec for children under 5. - **The trans applicant changing the gender marker.** A user is updating their gender marker on their Mexican passport at the Consulado in Houston. The brief cites Mexico's 2022 administrative-corrections rule verbatim, names the documents required, and produces a polite-formal script for the desk that does not require the user to disclose more than the form asks. ## 6. Page structure Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent. 1. **Welcome / sign-in.** A photographed-looking image of a hand holding a maroon Filipino passport open to the biodata page on a kitchen table at evening, a clipped photo printout next to it. One paragraph: "Embassy Appointment is a single, dated, cited brief for the person about to walk into a consulate — with the exact documents, the exact fees, and the exact words in your first language." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample brief" → loads the Argentine-consulate-London demo brief from section 8a. 2. **Pick-a-trip flow.** A short multi-step form: your nationality, the mission's country and city, which mission (the curated list reads from grounded data — "Consulate General of Argentina in London", "Consulate of Argentina in London — Wedmore Street" if more than one exists), the service (curated list per mission), the applicant's relationship to you, the situational flags. Each step is one screen on mobile, three on desktop side-by-side. The "generate brief" button reads "Build my brief" with a small honest progress indicator beneath. 3. **Brief-generation progress.** A vertical list of honest steps: "Reading the consulate's page in Spanish…" → "Reading the home-country government page…" → "Finding recent community posts that mention this service…" → "Reconciling fees…" → "Drafting the counter script in Castellano-rioplatense…" → "Translating into Tagalog…" Each step takes 4-12 seconds; total brief generation under 60 seconds. 4. **Brief detail view.** A three-column layout on desktop, stacked on mobile. Left column: the one-paragraph summary, the freshness indicator, and the source-confidence chip. Middle column: documents (with each item's required_form, validity, translation requirement), fees (with each line's amount, currency, payment method, and what-it-is-for sentence), the appointment-day flowchart (numbered steps, expandable). Right column: the counter script in the desk's language with phonetic guides; tap-to-play TTS per line; one-tap "loop this line" for rehearsal. Sticky header: mission name → service → "(i) why this brief is the way it is". 5. **Source pane.** Every fact in the brief is tagged with a citation chip; tap to slide in a side pane showing the source URL, the snippet verbatim, the publication date the snippet was confirmed, the source kind (consulate-official, home-country-government, etc.), and the freshness dot. Forum corroborations are shown in their own subsection of the pane with a separate label. 6. **Document checklist (private).** The list of documents to bring, each with a tick-box. Tap a document → upload a photograph (optional, opt-in). If uploaded, the app reads the dates and confirms whether the document meets the brief's validity window; if not, a one-line note explains why and what to do next. Photographs are deleted within 24 hours unless the user opts to keep them. 7. **Counter-script rehearsal view.** A full-screen practice mode: one line at a time, large type, the desk-language phrase, the phonetic guide underneath, the first-language translation below that, a "play" button, a "loop" button, and "I have this" / "rehearse again" buttons. 8. **Override panel.** "Something here doesn't match what I was told." The user picks the field (fees, a document, a flowchart step), types the discrepancy, taps "re-check". The app re-runs the relevant grounded check and shows both versions side-by-side, with timestamps and sources. 9. **Calendar block.** "Add to calendar." One-tap iCal download with the brief printable as the event description and an alarm at 06:30 the morning of. 10. **Printable brief.** A4 / Letter / phone-PDF, offline-readable, one page where possible. The PDF is laid out for a single-glance read in the queue: documents at the top, fees in the middle, flowchart on the lower-left, counter script on the lower-right. A small QR code in the corner links back to the live brief. 11. **"Bring a friend" share.** A read-only magic-link to the brief for a partner, sibling, or community caseworker. The shared view excludes the user's private document uploads by default; the user can include them per-share. 12. **Multi-trip mode (community caseworkers).** A sidebar listing briefs by label ("Mendoza family — Mexican consulate Houston", "Patel grandmother — Indian High Commission Sydney"). One-click duplicate of a brief template (situation tags only, no personal data) for the next family doing the same case. 13. **Footer.** "Built so nobody walks in unprepared." Privacy: "Your brief is yours. We never train on it." Capabilities `(i)` icon in header. ## 6b. First-visit onboarding Show a **first-visit onboarding** the first time a visitor lands on the app (detect via `localStorage` flag; do not show on return visits). Three slides, dismissible at any time. Persistent re-entry: a `?` icon in the header reopens it. **Slide 1 — What this is.** - Headline: "Welcome to Embassy Appointment." - Subhead: "A single, dated, cited brief for the person about to walk into a consulate — with the exact documents, the exact fees, and the exact words in your first language." - One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic checklist app: every fact is cited to a source, stamped with a date, and re-verified the next time you open the brief. The app refuses to print a fact it cannot back up. - Visual: a small annotated illustration of a brief page with the citation chips, freshness dots, and counter-script section labelled — not a generic passport icon. **Slide 2 — Try it now.** - One short prompt: "Try with the sample brief". - A live demo input pre-loaded with the Argentine-consulate-London child-passport-renewal case from section 8a. - 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the grounded citations on each fee line, the Castilian Spanish counter script, the diaspora-forum corroboration panel). **Slide 3 — How to remix this.** - Headline: "Make this yours." - Three short bullets: - "Swap the sample brief in `/data/seed-brief.json` for your own situation." - "Adjust the prompts in `/server/prompts/` to fit your community's missions and languages." - "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 (long-context grounded search)** — reads the consulate's own page, the home-country government portal, the destination-country resident-permit page, and the most recent diaspora-community forum threads in a single call. Reconciles disagreements. Every fact in the brief is cited to a source the user can open. - **Gemini 3.5 Flash (multilingual)** — translates Spanish, Portuguese, Tagalog, Tamil, Hindi, Urdu, Bengali, Mandarin, Cantonese, Korean, Vietnamese, Khmer, Amharic, Tigrinya, Swahili, Farsi, Arabic, French, Russian, English. Preserves register, document names, and currency symbols. - **Gemini 3.5 Flash (multimodal)** — optionally reads the user's own documents (birth certificates, expired passports, residence permits) to verify they meet the brief's validity window. Documents are processed only when the user explicitly opts in. - **Gemini 3.5 Flash + grounded search** — the freshness re-runs that re-check a single fact against the same authoritative sources, cheap and fast. - **Gemini TTS (2.5 Flash Preview)** — reads the counter script aloud in the desk's official language at counter cadence, so the user can rehearse before walking in. - **Firebase Auth** — Google and Apple sign-in, "bring a friend" share via magic link. - **Firestore** — stores your briefs, syncs across devices in real time. - **Firebase Storage** — keeps the user's private document photographs (when opted in) at upload resolution, with auto-deletion after 24 hours unless explicitly retained. - **Cost note** — see the detailed breakdown in 6d. A typical brief costs about $0.18 of Gemini API spend to generate; subsequent freshness re-runs are about $0.003 each. - **Privacy note** — your situation, your brief, and any uploaded documents are private to you and the people you explicitly share with. 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. Diaspora forum posts cited in the brief are publicly readable and are surfaced as corroboration, never as the authoritative source. **Backend services this app depends on:** - Auth: Firebase Auth (Google sign-in auto-provisioned; Apple sign-in requires Apple Developer config; magic-link email requires sender-domain authorisation) - Database: Firestore (auto-provisioned by AI Studio Build) - Storage: Firebase Storage (must be enabled in Firebase console; not auto-provisioned by AIS Build today) - Email: transactional only — "bring a friend" share invitation, calendar block iCal as attachment - Payments: not used in v1 - External APIs: Gemini API for all intelligence; Google Search via the Gemini `google_search` grounding tool **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) - `FIREBASE_STORAGE_BUCKET` — your Firebase Storage bucket name (after you enable Storage in the Firebase console) **Cost + privacy notes:** - One short paragraph per cost-sensitive capability: the primary grounded research call is the biggest line — typical brief uses ~30k-60k input tokens (the consulate page, the home-country portal, the destination-country page, three forum threads) and ~3k output tokens; runs at about $0.12 per brief. Freshness re-runs at $0.003 each. TTS narration of a 6-line script at about $0.01 per brief. - One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this brief forever" — gone in 60 seconds), what is never sent for training, what happens to uploaded documents (deleted within 24 hours unless explicitly retained). **Documentation links:** - AI Studio Build docs - Gemini API long-context, grounded search, multilingual, multimodal, TTS docs - Firebase Auth, Firestore, Firebase Storage docs - A short note on the `iCalendar` format used by the calendar-block download **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) - **Primary grounded research → brief (Gemini 3.5 Flash, high thinking, `google_search`)** — typical brief ingests ~30k-60k tokens of source content (consulate page, home-country portal, destination-country page, 3 forum threads) at ~$1.50/M input → ~$0.04-$0.075 input. Output ~3,000 tokens at ~$9/M → ~$0.015. Thinking at `high` adds approximately 2-3× the output-token billable for thinking traces. Total per brief: ~$0.10-$0.12. - **Translate brief into user's first language (Gemini 3.5 Flash, low thinking)** — input ~3,000 tokens (the structured brief), output ~3,000 tokens. Total per brief: ~$0.018. - **Verify a single user-uploaded document (Gemini 3.5 Flash, low thinking, multimodal)** — one image + ~500 tokens of context, ~300 tokens output. Per document: ~$0.004. A typical brief verifies 2-4 documents; total: ~$0.016 per brief. - **Freshness re-check a single fact (Gemini 3.5 Flash, low thinking, `google_search`)** — input ~2,000 tokens, output ~500 tokens. Per re-run: ~$0.0003. A brief opened 5 times before the appointment runs ~10 re-runs (amber dots only re-run); total: ~$0.003 per brief over its lifetime. - **Forum-corroboration extraction (Gemini 3.5 Flash, low thinking, `google_search`)** — input ~10,000 tokens of forum threads, output ~500 tokens. Per call: ~$0.015. Runs once per brief generation. - **TTS narration of counter script (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A typical 6-line counter script (~600 characters) ≈ $0.002 per narration. Cached per brief; charged once. - **Expected per-brief cost on first generation:** ~$0.15-$0.20. **Ongoing weekly freshness re-runs** (per brief, until the appointment): ~$0.01-$0.03 over the brief's lifetime. - **Image storage:** Firebase Storage standard tier, ~$0.026/GB/month. The user's optional document uploads delete within 24 hours unless retained. Per active user, ongoing storage cost is effectively zero. ## 7. Design language - **Mood:** A government waiting-room you can actually prepare for. Not a tech product. Not a consular help-page. A patient companion at the kitchen table the night before, with a desk lamp on and a printed page in front of you. The feel is closer to an honest civic-service brochure than to a SaaS dashboard. - **Typography:** Clean grotesque for the brief itself (Inter or Geist) — the brief must read as official-document-clean, not editorial-flourished. A serif display face (Source Serif Pro) only for the welcome screen, the printable PDF's mission name, and the counter-script desk-language phrase (which is the moment of voice in the app — the user speaks these sentences). Body 16-17 px, line-height 1.6, plenty of vertical rhythm. - **Palette:** Off-white background `#FAF8F4` (passport-page paper, not pure white), deep ink `#15181D` for body text, official-stamp navy `#1F3B5C` for citation chips and the mission-name banner, consular red `#8E2B25` reserved for "flagged for user review" and the red freshness dot, warm amber `#B8731F` only for the amber freshness dot, calm green `#2D6A4F` only for the green freshness dot. A muted teal `#2F6D7A` for the user's own annotations and overrides. Borrowed from civic signage, not from SaaS design systems. - **Imagery:** The brief itself is the hero. No hero photographs in the brief view. The welcome screen and the empty-state use a single photographed-looking still life: a passport, a paper printout, a kitchen table at evening. Period maps and consular icons are restrained line drawings, not full-saturation marketing illustrations. - **Hand-feel touches:** The freshness dots are small but legible (10 px). The citation chips have a slight inset shadow so they read as official-document footnotes, not as buttons. The counter-script lines on the rehearsal view sit on a card that looks like a folded printable note. The "loop this sentence" button is a small circular icon that rotates slightly on hover (motion stays under 200 ms, jumps for reduced-motion). - **Spacing:** consistent 4-px base. Generous whitespace — the brief needs air to read calmly. - **Radius:** consistent token set (e.g. 6 / 12 / 20 px). The brief sections use 6; the rehearsal cards use 12; the welcome card uses 20. - **Shadows:** subtle, layered, paper-tinted. Avoid heavy drop-shadows. - **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. No bouncing splash animations. The brief-generation progress indicators tick honestly per step; no fake spinners. The "amber dot re-check" pulses gently for 800 ms and then stops; respect reduced-motion by static glyph. - **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons that match the eventual brief layout, not spinners. Empty states have helpful next-action guidance ("Pick your nationality and your appointment city to build your first brief"). ## 8. Content generation rules - Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'. - Invent plausible families, missions, dates, fees, and document specifications that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on real-world consular practice — a Filipina mother renewing at the Bay Street consulate in Toronto, an Argentine mother at the Brook Street consulate in London, a Tamil father at the Cricket Place high commission in Sydney — but never claim that a specific fictional family is a real family, and never assert a fee or rule that the app would not, in production, source from a real cited consulate page. - Tone: warm, direct, free of corporate language. This template is for a person who has had a long week and is going into a consulate tomorrow morning. - Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'. - Body copy: short paragraphs (2-4 sentences). Use lists where appropriate. - Plain language. Avoid jargon — except where the user already speaks the jargon (the community caseworker user wants to see "OCI", "NRI", "apostille", "Schengen II" in the brief; the asylum-seeker user wants to see "biometric-residence-permit", "BRP", "ARC" used precisely). - Where the app outputs AI-generated content, never label it as "AI says" — let the brief speak as a prepared document. Use small uncertainty cues only where epistemic honesty requires them (a faint amber dot on a fee whose source is older than 30 days; tapping it reveals the source URL and the date it was last confirmed). ## 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 briefs (sidebar):** - "Argentine Consulate London — child passport renewal" (Maria Sofia Reyes Carvalho, 9 yrs, dual citizen, born in the UK) — the hero demo. - "Consulate General of the Philippines, Toronto — child passport renewal" (Liam Carlos del Rosario, 7 yrs, OFW family). - "Indian High Commission, Sydney — OCI surrender for adult dual citizen" (Anjali Iyer Krishnan, 31 yrs, naturalised AU 2024). - "Consulado-Geral do Brasil em Lisboa — birth registration of a child born in Portugal" (Pedro Henrique Oliveira Tavares, 4 months). - "Eritrean Embassy, Frankfurt — child passport renewal" (Selam Tesfay Yohannes, 11 yrs). **Sample brief in detail view (this is what the demo should show — the Argentine Consulate London case):** - **Mission official name:** "Consulado General de la República Argentina en Londres" - **Mission desk language:** es-AR - **Service:** Renovación de pasaporte de menor (dual citizen) - **Applicant:** Maria Sofia Reyes Carvalho, 9, dual UK / Argentine - **One-paragraph summary:** "You are renewing Maria Sofia's Argentine passport at the Brook Street consulate. She is a dual citizen born in the UK; her current Argentine passport expired six weeks ago. Bring four documents, pay £42 by card at window 1 (the consulate moved off money-orders-only in April 2024), confirm at window 3 that you have not renounced Argentine citizenship on her behalf. Total time in the building, on a Tuesday morning, is about 50 minutes." - **Documents (4):** - "Your DNI (Documento Nacional de Identidad), original" — required_form `original`, validity_window_note "currently valid (not expired)", translation_requirement null, why_it_matters_short "the desk confirms you are the parent of record from this document." - "Maria Sofia's expired Argentine passport, original" — required_form `original`, validity_window_note "expired passport is required for renewal; do not surrender it before the appointment", why_it_matters_short "the consulate biometrically links the new passport to the expired one." - "Maria Sofia's UK birth certificate, original or apostilled copy" — required_form `either_original_or_certified_copy`, validity_window_note "no expiry; if a certified copy, must carry the apostille seal", translation_requirement "certified translation into Spanish by a traductor público registered with the consulate; UK sworn translators not accepted", why_it_matters_short "the consulate verifies place of birth and parentage." - "One recent passport photograph for Argentine consular use" — required_form `original`, specification_detail "4 × 4 cm, white background, neutral expression, no glasses, head 65-75% of frame, taken within the last six months", backup_suggestion "the UK passport photo specification is similar but not identical — bring the Argentine-spec photograph, not a UK Post Office photograph", why_it_matters_short "Argentine specification differs from UK specification; the UK Post Office photograph is rejected at the window." - **Fees (3):** - "Consular act — child passport renewal" — £42 in GBP, payable by `card_credit_or_debit` at the window. What this is for: "the consular service of renewing a minor's passport". Refundable if no show: false. Citation: Consulado General de la República Argentina en Londres, fee schedule, verified 2026-05-15. - "Biometric capture fee" — £8 in GBP, payable by `card_credit_or_debit` at the same window. What this is for: "biometric capture (fingerprints, photograph)". Refundable if no show: false. Citation: same. - "Postal-return surcharge (optional)" — £6 in GBP, payable by `card_credit_or_debit` at the same window. What this is for: "delivery of the issued passport to your home address; otherwise collected in person on a later date". Refundable if no show: true. - **Fees total in GBP:** 56.00 (with postal return) or 50.00 (collection in person) - **Appointment day flow (5 steps):** 1. Arrival → security desk → ID check → bag scan → mobile phone allowed but muted. 5 minutes. 2. Ticket machine → select "Renovación de pasaporte — menor" → take a numbered ticket → wait in the seated area. 10-15 minutes. 3. Window 3 → biometrics → Maria Sofia stands at the line; she is photographed and fingerprinted; you are asked to confirm parental relationship. 5-10 minutes. 4. Window 1 → fee payment by card → you are given the receipt and a slip with the collection date. 5 minutes. 5. Collection or postal return → either return in 4-6 weeks with the receipt, or wait for the postal delivery to your home address. 0 minutes today. - **Counter script (3 lines):** - Moment: "confirming you are the parent of record" → desk_language_phrase "Buenos días. Soy [nombre completo], madre / padre de la solicitante. Aquí están mi DNI y el pasaporte anterior de mi hija." → phonetic_guide "BWE-nohs DEE-as. soy [nombre completo], MAH-dreh / PAH-dreh deh la so-lee-see-TAHN-teh. ah-KEE eh-STAHN mee deh-ene-EE ee el pah-sah-POR-teh ahn-tehr-YOHR deh mee EE-ha." → user_first_language_translation "Good morning. I am [full name], mother / father of the applicant. Here are my DNI and my daughter's previous passport." - Moment: "declaring you have not renounced Argentine citizenship on her behalf" → desk_language_phrase "No he renunciado a la nacionalidad argentina de mi hija. Ella mantiene la doble nacionalidad." → user_first_language_translation "I have not renounced my daughter's Argentine citizenship. She maintains dual nationality." - Moment: "asking for postal return" → desk_language_phrase "¿Podemos elegir el envío postal del pasaporte cuando esté listo? Pagaré el cargo adicional." → user_first_language_translation "May we choose postal return of the passport when it is ready? I will pay the additional fee." - **Forum corroborations (2):** - "Argentinos en Londres, Facebook group, public post, March 2026" — corroborates_what "card payment now accepted at window 1", agrees_with_consulate_official true, one_sentence_summary "A poster from March 2026 confirms the consulate has accepted card payment since April 2024, and the money-order-only policy of 2022 is no longer in effect." - "Reddit, r/argentina, public post, January 2026" — corroborates_what "postal return takes 4-6 weeks for minors", agrees_with_consulate_official "ambiguous", one_sentence_summary "A January 2026 poster reports postal return in 5 weeks for a child renewal in London; the consulate's page says 4-6 weeks but a March 2026 announcement extended this to 6-8 weeks for non-residents." - **Flagged for user review (1):** - field_path "fees[2].amount", reason "the consulate's fee-schedule page lists the postal-return surcharge as £6 (verified 2026-05-15), but a March 2026 announcement on the consulate's Facebook page mentions £8 for non-UK addresses. The brief uses £6 assuming a UK delivery address; if the user is forwarding to a non-UK address, confirm at the window." - **Confidence overall:** 0.91 - **Last freshness check:** 2026-05-27 (one day before today) **Sample input artefacts (for the build to demonstrate):** - A screenshot-style mock of the Consulado General de la República Argentina en Londres official page, in Spanish, listing the fee schedule. - A mock Filipino Facebook group post from March 2026 confirming a fee change for the Toronto consulate. - A mock Indian High Commission Sydney page describing the OCI surrender process. - A mock Eritrean Embassy Frankfurt page in Tigrinya describing the diaspora-tax declaration form. - A mock Consulado-Geral do Brasil em Lisboa page describing birth-registration documents for a child born in Portugal. **Sample voice copy:** - Onboarding: "Tell us where you are going. We'll tell you exactly what to bring, what to say, and what each fee is for." - Brief generation: "Reading the consulate's page in Spanish…" / "Finding recent community posts that mention this service…" / "Reconciling the fee schedules…" / "Drafting the counter script in Castellano-rioplatense…" / "Translating into Tagalog…" - Empty brief: "Pick your nationality and your appointment city to build your first brief." - Error (couldn't reconcile): "Two recent sources disagree on this fee. We've shown both. Phone the consulate to confirm before you go." - Save confirmation: "Saved — Argentine Consulate London, child passport renewal, appointment 12 June 2026, 10:30 am." - Freshness amber: "This fee was last verified 18 days ago. Tap to re-check." - Freshness red: "This fee is older than 30 days. Re-checking now." - Override accepted: "Your friend said £45. The consulate's page still says £42 (verified yesterday). Both are shown below." **Sample "bring a friend" share email subject + body:** - Subject: "Brian — Maria Sofia's passport appointment, Friday morning. Here's what to bring." - Body: "I built our brief for the Argentine consulate. It has the four documents, the fees, and the script for the windows. Tap to open the read-only copy." [Open Brief] ## 9. Media & assets - **Hero image (landing screen):** A photographed-looking still life of a kitchen table at evening — a maroon Filipino passport open to the biodata page, a stapled paper printout next to it, an enamel mug of tea blurred at the edge of frame. Generate via Nano Banana 2 with a prompt emphasising "wooden kitchen table, warm desk-lamp light, late evening, soft shadow under the passport, no people in frame, real worn paper, gentle out-of-focus tea mug". - **App icon / wordmark:** Set in the grotesque face. A small line-drawn passport silhouette as the only icon. No 'consular eagle' or 'globe' iconography. - **Empty-state illustration:** A simple line drawing of a single folded brief and a passport. Hand-drawn aesthetic, not a flat icon. - **Demo brief assets:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "screenshot-style mock-up of a Spanish-language consulate fee schedule page on a kitchen table". Each demo asset should look photographed, not rendered. - **Period / consulate maps:** A simple line drawing of the London neighbourhood with Brook Street marked for the Argentine consulate, and equivalent line drawings for the Toronto, Sydney, Lisbon, Frankfurt, and Houston demo cases. No image tiles needed for v1. - **Stock fallbacks:** If image generation fails, fall back to the photographed sample passport from `/public/samples/sample-passport.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"). - 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: "Pick your nationality and your appointment city", "Upload your existing passport to verify the expiry date", "Add a friend who is coming with you" — never a generic "Add your first item". - Smooth scroll for in-page anchors. - The brief-generation streams in section-by-section as the model produces them — documents first, then fees, then flowchart, then counter script — with a clear honest progress indicator above each section as it loads. - If a Gemini call fails, show a calm, specific error ("We couldn't reach the consulate's page right now. The cached brief from yesterday is still valid. Re-check before you go.") and offer retry. - Freshness dots pulse for 800 ms then settle to static; respect `prefers-reduced-motion` by static glyph. - The override panel re-runs in real time; the user sees their typed discrepancy, the source check spinning, and the reconciled answer arriving section by section. - The counter-script rehearsal "loop this sentence" replays the TTS audio with a 600 ms pause, three times by default, configurable. ## 11. Tech & responsive requirements - **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 the primary grounded research call, the translation call, the document-verification call, and the forum-corroboration call. Pin `gemini-3.5-flash` for the freshness re-check. Set `thinkingLevel` explicitly per call (`high` for the primary research, `low` for everything else). - **Grounded JSON pattern:** the primary research call and the freshness re-check use `google_search` grounding and emit JSON in the text body — `responseSchema` is NOT set on these calls. Parse with Zod server-side and populate Citation `source_url` fields from `response.groundingMetadata.groundingChunks[].web.uri`. - **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed brief on first launch. - **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as fallback for "bring a friend" share. - **Storage:** Firebase Storage for user document uploads (when opted in). Pre-signed URLs only; auto-delete after 24 hours unless the user explicitly retains. - **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 brief view. - Optimistic UI on writes; reconcile on response. - Offline-readable brief PDF (printable + service-worker cached) — the consulate basement has no signal. ## 12. Accessibility (WCAG 2.2 AA) - Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`. - All interactive controls reachable by keyboard with a visible focus ring. - Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. The freshness dots are accompanied by a text label (visible, not just colour) for users with colour-vision differences. - All images have meaningful `alt` text. The user-uploaded document photographs have `alt` describing the artefact ("photograph of an Argentine DNI, biodata side") set by the document-verification call. - Form fields have associated `