================ 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. --- # I Saw a Bug ## 1. Project **I Saw a Bug** is a field-guide-in-your-pocket for the parent who lives with a kid who wants to know the name of every insect they meet. The parent points the phone at the thing — a beetle on the path, a moth on the kitchen window, a bee on a hibiscus, a velvet ant in the dirt — and the app gives the kid the name, the habitat, one fact small enough to tell a friend at school tomorrow, and a tick on the running lifetime list the kid can show their grandparents at the weekend. It is built around eight-second attention spans and twelve-second answers, because the bug crawls away. This is the kind of app a tired mother in Sacramento builds for her seven-year-old after the third Saturday morning of "Mama, what kind of bug?" — Riza is Filipino-American, her daughter Mayari is in second grade, and the kalachuchi bush by the driveway brings something new every weekend. It is also the kind of app a father in Nairobi opens at the side of a footpath after his son Kibet, six and a half, picks up a matabele ant and gets stung — and the kind of app a grandmother in São Paulo passes back and forth with her grandson Théo at the edge of the quintal, where the cigarra shed its skin on the bark of the jabuticaba. Same shape of moment, different garden, different language, different risk profile. The single demo that proves the magic: phone aimed at a small brown beetle on a paving stone → in under eight seconds the screen reads "Ten- lined June beetle (Polyphylla decemlineata) · lives in soft soil and eats roots underground, mostly comes out at night · tell your friend: the boy beetles make a hissing sound when you pick them up, like a tiny bagpipe." A green tick appears on Mayari's lifetime list, taking her total from 47 to 48. She runs back to the screen door to tell her abuela, who is on FaceTime from Cebu. And in the harder cases — stings, bites, allergens, venomous lookalikes — the app is the careful adult in the conversation. A photograph of a yellowjacket on a picnic table never reads "harmless"; it reads "yellowjacket (Vespula species) · this one can sting, and the sting can hurt for a few hours. Some kids and grown-ups are very allergic. Step back slowly, do not swat" — and the parent gets a separate, plain- English caregiver note underneath the kid-facing copy. A black widow under a porch step is flagged in red before the species name, with the adult instruction to put the phone down, take the child inside, and call a pest professional. **Tagline:** _Name every insect your kid finds — in any garden, any language, with the safety call made carefully every time._ ## 2. Target audience - Parents of curious 4-10 year olds who collect insect sightings the way other kids collect Pokémon cards - Grandparents who do FaceTime walks in the garden with grandkids three cities, or three continents, away - Homeschooling families who use the daily walk as the science lesson and want a real running record of what was found - Nature-preschool teachers in Sacramento, Nairobi, São Paulo, Berlin, Bangalore, Manila who want a tool that names what the kids point at and never says "harmless" about something that stings - Speech-and-language therapists and occupational therapists who use outdoor naming walks with autistic or selectively-mute kids — the kid shows the phone the bug, the app speaks the name aloud, the kid repeats - Foster parents and respite carers building a shared-with-the-bio- family field journal during a long weekend together - Park rangers and nature-reserve docents running family programmes who want every kid's "I found one" to count - Diaspora families whose grandparents recognise the bug by its Tagalog, Swahili, Tamil, or Bahasa name, not its English one, and who want both names side by side - Kids themselves, age 6+ who have their own phones or a hand-me-down device, in a mode that's safe to give them: no chat, no open web, no DMs ## 3. Core value propositions Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app. - **Names the bug in under eight seconds, because the bug crawls away.** Gemini 3.5 Flash multimodal does identification + habitat + one age-appropriate fact + safety classification in one round-trip. No carousel of "could be one of these twelve". A confident answer, with a quiet honesty cue when confidence is low. - **Safety is never a soft footnote.** Every identification carries a safety category — `harmless`, `can_sting`, `can_bite`, `venomous`, `allergen_concern`, `disease_vector`, `unknown_proceed_with_caution`. Stings, bites, and allergens are flagged in the answer itself, not hidden behind a (i). The model is forbidden from calling a wasp, hornet, fire ant, black widow, brown recluse, kissing bug, tsetse, Anopheles mosquito, paper wasp, or velvet ant "harmless". - **Two voices in one answer.** A kid-facing sentence written for a 6-9 year old, and a separate caregiver sentence with the boring grown-up detail (the local hospital phrase to search for in case of a sting reaction, the right distance to back away from a hornet's nest, the honest note that this looks venomous and the user should not handle it). - **The lifetime list the kid actually shows people.** Every find is a tick. The list is a beautiful, scrollable, illustrated index card collection the kid swipes through with their abuela on FaceTime. Each card has the date found, the place found, the kid's own one-line caption ("I found this in our driveway"), and a tap-to-hear-it-again recording of the parent or grandparent reading the name aloud. - **Names the bug in your family's language, not only in English.** Tagalog, Cebuano, Swahili, Kikuyu, Brazilian Portuguese, Spanish (Mexican, Cuban, Argentine), Vietnamese, Cantonese, Mandarin, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Farsi, Khmer, Bahasa Indonesia, Yoruba, Igbo, Arabic. The common name in your language and in English, with the Latin binomial as a quiet anchor underneath. - **The fact is one fact, picked to be told.** The default is the smallest true thing the kid will repeat in the playground tomorrow. Not a Wikipedia paragraph. Not a list of three. One sentence, picture-shaped. - **Reads the answer aloud in the kid's language, at kid pace.** Gemini TTS in Tagalog, Swahili, Portuguese, English, Mandarin, Tamil — a little slower than adult news pace, so the kid can repeat after the voice. The grown-up sentence is read in a slightly more grown-up voice; the kid sentence in a warmer, slightly bouncier one. - **The list is portable and printable.** Export a four-page printable Insect Notebook for the bedroom wall: cover, lifetime list, ten hand-picked favourites with kid captions, parents' signature line. The grandparents get a copy. - **Refuses to play guess.** When the model can't tell from the photo (too small, too blurred, half-hidden under a leaf), the answer is "we couldn't be sure, but it looks like it could be a [family] — can you take another photo from above?" The model never invents a species the kid will be wrong about at school. ## 4. Features to build - Camera capture optimised for moving subjects — fixed-focus tap, burst capture (3 frames in 600ms, the model picks the best), a small "the bug is gone, use my last frame" recovery button - Upload from the photo library for finds at a friend's house that weren't named in the moment - Single-image identification call to Gemini 3.5 Flash returning the structured `BugFind` schema below - Kid voice / Grown-up voice split in every answer - Safety category surfaced as a coloured chip at the very top of the answer card — green, yellow, orange, red — with the verbal safety copy beside it (chips never communicate state by colour alone) - "Bug is gone" recovery — if the kid lost it before the photo captured, the parent can choose a colour, size, and habitat tag and get a "it was probably one of these three" gentle disambiguation card, marked clearly as a guess - Lifetime list with running total, sortable by date / place / kind - Per-find detail card with: photograph, name in family language, scientific name, habitat, the one fact, the safety chip, kid's caption (typed or voice), date, place, parent / grandparent voice recording, "show me another one like this in our area" button - "Show me one of these on the map" — the find marker on a small local map, with a privacy-respecting precision toggle (street / block / neighbourhood / town only) - Family sharing — invite Mama, Papa, Abuela, Lola, Babu, Avó to the same lifetime list. Each contributor has their own avatar so the kid sees who added which find. - FaceTime / video-call companion mode — a "show your list to a grandparent" mode that flips into a screen-share-friendly layout, with bigger text and a "next card" button under the parent's thumb - Printable Insect Notebook — four-page PDF, child's name on the cover, ten hand-picked favourites inside, lifetime list on the back - Multilingual TTS for both kid and grown-up sentences, in the family's language and in English - Safety-explain panel — every red / orange flag has a one-tap "what to do right now" panel for the grown-up (back away, do not handle, wash the area, call your local poison control) - "We don't know what this is" UI — explicit, warm, never humiliating, with a suggestion to try another angle or a daylight photo - Allergen flag layer — household-level allergen profile (bee, wasp, ant, mosquito) raises the urgency of relevant matches - Offline-friendly capture — the photo is taken offline, the identification call queues, and the kid sees a "we'll name this one when we're home" card with the photo already in the lifetime list - "Read aloud again" button on every card, for the kid who wants to hear the name three more times before bed - Caregiver-readable settings — age of child, family language, English level, allergen profile, location precision; all on one screen, in one column, no buried submenus ## 4b. Required Gemini capabilities + backend services **This template's intelligence comes from the Gemini capabilities below. Wire them up explicitly — don't substitute generic LLM calls.** ### Gemini capabilities (the load-bearing intelligence) - **Multimodal image input** (Gemini 3.5 Flash) — identifies the insect (or arachnid, or millipede, or other small arthropod the kid found and called a "bug") from a single photo or a 3-frame burst. Reads the visual cues that disambiguate similar species: yellowjacket vs. honeybee waist, monarch vs. viceroy hindwing stripe, black widow vs. false widow leg pattern. One call per find. - **Structured output / JSON Schema** — the response matches the `BugFind` schema below. Every field is typed; the schema is included verbatim in the system instruction and as `responseSchema`. The schema is converted from Zod to Gemini's `Schema` type via the SDK helper before being passed in. - **Multilingual generation** (built into Gemini 3.5 Flash) — produces the kid-facing sentence and the grown-up sentence in the family language AND in English, both, in a single call. Tagalog, Cebuano, Swahili, Kikuyu, Brazilian Portuguese, Spanish, Vietnamese, Cantonese, Mandarin, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Farsi, Khmer, Bahasa Indonesia, Yoruba, Igbo, Arabic. The English field is always populated so the lifetime list stays legible to a visiting cousin. - **Search grounding** — for the regional-context call. "Is this ten-lined June beetle present in Sacramento in late spring?" must resolve to a real range answer, not a guess. Grounded search backs the `regional_context` and `seasonality_note` fields. Per the AIS rules, `responseSchema` and `google_search` cannot be combined in one call — so this call returns JSON in the text body and citations are read server-side from `response.groundingMetadata.groundingChunks [].web.uri`. - **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — reads the kid sentence in the kid voice and the grown-up sentence in the grown-up voice. Two separate calls per find. Voice locale follows the `languageCode` of the answer; Gemini 2.5 TTS does not support mid-call voice switching, SSML ``, or SSML `` — pauses are encoded as `…` and blank-line `—`, and the style is prepended as a one-sentence directive inside the text input. - **Nano Banana 2** (`gemini-3.1-flash-image`) — used sparingly, only for the printable Insect Notebook cover illustration (the child's name in hand-drawn letters on a botanical background featuring three of the kid's favourite finds) and for the "bug is gone, what might it have been?" disambiguation card sketches. NEVER used to fabricate a realistic-looking photograph of an insect that pretends to be the one the kid actually found. - **Long context (1M tokens)** — used only when a parent asks the end-of-month "make us a printable notebook from the year so far" call, which reads the full year's `BugFind` records as input. Guard: a `BugFind` averages ~600 tokens; a 200-find year is ~120k tokens (comfortable). Chunk by season if a single user's lifetime list passes 1,500 finds. - **Thinking levels** — `medium` for the identification + safety call (the safety classification is load-bearing — never lower this). `low` for the kid-facing copy rewrite (a second pass that simplifies if the parent has set `english_level: emerging`). `low` for the regional-context grounding call. Omit `thinkingConfig` on TTS and Nano Banana 2 calls — the field is not supported on those models. ### Backend services - **Auth — Required.** Firebase Auth with Google sign-in (auto- provisioned by AI Studio Build). **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used for inviting a grandparent in another country who doesn't have a Google account) requires the sender domain to be authorised in Firebase Auth. - **Database — Required.** Firestore for `users`, `families`, `children` (with date-of-birth, language, English level, allergen profile), `bug_finds`, `notebook_exports`, `family_members`. - **File storage — Required.** Firebase Storage for the photographs the kid takes (one cropped 1024px JPEG sent to Gemini per find; original full-res kept for the lifetime-list card). **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 find. - **Email — Required (transactional).** Family invitations via magic links. Monthly notebook PDF delivery to grandparents who opt in. - **Payments — Not needed for v1.** Free for personal use. A future "printed Insect Notebook hardcover for the bedroom wall" tier could pipe to a print-on-demand partner and charge for that physical artefact only. - **External APIs:** Gemini API for all intelligence; optional local weather + season API only if the parent enables the "best bug days this week" optional surface. **Environment variables:** every secret (Gemini API key, Firebase service-account JSON, optional weather token) lives in environment variables — never in client bundle. Include a `.env.example`. **Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete this child's profile' inside the UI · explicit opt-in for any analytics · the family's bug photos and the kid's voice recordings are never sent to Gemini for model training (use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms) · location precision defaults to 'neighbourhood', not 'street'; street-level precision is opt-in and warned · the child's date-of-birth is used only to set the age-appropriate copy and is never displayed publicly. **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. ### Per-call model + tools matrix | Call | Model | thinkingLevel | Tools enabled | |------|-------|---------------|---------------| | Identify the bug + safety classification + facts → `BugFind` schema | `gemini-3.5-flash` | medium | (none) | | Simplify kid sentence for `english_level: emerging` | `gemini-3.5-flash` | low | (none) | | Regional + seasonality context (is this bug in this place at this time of year?) | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) | | "Bug is gone" — disambiguate from colour + size + habitat | `gemini-3.5-flash` | medium | (none) | | Kid-voice TTS for the kid-facing sentence | `gemini-3.1-flash-tts-preview` | n/a | n/a | | Grown-up TTS for the caregiver sentence | `gemini-3.1-flash-tts-preview` | n/a | n/a | | Notebook cover illustration (printable export only) | `gemini-3.1-flash-image` | n/a | n/a | | End-of-month notebook synthesis (long-context over the year's finds) | `gemini-3.5-flash` | medium | (none) — long-context | *Note for builders:* on TTS and Nano Banana 2 calls, omit `thinkingConfig` entirely — the field is not supported on those models. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. ### Primary structured-output schema (seed this verbatim in the prompt) ```typescript import { z } from "zod"; const SafetyCategory = z.enum([ "harmless", "can_sting", "can_bite", "venomous", "allergen_concern", "disease_vector", "unknown_proceed_with_caution", ]); const SafetyChipColor = z.enum(["green", "yellow", "orange", "red"]); const LocalisedSentence = z.object({ language: z.string(), // BCP-47, "tl-PH", "sw-KE", "pt-BR" text: z.string(), reading_level_grade: z.number().min(0).max(12), // approximate }); const CommonName = z.object({ language: z.string(), // BCP-47 name: z.string(), // "salagubang", "kunguni", "besouro" is_household_term: z.boolean(), // true for family-language word grandparents use }); const DisambiguationOption = z.object({ common_name_english: z.string(), scientific_name: z.string(), why_it_might_be_this: z.string(), // "matches the iridescent green back you described" why_it_might_not_be: z.string(), // "you said you saw it at night; this one is diurnal" safety_category: SafetyCategory, }); const SafetyAction = z.object({ audience: z.enum(["kid", "grown_up"]), action_text: z.string(), // "step back slowly, do not swat" urgency: z.enum(["informational", "advisable", "do_this_now"]), }); const BugFind = z.object({ find_id: z.string(), child_id: z.string(), photo_uris: z.array(z.string()), // burst frames or single // Identification identification_confidence: z.number().min(0).max(1), scientific_name: z.string().nullable(), // "Polyphylla decemlineata" or null if uncertain scientific_rank_resolved: z.enum([ "species", "genus", "family", "order", "unknown", ]), english_common_name: z.string().nullable(), // "ten-lined June beetle" family_language_common_names: z.array(CommonName), // ordered by household preference // Safety — load-bearing safety_category: SafetyCategory, safety_chip_color: SafetyChipColor, safety_one_line_for_kid: z.string(), // age-appropriate plain text safety_one_line_for_grown_up: z.string(), // includes the practical adult instruction safety_actions: z.array(SafetyAction), household_allergen_flag_triggered: z.boolean(), // true if matches profile // The answer card habitat_short: z.string(), // "lives in soft soil and eats roots" habitat_kid_facing: LocalisedSentence, the_one_fact: z.string(), // ONE sentence, English the_one_fact_kid_facing: LocalisedSentence, // in the family language the_one_fact_grown_up_facing: LocalisedSentence, // in the family language // Quietly under the answer scientific_name_pronounced: z.string().nullable(),// "puh-LIF-uh-luh dek-em-lin-ee-AY-tuh" size_range_mm: z.object({ min_mm: z.number(), max_mm: z.number(), }).nullable(), active_time_of_day: z.enum([ "diurnal", "crepuscular", "nocturnal", "varies", ]).nullable(), // For the "bug is gone" path is_disambiguation_card: z.boolean(), disambiguation_options: z.array(DisambiguationOption), // Honesty flagged_for_user_review: z.array(z.object({ field_path: z.string(), reason: z.string(), })), refused_to_identify_reason: z.string().nullable(),// e.g. "photo too blurred" // Metadata, written by the client found_at_iso: z.string(), // "2026-05-21T15:42:11Z" found_location_precision: z.enum([ "street", "block", "neighbourhood", "town", "withheld", ]), found_location_label: z.string().nullable(), // "by the kalachuchi bush", written by parent or kid kid_caption: z.string().nullable(), }); type BugFind = z.infer; ``` ### Common failure modes (and how to avoid them) - Agent silently downgrades `thinkingLevel` for the identification call to save quota — pin `gemini-3.5-flash` explicitly. Flash confuses yellowjackets with honeybees, viceroys with monarchs, and false widows with black widows, and the safety category becomes wrong. The kid sentence is cheap; the identity + safety are not. - Model calls a wasp "harmless because it didn't sting you in the photo" — pin in the system instruction: stinging species are NEVER `harmless`, regardless of photo behaviour. The category is about capability, not the moment. - Model invents a confident species when the photo is a blur — set `refused_to_identify_reason` and `scientific_rank_resolved: unknown`, and surface the "we couldn't be sure" UI. Never guess at species level under 0.7 confidence; resolve to family or order. - Model gives a Wikipedia-paragraph fact instead of one tellable sentence — pin "one sentence, picture-shaped, the kid will repeat this to a friend tomorrow". Add a unit test that fact length is ≤ 30 words. - Kid sentence reads at a college level — set the kid sentence's `reading_level_grade` and clamp; if above grade 4 and the child's age is below 8, route through the grade-floor rewrite call. - The family-language name is a Google-Translate-style transliteration of the English name ("polyphylla decemlineata" rendered phonetically in Tagalog) instead of the real household word — pin: "if the household language has a colloquial term for this insect group, prefer that; set `is_household_term: true`. Do NOT phonetically transliterate the Latin name." - TTS reads "Polyphylla decemlineata" in English instead of in Tagalog — pin: TTS reads the localised sentence, NOT the scientific name. The scientific name is shown silently in 11pt under the card and pronounced only when the parent taps the small loudspeaker beside it. - Photo is of a spider, not an insect — accept arachnids, myriapods, isopods, and other small arthropods the kid called "a bug". The schema's `scientific_name` is not limited to Insecta. The kid voice copy says "this isn't actually a bug — it's a [spider / millipede / woodlouse], its cousin!" - Photo is of a dangerous lookalike — disambiguation between black widow / false widow / cellar spider, or between western honeybee / yellowjacket / paper wasp, is mandatory; surface the disambiguation card with safety flags ON until the user confirms. - Search grounding returns a citation that is paywalled or fringe — server-side, filter `groundingChunks` for known-good sources (state agriculture extensions, university entomology departments, museum collections); discard tabloid or low-quality domains before showing citations. ### Negative constraints (hard rules) - Do NOT call a stinging or biting insect "harmless". Wasps, yellowjackets, hornets, fire ants, velvet ants, paper wasps, bees of any species, mosquitoes, kissing bugs, tsetse, blackflies, horseflies, deer ticks, blacklegged ticks, brown widows, black widows, brown recluses, hobo spiders, recluse spiders, redback spiders, funnel-web spiders, centipedes of any size are NEVER `harmless`. They are at minimum `can_sting` or `can_bite`. - Do NOT minimise venom. A black widow is `venomous`, never `can_bite`. A brown recluse is `venomous`, never `can_bite`. The safety chip is red, not orange. - Do NOT recommend handling, picking up, poking, or touching any arthropod in the safety actions, in any category, for any reason, regardless of how confident the model is that the species is harmless. The kid voice may say "you can look closely"; it never says "you can pick it up". - Do NOT invent species the kid will be wrong about at school. Under 0.7 confidence at species level, return `scientific_rank_resolved: family` or `genus` and name the family / genus only. - Do NOT mock or condescend in the "this isn't a bug, it's a spider/millipede" copy. The kid was looking carefully; reward that. - Do NOT use the kid's photos or voice recordings 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-share the lifetime list publicly. Family-only sharing, explicit per-family-member invitation, no "share to social" button. - Do NOT generate a Nano Banana 2 image of an insect that looks like a real photo. The notebook cover illustration is hand-drawn watercolour-style, never photoreal. The model will not be asked to produce "a photograph of a beetle"; it will be asked to produce "a pen-and-watercolour illustration". - Do NOT translate scientific names. "Polyphylla decemlineata" stays Latin. The pronunciation guide is in the family language's phonetic conventions. - Do NOT show street-level location precision by default. Default is `neighbourhood`. Street is opt-in, per-find, with a warning that this will be visible to family members on the list. ### 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: Identify the bug + safety classification + facts → `BugFind` schema Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` You are an entomologist + emergency-trained parent + careful storyteller. A child between 4 and 12 has pointed a phone at a small arthropod — insect, arachnid, myriapod, isopod, or close relative — and a grown-up needs an answer in under eight seconds. The user payload includes: - one to three photographs of the find (a burst of frames the camera took; the most-in-focus is usually the second or third) - the child's age (in whole years) - the child's family language as a BCP-47 code - the child's English level (one of: native, fluent, growing, emerging) - the household allergen profile (subset of: bee, wasp, ant, mosquito) - the rough location (city, region) — used for regional plausibility - the local time of day Output ONLY the BugFind JSON matching the provided schema. The schema includes both a kid-facing and a grown-up-facing localised sentence; populate both, in the family language and in English. Hard rules — identification: - If your confidence at species level is below 0.7, set `scientific_rank_resolved` to `genus` or `family`, leave `scientific_name` null, and put the family or genus name in `english_common_name` ("a kind of June beetle"). Do NOT guess a species the child will repeat as fact at school. - If the photo is too blurred, too dark, or the subject is too small to identify, set `refused_to_identify_reason` with a one-sentence honest explanation ("the subject is smaller than 30 pixels across, I cannot see legs or wing pattern"), set `scientific_rank_resolved: unknown`, and leave species/genus null. The UI will offer a retake. - Accept arachnids (spiders, scorpions, ticks, mites), myriapods (centipedes, millipedes), isopods (woodlice), and other small arthropods the child called "a bug". If the find is not an insect, the kid sentence opens with "this isn't actually a bug — it's a [spider / millipede / woodlouse], its cousin!" — said warmly, never as a correction. Reward the child's looking. - Distinguish dangerous lookalikes when present in the frame: yellowjacket vs. honeybee (waist shape, leg colour); monarch vs. viceroy (hindwing band); black widow vs. false widow (leg patterning, abdomen markings); fire ant vs. carpenter ant (head shape, sting visible); brown recluse vs. domestic spider (violin marking, eye arrangement). When any of these lookalike pairs is ambiguous, set safety to the more cautious of the two. Hard rules — safety, the load-bearing field: - Pick `safety_category` from the closed enum. Pick `safety_chip_color` from {green, yellow, orange, red} by this map: harmless → green; can_bite (mild) or unknown_proceed_with_caution → yellow; can_sting, allergen_concern, disease_vector → orange; venomous → red. - Stinging insects (any wasp, hornet, yellowjacket, fire ant, paper wasp, velvet ant, honeybee, bumblebee, sweat bee, carpenter bee, saddleback caterpillar, puss caterpillar) are NEVER `harmless`. Minimum `can_sting`. - Biting insects/arachnids that can break skin (mosquitoes, blackflies, horseflies, kissing bugs, tsetse, blacklegged ticks, deer ticks, lone star ticks, all centipedes, recluse spiders, widow spiders, hobo spiders, redback spiders, funnel-web spiders, Brazilian wandering spiders) are minimum `can_bite`; medically significant ones (widows, recluses, hobos, redbacks, funnel-webs, wandering spiders, kissing bugs in some regions, Anopheles mosquitoes in malaria-endemic regions) are `venomous` or `disease_vector` as appropriate. - If `household_allergen_flag_triggered` is true (the find is in the household's declared allergen profile), the kid sentence must acknowledge the allergy plainly ("you're allergic to wasp stings — let's step back slowly") and the grown-up sentence must surface the practical next step ("epinephrine on hand; head indoors"). - `safety_one_line_for_kid` is in the family language and at the child's reading level. `safety_one_line_for_grown_up` is in the family language for the speaking adult AND adds the boring practical detail. - `safety_actions[]` always includes at least one informational action; for orange and red chips include at least one `do_this_now` action. - Do NOT recommend handling, picking up, poking, or touching the arthropod in any safety_action, for any category. The kid voice may say "you can look closely with your eyes"; never "you can hold it". Hard rules — the one fact: - `the_one_fact` is ONE sentence, ≤ 30 words, English. The kid will repeat this at school tomorrow. Choose the smallest true thing that is interesting at age 7. - Examples of good facts: "the boy beetles make a hissing sound when you pick them up, like a tiny bagpipe"; "ladybirds taste so bad that birds spit them out"; "this kind of moth pretends to be a hummingbird so birds don't eat it". - Examples of bad facts: a Wikipedia-paragraph summary; three facts with semicolons; a fact about its taxonomy; a fact that requires the kid to know what "mandibles" or "ovipositor" means. - `the_one_fact_kid_facing` is the same fact in the family language, in kid-friendly words. `the_one_fact_grown_up_facing` is the slightly more grown-up version in the family language — one shade more detail, no more. Hard rules — naming in the family language: - If the family language has a colloquial household word for this insect group (Tagalog "salagubang" for any large beetle; Swahili "kunguni" for some bedbug-shaped insects; Brazilian Portuguese "joaninha" for ladybird; Tamil "vandu" for beetle; Cantonese "甲蟲" for beetle), set the localised name with `is_household_term: true`. The grandparent will recognise it. - If a more specific common name exists in the family language and it is widely used in the relevant region (e.g. "kalachuchi" for the moth that visits Plumeria), include both. - Do NOT phonetically transliterate the Latin name. The scientific name stays in the dedicated field, in Latin. - Order `family_language_common_names[]` by the parent's set household preference; the first is the one the TTS will read. `flagged_for_user_review` carries any field where confidence is below 0.7, with a one-sentence reason. The UI will show these as gentle underlines. No commentary. JSON only. ``` --- ### Call: Simplify kid sentence for `english_level: emerging` Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive a kid-facing sentence in the family language and the child's reading-level grade. If the grade is above 3 and the child's English level setting is `emerging`, rewrite the family- language sentence so its complexity matches a grade 1-2 reader in that language. If it already meets the floor, return it unchanged. Hard rules: - Same meaning. Do NOT drop the safety reference if one is in the sentence. - Same family language. Do NOT translate to English. - Same one fact. Do NOT add a second fact. - Prefer concrete, sensory words over abstract ones. Output: the rewritten sentence as a single string. No commentary. ``` --- ### Call: Regional + seasonality context (is this bug in this place at this time of year?) Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: search grounding ``` You verify whether the proposed identification is plausible at the user's location and the current time of year, and return a one-line note for the grown-up. Given the scientific name (if resolved), the city/region, and the month, return: - regional_plausibility: one of {expected, possible, unexpected, out_of_range} - seasonality_note: a one-sentence note for the grown-up ("this beetle is most active in late spring and early summer here, peaks around dusk") - one citation URL from grounded search Hard rules: - Use `google_search` grounding. Prefer state agriculture extension publications, university entomology department pages, museum collection notes, and national biodiversity databases. Avoid tabloid or hobby-blog sources. - If `regional_plausibility` is `unexpected` or `out_of_range`, the seasonality_note explains why ("this species is native to South America and not established in Sacramento; could be a recent import or a misidentification — consider a retake from a different angle"). The downstream UI shows a small "double-check this one" chip. - If the scientific name is null (genus or family only resolved), return regional_plausibility: possible with a generic note. Output the response as JSON in the text body (NOT via `responseSchema` — `responseSchema` and `google_search` cannot be combined in the same Gemini call today). Server-side: parse the JSON, then read citation URLs from the response's `groundingMetadata.groundingChunks[].web.uri` — do NOT ask the model to include URLs in the JSON body; it will hallucinate them. No commentary outside the JSON. ``` --- ### Call: "Bug is gone" — disambiguate from colour + size + habitat Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` The kid lost the bug before the camera caught it. The grown-up has recorded a short description: a dominant colour, an approximate size (small / medium / large, or a length in mm if measured), a habitat tag (in the soil, on the kitchen window, on a hibiscus flower, in the jabuticaba bark), the local season, and the child's location. Return at most three `DisambiguationOption` records — the three most plausible candidates in the user's region, ranked by likelihood. Each option includes a one-sentence "why it might be this" AND a one-sentence "why it might not be" — show your reasoning so the grown-up can pick. Hard rules: - Each option carries its own `safety_category`. If even one candidate is `can_sting`, `can_bite`, `venomous`, or `disease_vector`, the UI will show the safest-default chip until the user confirms. - Mark the BugFind as `is_disambiguation_card: true` so the lifetime list shows the entry as a guess, not a confirmed find. - Never include more than three options. The kid loses interest at three. - Do NOT include a "harmless" option unless every option is harmless. If even one is risky, the user must see that. Output: the JSON object matching the schema with `is_disambiguation_card: true` and `disambiguation_options[]` populated. No commentary. ``` --- ### Call: Kid-voice TTS for the kid-facing sentence Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` Voice: warm, a little bouncy, slightly slower than adult news pace — around 130 words per minute. Pick the Gemini 2.5 Flash TTS voice whose `languageCode` matches the localised sentence's language. Pronunciation follows the voice's native locale automatically. Pre-process the text before sending it to TTS: - Use `the_one_fact_kid_facing.text` plus the family-language common name from `family_language_common_names[0].name` as a short opener — "It's a [name]. [Fact.]". - At each comma, insert a brief pause via "…". At each sentence break, insert a blank line plus "—". Gemini 2.5 TTS does not support SSML ``; these textual cues are how you signal pace. - Skip the scientific name. The kid voice does NOT read Latin. The scientific name is read by a separate optional tap on the card's small loudspeaker icon and uses a slower, more careful pronunciation guide string. - Mid-call voice switching is NOT supported by Gemini 2.5 TTS — if the sentence has an English loan-word inside a Tagalog sentence, keep one voice and let pronunciation follow the chosen locale. Style direction: prepend ONE short directive sentence to the text input, exactly like: "Read warmly and a little bouncy, as a parent reading aloud to a 7-year-old who just found something exciting. …" 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 (Tagalog ng, Cantonese tones, Tamil retroflexes, Arabic emphatic consonants) are NOT exposed by Gemini 2.5 TTS — no SSML `` tag. Pronunciation comes from the chosen voice's native locale. No commentary. ``` --- ### Call: Grown-up TTS for the caregiver sentence Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` Voice: calm, factual, faster than the kid voice — around 165 words per minute. Pick the Gemini 2.5 Flash TTS voice whose `languageCode` matches the localised sentence's language and that is published as an adult voice. The grown-up voice should be different from the kid voice — if both genders are available for the locale, prefer the opposite gender from whichever the kid voice used, so the audio shifts noticeably when the grown-up sentence starts. Pre-process the text before sending it to TTS: - Use `safety_one_line_for_grown_up` followed by the grown-up fact string. If `household_allergen_flag_triggered`, prepend the allergy-acknowledgement sentence. - Encode pauses as "…" and paragraph breaks as a blank line plus "—" exactly as in the kid call. SSML is not supported. - Skip the scientific name in the spoken output. Style direction: prepend ONE short directive sentence: "Read calmly and factually, as a careful adult passing on practical information to another adult. …" No commentary. ``` --- ### Call: Notebook cover illustration (printable export only) Model: `gemini-3.1-flash-image` · n/a · n/a ``` You generate ONE square illustration for the printable Insect Notebook cover. The user payload includes the child's first name, three favourite finds from their lifetime list (with common names in the family language), and a colour palette of three soft botanic tones. Hard rules: - Style is pen-and-watercolour botanical illustration in the Beatrix Potter / Maria Sibylla Merian tradition — soft outlines, light wash, visible paper grain. - Style is NEVER photoreal. Do not produce anything that could be mistaken for a photograph of an insect. - Include the three favourite finds as small illustrations arranged around the child's hand-lettered name at the centre. - Use the supplied palette only. Do not invent new dominant colours. - No text other than the child's first name. No watermarks. No signatures. - Background is botanical (leaves, twigs, soft flowers) consistent with the child's region (kalachuchi for a Manila child, jabuticaba for a São Paulo child, a generic regional flowering shrub otherwise). Output: one square image. No commentary. ``` --- ### Call: End-of-month notebook synthesis (long-context over the year's finds) Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) — long-context ``` You receive every BugFind for one child for the year (or partial year) so far. The user wants a four-page printable Insect Notebook: cover (text payload only — the illustration call runs separately), lifetime list, ten favourites, parents' signature line. Hard rules: - Pick the ten favourites by a weighted score: kid-caption presence (+2), grandparent or co-parent reaction recorded (+1), unique family (+1), unique safety category (+1, capped). Tie-break by recency. - For each favourite, write a two-sentence summary in the family language at the child's reading level. Sentence 1 names the find and the place. Sentence 2 carries the one fact. - The lifetime list page is a typeset index of every find, chronological, with date, common name in family language, and the kid's caption (truncated to 40 characters). - Never invent a find that is not in the input. Never inflate the lifetime count. - Never include the household allergen flag detail on the printable — the kid shows this notebook to friends. Output: a structured JSON object with the four pages' content ready to typeset. No commentary outside the structured output. ``` ## 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 Saturday morning driveway find.** Riza in Sacramento is on her second coffee. Mayari, seven, runs in with her hands cupped: "Mama, what is it?" Riza opens the app, points the phone at the beetle on the paving stone, the answer comes back in Tagalog and in English in six seconds, the green chip says it's harmless, Mayari reads the Tagalog sentence aloud to herself, and the lifetime total ticks from 47 to 48. This is the demo loop the homepage opens with. - **The footpath sting.** Daniel in Nairobi is walking Kibet, six, back from the small market. Kibet picks up a matabele ant before his dad can stop him and gets stung. Daniel opens the app, photographs the ant on the ground (it is still there, still angry), the orange chip surfaces immediately, the kid sentence in Swahili says "this ant can sting, please don't pick it up", and the grown-up sentence says calmly: rinse the bite area, watch for swelling for 30 minutes, the sting is painful but rarely dangerous for a healthy 6-year-old. The lifetime list adds the find with the orange chip preserved. - **The cigarra shell.** Beatriz in São Paulo and Théo, four, are at the edge of the quintal. He found a translucent brown shell on the bark of the jabuticaba. He thinks the cigarra is dead. The app reads the photograph, recognises it is a moulted exoskeleton (not the insect itself), and the kid sentence in Brazilian Portuguese says "this is the cigarra's old shirt — she grew bigger and slipped out of it, like you growing out of your jacket". The lifetime list adds it with a small "moulted exoskeleton" tag so the kid knows it counts. - **The viceroy that wasn't a monarch.** A homeschool family in rural Wisconsin. The mother is teaching pollinators this week and the kid points at a butterfly on a milkweed. The app names it a viceroy, not a monarch, and the kid sentence opens "look at the bottom stripe on its wing — that's how you tell viceroys from monarchs". The grown-up sentence adds the mimicry context. - **The yellowjacket near the picnic.** A father in Berlin at a weekend picnic. His daughter, nine, asks about the wasp on the Brötchen. The app calls it a Vespula germanica, the chip is orange, the kid sentence says "this is a yellowjacket — it can sting, let's not swat, we move slowly away from the table". The grown-up sentence adds: known allergy in the household profile — epinephrine in the bag — back away to the bench. - **The black widow under the porch step.** A grandmother in Phoenix with her grandson, eight, on a back-yard scavenger hunt. The boy reaches towards a small black spider with an hourglass. The identification call returns a red chip before the species name, the kid sentence is read aloud immediately in English: "Step away from this one. This is a black widow and her bite can make you very sick." The grown-up sentence: do NOT attempt to remove this yourself, call a pest professional, the regional poison control number is in your phone's emergency settings. - **The mosquito in the rainy-season courtyard.** A father in Bangalore at dusk after rain. The kid points at a mosquito. The identification call resolves to Aedes aegypti with regional context: yes, dengue-relevant here, this month. The kid sentence says "this is the mosquito that can carry dengue — let's put on long sleeves and the mosquito spray". The grown-up sentence: peak biting times are dawn and dusk; remove standing water from the courtyard pots; this is the season for the school's dengue advisory. - **The bug-is-gone moment.** A mother in Lagos with her two girls. The bug was small, fast, brown, on the kitchen tile by the back door, gone before she got the camera open. She taps "bug is gone", picks colour brown, size small, habitat "kitchen near back door, at night, in Lagos in May". The model returns three disambiguation options with safety chips, the user notes they didn't see legs that felt long enough for any of the candidates, and saves it as an unconfirmed find. The kid still gets to add it to the list, with a small "we weren't quite sure" tag. - **The grandparent at the screen door.** Mayari, ten finds in, FaceTimes her abuela in Cebu and goes through the lifetime list, one card at a time. The grandparent calls out the Cebuano names she used as a child for the same insects. The mother quickly adds those as `is_household_term: true` Cebuano common names so future finds surface that name first. - **The "this isn't a bug, it's a spider" moment.** A four-year-old in Madrid runs in with a daddy long-legs. The kid sentence opens "this isn't actually a bug — it's a spider, its cousin! Spiders have eight legs and bugs have six". Safety: green chip; do not handle. - **The Saturday with two grandparents.** Co-parented household. Both grandparents are added to the lifetime list. One adds finds from his garden in Hanoi; the other from hers in Kraków. The kid sees one shared list with two contributor avatars and a small flag next to each card showing which country it was found in. ## 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 shot of a child's cupped hands holding a small beetle in afternoon sunlight, with a parent's hand visible at the edge of the frame. One paragraph: "I Saw a Bug names what your kid finds — in your family's language — with the safety call made carefully every time." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample lifetime list" → loads the demo data in section 8a. 2. **First-run family setup.** Three short cards in one flow: the child's name, age, family language (with a long picker that puts the user's auto-detected language at the top); allergen profile (bee / wasp / ant / mosquito — multi-select, optional); location precision default (street / block / neighbourhood / town / withheld), preselected to `neighbourhood` with a short explanation of why. 3. **Home — camera-first.** The default view is a viewfinder with a big shutter button at the thumb's resting position. Underneath the viewfinder, the running lifetime total ("48 finds — last one was a ten-lined June beetle"). Above the viewfinder, a small horizontal rail of recent finds the kid can swipe through to show a friend. 4. **Capture → answer card** (the eight-second loop). After the shutter, the camera shows the captured frame and a thin progress line ("finding…") for 4-6 seconds. The answer card slides up: at the top, the safety chip (coloured AND text-labelled), the localised common name in family language, the English name, the one fact in the family language and (below, lighter) in English, the grown-up sentence in a slightly different background, and the scientific name with a quiet pronunciation guide. A "read it to me" button next to the kid sentence. A "save to my list" button at the bottom (auto-saved by default, this is the manual-save fallback). 5. **The lifetime list.** A scrollable grid of index-card-shaped tiles, one per find. Each tile shows the cropped photograph, the family-language common name, the kid's caption if any, the date, the small safety chip in the corner. A running total at the top ("48 finds, 4 categories: 31 harmless, 9 can_sting, 6 can_bite, 2 venomous"). Filters: by safety category, by family, by place, by contributor. 6. **Find detail card.** Tapping a tile in the lifetime list opens the full card: the photograph at full width, all the localised sentences, the regional context note, the safety actions, the kid's caption, the audio "read it again" buttons, the contributor avatars who added it, a small map showing the location at the precision the parent chose. 7. **Bug-is-gone path.** A separate entry point under the camera shutter, labelled "the bug is gone". Four short pickers — colour, size, habitat tag, when (now / earlier today / yesterday). Returns a disambiguation card with up to three candidates, each with its own safety chip and "why this / why not this" reasoning. 8. **Family & contributors.** Invite a parent, grandparent, carer. Magic-link email. Each contributor's avatar appears on the cards they added. A small "languages" panel lists which family-language common names came from which contributor. 9. **Safety panel** (entered from any orange or red chip). A focused screen with the safety actions for this find, large legible text, a regional poison-control phone-number lookup widget (offline- capable: the user can paste in their local number in setup), and a "this isn't an emergency tool" reminder at the bottom. 10. **Printable Insect Notebook export.** Preview of the four-page PDF — cover with the child's hand-lettered name and three favourite finds illustrated; lifetime list page; ten favourites with two-sentence summaries; signature page for the parents. Buttons: "download PDF", "email to grandparents". 11. **Settings.** One column. Child profile (name, age, family language, English level); allergen profile; location precision default; voice preferences (which family-language TTS voice is the kid voice; which is the grown-up voice); contributors; privacy ("delete this child's profile" with a 60-second confirm delay); the capabilities panel link. 12. **Footer.** "Made for the kid who needs to know what every bug is." Privacy: "Your kid's finds 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 I Saw a Bug." - Subhead: "Name what your kid finds — in your family's language — with the safety call made carefully every time." - One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic photo-identification app: it answers in under eight seconds, it gives one fact small enough to repeat at school, and it never tells a kid that a yellowjacket is harmless. - Visual: a small annotated illustration of an answer card with the green/yellow/orange/red safety chip labelled, the kid sentence labelled, the grown-up sentence labelled, and the "read aloud" button labelled. **Slide 2 — Try it now.** - One short prompt: "Try with the sample lifetime list". - A live demo input pre-loaded with three finds from the seed content in section 8a — a harmless beetle, a yellowjacket, a black widow. - 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the Tagalog common name surfacing first, the orange chip on the yellowjacket, the red chip pre-empting the species name on the black widow). **Slide 3 — How to remix this.** - Headline: "Make this yours." - Three short bullets: - "Swap the sample lifetime list in `/data/seed-finds/` for your own." - "Adjust the family-language defaults in `/server/config/languages.ts`." - "Wire up your Gemini API key and Firebase project via the env-var list in the capabilities panel." - Primary CTA: "Use this template" → links to AI Studio Build remix entry point. - Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again). **Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`. **Don't:** - Don't gate content behind the modal. The page beneath must be fully usable. - Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`. - Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only. ## 6c. Capabilities info button (persistent in header) Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**. **Panel contents (in this order):** **Gemini capabilities used (the hero list):** - **Gemini 3.5 Flash (multimodal)** — identifies the insect (or spider, millipede, woodlouse) from a single photo or a 3-frame burst, and classifies its safety in one round-trip. One call per find. - **Gemini 3.5 Flash (multilingual)** — writes the kid sentence and the grown-up sentence in your family's language and in English in the same call. Tagalog, Cebuano, Swahili, Kikuyu, Brazilian Portuguese, Spanish, Vietnamese, Cantonese, Mandarin, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Farsi, Khmer, Bahasa Indonesia, Yoruba, Igbo, Arabic. - **Gemini 3.5 Flash + grounded search** — checks whether the species is expected in your region, in this season. Surfaces "double-check this one" when something is out of range. - **Gemini 3.5 Flash (long context)** — at the end of the month, reads the whole year's finds to assemble the printable Insect Notebook. - **Gemini TTS** — reads the kid sentence in a kid voice and the grown-up sentence in a grown-up voice, in your family's language, at the right pace for each. - **Nano Banana 2 (Gemini 3.5 Flash Image)** — draws the printable Notebook's cover illustration in watercolour-botanical style. Used ONLY for the cover; never for fabricating "photos" of insects. - **Firebase Auth** — Google and Apple sign-in, family invitations via magic links. - **Firestore** — stores the lifetime list, syncs across devices in real time so the grandparent on FaceTime sees the new find as it saves. - **Firebase Storage** — keeps the original photographs. - **Cost note** — see the detailed breakdown in 6d. A family that finds one bug a day for a year costs about $5.50 of Gemini API spend, total. - **Privacy note** — your child's photographs and voice recordings are private to your family. 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 location precision defaults to neighbourhood, never street. **Backend services this app depends on:** - Auth: see section 4b - Database: see section 4b - Storage: see section 4b - Email: see section 4b - Payments: see section 4b (not used in v1) - External APIs: see section 4b **Environment variables you'll need to configure:** - `GEMINI_API_KEY` — your Google AI Studio API key - `FIREBASE_PROJECT_ID` — your Firebase project id - `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only) - `WEATHER_API_KEY` — optional, only for the "best bug days this week" surface **Cost + privacy notes:** - One short paragraph per cost-sensitive capability: the identification call is the headline cost at ~$0.014 per find; the TTS calls add ~$0.001 per find; the end-of-month notebook is a weekly cost peak (~$0.40 for a 200-find year). Total per family per year: under $6. - One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this child's profile" — 60-second confirm, gone in 60 seconds), what is never sent for training, why the default location precision is neighbourhood. **Documentation links:** - AI Studio Build docs - Gemini API multimodal, multilingual, long-context, TTS, Nano Banana 2 docs - Firebase Auth, Firestore, Firebase Storage docs - A short note on the safety-classification taxonomy and why yellowjackets are never `harmless` **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) - **Identify + safety + facts (Gemini 3.5 Flash, medium thinking)** — one 3-frame burst (~3 images) plus ~800 output tokens. ~$0.014/find. - **Kid-sentence simplifier (Gemini 3.5 Flash, low thinking)** — runs only when `english_level: emerging` and the kid sentence is above grade 3. ~$0.0008/find when it runs; on average ~$0.0002/find amortised. - **Regional + seasonality (Gemini 3.5 Flash + grounded search)** — ~$0.001/find. Cached per species per region per season, so a household sees this cost mostly on novel finds. - **Bug-is-gone disambiguation (Gemini 3.5 Flash, medium thinking)** — ~$0.011/find, only runs when invoked. - **Kid-voice TTS (Gemini 2.5 Flash TTS)** — ~$10/M output tokens, effectively ~$0.000003/character. A 25-word kid sentence ≈ $0.0004/find. Cached per find; charged once. - **Grown-up TTS (Gemini 2.5 Flash TTS)** — similar to kid TTS, slightly longer sentence ≈ $0.0007/find. Cached per find. - **Notebook cover illustration (Nano Banana 2)** — ~$0.03/image. Generated once per child per quarter when the printable notebook is exported. - **End-of-month notebook synthesis (Gemini 3.5 Flash, medium thinking, long-context)** — runs once a month per child. A 200-find year is ~120k input tokens + ~2k output ≈ $0.16/run. A full-year synthesis at year-end ≈ $0.40. - **Expected per-find cost on first ingest:** ~$0.016 (identify + TTS ×2 + amortised grounded search). **A year of one-find-a-day:** ~$5.80 + ~$0.40 quarterly cover illustrations × 4 + monthly notebooks ≈ ~$8 per child per year, total. - **Image storage:** Firebase Storage standard tier, ~$0.026/GB/month. A cropped 1024px JPEG is ~250 KB; 365 finds use ~90 MB ≈ ~$0.003/month. ## 7. Design language - **Mood:** A child's nature journal that happens to live on a phone. Not a smart-home device. Not a museum app. The morning kitchen at 8am with a six-year-old running in barefoot, the parent's second coffee cooling, the screen door open, the day not yet started but the bug already named. - **Typography:** A soft display serif for find names and chapter headings (Tiempos Text or Source Serif Pro), a clean geometric-humanist sans for app chrome (Söhne or Inter). A hand- drawn accent serif only for the printable Insect Notebook cover and for the kid's own captions — never for the identification answer itself, which sits in serif for legibility. - **Palette:** Botanical paper background `#F6F2E8` for the answer card, deep ink `#1E1B14` for body text, herbarium green `#3F6B47` for the kid sentence ribbon, warm rust `#A14B2F` for the scientific name accent, and the four safety colours: harmless `#3F8F50`, caution `#B5862C` (warm ochre yellow), warning `#C26A1E` (warm orange), danger `#A82A1E` (deep red). Each safety chip carries a text label AND an icon AND the colour, so colour is never the only signal. - **Imagery:** The kid's actual photographs are the hero. The cropped square sits at the top of every answer card; never replaced; never re-illustrated. Nano Banana 2's watercolour illustrations appear ONLY on the printable Notebook cover and on the "bug is gone" disambiguation card sketches, and they are always visibly drawn, never mistaken for photos. - **Hand-feel touches:** A barely-visible paper grain on the answer- card background. The "read it to me" loudspeaker icon has a small ripple animation when the audio is playing; the safety chip pulses once gently on first appearance for orange or red, never animates again. The lifetime list scrolls with a tiny inertial bounce so it feels like flipping cards. - **Spacing:** consistent 4-px base. Generous whitespace — the answer card needs to be glanceable in eight seconds. - **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Lifetime list tiles use 12; the answer card uses 20; chips use 999. - **Shadows:** subtle, layered, warm-grey-tinted. Avoid heavy drop- shadows. - **Motion:** purposeful — entrance fades, the safety-chip first- appearance pulse, the audio ripple, the lifetime-list tile inertia. Respect `prefers-reduced-motion`. No bouncing splash animations. The orange/red safety-chip pulse on first appearance is the meaningful motion; respect reduced-motion by appearing without the pulse. - **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Point the camera at the bug — we'll do the rest"). The "finding…" progress line on the answer card has three honest sub-states: "looking at the photo" → "checking what it might be" → "writing it up for you", each ~2 seconds, so the kid sees what's happening. ## 8. Content generation rules - Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'. - Invent plausible names, locations, finds, and dates that fit the domain (use the seed content in section 8a as a starting point). When inventing, anchor to specific houses, gardens, footpaths, schools, and seasons — Sacramento in May, Nairobi in February, São Paulo in October — but never claim a fictional family is a real one. - Tone: warm, direct, free of corporate language. This template is for a parent, not a brand. - Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'. No 'AI-powered identification'. - Body copy: short paragraphs (2-4 sentences). Use lists where appropriate. - Plain language. Avoid jargon — except where the user already speaks the jargon (the homeschool parent wants to see "Lepidoptera" in the small print under the answer; the kid wants to see the family-language common name first). - Where the app outputs AI-generated content, never label it as "AI says" — let it speak naturally. Use small uncertainty cues only where epistemic honesty requires them (a low confidence identification shows the family name with a faint dashed underline; tapping it reveals the genus / species options the model considered). ## 8a. Seed content (use these specific examples) Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, dates, and snippets verbatim where helpful, or generate close variants that sit in the same world. **Sample children's lifetime lists (sidebar):** - "Mayari's List, age 7" (48 finds, contributors: Mama, Tatay, Lola Cecilia in Cebu) — Sacramento backyard, kalachuchi bush, alley, school garden. Family languages: Tagalog and Cebuano. - "Kibet's List, age 6" (29 finds, contributors: Baba, Mama, Bibi Wanjiku) — Nairobi neighbourhood path, school grounds. Family language: Swahili, with a few Kikuyu names from the grandmother. - "Théo's List, age 4" (18 finds, contributors: Mamãe, Papai, Vovó Sofia) — São Paulo quintal, jabuticaba tree, Avenida 9 de Julho pavement crack. Family language: Brazilian Portuguese. - "Asha's List, age 8" (61 finds, contributors: Amma, Appa, Paati in Chennai) — Bangalore garden, school field, rooftop terrace, the road to the temple. Family languages: Tamil and English. **Sample find in detail view (this is what the demo should show):** - **Photograph:** a single small brown beetle on a paving stone in late afternoon light, taken at 30 cm with the camera held by a parent. 1024px cropped JPEG. - **Identification confidence:** 0.91 - **Scientific name:** "Polyphylla decemlineata" - **Scientific rank resolved:** species - **English common name:** "ten-lined June beetle" - **Family-language common names (Tagalog first):** - Tagalog: "salagubang na may sampung guhit" (`is_household_term: false`, descriptive) - Cebuano: "bakukang" (`is_household_term: true`, the grandmother's word for any beetle) - English: "ten-lined June beetle" - **Safety category:** harmless - **Safety chip color:** green - **Safety one line for kid (Tagalog):** "Hindi siya nangangagat — pwede mo siyang tingnan, pero huwag mong hawakan." - **Safety one line for grown-up (Tagalog):** "Hindi nangangagat o nanunuya. Kung tatakbo siya, hayaan mo na lang." - **Habitat short (English):** "lives in soft soil and eats roots underground, comes out at night" - **Habitat kid-facing (Tagalog):** "Nakatira siya sa malambot na lupa at kumakain ng ugat — paglabas niya, gabi na." - **The one fact (English):** "the boy beetles make a hissing sound when you pick them up, like a tiny bagpipe" - **The one fact kid-facing (Tagalog):** "Ang mga lalaking salagubang ay sumisitsit kapag hinawakan mo sila — parang maliit na bagpipe." - **The one fact grown-up-facing (Tagalog):** "Ang mga lalaking salagubang na ito ay sumisitsit sa pamamagitan ng paghihimakas ng pakpak — natural na panlaban sa mga predator." - **Scientific name pronounced:** "puh-LIF-uh-luh dek-em-lin-ee-AY-tuh" - **Size range:** 22-28 mm - **Active time of day:** crepuscular - **Found at:** 2026-05-21T15:42:11Z - **Found location precision:** neighbourhood - **Found location label:** "sa driveway, malapit sa kalachuchi" - **Kid caption:** "nahanap ko sa driveway namin!" **Sample yellowjacket find (the orange chip example):** - **Photograph:** a Vespula on a wooden picnic table, sunlight, the edge of a Brötchen visible. - **Scientific name:** "Vespula germanica" - **English common name:** "European wasp / German yellowjacket" - **Family-language common names (German first, family is bilingual):** - German: "Gemeine Wespe" (`is_household_term: true`) - English: "yellowjacket" - **Safety category:** can_sting - **Safety chip color:** orange - **Safety one line for kid (German):** "Diese Wespe kann stechen. Wir bewegen uns langsam weg, nicht schlagen." - **Safety one line for grown-up (German):** "Stich möglich; bekannte Allergie im Haushaltsprofil — Epinephrin in der Tasche; zur Bank zurückziehen; nicht schlagen, das setzt Alarmpheromone frei." - **Household allergen flag triggered:** true (wasp is in the household allergen profile) - **The one fact (English):** "yellowjackets release an alarm scent when swatted, which calls more wasps to the spot" - **The one fact kid-facing (German):** "Wespen rufen ihre Freunde, wenn man nach ihnen schlägt — also bleiben wir ruhig." **Sample black widow find (the red chip example):** - **Photograph:** a small black spider with a red hourglass on its abdomen, under a porch step, low light. - **Scientific name:** "Latrodectus hesperus" - **English common name:** "western black widow" - **Family-language common names (English-only household):** - English: "black widow" - **Safety category:** venomous - **Safety chip color:** red - **Safety one line for kid (English):** "Step away from this one. Black widows can give a bite that makes people very sick. Let's go inside." - **Safety one line for grown-up (English):** "Adult medical attention is warranted on suspected bite, especially for children and elderly; do not attempt to remove the spider yourself; call a pest professional. Regional poison control number lives in your emergency settings." - **Safety actions:** kid → "step back slowly" (do_this_now); kid → "go inside, tell a grown-up immediately" (do_this_now); grown-up → "call a pest professional" (advisable); grown-up → "if you suspect a bite, call poison control" (do_this_now). - **The one fact (English):** suppressed — for red-chip finds the one-fact field is hidden in the answer card and surfaced only in the lifetime-list detail page later. **Sample input artefacts (for the build to demonstrate):** - A photograph of a ten-lined June beetle on a paving stone in Sacramento, taken by Mayari's mother at 3:42 pm on 21 May. - A photograph of a matabele ant on a footpath in Nairobi after a small sting. - A photograph of a moulted cigarra exoskeleton on a jabuticaba trunk in São Paulo. - A photograph of a Vespula on a picnic-table edge in Berlin with the wasp allergy active. - A photograph of a western black widow under a porch step in Phoenix. - A photograph of an Aedes mosquito on a child's forearm in Bangalore at dusk. **Sample voice copy:** - Onboarding: "Point the camera at the bug. We'll name it in your language — and tell you whether it can sting." - Capture progress: "looking at the photo…" / "checking what it might be…" / "writing it up for you…" - Empty list: "Mayari's list is waiting for the first find. Photograph the first bug you see and we'll add it." - Error (couldn't read): "We couldn't be sure from this photo. Can you take another from above, with the bug fully in frame?" - Save confirmation: "Added to Mayari's list — ten-lined June beetle (bakukang)." - Orange chip surface: "This one can sting. Let's read what to do." - Red chip surface: "Step away. We'll read this together once you're back inside." - Low-confidence note: "We weren't sure of the species. Tap the family name to see what we considered." **Sample family invitation email subject + body:** - Subject: "Lola — Mayari is making her bug list. Want to add the Cebu finds?" - Body: "Hi Lola — Mayari has 48 finds in her list so far. She wants you to add the ones you find in the garden in Cebu. Tap to join the list — your finds will show up under your name." [Open List] ## 9. Media & assets - **Hero image (landing screen):** A photographed-looking shot of a child's cupped hands holding a small brown beetle in afternoon sunlight, with a parent's hand at the edge of the frame for scale, taken from above at a slight angle. Generate via Nano Banana 2 with a prompt emphasising "small child's hands, soft afternoon sunlight, a real beetle visible, blurred garden background, no faces, gentle shadow, no overlay text". Style is photographed, not illustrated. - **App icon / wordmark:** Set in the display serif. A small hand- drawn beetle silhouette before the title word. The icon's beetle is visibly drawn, not photoreal, so the icon never looks like a real insect. - **Empty-state illustration:** A simple line drawing of a child's hand reaching out an index finger towards a small bug at the edge of a leaf. Hand-drawn aesthetic, never a flat icon. - **Answer-card chip icons:** Four custom safety chip icons — harmless (a small leaf), can_sting (a small concentric ripple), can_bite (a small jaw shape), venomous (a small cross). Always paired with the text label and the colour. - **Demo find photographs:** Generated per the prompts in section 8a — Nano Banana 2 prompts that request "the actual insect, in natural light, on its actual surface, no overlay, no caption, no watermark, no decorative bokeh". Each demo find photograph should look real, taken by a parent. - **Notebook cover illustration:** Watercolour-botanical style, generated by Nano Banana 2, always carrying the child's hand- lettered name and three of their favourite finds illustrated around it. The style sets a clear visual border between "real finds the kid photographed" (photos) and "illustrations the app drew for the printable" (visibly watercolour). - **Stock fallbacks:** If image generation fails, fall back to the photographed sample beetle in `/public/samples/sample-beetle.jpg`. Never to a "🐞" emoji. - **Generated imagery:** prefer Nano Banana 2 over stock photography for hero and illustrative assets. Prompt for warmth, asymmetry, and slight imperfection — avoid the glossy 'AI render' look. NEVER use Nano Banana 2 to fabricate an identification photo of an insect the kid did not actually find. - **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift. - **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only. ### Build-time asset manifest (explicit specs) Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs. **Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id: - `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted. - `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence. - `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible. **Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill): - `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt. - `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline. - `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails. **Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint. **Hard rules** - Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error. - No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code. - Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`). - File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`. ## 10. Interactivity & states - Every interactive element has hover, focus, active, and disabled states. - Forms validate inline and show specific error messages (not "Invalid input"). - Loading states use skeletons that match the eventual layout, not spinners. - Empty states explain the next action with a button whose label fits THIS app's domain: "Point the camera at a bug", "Add Lola as a contributor", "Try the bug-is-gone path" — 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 three-step "finding…" indicator before content starts arriving. - If an AI call fails, show a calm, specific error ("We couldn't be sure from this photo. Want to try another angle, or describe what you saw?") and offer retry. - Low-confidence identifications show the family name with a faint dashed underline; tapping reveals the alternate genus / species the model considered. - The orange/red safety-chip pulse on first appearance is the meaningful motion; reduced-motion users see the chip appear without the pulse. The chip's colour, label, and icon are unchanged. - The audio "read it to me" button shows a small ripple animation while playing; pressed-state is clear; pausing snaps to the current word. ## 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 identification + safety + facts and for "bug is gone" disambiguation and for the monthly notebook synthesis; `gemini-3.5-flash` for kid-sentence simplifier and for grounded regional context; `gemini-3.1-flash-tts-preview` for both kid and grown-up TTS; `gemini-3.1-flash-image` for the notebook cover illustration. Set `thinkingLevel` explicitly per call; omit `thinkingConfig` on TTS and image calls. - **Image upload to Gemini:** crop to 1024px square server-side before sending. For multi-frame bursts, send 2-3 frames via the Gemini Files API (`files/*` resource name) or as `inlineData` (base64). Do NOT pass Firebase Storage public URLs directly to `generateContent`. - **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed lifetime list on first launch. - **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as fallback for grandparents. - **Storage:** Firebase Storage for original photographs (NOT auto- provisioned by AI Studio Build — enable in Firebase console first). Pre-signed URLs only. - **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. - Use `clamp()` for fluid typography. Prefer container queries over media queries for component-level responsiveness. - Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS — the shutter button sits inside the safe area at the thumb's resting position. - Zero horizontal overflow at any width. Zero layout shift on load. - Persist user data in Firestore. Use real-time listeners on the lifetime list view so the grandparent on FaceTime sees a new find as soon as it saves. - Optimistic UI on writes (the photograph appears in the lifetime list immediately, with a small "finding…" placeholder until the identification call returns); reconcile on response. - Camera capture uses the Web Camera API with fixed-focus tap and 3-frame burst where supported; falls back to native camera otherwise. - **iOS Safari gotchas (graceful degradation):** camera permission does NOT persist across page reloads on iOS — re-request on every capture session; backgrounded Safari tabs pause `getUserMedia` — checkpoint state on `visibilitychange` and re-acquire the stream on return; iOS may degrade resolution or torch availability under Low Power Mode — always offer `` as a fallback so a bug photo still saves when WebRTC is denied; rotation can drop the camera track — re-bind on `orientationchange`. ## 12. Accessibility (WCAG 2.2 AA) - Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`. - All interactive controls reachable by keyboard with a visible focus ring. - Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. The four safety chip colours are tested against the bone-paper background and meet AA. - The safety chip NEVER communicates state by colour alone — the text label and icon are always present, in that order, at the same size. - All images have meaningful `alt` text. The original photographs have `alt` describing the find ("photograph of a small brown beetle on a paving stone in afternoon sunlight"). - Form fields have associated `