================ 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.
---
# Why Machine
## 1. Project
**Why Machine** is a calm, age-calibrated answering companion for the
parent of a small child who asks "but why?" forty times a day. The
parent (or the child themselves, with the parent within earshot)
voices a question. Gemini answers it in roughly ninety seconds — in
vocabulary a five-year-old actually uses — and then offers three
follow-up "but why?" buttons the child can pull on, so the next
question is always the next *deeper* question, not the same one
again. Curiosity scales instead of stalls. The parent doesn't have
to invent thirty kitchen-sink physics lectures over breakfast.
This is the kind of app a Filipino-American mum in Daly City opens on
the school run when her four-year-old, Mateo, asks for the fifth time
that morning *why is the sky blue?* and her own honest answer —
*because light has colours in it and the sky picks blue, anak* — is no
longer landing. It is also the kind of app a Pakistani-Canadian dad in
Mississauga opens at 7 pm when his five-year-old, Zara, has just
asked *but why do my eyes see at all, baba?* and he has been at the
hospital since six in the morning and would honestly like to know the
answer himself. And it is the kind of app a Brazilian mum in São
Paulo opens when her six-year-old, Helena, asks *mas por que as
borboletas têm pó nas asas?* in the kitchen while she stirs feijão
and wishes for two more hands. Same shape of moment — a small child
pulling on a thread of the world — different first language,
different question, different evening.
The single demo that proves the magic: the child asks *"why is the
sky blue?"* into the parent's phone. In under three seconds a voice
that is unhurried and gentle, with the rhythm of an aunt telling a
short story, begins: *"So — sunlight looks white, but it's really lots
of tiny colours all mixed together. When sunlight goes through the air
above us, the air bumps the blue colour around the most, like a
little kid kicking a ball into every corner of the playground. So when
you look up, blue is the colour that has bounced into your eyes from
everywhere. That's why the sky looks blue."* The answer ends. Three
soft buttons appear: **"but why is light made of colours?"** ·
**"but why do my eyes see blue?"** · **"ask a new question"**. The
parent can sip their coffee. The child taps the first one.
And in the questions the model is genuinely uncertain about —
*"why do we dream?"*, *"why is there something instead of nothing?"*,
*"why did Grandpa die?"* — Why Machine says, in a five-year-old's
words, *"the honest answer is grown-ups don't fully know, but here's
the part we do know"*. It never pretends. Some of the most important
moments a child has with a parent is the moment the parent says *I
don't know*. The app holds that line for them.
**Tagline:** _The forty-questions-a-day machine — turn any "but why?" into a calm, age-calibrated answer, in any home, any language, any pace._
## 2. Target audience
- Parents of three-to-eight-year-olds in the "but why?" phase — the years when curiosity outruns the adult attention budget
- Bilingual and multilingual households where the parent's strongest answering language is not the child's school language, and answers should be available in either
- Parents of neurodivergent kids whose special-interest depth (trains, dinosaurs, weather, machines) outpaces what a single adult can field — autistic children especially benefit from a long, calm, on-topic deepening track instead of "we'll look it up later"
- Grandparents and aunts/uncles who do regular childcare — pickup grandparents who want the kid to keep asking but don't always have the vocabulary in the moment
- Single parents and solo carers running long days alone — the question budget exhausts faster with one adult and no relief
- Foster parents and adoptive parents in the early bonding months when a steady, calm "I will answer your questions" presence matters
- Homeschool and worldschool parents who use kid-questions as the spine of their day's learning
- Teachers in early-years classrooms (ages 4-7) who want a small-group "ask the machine" station that doesn't sit them in front of YouTube
- Children's-hospital and paediatric-ward family rooms — kids in long admissions, parents emotionally tapped, calm voice doing some of the answering
- Family caregivers of a parent in late-stage dementia who has rediscovered the "but why?" register — gentle, calm, age-neutral explanations are surprisingly useful here too (with a different on-screen mode, see 6)
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons parents pick this app.
- **Calibrated to the actual child, not a generic age band.** The parent sets the child's age, the words they already know (trains, dinosaurs, weather, the body), the words they don't yet (chemistry, atom, gravity), and the household's first language. Gemini 3.5 Flash tailors every answer to that calibration — a vocabulary list, a metaphor budget, a sentence-length ceiling — instead of dumping a Wikipedia paragraph.
- **The "next deeper why?" is the killer feature.** After every answer, the app proposes the three most natural follow-up *whys* the child would ask next — phrased in the child's voice, not the parent's. Tap one and the next answer arrives. Curiosity becomes a tree the child climbs, not a wheel the parent pushes.
- **Never makes up science.** When Gemini is uncertain — *"why do we dream?"*, *"why is there gravity?"*, *"why did the dinosaurs die?"* — the answer leads with *"the honest answer is we don't fully know"* and then explains the part we do. Children deserve epistemic honesty more than adults do. This is the single most important rule in the system prompt and the one most expensive to get wrong.
- **Hard topics, age-calibrated, with the parent in the loop.** Death, divorce, illness, war, sex, the news — every "hard topic" has a parent-set policy: *answer me first* (the answer is queued for the parent to approve before the child sees it), *answer gently* (the answer plays, with a soft summary to the parent's lock screen afterwards), *not now* (the app says, in the child's voice, *"that's a question to ask your mum or dad — let's go find them"*). The parent stays the parent.
- **Voice-first, screen-secondary.** A child asks out loud; the answer plays back through the phone speaker or AirPods. The screen is for the *next-deeper-why* buttons and for the small illustration that arrives a few seconds in. This is not a TikTok pacifier.
- **Multilingual answering for multilingual homes.** The Filipino mum can ask in Taglish and get an answer in either Tagalog or English (or both, code-switched gently like her own household). The Pakistani-Canadian dad can ask in Urdu and the answer comes back in Urdu, or English, or both — with the technical word ("oxygen") in English and the soft connectors in Urdu. The Brazilian mum can ask in Portuguese and get Portuguese back, with the option to slip into English for the rare technical noun. Asian, African, Latin American, European languages all sit equally in the catalogue.
- **A calm voice the child trusts.** Gemini TTS at the slow end of letter-reading pace, with breath, with pauses at the comma — not the sing-song fake-cheery voice of half the kids' apps. Parents tell us it sounds like a kind aunt who has nowhere else to be.
- **One small illustration, generated in-line.** As the audio plays, a soft Nano-Banana-2 illustration of the answer's central image fades in — the sky as a playground, the eye as a window with curtains, the heart as a pump. One image per answer. Never a deluge. Never a slot-machine carousel.
- **Saved as a "what we wondered about today" book.** Every question the child has asked, every answer that played, every illustration that arrived — saved as a private book in the family's archive. Read it back at the end of the week. Print it at the end of the year.
- **No ads, no tracking, no "see more videos like this".** The app's job is to answer one question well and stop. There is no infinite-scroll surface anywhere.
## 4. Features to build
- Big push-to-talk button on home screen, sized for a four-year-old's thumb and high-contrast for low-vision adult co-users; long-press to speak; release to send
- Live API streaming question-in / answer-out — the answer begins playing before the question has finished processing
- Age + language + vocabulary calibration set by the parent on first run (and edited any time in Settings → "Who is this for?")
- "But why?" follow-up button generator — three follow-up whys appear in the child's own voice register after every answer
- "Ask a new question" button — always present, always one tap
- Hard-topic policy editor — per-topic toggle for *answer me first* / *answer gently* / *not now* — covers: death, divorce, illness, sex, the news, war, money worry, scary animals, religion, racism, gender, the child's own diagnosis
- Parent lock-screen summary — when "answer gently" is set, the parent's phone shows a one-line summary of what the child just asked and was answered ("Mateo just asked about death; the answer was age-calibrated, here's the transcript")
- Honest-uncertainty mode — calls the model in a way that *requires* the answer to start with "the honest answer is we don't fully know" when confidence is low, and lets the model gracefully say so
- "What we wondered about today" book — every question and answer of the day saved as a printable picture-book page
- Saved-answers archive — search the whole library by topic, by day, by who asked
- Multi-child mode — toggle between siblings; each one's calibration, each one's archive
- Multi-parent mode — both parents (and grandparents, child-minders) co-own the archive and the calibration; only the *primary* parent can change hard-topic policies
- Time-of-day mode — *morning brisk* / *dinner-table calm* / *bedtime hush* — alters voice pace and length budget per slot
- Sleep mode — at the child's bedtime the app refuses to start a new thread, suggests "let's save that one for tomorrow and ask first thing", logs the question so it doesn't get lost
- One-image illustration per answer — Nano Banana 2 generates one soft illustration of the central metaphor; never more than one per answer
- Voice catalogue — pick from a small set of calm voices in the household's language(s); pre-listen each one with the same sample sentence
- Translate-with-context button — replay the same answer in a different language, preserving the metaphor (the sky as a playground works in Tagalog, English, Urdu, Portuguese — verify and adapt; the parent reviews if it doesn't)
- Family-quote moments — when the child says something extraordinary in their question ("but why does the moon follow our car?"), it gets saved verbatim into the book with the date and the answer that followed
- Offline graceful degradation — if the network drops mid-answer, the audio finishes from buffer, no scary error, the next question waits with a "we'll pick up when we're back" card
- Parent-only "what did my kid ask today?" digest — end-of-day email or in-app digest, optional, with the child's questions and the answers given
- Refuse-list — the parent can add words or topics the app must never field ("just send the child to me if they ask about Grandma's accident"); the app says "that's one for your mum or dad" in the child's voice and quietly notifies the parent
- Print-an-illustration mode — for the child who wants to colour their own version of the illustration, output a high-contrast line-art version to print at home
## 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)
- **Live API (voice-in, voice-out, low latency)** — the primary surface. The child speaks; the answer begins playing in under three seconds. Streaming token-by-token to the TTS pipeline so the first words land before the question's last word has been processed. Live API session uses an age-calibrated `systemInstruction`, with the household's calibration variables interpolated.
- **Gemini 3.5 Flash (reasoning + structured output)** — for the "next deeper why?" generator. Given the answer just played, produce three follow-up *why* questions in the child's voice, at the same calibration. Low thinking for typical cases, medium for hard topics (where the next-deeper-why choices are themselves sensitive).
- **Gemini 3.5 Flash (cheap structured pass)** — for the policy guard: takes the child's incoming question and the parent's hard-topic policy and routes it to *answer freely* / *answer gently* / *answer-me-first* / *not now*. Sub-200ms decision; precedes the Live-API answer call.
- **Gemini 2.5 Flash Preview TTS** — for the calibrated voice. We use TTS rather than Live API's built-in voice when the parent has picked a specific calm voice from our catalogue, or when we need to render the saved answer in another language for the book. The Live API hand-off to TTS is invisible to the user but matters for voice consistency in the archive.
- **Multilingual register handling (Gemini 3.5 Flash)** — handles Tagalog and Taglish, Vietnamese, Cantonese, Mandarin, Korean, Japanese, Tamil, Hindi, Urdu, Bengali, Punjabi (Gurmukhi and Shahmukhi), Amharic, Swahili, Farsi, Khmer, French, Spanish, Brazilian and European Portuguese, German, Italian, Arabic, English. The household-language matrix is in the calibration; the answer respects code-switching when the parent allows it.
- **Search grounding (Gemini 3.5 Flash + `google_search`)** — for fresh-fact questions where the answer must not be older than the model's training cut-off ("why is it cold today?", "why did the rocket explode?"). Used sparingly and only when the model itself flags the question as time-sensitive.
- **Nano Banana 2 (`gemini-3.1-flash-image`)** — one image per answer, generated from a short illustration brief. Style is "soft hand-drawn picture-book, warm, low-saturation, no text in the image, one central scene".
- **Long context** — for the "what we wondered about today" book and the family-quote feature: at the end of the day Gemini 3.5 Flash reads the day's questions and answers as a single context and produces a one-page picture-book layout brief. A year of daily questions is ~50k tokens — comfortable. We chunk by month if a family is rebuilding the whole year at once.
- **Thinking levels** — `low` for the policy guard and for follow-up-why generation on neutral topics. `medium` for the primary answer call and for follow-up-why generation on hard topics. We deliberately do not use `high` — children's questions reward calm, fast, grounded answers, not deep deliberation.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). **Apple sign-in is optional but user-configured**: requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used to invite a second parent or a grandparent) requires the sender domain to be authorised in Firebase Auth. The child does NOT have an individual account; the parent's account scopes everything.
- **Database — Required.** Firestore for `families`, `children`, `calibrations`, `policies`, `questions`, `answers`, `daily_books`, `parent_invitations`, `voice_preferences`.
- **File storage — Required.** Firebase Storage for the generated illustrations and for the TTS audio of each saved answer. **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 answer is saved. Pre-signed URLs only; the child's question audio and the generated answers are private to the family.
- **Email — Required (transactional).** Magic-link invitations for a second parent / grandparent. Optional daily digest email to the primary parent (off by default).
- **Payments — Not needed for v1.** Free for personal use. A future "printed picture-book of the year's questions" tier could pipe to a print-on-demand partner and charge for the physical artefact only.
- **External APIs:** Gemini API for all intelligence. No third-party content APIs — children's questions are answered from Gemini's pretraining, with `google_search` grounding only when freshness matters and the policy guard has approved it.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, any future Stripe key) 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 child's data' inside the UI with a real 60-second hard delete · explicit opt-in for any analytics · the child's audio 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) · COPPA-style posture: the parent is the account holder, the child's data lives under the parent's account and is deletable in one tap.
**Read this first — prompt-craft rules that apply to every call in this template:**
1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below.
2. **Pin `thinkingLevel` explicitly** per call. See the matrix.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schema is below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.**
4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate.
5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled.
6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow.
7. **Grounded responses can wrap JSON in ```json fences or add prose preamble.** Server-side, strip fences and brace-extract:
```typescript
function safeExtractJSON(raw: string): T {
const clean = raw.replace(/```json\s*|```/gi, '').trim();
const s = clean.indexOf('{'); const e = clean.lastIndexOf('}');
if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response');
return JSON.parse(clean.slice(s, e + 1)) as T;
}
```
8. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Policy guard — route an incoming question | `gemini-3.5-flash` | low | (none) |
| Live answer to the child's question | `gemini-3.5-flash` (Live API session) | medium | (none) — Live API voice |
| Fresh-fact answer (e.g. weather, current event) | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| Generate three "next deeper why?" follow-ups | `gemini-3.5-flash` | low | (none) |
| Generate three follow-ups on a hard topic | `gemini-3.5-flash` | medium | (none) |
| Render an answer's voice in a chosen catalogue voice | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Translate a saved answer into another household language | `gemini-3.5-flash` | low | (none) |
| Generate one picture-book illustration per answer | `gemini-3.1-flash-image` | n/a | n/a |
| End-of-day "what we wondered" book layout | `gemini-3.5-flash` | low | (none) — long-context over the day |
*Note for builders:* on the TTS call and the image-generation call, 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. On the fresh-fact answer call, `responseSchema` and `google_search` cannot be combined in the same Gemini call today — instruct the model to emit a JSON object inside the text body and parse server-side; read citation URIs from `response.groundingMetadata.groundingChunks[].web.uri`, not from the JSON body.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const Calibration = z.object({
child_name: z.string(),
child_age_years: z.number().min(2).max(12),
child_pronoun: z.enum(["she", "he", "they"]),
household_languages: z.array(z.string()), // BCP-47, in order of preference
preferred_answer_language: z.string(), // BCP-47
allow_code_switching: z.boolean(), // can the answer slip into a second language for nouns?
vocabulary_known: z.array(z.string()), // words the child already uses comfortably
vocabulary_unknown: z.array(z.string()), // words the child does NOT yet know — avoid or define
metaphor_anchors: z.array(z.string()), // "trains", "dinosaurs", "the playground", "cooking", "the body"
max_sentence_word_count: z.number().min(6).max(20),
max_answer_seconds: z.number().min(30).max(180),
voice_id: z.string(), // chosen from the catalogue
time_of_day_mode: z.enum(["morning-brisk", "midday-calm", "dinner-calm", "bedtime-hush"]),
});
const Policy = z.object({
topic: z.enum([
"death", "divorce", "illness", "sex_bodies",
"the_news", "war", "money_worry", "scary_animals",
"religion", "racism", "gender", "child_own_diagnosis", "other",
]),
policy: z.enum(["answer-freely", "answer-gently", "answer-me-first", "not-now"]),
notes_for_model: z.string().nullable(), // parent-supplied context, e.g. "Grandma died last month"
});
const PolicyGuardDecision = z.object({
detected_topic: z.string(), // free text, model picks
matched_policy_topic: z.string().nullable(), // null if no policy matches; default applied
decision: z.enum(["answer-freely", "answer-gently", "answer-me-first", "not-now"]),
decision_reason_for_parent: z.string(), // one sentence for the parent dashboard
freshness_required: z.boolean(), // true if grounded search needed
});
const HonestyFlag = z.enum([
"well-established-science",
"the-answer-is-uncertain",
"the-answer-is-contested",
"the-answer-depends-on-belief",
"the-answer-is-not-known-by-anyone",
"the-answer-is-developmental-and-personal",
]);
const Answer = z.object({
answer_id: z.string(),
question_verbatim: z.string(), // exactly what the child said
question_language: z.string(), // BCP-47
answer_language: z.string(), // BCP-47
answer_text: z.string(), // ready for TTS
answer_seconds_estimated: z.number(),
honesty_flag: HonestyFlag,
honest_uncertainty_lead_sentence: z.string().nullable(),
// if honesty_flag is not well-established, the answer
// MUST begin with this sentence
central_metaphor: z.string(), // "the sky as a playground"
vocabulary_used_outside_known: z.array(z.string()),
// any word not in vocabulary_known — flagged for review
illustration_brief: z.string(), // 1-2 sentences for Nano Banana 2
code_switch_inserts: z.array(z.object({
phrase_verbatim: z.string(),
language: z.string(), // BCP-47
gloss: z.string(),
})),
source_grounding_uris: z.array(z.string()), // only populated if google_search was used
});
const NextDeeperWhy = z.object({
followup_id: z.string(),
text_in_child_voice: z.string(), // "but why is light made of colours?"
text_in_parent_voice_for_dashboard: z.string(), // "child is asking about the wave nature of light"
predicted_topic: z.string(),
predicted_honesty_flag: HonestyFlag,
predicted_difficulty_for_child: z.enum(["natural-next-step", "small-stretch", "real-stretch"]),
});
const FollowupSet = z.object({
for_answer_id: z.string(),
options: z.array(NextDeeperWhy).length(3), // documentation only — clamp server-side
also_offer_new_question_button: z.boolean(), // always true
});
const QuestionLogEntry = z.object({
entry_id: z.string(),
child_id: z.string(),
parent_id: z.string(),
asked_at_iso: z.string(),
question_verbatim: z.string(),
question_language: z.string(),
policy_decision: PolicyGuardDecision,
answer: Answer.nullable(), // null if decision was "not-now" or "answer-me-first" pending
followups_presented: FollowupSet.nullable(),
followup_tapped: z.string().nullable(),
illustration_uri: z.string().nullable(),
audio_uri: z.string().nullable(),
saved_to_book: z.boolean(),
});
const DailyBookPage = z.object({
date_iso: z.string(),
questions_in_order: z.array(z.string()), // question_verbatims
page_title_for_book: z.string(), // "The day Mateo wondered about the sky"
page_intro_one_sentence: z.string(), // a one-line frame the parent reads aloud
hero_illustration_uri: z.string(), // chosen from the day's illustrations
});
type Calibration = z.infer;
type Policy = z.infer;
type Answer = z.infer;
type NextDeeperWhy = z.infer;
type FollowupSet = z.infer;
type QuestionLogEntry = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` for the live answer to save quota — pin `gemini-3.5-flash` explicitly for the Live API session. Flash answers small children passably but slips on metaphor consistency and code-switching grace; on hard topics it is noticeably colder.
- Agent calls `gemini-3.5-flash` or `gemini-3.1-flash-tts-preview` — wrong model IDs. Use `gemini-3.5-flash`, `gemini-3.5-flash`, `gemini-3.1-flash-tts-preview`, `gemini-3.1-flash-image`. Pin in the call matrix; the agent should never auto-pick.
- Answer is too long — typical adult instinct is to over-explain. The system instruction enforces `max_answer_seconds` and `max_sentence_word_count`. The model is told to *prefer leaving a thread unresolved for the next "but why?"* rather than complete the picture in one breath.
- Vocabulary overshoots calibration — the model uses "wavelength" or "scattering" to a five-year-old. The system instruction lists `vocabulary_known` and `vocabulary_unknown`; the answer must use no word from the unknown list without a one-sentence inline definition that itself uses only known words. Server-side check: `vocabulary_used_outside_known` from the response is diffed against the calibration; if non-empty, the answer is regenerated once with a stricter prompt.
- Hard topic slips past the policy guard — the policy-guard `responseSchema` includes `detected_topic` as free text *and* `matched_policy_topic` from the enum; if the free text contains "die", "dead", "kill", "war", "sex", "cancer", "divorce" but no enum match, server-side route to `answer-me-first` as a safety default and surface to the parent for review.
- Model invents science — explicitly forbidden. The `honesty_flag` enum has `the-answer-is-not-known-by-anyone` for exactly this case. If the model is uncertain, it MUST set the flag and prepend `honest_uncertainty_lead_sentence`. Negative constraint in 4b is repeated in the system instruction.
- Voice is too sing-song — the TTS style directive is "warm, unhurried, slightly low-pitched, with breath at commas, like an aunt telling a short story at the kitchen table". The TTS voice catalogue is curated; the cartoon-fake voices are deliberately excluded.
- Live API session leaks across siblings — each session is scoped to one child; switching siblings closes and reopens the session with the new calibration. Never let one calibration cross-contaminate another.
- Three follow-ups all the same shape — model produces three rephrasings of the same question. Prompt requires *different predicted_topic and different predicted_difficulty_for_child* across the three options.
- Image arrives before the audio — the Nano Banana 2 call starts in parallel with the audio but the UI gates the image fade-in to ~3 seconds after the audio begins, so the picture lands *during* the story, not before it. Server-side delay; never client-side polling.
- Mid-call voice switch on TTS — not supported by Gemini 2.5 TTS. For code-switched inserts, render the inserted phrase in italics in the on-screen caption; the audio stays in one voice. Optional: stitch a second TTS call client-side for dramatic switches in the saved book.
### Negative constraints (hard rules)
- Do NOT make up science. If the model is uncertain, the answer MUST begin with `honest_uncertainty_lead_sentence` — in five-year-old vocabulary, "the honest answer is grown-ups don't fully know yet, but here's the part we do know". Never paper over.
- Do NOT exceed `max_sentence_word_count`. The calibration sets it; the prompt enforces it. Long sentences for small children are a tell of an overproud LLM.
- Do NOT use any word in `vocabulary_unknown` without an inline definition that itself uses only words in `vocabulary_known`. No "wavelength is the distance between two peaks of a wave" to a child who doesn't have "wavelength", "peaks", or "wave" yet.
- Do NOT translate the central metaphor across language without checking it lands. The "sky as a playground" metaphor works in English, Tagalog, Urdu, Portuguese, and Filipino-American Taglish (verified). The "atoms as little balls" metaphor does not land in every language equally — the model should pick one metaphor at a time and confirm it inside the answer's language register.
- Do NOT proselytise. On religion or belief questions, the policy is `the-answer-depends-on-belief` and the answer says *"different families believe different things — here's what's known about how the world works, and here's what your mum and dad can tell you about what your family believes"*. Never advocate.
- Do NOT auto-answer hard topics where the parent's policy says `answer-me-first` or `not-now`. The model is told the policy; the model obeys. Edge case: if the question is ambiguous between an easy topic and a hard topic ("why did the cat go to sleep forever?"), the policy guard routes to `answer-me-first` and notifies the parent.
- Do NOT use the child's audio 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 auto-publish or auto-share. Archives are private to the family and the parents the primary account holder has invited. There is no public-by-default anything.
- Do NOT extend the answer beyond `max_answer_seconds`. Prefer leaving a thread for the next "but why?" tap than completing the picture in one breath.
- Do NOT produce an illustration that contains text, brand marks, child likenesses, or any depiction of a real child. Illustration style is generic picture-book; the central metaphor is the only subject.
- Do NOT serve ads. Do NOT include "see more" links to third-party content. Do NOT track the child across the network. The app's job is to answer one question well and stop.
### 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: Policy guard — route an incoming question
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You are the policy guard for an answering app for a small child. You
receive: (1) the child's question, verbatim, in the language the child
spoke it; (2) the household's calibration (child age, languages,
metaphor anchors, vocabulary); (3) the parent-set policies for each
sensitive topic.
Your job is to classify the question and return a PolicyGuardDecision.
This decision precedes any answer. You are NOT the answerer.
Hard rules:
- Be fast and conservative. If the question touches death, illness,
divorce, sex, the news, war, money worry, scary animals, religion,
racism, gender, or the child's own diagnosis, match it to the
closest topic in the parent's policy list and return that policy's
decision verbatim.
- If the question is ambiguous between a neutral topic and a sensitive
one ("why did the cat go to sleep forever?", "where do babies come
from?"), default to the more sensitive matching topic and let the
parent decide.
- If no policy matches and the question is neutral (sky, animals,
food, machines, weather, plants, dinosaurs, the body in non-sexual
terms, language, maths, colours, sounds, time), return
"answer-freely".
- Set freshness_required=true ONLY for genuinely time-sensitive
questions: today's weather, current news, a sports result, a rocket
launch this week, a politician's name. NEVER for evergreen science.
- decision_reason_for_parent is one sentence — written for the parent
to glance at on a lock-screen. Plain language. No jargon.
Output ONLY a PolicyGuardDecision JSON matching the provided schema.
No commentary. JSON only.
```
---
### Call: Live answer to the child's question
Model: `gemini-3.5-flash` (Live API session) · thinkingLevel: medium · Tools: (none)
```
You are answering a small child who has asked a "why?" question. The
child is named in the calibration. Your voice is warm, unhurried,
slightly low-pitched, with breath at commas — like an aunt telling a
short story at the kitchen table.
Your single most important rule: NEVER make up science. If you are
genuinely uncertain — because the question is not fully understood
by anyone, or because it depends on belief, or because it is a deeply
personal "why?" — you MUST begin the answer with one sentence,
written in the child's calibrated vocabulary, that says so. Examples:
- "The honest answer is grown-ups don't fully know why we dream — but
here's the part we do know."
- "Different families believe different things about that — so I'll
tell you what people who study the stars know, and your mum can
tell you what your family believes."
- "I don't know what was in Grandpa's heart that day. Nobody fully
does. But here is what we do know about why people sometimes feel
very sad."
You receive a Calibration object. You MUST:
- Use only words in vocabulary_known, or define any new word inline
using ONLY words from vocabulary_known. Never use a word from
vocabulary_unknown without that inline definition.
- Keep every sentence at or under max_sentence_word_count words.
- Keep the whole answer at or under max_answer_seconds. Prefer
leaving a thread for the next "but why?" tap to completing the
picture in one breath.
- Pick ONE central metaphor and stick to it. Draw the metaphor from
metaphor_anchors when possible (trains, dinosaurs, the playground,
cooking, the body). Name the metaphor in central_metaphor.
- Answer in preferred_answer_language. If allow_code_switching is
true, you MAY drop in one or two phrases in another household
language for soft connectors or for technical nouns — record them
in code_switch_inserts with translation and language.
- Time-of-day mode tunes pace: morning-brisk is shorter and lighter,
bedtime-hush is the slowest with the most pauses.
- Pronouns: refer to the child using child_pronoun. Refer to
"you" when speaking directly to them.
Honesty flag rules:
- well-established-science → standard answer, no lead sentence.
- the-answer-is-uncertain → lead with "the honest answer is grown-ups
don't fully know — here's the part we do know" in calibrated words.
- the-answer-is-contested → lead with "different people have
different answers — here's what most agree on".
- the-answer-depends-on-belief → lead with "different families
believe different things; here's what's known about the world, and
your mum or dad will tell you what your family believes".
- the-answer-is-not-known-by-anyone → lead with "this is one of the
questions grown-ups have asked forever and still don't fully know".
- the-answer-is-developmental-and-personal → lead with "this is one
to ask your mum or dad — they know about your family".
Vocabulary-used check: at the end, list every word you used that is
NOT in vocabulary_known. We will diff this server-side against
calibration and may regenerate with a stricter prompt.
Illustration brief: one or two short sentences naming the central
metaphor's image, in concrete picture-book terms (e.g. "soft picture-
book illustration: a friendly playground at dusk with one blue ball
that has bounced into every corner, no people, warm low-saturation
palette"). NEVER include text, brand marks, or any depiction of a
real child.
Output the Answer JSON matching the provided schema. Then begin
streaming the answer_text aloud through Live API. No commentary.
```
---
### Call: Fresh-fact answer (grounded)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding
```
You answer a small child's question that requires fresh information
(today's weather, a current event, a sports result, a recent rocket
launch). You ground via google_search and answer at the child's
calibration.
Hard rules:
- Apply the same calibration rules as the main answer call:
vocabulary, sentence length, metaphor, language, pronoun, voice.
- Pick the smallest amount of fresh information that answers the
question. Do NOT pack the answer with current news.
- Cite via grounding. Do NOT ask the model to embed URLs in the JSON
body — read them server-side from
groundingMetadata.groundingChunks[].web.uri.
- If grounded results are sparse or contradictory, set honesty_flag
to the-answer-is-uncertain and lead with the honest sentence.
Output the Answer JSON as a JSON object inside the text body (NOT
via responseSchema — responseSchema and google_search cannot be
combined in the same Gemini call today). Server-side: parse the
JSON, then attach citation URIs from groundingMetadata.
No commentary outside the JSON.
```
---
### Call: Generate three "next deeper why?" follow-ups
Model: `gemini-3.5-flash` · thinkingLevel: low (medium for hard topics) · Tools: (none)
```
You receive the Answer JSON that just played for the child. Your
job: propose exactly three follow-up "but why?" questions, phrased
in the child's own voice (short, direct, calibrated vocabulary),
that would be the most natural next steps from the answer just heard.
Hard rules:
- The three follow-ups must differ in predicted_topic and in
predicted_difficulty_for_child. One should be a natural-next-step
(the obvious next thread); one should be a small-stretch (a side
thread the child wouldn't necessarily think of); one should be a
real-stretch (a thread that opens onto a much bigger idea, but is
still within reach).
- Each text_in_child_voice begins with "but why" or the calibration's
preferred-language equivalent ("ngunit bakit", "lekin kyun", "mas
por que"). Keep them short — child sentence length, not adult.
- Each text_in_parent_voice_for_dashboard is the same question
rephrased for the parent's understanding ("child is asking about
the wave nature of light"). One sentence, parent-vocabulary.
- predicted_honesty_flag uses the same enum as the main answer; if
the next-deeper-why is itself uncertain or contested, mark it.
- Do NOT propose follow-ups on topics governed by an answer-me-first
or not-now policy — substitute a neutral side-thread instead.
- Always offer the "ask a new question" button alongside the three;
also_offer_new_question_button is always true.
Output the FollowupSet JSON matching the provided schema. Length
of options[] must be exactly 3. No commentary outside the JSON.
```
---
### Call: Render an answer's voice in a chosen catalogue voice
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, unhurried, slightly low-pitched, with breath at commas,
like an aunt telling a short story at the kitchen table. Pick the
Gemini 2.5 Flash TTS voice whose `languageCode` matches the answer
language — pronunciation will follow that locale automatically. The
parent has chosen voice_id from our calibrated catalogue; pass it
through.
Pre-process the text before sending it to TTS:
- Read from answer_text.
- At every comma, leave the text as-is — the voice will pause
naturally. At paragraph breaks, insert a blank line plus an
em-dash (`—`). Gemini 2.5 TTS does not support SSML ``
— these textual cues are how you signal pace.
- For code_switch_inserts, keep the whole answer in one voice and
render the inserted phrase as written; the descendant on-screen
caption shows the script switch in italics. Mid-call voice
switching is not supported.
- Target rate: ~95 words per minute for bedtime-hush; ~130 for
morning-brisk. Most calls land at ~110.
Style direction: prepend ONE short directive sentence to the text
input, exactly like: "Read this aloud to a small child, warmly and
unhurriedly, with breath at the commas. …". 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 (Polish ł, German ü, Tagalog ng, Urdu retroflex
consonants) are NOT exposed by Gemini 2.5 TTS — no SSML ``
tag. Pronunciation comes from the chosen voice's native locale.
```
---
### Call: Translate a saved answer into another household language
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You translate a previously generated Answer from its source language
into another household language. The child is the audience. The
calibration applies in the target language too.
Hard rules:
- Keep the same central_metaphor where it lands naturally. If the
metaphor does NOT land in the target language (cultural mismatch,
no equivalent picture-book frame), pick a closely related one and
note the swap in translation_voice_notes (an array of one-sentence
notes).
- Preserve the same honesty_flag and the honest_uncertainty_lead_
sentence. The lead sentence is translated, not paraphrased away.
- Preserve sentence-length ceiling (max_sentence_word_count) in the
target language. Some languages naturally use longer phrases — if
so, split.
- Do NOT translate proper nouns (the child's name, family names,
brand names, dish names). Keep them verbatim.
- Do NOT change the answer's substance. This is not a re-answer.
Output the Answer JSON with answer_language switched, answer_text
translated, central_metaphor unchanged or noted, code_switch_inserts
re-resolved against the target language. No commentary outside the
JSON.
```
---
### Call: Generate one picture-book illustration per answer
Model: `gemini-3.1-flash-image` · n/a · n/a
```
Render ONE soft picture-book illustration based on illustration_brief.
Style:
- Warm, low-saturation, hand-drawn picture-book aesthetic.
- One central scene, one central metaphor. NEVER a collage.
- No text in the image. No brand marks. No real-child likenesses.
- Composition leaves space for warmth; don't fill the frame.
- Light, soft shadows. No hard outlines.
- The metaphor is the subject — "the sky as a playground", "the eye
as a window with curtains", "the heart as a small pump in the
chest". Render the metaphor concretely; the child will see it.
The prompt for Nano Banana 2 is illustration_brief verbatim, with
the style header prepended.
Return the generated image. No commentary.
```
---
### Call: End-of-day "what we wondered" book layout
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none, long context over the day)
```
You receive every QuestionLogEntry from the child's day (typically
8-25 entries). Produce a DailyBookPage that frames the day for the
parent to read aloud.
Hard rules:
- page_title_for_book is one short sentence in the calibrated voice,
with the child's name, e.g. "The day Mateo wondered about the sky"
or "Helena's borboleta afternoon".
- page_intro_one_sentence is the single sentence the parent reads
to open the page. Calibrated to the child's vocabulary.
- questions_in_order lists the day's verbatim questions, in the
order asked. Do not paraphrase. Do not abridge.
- hero_illustration_uri is selected from the day's generated
illustrations — pick the one whose central_metaphor best captures
the day's emotional through-line (a day of bug questions → bug
illustration; a day of grief questions → the soft pump-in-the-
chest illustration).
- Do NOT include questions where policy_decision was "not-now" or
pending "answer-me-first". They belong to the parent's dashboard,
not the child's book.
Output the DailyBookPage JSON matching the schema. No commentary.
```
## 5. Use cases & content to include
Build dedicated UI sections or flows for each of these — they tell you what content the app must support.
- **The school-run morning.** A Filipino-American mum in Daly City is driving her four-year-old, Mateo, to preschool. From the back seat: *"Mama, why is the sky blue?"* She taps the big mic button on her CarPlay screen, hands the question to the app, and three seconds later the calm voice begins. Mateo listens. When the answer ends, the dashboard shows three follow-up buttons. Mateo asks for the second one. By the time they pull up at the gate, he has learned two things and forgotten about the rice cracker he was negotiating for.
- **The dinner-table grief question.** A Pakistani-Canadian dad in Mississauga is plating biryani when his five-year-old, Zara, asks why her dadi (paternal grandmother) doesn't pick her up at the door anymore. The dad has the policy set to *answer-me-first* on death. The app pauses, sends him a soft lock-screen card: *"Zara just asked about Dadi. We've drafted a calm, age-five answer; tap to review before it plays."* He reads it standing in the kitchen, accepts it with one small edit ("she loved you so much"), the answer plays. Then he sits down with her and the app says nothing more.
- **The kitchen-table butterfly afternoon.** A Brazilian mum in São Paulo, Helena's mother, is stirring feijão. Helena, six, asks: *"mas por que as borboletas têm pó nas asas?"* The answer plays in calm Brazilian Portuguese — the butterfly's wings as roof tiles that have to stay light so the butterfly can fly — with one English word ("scale") gently glossed back to Portuguese ("escama"). A picture-book illustration of a butterfly with translucent tile-wings fades in. Helena taps the *but why don't they fall off?* button.
- **The hospital waiting room.** A parent has been waiting four hours for the orthopaedic team to set their seven-year-old's wrist. The child has run out of questions about the cast and started in on *why does the moon follow our car?*. The parent passes the phone over. The child asks. The app, in *midday-calm* mode, takes them through the moon-following illusion in three follow-up taps. Twenty minutes of waiting room dissolved. Parent texts their sister: *"this thing has earned its keep tonight."*
- **The forty-questions car ride.** An autistic six-year-old whose special interest is trains is in the back of a car for two hours. The app's calibration knows: vocabulary_known includes *carriage, locomotive, signal box, points, gauge*. He asks one train question. Then a follow-up. Then a follow-up. Forty questions over two hours, never a hand-wave answer, never a "we'll look it up later". His mother, in the front seat, listens and learns enough about narrow-gauge railways to keep up with him at dinner.
- **The honest-uncertainty moment.** A five-year-old asks *"why do we dream?"*. The model is genuinely uncertain — neuroscience does not have a definitive answer. The app's answer begins, calibrated, with: *"The honest answer is grown-ups don't fully know why we dream — but here's the part we do know."* Then it explains what is known about sleep, the brain doing housework at night, and dreams as the kind of stories the brain tells while it tidies up. The child taps *"but why does my brain do housework?"*.
- **The two-language household.** A Vietnamese-American household in San José: the grandparents speak Vietnamese, the parents are bilingual, the kids' first language is English but they understand Vietnamese. The calibration: `household_languages: [en, vi]`, `preferred_answer_language: en`, `allow_code_switching: true`. The four-year-old, Linh, asks a question in English. The answer plays in English with one or two soft Vietnamese connectors and the technical noun glossed both ways. The grandmother, listening from the next room, smiles.
- **The "ask Grandma" answer.** A child asks *"why do we go to mosque on Fridays?"*. The policy is `the-answer-depends-on-belief`. The app says, in calibrated voice: *"different families believe different things — and your family has its own beautiful story. The part we know about how the world works is small; the part your mum and dad and Daadi can tell you is much bigger. Let's go ask them."* The child finds the parent. The parent answers. The app has done its job.
- **The big-feelings question.** A child whose parents are divorcing asks *"why doesn't Papa live here anymore?"*. The policy is `answer-me-first` on divorce. The app does not answer. It surfaces to the primary parent: *"Lina just asked why Papa doesn't live here anymore. Here are three age-five framings we've drafted; pick one, edit, or write your own. We can voice it for you, or you can talk to her yourself."*
- **The "Grandma is dying" calibration.** A family whose grandmother is in hospice has set a policy with parent-supplied context: *"Grandma is dying; Mateo knows she is very sick; we want the app to be gentle and to nudge him toward us for big questions."* For two months any death-adjacent question triggers `answer-me-first` and a gentle nudge: *"that's one to ask Mama or Papa — let's go find them."*
- **The end-of-day book.** At 7 pm the day's eighteen questions become a single picture-book page: *"The day Helena wondered about her butterfly garden."* The mother reads it to Helena before bed. Helena recognises every illustration. She insists on one extra question. The app refuses, gently, in her voice: *"that's one for tomorrow, querida — let's save it for first thing."* The mother is grateful for the assist.
- **The Friday family-quote moment.** Mateo, age four, has asked *"but why does the moon follow our car?"*. The app saves this verbatim into the family-quote book — date, time, who was driving, the answer that followed. At the end of the year the book is printed. His grandparents in Manila read it and weep a little.
## 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 parent's hand on a phone in a stroller, late afternoon, a child's shadow blurring into the frame. One paragraph: "Why Machine answers your kid's 'but why?' questions calmly, in the words they actually know — and offers the next deeper why, so curiosity becomes a thread to pull, not a question to repeat." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample family" → loads the demo family in section 8a.
2. **Calibration / "Who is this for?"** First-run flow. Three short panes: (a) child name, age, pronoun, language(s); (b) what words they already know (suggested chips from age band + free-text add); (c) what topics need parent care (death, illness, divorce, sex, the news, etc. — set a policy per topic). Skip-able with sensible defaults; editable any time.
3. **Home screen / Big-mic.** Centred: a large round push-to-talk button. Above it: the child's name and a single sentence ("Ask away, Mateo"). Below it: "Ask a new question" / "Today's questions" / "Settings". The screen is uncluttered — high-contrast, large tap targets, nothing else to tap.
4. **Live answer screen.** When the child speaks, the mic ripples gently. As the answer begins to play, the screen shows a soft animated waveform and, three seconds in, the picture-book illustration fades in. The transcript scrolls below the illustration in calibrated typography. At the end of the answer, three follow-up *"but why?"* buttons rise into place, plus an *"ask a new question"* button.
5. **Today's questions.** A vertical list of the day's questions, in order asked. Tap any one to replay the answer, see its illustration, or re-thread from there. Filter chips: "questions Mateo asked", "questions I asked the app", "questions sent to me for review".
6. **Parent dashboard.** A separate space — distinct visual mode, "calm but adult" — where the parent can: review queued *answer-me-first* questions; tune the calibration; tune policies; see the day's digest; check which words landed and which didn't.
7. **Books.** The "what we wondered about today" archive. Calendar view, one card per day. Tap a card → printable picture-book page. Buttons: "Read tonight", "Print this page", "Share to a co-parent".
8. **Voices.** Pick from a curated catalogue of calm voices in the household's languages. Pre-listen each one with the same sample sentence: *"Hello — let's wonder about something together."*
9. **Hard-topics policy.** A dedicated screen, parent-only, with each sensitive topic and a clear three-way toggle: *answer-freely* / *answer-gently* / *answer-me-first* / *not-now*. A free-text "context" field per topic ("Grandma is dying. The dog ran away last week."). Visual reminder: "you can change this any time".
10. **Co-parents & grandparents.** Invite a second parent or a grandparent. Magic-link email. The invited adult sees the same calibration but only the *primary* parent edits policies and refuse-lists.
11. **Multi-child mode.** Switch between siblings; each one's calibration and archive is isolated. The switcher is one tap from the home screen.
12. **Settings.** Account, voices, time-of-day mode, network behaviour (offline grace), delete-my-child's-data (one tap, 60-second hard delete), capabilities `(i)` icon.
13. **Footer.** "Made for the questions that don't stop coming." Privacy: "Your child's questions are yours. We never train on them." 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 Why Machine."
- Subhead: "Your kid asks. We answer — in their actual words, at their pace. Then we offer the next deeper why, so curiosity becomes a thread to pull."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic chatbot: it is calibrated to *this* child's vocabulary, it never makes up science, it routes hard topics back to the parent, and it offers the *next deeper why* instead of a one-shot answer.
- Visual: a small annotated illustration of a phone screen showing a question, an answer playing, and three soft "but why?" buttons rising into view — not a generic AI sparkle icon.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample family".
- A live demo input pre-loaded with the calibration for "Mateo, age 4, English/Tagalog household, knows trains and dinosaurs". The parent taps the big mic, the demo question *"why is the sky blue?"* plays back, and the answer streams.
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the three-second time-to-first-audio, the picture-book illustration arriving mid-story, the three follow-up buttons in Mateo's calibrated voice).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample family in `/data/seed-family/` for your own calibration."
- "Adjust the prompts in `/server/prompts/` to fit your household's language(s) and hard-topic policies."
- "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 (Live API)** — the child speaks, the answer begins playing in under three seconds. Streaming voice-in, voice-out. The single most important capability in the app.
- **Gemini 3.5 Flash (calibrated reasoning)** — generates the answer at the child's exact vocabulary and sentence-length budget, picks a single central metaphor, and offers three follow-up "but why?" questions in the child's own voice.
- **Gemini 3.5 Flash (policy guard)** — sub-200ms classification of every incoming question against the parent's hard-topic policies. Cheap, fast, conservative.
- **Gemini 3.5 Flash + grounded search** — used sparingly, only for genuinely time-sensitive questions (today's weather, current events). Evergreen science is never grounded.
- **Gemini 2.5 Flash TTS** — renders the calm catalogue voice the parent has picked, in the household's language. The slow, breath-at-commas register is the one most parents tell us their kid trusts.
- **Nano Banana 2 (Gemini 3.5 Flash Image)** — generates one soft picture-book illustration per answer, of the central metaphor. Never more than one per answer.
- **Long context (Gemini 3.5 Flash)** — at the end of the day, reads every question and answer as a single context to produce the "what we wondered about today" picture-book page.
- **Firebase Auth** — Google and Apple sign-in for the parent; the child does NOT have an individual account.
- **Firestore** — stores the calibration, the policies, the daily questions and answers; syncs across the parents' devices in real time.
- **Firebase Storage** — keeps the generated illustrations and the TTS audio of saved answers.
- **Cost note** — see the detailed breakdown in 6d. A typical day of 18 questions costs about $0.40 of Gemini API spend; a full year of a curious 5-year-old is around $130.
- **Privacy note** — your child's questions are private to your family and the co-parents you've invited. 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. The child does not have their own account; everything sits under the parent's account and can be deleted in one tap.
**Backend services this app depends on:**
- Auth: see section 4b. Apple sign-in requires user-config (Apple Developer + Service ID).
- Database: see section 4b
- Storage: see section 4b. Firebase Storage must be enabled in the Firebase console before first answer is saved.
- Email: see section 4b. Magic-link email requires sender-domain authorisation in Firebase Auth.
- 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)
- `FIREBASE_STORAGE_BUCKET` — your enabled Storage bucket name
- `LIVE_API_PROJECT_ID` — your Gemini Live API project id (if separate)
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: Live API is billed per second of audio in + audio out, with text-token equivalents; a typical 90-second answer costs ~$0.03. Nano Banana 2 illustration is ~$0.03/image. The day's book layout is a single long-context call ~$0.01. Most household budgets settle around $10-15/month for a high-question child.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this child's data forever" — gone in 60 seconds), what is never sent for training, COPPA-style posture.
**Documentation links:**
- AI Studio Build docs
- Gemini API Live, multilingual, long-context, TTS, Image docs
- Firebase Auth, Firestore, Firebase Storage docs
- A short note on the hard-topic policy spec for parents migrating from another answering app
**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)
- **Policy guard (Gemini 3.5 Flash, low thinking)** — every incoming question. ~200 input tokens, ~80 output tokens. ~$0.00004/question. Negligible at the household level.
- **Live answer (Gemini 3.5 Flash, Live API, medium thinking)** — typical 90-second answer with calibration context. Billed per second of streaming audio plus token equivalents; effectively ~$0.025-0.035/answer. The bulk of the daily cost.
- **Fresh-fact answer (Gemini 3.5 Flash + grounded search)** — small minority of questions. ~$0.001/question. Rare.
- **Follow-up generator (Gemini 3.5 Flash, low thinking)** — per answer, three follow-ups. ~$0.002/answer.
- **Hard-topic follow-up generator (Gemini 3.5 Flash, medium thinking)** — rare, sensitive questions. ~$0.005/answer.
- **TTS for catalogue voice (Gemini 2.5 Flash TTS)** — when the parent has picked a catalogue voice or when we render a saved answer in another language. ~$0.000003/character; a 250-character answer ≈ $0.0008. ~$0.005 for a 250-word translated archive answer.
- **Nano Banana 2 (illustration per answer)** — ~$0.03/image. The largest per-answer line item after the Live API call.
- **End-of-day book layout (Gemini 3.5 Flash, low thinking, long-context)** — daily. ~$0.01/day for a typical 18-question day.
- **Daily digest email (optional, free)** — opted-in only.
- **Expected per-answer cost:** ~$0.06 (Live answer + image + follow-ups). **A day of 18 questions:** ~$1.10 including the end-of-day book layout. **A year for a high-question 5-year-old:** ~$130-200.
- **Image + audio storage:** Firebase Storage standard tier, ~$0.026/GB/month. A picture-book illustration is ~150 KB; a TTS clip of a 90-second answer is ~700 KB. A year of 6,000 answers ≈ ~5 GB ≈ ~$0.13/month.
## 7. Design language
- **Mood:** A kind, unhurried adult at the kitchen table with a small child. Not a tech product. Not an EdTech kiosk. The mid-afternoon kitchen at school-run time, the parent crouched at eye-level, the child holding a leaf they want to know the name of. The app is the calm voice the parent wishes their own brain still had at 5 pm.
- **Typography:** A friendly display serif for the answer transcript and chapter headings (Source Serif Pro or Crimson Pro). A clean grotesque for app chrome (Inter or Geist). A larger-than-typical body size — 18-20 px — because parents read this with their kid leaning on their shoulder, and the transcript is sometimes read aloud as a fallback.
- **Palette:** Warm milk background `#F8F2E6` for the main screen, deep ink `#1B1714` for body text, a soft kid-friendly orange `#E68A4C` for the big-mic button (warm, not aggressive), a slate blue `#3F5A78` for the parent dashboard mode (visually distinct, calmer, more adult), and a forest green `#2F6041` only for confirmation moments. Avoid bright primaries; the app is *for* kids but is *used by* parents at the end of a long day.
- **Imagery:** The generated picture-book illustrations are the visual hero. Soft, low-saturation, with space around the central subject. Never crowded. Never branded. Never a child's likeness. The app icon and wordmark sit in serif on warm milk; no character mascot.
- **Hand-feel touches:** The big-mic button has the slight unevenness of a wax stamp — circular but with the warmth of having been pressed by hand. The picture-book illustration fades in with a slow opacity ramp, not a bouncy reveal. The three follow-up buttons rise into place with a gentle 200ms stagger, respecting `prefers-reduced-motion`.
- **Spacing:** consistent 4-px base. Generous whitespace — the app's job is to slow the parent's breathing.
- **Radius:** consistent token set (e.g. 8 / 16 / 28 px). The big-mic uses 28; cards use 16; small chips use 8.
- **Shadows:** subtle, warm-tinted. Avoid hard drop-shadows; favour soft ambient.
- **Motion:** purposeful — the mic ripple, the illustration fade-in, the follow-up rise. Respect `prefers-reduced-motion`. No bouncing splash animations. No sing-song hero animations. The waveform during the answer is the one ambient motion, and it stills the moment the answer ends.
- **States:** every interactive element has hover, focus, active, and disabled. Loading uses skeletons not spinners where possible. The mic button has three states: idle, listening, processing. The processing state is honest — "thinking…" with a real five-word status ("calibrating to Mateo's words").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible names, dates, calibrations, sample questions, sample answers that fit the domain (use the seed content in section 8a as a starting point). When inventing, span Asian, African, Latin American, and European households equally — Filipino-American, Pakistani-Canadian, Brazilian, Nigerian, Vietnamese-American, Korean, Salvadoran, Greek, French. Never centre Europe by default. Never invent a real child.
- Tone: warm, direct, free of corporate language. This template is for a person — specifically a tired parent at the end of a long day — not a company.
- Headlines: punchy and concrete. No 'Empower your child's curiosity'. No 'Revolutionize learning'. No 'Seamless'. No 'AI-powered'.
- Body copy: short paragraphs (2-4 sentences). Use lists where appropriate.
- Plain language. Avoid jargon — except where the parent is already a domain user (the speech therapist parent wants to see "phoneme" in Settings; the doctor parent wants to see "SPIKES-style framing" on the medical-news policy).
- Where the app outputs AI-generated content, never label it as "AI says" — let it speak naturally. Use small uncertainty cues only where epistemic honesty requires them. The honest-uncertainty lead sentence is itself the cue — visible, intentional, on the page.
## 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 families (sidebar):**
- "Mateo, 4" — Filipino-American household in Daly City. Languages: English (primary), Tagalog. Code-switching allowed. Special interests: trains, dinosaurs, the moon. Hard-topic policies: death → answer-me-first (great-grandma in hospice, Manila); the news → answer-gently.
- "Zara, 5" — Pakistani-Canadian household in Mississauga. Languages: English (primary), Urdu. Code-switching allowed. Special interests: animals, kitchens, how things work. Hard-topic policies: death → answer-me-first (paternal grandmother died last year); religion → answer-gently with family-context.
- "Helena, 6" — Brazilian household in São Paulo. Languages: Brazilian Portuguese (primary), English. Code-switching allowed for technical nouns. Special interests: butterflies, plants, the sea. Hard-topic policies: all defaults except divorce → answer-gently (parents recently separated, amicably).
- "Linh, 4" — Vietnamese-American household in San José. Languages: English (primary), Vietnamese. Grandparents speak only Vietnamese; calibration allows soft Vietnamese connectors. Hard-topic policies: standard defaults.
**Sample answer in the live answer screen (this is what the demo should show):**
- **Calibration:** Mateo, 4 years, pronoun "he", `household_languages: ["en-US", "tl-PH"]`, `preferred_answer_language: "en-US"`, `allow_code_switching: true`, `vocabulary_known: ["sky", "blue", "light", "playground", "ball", "bounce", "colour", "sun", "air"]`, `vocabulary_unknown: ["wavelength", "scattering", "molecule", "atom", "photon", "electromagnetic"]`, `metaphor_anchors: ["trains", "playground", "dinosaurs", "the moon"]`, `max_sentence_word_count: 12`, `max_answer_seconds: 90`, `voice_id: "aunt-warm-en-US-tagalog-lilt-01"`, `time_of_day_mode: "morning-brisk"`.
- **Question verbatim:** "Mama, why is the sky blue?"
- **Question language:** "en-US"
- **Answer language:** "en-US"
- **Honesty flag:** `well-established-science`
- **Honest-uncertainty lead sentence:** null
- **Central metaphor:** "the sky as a playground; blue as the ball that bounces the most"
- **Answer text (calibrated, ≤12 words per sentence, ≤90 seconds total):**
> "So — sunlight looks white, but it's really lots of tiny colours all mixed together. When sunlight goes through the air above us, the air bumps the blue colour around the most, like a little kid kicking a ball into every corner of the playground. So when you look up, blue is the colour that has bounced into your eyes from everywhere. That's why the sky looks blue. At sunset, the sun's light has to go through more air to reach you. By then, most of the blue has already bounced away, and the orange and pink colours are the ones left for your eyes to see."
- **Vocabulary used outside known:** [] (all words were in known list)
- **Illustration brief:** "soft picture-book illustration: a friendly playground at dusk with one bright blue ball that has bounced into every corner — swings, slide, sandpit — no children in the frame, warm low-saturation palette, hand-drawn"
- **Code-switch inserts:** [] (none on this answer; the parent has allowed code-switching but the model did not need it)
- **Source grounding URIs:** [] (evergreen physics, no grounding)
- **Three follow-up "but why?" options:**
- `text_in_child_voice: "but why is light made of colours?"` · `predicted_difficulty: natural-next-step` · `predicted_topic: "wave nature of light"` · `predicted_honesty_flag: well-established-science`
- `text_in_child_voice: "but why do my eyes see blue?"` · `predicted_difficulty: small-stretch` · `predicted_topic: "human vision"` · `predicted_honesty_flag: well-established-science`
- `text_in_child_voice: "but why is the sky black at night?"` · `predicted_difficulty: real-stretch` · `predicted_topic: "scattering vs no sunlight; Olbers' paradox lite"` · `predicted_honesty_flag: well-established-science`
**Sample honest-uncertainty answer (also include in the demo set):**
- **Question verbatim:** "Why do we dream?" (asked by Helena, age 6, in Portuguese: "Por que a gente sonha?")
- **Honesty flag:** `the-answer-is-uncertain`
- **Honest-uncertainty lead sentence (Portuguese):** "A resposta honesta é que os adultos não sabem totalmente por que a gente sonha — mas aqui está a parte que a gente sabe."
- **Answer text excerpt (Portuguese, calibrated to Helena's vocabulary):**
> "À noite, quando você dorme, o seu cérebro continua trabalhando um pouquinho — como quem arruma os brinquedos antes de dormir. Ele guarda as coisas que você viu no dia, e às vezes ele monta histórias com esses pedaços. Essas histórias são os sonhos. Por que ele monta histórias? Os adultos ainda estão descobrindo. Mas o cérebro adora histórias — assim como você adora as suas borboletas."
- **Central metaphor:** "the brain as a child tidying away the day's toys and making up stories with the pieces"
- **Three follow-up options:**
- `text_in_child_voice: "mas por que o cérebro inventa histórias?"` · `natural-next-step`
- `text_in_child_voice: "mas por que a gente esquece os sonhos?"` · `small-stretch`
- `text_in_child_voice: "mas por que a gente precisa dormir?"` · `real-stretch`
**Sample hard-topic answer-me-first queue entry (parent dashboard):**
- **Asked at:** 2026-05-12 18:43 PDT
- **Asker:** Zara, 5
- **Question verbatim (Urdu):** "Daadi ab kyun nahi aati door par?"
- **Question verbatim (English gloss for the parent):** "Why doesn't Daadi come to the door anymore?"
- **Matched policy topic:** "death"
- **Decision:** "answer-me-first"
- **Decision reason for parent:** "Zara is asking about Daadi, who passed away last year. We've drafted a calibrated answer; please review."
- **Drafted answer (Urdu, calibrated):** "Daadi tumhare bohot pyaari thi, aur woh tumhe bohot pyaar karti thi. Woh ab is duniya mein nahi hain — par tumhare ammi-abu tumhe bata sakte hain ke woh kahan hain tumhare dil mein."
- **Parent action buttons:** "Accept and play to Zara" / "Edit and play" / "I'll talk to her myself" / "Save for later".
**Sample hard-topic refuse / "ask Mama" answer:**
- **Question verbatim:** "Why do we go to mosque on Fridays?" (asked by Zara, age 5)
- **Matched policy topic:** "religion"
- **Decision:** "not-now" with redirect
- **Answer text (calibrated):** "Different families believe different things, Zara — and your family has its own beautiful story. That's a question to ask your ammi and abu. Let's go find them."
**Sample sample voice copy:**
- Onboarding: "Tap the big mic and ask any 'but why?' question. The first time, we'll tune the answer to your kid."
- Processing: "Thinking like a four-year-old…" / "Picking a metaphor…" / "Drawing the playground…"
- Empty home: "Mateo's first question of the day — tap the mic to start."
- Error (couldn't hear): "We couldn't quite catch that one. Want to try again, a little closer to the phone?"
- Save confirmation: "Added to today's book — Mateo wondered why the sky is blue."
- Hard-topic queued: "Zara asked something tender. We've sent a draft to your phone for review."
- Sleep mode: "That's a great one to save for tomorrow, Mateo. We'll ask it first thing."
**Sample co-parent invitation email subject + body:**
- Subject: "Hey Sara — I'm using this thing to answer Zara's 'why' questions. Want in?"
- Body: "I keep handing Zara my phone when I'm out of answers. This app calibrates to her vocabulary and routes the big questions back to us. Tap to join the family archive — it'll show you what she asked today." [Join the family]
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a parent's hand holding a phone in a stroller's cup-holder, late afternoon, a child's small shoulder blurring at the edge of frame, soft golden hour. Generate via Nano Banana 2 with a prompt emphasising "parent hand, phone screen lit warmly, stroller frame, child shoulder out-of-focus, school-run hour, no faces visible".
- **App icon / wordmark:** Set in the friendly display serif on warm milk background. No character mascot, no sparkle, no AI iconography.
- **Empty-state illustration:** A simple line drawing of a child's hand on a parent's arm, asking. Hand-drawn aesthetic, not a flat icon.
- **Demo answer illustrations:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "soft picture-book illustration, low-saturation, hand-drawn, no text, no real children, one central metaphor scene". Each demo answer should look hand-drawn, not rendered.
- **Voice catalogue:** Pre-recorded sample sentence ("Hello — let's wonder about something together") rendered via Gemini 2.5 TTS in each catalogue voice, cached at build time. Catalogue spans languages explicitly: English (US, UK, Indian, Filipino-accented), Tagalog, Vietnamese, Mandarin, Cantonese, Korean, Japanese, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer, French, Brazilian Portuguese, European Portuguese, Spanish (Latin American + Iberian), German, Italian, Arabic.
- **Stock fallbacks:** If image generation fails, fall back to a curated illustration from `/public/samples/sample-illustration.jpg`. Never to a "🤔" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock photography. Prompt for warmth, asymmetry, and a touch of hand-drawn 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. The mic button is custom, not a Lucide icon.
### 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: "Ask the first question", "Set Mateo's calibration", "Invite a co-parent" — never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- All AI-generated content streams in token-by-token where supported, with a clear "thinking…" indicator before content starts arriving. The "thinking…" status uses a five-word honest line ("calibrating to Mateo's vocabulary"), not a spinner.
- If an AI call fails, show a calm, specific error ("That one stalled out — want to try once more, or save it for later?") and offer retry. NEVER expose a stack trace to a parent at school-run hour.
- Hard-topic queued questions show a clear "waiting for you" badge and a soft notification on the parent's lock screen.
- The picture-book illustration always fades in at ~3 seconds into the answer, not before — even if Nano Banana 2 returned earlier. Server-side delays the broadcast.
- The follow-up buttons rise into place after the audio ends with a 200ms stagger that respects `prefers-reduced-motion`.
- The map dot for time-of-day mode (morning / midday / dinner / bedtime) updates automatically by local time, with a manual override.
## 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 Live API session, the follow-up generator, the translation, and the daily book layout. Pin `gemini-3.5-flash` for the policy guard and the grounded fresh-fact call. Pin `gemini-3.1-flash-tts-preview` for the catalogue voice rendering. Pin `gemini-3.1-flash-image` for the illustration. Set `thinkingLevel` explicitly per call.
- **Live API plumbing:** the client opens a WebSocket session to a Cloud Run server function that brokers the Gemini Live API session. The server interpolates the calibration into the systemInstruction at session start. Voice frames stream in real time; the policy guard runs as a parallel pre-flight Gemini 3.5 Flash call within the same WebSocket so the first audio out is gated until the guard decision arrives (typically <200ms after the question end).
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed family on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it (user-configured); magic-link email as fallback for co-parent invitations (user-configured: requires sender-domain authorisation).
- **Storage:** Firebase Storage for illustrations and TTS audio. Must be enabled in the Firebase console before first save. Pre-signed URLs only.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+.
- **CarPlay & Android Auto consideration:** the home screen and the mic must be operable from a car-mounted phone — large tap targets (≥ 60 × 60 px on the mic), no fine text, audio-first.
- 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 today-questions view (parent and co-parent stay in sync).
- Optimistic UI on writes; reconcile on response.
- Audio is the primary surface; the app must work fully when the screen is dark and the phone is in the parent's pocket connected to a Bluetooth speaker or AirPods.
- **iOS Safari gotchas (graceful degradation):** the Live API session must survive call/Siri/alarm interruption — listen for `MediaStreamTrack.onmute` and pause/resume; Safari backgrounded tabs throttle WebSocket and kill `getUserMedia` — pair `visibilitychange` with a screen Wake Lock when an answer is playing so a child's question is never cut off because the parent locked the phone; mic permission does NOT persist across reloads on iOS — re-request on every session start; PCM streaming via `AudioWorklet` (Safari `MediaRecorder` is AAC-only). For pocket-with-AirPods playback, request `audio` media-session control intents so a single AirPod tap can pause/resume the answer.
## 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 generated illustrations have `alt` describing the central metaphor ("soft picture-book illustration of a playground at dusk with one blue ball bouncing into every corner").
- Form fields have associated `