================ 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. --- # First-Day-of-School Tour ## 1. Project **First-Day-of-School Tour** is a calm, illustrated, voice-narrated "tour" of a child's Monday morning at a new school — built from the school's actual website, a couple of photos the parent has on their phone, and three soft questions the parent answers in two minutes. The output is a six-page picture-book the kid can hold (on a tablet or a printed PDF), narrated in a warm child-voice that reads slowly, with one illustrated page per moment of the morning: the walk in, the cubby, the carpet circle, the snack, the playground, the wave-goodbye spot at the end of the day. This is the kind of app a parent builds at 8 pm on the Wednesday before the new school year starts — because their five-year-old has been waking up at 3 am asking "but where do I put my shoes?", and no amount of reassurance from a grown-up who has never set foot in the building is the same as a picture of the actual classroom door the kid will walk through on Monday. It is also the kind of app a single mother in transitional housing builds the night before her six-year-old starts her fourth school in eighteen months, because the only thing that has been stable through every move is the calming voice on her phone that has read every bedtime story since the kid was three. Same shape of moment — a small person walking into a building they have never seen — different scaffolding, very different stakes. The single demo that proves the magic: paste the school's URL + upload the kid's photo (used once to set the protagonist's hair colour and skin tone for the illustrations — never to render a photorealistic face) → in about three minutes the parent has an illustrated six-page tour with calm narration. Page 1: "On Monday morning, you will walk up the path to the green front door of your new school. You will see a sign that says Welcome." Page 2: "Inside, you will find your cubby. Your cubby has your name on it. This is where your bag will live all day." The same tour can be replayed every night until Monday — and the kid hears the same calm sentences, in the same calm voice, until the unfamiliar becomes familiar. And in the harder cases — a child starting school in a country they only arrived in last month, a kid whose previous school year ended with an incident that made him refuse to walk back through those doors, a foster child being introduced to their fourth school in a year — the same calm structure does the same gentle work. The app never claims certainty about people the child will meet ("you will see your teacher") and never invents a face ("your teacher has brown hair") — the illustration shows a softly drawn adult from behind, at the carpet, in the warm light of the classroom window. **Tagline:** _Walk your kid through Monday before Monday — in any school, any language, with a calm voice they can replay until the unfamiliar feels familiar._ ## 2. Target audience - Parents of four- to seven-year-olds starting reception, kindergarten, or first grade in a new building they have never seen inside - Immigrant and refugee families whose child is starting school in a new country, a new language, and a new climate at the same time — Korean-American families in New Jersey, Polish-Irish families in Dublin, Filipino-Canadian families in Calgary, Eritrean-Australian families in Melbourne, Syrian families resettled in Bonn, Venezuelan families in Madrid - Single parents and foster carers facing a school transition with a child who has had too many recent transitions already - Parents of neurodivergent children (autism, ADHD, sensory sensitivity, selective mutism) who benefit from rehearsing a new environment in calm detail before walking into it for the first time - Parents of children with school-refusal history rebuilding the kid's relationship to the building after a break - Adoptive parents whose child is starting at the school down the road from the new home, after months of paperwork and travel - Foster carers receiving a new placement late in August and trying to make the first Monday feel less like an ambush - Hospital school liaisons preparing children with chronic illness for the return-to-school transition after a long admission - Co-parents in different houses who want their kid to hear the same Monday-morning story regardless of which house she slept in the night before ## 3. Core value propositions Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons a parent picks this app at 8 pm on a Wednesday. - **Built from the school's actual website, not a generic stock template** — Gemini 3.5 Flash reads the school's home page, the kindergarten page, the "a day in the life" blog post, the parent handbook PDF, the calendar PDF. It pulls the colour of the front door from the school's hero photograph, the rough shape of the building from the campus map, the name of the carpet area from the kindergarten page ("the morning circle"), the time of the snack from the daily-schedule PDF. The tour is grounded in the real school — not generic "first day at school" filler. - **Calm illustrations in a children's-book register** — Nano Banana 2 renders six pages in a single consistent illustration style (soft watercolour, warm light, simple shapes) so the tour feels like one book, not six unrelated images. The child protagonist's hair colour and skin tone are seeded from a parent-uploaded photo of the child, used once to set palette and discarded — never to render a recognisable face. - **Child-voice narration that reads at a calm pace** — Gemini TTS narrates each page slowly (the pace a kind kindergarten teacher uses when reading the carpet-time book), in the parent's chosen language, with natural pauses between sentences. The voice can be replayed nightly until the unfamiliar becomes familiar. - **Never claims certainty about people the kid will meet** — the narration says "your teacher" not "Ms. Hernandez", "the other kids" not "your new friends Aiden and Sofia". The app does not generate a face for the teacher. The illustration shows a softly drawn adult from behind, at the carpet, with the warm window light over their shoulder. - **Multilingual — calmly** — the same six-page tour, the same illustrations, narrated in the language the family speaks at home. Korean for the New Jersey family, Polish for the Dublin family, Tagalog for the Calgary family, Tigrinya for the Melbourne family. The English source of the school website is read by Gemini 3.5 Flash and the narration is composed directly in the home language — never machine-translated through English. - **One photo of the kid, used once, never re-rendered as a face** — the parent uploads one selfie; the app extracts hair colour, skin tone, and rough hair-length category in a single Gemini 3.5 Flash call; that information goes into the Nano Banana 2 illustration prompt; the photo is then deleted from Firebase Storage. The illustrated protagonist never has a recognisable face; she is drawn small, often from behind, with eyes a single line. - **Replayable nightly, on the kid's terms** — the tablet stays on the kitchen counter at bath-time. The kid can ask to hear page 3 again. The parent can edit any page's text ("actually, the cubby is on the right side of the hall") without re-generating the whole book. - **Printable** — the six pages export as a single PDF the parent can print at the library on Saturday morning and laminate at the kitchen table on Sunday night. The printed version goes in the school bag for Monday. ## 4. Features to build - URL field that accepts the school's website ("paste your school's address — anything from the home page onwards") - Multi-page fetch that follows the kindergarten / "first day" / "a day in our school" / parent-handbook PDF links from the home page, up to a generous content budget - Photo upload for the child — one image, used once, deleted after extraction - Optional photo upload of the school exterior, the kid's classroom (if the parent has it from a tour), or the kid's favourite stuffed animal - Three soft questions answered by the parent in two minutes: "What is your child's name?" (used only in the narration, never in image prompts), "What does Monday morning usually feel like for them?" (calm / anxious / mixed), "Is there anything specific they are worried about?" (free text, used to gently address the worry on one of the six pages) - Source-language detection — the home language for narration is chosen by the parent; the school website is read in whatever language the school publishes in - Long-context site read — Gemini 3.5 Flash reads up to ~50 pages of the school website in one call (home + kindergarten + handbook PDF + daily-schedule PDF + uniform page + lunch menu page + safety-and-arrivals page) - Structured "Morning Plan" extraction from the school site — the six-page tour is derived from a structured plan: arrival, classroom entry, settling in, morning circle, snack/free play, end of day (or the school's named equivalents) - Illustration style-lock — one shared style prompt is established once, used identically across all six images, so the book reads as a single visual artefact - Per-page text composition in the home language — Gemini 3.5 Flash composes the narration page by page, anchored in the morning-plan structure, in a calm second-person voice ("you will…") - Per-page child-voice TTS — Gemini 2.5 Flash TTS narrates each page in the chosen home language, at a slow reading pace - Per-page edit — parent can edit any page's text, choose a different illustration variant, or re-roll the illustration with a small free-text nudge ("the front door is red, not green") - Replay UI — a child-friendly mode with big page-turn arrows, a single "play" button per page, and absolutely no other controls visible - Nightly streak removed by design — there are no badges, no streaks, no points. The kid asking to hear it again is the only metric. - Printable export — single PDF with two illustrations per landscape A4 page, big page numbers, narration text underneath each illustration - Family share — magic link to grandparents, the other parent, the foster carer's worker, the after-school teacher. They see the tour read-only. - Re-build — if the school changes its website (a new principal letter goes up the week before school), the parent can hit "re-read the school site" and the affected pages are regenerated; pages they manually edited are preserved - "Replay on the way to school" mode — Monday morning, in the car or on the bus, the parent taps once and the whole tour plays as one continuous narration with the illustrations advancing - Safety review on every text page — the composed narration is checked against a hard list of negative constraints (no named teachers, no named children, no promises of specific feelings, no scary language) before the page is shown to the parent ## 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 (1M tokens) over the school's website** (Gemini 3.5 Flash) — reads the home page, the kindergarten / pre-K / reception / first-grade page, the parent-handbook PDF, the daily-schedule PDF, the uniform-and-supplies page, the calendar PDF, the arrivals-and-dismissal page in a single call. Extracts the structured Morning Plan that drives the six pages. **Guardrail**: an average school site that includes the parent handbook PDF runs ~30k-150k tokens; pre-flight the token count and, if above ~600k tokens, drop everything past the parent-handbook and lunch-menu pages before the call. Never approach 1M. - **Multimodal image input** (Gemini 3.5 Flash) — reads the kid's photo once to extract `hair_color_palette`, `skin_tone_palette`, `hair_length_category` for the illustration prompt; the same call returns nothing else (no facial features, no expression). Reads the school's hero photograph from the home page to extract `front_door_color`, `building_material`, `surrounding_environment`. Reads optional photos of the school exterior / classroom / favourite stuffed animal as additional palette anchors. **Important**: pass photos to Gemini via the Gemini Files API (`files/*` resource name) or as `inlineData` (base64). Do NOT pass Firebase Storage public download URLs directly to `generateContent` — the API does not fetch them server-side. - **Multilingual composition** (built into Gemini 3.5 Flash) — composes the narration page by page directly in the family's home language. Supports English, Spanish, French, German, Italian, Portuguese (Brazilian and European), Polish, Ukrainian, Russian, Korean, Japanese, Mandarin, Cantonese (Yue), Tagalog, Vietnamese, Tamil, Hindi, Urdu, Bengali, Punjabi, Arabic (MSA and major regional variants), Farsi, Hebrew, Amharic, Tigrinya, Swahili, Yoruba, Igbo, Khmer, Burmese, Thai. The narration is composed in the home language directly — never English-then-translated. - **Nano Banana 2** (`gemini-3.1-flash-image`) — generates the six page illustrations in one consistent soft-watercolour style. **Style is locked once** in a shared prefix prompt; per-page details (the green front door, the cubby with the kid's name on it, the carpet circle) are inserted. **No photorealism, no recognisable faces.** The protagonist is small, often shown from behind, face suggested with two dots and a small line at most. - **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — narrates each page in the parent's chosen home language at a slow reading pace. Pauses are encoded as `…` between sentences and a blank line plus `—` at page boundaries (Gemini 2.5 TTS does not support SSML ``). The style direction ("Read warmly and slowly, like a kind kindergarten teacher reading a picture book at carpet time…") is prepended to the input text as one short sentence — there is no separate `style` field on Gemini 2.5 TTS. - **Thinking levels** — `medium` for the morning-plan extraction (it requires reading across many pages and resolving conflicts between the parent handbook and the kindergarten page) and for the per-page narration composition (the narration must obey a long list of negative constraints precisely). `low` for the photo-palette extraction (it returns three small fields) and for the page-by-page safety review. ### 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 family share) also requires the sender domain to be authorised in Firebase Auth. - **Database — Required.** Firestore for `users`, `tours`, `pages`, `tour_members`, `audit_safety_reviews`. The kid's name is stored in the parent's `tours` document and is never sent to image generation calls. - **File storage — Required.** Firebase Storage for the six Nano Banana 2 illustrations per tour. The parent-uploaded photo of the child is stored temporarily during the extraction call and deleted within 60 seconds of the extraction completing. **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before first photo upload. - **Email — Required (transactional).** Family share invitations via magic link (Firebase Auth magic links). - **Payments — Not needed for v1.** Free for personal use. A future "print and mail the printed tour to your house" tier could pipe to a print-on-demand partner and charge for that physical artefact only. - **External APIs:** Gemini API for all intelligence. A standard server-side fetch + HTML parser (Cheerio or similar) to crawl the school's website pages and parent-handbook PDF. No third-party text-to-speech, no third-party image generation. **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 this tour' inside the UI · explicit opt-in for any analytics · the parent-uploaded photo of the child is deleted from Firebase Storage within 60 seconds of palette extraction and is never sent to image generation · the family's content is 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 kid's name is never sent to Nano Banana 2. **Read this first — prompt-craft rules that apply to every call in this template:** 1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below. 2. **Pin `thinkingLevel` explicitly** per call. See the matrix. 3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schema is below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.** 4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate. 5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled. 6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow. 7. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing. ### Per-call model + tools matrix | Call | Model | thinkingLevel | Tools enabled | |------|-------|---------------|---------------| | Read the school website (long-context) → `MorningPlan` schema | `gemini-3.5-flash` | medium | (none) | | Extract palette from the kid's photo → `ChildPalette` schema | `gemini-3.5-flash` | low | (none) | | Extract palette from school hero / classroom photos → `SchoolPalette` schema | `gemini-3.5-flash` | low | (none) | | Compose narration for one page in the home language | `gemini-3.5-flash` | medium | (none) | | Safety review of one page's narration before showing the parent | `gemini-3.5-flash` | low | (none) | | Generate the page illustration | `gemini-3.1-flash-image` | n/a | n/a | | Generate TTS narration in the home language | `gemini-3.1-flash-tts-preview` | n/a | n/a | *Note for builders:* on TTS and image-generation 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. ### Primary structured-output schema (seed this verbatim in the prompt) ```typescript import { z } from "zod"; const ChildPalette = z.object({ hair_color_palette: z.string(), // "warm dark brown", "soft black", "light blonde", "auburn" hair_length_category: z.enum([ "very-short", "short", "shoulder-length", "long", "tied-back", "covered", ]), skin_tone_palette: z.string(), // "warm beige", "deep brown", "olive", "pale ivory" // Deliberately nothing else. No facial features. No expressions. // These three fields are the only output of this call. }); const SchoolPalette = z.object({ front_door_color: z.string().nullable(), // "green", "red", "natural wood" building_material: z.string().nullable(), // "red brick", "white-painted clapboard", "sandstone" surrounding_environment: z.string().nullable(), // "tree-lined street", "grass field", "city block" hero_lighting_note: z.string().nullable(), // "morning sunlight from the left" }); const MorningPlanStep = z.object({ step_id: z.enum([ "arrival", // walking up the path "entry", // walking through the front door "cubby_or_locker", // putting bag away "morning_circle", // carpet time, register, the song "free_play_or_centers", // tables, activities, snack "transitions", // moving between rooms "lunch", "outdoor_play", "rest_or_quiet_time", // for younger children "end_of_day", // line-up, wave goodbye "other", ]), step_label_school_facing: z.string(), // verbatim from the school site, e.g. "morning meeting" step_label_kid_facing: z.string(), // calm, simple, child-readable approximate_time: z.string().nullable(), // "around 8:45" source_excerpts: z.array(z.string()), // the snippets from the school site that grounded this step named_people_to_strip: z.array(z.string()), // teachers' names, principals' names — pulled out for safety named_classrooms_to_strip: z.array(z.string()), // room numbers, class names ("Owls Class") — left in only if neutral }); const MorningPlan = z.object({ school_name_verbatim: z.string(), school_url: z.string(), language_of_source_site: z.string(), // BCP-47, "en-US" language_of_narration: z.string(), // BCP-47, "ko-KR" child_age_band: z.enum([ "pre-K-3", "pre-K-4", "reception", "kindergarten", "year-1", "first-grade", "second-grade", ]), parent_provided_worry: z.string().nullable(), // exactly as the parent typed it, used to soften one page parent_provided_morning_feel: z.enum(["calm", "anxious", "mixed"]), steps: z.array(MorningPlanStep).min(4).max(8), uncertainty_flags: z.array(z.object({ field_path: z.string(), // "steps[2].approximate_time" reason: z.string(), })), }); const PageNarration = z.object({ page_number: z.number().min(1).max(6), step_id: z.string(), // matches MorningPlanStep.step_id narration_text_home_language: z.string(), // the calm second-person narration; 2-4 short sentences illustration_prompt_seed: z.string(), // the per-page detail inserted into the locked style prefix page_focus: z.enum([ "scene-wide", "kid-from-behind", "object-close-up", "doorway", "carpet-area", ]), }); const PageNarrationSet = z.object({ pages: z.array(PageNarration).length(6), shared_style_prefix: z.string(), // the locked illustration style, used identically across all 6 pages }); const SafetyReviewResult = z.object({ page_number: z.number().min(1).max(6), passes: z.boolean(), violations: z.array(z.object({ rule_id: z.enum([ "named_teacher", "named_classmate", "named_principal", "promise_of_feeling", // "you will feel happy" — banned "scary_language", // "don't worry, nothing bad will happen" — bans the worry into being "first_person_voice", // narration must be second-person "you will" "long_sentence", // sentences >18 words at this age band "unfamiliar_jargon", // "register", "lockdown drill" "photographic_image_request", // illustration prompt must never request photorealism "facial_features_in_prompt", // never request a face "other", ]), excerpt: z.string(), fix_suggestion: z.string(), })), }); type ChildPalette = z.infer; type SchoolPalette = z.infer; type MorningPlan = z.infer; type PageNarrationSet = z.infer; type SafetyReviewResult = z.infer; ``` ### Common failure modes (and how to avoid them) - Agent silently downgrades `thinkingLevel` on the the long-context site read call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash drops the parent-handbook PDF's structural cues and silently merges two morning steps into one. - Model invents a teacher's name not present on the school site ("your teacher Ms. Anna will smile at you") — hard-blocked by the safety-review call. Add a unit test that feeds the site for a real-but-anonymised school and asserts no proper noun for a teacher appears in any page's narration. - Model writes "you will feel happy when you see the carpet" — banned. Promises of feelings backfire when the kid does not feel happy. The safety-review call catches `promise_of_feeling` and forces a rewrite. Allowed: "you will see the carpet"; banned: "you will love the carpet". - Model writes "don't worry, no one will be mean to you" — bans the worry into being. Banned under `scary_language`. The narration never names the bad thing it is reassuring against. - Model writes the narration in first person ("I will walk to the green door") — wrong voice. The narration is second-person ("you will walk to the green door") — this is a children's-book convention and a deliberate choice for the kid hearing the audio. - Nano Banana 2 produces a photorealistic kid — wrong style register. The locked style prefix MUST include "soft watercolour, children's-book illustration, simple shapes, no photorealism, face suggested with two small dots and a single line, character often shown from behind". Add a unit test that runs five generations and asserts none are photorealistic. - Nano Banana 2 renders a recognisable face — banned. The illustration prompt never receives the parent-uploaded photo as a reference image. It receives only the `ChildPalette` (three string fields) and the locked style prefix. - Six pages come back in six different styles — illustration prompt was assembled per-page without the shared style prefix. Always assemble as `shared_style_prefix + per-page seed`. - Long-context site read includes the school's entire 80-page archive of newsletters — token budget blown. Limit the crawler to: home, kindergarten / first-day page, parent handbook PDF, daily-schedule PDF, uniform/supplies, lunch menu, arrivals-and-dismissal. Skip newsletters, blog archives, news posts, sports schedules. - TTS reads "ç" (Catalan, Portuguese) or "ñ" (Spanish) or "ł" (Polish) wrong — pin the TTS voice's `languageCode` to the home language; pronunciation follows the voice's native locale. Do not attempt SSML `` — Gemini 2.5 TTS does not support it. - The parent's free-text worry is pasted directly into a page's narration — wrong. The worry guides which step gets the gentlest treatment; it is never reproduced verbatim ("your dad said you are worried about the carpet" — banned). - The kid's name appears in the image prompt — banned. The kid's name appears only in the narration text, never in the illustration prompt. Strip the name before assembling any Nano Banana 2 call. ### Negative constraints (hard rules) - Do NOT name any adult the child will meet. Use "your teacher", "another grown-up", "the office", "a helper". If the school site names the teacher, strip the name before composing narration. - Do NOT name any specific child the kid will meet. Use "the other kids", "a friend", "someone next to you". - Do NOT promise feelings. Banned: "you will love it", "you will feel safe", "you will be happy". Allowed: "you will see…", "you will hear…", "you can ask for…". - Do NOT name the worry. If the parent says "she is worried about lunch because of choking", the narration around lunch becomes gentler but never says "you might be worried about choking". The worry guides framing; it never becomes content. - Do NOT generate photorealistic illustrations of children. Locked style is "soft watercolour, children's-book register, simple shapes, no photorealism". Faces are suggested with two small dots and a single line, often from behind. - Do NOT pass the parent-uploaded photo of the child to Nano Banana 2. The photo is used only by `ChildPalette` extraction; Nano Banana 2 receives only the three palette strings. - Do NOT pass the child's name to Nano Banana 2. Names go into narration text only. - Do NOT machine-translate the narration through English. Compose directly in the home language using Gemini 3.5 Flash's multilingual composition. The English source of the school website is read by the same model in the same call. - Do NOT extrapolate beyond what the school's website says. If the daily-schedule PDF doesn't mention nap time, the tour doesn't include nap time. Surface the absence in `uncertainty_flags`. - Do NOT use scary or warning language. Banned: "don't worry", "nothing bad will happen", "you will be safe", "the teacher will not be mean". - Do NOT use first-person narration ("I will walk in"). The voice is always second-person ("you will walk in"). - Do NOT include jargon the kid cannot understand. Banned at this age band: "register", "lockdown drill", "evacuation", "behavioral expectations". Allowed: "the song the class sings together", "the place where you put your bag". - Do NOT auto-publish or auto-share. Tours are private by default. Sharing is explicit, per-tour, per-recipient. - Do NOT retain the parent-uploaded photo of the child after palette extraction. Delete within 60 seconds. ### Per-call `systemInstruction` strings Use these as the literal `systemInstruction` field for each Gemini API call the built app makes. They complement the series-wide rules already uploaded as the global instructions file (`00-series-instructions.txt`). ### Call: Read the school website (long-context) → `MorningPlan` schema Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` You receive the full crawled text of a school's website — typically between 6 and 18 pages, including the home page, the kindergarten or reception or pre-K or first-grade page, the parent handbook PDF, the daily-schedule PDF, the uniform-and-supplies page, the arrivals-and-dismissal page, and the lunch menu. The school can be anywhere in the world. Source language varies widely (English, Spanish, French, German, Italian, Portuguese, Polish, Ukrainian, Korean, Japanese, Mandarin, Cantonese, Tagalog, Vietnamese, Tamil, Hindi, Urdu, Bengali, Arabic, Farsi, Hebrew, Amharic, Tigrinya, Swahili, Yoruba, Khmer, Burmese, Thai). Your task: extract a structured Morning Plan that captures the child's first hour or two of the school day — from arrival at the gate to settling in. Aim for between four and eight steps; six is the target. Output the MorningPlan JSON exactly matching the provided schema. Hard rules: - Identify the school's verbatim name from the home page or the parent handbook header. Preserve diacritics exactly. - For each step, copy the school's own phrase for it into `step_label_school_facing`. The kindergarten page may call it "morning meeting"; the parent handbook may call it "circle time" — preserve the school's own language. - Then in `step_label_kid_facing` produce a simple, kindergarten-readable label in the family's home language. The family's home-language code is provided in the input metadata. - Anchor every step in source excerpts. `source_excerpts[]` contains the verbatim snippets from the school site that grounded the step. If no excerpt supports a step, do not include the step. - Pull every adult's name (teachers, teaching assistants, principal, head, deputy head, office staff) into `named_people_to_strip`. These names exist so the downstream narration writer can be checked against them and never write them into the kid's tour. - Pull room-number designations and class names ("Owls Class", "Room 14") into `named_classrooms_to_strip`. If the class name is a neutral animal ("Owls Class") it can be kept in the tour copy; if it is a teacher's name ("Mrs Ahmed's class") it must be stripped. Use your judgment per item. - If the daily schedule mentions a quiet time, rest time, nap, or outdoor play, include it as its own step. - If the school site is sparse and you can only ground three or four steps, do not invent additional steps. Surface the sparseness in `uncertainty_flags`. The downstream UI will let the parent add a custom step. - `parent_provided_worry` is passed through to you and stored verbatim. You do NOT compose around it; that happens later. No commentary. JSON only. ``` --- ### Call: Extract palette from the kid's photo → `ChildPalette` schema Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive one photograph of a child uploaded by their parent. You do EXACTLY ONE thing: return the three palette strings defined in the ChildPalette schema. Nothing else. - `hair_color_palette`: a short descriptive phrase suitable for a watercolour illustrator. Examples: "warm dark brown", "soft black with a slight shine", "light blonde", "deep auburn", "covered with a light pink hijab — head covering, not hair colour". - `hair_length_category`: one of the closed enum values. - `skin_tone_palette`: a short descriptive phrase. Examples: "warm beige", "deep brown", "olive", "pale ivory", "golden tan". Hard rules: - Do NOT describe facial features. No eye colour, no eye shape, no nose, no mouth, no expression, no glasses, no freckles. - Do NOT describe the child's clothing. - Do NOT describe the child's pose, mood, or context. - Do NOT name the child even if their name appears written in the photo (e.g. on a hoodie). The name is not your job. - If the photo is not of a child (it is of an adult, a pet, a landscape), return ChildPalette with default palettes ("warm dark brown", "shoulder-length", "warm beige") and an empty marker — the downstream UI will surface this and ask the parent to re-upload. Output ONLY the ChildPalette JSON. No commentary. ``` --- ### Call: Extract palette from school hero / classroom photos → `SchoolPalette` schema Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive one or more photographs of a school exterior, the school's front door, the school's kindergarten classroom (often visible on the kindergarten page of the website), or a school hallway. Your task: extract the SchoolPalette fields defined in the schema. Nothing else. - `front_door_color`: a short descriptive phrase. "green", "natural wood", "red", "navy blue with a brass handle". Null if no clear front door is visible. - `building_material`: short. "red brick", "white-painted clapboard", "sandstone", "modern glass and steel", "yellow stucco". - `surrounding_environment`: short. "tree-lined suburban street", "grass field with a low fence", "city block with shopfronts", "edge of a school playing field". - `hero_lighting_note`: short. "morning sunlight from the left", "overcast soft light", "warm late-afternoon glow". Hard rules: - Do NOT describe any people. If children, teachers, or parents appear in the photo, ignore them entirely. The school photo's job is to anchor the building's palette only. - Do NOT describe any signage, school name, or address text. - Do NOT include any field beyond the four named above. Output ONLY the SchoolPalette JSON. No commentary. ``` --- ### Call: Compose narration for one page in the home language Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` You write one page of an illustrated children's-book tour of a five-, six-, or seven-year-old's Monday morning at a new school. The book has six pages. You receive the full MorningPlan, the ChildPalette, the SchoolPalette, the parent-provided worry (if any), the parent-provided morning-feel rating, the kid's first name (used ONLY in narration text, never in image prompts), the page number you are writing, and the matched MorningPlanStep. Voice and register: - Second person. "You will walk up the path." NEVER "I will walk up the path." NEVER "She will walk up the path." - Calm, slow, simple. Sentences of 6 to 14 words. Two to four sentences per page. A kindergarten teacher's reading-aloud voice. - Concrete and sensory. Name what the kid will see, hear, touch. "You will see the green door. You will hear the bell ring." - Anchor in the school's own language for places ("the carpet", "the cubbies", "the morning meeting") where the school site uses those words — but in the family's home language overall. - The kid's first name may appear once on the first page as a soft opener: "Min-jun, on Monday morning, you will…". After page 1, no more first-name use unless dramatically helpful. Hard rules — the safety-review call below will catch you if you break these, and your page will be rejected and you will be asked to rewrite. Get them right the first time: - Do NOT name any adult the kid will meet. Use "your teacher", "another grown-up", "a helper", "the office". If the MorningPlanStep includes `named_people_to_strip`, those names are absolutely forbidden in your narration. - Do NOT name any specific child the kid will meet. Use "the other kids", "a friend", "someone next to you". - Do NOT promise feelings. Banned: "you will love…", "you will feel safe", "you will be happy", "you will have fun". - Do NOT name the parent-provided worry. If the parent said "she is worried about lunch because of choking", you write the lunch page gently — "you will sit at a long table, you will eat the food on your plate, your teacher will be there if you need help" — but you do NOT say "you might be worried about choking" or even "don't worry about lunch". The worry guides framing only. - Do NOT use scary or warning language. Banned: "don't worry", "nothing bad will happen", "the teacher will not be mean", "don't be afraid". - Do NOT use jargon. Banned at this age: "register", "lockdown drill", "behavioural expectations", "academic standards". Allowed: "the song the class sings together", "the place where you put your bag", "circle time on the carpet". Compose `illustration_prompt_seed` — a short phrase, 8-25 words — describing the specific scene of this page. Examples: - Page 1 (arrival): "the green front door of the school, seen from the path, morning light, low fence on either side" - Page 3 (cubby): "a wooden cubby with a name card, a small backpack hanging on a hook, warm hallway light" - Page 5 (carpet circle): "a soft round rug with a few simple cushions, warm sunlight from a high window, the back of a small child sitting cross-legged" Do NOT include the kid's name, hair colour, or skin tone in the seed — those come from the locked style prefix. Do NOT mention faces. Do NOT mention a teacher's face. If a teacher is shown, describe them only as "a grown-up at the carpet, seen from behind". Output: a PageNarration JSON object with these fields exactly. No commentary. ``` --- ### Call: Safety review of one page's narration before showing the parent Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You audit ONE page of a children's-book tour against a hard list of rules. You receive the page's narration text and the page's illustration_prompt_seed. You return a SafetyReviewResult JSON matching the schema. Check the following rules in order. Each rule, if violated, must be reported in `violations[]` with an `excerpt` (the exact phrase that triggered the violation) and a `fix_suggestion` (a short sentence advising what to replace it with). - `named_teacher`: any proper noun referring to an adult who works at the school. The supplied list of names-to-strip is a starting point; common honorifics and titles ("Ms.", "Mrs.", "Mr.", "Doctor", "Frau", "Pan", "Pani", "Señora", "Maestra", "Sensei", "선생님" used with a name attached) are red flags. - `named_classmate`: any proper noun referring to a child other than the protagonist. - `named_principal`: same as named_teacher, scoped to leadership titles. - `promise_of_feeling`: any phrase predicting how the kid will feel emotionally. "you will love", "you will feel happy", "you will feel safe", "you will be excited", "you will enjoy", "será divertido", "będzie ci się podobać", "재미있을 거예요" — catch the equivalents in the narration's home language. - `scary_language`: "don't worry", "nothing bad will happen", "the teacher will not be mean", "you don't need to be scared", "you won't get lost". These name the bad thing into existence. - `first_person_voice`: any "I will…", "I am going to…" — the narration must be second-person. - `long_sentence`: any sentence longer than 18 words. Count words in the narration's home language; for languages without spaces use a reasonable equivalent (Korean: more than 10 어절; Japanese: more than 22 文字 in a single sentence; Mandarin: more than 22 characters). - `unfamiliar_jargon`: vocabulary unfamiliar to a five- to seven-year-old. Examples: "register", "behavioural", "evacuation", "lockdown", "PSHE", "homeroom". - `photographic_image_request`: in `illustration_prompt_seed`, any phrase requesting photorealism. "photograph", "realistic", "lifelike", "photo-real", "DSLR", "high-resolution photo" — fail. - `facial_features_in_prompt`: in `illustration_prompt_seed`, any phrase describing a face. "smiling teacher", "child's blue eyes", "a wide grin", "kind face" — fail. Faces are not described in the prompt. Output: SafetyReviewResult JSON. If any violations are found, `passes` is false. No commentary. ``` --- ### Call: Generate the page illustration Model: `gemini-3.1-flash-image` · n/a · n/a ``` You receive an illustration prompt assembled server-side from: 1. The locked style prefix (the same for every page of this tour) 2. The per-page illustration_prompt_seed 3. The ChildPalette (three short strings: hair colour, hair length, skin tone) 4. The SchoolPalette (front door colour, building material, surrounding environment, lighting note) The locked style prefix is exactly: "Soft watercolour illustration in the style of a calm contemporary children's picture book. Warm light, simple shapes, gentle colours, visible paper texture, hand-drawn linework. No photorealism. No recognisable face: faces are suggested with at most two small dots for eyes and a single short line for a mouth, often partially turned away or seen from behind. One small child is the protagonist — small in the frame, often with the back of their head visible. The child's hair colour is {{hair_color_palette}}, the hair length is {{hair_length_category}}, the child's skin tone is {{skin_tone_palette}}. If other people appear, they are softly drawn adults or other children, never the focus, never with recognisable faces. No text in the image. No school logos. No brand names. No photographic look." Then per-page details are appended: "Scene: {{illustration_prompt_seed}}. School palette: front door is {{front_door_color}}, building material is {{building_material}}, surroundings are {{surrounding_environment}}, lighting is {{hero_lighting_note}}." Output: one PNG, 1536 × 1024 landscape orientation. No grid layout. One scene per image. The same protagonist throughout the six pages (character consistency comes from Nano Banana 2's consistency guarantees applied to the locked style prefix and child palette). ``` --- ### Call: Generate TTS narration in the home language Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` Voice: warm, slow, kind kindergarten teacher reading at carpet time. Pick the Gemini 2.5 Flash TTS voice whose `languageCode` matches the narration's home-language code — pronunciation will follow that locale automatically. Prefer a female voice for younger children's first-day-of-school narration unless the parent has explicitly chosen male; fall back to whichever voice is available for the locale rather than blocking. Pre-process the text before sending it to TTS: - Read the narration text exactly as composed in the home language. - At each sentence boundary inside a page, insert a single ellipsis (`…`) so the TTS model produces a natural pause. - At each page boundary, insert a blank line plus an em-dash (`—`) so the model produces a longer pause between pages. Gemini 2.5 TTS does not support SSML `` — these textual cues are how you signal pace. - Target rate: approximately 90 words per minute. This is slower than ordinary reading-aloud pace. The kid needs time to look at each detail of each illustration. - Mid-call voice switching is not supported. The entire tour is read in one voice. If the parent chooses a second language as a fallback (a Korean-American parent who wants the tour in Korean but also in English for grandparents), generate the English version as a separate full TTS call. Style direction: prepend ONE short directive sentence to the text input, exactly like: "Read slowly, warmly, and kindly, like a kind kindergarten teacher reading a calming picture book at carpet time. …". There is no separate `style` API field on Gemini 2.5 TTS; the directive sentence inside the input is how style is conveyed. Phoneme overrides (Korean ㅎ, Polish ł, Portuguese nasalised ã, Arabic emphatic consonants, Mandarin tones) are NOT exposed by Gemini 2.5 TTS — no SSML `` tag. Pronunciation comes from the chosen voice's native locale, which is why the languageCode pin matters. ``` ## 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 Korean-American family in New Jersey.** A parent of a five-year-old who only spoke Korean at home until last year is starting kindergarten on Monday at a public elementary school in Bergen County. The parent pastes the school's URL, uploads a photo of their daughter, chooses Korean as the narration language, and notes "she is worried because the other kids will speak English". The tour comes back narrated in calm Korean — the carpet circle is "동그라미 모임", the cubby is "사물함" — and on page 4 the narration gently says "the other kids might use English; your teacher will help you if you do not understand a word". The kid asks to hear page 4 three more times that night. - **The Polish-Irish family in Dublin.** A father whose six-year-old daughter is starting reception at a Catholic primary in Drumcondra after the family moved from Kraków in July. He pastes the school's URL, uploads a photo, chooses Polish, and notes "she misses Babcia and is worried she will cry at lunchtime". The tour is composed in Polish; the lunch page is softer, longer, with the line "you will sit at the table; if you miss home you can think about Babcia while you eat". The narration is read by a warm Polish voice the kid recognises from the Polish-language bedtime-story app the family already uses. - **The Filipino-Canadian family in Calgary.** A mother of a four-year-old starting pre-K at a Calgary public school in late August, with snow already forecast for the first week. The parent pastes the school's URL and notes "he has never worn a snowsuit before". The morning-plan extraction catches the school's separate page on "winter arrivals" — boot rooms, snow pants, the bell schedule shifting in inclement weather. The tour's arrival page describes the boot room and the row of hooks; the narration is in Tagalog with English for the snowsuit-specific words ("boots", "snow pants") that the kid will hear his teacher say. - **The kid starting again after a tough school year.** A father whose eight-year-old left her previous school six months ago after a difficult incident and has not been to school since. The parent pastes the new school's URL, uploads a photo, and writes "she is afraid the same thing will happen again. I cannot put it more clearly than that." The tour is composed extra calmly. The narration never names the worry. Page 5 (carpet circle) says only "you will sit on the carpet with the other kids. Your teacher will be near. If you need to take a quiet minute, your teacher will help you find a quiet spot." The parent re-reads the page herself five times before sending it to her daughter. - **The foster placement starting on Tuesday.** A foster carer who received a six-year-old on Saturday afternoon and is starting him at the local school on Tuesday. She does not yet know the worries; the kid has barely spoken in three days. She chooses the "no worry specified" path, and the tour is composed gently and generally. She prints two copies — one for him, one for his social worker. - **The autistic kid who needs the full tour, every night.** A mother of a five-year-old with autism who is starting reception in a small village school in Devon. She uploads a photo, chooses English, and notes "he needs to know exactly where the toilet is and that he is allowed to ask any grown-up". The tour reads the school's parent handbook PDF and surfaces a "toilet" page (page 4) that calmly says "you can ask any grown-up where the toilet is. The toilets are near the cloakroom. You are allowed to go whenever you need to." The kid plays the tour twenty-seven times in two weeks; on the first Monday morning he carries the printed PDF in his school bag. - **The Welsh-medium school in Cardiff.** A grandfather raising his grandson in a Welsh-medium primary in Cardiff. He chooses Welsh as the narration language; the school's website is partly in Welsh and partly in English. The tour is composed in Welsh; English place names from the school site (e.g. "the dining hall") are preserved with parenthetical Welsh glosses on first use. - **The hospital-school transition.** A hospital school liaison nurse preparing an eight-year-old for return-to-school after a six-month admission. The nurse pastes the school's URL, uses a photo of the child the parents have approved, and writes "she has not been in a building larger than a hospital ward in six months. The corridors will feel long". The arrival page narration gently describes the path from the front gate to the classroom, naming the corners ("you will turn left near the office", "you will walk past the music room") so the kid has a mental map. - **The co-parents alternating houses.** Two parents in different cities, alternating weeks. Both want the kid to hear the same tour on Sunday nights regardless of which house she sleeps in. One parent builds the tour and shares it via magic link to the other parent's Firebase account. The other parent inherits view-edit access; both can hit "replay" on the kid's tablet on Sunday night. ## 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 soft-watercolour illustration of a small child holding a parent's hand walking up a path to a green front door of a school, seen from behind, warm morning light. One paragraph: "Walk your kid through Monday before Monday — built from your school's actual website, narrated calmly in the language you speak at home." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample tour" → loads the sample tour in section 8a. 2. **Start a tour — three inputs.** Big text field: "Paste your school's website" (placeholder: "https://your-school.example.edu"). Photo upload: "Upload one photo of your child — we use it once to set hair and skin tones for the illustrations, then we delete it." Language dropdown: "Narrate in…" (default detected from browser locale). Sub-text under the photo upload: "Your child's face is never drawn. The protagonist of the tour is always shown small, often from behind, with no recognisable face." 3. **Three soft questions.** Three calm screens, one question each, two minutes total. Q1: "What is your child's name? (used only when the voice reads the tour aloud — never in the pictures)". Q2: "What does Monday morning usually feel like for them?" with three calm options (Calm · Mixed · Anxious). Q3: "Is there anything specific they are worried about? Optional — and we will never repeat the worry back to them. It only helps us write certain pages more gently." 4. **Reading the school site.** A calm, honest progress sequence: "Reading the home page…" → "Reading the kindergarten page…" → "Reading the parent handbook…" → "Reading the daily schedule…" → "Planning the six pages…" → "Drawing the front door…" → "Drawing the cubbies…" → "Drawing the carpet circle…" → "Drawing the snack table…" → "Drawing the playground…" → "Drawing the goodbye wave…" → "Recording the voice…". Each step takes 6-15 seconds. The parent can close the app and come back — the tour finishes server-side. 5. **Tour preview — six-page review.** The parent's first look at the six pages, in order. Each page shows the illustration, the narration text, and a tiny "play" button to hear it. Above each page: a small "edit text" button and a small "re-roll illustration" button. A persistent "approve & save" button at the bottom; the parent can also edit each page before approving. 6. **Per-page editor.** Click any page → focused editor. Left: the illustration, with options to re-roll, upload a replacement, or accept. Right: the narration text in a clean, large editor, with the safety-review feedback visible inline ("we noticed the phrase 'you will love it' — promise of feelings, suggested rewrite: 'you will see…'"). Below: the audio preview, re-recordable if the parent edits the text. 7. **Kid mode — the replay screen.** A clean, full-screen view designed for the kid to use, age-appropriate. Big page-turn arrows. One single "play" button per page. No edit chrome, no menus, no notifications. Optional "full tour" button that plays page 1 through page 6 continuously, advancing the illustration as the narration reaches each page boundary. A tiny home button in the corner returns the parent to the editor. 8. **Print preview & PDF export.** Side-by-side typeset preview: two pages per landscape A4 page, illustration on top, narration text below. Big page numbers. Choose: cover sheet on/off, kid's name on cover on/off. Export PDF; the parent prints at the library and laminates at home. 9. **Family share.** Modal: "Share this tour with someone who helps look after your kid". Magic-link email; arrival drops the recipient straight into the kid-mode screen with read-only permission. Optional toggle: allow the recipient to edit too (off by default). 10. **Tour library.** A grid of every tour the parent has built. A tour for kindergarten, a tour for the summer-camp, a tour for the new after-school class. Filter by kid. Re-build button per tour if the school's website has changed. 11. **Settings.** Delete this tour. Delete the kid's photo from history (already deleted from Firebase Storage at extraction time; this clears the local cache too). Change the narration voice. Change the narration language. Sign out. 12. **Footer.** "Made for the Sunday nights before the first Monday." Privacy: "We never train on your tours. We delete your kid's photo within 60 seconds." 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 First-Day-of-School Tour." - Subhead: "Walk your kid through Monday before Monday — built from your school's actual website, narrated calmly in the language you speak at home." - One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic "social story" template: it reads the school's actual website (not a stock template), it never invents teachers' or other children's names, it never promises feelings, and the illustrations are always stylised with no recognisable faces. - Visual: a soft-watercolour illustration of the same child walking up the path, with a small annotated callout marking the three safety rails (no named teacher, no recognisable face, no promises of feelings). **Slide 2 — Try it now.** - One short prompt: "Try with the sample tour". - A live demo pre-loaded with the Bergen County kindergarten tour from section 8a. - 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the parent-handbook PDF excerpt that grounded page 3, the Korean narration on page 4, the soft watercolour illustration of the cubby). **Slide 3 — How to remix this.** - Headline: "Make this yours." - Three short bullets: - "Swap the sample school URL for your own." - "Adjust the calm-narration rules in `/server/prompts/compose-page.ts` if your kid responds better to a different register." - "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)** — reads the full school website, the parent handbook PDF, the daily-schedule PDF, the arrivals page, the lunch menu in a single call. Extracts the structured Morning Plan that drives the six pages. - **Gemini 3.5 Flash (multimodal)** — reads one photo of your child to extract three palette strings (hair colour, hair length, skin tone) for the illustrations, then we delete the photo. Reads optional photos of the school exterior or classroom to anchor the illustrations to the real building. - **Gemini 3.5 Flash (multilingual composition)** — composes the narration in your home language directly. Not machine-translated. Supports 30+ languages including Korean, Polish, Tagalog, Tigrinya, Vietnamese, Arabic, Welsh, and more. - **Gemini 3.5 Flash (safety review)** — every page of narration is reviewed by a separate, faster Gemini call before you see it. It catches named teachers, named classmates, promises of feelings, and scary "don't worry" language. - **Nano Banana 2 (`gemini-3.1-flash-image`)** — generates the six illustrations in one consistent soft-watercolour style. No photorealism. No recognisable faces. The protagonist is always small in the frame, often from behind. - **Gemini TTS** — narrates each page in your home language at a slow, kind reading pace. - **Firebase Auth** — Google and Apple sign-in, family share via magic links. - **Firestore** — stores your tours, syncs across devices in real time. Your kid's name lives here; it is never sent to image generation. - **Firebase Storage** — temporarily stores the photo of your child during palette extraction (deleted within 60 seconds), and permanently stores the six generated illustrations. - **Cost note** — see the detailed breakdown in 6d. A full six-page tour costs about $0.35 of Gemini API spend, total, generated once. - **Privacy note** — your tour is private to you and anyone you share it with. The photo of your child is used ONCE — to read three palette strings (hair colour, hair length, skin tone) — and is then deleted from Firebase Storage within 60 seconds. The photo is never passed to Nano Banana 2. The child's face is never drawn. The child's name is never sent to Nano Banana 2. 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. **Backend services this app depends on:** - Auth: see section 4b - Database: see section 4b - Storage: see section 4b - Email: see section 4b - Payments: see section 4b (not used in v1) - External APIs: see section 4b **Environment variables you'll need to configure:** - `GEMINI_API_KEY` — your Google AI Studio API key - `FIREBASE_PROJECT_ID` — your Firebase project id - `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only) - `CRAWLER_USER_AGENT` — a custom User-Agent string so school websites can identify the crawler (default: "FirstDayTourBot/1.0 (+https://example.com/about-the-bot)") **Cost + privacy notes:** - One short paragraph per cost-sensitive capability: long-context site read is billed per token of input — a typical school site read is ~80k tokens of input + a small structured output, costing about $0.11 per tour build. - One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this tour forever" — gone in 60 seconds), and the specific commitment about the child's photo: "uploaded once, used once for palette extraction, deleted from Firebase Storage within 60 seconds of extraction completing, never sent to Nano Banana 2". **Documentation links:** - AI Studio Build docs - Gemini API long-context, multimodal, multilingual, TTS docs - Nano Banana 2 docs - Firebase Auth, Firestore, Firebase Storage docs - A short note on the safety-review pattern (a second Gemini call as a checker on the first) **Accessibility:** same standards as the onboarding modal — focus trap, `Esc`, ARIA, restored focus. **Behaviour:** - Always available — single click from anywhere in the app. - Tooltip on the `(i)` icon: "How this app is built". - Mobile: opens as a full-screen sheet that slides up. - Should be the most honest part of the app — never hand-wave service requirements; never say "AI" without naming the specific Gemini model and capability. ## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping) - **Read the school website (Gemini 3.5 Flash, medium thinking)** — typical school site crawl ≈ 80k input tokens (home page + kindergarten page + parent handbook PDF + daily-schedule PDF + a few smaller pages) + ~2k output tokens (the MorningPlan JSON). Input: 80,000 × $1.50 / 1,000,000 = $0.100. Output: 2,000 × $9 / 1,000,000 = $0.010. **~$0.11 per tour build.** - **Extract palette from the kid's photo (Gemini 3.5 Flash, low thinking)** — one image, ~250 output tokens. ~$0.003 per tour. - **Extract palette from school photos (Gemini 3.5 Flash, low thinking)** — up to 3 images, ~300 output tokens total. ~$0.005 per tour. - **Compose narration per page (Gemini 3.5 Flash, medium thinking)** — 6 calls per tour, each ~6k input tokens (the MorningPlan + palettes + safety rules) + ~250 output tokens. Input: 36,000 × $1.50 / 1,000,000 = $0.045. Output: 1,500 × $9 / 1,000,000 = $0.008. **~$0.053 per tour, all 6 pages.** - **Safety review per page (Gemini 3.5 Flash, low thinking)** — 6 calls per tour, each ~500 input tokens + ~150 output tokens. Input: 3,000 × $0.075 / 1,000,000 = $0.0002. Output: 900 × $0.30 / 1,000,000 = $0.0003. **~$0.0005 per tour.** - **Page illustrations (Nano Banana 2)** — 6 illustrations × ~$0.03/image = **~$0.18 per tour.** - **TTS narration (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A six-page tour is ~600 characters of narration total, ~$0.002 per tour. - **Re-rolls** — a parent who re-rolls one illustration per tour adds ~$0.03. A parent who edits one page's text and re-runs the safety review and the TTS adds ~$0.003. - **Expected per-tour cost on first build:** ~$0.35. **Including a couple of re-rolls and one text edit:** ~$0.40. **Replays cost nothing** — TTS is generated once and cached in Firebase Storage as MP3. - **Image storage:** Firebase Storage standard tier, ~$0.026/GB/month. A six-page tour with six PNG illustrations (~1 MB each) + six MP3 narration files (~150 KB each) ≈ ~7 MB ≈ ~$0.0002/month. A family with five tours uses ~35 MB, costing about $0.001/month. ## 7. Design language - **Mood:** A picture book the parent reads before bed, made calmly in the kitchen on a Wednesday evening. Not a tech product. Not a "social story" template. The kitchen island at 8 pm with the laptop open, the kid already in bed, the school's website pasted into a text field, a cup of tea cooling beside the trackpad. The single tap that turns into Monday morning being a known shape. - **Typography:** Display serif for the page narration (Source Serif Pro or Fraunces) at a generous size, set in calm short paragraphs. A friendly sans for the parent-facing app chrome (Inter or Geist). Never use a "child-like" novelty font for the kid-mode view; the narration is set in serif because that is how picture books are set, and the kid recognises the register. - **Palette:** Warm white background `#FBFAF6`, soft ink `#1C1A17` for body text, mossy green `#5B7F58` for primary buttons (the green of the front door in the hero illustration), warm clay `#C97C5D` for the gentle accent on safety-review notes, soft butter yellow `#F0E2A0` reserved for "look here" emphasis on the per-page editor, calm sky `#7E9DBE` for the parent's annotation marks so they cannot be mistaken for the model's narration. No bright primary blues. No SaaS purple. - **Imagery:** The Nano Banana 2 illustrations are the hero. They are reproduced at full width on the kid-mode replay screen, with the narration text set below in a quiet typesetting. The parent-mode editor surrounds each illustration with thin warm-grey chrome so the illustration always feels like the centre of gravity, never a thumbnail. The hero illustration on the welcome screen is a Nano Banana 2 generation from the locked style prefix — same artist, same palette as the kid's eventual tour. - **Hand-feel touches:** A very faint paper grain on the kid-mode replay backgrounds. The page-turn animation is a slow horizontal slide (450 ms by default, instant with reduced motion). The "(i)" icon in the header has a single soft glow on hover — never aggressive. - **Spacing:** consistent 4-px base. Generous whitespace. The kid-mode screen has at least 48px of breathing room around each illustration so the kid's eye is not crowded. - **Radius:** consistent token set (e.g. 8 / 16 / 24 px). Page illustration cards use 16; the welcome card uses 24; the safety-review inline cards use 8. - **Shadows:** subtle, layered, warm-tinted. Avoid heavy drop-shadows. - **Motion:** purposeful — page turns, soft fades, the "approve & save" confirmation slowly relaxing into a green check. Respect `prefers-reduced-motion`. No bouncing splash animations. No spring physics on the page turns — slow, calm slides only. - **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Paste your school's website to start"). ## 8. Content generation rules - Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'. - Invent plausible school names, addresses, kid names, narration text that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on the multi-cultural examples — a Korean-American kindergartener in Bergen County, a Polish-Irish first-grader in Drumcondra, a Filipino-Canadian pre-K kid in Calgary — but never claim that an invented school is a real institution. - Tone: warm, direct, free of corporate language. This template is for a parent, not a company. - Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'. - Body copy: short paragraphs (2-4 sentences). Use lists where appropriate. - Plain language. Avoid jargon. Where the parent is using the app at 8 pm on a Wednesday, every word is read in a hurry and must mean exactly what it appears to mean. - Where the app outputs AI-generated content (the page narration), never label it as "AI says" — let it speak naturally. The safety-review feedback on the per-page editor is the only place the model's mechanism becomes visible to the user, and even there the language is gentle ("we noticed 'you will love it' — that's a promise of a feeling, which can backfire if the kid doesn't feel it. Suggested rewrite: 'you will see…'"). ## 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 tours (sidebar):** - "Min-jun's Bergen County Kindergarten Tour" (6 pages, Korean narration, sample family: a Korean-American household in Tenafly, NJ; child is starting kindergarten on Monday). - "Kasia's Drumcondra Reception Tour" (6 pages, Polish narration, sample family: a Polish-Irish family who arrived in Dublin in July; daughter is starting at a Catholic primary). - "Mateo's Calgary Pre-K Tour" (6 pages, Tagalog with English winter-clothing words; sample family: a Filipino-Canadian household in Calgary's northeast; son is starting pre-K in late August). - "Sample English-language tour — Devon village school" (6 pages, English narration; sample family: a five-year-old starting reception at a small Devon village primary). **Sample tour in detail view (this is what the demo should show — Min-jun's tour):** - **School (verbatim, sample only):** "Maple Ridge Elementary School", a fictional public elementary in Bergen County, NJ - **Source URL:** `https://www.maple-ridge-sample-school.example.edu` - **Language of source site:** en-US - **Language of narration:** ko-KR - **Child age band:** kindergarten - **Parent-provided worry:** "she is worried the other kids will speak English and she will not understand" - **Parent-provided morning feel:** anxious - **Morning plan steps (6):** - `arrival` — school-facing: "morning drop-off at the front circle"; kid-facing (Korean): "월요일 아침, 학교 앞 길을 걸어 올라가요" — "On Monday morning, you walk up the path to your school." - `entry` — school-facing: "main entrance, green front doors"; kid-facing: "초록색 문을 지나 안으로 들어가요" — "You go through the green doors." - `cubby_or_locker` — school-facing: "kindergarten cubbies in the K wing hallway"; kid-facing: "사물함에 가방을 걸어요" — "You hang your bag in your cubby." - `morning_circle` — school-facing: "morning meeting on the carpet, 8:40 am"; kid-facing: "동그라미 모임 시간에 카펫에 앉아요" — "You sit on the carpet for circle time." - `free_play_or_centers` — school-facing: "centers and snack, 9:30 am"; kid-facing: "여러 활동을 하고 간식을 먹어요" — "You do different activities and have a snack." - `end_of_day` — school-facing: "pick-up at the front circle, 3:00 pm"; kid-facing: "오후에 아빠가 같은 자리에서 기다려요" — "In the afternoon, Daddy waits for you in the same spot." - **Sample page 4 narration (carpet circle, in Korean):** - "민준아, 그다음에 너는 카펫에 앉을 거야. … 다른 친구들도 카펫에 앉을 거야. … 선생님은 가까이 계실 거야. … 만약 영어로 무슨 말인지 잘 모르겠으면, 선생님께 천천히 다시 말해 달라고 부탁할 수 있어." - Rough English meaning (for the builder, not for the kid): "Min-jun, after that, you will sit on the carpet. The other kids will sit on the carpet too. Your teacher will be nearby. If you do not understand something in English, you can ask your teacher to say it again slowly." - **Sample page 4 illustration prompt seed:** "a soft round rug with a few simple cushions, warm sunlight from a high window, the back of a small child sitting cross-legged, a softly drawn adult standing nearby seen from behind" - **Locked style prefix (shared across all six pages):** "Soft watercolour illustration in the style of a calm contemporary children's picture book. Warm light, simple shapes, gentle colours, visible paper texture, hand-drawn linework. No photorealism. No recognisable face: faces are suggested with at most two small dots for eyes and a single short line for a mouth, often partially turned away or seen from behind. One small child is the protagonist — small in the frame, often with the back of their head visible. The child's hair colour is warm dark brown, the hair length is short, the child's skin tone is warm beige. If other people appear, they are softly drawn adults or other children, never the focus, never with recognisable faces. No text in the image. No school logos. No brand names. No photographic look." - **Safety review notes (sample, page 4 first pass — caught):** one violation, `promise_of_feeling`, excerpt "너는 친구들이랑 잘 지낼 거야" ("you will get along well with friends"), fix suggestion "rewrite as a description of what the kid will see and hear, not how they will feel". - **Number of re-rolls in this sample tour:** 1 illustration re-roll (front-door colour adjusted from blue to green to match the school's actual photo); 0 narration edits after safety-review approval. - **TTS voice:** the Korean Gemini 2.5 Flash TTS voice with `languageCode: ko-KR`, female, warm. - **Total cost of this sample tour build:** ~$0.38. **Sample input artefacts (for the build to demonstrate):** - A pasted URL of a fictional school home page (`https://www.maple-ridge-sample-school.example.edu`) and its linked kindergarten page, parent handbook PDF, daily-schedule PDF, arrivals-and-dismissal page, and lunch menu — all included as static seed HTML in `/data/seed-school-site/`. - A photo of the sample child Min-jun, included only in the seed content as a stylised soft-watercolour illustration (no real child's photo ships with the template — the seed photo is itself a generated illustration, demonstrating the palette-extraction call's behaviour on a non-photographic input). - A second sample school for the Dublin reception tour (a fictional `https://www.drumcondra-sample-primary.example.ie`) with bilingual English/Irish content. - A third sample for the Calgary pre-K tour (`https://www.calgary-sample-school.example.ca`) including the school's "winter arrivals" page. **Sample voice copy:** - Onboarding: "Paste your school's website. We'll walk your kid through Monday — calmly, in the language you speak at home." - Processing: "Reading the school's website…" / "Planning the six pages…" / "Drawing the front door…" / "Recording the voice…" - Empty tour library: "This is where your tours will live. Paste a school website to start one." - Error (couldn't read the school site): "We couldn't reach this school's website. Are you sure the address is right? If the school's site is password-protected, we cannot read it — try uploading a copy of the parent handbook PDF instead." - Safety-review caught a promise of a feeling: "We noticed 'you will love it' on page 4. A promise of a feeling can backfire if the kid doesn't feel it. We've rewritten this to: 'you will see…' Want to keep the rewrite?" - Save confirmation: "Saved to Min-jun's tour. You can play it tonight, or share it with someone." - Family share invitation email subject + body: - Subject: "A calm Monday-morning tour for Min-jun" - Body: "Hi — I made a six-page picture-book tour of Min-jun's first day. It's calm, it's in Korean, and we can play it any night this week to help her get ready. Tap to open and play." [Open Tour] ## 9. Media & assets - **Hero image (landing screen):** A soft-watercolour illustration of a small child, seen from behind, holding a parent's hand, walking up a path to a green front door of a school, warm morning light. Generate via Nano Banana 2 using the locked style prefix; do not use stock photography. - **App icon / wordmark:** Set in the display serif. A small watercolour-style green door as the icon. No bright SaaS gradient. - **Empty-state illustration:** A simple line drawing of an empty cubby waiting for a backpack. Hand-drawn aesthetic, not a flat icon. - **Sample tour illustrations:** Generated per the prompts in section 8a — Nano Banana 2 generations from the locked style prefix, one per page across the four sample tours. Each illustration is checked to ensure no recognisable face appears. - **Page-turn sound (optional, kid-mode):** a soft real-paper page-turn sound; off by default; the parent can enable it in settings. - **Stock fallbacks:** If Nano Banana 2 fails mid-tour, fall back to a hand-illustrated placeholder from `/public/samples/sample-page-fallback.png` and surface "we couldn't draw this page right now — re-roll?" to the parent. Never to a generic stock photo of a school. - **Generated imagery:** prefer Nano Banana 2 over stock photography. The locked style prefix is the single source of consistency. - **Optimisation:** WebP/AVIF for the page illustrations (PNG fallback for print export); `loading="lazy"`; explicit `width`/`height` to prevent layout shift. Pre-generate three resolutions per illustration: 480px for the editor thumbnail, 1024px for the kid-mode replay screen, 2048px for the print PDF. - **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only. ### Build-time asset manifest (explicit specs) Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs. **Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id: - `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted. - `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence. - `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible. **Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill): - `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt. - `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline. - `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails. **Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint. **Hard rules** - Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error. - No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code. - Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`). - File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`. ## 10. Interactivity & states - Every interactive element has hover, focus, active, and disabled states. - Forms validate inline and show specific error messages (not "Invalid input"). The school URL field validates that the URL is reachable; if not, it shows "We couldn't reach this address. Could you double-check it, or paste the kindergarten page directly?" - Loading states use skeletons that match the eventual layout, not spinners. The page-by-page generation has visible skeletons for each of the six illustration cards. - Empty states explain the next action with a button whose label fits THIS app's domain: "Paste your school's website", "Upload one photo of your kid", "Try the sample tour" — never a generic "Get started". - Smooth scroll for in-page anchors. - All AI-generated content streams in token-by-token where supported, with a clear "thinking…" indicator before content starts arriving — visible only in the parent-mode editor, never in the kid-mode replay screen. - If an AI call fails, show a calm, specific error ("We couldn't draw this page right now — Nano Banana 2 returned an error. Try re-rolling, or come back in a few minutes.") and offer retry. - The safety-review feedback appears inline in the per-page editor, calmly, with the rule name in plain English. The parent can accept the suggested rewrite or override it. - The page-turn animation in kid-mode takes 450 ms with `prefers-reduced-motion` falling back to instant. - The "approve & save" button on the tour preview slowly relaxes into a soft green check on success, never a celebratory bounce. ## 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 long-context site read, palette extraction, and narration composition; `gemini-3.5-flash` for the per-page safety review; `gemini-3.1-flash-image` for the six illustrations; `gemini-3.1-flash-tts-preview` for the narration audio. Set `thinkingLevel` explicitly per call. - **Database:** Firestore (auto-provisioned by AI Studio Build). Show the four sample tours on first launch. - **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email for family share. - **Storage:** Firebase Storage for the generated illustrations (permanent) and the parent-uploaded kid photo (deleted within 60 seconds of palette extraction). Pre-signed URLs only. - **Web crawler:** server-side fetch with a polite User-Agent string, respect for `robots.txt`, cap the crawl at the home page + the 5-6 named child pages + 2 PDFs. Never store the crawled HTML beyond the lifetime of the Gemini call. - **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. The kid-mode replay screen is especially tested on 768 px (a typical kid's tablet) and on 1024 px (a parent's laptop). - 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 tour preview so the per-page generations appear as they complete. - Optimistic UI on writes; reconcile on response. - The kid-mode replay screen has an explicit "kiosk-like" lock: no notifications, no haptics, no scrolling outside the page-turn flow. Tapping the corner returns the parent to the editor with a brief tap-and-hold gesture. ## 12. Accessibility (WCAG 2.2 AA) - Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`. - All interactive controls reachable by keyboard with a visible focus ring. - Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. - All images have meaningful `alt` text. The page illustrations have `alt` describing the scene at the level a screen reader user wants: "page 4 — a soft watercolour of a round carpet with cushions, a small child sitting cross-legged seen from behind, warm sunlight from a high window". - Form fields have associated `