# 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. --- # Fridge → Dinner Tonight ## 1. Project **Fridge → Dinner Tonight** turns a single photograph of an open fridge into a paced, achievable dinner plan for the evening you are actually in. It is 6:30 pm. The user has spent eleven minutes holding the fridge door open, has closed it twice, has opened the takeaway app and closed it, and is now in the small failure-state where every option feels wrong. They photograph the fridge. In twelve seconds the app comes back with a real dinner: a starter, a main, an optional dessert, the order to start each in so everything arrives at the table at the same time, the substitutions it had to make ("you have onions, recipe wanted shallots — use the onions and cut the cook time by two minutes"), and an honest note on what is missing if anything fundamental is. If the evening is for guests, one tap turns the plan into a printable 4K dinner-party menu poster — the courses laid out in editorial typography, the wines suggested, the date and the hosts' names rendered legibly in-image. The poster is optional and never the default. Most weeknight users will never see it. It is there for the Friday when six people are coming and the user wants the menu pinned to the fridge by 7:15 pm. The single demo that proves the magic: the user takes a photo of a fridge containing half a roast chicken, a fennel bulb, half a lemon, a tub of crème fraîche, two eggs, a wilting bag of spinach, and a sad-looking heel of parmesan. In twelve seconds the app returns a three-course dinner — a quick fennel-and-lemon salad to start, a chicken-spinach gratin with the crème fraîche and parmesan to follow, and a soft-set lemon custard from the eggs and the rest of the lemon for dessert. Each dish lists exactly what is being used, the prep and cook time, the order to start in, and what to skip if the user is short on time. No invented ingredients. No "you'll also need…" demands that defeat the whole point. **Tagline:** _Drop a fridge photo. Get back tonight's dinner — paced, honest, and (optionally) a 4K menu poster you can hang._ ## 2. Target audience This is a job-to-be-done template. The job is universal: you have food in the house, you are tired, you do not know what to make. The audience is anyone who cooks a few times a week, lives in a house with a fridge, and would rather not order in tonight. The shape of the people who land on this in the gallery: - Weeknight cooks at 6:30 pm who don't want to think anymore - New cooks who can't yet "see" a meal in a set of ingredients - People who hate food waste and want to use the wilting things up - Hosts the morning of a dinner party, looking at a half-stocked fridge and deciding what to top up at the shops - People with one specific allergy or restriction (gluten, dairy, shellfish, peanut, sesame, egg) who get tired of generic recipes and want a plan that actually honours their list - People cooking for one and tired of recipes that scale to four - Parents at the end of the working day who need a 30-minute plan and a 60-minute plan and want to choose between them - Anyone who has ever stood at the open fridge for ten minutes The 19-gallery rule: this is universal, not persona-led. The voice opens on the job ("It is 6:30 pm and the fridge is open") not on a named character. ## 3. Core value propositions Surface these clearly in copy, visual emphasis, and screen ordering. They are the reasons a visitor stops scrolling and taps. - **One photo, real plan, twelve seconds.** Open the camera, take the picture, put the phone down. Twelve seconds later the dinner is on screen, paced and ordered, every ingredient anchored to something visible in the fridge. - **Never invents ingredients.** If the recipe wants shallots and the fridge has onions, the app says so, picks the closest match, and tells you what to adjust. If something fundamental is missing (a stock cube for a risotto, an acid for the dressing) the app says that too — and offers a different recipe that doesn't need it. The line we never cross: telling the user they have something they don't. - **Paced to your evening.** Every plan returns prep time, cook time, total wall-clock time, and the order to start each dish in so the starter, main, and dessert finish within five minutes of each other. "Start the gratin at 7:42 if you want to eat at 8:30" — that line is the difference between a recipe app and a dinner app. - **Substitutions are visible, not silent.** When the model swaps an ingredient it shows the swap chip on the recipe card — "shallots → onions (reduce 2 min)", "thyme → dried oregano (use half)". The user sees every compromise made on their behalf. - **Allergen-aware, strictly.** If the user names allergies on first visit, the plan honours them as hard exclusions and the app refuses to surface a recipe that touches them, even by cross-contamination risk where relevant (shellfish, peanut, sesame, egg, dairy, gluten, soy, fish, tree-nut). The exclusion is visible in the plan ("dairy-free path; no butter, no cream"). - **Dinner-party poster is one tap, not the default.** Most users never see it. For the user who is cooking for guests, one tap generates a 4K poster with the menu rendered in legible editorial typography (the hero of the post-I/O 2026 Nano Banana Pro capability). Two-line title, three courses, wines if surfaced, date, hosts' names. Print-ready PDF below the image. - **Honest about ambition.** A weeknight plan with 25 minutes of work and one pan is the default. A "show off for the guests" plan is opt-in. The app never demands the user roast a duck on Tuesday because Tuesday is not duck night. - **Hard rule on food safety.** The plan refuses to cook with ingredients the photo suggests are spoiled (visible mould, slime on protein, expired packaging dates if visible in the photo). When in doubt the app says so plainly and offers a recipe that doesn't use the questionable item. The disclaimer copy is prominent: this is not a substitute for your own judgement, your own nose, or your local food-safety authority. ## 4. Features to build - **One-photo fridge capture** — open camera → frame fridge → shoot. Or upload an existing photo. Multiple photos supported (shelf-by-shelf for a packed fridge). - **Ingredient inventory view** — what the model saw, as chips the user can tap to remove (false positives) or add (things in drawers it missed). Quantities estimated where visible. - **Plan card** — starter / main / (optional dessert), each with ingredients list (used: green chip, swapped: amber chip, optional: grey chip), prep time, cook time, total time, ordered steps with timing anchors. - **Pacing strip** — a horizontal timeline: 7:42 start gratin → 7:55 toss salad → 8:05 dress salad → 8:25 plate main → 8:28 finish dessert. Adjustable target eat-time. - **Substitution panel** — every swap shown as a chip with the reason in plain English. Tap a chip to revert (forces the model to pick a different recipe). - **Allergies & restrictions** — single-pass setup on first visit; persisted per device. Hard exclusions; soft preferences (no liver, no fennel, prefer Italian, vegetarian Tuesdays). - **Cuisine preference** — soft dial. The user can nudge toward Italian, French, Japanese, Mexican, Levantine, Filipino, Indian, or "surprise me". Not a hard filter. - **Time budget** — slider: 15 min, 30 min, 45 min, 60 min, "I have the evening". The plan re-runs against the new budget. - **Skip dessert / Skip starter** — single tap to drop a course. The plan re-paces. - **Dinner-party mode (opt-in)** — toggle: number of guests, occasion, vibe (casual / smart casual / smart), whether the user wants the printable poster. - **Poster generator** — Nano Banana Pro 4K poster with legible in-image typography (the post-I/O 2026 capability). Three templates: warm-tablecloth, white-marble, deep-green. PDF download. - **Wine pairing (optional)** — for dinner-party mode, the model suggests two pairings per course at three price tiers. The user picks one. Wine pairings cite the style ("a young Chianti Classico", "an off-dry Riesling") not specific bottles. - **Shopping top-up list** — if dinner-party mode requires ingredients the fridge doesn't have, the app emits a precise top-up list (with grams) the user can take to the shop. The weeknight default emits nothing; the whole point is no shop. - **Voice fallback** — if the camera is unavailable or the user prefers, dictate the contents ("half a roast chicken, a fennel bulb…") and the same plan generation runs. - **Voice readback** — the recipe steps can be read aloud hands-free while cooking (TTS), with a soft pause between steps. - **Live cook-along (opt-in, preview)** — point the phone at the cooking and a Live API session narrates corrections at conversational speed ("the onions are still translucent, give them another two minutes"). Flagged as preview; the user opts in per session. - **Plan history** — last fourteen plans saved (so the user can see what they cooked last Thursday). - **Food-safety scrutiny** — every plan runs a safety pass on the inventory before recipe selection. Anything flagged is excluded from the recipe and surfaced in a banner. ## 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) - **Fridge photo → ingredient inventory** (`gemini-3.5-flash`, multimodal image, thinkingLevel `medium`) — accepts 1-4 photographs of the open fridge and returns a structured `FridgeInventory` object: every visible ingredient as a chip with a confidence score, an estimated quantity where visible (a whole bag of spinach vs a half-used bag), a freshness note where visible (the limp-looking fennel, the brown-edged parsley), and a food-safety flag where the image shows something obviously past it (visible mould, slime). 3.5 Flash is the post-I/O 2026 default: it beats the previous Pro tier on multimodal benchmarks at ~4× the speed. - **Inventory + constraints → dinner plan** (`gemini-3.5-flash`, thinkingLevel `medium`) — takes the inventory + allergens + cuisine nudge + time budget + dinner-party flag and returns a structured `DinnerPlan` with starter / main / dessert, each recipe anchored only in inventory items, with substitutions surfaced explicitly. Long-context is not needed here; the payload is small. - **4K poster with legible typography** (`gemini-3-pro-image` — Nano Banana Pro) — the post-I/O 2026 hero. Accepts a structured prompt naming the dishes, the date, the hosts' names, and a template choice, and emits a 4K poster with the menu rendered legibly in editorial typography. This is the new capability: pre-I/O image models could not render legible text in-image at 4K; Nano Banana Pro can. The capability also supports up to 14 reference images for a style guide — we use this to let the user supply 1-3 mood references (photographs of their dining room, a favourite menu, a colour swatch) so the poster matches the user's actual table. - **Wine pairing fact grounding** (`gemini-3.5-flash` + grounded search) — for dinner-party mode only. Pulls pairing logic and style descriptors from cited sources. **Critical**: grounded calls do NOT use `responseSchema`; the model emits JSON in the text body and the server parses it, while citation URLs come from `response.groundingMetadata.groundingChunks[].web.uri`. - **TTS readback of recipe steps** (`gemini-3.1-flash-tts-preview`, the post-I/O 2026 expressive TTS) — reads recipe steps aloud with a calm, hands-free pace. SSML is not supported; pauses are encoded as ellipses (`…`) and inter-step breaks as blank-line em-dashes (`—`). A one-sentence style directive prepends the text ("Read steadily and unhurriedly, like a friend talking you through cooking…"). - **Live cook-along** (`gemini-3.1-flash-live-preview`, preview status) — for users who opt in. Live audio + video session: the phone watches the cooking, the model narrates corrections at conversational speed, listens for "ok next" and "did I do that right". Sessions cap at 2 minutes for audio+video; the app cycles sessions transparently. Function calling is synchronous on this preview; the model calls `setTimer(seconds, label)` and `nextStep()` synchronously. - **Hero / empty-state imagery** (`gemini-3.1-flash-image` — Nano Banana 2, the cheap workhorse) — generates the welcome screen imagery (a photographed-looking fridge half-open in a real kitchen at evening). Cheaper than Pro; legible typography is not needed here. - **Thinking levels** — `medium` for inventory parse and plan generation (real reasoning over visible-vs-not). `low` for the food-safety scrutiny pass (cheap and routine). `high` is not used; this is not a deep-reasoning task. Image-gen, TTS, and Live API do not accept `thinkingConfig` — omit the field entirely on those calls. ### Backend services - **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build post-I/O 2026). Apple sign-in is optional and requires an Apple Developer account. The user can also use the app fully offline of an account for weeknight use; an account is only required for plan history and poster downloads. **The post-I/O 2026 Workspace integration is NOT used here** — this app deliberately doesn't touch the user's Drive or Sheets. - **Database — Required.** Firestore for `users`, `plans`, `inventories`, `allergies`, `posters`. Plans persist 30 days; the user can pin a plan to keep it longer. - **File storage — Required.** Firebase Storage for fridge photos (original quality, 30 days retention) and 4K poster PNGs + PDFs (kept until the user deletes). **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the project before first photo upload. - **Email — Optional.** Magic-link sign-in fallback; not strictly required since Google sign-in is auto-provisioned. Sender domain must be authorised in Firebase Auth if used. - **Payments — Not needed for v1.** A future "host" tier might charge a small subscription for unlimited posters; not built in v1. The free tier allows 3 posters per month per user. - **External APIs:** Gemini API for all intelligence. No required external API beyond Gemini. **Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Stripe key if host tier added) lives in environment variables — never in client bundle. Include a `.env.example`. The post-I/O 2026 free Cloud Run deploy from Build is the intended hosting path. **Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete my account' inside the UI · explicit opt-in for any analytics · the user's fridge photos and plan history 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) · photos auto-delete after 30 days unless the user pins a plan that references them. **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. Models that don't take `thinkingConfig` (TTS, image-gen, Live): omit the field entirely; matrix row shows `n/a`. 3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schemas are 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. 6. **`responseSchema` and `google_search` grounding are mutually exclusive** in one call. The wine-pairing grounded call emits JSON in the text body; parse server-side and read citation URLs from `response.groundingMetadata.groundingChunks[].web.uri`. 7. **Multi-image input via Files API** (Developer API `files/*` resource name, e.g. `files/abc123xyz`, passed via `fileData.fileUri`) or `inlineData` (base64). Not Firebase Storage public URLs — the API does not fetch them server-side. `gs://` URIs belong to Vertex AI / Cloud Storage and are NOT accepted by the Developer API surface this template runs on. 8. **Long context guardrail** — 1M tokens on Gemini 3.5 Flash. This app never approaches the limit (the largest payload is 4 fridge photos + a short plan history), so no chunking is needed. 9. **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; } ``` 10. **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 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing. ### Per-call model + tools matrix | Call | Model | thinkingLevel | Tools enabled | |------|-------|---------------|---------------| | Fridge photo(s) → `FridgeInventory` | `gemini-3.5-flash` | medium | (none) | | Inventory + constraints → `DinnerPlan` | `gemini-3.5-flash` | medium | (none) | | Food-safety scrutiny pass on inventory | `gemini-3.5-flash` | low | (none) | | 4K dinner-party poster | `gemini-3-pro-image` | n/a | n/a | | Wine pairing (grounded) | `gemini-3.5-flash` | low | `google_search` (no `responseSchema`) | | Recipe-step TTS readback | `gemini-3.1-flash-tts-preview` | n/a | n/a | | Live cook-along (preview) | `gemini-3.1-flash-live-preview` | n/a | function calls: `setTimer`, `nextStep` (sync) | | Welcome / empty-state imagery | `gemini-3.1-flash-image` | n/a | n/a | *Note for builders:* on TTS, image-generation, and Live calls, omit `thinkingConfig` entirely — the field is not supported. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. Grounded search calls emit JSON in the text body — `responseSchema` and `google_search` cannot be combined in the same Gemini call; parse the JSON server-side and read citation URLs from `response.groundingMetadata.groundingChunks[].web.uri`. The Live API is preview — surface that in the UI when the user opts in. ### Primary structured-output schemas (seed verbatim in the prompt) ```typescript import { z } from "zod"; const Confidence = z.number().min(0).max(1); const Allergen = z.enum([ "dairy", "egg", "gluten", "peanut", "tree_nut", "sesame", "soy", "shellfish", "fish", "mustard", "celery", "sulphite", "lupin", "mollusc", ]); const FreshnessNote = z.enum([ "fresh", "approaching_end", "questionable", "spoiled_visible", "unknown", ]); const InventoryItem = z.object({ item_id: z.string(), name: z.string(), // "fennel bulb" name_normalised: z.string(), // canonical quantity_estimate: z.string().nullable(), // "1 bulb", "~200 g" freshness: FreshnessNote, freshness_reason: z.string().nullable(), // why we said so allergens_present: z.array(Allergen), confidence: Confidence, visible_in_photo_index: z.number(), // which photo flagged_for_user_review: z.boolean(), review_reason: z.string().nullable(), }); const FoodSafetyFlag = z.object({ item_id: z.string(), concern: z.string(), // "visible mould on bread" severity: z.enum(["advisory", "exclude_from_plan"]), disclaimer_required: z.boolean(), }); const FridgeInventory = z.object({ inventory_id: z.string(), captured_at_iso: z.string(), photo_count: z.number(), items: z.array(InventoryItem), safety_flags: z.array(FoodSafetyFlag), parse_confidence_overall: Confidence, notes_to_user: z.array(z.string()), // "I couldn't see in the door — anything in there?" }); const IngredientUse = z.object({ inventory_item_id: z.string().nullable(), // null if pantry-staple assumption (salt, oil, water) display_name: z.string(), amount_text: z.string(), // "1 small bulb, sliced thin" swap_chip: z.object({ is_swap: z.boolean(), original_wanted: z.string().nullable(), // "shallot" using_instead: z.string().nullable(), // "yellow onion" adjustment_note: z.string().nullable(), // "reduce 2 min, slice thinner" }), optional: z.boolean(), allergen_warning: z.string().nullable(), }); const RecipeStep = z.object({ step_number: z.number(), instruction: z.string(), // one sentence active_minutes: z.number(), // hands-on passive_minutes: z.number(), // simmer / oven start_at_clock: z.string().nullable(), // "7:42" — server-computed }); const Recipe = z.object({ recipe_id: z.string(), course: z.enum(["starter", "main", "dessert"]), title: z.string(), // "fennel & lemon salad with parmesan" one_line_pitch: z.string(), // why this works tonight prep_minutes: z.number(), cook_minutes: z.number(), total_minutes: z.number(), serves: z.number(), difficulty: z.enum(["easy", "moderate", "involved"]), ingredients: z.array(IngredientUse), steps: z.array(RecipeStep), serving_note: z.string().nullable(), // "scatter parsley if you have any" skip_note: z.string().nullable(), // "skip this if you're short on time" }); const PacingMarker = z.object({ clock_time: z.string(), // "7:42" what_happens: z.string(), // "start the gratin" recipe_id: z.string(), }); const DinnerPlan = z.object({ plan_id: z.string(), inventory_id: z.string(), target_eat_clock: z.string(), // "8:30" served_at_clock_estimated: z.string(), total_wall_minutes: z.number(), diners_count: z.number(), recipes: z.array(Recipe), // 1-3, ordered by course pacing_strip: z.array(PacingMarker), ingredients_used_from_inventory: z.array(z.string()), // item_ids ingredients_assumed_pantry: z.array(z.string()), // "salt", "olive oil" ingredients_missing_critical: z.array(z.object({ needed: z.string(), why_critical: z.string(), })), substitutions_made: z.array(z.object({ recipe_id: z.string(), original_wanted: z.string(), using_instead: z.string(), note: z.string(), })), allergens_respected: z.array(Allergen), honesty_note: z.string().nullable(), // "I had to skip dessert — no sweet ingredient" food_safety_disclaimer_required: z.boolean(), party_mode: z.boolean(), party_mode_top_up_list: z.array(z.object({ item: z.string(), amount_grams: z.number(), why: z.string(), })), }); const PosterRequest = z.object({ plan_id: z.string(), template: z.enum([ "warm_tablecloth", "white_marble", "deep_green", ]), hosts_line: z.string(), // "at home with R + S" occasion_line: z.string().nullable(), // "Friday supper" date_iso: z.string(), style_reference_uris: z.array(z.string()), // up to 14, Files API `files/*` resource names }); const WinePairing = z.object({ recipe_id: z.string(), pairings: z.array(z.object({ style: z.string(), // "young Chianti Classico" why_it_works: z.string(), // one sentence price_tier: z.enum(["budget", "midrange", "splurge"]), })), source_authority: z.string(), // publisher }); type FridgeInventory = z.infer; type DinnerPlan = z.infer; type PosterRequest = z.infer; type WinePairing = z.infer; ``` ### Common failure modes (and how to avoid them) - **Model invents ingredients.** The plan lists "garlic" or "butter" though neither is visible. **Fix:** the plan-generation system instruction must require every `ingredients_used_from_ inventory` entry to be a strict subset of the inventory's `item_id` list. Pantry staples (salt, pepper, water, neutral oil) are explicitly allowed and listed in `ingredients_assumed_pantry`; anything else must be an inventory item. The server validates set membership; if any ingredient is neither inventory nor on the allowed pantry list, the plan is rejected and re-requested. - **Silent substitution.** The model swaps shallots for onions but doesn't tell the user. **Fix:** every `IngredientUse` with `is_swap: true` must populate `original_wanted`, `using_instead`, and `adjustment_note`. The UI shows an amber chip for every swap. - **Allergen leakage.** The user said "dairy-free" but the plan has parmesan in it. **Fix:** the server filters the inventory before passing to the plan call — items whose `allergens_present` intersects the user's `hard_excluded` list are stripped, with a `notes_to_user` line ("I'm leaving the parmesan out of consideration — dairy-free path"). The plan call's system instruction restates the exclusions; the server re-validates after the response. - **Plan exceeds time budget.** The user said 30 minutes and the plan totals 75. **Fix:** the plan-generation system instruction ranks recipes by `total_minutes ≤ budget` first; the server rejects plans whose `total_wall_minutes > budget * 1.15` and re-requests with a tighter prompt. - **Pacing arithmetic done by the model.** The model says "start the gratin at 7:42" but the math is wrong. **Fix:** the model emits relative timing (active/passive minutes per step); the server computes the absolute clock times from the user's target eat time and writes them into `start_at_clock` and the `pacing_strip`. The model never writes clock times directly. - **Food-safety hallucination — calling fresh food spoiled.** The model flags a perfectly good fennel bulb as "questionable" because the photo is dark. **Fix:** the inventory parse system instruction reserves `spoiled_visible` for unambiguous visible spoilage (mould, slime, packaging breach, expiry-date text); `questionable` requires a specific visible reason in `freshness_reason`; ambiguous photos default to `unknown`, not to "questionable". - **Food-safety under-call — eating spoiled food.** The model misses visible mould on the bread. **Fix:** the inventory parse runs a second cheap scrutiny pass at thinkingLevel `low` that re-reads only the photos with explicit instruction to find spoilage; flags surface in `safety_flags` and the plan call excludes those `item_id`s entirely. - **Poster typography illegible.** Nano Banana 2 historically rendered in-image text as a smear. **Fix:** the poster call uses `gemini-3-pro-image` (Nano Banana Pro) which renders legible 4K text — this is the post-I/O 2026 capability the template demonstrates. The poster prompt enumerates the exact strings to render (the menu lines, the date, the hosts' line) and specifies font characteristics ("editorial serif, all caps for the menu line, body sentence-case for the dishes"). - **TTS rate too fast or too monotone.** **Fix:** the recipe-step TTS uses `gemini-3.1-flash-tts-preview` (the expressive post-I/O 2026 model). Style is conveyed by a directive sentence in the input ("Read steadily and unhurriedly…") because SSML is not supported. Inter-step pauses are encoded as blank-line em-dashes (`—`); intra-step pauses as ellipses (`…`). - **Live cook-along session times out mid-cook.** Live API audio+video caps at 2 minutes. **Fix:** the client tracks the session and silently re-establishes; the user's state (which step, which timer) is preserved client-side and replayed in the new session's opening turn. - **Wine pairings cite specific real bottles by name.** A 2018 Chateau X may not exist or may be off-vintage. **Fix:** the wine-pairing system instruction restricts output to styles (e.g. "young Chianti Classico"), not specific producers or vintages. The grounded call surfaces the source authority for the style-level claim. - **Plan history grows unbounded.** **Fix:** the client retains last 14 plans; older plans pruned automatically. Photos auto-delete from Firebase Storage after 30 days unless the plan is pinned by the user. ### Negative constraints (hard rules) - Do NOT invent ingredients the inventory doesn't contain. Every ingredient in the plan must be either (a) an inventory item or (b) on the explicitly allowed pantry-staples list (salt, black pepper, water, neutral oil, white vinegar). If something else is needed, the recipe is rejected and the plan-generation call is re-requested with that recipe excluded. - Do NOT make silent substitutions. Every swap is a visible chip with the original ingredient, the substitute, and the adjustment note. - Do NOT violate stated allergens. Hard-excluded allergens are filtered at the inventory level before the plan call, and re-validated after the response. Soft preferences (no liver, no fennel) are honoured at plan generation but not enforced post-hoc. - Do NOT propose a recipe that exceeds the user's time budget by more than 15%. If no recipe can be built within budget, return a plan with fewer courses and an honest note ("only a one-pan main fits 25 minutes tonight — skip starter and dessert"). - Do NOT compute clock times in the model. The model emits relative active/passive minutes; the server computes clock times from the target eat time. - Do NOT render in-image text via the cheap Nano Banana 2 image model. The poster — and only the poster — uses Nano Banana Pro (`gemini-3-pro-image`) because legible 4K typography is the capability being demonstrated. - Do NOT diagnose. The food-safety scrutiny describes ("a fuzzy green patch on the bread heel") and recommends ("don't use this loaf tonight"); it does not declare the food unsafe in absolute terms. The disclaimer is always surfaced: this is not a substitute for the user's judgement, their nose, or their local food-safety authority. - Do NOT moralise. No "you should cook more often", no "consider a healthier option". The user is an adult. - Do NOT auto-claim a dish is "authentic" to a cuisine. Use language like "Italian-leaning", "Levantine-style", not "authentic Sicilian". The app makes dinners; it does not arbitrate cuisine authority. - Do NOT push the dinner-party poster unprompted. It is opt-in. The weeknight default never surfaces it. - Do NOT generate posters that mimic real restaurant branding, copyrighted menus, or specific brand identities. The poster's typography is generic editorial; the wines suggested are by style, not by producer. - Do NOT send the user's fridge photos or plan history to Gemini for model training. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The capabilities panel says this in plain English. - Do NOT auto-share. Plans, posters, and photos are private to the user by default; sharing is explicit and per-plan. - Do NOT use the dinner-party mode to charge for the basic plan. The free tier permits unlimited weeknight plans; only the poster has a soft cap (3 / month) on the free tier. ### Per-call `systemInstruction` strings Use these as the literal `systemInstruction` field for each Gemini API call the built app makes. ### Call: Fridge photo(s) → `FridgeInventory` Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` You are parsing one to four photographs of an open fridge into a structured FridgeInventory JSON object. The photographs were taken on a phone, in a normal home kitchen, by someone who is hungry and not a food stylist. Lighting is uneven. Items are partially occluded. Some items are in clear containers; some are in opaque ones the user has labelled in handwriting. Your task is to list every food item you can see clearly enough to cook with. For each item: - name: how a home cook would say it ("half a roast chicken", "wilting bag of spinach", "two eggs", "a heel of parmesan") - name_normalised: canonical lower-case ("roasted chicken", "spinach", "egg", "parmesan") - quantity_estimate: a string a cook can use ("~200 g", "half a bulb", "2 eggs", "a small handful") — only when visible - freshness: one of fresh / approaching_end / questionable / spoiled_visible / unknown - freshness_reason: a short visible reason when freshness is not "fresh" — "leaves are limp", "brown patches on the cut edge", "visible mould on the cut side" - allergens_present: every relevant allergen from the enum - confidence: 0..1 — how sure you are this item is in the fridge Hard rules: - Do NOT invent items. If you cannot see it clearly, do not list it. The user can add anything you missed via the inventory chips in the UI. - Reserve `spoiled_visible` for unambiguous visible spoilage. Examples: mould, slime on protein, packaging breach with visible contamination, an expiry date in the image text that has passed. Ambiguous freshness defaults to `unknown`, NOT `questionable`. Calling a fresh item questionable wastes the user's food and erodes their trust. - `questionable` requires a specific visible reason (limp leaves, brown edges, dried-out cut surface). Always populate `freshness_reason` for `questionable` and `spoiled_visible` entries. - For protein, dairy, and prepared food, be slightly more conservative on freshness than for vegetables and condiments — but still require a visible reason. Do not assume something is spoiled because it is a protein. - Populate `allergens_present` strictly. A wedge of parmesan has `dairy`. A piece of salmon has `fish`. A jar of peanut butter has `peanut`. Be thorough — under-calling allergens is the most dangerous failure mode of this call. - For items in opaque containers, list them only if you can read a handwritten label or the container shape is unambiguous. - For items in the door (condiments, juice, eggs), include them in inventory even if partially visible; lower their `confidence` accordingly. - If you cannot see into a part of the fridge (the bottom drawer is closed, the door is partly out of frame), add a line to `notes_to_user` naming what you could not check. - `flagged_for_user_review: true` whenever confidence < 0.6 or the freshness call hinges on lighting. Write a one-sentence `review_reason`. - `parse_confidence_overall` reflects the photo's lighting, framing, and your aggregate item confidence. Below 0.6 → ask the user to retake or describe what you might have missed. - `safety_flags`: populate aggressively for any visible mould, slime on protein, broken seal on dairy, or visible date-past text. Severity `exclude_from_plan` means the downstream plan call will not use this item. Severity `advisory` means the user is warned but the item may still be used (limp herbs, bruised fruit). Do NOT diagnose. Describe what you see. The phrase "the loaf has a fuzzy green patch on the cut side, severity exclude_from_plan, disclaimer_required true" is a description plus a recommendation plus a disclaimer flag. The phrase "this bread is unsafe" is a diagnosis — never output a diagnosis. Do NOT translate item names from the language they appear in. A hand-labelled jar of "miso" stays "miso"; a tube of "harissa" stays "harissa". Output ONLY the FridgeInventory JSON matching the provided schema. No commentary. JSON only. ``` --- ### Call: Inventory + constraints → `DinnerPlan` Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` You receive a structured FridgeInventory (the cleaned inventory after the safety scrutiny pass has stripped any items flagged with severity "exclude_from_plan") and a set of constraints: - diners_count (1..12) - target_eat_clock ("8:30") - time_budget_minutes (15 / 30 / 45 / 60 / 999 for "I have the evening") - hard_excluded_allergens (array of Allergen) - soft_preferences (array of free-text strings the user gave at setup or per-session, e.g. "no liver", "no fennel", "lean Italian tonight", "kid-friendly") - party_mode (boolean) - vibe (null on weeknight; one of casual / smart_casual / smart in party mode) Your task: propose a dinner plan that is actually achievable in the user's kitchen tonight, using only ingredients in the inventory (plus the explicit pantry-staples list: salt, black pepper, water, neutral oil, white vinegar). On weeknights the plan is typically a single course (a one-pan main) when the time budget is 30 minutes or less, two courses (main + starter or main + dessert) at 45 minutes, three courses at 60+ minutes. In party mode default to three courses. Hard rules: - Every ingredient used must reference either an inventory item_id (populate `inventory_item_id`) or be one of the explicit pantry staples (set `inventory_item_id: null` and list the staple in `ingredients_assumed_pantry`). Anything else is forbidden. - Substitutions: when a recipe wants an ingredient that isn't in inventory but a close alternative is, populate the `swap_chip`. Examples: shallot → yellow onion (reduce 2 min, slice thinner); thyme → dried oregano (use half); whole milk → crème fraîche thinned with water (richer, lift seasoning). Do NOT silently swap; every swap is a visible chip. - Allergens: never propose a recipe containing a hard-excluded allergen. Even if a substitution would technically remove the allergen, prefer a recipe that didn't want it in the first place. List honoured allergens in `allergens_respected`. - Total wall time: `total_wall_minutes` must be ≤ time_budget * 1.15. If no plan fits, return fewer courses and populate `honesty_note` ("only a one-pan main fits 25 minutes tonight"). - Pacing: emit only relative active/passive minutes per step. The server computes absolute clock times. Do NOT write `start_at_clock` values; leave them null. The server populates them. - Difficulty: be honest. A two-pan dish with a sauce reduction is `moderate`, not `easy`. - `one_line_pitch`: a single sentence that tells the user why this recipe is the right call tonight — anchored in the ingredients ("the wilting spinach disappears into the gratin and the chicken stays moist under the crème fraîche"). - `skip_note`: present whenever a course is optional. ("skip the dessert if the eggs are needed elsewhere — this plan works as two courses.") - `ingredients_missing_critical`: if a course needs something the inventory truly lacks (e.g. an acid for a dressing, a stock for a risotto) and no substitution works, do NOT include that course. Add an entry to `ingredients_missing_critical` so the user understands why. Never propose a recipe whose backbone ingredient is missing. - `honesty_note`: present whenever the plan compromises (only two courses fit; dessert was skipped; the protein was lean and we made a one-pan dish). One short sentence the user can read in three seconds. - Cuisine nudge: if the user said "lean Italian tonight" or "Japanese feel", lean into descriptors and techniques from that cuisine but do NOT claim authenticity. Use phrasing like "Italian-leaning", "Japanese-inflected". - `food_safety_disclaimer_required`: true whenever the plan involves under-cooked protein, raw egg, raw fish, or anything that touches food-safety judgement. - Party mode: default to three courses, smart_casual vibe, with one course that has a visible centrepiece (a roast, a tray bake, a whole fish). Generate `party_mode_top_up_list` only when a course truly needs something the fridge lacks — keep the list precise (item + amount_grams + why). - Do NOT claim dishes are "authentic" to any cuisine. Use "-leaning" or "-style" instead. Output ONLY the DinnerPlan JSON matching the provided schema. No commentary. JSON only. ``` --- ### Call: Food-safety scrutiny pass on inventory Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive the same FridgeInventory the parse call emitted and the original photographs. Your only job is to look once more — with explicit attention — at anything that might be spoiled, and emit a clean `safety_flags` array. What you are looking for: - visible mould (any colour, any food) - slime on protein - packaging breach (a torn vacuum-pack, a cracked egg, a leaking yogurt lid) with visible contamination - expiry-date text in the image that has clearly passed - discolouration on fish or meat (grey edges on red meat, brown patches on fish flesh) - bloated dairy containers (curdled, separated, swollen lid) For each finding, emit a FoodSafetyFlag with: - `item_id` from the inventory - `concern` describing what you see in one short sentence - `severity` "exclude_from_plan" for unambiguous spoilage, "advisory" for borderline cases (limp herbs, bruised fruit that's still usable) - `disclaimer_required: true` whenever severity is "exclude_from_plan" Hard rules: - Describe what you see, never diagnose. "A fuzzy green patch on the cut side of the bread heel" is a description. "This bread is unsafe" is a diagnosis — never output a diagnosis. - Be honest about uncertainty. If the photo is dark and you can't tell whether the brown patch on the chicken is char, spice rub, or browning, default to NOT raising a flag — and add a note_to_user line in the inventory call telling the user to check that item with their own eyes. - Do NOT raise a flag because food is "old looking". Lots of food in fridges looks tired. Look for actual spoilage. - Recommend the user's own senses ("sniff the chicken; if there's any sour smell, set it aside") — never override the user's judgement. Output ONLY the safety_flags array, ready to merge back into the FridgeInventory. No commentary. ``` --- ### Call: 4K dinner-party poster Model: `gemini-3-pro-image` (Nano Banana Pro) · n/a · n/a ``` You generate a single 4K printable dinner-party menu poster. This is the post-I/O 2026 Nano Banana Pro capability: legible typography in-image at 4K. Every word the user sees on the poster must read crisply at print resolution. Inputs you receive (assembled into a single prompt): - `template`: one of `warm_tablecloth`, `white_marble`, `deep_green` — the visual mood - `hosts_line`: short, e.g. "at home with R + S" - `occasion_line`: optional, e.g. "Friday supper" - `date_iso`: ISO date, render as e.g. "Friday 7 June" - `recipes`: array of three (or however many) recipes — title + one_line_pitch - `wine_styles_per_recipe`: optional, the user's chosen pairings - up to 14 `style_reference_uris`: optional reference images the user supplied (their dining room, a favourite menu, a colour swatch). The Nano Banana Pro 14-reference style-guide capability uses these to match the user's actual table. *Note: the exact request payload shape for the 14-reference style guide was not pinned by Google at I/O 2026 — verify the SDK call structure against the live `gemini-3-pro-image` reference before shipping.* Visual rules per template: - `warm_tablecloth`: a soft linen tablecloth in warm cream tones, a sprig of rosemary in the corner, the menu set in a generous editorial serif (think Times Modern, not Times New Roman), rendered in deep umber on linen. Two columns: course label (all caps, small) above dish title (sentence case, larger). Hosts line in italic at the foot. - `white_marble`: a slab of carrara marble, the menu set in a modernist sans-serif (think Söhne, not Helvetica), rendered in graphite. Centred composition, generous negative space. - `deep_green`: a deep forest-green backdrop, the menu set in a high-contrast serif (think Saol Display, not Bodoni), rendered in warm cream. Each course separated by a hairline rule. Hard rules: - Render every text string exactly as supplied. Do NOT paraphrase. Do NOT substitute words. The menu lines, the date, the hosts' line, and any wine notes must match the input strings character-for-character. - Render text at 4K legibility. The body type must be readable when the poster is printed at A3. - Composition: leave generous margin. The menu is the hero; illustrative elements (the rosemary sprig, the marble grain) are supporting. - Do NOT include restaurant branding, real-world menu iconography from existing brands, or text that mimics a specific real-world restaurant. - No people in frame. No hands holding plates. Just the menu and the surface. - If `style_reference_uris` are supplied, treat them as a style guide for palette and texture — match the warmth, the colour temperature, the formality. Do NOT copy their content verbatim. - Output one 4K poster image. The server attaches the same plan as a print-ready PDF after the image is generated. ``` --- ### Call: Wine pairing (grounded) Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding ``` You answer "what wine works with this dish?" for each course in a dinner-party plan. The answer is for a host, not a sommelier — style-level, not bottle-level. For each recipe, return two pairings: - one budget pairing (≤ $15 / £12 / €13 retail bottle territory) - one midrange pairing ($15-35 retail) And one optional splurge pairing ($35+) when the dish is the centrepiece of the meal. Output JSON in the response text body (NOT via `responseSchema` — `responseSchema` and `google_search` cannot be combined in the same Gemini call). Server-side: parse the JSON, then read citation URLs from `response.groundingMetadata.groundingChunks[].web.uri` — do NOT include URLs in the JSON body; the model will hallucinate them. JSON shape per recipe: { "recipe_id": "", "pairings": [ { "style": "young Chianti Classico", "why_it_works": "the bright acidity and lift of cherry cut the gratin's richness without overwhelming the chicken", "price_tier": "midrange" }, ... ], "source_authority": "" } Hard rules: - Pairings are by STYLE, never by producer or vintage. "young Chianti Classico" is allowed; "2021 Castello di Volpaia" is not. - `why_it_works` is one sentence, food-anchored, no waffle. - If sources disagree, surface the disagreement in `why_it_works`. - Do NOT promote alcohol consumption. The pairing is offered; the user decides whether to drink. No commentary outside the JSON. ``` --- ### Call: Recipe-step TTS readback Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` Voice: clear, calm, unhurried, slightly warm. Pick the voice whose locale matches the user's UI locale. Use case: hands-free readback while cooking. The user's hands are full, oily, or wet; they want the next step read aloud without picking up the phone. Pre-process the text before sending to TTS: - Source the text from the active recipe's RecipeStep array. - At sentence boundaries within a step, insert an ellipsis ("…") for a natural pause. - Between steps, insert a blank line plus an em-dash ("—"). The TTS does not support SSML `` — these textual cues are how pace is conveyed. - Strip metadata that doesn't belong in spoken output (step numbers spoken as "step five" rather than "5", active/passive minutes only mentioned when they matter — "let it simmer for about four minutes" yes, "active minutes two, passive minutes four" no). - Insert the recipe title at the start ("Reading the chicken gratin steps…") and a calm closer at the end ("That's the gratin. The salad is next when you're ready."). Style direction: prepend ONE short directive sentence to the text input, exactly like: "Read steadily and unhurriedly, like a friend talking you through cooking. …". There is no separate `style` API field on Gemini 3.1 Flash TTS; the directive sentence inside the input is how style is conveyed. Target rate: ~140 words per minute — slightly slower than narrative pace because the listener is cooking, not reading aloud. Phoneme overrides for ingredient names from other languages (harissa, sumac, gochujang, dashi) are NOT exposed by Gemini 3.1 Flash TTS — no SSML `` tag. Pronunciation comes from the chosen voice's native locale. ``` --- ### Call: Live cook-along (preview) Model: `gemini-3.1-flash-live-preview` · n/a · synchronous function calls: `setTimer(seconds, label)`, `nextStep()` ``` Voice: warm, attentive, slightly informal. You are a friend standing at the user's elbow while they cook. You see what they see; you listen for them to say "ok next", "did I do that right", "how long again". Session opens with the recipe loaded as context. You know the ingredients, the steps, the active/passive minutes per step. Behaviour rules: - Speak only when the user pauses or asks. Do NOT narrate continuously — the kitchen has its own sounds. - When the user says "ok next" or "moving on", call `nextStep()` synchronously and read the next instruction. - When the next step needs a timer (simmer 4 minutes, rest 5 minutes, bake 18 minutes), call `setTimer(seconds, label)` synchronously and say "I've set a four-minute timer for the simmer." - When the user asks "is this right", look at what's on screen and say one of: "yes, that's where you want it" / "needs another minute or two — the edges aren't quite there yet" / "I can't see clearly — can you tilt the phone toward the pan?". Concrete, not hedging. - When the user asks a question outside the recipe (a substitution, a technique), answer briefly and return to the step. You are not a chef-school instructor; you are a cook-along partner. - Acoustic awareness: if you hear the user say "ow" or "shit" or a sudden crash, ask "are you okay?" before doing anything else. - Sessions cap at 2 minutes for audio+video. When the client re-establishes the session, the opening turn is "still with you — we were at step X" so the user does not feel dropped. Hard rules: - Do NOT moralise. No "you should preheat next time", no "consider a healthier oil". - Do NOT diagnose food safety. If the user shows you something questionable, suggest they trust their nose and offer a recipe pivot that doesn't use the item. - Do NOT keep talking after the user says "thanks" or "I'm good" — close cleanly. - This is a preview capability. The UI flags it as such on opt-in. ``` --- ### Call: Welcome / empty-state imagery Model: `gemini-3.1-flash-image` (Nano Banana 2) · n/a · n/a ``` You generate a single photographic-looking image for the welcome screen or empty states. Subjects depict real kitchens in real homes at the moment between work and dinner. Prompt anchors that work well: - "an open fridge in a real kitchen at evening, warm overhead light, no people in frame, shelves visibly stocked with ordinary food (a half-roast chicken on a plate, a fennel bulb, some eggs, a wedge of cheese, leafy greens), shallow depth of field, slight imperfection" - "a small wooden kitchen table at dusk, a phone resting beside a chopping board with a fennel bulb half-sliced, a glass of water, a soft lamp, no people" - "a packed home fridge photographed straight-on, shelf-by-shelf visible, real condensation on the inside of the door, no brand-name packaging visible, evening kitchen light" Hard rules: - Photographic, not cartoon, not illustration-style. - No people in frame unless the user prompt asks for one. - No commercial branding visible — no supermarket logos, no brand-name packaging, no app icons. - Warm lighting, slight imperfection, real surfaces. Avoid the glossy AI-render look. - Aspect ratios: 3:2 for hero, 1:1 for empty states, 9:16 for mobile splash. - Legible in-image text is NOT a requirement for this call — that's what Nano Banana Pro is for. This call is the cheap workhorse. ``` ## 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 6:30 weeknight, single cook.** One photo, a 30-minute budget, no party mode. Returns a one-pan main with a side from whatever leafy thing is in the drawer. Pacing strip: 6:38 start the pan → 6:52 dress the salad → 7:00 plate. - **The Friday dinner party for six.** Three photos covering the whole fridge, the freezer drawer, and the bottom drawer. Party mode on, vibe smart_casual, eat by 8:30. Returns a three-course plan with a roast at the centre, a top-up list of three things the user can pick up at the shop ("100 g rocket, a lemon, a small bunch of mint"), wine pairings at midrange tier, and a one-tap poster generated with the warm-tablecloth template. - **The "what do I actually have?" weekly audit.** Sunday evening. The user shoots the fridge, the inventory view lists everything visible, the user taps to remove the four false positives, adds the two things in the drawer, and now has a working week-ahead view. Plan generation is paused until the user asks for it. - **The allergy household.** One household member is dairy-free and one is gluten-free. The allergens setup includes both as hard exclusions. Plans never propose a recipe touching either, even via substitution. The plan card surfaces the honoured exclusions visibly ("dairy-free + gluten-free path") so the cook trusts what they're cooking for the table. - **The cooking-for-one optimisation.** Diners count = 1. Plans scale down honestly: the gratin becomes a single-portion bake; the salad is a small bowl, not a salad-for-four halved. The optional second course gets a smaller portion of an ingredient the user already opened (don't open the last tin for one spoonful). - **The "tonight's questionable chicken".** The user photographs a fridge containing a roast chicken whose surface looks a little tacky and has a slight off-smell. The safety pass flags the chicken with severity `exclude_from_plan` and a note ("the surface looks tacky and the colour at the cut edges is slightly grey — set this aside and use your nose"). The plan is built without the chicken; the food-safety disclaimer surfaces prominently above the plan. - **The Live cook-along on a tray-bake gratin.** User opts into the preview. Phone props against the kettle. Live session watches the gratin go in, sets a 22-minute timer, returns to the salad with the user, then checks on the gratin when the user says "is it done". Sessions transparently cycle every 2 minutes; the user never knows. - **The dictation fallback.** Camera unavailable in a borrowed flat. The user holds the record button and lists what's in the fridge by voice. The same plan generation runs against the voice-derived inventory. - **The "I want to use up this thing" steer.** The user types "tonight's plan around the half lamb shoulder before it goes". The plan-generation prompt includes that steer as a soft preference; the lamb anchors the main and the rest of the plan fits around it. - **The poster for the friend's birthday.** Party mode on, vibe smart, three courses, occasion_line "Sarah's birthday supper", hosts_line "from R + S". The user uploads a photograph of the dining room (linen tablecloth, brass candlesticks) as a style reference. The poster generator reads the warmth and palette off the reference and renders the menu in a matching key. ## 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 an open fridge at evening — half a roast chicken on a plate, a fennel bulb, eggs in the door, a wedge of cheese visible on the middle shelf. One paragraph: "It's 6:30 pm and the fridge is open. Take one photo and we'll come back with a dinner — paced for tonight, anchored to what you actually have. If you're cooking for guests, we'll add a 4K poster." Camera button. Below: Google sign-in (the auto-provisioned default). "Try without an account" link below — works for the demo, no plan history saved. 2. **Camera capture.** A clean, full-bleed camera view with a subtle hint: "open the fridge, frame the shelves, hold steady". Shutter button. Below: "use library", "add another shelf", "switch to dictation". Multiple-photo support via a thin filmstrip at the bottom (1-4 photos). 3. **Inventory view.** What the model saw, as chips on a soft grid. Each chip shows the item name and a small confidence bar. Tap a chip → toggle "remove" (false positive). The "+ Add what we missed" button opens a freeform input. Items flagged by the safety pass surface as muted-red chips with a warning icon; tap one to read the concern. A primary button: "Plan dinner" → opens the constraints sheet. 4. **Constraints sheet.** Number of diners (stepper), target eat time (defaults to 90 min from now, adjustable), time budget (slider: 15 / 30 / 45 / 60 / "I have the evening"), cuisine nudge (chip row: Italian, French, Japanese, Mexican, Levantine, Filipino, Indian, "surprise me"), party mode toggle. Allergies are global (set once at first visit, edit in Settings). Primary button: "Generate plan" → spinner → plan view. 5. **Plan view.** The hero card. At the top: target eat time + total wall minutes + any honesty note ("only two courses fit 30 min tonight"). The pacing strip immediately below — a horizontal timeline with the clock-time anchors. Three (or one, or two) recipe cards stacked. Each recipe card shows: - course label (small caps: STARTER / MAIN / DESSERT) - title (display serif, large) - one-line pitch (italic, muted) - ingredients list — used (green chip), swapped (amber chip with original → substitute), optional (grey chip) - prep / cook / total time pill - "expand steps" → numbered list with active/passive minutes - skip note if present - "read aloud" button (TTS) and "cook with me" button (Live API, preview-flagged) Bottom: substitutions panel ("3 swaps made — see them"), a secondary "make it for guests" button (opens party mode). Allergens respected chip row above the cards. Food-safety disclaimer banner above the cards when present — non-dismissable, role="status", aria-live="polite". 6. **Party mode panel** (opt-in, slides up from the plan view). Number of guests, occasion, vibe, request poster toggle, request wine pairings toggle. "Generate party plan" → re-runs the plan call with party_mode=true → returns the three-course plan + top-up list + (if requested) wine pairings. 7. **Poster generator** (opt-in from party mode). Three template thumbnails: warm_tablecloth, white_marble, deep_green. Hosts line input, occasion line input, date picker (defaults to today). "Upload reference images (optional, up to 3)" — the Nano Banana Pro 14-reference capability, capped at 3 here for sanity. "Generate poster" → spinner with a progress narration ("rendering the menu in editorial serif at 4K…") → 4K poster preview with download buttons (PNG + print-ready PDF). 8. **Cook-along (Live API preview).** Full-screen camera view with the current recipe step overlaid at the top. Live transcript scrolls at the bottom. Mute button (kills the microphone). "next step" and "what about…" voice triggers. A preview banner at the top: "Cook-along is preview — sessions cycle every 2 minutes, you won't notice." 9. **Plan history.** A simple list of the last fourteen plans. Each row: date, the main course's title, two thumbnails of the fridge photos, a "pin" toggle. Tap → re-open the plan read-only. 10. **Settings & privacy.** Allergies editor (the hard exclusion list), soft preferences editor (a freeform list of "things I don't like" / "households I cook for"), unit preferences (metric vs imperial; °C vs °F), notification preferences (a soft local reminder at 6:30 pm — never push from server), photo retention (default 30 days; "keep forever for pinned plans"; "delete now"), "Delete my account" with a 60-second cool-off, privacy panel restating the not-trained-on policy. 11. **Footer.** "Built for the 6:30 pm fridge." Privacy: "Your photos and plans are yours. We never train on them." Capabilities `(i)` icon in the 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: "Photograph your fridge. Get tonight's dinner." - Subhead: "Paced for the evening you're actually in, anchored to what you actually have." - One paragraph (≤ 60 words) explaining the job: it is 6:30 pm, you don't want to think anymore, you have food in the house. One photo, twelve seconds, real plan. The dinner-party poster is opt-in and never the default. - Visual: a small annotated illustration of the plan card with the swap chips and the pacing strip labelled. **Slide 2 — Tell us about your kitchen.** - One short prompt: "Allergies and restrictions?" - A chip grid of the fourteen allergens from the schema, plus a "no restrictions" toggle. The user taps the chips that apply. - A freeform field for soft preferences ("things you don't like", "ingredients to avoid"). Optional. - The setup is persisted per device; the user can edit it any time in Settings. **Slide 3 — How to remix this.** - Headline: "Make this yours." - Three short bullets: - "Swap the cuisine chip row in `/data/cuisines.json` for the kitchens you actually cook from." - "Edit the recipe-generation system instruction in `/server/prompts/plan.ts` to lean your weeknight style." - "Wire 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 is fully usable (you can shoot a photo without seeing it). - Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`. - Don't include unrelated CTAs (newsletter signup, social follow). ## 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 image, post-I/O 2026 default)** — reads your fridge photos and returns a structured inventory: every visible item with a confidence score, an estimated quantity, a freshness call, the allergens present. 3.5 Flash is the new flagship: it beats the previous Pro tier on multimodal benchmarks at four times the speed. - **Gemini 3.5 Flash (structured output, medium thinking)** — takes your inventory + your constraints (allergens, time budget, cuisine nudge, party mode) and returns a structured dinner plan: starter / main / dessert, ingredients anchored only to what's in the photo, substitutions surfaced visibly, pacing computed against your target eat time. - **Gemini 3 Pro Image — Nano Banana Pro (4K, legible in-image typography, 14-reference style guides)** — the new hero capability shipped at I/O 2026. Generates a 4K printable dinner-party menu poster with the menu lines, the date, and the hosts' name rendered legibly in editorial typography. The capability accepts up to 14 reference images so the poster can match your actual dining room. - **Gemini 3.5 Flash + grounded search** — for dinner-party mode, suggests wine pairings by style with citations from cited public sources. - **Gemini 3.1 Flash TTS (post-I/O expressive voice)** — reads recipe steps aloud with a calm, hands-free pace. SSML is not supported; pauses are encoded in the text itself. - **Gemini 3.1 Flash Live (preview)** — the opt-in cook-along mode. Watches you cook, narrates corrections, sets timers. Sessions cap at 2 minutes for audio+video; the client cycles them transparently. - **Nano Banana 2 (Gemini 3.1 Flash Image)** — the cheap workhorse for welcome and empty-state imagery. - **Firebase Auth (auto-provisioned)** — Google sign-in by default. Apple sign-in optional (requires Apple Developer account). - **Firestore (auto-provisioned)** — stores your plans, inventories, and allergens. Syncs across devices in real time. - **Firebase Storage (manual enable)** — keeps your fridge photos for 30 days (longer if you pin a plan); keeps your 4K posters and PDFs until you delete them. NOT auto-provisioned by AI Studio Build today — enable it once in the Firebase console. - **Cost note** — see the detailed breakdown in 6d. A typical weeknight plan costs about $0.04 of Gemini API spend. A full dinner-party plan with a 4K poster costs about $0.32. - **Privacy note** — your photos and plans are private to you. 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. - **Food-safety note** — this app describes what it sees and recommends what to skip; it does not declare food unsafe in absolute terms. Whenever a question touches sanitation or spoilage, the answer is flagged as not a substitute for your own judgement, your own nose, or your local food-safety authority. **Backend services this app depends on:** - Auth: see section 4b - Database: see section 4b - Storage: see section 4b — REQUIRES manual enable in Firebase console; AIS Build does not auto-provision Storage today. - Email: see section 4b — optional, magic-link fallback only. - Apple sign-in: optional, requires an Apple Developer account and Service-ID config. - Payments: see section 4b (not used in v1; soft cap of 3 posters/month on free tier). - External APIs: see section 4b — Gemini only. **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` — the bucket name you enabled manually **Cost + privacy notes:** - A typical weeknight plan: ~1 inventory parse + 1 plan call + optional 1 safety scrutiny + 0-1 TTS readback ≈ $0.03-0.05. - A dinner-party plan with a 4K poster: ~1 inventory parse + 1 plan call + 1 safety scrutiny + 1 wine grounding + 1 Nano Banana Pro poster ≈ $0.30-0.35. - Privacy: photos auto-delete after 30 days unless the plan is pinned. "Delete this plan" removes the photo and the plan record in under 60 seconds. **Documentation links:** - AI Studio Build docs (post-I/O 2026 release notes) - Gemini API multimodal image + structured output docs - Gemini API Nano Banana Pro docs (4K, legible typography, 14-reference style guides) - Gemini API Live preview docs (3.1 Flash Live) - Gemini API expressive TTS docs (3.1 Flash TTS) - Firebase Auth, Firestore, Firebase Storage docs **Accessibility:** same standards as the onboarding modal — focus trap, `Esc`, ARIA, restored focus. **Behaviour:** - Always available — single click from anywhere in the app. - Tooltip on the `(i)` icon: "How this app is built". - Mobile: opens as a full-screen sheet that slides up. - Should be the most honest part of the app — never hand-wave service requirements; never say "AI" without naming the specific Gemini model and capability. ## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping) - **Fridge photo → `FridgeInventory` (Gemini 3.5 Flash, medium thinking, multimodal image)** — a single 4-MP photo plus the system instruction averages ~2,500 input tokens (the image dominates) and ~800 output tokens. At Gemini 3.5 Flash global pricing ($1.50/M input, $9/M output) that is ~$0.0110 per photo parse. A typical capture is 1-2 photos → ~$0.01-0.02 per inventory. - **Food-safety scrutiny pass (Gemini 3.5 Flash, low thinking)** — re-reads the same photos with a tighter instruction. Cached input on the photo bytes reduces cost to ~$0.0008 per scrutiny (Flash cached-input pricing is $0.15/M). - **Inventory + constraints → `DinnerPlan` (Gemini 3.5 Flash, medium thinking)** — payload is small (~1,200 input tokens, ~1,500 output tokens including three recipes with steps). ~$0.016 per plan generation. - **4K dinner-party poster (Nano Banana Pro, `gemini-3-pro-image`)** — flat $0.24 per 4K image at Pro tier ($2/M input, $12/M output; effective per-image rate based on the standard image output token count). Single generation per poster request. - **Wine pairing (Gemini 3.5 Flash + grounded search)** — ~$0.002 per call, plus the grounded-search call cost. A three-course party plan triggers one call. - **Recipe-step TTS (Gemini 3.1 Flash TTS preview)** — approximate ~$0.0010 per 200-word recipe step set (the exact TTS character-token price was not pinned at I/O 2026; treat as a working estimate and verify before shipping). Cached per plan; charged once. - **Live cook-along (Gemini 3.1 Flash Live preview)** — billed per audio+video session-minute. A typical 30-minute cook-along spent ~12 minutes actively talking with the model → ~$0.18. Preview tier may be free or discounted as of June 2026; verify current pricing. - **Welcome / empty-state imagery (Nano Banana 2, `gemini-3.1-flash-image`)** — ~$0.02/image. Generated once per app install (cached client-side) or per regenerate-empty-state action. - **Expected per-plan cost:** - Weeknight plan (no party mode, no poster, no Live): ~$0.04 - Weeknight plan with TTS readback: ~$0.04 - Dinner-party plan with 4K poster + wine pairings: ~$0.32 - Dinner-party plan with Live cook-along (30 min): ~$0.50 - **Storage:** fridge photos at upload quality average ~600 KB per photo; 30 days retention at ~10 plans/month per active user ≈ ~12 MB/user/month at Firebase Storage standard tier ($0.026/GB/month) ≈ ~$0.0003/user/month. Posters at 4K PNG + PDF average ~2 MB per saved poster → negligible. ## 7. Design language - **Mood:** A real kitchen at 6:30 pm, not a SaaS product. Not a food-blog. Not a recipe-app aesthetic with red-and-white gingham. The light is the warm overhead of a real kitchen after work. The surfaces are wood and ceramic, not polished marble. The typography is editorial: confident, generous, unhurried — like a small magazine, not a software dashboard. - **Typography:** A workhorse grotesque for app chrome and ingredient chips (Inter or Söhne). A display serif for recipe titles and the dinner-party poster's heading style (Fraunces or Saol Display). A small monospaced for the pacing strip clock times (JetBrains Mono or IBM Plex Mono) — clock times are data; they read better in mono. - **Palette:** - Warm paper background `#F6F1E8` for the main surface - Deep ink `#1B1714` for body text - Burnt sienna `#A75432` for the primary action (the camera button, the "generate plan" button) — warm, kitchen-y, not SaaS-blue - Olive `#5B6E3A` for the "used from fridge" green chip - Amber `#C7873A` for the "swap" chip — visible without alarming - Pale grey `#D5CFC2` for the "optional" chip and for muted secondary text - Tomato red `#C53A2C` only for the food-safety disclaimer banner border and for hard-allergen exclusion chips - Deep forest `#1F3A2D` and warm cream `#F4ECDC` for the deep_green poster template - **Imagery:** Photographic. Real kitchens at evening. Real fridges, not styled product shots. Warm overhead light. Slight imperfection (a smudge on the glass, a crumb on the shelf). Generated via Nano Banana 2 with prompts emphasising real surfaces, real food, no brand-name packaging. The dinner-party posters are Nano Banana Pro — the editorial-magazine quality is the demonstration. - **Hand-feel touches:** The plan card slides up from below with a slight paper-shadow when generation completes. The swap chips have a soft amber glow on hover. The pacing strip has a tiny ticking second-hand animation on the active step during cook-along (respects prefers-reduced-motion). The inventory chips have a satisfying tactile pop when removed. The poster preview lifts with a slight tilt on hover, like a printed page lifting off the table. - **Spacing:** consistent 4-px base. Generous whitespace — the plan card needs air to breathe. - **Radius:** consistent token set (e.g. 8 / 14 / 22 px). Ingredient chips use 8; recipe cards use 14; the welcome hero uses 22. - **Shadows:** subtle, warm-tinted, layered. The plan card has one soft drop-shadow; nothing else does. Heavy drop-shadows feel SaaS — avoid. - **Motion:** purposeful — generation entrance, hover lifts on cards, the pacing strip's ticking second hand. Respect `prefers-reduced-motion`. No bouncing splash animations. No hero parallax. The dinner-party poster generation has a small progress narration with three steps ("composing the menu… setting type… rendering at 4K…") because the wait is real (~8 seconds for Nano Banana Pro) and the narration earns the patience. - **States:** every interactive element has hover, focus, active, and disabled states. Loading uses skeletons that match the eventual layout, not spinners — except for the poster generator, which uses the narrated progress because the user is watching that specific render happen. ## 8. Content generation rules - Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders. - Invent plausible fridges, plausible plans, plausible cooks. Lean on realistic patterns — a wilting bag of spinach, a heel of parmesan, a half-tub of crème fraîche, two leftover eggs — but never claim a fictional ingredient came from a real brand or that a fictional plan was tested by a real cook. - Tone: warm, direct, free of corporate language. The user is tired and hungry. Cut filler. - Headlines: punchy and concrete. No 'Empower your X'. No 'Revolutionize'. No 'Seamless'. No 'AI-powered'. - Body copy: short paragraphs (2-4 sentences). Use lists where appropriate. - Plain language. Avoid jargon — except where the cook already speaks the jargon ("crème fraîche", "deglaze", "gratin", "vinaigrette" because home cooks say those words). - Where the app outputs AI-generated content, never label it "AI says" — let it speak naturally. Use small uncertainty cues only where epistemic honesty requires them (a low-confidence inventory item shows a faint underline; tapping reveals the model's reasoning). - Substitution chip copy is one short clause: "shallots → onions (reduce 2 min, slice thinner)". Not a paragraph. - The food-safety disclaimer copy is exactly: "We describe what we see and recommend what to skip. This is not a substitute for your own judgement, your own nose, or your local food-safety authority." ## 8a. Seed content (use these specific examples) Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, and snippets verbatim where helpful, or generate close variants that sit in the same world. Invent fictional households; do NOT use real public figures. **Sample fridge (the demo capture):** - half a roast chicken on a white ceramic plate, breast and one leg remaining - a whole fennel bulb with fronds still attached - half a lemon, cut side facing the front - a tub of crème fraîche, ~200 g, lid on, ~half full - two eggs in the door, in their original carton - a wilting bag of spinach, ~80 g, leaves limp but still green - a wedge of parmesan, ~120 g, slightly dried at the cut edge - a glass jar of cornichons (closed, not used in the plan) - a half-bottle of dry white wine (not used in the weeknight plan; used as a pairing reference in party mode) - a tube of harissa paste (closed, ~half full) **Sample inventory parse result (the demo):** - 10 items, parse_confidence_overall 0.86 - 1 advisory safety flag on the spinach ("leaves are limp; still usable in cooked dishes, not raw") - notes_to_user: "I couldn't see the bottom drawer clearly — is there anything there?" **Sample weeknight plan (the demo, 30-min budget, single cook):** - target_eat_clock: "7:15" (set from a 6:43 pm capture) - total_wall_minutes: 28 - One course: a one-pan chicken-spinach gratin with crème fraîche and parmesan - Recipe title: "chicken, spinach & parmesan, one pan" - One-line pitch: "the wilting spinach disappears into the gratin and the chicken stays moist under the crème fraîche" - Prep 8 min, cook 20 min, total 28 min, serves 2 - Difficulty: easy - Ingredients: roast chicken (used) · spinach (used, wilted into the cream) · crème fraîche (used, ~150 g) · parmesan (used, ~80 g grated) · lemon (used, zest only) · salt + black pepper + olive oil (pantry assumed) - Steps (5): preheat oven to 200 °C / heat oil, wilt spinach 3 min / pull chicken into bite pieces, layer with spinach, cover with crème fraîche thinned with a splash of pasta water / scatter parmesan and lemon zest / bake 18 min until top is bronzed - Pacing strip: 6:47 preheat oven · 6:50 start spinach · 6:55 layer + scatter · 6:57 into oven · 7:15 plate - Substitutions made: none - Honesty note: "only one course fits 30 minutes tonight — skip the salad or open the cornichons for crunch" - Food-safety disclaimer required: false **Sample dinner-party plan (the demo, 60-min budget, six diners):** - target_eat_clock: "8:30" - total_wall_minutes: 95 (this exceeds the 60-min weeknight cap but is appropriate in party mode where the budget shifts) - Three courses: - Starter: "fennel & lemon salad with parmesan shavings" (prep 10 / cook 0 / total 10 min, serves 6) - Main: "chicken & spinach gratin with parmesan crust" (prep 15 / cook 35 / total 50 min, serves 6) - Dessert: "soft-set lemon custard with crème fraîche" (prep 10 / cook 25 / total 35 min, serves 6, requires top-up: 200 g sugar from the pantry — flagged in top-up if not visible) - Substitutions: thyme → dried oregano (use half) on the gratin topping - Top-up list: 100 g rocket, 1 lemon, 200 g caster sugar — "the rocket lifts the fennel salad; the lemon doubles for the custard; sugar for the custard." - Wine pairings: - Starter: young Picpoul de Pinet (budget) / Vermentino di Gallura (midrange) - Main: village-level Chablis (midrange) / Beaujolais Cru "Morgon" (midrange) - Dessert: late-harvest Riesling (midrange) / Sauternes (splurge) - Honesty note: "this plan crosses the 60-min budget by 35 minutes — party mode default. Set time budget to 45 min to tighten." - Food-safety disclaimer required: false **Sample dinner-party poster (the demo):** - Template: warm_tablecloth - Hosts line: "from R + S" - Occasion line: "Friday supper" - Date: "Friday 7 June" - Menu lines as rendered legibly on the poster: - STARTER · fennel & lemon salad, parmesan shavings - MAIN · chicken & spinach gratin, parmesan crust - DESSERT · soft-set lemon custard with crème fraîche - The poster is generated at 3840 × 5400 px (4K-portrait), ready to print at A3. **Sample voice copy:** - Onboarding: "Photograph your fridge. We'll build a dinner." - Camera hint: "Open the fridge, hold steady, hit the shutter." - Inventory empty state: "Nothing to see yet — point the camera at the fridge." - Parsing: "Reading what's in there…" / "Checking freshness…" / "Planning the courses…" / "Setting the clock for 7:15…" - Plan confirmation: "Plan ready — start the oven at 6:47 to eat at 7:15." - Swap chip example: "shallots → onions (reduce 2 min, slice thinner)" - Allergen banner: "dairy-free path · no butter, no cream, no cheese" - Food-safety disclaimer: "We describe what we see and recommend what to skip. This is not a substitute for your own judgement, your own nose, or your local food-safety authority." - Poster CTA: "Cook for guests this week? One tap turns this plan into a 4K menu poster you can pin to the fridge." - Poster narration: "Composing the menu… setting type at 4K… rendering the linen…" - Live cook-along banner: "Cook-along is preview — sessions cycle every 2 minutes, you won't notice." - TTS opener: "Reading the chicken gratin steps…" - Low-confidence note: "We weren't sure about this one. Tap the chip to remove it or confirm what it is." - Honesty note example: "only one course fits 30 minutes tonight — skip the salad or open the cornichons for crunch." **Sample fictional cooks (jobs-to-be-done framing, not persona biographies):** - A weeknight cook reheating around a half-roast chicken - A first-time host preparing for six on a Friday - A cook avoiding dairy for a partner whose intolerance flares - A parent who has 30 minutes between the school run and dinner - A flatmate cooking for one and wanting to use the wilting spinach before it's gone ## 9. Media & assets - **Hero image (landing screen):** A photographed-looking shot of an open fridge at evening — half a roast chicken on a ceramic plate, a fennel bulb, eggs in the door, a wedge of parmesan, a half-lemon — under warm overhead kitchen light, no people in frame, slight condensation on the door. Generate via Nano Banana 2 (`gemini-3.1-flash-image`) — this is the cheap workhorse call, not the Pro typography demo. - **App icon / wordmark:** Set in the display serif. A small illustration of an open fridge silhouette. No emoji. - **Empty-state illustration:** A simple line drawing of a fridge with a question mark on the door. Hand-drawn aesthetic, not a flat icon. Generate once at build time via Nano Banana 2 (`gemini-3.1-flash-image`), 1:1 WebP at 1024×1024, prompt: "single hand-drawn ink line illustration of a tall household fridge in three-quarter view with a single hand-drawn question mark on the door, off-white paper background, slight pen imperfection, no shading, no colour fill, no text, no commercial branding". Ship as a seed asset at `/public/samples/empty-state-fridge.webp`. - **Demo poster (Nano Banana Pro 4K):** Generated per the seed content above — the warm_tablecloth template with the three demo courses. This is the hero artefact that demonstrates the post-I/O 2026 legible-typography capability. Show it in the capabilities panel and below the dinner-party CTA on the landing screen. - **Stock fallbacks:** If image generation fails, fall back to the photographed sample fridge at `/public/samples/sample- fridge.jpg` (3:2 WebP, 2048×1365 — ship as a seed asset; recreate via Nano Banana 2 (`gemini-3.1-flash-image`) with the prompt: "photographic open fridge at evening, half a roast chicken on a ceramic plate on the middle shelf, a fennel bulb, eggs in the door tray, a wedge of parmesan, a half-lemon, warm overhead kitchen light, no people in frame, slight condensation on the inside of the door, no commercial branding, real worn fridge"). Never to a "🍳" emoji. - **Generated imagery:** prefer Nano Banana 2 over stock photography for hero + empty states. Nano Banana Pro for the user's posters only — cost-justified by the capability. - **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift. - **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only. ## 10. Interactivity & states - Every interactive element has hover, focus, active, and disabled states. - Forms validate inline and show specific error messages (not "Invalid input"). "Eat time can't be in the past — pick a later target" is the right shape. - Loading states use skeletons that match the eventual layout, not spinners — except for the poster generator, which uses the three-step narrated progress because the wait is real. - Empty states explain the next action with a button whose label fits THIS app's domain: "Take a photo of your fridge", "Add what we missed", "Tighten the time budget" — 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. - If an AI call fails, show a calm, specific error ("The photo is too dark to read confidently. Want to retake, or describe what's in the fridge?") and offer retry. - Low-confidence inventory items in the chip grid are faintly underlined; tapping reveals the model's brief reasoning. - Substitution chips have a tap action to revert the swap (forces the plan call to re-run without that recipe). - The pacing strip's ticking second-hand animation respects `prefers-reduced-motion` and falls back to a static caret. - The food-safety disclaimer banner is non-dismissable when present, surfaces with `role="status"` and `aria-live="polite"`, and stays pinned above the plan card. - The poster generator's progress narration runs at ~1 phrase every 2 seconds — earns the ~8-second Nano Banana Pro wait. - Live cook-along surfaces a clear preview banner and a prominent mute button. ## 11. Tech & responsive requirements - **Stack:** React + TypeScript + Tailwind CSS. Functional components + hooks. Use Shadcn UI primitives where appropriate. Camera capture via the `MediaDevices.getUserMedia` API with a fallback to the `` element on browsers that don't support it. - **Build runtime:** AI Studio Build — full-stack with Cloud Run server-side functions (post-I/O 2026 free 2-app Cloud Run deploy from Build). 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 inventory parse, the plan call, the safety scrutiny, and the wine grounding; `gemini-3-pro-image` (Nano Banana Pro) for the 4K poster only; `gemini-3.1-flash-image` (Nano Banana 2) for welcome/empty imagery; `gemini-3.1-flash-tts-preview` for the recipe-step TTS; `gemini-3.1-flash-live-preview` for the cook-along. Set `thinkingLevel` explicitly per call per the matrix; omit `thinkingConfig` entirely on TTS, image, and Live calls. - **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed inventory + sample plan on first launch (the demo data above). - **Auth:** Firebase Auth — Google sign-in by default (auto- provisioned post-I/O 2026); Apple sign-in optional next to it; magic-link email as fallback (sender domain must be authorised in Firebase Auth if used). The app permits anonymous use for the demo path; sign-in is only required for plan history and poster downloads. - **Storage:** Firebase Storage for fridge photos and poster artefacts. Pre-signed URLs only. Photos are server-side re-uploaded to the Gemini Developer API Files API and referenced via `fileData.fileUri: "files/abc123xyz"` (the `files/*` resource name; NOT a `gs://` URI — that's Vertex AI). Do NOT pass Firebase Storage public URLs to `generateContent`. - **Live API session handling + state recovery:** the cook-along client tracks current step + active timers locally and re-establishes the Live session transparently every 2 minutes (audio+video) / 15 minutes (audio-only). Persist a `SessionSyncState` to `sessionStorage` on every tick and pass a concise context summary into the first system message of the next handshake cycle: ```typescript interface SessionSyncState { activeSessionId: string; accumulatedSegments: Array<{ speaker: string; text: string; timestamp: number }>; currentActiveStepIndex: number; // recipe step cursor activeTimers: Array<{ label: string; endsAtMs: number }>; } ``` - **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. - Use `clamp()` for fluid typography. Prefer container queries over media queries for component-level responsiveness. - Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS. - Zero horizontal overflow at any width. Zero layout shift on load. - Persist user data in Firestore. Use real-time listeners on the plan view (so a user starting on phone can finish on tablet). - Optimistic UI on writes; reconcile on response. - Cook-along mode uses the screen-wake-lock API to keep the display on; falls back gracefully if unsupported. - The poster image is generated server-side, written to Firebase Storage, and served via a pre-signed URL with a `Content- Disposition: attachment` header on the download endpoint. - The PDF version of the poster is composed server-side via `pdf-lib` or `pdfkit`, embedding the 4K PNG; print-ready at A3 with 5 mm bleed. ## 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. The camera capture button is reachable by keyboard with `Enter` triggering shutter on browsers that allow it; there is also a "use library" path for keyboard-only users who cannot operate a camera UI. - Colour contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. The amber swap-chip text on warm-paper background is verified at 4.6:1; the burnt-sienna primary button text (white on `#A75432`) is verified at 5.2:1. - All images have meaningful `alt` text. The fridge photos have `alt` describing what the model parsed ("photograph of an open fridge containing half a roast chicken, a fennel bulb, half a lemon, crème fraîche, two eggs, wilting spinach, a wedge of parmesan"). - Form fields have associated `