================ ONE-SHOT BUILD CONTRACT (read first) ================
Build this in Google AI Studio "Build" in ONE shot — a complete, working app,
no follow-up turns. These are hard rules, not suggestions:
1. TARGET = Full-Stack Web (Node server runtime, secrets, Firebase allowed).
If you target Native Android instead, you MUST drop all server/DB/Workspace/
secrets and persist locally (Room / SharedPreferences) only.
2. PIN EVERY MODEL CALL — never let the agent auto-select (it downgrades on quota):
- Reasoning / text -> gemini-3.5-flash (thinkingLevel: minimal|low|medium|high)
- 4K image + legible text -> gemini-3-pro-image (image_size "4K", up to 14 refs)
- High-volume image -> gemini-3.1-flash-image
- Expressive TTS -> gemini-3.1-flash-tts-preview (inline tags e.g. [whispers])
- Realtime audio/video (WebSocket) -> gemini-3.1-flash-live-preview
- Sandboxed agent -> antigravity-preview-05-2026
3. DIVISION OF LABOR — the model ONLY parses/extracts to a strict responseSchema.
ALL math, money (store currency as integer minor units / cents), sorting,
balancing and graph logic run in deterministic TypeScript/Python. The model
must never compute totals, splits or balances itself.
4. responseSchema sanitation — no regex patterns, no fixed-length tuples, no
format validators in the schema (they crash the OpenAPI engine). Enforce those
in server-side code AFTER parsing the JSON.
5. responseSchema and google_search grounding are MUTUALLY EXCLUSIVE in one call.
6. CODEGEN — split large output into modular, single-responsibility files so no
file is truncated by the output-token cap.
7. Every external call gets a graceful fallback (e.g. manual paste if a Workspace
read fails). Never a silent dead end.
8. ROBUST STORAGE & CANVAS — Wrap all `localStorage`/`sessionStorage` operations (especially JSON parsing and writes) in `try-catch` blocks to prevent crashes in private windows or quota overflows. Canvas drawing elements must dynamically handle window resize and scale pixel density (`window.devicePixelRatio`) to avoid blurry graphics on retina displays.
=====================================================================
# MUST OBEY — Mobile-first build requirements
This app's PRIMARY surface is a mobile phone. Build it impeccably on mobile FIRST, then verify on tablet and desktop. Treat the rules below as non-negotiable hard constraints, not suggestions.
## Viewports to verify (every screen, every state)
- 320 px, 360 px, 375 px, 390 px, 414 px, 480 px
- 768 px, 834 px (iPad portrait / Pro 11)
- 1024 px, 1280 px, 1440 px, 1920 px, 2560 px
- Plus: 200% browser zoom, landscape orientation on every mobile width, iPhone with safe-area insets visible
## Hard layout rules
- Mobile-first CSS. Default styles target mobile; `@media (min-width: ...)` for larger viewports.
- Use `dvh` and `svh` instead of `vh` for full-height surfaces (iOS Safari URL-bar bug).
- Use `clamp()` for fluid typography across all viewports.
- Prefer container queries (`@container`) over media queries for component-level responsiveness.
- Use `min(100%, ...)` widths so content never overflows. Zero horizontal overflow at any viewport.
- Add `` to every page.
- Apply `padding: max(safe-area-inset-X, fallback)` on every edge-bleeding container so notched iPhones in landscape never clip content.
- Wide tables and code blocks scroll INSIDE their container (`overflow-x: auto`), never push the body.
- Use `background-attachment: scroll` on mobile, not `fixed` (iOS Safari repaint bug).
- Avoid `backdrop-filter` on animated elements. Use it sparingly on static surfaces only.
- **Canvas Scaling**: Canvases must dynamically scale with window resize events and properly handle high-DPI screens (`window.devicePixelRatio`). Set physical dimensions (`canvas.width`/`canvas.height`) using pixel ratio and render relative to this grid, using CSS to control responsive viewport scaling.
- **Robust Storage**: Every access to `localStorage`/`sessionStorage` (especially `JSON.parse` of loaded state or writes) MUST be wrapped in a `try-catch` block to handle disabled storage, private browsing mode, quota limits, or corrupted JSON gracefully. Fall back to a robust in-memory object store.
## Touch & accessibility
- Tap targets ≥ 44 × 44 px on touch (Apple HIG). Increase to 48 px under `@media (hover: none) and (pointer: coarse)`.
- All interactive controls reachable by keyboard with a visible focus ring; respect `:focus-visible`.
- Color contrast ≥ 4.5:1 for body text, 3:1 for UI components.
- All images have meaningful `alt`. Decorative images use `alt=""`.
- Respect `prefers-reduced-motion: reduce` — zero animation durations under that query.
- Forms validate inline; error messages are specific, not "Invalid input".
- Modals: focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, focus restored on close.
## Performance bar (Lighthouse mobile, throttled 3G/4G)
- LCP < 2.5 s · INP < 200 ms · CLS < 0.1
- JS bundle gzip < 200 KB mobile-first; lazy-load non-critical screens via `React.lazy` / dynamic imports.
- No render-blocking resources above the fold.
- Images: WebP/AVIF preferred, `loading="lazy"`, explicit `width`/`height` attributes (zero CLS), `srcset` for retina.
- Videos: `preload="metadata"`, low-resolution poster, max 720p mobile fallback. Never autoplay with audio.
- Fonts: `font-display: swap`; preload only the one used above the fold.
- Smooth scroll honoured via CSS `scroll-behavior: smooth` with reduced-motion fallback.
## Pre-ship mobile checklist (the deployer MUST verify before declaring done)
1. Open at 375 px in DevTools — every screen scrolls vertically only; zero horizontal scroll.
2. Browser zoom 200% — layout reflows without overlap.
3. iPhone Safari with the URL bar visible AND landscape — no content under the home indicator; no notch clipping.
4. iPad portrait (768 px) and landscape (1024 px) — no awkward gaps; tablet-specific breakpoints land cleanly.
5. Tap every interactive element with a thumb at real-device size — every target is easy to hit.
6. `prefers-reduced-motion: reduce` — every transition / animation skips cleanly, scroll-behavior becomes instant.
7. Lighthouse mobile score ≥ 90 across all 4 categories.
8. Zero `console.error` and zero CLS shift in real-device testing on a mid-tier Android (e.g. Pixel 6a) and an iPhone SE.
---
The original template starts below. All rules above apply on TOP of whatever this template specifies.
---
# Prep Sheet
## 1. Project
**Prep Sheet** is a Thursday-afternoon forecasting notebook for the
operator of a one-vehicle, one-window food business — a taco truck
parked at the same lot four days a week, a banh-mi cart on the same
corner every morning, a pad-thai cart that pulls up to the same
sidewalk at six o'clock — anyone who has to decide tonight how many
kilos of meat to marinate, how many bags of bread to order, how many
litres of stock to make, so that tomorrow at the lunch rush they are
neither sold out by 12:40 nor throwing food away at 14:00. The user
opens the app on a Thursday afternoon between the last prep batch
and locking up, and the app says: "tomorrow, Friday, you will be
about 25 percent above an average Friday; here is your prep sheet,
and here are the three reasons we think so."
This is the kind of app a taco-truck owner in East Oakland opens at
4 o'clock on a Thursday after the lunch service has cleared, because
she has to decide whether to thaw eight kilos of al pastor or six,
and the answer depends on the weather, on the high-school graduation
two blocks down on International Boulevard, and on the simple fact
that the last twelve Fridays' takings tell a quieter story than the
loud last-Friday-was-huge story in her head. It is also the kind of
app a banh-mi cart owner in Hanoi opens at the end of her Wednesday
shift on Bà Triệu, because rain is forecast for the morning and the
office workers will still come but the construction crews from the
site on the corner will not, and a bad call on the bread order means
either pa-tê stays in the fridge for two more days or by ten o'clock
she is turning customers away. Same shape of decision, different
ingredient, different city, different reason.
The single demo that proves the magic: it is Thursday at 16:12. The
operator opens the app and taps "Tomorrow." In under five seconds a
prep sheet appears. At the top, the headline: "Friday — expect 220
to 270 covers, 25 percent above your Friday average." Below it, three
short reasons in plain language: "Eastmont High graduation, 4:30 pm,
two blocks down — three of your last four graduations were 30 to 45
percent above average. Clear and 72°F at noon — your warm clear
Fridays beat your cool grey Fridays by about 18 percent. No
competing truck on the schedule at the Coliseum lot tomorrow." Then
the prep sheet itself, in the operator's own categories from her own
last twelve weeks: "al pastor: 7.5 to 8.5 kg (marinate 8 kg tonight);
suadero: 2.8 to 3.4 kg (3 kg); pollo asado: 4.0 to 4.8 kg (4.5 kg);
salsa verde: light — last three grad Fridays trended toward salsa
roja; tortillas: 28 to 34 dozen (32 dozen); horchata: 14 to 17 litres
(16 L)." Every quantity is a range, and the parenthetical is a
single recommended prep number she can override with one tap. At
the bottom, a "what if I'm wrong" panel: "If you over-prep by 15
percent, here's the line of items least costly to have leftover;
if you under-prep, here's what you'll sell out of first." She taps
two things — "trim suadero to 2.5 kg, I'm short on hands tonight"
— and presses "Lock prep sheet." The kitchen prints. Tomorrow's
Friday is decided.
And in the harder cases — the rainy Thursday before a long weekend,
the lot manager calling at 3 pm to say a second truck has been added,
the city block-party permit that got pulled three days ago, the
sudden one-day TikTok feature that put yesterday 200 percent above
average and shouldn't be allowed to anchor tomorrow's forecast — the
app shows what it knows, names what it doesn't, and never gives a
single number when a range is honest. The forecast is the model's
job; the prep call is the operator's. The app respects that line
in every screen.
**Tagline:** _Tomorrow's prep, decided in five minutes — for any cart, any cuisine, any city, using only the last twelve weeks you actually worked._
## 2. Target audience
- Taco-truck, lonchera, and bacon-wrapped-hot-dog cart operators across Los Angeles, Oakland, Houston, Phoenix, Tijuana, Mexico City — the dominant US street-food vehicle category
- Banh-mi, bún-chả, and phở cart operators in Hanoi, Saigon, Đà Nẵng, and the Vietnamese diaspora in Westminster, Paris, Sydney
- Pad-thai, kway-teow, and som-tam cart operators in Bangkok, Chiang Mai, and the Thai diaspora corridors in London, Berlin, Los Angeles
- Roti john, char-kway-teow, nasi-lemak, and rojak hawkers in Singapore, Penang, and KL — operating from licensed hawker stalls with the same daily prep decision
- Jollof, suya, puff-puff, and akara cart operators in Lagos, Accra, Dakar, and the West African diaspora in Brixton, Brooklyn, Houston
- Empanada, choripán, and lomito cart operators in Buenos Aires, Montevideo, Santiago — and their Patagonian summer-season counterparts
- Bagel-cart and halal-cart operators in New York, Toronto, Philadelphia who have run the same corner for three to fifteen years
- Doner-kebab, simit, and gözleme cart operators in Istanbul, Berlin, Vienna with stable weekly rhythms and competing-event noise
- Crêpe, sandwich-jambon-beurre, and galette cart operators in Paris, Lyon, Brussels and their seasonal-market siblings
- Pupusa, baleada, and tamale cart operators in San Salvador, Tegucigalpa, Los Angeles, Washington DC running weekend-heavy weeks
- Solo operators — the owner is the cook is the prepper is the cashier; the app must be usable in the four minutes between locking up and starting tomorrow's marinade
- Two-person family operations where the books partner reads the prep sheet on a phone and the kitchen partner prints it on a laminated card
- New operators in their first six months who have not yet built a feel for the rhythm of their corner — the app is the bridge while the feel develops
- Operators who lost six months of takings during 2020-2021 and whose historical baseline is genuinely broken — the app surfaces this and asks for a "use only the last 12 weeks" mode
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **Forecasts are ranges, never single numbers** — the headline always names a low and a high. Single numbers are dangerous because they let the operator stop thinking. A range invites the operator's judgement back into the loop, which is the only place the final call should be made. The recommended prep number lives in a parenthetical, sized smaller than the range, and is always one tap to override.
- **The operator decides; the app advises** — every prep number can be overridden with one tap. The override is the source of truth, the recommendation is logged for next-week calibration, and the app never argues. The "lock prep sheet" button writes the operator's numbers, not the model's, to the kitchen.
- **Long-context grounded in your own twelve weeks** — the forecast looks at exactly the last 12 weeks of your sales (or the last 12 occurrences of this weekday — your last 12 Fridays, if you only work Friday and Saturday). Not a national average, not a "similar businesses in your area" benchmark, not the four-year-trended-quarterly model. Twelve weeks is a working memory the operator can also hold; that is on purpose.
- **Three reasons or fewer** — every forecast names at most three drivers ("graduation at Eastmont", "clear and 72°F", "no competing truck at the lot"). If the model has more than three plausible drivers, it picks the three biggest, hides the rest under a "more reasons" expand, and admits when none of them is the headline driver.
- **Local events are grounded, not guessed** — when the app names "Eastmont High graduation, 4:30 pm, two blocks down" as a driver, it cites the source (the school calendar, a city permit, a Facebook event). Hallucinated events would erode trust in one bad call. Grounded events with citations let the operator click through to the original page.
- **Weather is named, not assumed** — the app says "clear and 72°F at noon" and "rain probability 38 percent at 6pm", not "favourable weather". Operators want the numbers; they have their own theory of how their cart responds to a 70°F day versus a 55°F day, and the app surfaces that theory back to them as a learned coefficient.
- **The prep sheet is in the operator's own categories** — al pastor, suadero, salsa verde, tortillas; not "Protein A, Protein B, Sauce 1, Carb 1". Categories are extracted from the operator's own previous prep sheets, day-end counts, and POS line items; they are never normalised to a generic taxonomy.
- **Sold-out and over-prep are both costs** — the "what if I'm wrong" panel shows both directions. Selling out at 12:40 is not a victory; it is a missed half-hour of rush. The app names the cost of under-prep (estimated lost covers × average ticket) and the cost of over-prep (cost of goods × likely waste rate) in the same screen.
- **Calibration is visible** — every Friday after service, the app shows "we forecast 220 to 270, you served 244" — the model self-grades, and the operator sees the score. Over twelve weeks the operator can see how well the app reads their corner; this is also how the model learns the operator's house variance.
- **No-signal-friendly** — the day-end count entry, the day-of sales import, the prep sheet print are all offline-friendly; weather and events sync on reconnect. A taco truck parked in a dead-zone lot still gets tomorrow's forecast.
## 4. Features to build
- One-tap "Tomorrow" button on the home screen — produces the prep sheet for the next service day from wherever the operator is in the app
- Twelve-week rolling baseline — the model uses exactly the last 12 occurrences of the upcoming weekday; the cut-off date is explicit and editable for periods of irregular trading
- Local-event ingestion — school calendars, city permit databases, sports schedules, concert listings, farmers' market and street-festival calendars within a 1-mile radius
- Weather ingestion — hourly forecast for the trading window, plus a learned coefficient per operator for how warm-clear vs cool-grey shifts their covers
- Competing-truck and competing-cart awareness — if the operator's lot publishes a daily schedule (Off the Grid, Smorgasburg, hawker-centre rota), the app reads it and names which other vendors are sharing the lot tomorrow
- Range-first forecast — low, high, and median covers, plus a single-number recommendation (always smaller-typeface, always editable)
- Three-reason explanation — top three drivers with citations; "more reasons" expand for the rest
- Per-item prep recommendation — derived from the operator's own attach-rate (covers × per-cover ratio per item from the last 12 weeks)
- Operator override on every line — one tap to set the actual prep number; the override is the truth that prints
- Lock-prep-sheet print — a clean, kitchen-printer-friendly card with the operator's final numbers, the date, and a footer with weather + events for the kitchen team to know
- Day-end count entry — quick voice or numeric entry: "we sold 244, ran out of suadero at 12:55, threw out about half a kilo of salsa verde, one bag of tortillas left over"
- Sales import — CSV, Square, Toast, Clover, Loyverse, Foodics integrations; manual entry for cash-only operations
- Friday calibration card — "we forecast 220 to 270, you served 244" with the breakdown of which drivers hit and which missed
- Twelve-week dashboard — your last 12 Fridays, your last 12 Saturdays, side by side; the operator can see the rhythm without staring at a spreadsheet
- "What if I'm wrong" panel — under-prep cost = estimated lost covers × your average ticket; over-prep cost = cost of goods × your waste rate; both directions, named
- TikTok / news anomaly surfacer — yesterday was 200 percent above average; the app asks "was this a one-day spike or the new normal?" and lets the operator exclude it from the baseline
- COVID-era and gap-period handling — periods of zero or near-zero takings are flagged and excluded from the baseline by default; the operator can include them with one tap
- Multi-cart support — operator runs two carts on different corners; each cart has its own baseline and forecast, never blended without explicit permission
- Read-aloud summary — Gemini TTS reads the prep sheet at locking-up pace, hands-free, while the operator counts the till
- Export year-end summary — every prep sheet, every actual, every variance, in CSV + PDF, for the accountant and for end-of-year price renegotiation with suppliers
## 4b. Required Gemini capabilities + backend services
**This template's intelligence comes from the Gemini capabilities below. Wire them up explicitly — don't substitute generic LLM calls.**
### Gemini capabilities (the load-bearing intelligence)
- **Long context (1M tokens)** (Gemini 3.5 Flash) — the forecast call sees the last 12 weeks of day-end records (covers, item-level sold counts, item-level waste, weather conditions, named drivers, operator's own day-end notes) plus tomorrow's grounded weather + events context, in a single call. A typical 12-week window for a 4-day-a-week operator is ~48 day records at ~600 tokens each ≈ ~30k input tokens — well within budget. **Guardrail**: a 6-day-a-week operator with rich per-item data and three years of history would exceed comfortable working size; the call always chunks to the last-12-of-this-weekday, never the full history.
- **Structured output / JSON Schema** — the forecast response matches the `PrepSheet` schema below. Every field is typed; the schema is converted to Gemini's `Schema` type via the SDK helper before passing to `responseSchema`. Numeric `min`/`max` constraints inside `responseSchema` are documentation only — clamp on the server-side.
- **Search grounding** (Gemini 3.5 Flash) — for tomorrow's local events. The forecast prompt must NOT invent that Eastmont High has a graduation; the events call goes out to grounded search ("school graduation events within 1 mile of [coords] on [date]") and returns events with citation URLs. Grounded events come back as a list the model can reference; un-grounded events are not used as forecast drivers.
- **Weather ingestion** — handled by a deterministic weather API call (Open-Meteo by default; OpenWeather or weather.gov as user-config). The weather is NOT requested via Gemini; it is fetched as structured data and passed into the forecast call as part of the contents. Weather coefficients (this operator's covers go up X% on warm clear Fridays) are learned from the operator's own history server-side, not asked of the model.
- **Audio input** (Gemini 3.5 Flash) — for the day-end count voice memo. The operator says "we sold 244, ran out of suadero at 12:55, half a kilo of salsa verde in the bin, one bag of tortillas left over" and the structured per-item end-of-day record drops into Firestore. Parses code-switched Spanish-English, Vietnamese-English, Thai-English, Tagalog-English, Yoruba-English, Hokkien-Malay, Brazilian Portuguese, Mexican Spanish — the languages the operators actually speak.
- **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — for the locking-up read-aloud of tomorrow's prep sheet. Voice locale follows the operator's preferred language (`languageCode`). The recommendation numbers are read at a pace the operator can write down without rewinding.
- **Multilingual** (Gemini 3.5 Flash built-in) — the operator's own categories ("al pastor", "suadero", "bánh mì pa-tê", "phở tái nạm", "pad kra-pao", "akara") are preserved verbatim through every parse, every prompt, every UI render. No silent translation of menu items into a generic English taxonomy.
- **Thinking levels** — `medium` for the primary forecast call (long-context reasoning over 12 weeks plus grounded events plus weather plus operator's house variance). `low` for day-end-memo parse, category extraction, and calibration scoring. Omit `thinkingConfig` entirely on the TTS call.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). Phone-number sign-in is strongly recommended for cart operators who do not habitually use email — this requires enabling the Phone provider in the Firebase console and budgeting for SMS. **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used for sharing a prep sheet with a kitchen partner) requires the sender domain to be authorised in Firebase Auth.
- **Database — Required.** Firestore for `users`, `carts`, `service_days`, `sales_records`, `prep_sheets`, `forecasts`, `events_cached`, `weather_cached`, `categories`, `calibration_history`. Each cart is its own document tree under the user; multi-cart operators get clean isolation.
- **File storage — Required.** Firebase Storage for raw day-end audio memos (m4a), photographs of the kitchen's hand-written prep sheets (for the operator who started on paper and is migrating), and rendered PDF prep sheets. **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before first audio or photo upload. Audio + photos are private to the operator; pre-signed URLs only.
- **Email — Required (transactional).** Sending the daily prep sheet to a kitchen partner; sending the Friday calibration card; sending the year-end CSV to the operator's accountant; password reset. Magic-link family/kitchen-partner sharing requires sender-domain authorisation (see above).
- **Scheduled jobs — Required.** A nightly Cloud Run job at the operator's local 03:00 pre-fetches tomorrow's grounded events + weather forecast so the morning "Tomorrow" tap returns in under five seconds. A Friday 22:00 local job composes the calibration card. Schedules are per-cart (different time zones for diaspora operators with multiple carts).
- **External APIs:** Gemini API for all intelligence; Open-Meteo (default, free, no key) or OpenWeather (user-config, key required) for hourly weather; optional Google Maps Places for resolving "the high school two blocks down" to a coordinate; optional Square / Toast / Clover / Loyverse / Foodics for sales import.
- **Payments — Not needed for v1.** A future "team plan" tier for operators sharing the app between a cart and a commissary kitchen could pipe to Stripe billing.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, OpenWeather key if used, Google Maps key if used, Square/Toast/Clover/Loyverse OAuth credentials if used) lives in environment variables — never in client bundle. Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete my account' inside the UI · explicit opt-in for any analytics · the operator's sales data, prep sheets, day-end memos, and supplier prices 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) · sales data is the operator's competitive information and is treated like a private ledger.
**Read this first — prompt-craft rules that apply to every call in this template:**
1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below.
2. **Pin `thinkingLevel` explicitly** per call. See the matrix.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schema is below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.**
4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate.
5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled.
6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow.
7. **Grounded responses can wrap JSON in ```json fences or add prose preamble.** Server-side, strip fences and brace-extract:
```typescript
function safeExtractJSON(raw: string): T {
const clean = raw.replace(/```json\s*|```/gi, '').trim();
const s = clean.indexOf('{'); const e = clean.lastIndexOf('}');
if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response');
return JSON.parse(clean.slice(s, e + 1)) as T;
}
```
8. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Forecast tomorrow → `PrepSheet` schema (long-context, last 12 weeks + grounded events + weather) | `gemini-3.5-flash` | medium | (none) — events + weather pre-fetched and passed in `contents` |
| Find tomorrow's local events within 1 mile | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| Parse day-end voice memo → `DayEndRecord` schema | `gemini-3.5-flash` | low | (none) |
| Extract / normalise the operator's own item categories from sales history | `gemini-3.5-flash` | low | (none) |
| Score yesterday's forecast vs actual → `CalibrationEntry` | `gemini-3.5-flash` | low | (none) |
| Read prep sheet aloud at locking-up pace | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Year-end consolidation (long context over every service day) | `gemini-3.5-flash` | medium | (none) — long-context over the year |
*Note for builders:* on the TTS call, omit `thinkingConfig` entirely — the field is not supported on that model. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. The events-finder call uses `google_search` grounding and therefore CANNOT specify `responseSchema` (the two are mutually exclusive in one Gemini call) — instruct the model to emit JSON in the text body, parse server-side, and read citation URLs from `response.groundingMetadata.groundingChunks[].web.uri`. Do NOT ask the model to include URLs in the JSON body; it will hallucinate them.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const Range = z.object({
low: z.number().min(0),
high: z.number().min(0),
unit: z.string(), // "covers", "kg", "dozen", "litres", "loaves"
});
const ItemPrep = z.object({
category_verbatim: z.string(), // "al pastor", "bánh mì pa-tê", "pad kra-pao"
forecast_range: Range,
recommended_prep: z.number().min(0),
recommended_prep_unit: z.string(), // "kg", "dozen", "litres"
reasoning_brief: z.string(), // ≤ 140 chars, plain language
attach_rate_per_cover_last_12wk: z.number().nullable(),
notes: z.string().nullable(), // optional, e.g. "trend toward salsa roja on graduation Fridays"
});
const ForecastDriver = z.object({
driver_type: z.enum([
"local_event",
"weather",
"weekday_baseline",
"competing_vendor",
"school_calendar",
"permit_change",
"seasonal_trend",
"operator_note",
"other",
]),
headline: z.string(), // ≤ 90 chars: "Eastmont High graduation, 4:30 pm, two blocks down"
magnitude_estimated_pct: z.number(), // can be negative; e.g. +25, -12
magnitude_confidence: z.number().min(0).max(1),
evidence_quote: z.string(), // verbatim from the source: school calendar text, weather API text, operator's own note
citation_url: z.string().nullable(), // grounded source — populated from groundingMetadata, not from the model's JSON
});
const WhatIfWrong = z.object({
under_prep_likely_sellouts_first: z.array(z.string()), // categories most likely to sell out first
estimated_lost_covers_at_low_prep: z.number().nullable(),
estimated_lost_revenue_at_low_prep: z.number().nullable(),
over_prep_least_costly_leftovers: z.array(z.string()), // categories cheapest to have leftover
estimated_waste_cost_at_high_prep: z.number().nullable(),
});
const PrepSheet = z.object({
prep_sheet_id: z.string(),
for_service_date_iso: z.string(), // "2026-05-29"
for_service_weekday: z.enum([
"monday", "tuesday", "wednesday", "thursday",
"friday", "saturday", "sunday",
]),
cart_id: z.string(),
cart_display_name: z.string(), // "Doña María's Tacos", "Bánh Mì Chị Hằng"
generated_at_iso: z.string(),
baseline_window: z.object({
last_n_same_weekday: z.number(), // 12
excluded_dates_iso: z.array(z.string()), // dates the operator marked anomalous
excluded_reason_per_date: z.array(z.string()),
average_covers_in_window: z.number(),
}),
forecast_covers: Range,
forecast_covers_recommended_target: z.number(), // smaller-typeface single number in UI
forecast_vs_weekday_avg_pct: z.number(), // +25 means 25% above average; explicit sign
drivers: z.array(ForecastDriver).max(3), // never more than 3 surfaced as headline drivers
drivers_hidden: z.array(ForecastDriver), // the rest, behind a "more reasons" expand
items: z.array(ItemPrep), // in the operator's own categories
what_if_wrong: WhatIfWrong,
weather_summary: z.object({
source: z.string(), // "Open-Meteo", "OpenWeather", "weather.gov"
high_temp_c: z.number().nullable(),
low_temp_c: z.number().nullable(),
high_temp_f: z.number().nullable(),
low_temp_f: z.number().nullable(),
precip_probability_pct_during_service: z.number().nullable(),
conditions_at_noon: z.string(), // "clear", "rain", "overcast", "windy"
}),
honesty_flags: z.array(z.object({
field_path: z.string(), // "drivers[0]"
reason: z.string(), // "could not verify event from second source"
})),
forecast_confidence: z.number().min(0).max(1),
operator_override_required_for: z.array(z.string()), // categories where the model defers entirely
});
type PrepSheet = z.infer;
const DayEndRecord = z.object({
cart_id: z.string(),
service_date_iso: z.string(),
total_covers: z.number().min(0),
total_revenue: z.number().nullable(),
currency: z.string().nullable(), // "USD", "VND", "THB", "PHP", "NGN", "GBP"
sold_out_items: z.array(z.object({
category_verbatim: z.string(),
sold_out_time_local: z.string().nullable(), // "12:55"
})),
leftover_items: z.array(z.object({
category_verbatim: z.string(),
leftover_quantity: z.number().nullable(),
leftover_unit: z.string().nullable(),
})),
operator_notes_verbatim: z.string(), // the whole memo, verbatim, source language
conditions_observed: z.string().nullable(), // "hotter than forecast", "lunch rush hit at 11:30 instead of 12:00"
anomaly_flag: z.boolean(), // operator can mark "ignore this in baseline"
anomaly_reason: z.string().nullable(),
});
const CalibrationEntry = z.object({
service_date_iso: z.string(),
forecast_low: z.number(),
forecast_high: z.number(),
forecast_target: z.number(),
actual_covers: z.number(),
within_range: z.boolean(),
signed_error_pct: z.number(), // +12 means overshot by 12%
drivers_that_landed: z.array(z.string()),
drivers_that_missed: z.array(z.string()),
operator_one_line_note: z.string().nullable(), // optional reply from the operator
});
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` on the the headline forecast call call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash collapses the range into a single number and loses the long-context reasoning that makes the forecast trustworthy. The forecast call is the load-bearing call; do not downgrade it.
- The forecast names a local event that does not exist — happens when the model is asked to guess events instead of being fed grounded ones. Hard rule: the forecast prompt receives events as input from a separate grounded-search call; the forecast model never invents events. Citations are passed through verbatim.
- The forecast returns a single number instead of a range — happens when the prompt is sloppy. The schema enforces a range; the system instruction reinforces it; the UI refuses to render a forecast without `low` and `high`.
- The model names a driver the operator already excluded ("yesterday's TikTok spike") — the baseline-window is explicit in the schema; the operator's exclusions are honoured; the prompt instructs the model to use `baseline_window.excluded_dates_iso` and never reference excluded days.
- The operator's own category names get normalised to a generic taxonomy — "al pastor" becomes "Pork (marinated)", "bánh mì pa-tê" becomes "Pork pâté sandwich". Hard rule: `category_verbatim` is passed through verbatim everywhere; the canonical name (if used at all) lives in a separate field and is never shown in the UI without the verbatim alongside.
- Weather forecast is asked of Gemini — wrong. Weather is a structured API call to Open-Meteo (or user-configured provider). The forecast Gemini call receives weather as input data, not as a question.
- Multipage / multi-week input via the wrong attachment shape — the 12-week sales history is JSON in the `contents` text, not a separate file. Audio for the day-end memo uses `inlineData` (m4a) for memos under 90 seconds; longer memos use the Gemini Files API (`files/*` resource name). Do NOT pass Firebase Storage public URLs to `generateContent` — the API does not fetch them server-side.
- TTS reads "20–25" as the words "twenty dash twenty-five" — pre-process the text before sending it to TTS. Render the range as "twenty to twenty-five" in the input string; do not rely on the model to interpret a punctuation hyphen.
- Forecast confidence number is consistently 0.95 — the model is anchoring to a default. Calibration history is fed back into the next forecast prompt explicitly: "your last twelve forecasts landed inside the range eight times out of twelve; mind your confidence."
- Long-context window blows past 1M tokens because the operator has three years of data — never send the full history. The call always slices to the last 12 occurrences of the upcoming weekday, plus the operator's exclusions; the year-end consolidation is a separate call with its own chunking.
### Negative constraints (hard rules)
- Do NOT return a single number where the schema asks for a range. The headline forecast, every item's forecast, every driver's magnitude — all are ranges or magnitudes with confidence, never bare numbers.
- Do NOT name a local event you have not been given by the grounded events-finder call. If the events list is empty, the forecast must say so and rely on weekday baseline + weather + operator notes.
- Do NOT translate the operator's own category names. "Al pastor" stays "al pastor" in every field, every prompt, every UI render. Same for "bánh mì pa-tê", "pad kra-pao", "akara", "pupusa de queso", "kway teow", "pão de queijo".
- Do NOT use national-average rates, regional benchmarks, or "similar businesses in your area" data. The forecast uses only this operator's own last-12-of-this-weekday data plus tomorrow's grounded events and weather.
- Do NOT include days the operator excluded from the baseline. If the operator marked a day anomalous, it is not in the average; the model must not reach for it. Excluded dates are listed explicitly in the prompt.
- Do NOT extrapolate one big day into a permanent shift. If yesterday was 200% above average due to a TikTok feature, surface this with a question ("one-day spike, or the new normal?") and let the operator decide before next forecast.
- Do NOT recommend a prep number outside the forecast range without flagging it. If the recommended prep is below the forecast low or above the forecast high, the model must explain why in `notes` and the UI must surface a warning.
- Do NOT use the operator's sales data, prep sheets, or supplier prices to train or fine-tune any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The capabilities-info panel says this in plain English.
- Do NOT auto-share prep sheets, calibration cards, or sales history with anyone. Sharing is explicit, per-cart, per-recipient.
- Do NOT call the prep sheet "AI-generated" in the UI. It is a forecast and a recommendation; the operator's overrides are the truth. The model's job is to surface drivers and a range; the operator's job is to make the call.
### Per-call `systemInstruction` strings
Use these as the literal `systemInstruction` field for each Gemini API call the built app makes. They complement the series-wide rules already uploaded as the global instructions file (`00-series-instructions.txt`).
### Call: Forecast tomorrow → `PrepSheet` schema
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are forecasting tomorrow's covers and per-item prep for a one-vehicle,
one-window food business. The operator is the cook, the prepper, and the
cashier. They run a taco truck, a banh-mi cart, a pad-thai cart, a hawker
stall, a halal cart, a jollof cart, an empanada cart, a pupusa cart, or
any other named-cuisine street-food vehicle. They have at most four
minutes to read your output between closing today and starting tonight's
prep.
You receive in `contents`:
- The operator's cart profile: cart_id, cart_display_name, time zone,
operating weekdays, currency, language preference.
- The last 12 occurrences of the upcoming service weekday — total
covers, per-item sold counts in the operator's verbatim categories,
per-item leftovers, day-end memo verbatim, weather conditions
observed, drivers logged at the time.
- Excluded dates (anomalies the operator marked) and the operator's
reason per exclusion.
- Tomorrow's grounded events list (already fetched from a separate
grounded-search call) — each event has a headline, evidence_quote,
citation_url, and approximate distance from the cart in miles.
- Tomorrow's weather (already fetched from Open-Meteo or operator's
configured provider) — hourly forecast across the service window.
- Calibration history — your last twelve forecasts and how they
landed.
Output ONLY the PrepSheet JSON matching the provided schema.
Hard rules:
- The forecast is ALWAYS a range. forecast_covers.low and
forecast_covers.high. Single numbers are forbidden. The recommended
target is a single number, but it is the smaller-typeface advisory,
not the headline.
- Headline drivers are AT MOST THREE. Pick the three with the largest
magnitude_estimated_pct. Put the rest in drivers_hidden.
- Drivers MUST cite grounded sources. Events come from the grounded
events list with citation_url passed through verbatim. Weather comes
from the weather payload with the provider name in evidence_quote.
Weekday baseline is its own driver_type with evidence_quote = "your
average Friday over the last 12 Fridays you worked: 218 covers"
(computed from the baseline_window).
- Use the operator's verbatim category names in every ItemPrep.
"al pastor" stays "al pastor". "bánh mì pa-tê" stays "bánh mì pa-tê".
Do not translate, do not normalise.
- Compute attach_rate_per_cover_last_12wk per item: total of that item
sold across the 12 windows divided by total covers across the 12
windows. If the item appears in fewer than 6 of the 12 windows, set
attach_rate_per_cover_last_12wk to null and add an honesty_flag.
- recommended_prep for each item should sit between
attach_rate × forecast_covers.low and attach_rate × forecast_covers.high,
rounded to a sensible unit. If you must propose a value outside that
band, explain why in notes and flag the field.
- For items where the operator has flagged variability ("salsa verde
trends low on graduation Fridays"), honour the trend in
recommended_prep and surface the reason in notes.
- what_if_wrong always populated. under_prep_likely_sellouts_first
ranked by attach rate × stability; over_prep_least_costly_leftovers
ranked by shelf life × cost of goods.
- forecast_confidence is calibrated against your own calibration
history. If your last twelve forecasts landed inside the range
eight out of twelve, do not return 0.95 — return something in the
0.65 - 0.75 band. Be honest.
- operator_override_required_for: list any category where the data is
too thin for a recommendation (new menu item, fewer than 6 weeks of
data, seasonal item starting tomorrow).
- honesty_flags: any field where you are reaching, where the data is
noisy, where the model disagrees with the operator's own past
notes, where an event has only one source.
No commentary. JSON only.
```
---
### Call: Find tomorrow's local events within 1 mile
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: search grounding
```
You find local events that may drive foot traffic past a food cart
tomorrow.
Given:
- Cart coordinates (lat, lng).
- Tomorrow's date (ISO).
- Cart trading window (e.g. 11:00 to 14:30 local).
- Operator's logged "anchor sources" — schools, sports venues, festival
organisers, churches, mosques, temples within 1 mile that have driven
spikes in the past.
Search for:
- School graduations, sports games, performances at the named anchor
schools.
- Concert and sports events at nearby venues.
- Block-party and street-festival permits in the cart's neighbourhood.
- Farmers' market and night-market schedules within 1 mile.
- Construction site openings / closings that affect daytime population
in the immediate area.
- Public holidays in the cart's country that fall on tomorrow.
Hard rules:
- Use `google_search` grounding for every event named. If you cannot
find a citation, do NOT include the event.
- Events more than 1 mile from the cart are excluded unless they are
major venues (stadium, university, convention centre) with attendance
> 5,000.
- Preserve the event's name verbatim ("Eastmont High graduation",
not "high school graduation event"). Preserve the event start time
in local time.
- Estimate magnitude_estimated_pct conservatively. A small parade two
blocks away is +5% to +15%, not +60%. Be honest about the size of
the inference.
- Each event gets one short evidence_quote (≤ 140 chars) drawn from
the source — the school calendar text, the city permit text, the
festival organiser's post.
Output the response as JSON in the text body (NOT via `responseSchema`
— `responseSchema` and `google_search` cannot be combined in the same
Gemini call today). Server-side: parse the JSON, then read citation
URLs from the response's `groundingMetadata.groundingChunks[].web.uri`
and zip them into each event by index — do NOT ask the model to
include URLs in the JSON body; it will hallucinate them.
If you find no events at all, return an empty array. Do not invent
events to populate the list.
No commentary outside the JSON.
```
---
### Call: Parse day-end voice memo → `DayEndRecord` schema
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You parse a 30-90 second voice memo recorded by a street-food operator
at the end of service. The operator may speak Spanish, Mexican Spanish,
Brazilian Portuguese, Vietnamese, Thai, Tagalog, Cantonese, Hokkien,
Mandarin, Yoruba, Igbo, Hausa, Swahili, Arabic, French, Punjabi, Tamil,
Urdu, Bengali, or English, often code-switching mid-sentence.
Typical memo: "we sold 244 covers, ran out of suadero at 12:55, half
a kilo of salsa verde in the bin, one bag of tortillas left over,
weather hotter than forecast, lunch rush hit at 11:30 instead of
12:00." The Vietnamese cart owner says: "bán được 180 cái, hết pa-tê
lúc mười một giờ rưỡi, còn lại tám cái bánh mì, trời mưa từ chín giờ."
Hard rules:
- transcript first, verbatim, in the source language. Do NOT translate.
- Preserve item names verbatim. "suadero", "salsa verde", "pa-tê",
"bánh mì", "pad kra-pao", "akara" — never normalised.
- total_covers is the number the operator named. If they did not name
one, set null and add a follow_up.
- sold_out_items: each item the operator said "ran out of" or "hết"
or "agotamos" or "fini", with the time if named.
- leftover_items: each item with a quantity and unit if the operator
named one ("half a kilo", "một cân", "two bags").
- anomaly_flag: only TRUE if the operator explicitly says "ignore this
for next week" or "this was a one-off" or similar. Do not infer
anomalies from numbers alone.
- conditions_observed: free text drawn verbatim from the memo when the
operator notes weather or rush timing.
No commentary outside the structured output.
```
---
### Call: Extract / normalise the operator's own item categories
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive the operator's sales history (CSV from Square / Toast /
Loyverse / Foodics or manual entries) and produce a normalised
category list — but the verbatim names ALWAYS dominate.
Hard rules:
- category_verbatim is the name the operator's POS or notebook uses,
exactly as written. Even with typos. "al pastor", "Al Pastor",
"al-pastor" are distinct strings, but you propose they fold into
one canonical category — the user confirms.
- category_canonical is your normalisation suggestion. The UI shows it
greyed out next to the verbatim, so the operator can see what you
proposed but never has it imposed.
- Conservative folding. "al pastor — taco" and "al pastor — burrito"
are DIFFERENT categories; they have different attach rates and
different prep implications. Do not fold them.
- Do NOT translate. "bánh mì pa-tê" stays in Vietnamese. "akara" stays
in Yoruba. The English gloss is optional metadata only.
- If you see a category fewer than three times in the history, flag it
as low_confidence and do not propose a canonical.
Output: an array of category objects with verbatim, canonical (or
null), confidence (0-1), and occurrence_count. No commentary.
```
---
### Call: Score yesterday's forecast vs actual → `CalibrationEntry`
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive yesterday's forecast and yesterday's actual day-end
record. Produce a calibration entry that the next forecast call will
see in its prompt context.
Hard rules:
- within_range is strict: actual_covers >= forecast_low AND
actual_covers <= forecast_high.
- signed_error_pct = (actual - target) / target × 100. Positive means
the actual exceeded the target.
- drivers_that_landed: list driver headlines whose magnitude was
consistent with the actual outcome.
- drivers_that_missed: list driver headlines whose direction or
magnitude was wrong (a forecast that named "graduation +30%" when
the actual was -5% counts as a missed driver).
- operator_one_line_note: if the operator left a note in the day-end
memo about why yesterday went how it went, paste it verbatim.
The calibration entry is used to keep future forecasts honest about
confidence. Do not soft-grade. If yesterday was outside the range,
say so; if a driver missed, name it.
No commentary outside the structured output.
```
---
### Call: Read prep sheet aloud at locking-up pace
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, unhurried. Pick the Gemini 2.5 Flash TTS voice whose
`languageCode` matches the operator's preferred language. Prefer the
gender the operator selected in settings; fall back to whichever is
available rather than blocking.
Pre-process the text before sending it to TTS:
- Build the input string by concatenating the prep sheet line by line.
- Render ranges as words: "twenty to twenty-five", NOT "20-25".
- Render units in full: "kilos" or "kilograms" not "kg"; "dozen" not
"dz"; "litres" not "L". Pronouncing punctuation is the failure mode.
- At each line break, insert a single ellipsis (`…`) so the TTS
model produces a natural pause. At paragraph breaks, insert a
blank line plus an em-dash (`—`). Gemini 2.5 TTS does not support
SSML `` — these textual cues are how you signal pace.
- Skip citation URLs and honesty_flags from the read-aloud (they are
for the screen, not for the locking-up moment).
- Target rate: ~120 words per minute — the operator is writing
numbers on a kitchen pad as you read.
Style direction: prepend ONE short directive sentence to the text
input, exactly like: "Read calmly and clearly, as a prep brief
spoken aloud to a tired cook at the end of a long Thursday. …".
There is no separate `style` API field on Gemini 2.5 TTS; the
directive sentence inside the input is how style is conveyed.
Phoneme overrides (Spanish ñ, Vietnamese tonal marks, Thai vowel
diacritics, Arabic emphatic consonants) are NOT exposed by Gemini
2.5 TTS — no SSML `` tag. Pronunciation comes from the
chosen voice's native locale.
```
---
### Call: Year-end consolidation (long context over every service day)
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none, long context over the year)
```
You receive the operator's entire trading year — every service day,
every prep sheet, every day-end record, every calibration entry.
Produce a year-end summary the operator can hand their accountant
and use for next-year planning.
Hard rules:
- Do NOT generate national-average comparisons. The summary is the
operator's own year, against itself.
- Weekday rhythms: average covers per weekday over the year, with
high-water and low-water dates and the operator's notes for each.
- Seasonal rhythms: which months ran hot, which ran cold, and the
weather + events that the calibration history says actually drove
them.
- Top 10 surprise days: days where the forecast was off by more than
30% in either direction, with the operator's note on why.
- Calibration trend: how the range-hit rate moved across the year.
Did the model get more honest? Less?
- Items with biggest attach-rate shifts year-over-year, flagged for
the operator's review.
- Suggested baseline-window adjustments for next year: weekdays where
12 weeks is too short or too long given the operator's actual
seasonality.
Output as JSON. No commentary outside the structured output.
```
## 5. Use cases & content to include
Build dedicated UI sections or flows for each of these — they tell you what content the app must support.
- **The first Thursday.** A new operator has been logging day-ends in the app for exactly 12 weeks. Tonight is the first Thursday the app has enough data to forecast Friday. The "Tomorrow" button on the home screen lights up for the first time; the prep sheet renders with a "calibrating week 1 of 12" badge that fades after the first dozen forecasts. The drivers are humble, the confidence is in the 0.6 band, and the operator's overrides will teach the model their corner faster than any baseline.
- **The graduation Friday in East Oakland.** Doña María, taco truck on International Boulevard, taps "Tomorrow" at 16:12 on Thursday. Eastmont High has graduation at 4:30 pm two blocks down; the events-finder grounds this from the district's calendar with a citation URL. Three of her last four graduation Fridays ran 30 to 45 percent above her Friday average. Weather is clear, 72°F at noon. The prep sheet lifts al pastor to 8 kg (from her 6.5 kg Friday baseline), nudges salsa roja up and salsa verde down (the model learned from her own last three graduation Fridays that the salsa mix shifts), and adds an extra eight dozen tortillas. She overrides suadero down to 2.5 kg because her hands are tired tonight. The kitchen prints; tomorrow is decided.
- **The rainy Wednesday morning in Hanoi.** Chị Hằng, banh-mi cart on Bà Triệu, taps "Ngày mai" at 17:30 on Tuesday. The weather payload says rain probability 78 percent between 7 and 10 am. The model knows that her rainy-morning Wednesdays run 20 to 30 percent below her dry-morning Wednesdays; the office workers still come for lunch but the construction crews from the site on the corner do not. The prep sheet trims pa-tê by 1.5 kg, trims bread by 30 loaves, and keeps the egg supply flat (office workers eat more egg banh-mi on rainy mornings — the model noticed). Chị Hằng overrides bread down a further 20 loaves; the last rainy Wednesday she had 40 left over. The override is logged for next week.
- **The TikTok spike.** A user's pad-thai cart in Bangkok went viral on Sunday — 480 covers against an average Sunday of 160. Today is Tuesday and the operator is about to forecast Wednesday. The app surfaces a flag: "Sunday was 200 percent above your average. Was this a one-day spike, or the new normal? Excluding Sunday will keep your baseline honest; including it may have us over-prep all week." The operator taps "one-day spike, exclude from baseline." The Tuesday prep sheet looks normal; the Sunday memory is preserved but not blended.
- **The pulled permit.** The block-party permit two blocks over got pulled three days ago. Last week the app named the block party as the +15 percent driver for Friday's forecast. Tonight the events-finder no longer returns the block party. The forecast steps down, the prep sheet shrinks, and a small banner says "Block party scheduled last week is no longer on the city permit list — forecast adjusted accordingly."
- **The competing-truck addition.** Doña María's lot manager texts at 3 pm: "second truck added tomorrow, kebab cart at the other end." The app's lot-schedule integration picks this up at the 03:00 pre-fetch. The forecast notes "second vendor at the lot tomorrow — kebab, not taco; modest cannibalisation expected, ~5-10%." The prep sheet trims slightly; the operator accepts.
- **The two-cart operator.** A pupusa operator in San Salvador runs one cart in Centro on Friday-Saturday and a second cart in Antiguo Cuscatlán on Sunday. Each cart has its own baseline and its own forecast. The app never blends them. The Centro Saturday and the Cuscatlán Sunday show side by side on the dashboard with their own drivers.
- **The Sunday calibration card.** Yesterday Doña María served 244 covers against a forecast of 220-270 with a target of 240. The Sunday morning card says: "Forecast: 220-270 (target 240). Actual: 244. You sold inside the range. Eastmont graduation landed (+28%, we said +25-35%). Salsa verde sold faster than the last three grad Fridays — your trend may be reverting. Suadero left over: 0.4 kg." The operator taps "noted, used less suadero on purpose" and the model logs the override reason for next week.
- **The kitchen partner.** A two-person banh-mi family operation: the books partner in Hanoi reads the prep sheet on her phone, taps "share with kitchen," and the kitchen partner — her husband, no smartphone — gets a one-page laminated card printed at the corner shop that says the same prep sheet in his categories, in his language, with the weather as a single line at the top.
- **The dispute.** A supplier raised prices on bread last week. The operator reads the prep sheet, sees that the recommended bread order is still costed at last week's price, and overrides. The app's price-book learns from the override; next week's prep sheet shows the new bread cost in the what-if-wrong panel.
- **The Lagos jollof cart on Friday.** A jollof cart parked outside an office on Awolowo Road in Ikoyi taps "Tomorrow" at the end of Thursday's shift. The events-finder grounds an office happy hour at the building two doors down (a Facebook event with 280 RSVPs, citation URL surfaced). Weather is hot and dry. The prep sheet steps jollof up by 20%, suya up by 30%, and suggests double the small-chops because office happy hours are small-chops heavy. The operator overrides small-chops down by 20% — she knows that crowd, she has been parked there for eleven years.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent.
1. **Welcome / sign-in.** A photographed-looking image of a taco truck at end of service: the window steaming, the operator wiping the counter, the last customer walking away with a foil-wrapped order, dusk light. One paragraph: "Tomorrow's prep, decided in five minutes — using only the last twelve weeks you actually worked." Google sign-in by default; phone-number sign-in next to it; Apple sign-in as a third option. Below: "Try with the sample cart" → loads the demo data in section 8a.
2. **Empty state — "Set up your cart."** Three short steps: 1) Name your cart, pick a language, set your trading days. 2) Connect a POS (Square, Toast, Loyverse, Foodics, manual). 3) Mark your "anchor sources" — the schools, venues, churches within 1 mile that drive your foot traffic. The third step has a small map; the operator drops pins.
3. **Home / Today + Tomorrow.** The home screen shows today's prep sheet (locked, with progress indicators if the operator is mid-day-end count) and tomorrow's "Tap to forecast" button. The "Tomorrow" button is the largest interactive element on the screen.
4. **Tomorrow — the prep sheet.** The headline card: forecast range, recommended target (smaller, faded), percent above/below weekday average. Below: three driver cards with citations and "more reasons" expand. Below: the items list — one row per category, each row with a range, a recommended prep number, and an inline edit affordance. Below: the what-if-wrong panel. At the bottom: "Lock prep sheet" (primary) and "Read aloud" (secondary, opens TTS player). Sticky footer on mobile: the "Lock prep sheet" button is always one thumb-tap away.
5. **Item detail.** Tap any item in the prep sheet → modal/sheet with the item's last-12-week chart (covers vs sold), attach-rate trend, leftover trend, and the operator's notes from past day-ends mentioning this item. The operator can adjust the recommended prep here and see the impact in the what-if-wrong panel update live.
6. **Driver detail.** Tap any driver card → expanded view with the full evidence quote, the citation URL (opens in browser), the historical magnitude on similar days (e.g. "your last 4 graduation Fridays: +35%, +28%, +42%, -8% — the last one was the day the school changed venue"), and the model's confidence breakdown.
7. **Day-end / Close-out.** A one-button voice memo with a prompt: "How did today go?" The operator talks for 30-90 seconds; the parsed `DayEndRecord` appears with editable fields. A "covers" numeric input is always visible in case the operator prefers typing. A "this was a one-off" toggle marks the day as excluded from the baseline.
8. **Friday calibration card.** A weekly summary card that surfaces every Friday after service (configurable per cart). Headline: forecast range, actual covers, in-range or out-of-range badge. Below: driver-by-driver landed/missed list. Below: the operator's own note from the day-end memo. At the bottom: a "what to change for next week" suggestion the operator can accept or dismiss.
9. **Twelve-week dashboard.** A grid showing the last 12 of each weekday side by side. Each cell shows covers, weather, primary driver, and whether the day was excluded. Hover/tap a cell to see the prep sheet that was locked that day vs the day-end record. This is the operator's working memory made visible.
10. **Cart settings.** Cart name, language, time zone, trading weekdays + windows, currency, POS connection, anchor sources (with the map), kitchen-partner email/phone for shared prep sheets, accountant email for the year-end export, weather provider, lot-schedule integration if applicable.
11. **Multi-cart switcher.** A top-bar dropdown for operators with more than one cart. Each cart is fully isolated; the data, the categories, the baseline, the calibration all live per-cart. A pinned "compare two carts" view lets the operator look at both side by side without blending them.
12. **Year-end export.** A button that produces every prep sheet, every actual, every variance, every calibration in CSV + PDF, in the cart's currency, for the operator's accountant and for end-of-year supplier renegotiation. The PDF includes the year-end consolidation call's summary on the first page.
13. **Footer.** "Built for the carts that already know their corner." Privacy: "Your sales data is yours. We never train on it." Capabilities `(i)` icon in header.
## 6b. First-visit onboarding
Show a **first-visit onboarding** the first time a visitor lands on the app (detect via `localStorage` flag; do not show on return visits). Three slides, dismissible at any time. Persistent re-entry: a `?` icon in the header reopens it.
**Slide 1 — What this is.**
- Headline: "Welcome to Prep Sheet."
- Subhead: "Tomorrow's prep, decided in five minutes — using only the last twelve weeks you actually worked."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic forecasting app: the forecast is always a range; the operator decides every line; the events that drive the forecast are grounded with citations; the baseline is your own twelve weeks, not a national average.
- Visual: a small annotated illustration of a prep sheet card with the range, the three drivers, and the item list labelled — not a generic chart icon.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample cart".
- A live demo input pre-loaded with Doña María's twelve weeks of Friday data from the seed content in section 8a, plus tomorrow's grounded events (Eastmont High graduation) and weather (clear, 72°F at noon).
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the range that resists collapsing to a single number; the three drivers with citation URLs; the item categories preserved verbatim in "al pastor" and "salsa verde").
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample cart in `/data/seed-cart/` for your own POS export."
- "Adjust the anchor-sources list in `/data/anchors/` for your corner's schools, venues, and churches."
- "Wire up your Gemini API key, Firebase project, and weather provider via the env-var list in the capabilities panel."
- Primary CTA: "Use this template" → links to AI Studio Build remix entry point.
- Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again).
**Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`.
**Don't:**
- Don't gate content behind the modal. The page beneath must be fully usable.
- Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only.
## 6c. Capabilities info button (persistent in header)
Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**.
**Panel contents (in this order):**
**Gemini capabilities used (the hero list):**
- **Gemini 3.5 Flash (long context)** — the forecast call sees the last 12 occurrences of tomorrow's weekday in one call, plus grounded events, plus weather, plus your calibration history. The headline range and the per-item recommendations come from a single reasoning step over your own twelve weeks.
- **Gemini 3.5 Flash + grounded search** — finds tomorrow's local events (school graduations, sports games, festival permits, concerts) within 1 mile of your cart, with citation URLs you can click through to verify.
- **Gemini 3.5 Flash (structured output)** — the prep sheet is a typed JSON object; every range, every driver, every item recommendation is schema-enforced; single-number forecasts are not possible at the schema level.
- **Gemini 3.5 Flash (audio input)** — your end-of-day voice memo parses into structured per-item counts and leftovers in any language you speak.
- **Gemini TTS** — reads tomorrow's prep sheet aloud at locking-up pace, in your preferred language, hands-free while you count the till.
- **Firebase Auth** — Google, phone-number, and Apple sign-in. Sharing prep sheets with a kitchen partner uses magic-link email.
- **Firestore** — stores your cart, your sales history, your prep sheets, your day-end records, your calibration entries. Syncs across devices in real time.
- **Firebase Storage** — keeps your raw day-end audio memos and rendered PDF prep sheets at original quality, forever.
- **Open-Meteo (or your choice)** — the hourly weather forecast that the Gemini call uses as a forecast driver. Open-Meteo is the free default; OpenWeather and weather.gov are user-config alternatives.
- **Cost note** — see the detailed breakdown in 6d. A typical four-day-a-week cart spends about $0.60 of Gemini API per week on forecasts, calibration, and day-end memo parsing.
- **Privacy note** — your sales data, prep sheets, supplier prices, and day-end memos are private to you and the kitchen partners you invite. 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. Your sales data is your competitive information; we treat it like a private ledger.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b
- Email: see section 4b
- Scheduled jobs: see section 4b
- External APIs: see section 4b
**Environment variables you'll need to configure:**
- `GEMINI_API_KEY` — your Google AI Studio API key
- `FIREBASE_PROJECT_ID` — your Firebase project id
- `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only)
- `WEATHER_PROVIDER` — `open-meteo` (default) | `openweather` | `weather-gov`
- `OPENWEATHER_API_KEY` — optional, only if `WEATHER_PROVIDER=openweather`
- `GOOGLE_MAPS_API_KEY` — optional, for higher-fidelity geocoding of "the school two blocks down"
- `POS_PROVIDER` — `square` | `toast` | `clover` | `loyverse` | `foodics` | `manual`
- `POS_OAUTH_CLIENT_ID` / `POS_OAUTH_CLIENT_SECRET` — optional, only if POS provider is connected
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: the long-context forecast call is billed per token of input — a 12-week window for a 4-day-a-week cart costs about $0.04 per forecast. Daily forecasts × 4 service days × 4 weeks ≈ ~$0.65/month per cart.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this cart's data" — gone in 60 seconds, including raw audio and PDFs in Storage), what is never sent for training.
**Documentation links:**
- AI Studio Build docs
- Gemini API long-context, structured output, grounded search, audio input, TTS docs
- Firebase Auth, Firestore, Firebase Storage docs
- Open-Meteo API docs
- A short note on how to add a new weather or POS provider
**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)
- **Forecast tomorrow (Gemini 3.5 Flash, medium thinking, long-context over last 12 of weekday)** — ~30k input tokens (12 service-day records + grounded events + weather + calibration history) + ~1.5k output tokens. ~$0.045/forecast. A 4-day-a-week cart running daily forecasts costs ~$0.18/week.
- **Find tomorrow's local events (Gemini 3.5 Flash + grounded search)** — ~$0.002/run. Runs once per service-day pre-fetch.
- **Parse day-end voice memo (Gemini 3.5 Flash, low thinking, audio input)** — a 60-second m4a memo + ~500 output tokens ≈ ~$0.003/memo. ~$0.012/week for a 4-day-a-week cart.
- **Extract / normalise categories (Gemini 3.5 Flash, low thinking)** — runs once at onboarding, occasionally on POS-history-import. ~$0.001/run.
- **Calibration scoring (Gemini 3.5 Flash, low thinking)** — ~$0.001/service day. ~$0.004/week.
- **TTS read prep sheet aloud (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A 600-character prep sheet read-aloud ≈ $0.002/run. Cached per prep sheet; charged once per generation.
- **Year-end consolidation (Gemini 3.5 Flash, medium thinking, long-context over the year)** — runs once a year. ~$1.20 for a 4-day-a-week cart with 200 service days.
- **Expected per-week cost on a 4-day-a-week cart:** ~$0.20 of Gemini API spend. **Per-month:** ~$0.85. **Per-year (including year-end consolidation):** ~$11.50 per cart.
- **Weather:** Open-Meteo is free; OpenWeather is ~$0/month at this volume (well within the free tier). No incremental cost.
- **Storage:** Firebase Storage standard tier, ~$0.026/GB/month. A year of day-end m4a memos (~30 seconds each, ~200 service days) ≈ ~50 MB. A year of prep-sheet PDFs ≈ ~20 MB. Storage cost is effectively zero for a single cart.
## 7. Design language
- **Mood:** A kitchen prep board on a Thursday afternoon. Not a SaaS dashboard. Not a forecasting app for executives. The four-minute window between locking the till and starting tonight's marinade, with a phone propped against the salt cellar, the lights still buzzing, the smell of fryer oil cooling. Honest, fast, legible.
- **Typography:** A clean industrial sans-serif for the chrome (Inter or Geist), at sizes the operator can read at arm's length. A monospaced face for numbers (JetBrains Mono or IBM Plex Mono) — the operator scans figures, not paragraphs, and the columns line up. A small display face only on the headline forecast card (Söhne or a similar editorial sans) — never decorative.
- **Palette:** Deep prep-board grey `#1F1F1F` for the background of the headline card, off-white `#F8F6F1` for the body, a warm prep-tape orange `#E08B3A` for the recommended prep numbers (small, deliberate), a quiet sage `#5B7A6B` for the in-range / on-target chips, a firm tomato `#C8473A` only for sold-out / out-of-range / honesty flags. A muted lot-lamp yellow `#D7B85F` only for "operator override required" badges. Borrowed from a stainless-steel prep table and a Sharpie on butcher paper, not from analytics dashboards.
- **Imagery:** Photographs of the actual cuisines in the seed content — al pastor on the trompo, banh-mi assembled on the counter, pad-thai being tossed, jollof in the pot. Not generic stock food photography. Generated via Nano Banana 2 with prompts emphasising "warm afternoon kitchen light, real hands, real condensation on a soda can at the edge of frame".
- **Hand-feel touches:** The prep sheet itself looks like a print-out — slightly off-white background, a faint paper grain, a thin rule between sections, the date in a top-right corner like a kitchen ticket. The "Lock prep sheet" button has a satisfying solidness; when tapped, the prep sheet header shifts to a "locked" state with the date stamped, like a kitchen punch-clock.
- **Spacing:** consistent 4-px base. Tighter than usual on the prep-sheet card itself (the operator wants the whole thing visible without scrolling on a phone), looser everywhere else.
- **Radius:** consistent token set (6 / 12 / 20 px). Headline cards use 12; the print-out-style prep sheet uses 6; the welcome card uses 20.
- **Shadows:** subtle, layered, slightly warm. The headline forecast card sits one shadow-step above the body. Avoid heavy drop-shadows.
- **Motion:** purposeful — entrance fades, the "Lock prep sheet" stamp animation, the calibration card slide-in on Friday. Respect `prefers-reduced-motion`. No bouncing splash animations. No theatrical hero animations. The headline card's range animates from 0 to the forecast range on first render; respect reduced-motion by jumping rather than animating.
- **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Log one day-end memo to start your baseline").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible cart names, neighbourhoods, item categories, dates, voice memos that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on real street-food vehicle archetypes — East Oakland taco truck, Hanoi banh-mi cart, Bangkok pad-thai cart, Lagos jollof cart, Centro pupusa cart — but never claim that a fictional vendor is a real operating business.
- Tone: warm, direct, practical, free of corporate language. This template is for a working operator at the end of a long shift, not for an investor deck.
- Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'. No 'AI-powered forecasting'.
- Body copy: short paragraphs (2-4 sentences). Use lists where appropriate.
- Plain language. Avoid jargon — except where the operator already speaks the jargon ("attach rate" is fine for someone with a POS; "covers" is fine for anyone running a service window).
- Where the app outputs AI-generated content (the headline range, the drivers, the item recommendations), never label it as "AI says" — let it speak naturally. Use small uncertainty cues only where epistemic honesty requires them (a low-confidence driver shows a faint badge; tapping reveals what the model is reaching for and why).
## 8a. Seed content (use these specific examples)
Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, dates, and snippets verbatim where helpful, or generate close variants that sit in the same world.
**Sample carts (sidebar):**
- "Doña María's Tacos" (East Oakland, International Boulevard lot, Thu-Sun, USD) — taco truck, 11 years on the same lot, run by María and her daughter. Categories: al pastor, suadero, pollo asado, carnitas, salsa verde, salsa roja, salsa de árbol, tortillas (corn), tortillas (flour), horchata, agua de jamaica.
- "Bánh Mì Chị Hằng" (Hanoi, Bà Triệu corner near the construction site, Mon-Sat, VND) — banh-mi cart, 7 years, run by Hằng on her own. Categories: bánh mì pa-tê, bánh mì thịt nguội, bánh mì trứng, bánh mì xíu mại, đồ chua, pa-tê, bánh mì (loaves).
- "Pad Thai Pim" (Bangkok, off Sukhumvit Soi 38, Wed-Sun, THB) — pad-thai cart, 4 years, run by Pim and her cousin. Categories: pad thai goong, pad thai gai, pad thai jay, pad kra-pao, suea rong hai (cried-tiger beef), nam-jim jaew, kanom buang.
- "Jollof on Awolowo" (Lagos, Ikoyi office corridor, Mon-Fri lunch, NGN) — jollof cart, 11 years, run by Mama Chinedu and one apprentice. Categories: jollof, fried rice, suya beef, suya chicken, small chops, moin moin, plantain.
- "Pupusería Centro" (San Salvador, Centro centro on Friday-Saturday + Antiguo Cuscatlán on Sunday, USD) — two-cart pupusa operator. Categories: pupusa de queso, pupusa revuelta, pupusa de frijol con queso, curtido, salsa roja.
**Sample prep sheet in detail view (this is what the demo should show):**
- **Cart:** Doña María's Tacos
- **For service date:** Friday, 29 May 2026
- **Generated at:** Thursday, 28 May 2026, 16:12 local (America/Los_Angeles)
- **Baseline window:** Last 12 Fridays Doña María worked (one Friday excluded: 17 April 2026, "lot was closed for repaving" — operator's note)
- **Average covers in window:** 196
- **Forecast covers:** 220 to 270
- **Forecast covers recommended target:** 245
- **Forecast vs weekday avg:** +25%
**Drivers (3):**
1. **Eastmont High graduation, 4:30 pm, two blocks down** — magnitude +25 to +35%, confidence 0.82, driver_type school_calendar, evidence_quote "Eastmont High School Class of 2026 Commencement, Friday May 29, 4:30 pm, Eastmont Stadium" (citation: oaklandschools.org/eastmont/calendar/2026-05-29)
2. **Clear and 72°F at noon** — magnitude +12 to +20%, confidence 0.76, driver_type weather, evidence_quote "Open-Meteo: high 72°F, low 58°F, precip 4%, clear at noon"
3. **No competing truck at the Coliseum lot tomorrow** — magnitude +5 to +12%, confidence 0.65, driver_type competing_vendor, evidence_quote "Lot schedule: Coliseum lot 11am-3pm, Doña María's Tacos only" (citation: offthegrid.com/coliseum/2026-05-29)
**Hidden drivers (2):**
4. **Weekday baseline trending up over last 6 weeks** — magnitude +3 to +6%, confidence 0.58
5. **Construction crew at MacArthur project — flagged in 2 of last 3 weeks' memos as a smaller Friday driver** — magnitude +2 to +5%, confidence 0.51
**Items (10) — each in Doña María's verbatim categories:**
| Category | Forecast range | Recommended prep | Attach rate (last 12 wk) | Notes |
|---|---|---|---|---|
| al pastor | 7.5 to 8.5 kg | 8 kg | 0.034 kg/cover | "trend toward al pastor on grad Fridays — was 0.041 last grad Friday" |
| suadero | 2.8 to 3.4 kg | 3 kg | 0.013 kg/cover | "consistent attach across the window" |
| pollo asado | 4.0 to 4.8 kg | 4.5 kg | 0.018 kg/cover | "drops on hot Fridays; today is 72°F so flat" |
| carnitas | 2.2 to 2.8 kg | 2.5 kg | 0.010 kg/cover | "small leftover last 3 Fridays — consider 2.3 kg" |
| salsa verde | 2.5 to 3.0 L | 2.5 L (light) | 0.011 L/cover | "trend toward salsa roja on grad Fridays — light tonight" |
| salsa roja | 3.5 to 4.5 L | 4 L | 0.016 L/cover | "graduation Fridays consistently lift salsa roja" |
| salsa de árbol | 1.4 to 1.7 L | 1.5 L | 0.006 L/cover | "small but stable" |
| tortillas (corn) | 28 to 34 dozen | 32 dozen | 0.13 doz/cover | "warm Fridays lift corn slightly" |
| tortillas (flour) | 8 to 11 dozen | 10 dozen | 0.04 doz/cover | "flat" |
| horchata | 14 to 17 L | 16 L | 0.066 L/cover | "warm afternoon lifts horchata; clear at 72°F is your sweet spot" |
**What if I'm wrong:**
- Under-prep likely sellouts first: al pastor, salsa roja, tortillas (corn). Estimated lost covers at low prep: ~30. Estimated lost revenue: ~$420 (avg ticket $14).
- Over-prep least costly leftovers: salsa de árbol (refrigerates 5 days), carnitas (refreezes), horchata (keeps 2 days cold). Estimated waste cost at high prep: ~$45 (using your last 12 weeks' waste pricing).
**Weather summary:**
- Source: Open-Meteo
- High: 72°F (22°C), Low: 58°F (14°C)
- Precip probability during service: 4%
- Conditions at noon: clear
**Honesty flags:**
- "drivers[2] — competing_vendor source has been wrong twice this year; verify before locking prep"
**Forecast confidence:** 0.74
**Operator override required for:** "elote" (only 4 occurrences in window, too thin for a recommendation)
**Sample day-end voice memo (parses into DayEndRecord):**
- Audio (60 seconds, Spanish-English code-switch): "Hoy fue 244 covers, ran out of suadero a las doce y cincuenta y cinco, salsa verde sobró como medio kilo, tortillas of corn había un paquete left over, weather was hotter than they said, lunch rush hit at 11:30 in vez de 12:00, no anomaly, just a normal grad-Friday but I underprepped suadero."
- Parsed: total_covers 244; sold_out_items [suadero at 12:55]; leftover_items [salsa verde ~0.5 kg, tortillas corn ~1 packet]; conditions_observed "hotter than forecast, lunch rush hit at 11:30 instead of 12:00"; anomaly_flag false; operator_notes_verbatim (the whole memo).
**Sample calibration card (Friday 22:00 local, after service):**
- Forecast: 220-270 (target 245). Actual: 244. **Within range.**
- Drivers that landed: "Eastmont graduation (+28% — we said +25 to +35%)", "Clear and 72°F (+15% — we said +12 to +20%)".
- Drivers that missed: none.
- Operator's note: "underprepped suadero by half a kilo — should have stuck to 3 kg, not the 2.5 kg I overrode to."
- Suggestion for next week: "consider holding the suadero recommendation when you override down on tired-hands nights — your prep judgement is overcorrecting".
**Sample voice copy:**
- Onboarding: "Tomorrow's prep, decided in five minutes — using only the last twelve weeks you actually worked."
- Empty baseline: "Log one day-end memo to start your baseline. After twelve weeks, the 'Tomorrow' button lights up."
- Pre-fetch in progress: "Pulling tomorrow's weather and events…" (3-5 seconds, runs at 03:00 local automatically; this state is only seen if the operator opens the app before the pre-fetch finishes)
- Forecast ready: "Friday — expect 220 to 270 covers, 25 percent above your Friday average."
- One-driver fallback: "We could not find any local events near your cart for tomorrow. Forecast uses your weekday baseline and the weather only."
- Lock confirmation: "Prep sheet locked for Friday, 29 May. The kitchen has it."
- Calibration ready: "Yesterday: forecast 220 to 270, you served 244. Within range. Tap to see what landed and what missed."
- Anomaly question: "Sunday was 200 percent above your Sunday average. Was this a one-day spike, or the new normal? Choose to include or exclude before next Sunday's forecast."
**Sample kitchen-partner share email:**
- Subject: "Friday's prep — Doña María's, 29 May"
- Body: "Mama, the prep sheet for Friday is locked. Al pastor 8 kg, suadero 3 kg, pollo asado 4.5 kg, carnitas 2.5 kg, salsa verde light at 2.5 L, salsa roja up at 4 L. Tortillas corn 32 dozen, flour 10 dozen. Horchata 16 L. Weather clear and 72°F, Eastmont graduation at 4:30 pm two blocks down. Tap to open."
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a taco truck at end of service: the window steaming, the operator wiping the counter with a damp rag, the last customer walking away with a foil-wrapped order, dusk light, the city behind. Generate via Nano Banana 2 with a prompt emphasising "wooden curb, warm dusk light, hands of a woman in her forties wiping a stainless counter, real condensation on the window, no people's faces in focus, soft shadow under the truck".
- **App icon / wordmark:** Set in the chrome sans-serif. A small underline mark like a pencil tick. No icon — just type.
- **Empty-state illustration:** A simple line drawing of a clipboard with a prep sheet and a pencil clipped at the top. Hand-drawn aesthetic, not a flat icon.
- **Demo cart photographs:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "real prep board, real ingredients, warm afternoon kitchen light, no faces in focus, no logos, slight asymmetry, soft shadows". Each demo cart should look photographed, not rendered.
- **Stock fallbacks:** If image generation fails, fall back to the photographed sample cart from `/public/samples/sample-cart.jpg`. Never to a "🌮" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock photography. Prompt for warmth, asymmetry, and slight imperfection — avoid the glossy 'AI render' look.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift.
- **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only.
### Build-time asset manifest (explicit specs)
Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs.
**Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id:
- `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted.
- `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence.
- `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible.
**Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill):
- `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt.
- `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline.
- `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails.
**Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint.
**Hard rules**
- Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error.
- No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code.
- Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`).
- File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and disabled states.
- Forms validate inline and show specific error messages (not "Invalid input").
- Loading states use skeletons that match the eventual layout, not spinners.
- Empty states explain the next action with a button whose label fits THIS app's domain: "Log one day-end memo", "Connect your POS", "Drop a pin on your anchor school" — never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- The forecast call streams: the headline range arrives first (within a second of the tap), then the three drivers stream in, then the items list, then the what-if-wrong panel. Each chunk has a "thinking…" indicator before content arrives.
- If the events-finder call returns nothing, the forecast still produces a clean prep sheet with weather + weekday baseline only — and the UI says so explicitly.
- If a Gemini call fails, show a calm, specific error ("We couldn't reach the forecast service — here's yesterday's prep sheet to start from, and we'll retry in 60 seconds") and offer retry.
- The forecast range and the recommended prep numbers are always editable. Editing the recommended prep updates the what-if-wrong panel live; editing the forecast range itself surfaces a "are you sure?" confirmation because that's a deeper override.
- The headline card's range animates from 0 to the forecast range on first render; `prefers-reduced-motion` falls back to instant.
- The "Lock prep sheet" stamp animation is 300 ms; `prefers-reduced-motion` falls back to a colour change only.
## 11. Tech & responsive requirements
- **Deterministic arithmetic — server-side TypeScript only.** Any money-split / settlement / total / VAT / unit-conversion math runs in TypeScript on the server using integer-cents arithmetic. The Gemini call only extracts entities (line items, payer, currency code, raw quantities) into structured JSON. Never let the model do the sum — Gemini will return plausible-looking arithmetic that does not actually balance.
- **TTS markdown-stripping preprocessor:** before sending any user-authored markdown to `gemini-3.1-flash-tts-preview`, strip non-spoken markdown: `#`/`##`/`###` headings (keep the title text), `**bold**` (keep the inner text), `[label](url)` (keep `label`, drop URL), `` ``` `` fenced code blocks (skip entirely), `>` block-quote markers (keep the text), and `|` table pipes (read row-by-row as sentences). Insert `…` between sentences for a short pause and a blank line plus `—` between paragraphs for a long pause. The model does not understand markdown; raw markdown will be read aloud as literal characters ("asterisk asterisk").
- **File downloads on Safari / Firefox:** when offering local-disk save of any export (PDF, CSV, MP3, ZIP, JSON, image), fall back to `` with a blob URL — the File System Access API (`showSaveFilePicker()`) is Chromium-only. Detect with `'showSaveFilePicker' in window`; otherwise use the anchor-download path.
- **Stack:** React + TypeScript + Tailwind CSS. Functional components + hooks. Use Shadcn UI primitives where appropriate.
- **Build runtime:** AI Studio Build — full-stack with Cloud Run server-side functions. All Gemini API calls happen server-side; API key lives in Secrets Manager, never in client bundle.
- **Model selection:** explicitly pin `gemini-3.5-flash` for the forecast and year-end consolidation; `gemini-3.5-flash` for events, day-end memo, category extraction, calibration; `gemini-3.1-flash-tts-preview` for read-aloud. Set `thinkingLevel` explicitly per call.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed cart on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; phone-number sign-in next to it; Apple sign-in third; magic-link email for kitchen-partner sharing.
- **Storage:** Firebase Storage for raw audio memos and rendered PDF prep sheets. Pre-signed URLs only.
- **Scheduled jobs:** Cloud Run / Cloud Scheduler — 03:00 local pre-fetch of tomorrow's events + weather; Friday 22:00 local calibration card composition. Schedules are per-cart and stored in cart settings.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. The "Lock prep sheet" button must be one thumb-tap away on every viewport.
- 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 day-end record and the locked-prep-sheet state.
- Optimistic UI on writes; reconcile on response.
- The day-end voice memo uses MediaRecorder with `audio/mp4;codecs=mp4a` where supported; falls back to `audio/webm;codecs=opus` on Android Chrome.
- **iOS Safari gotchas (graceful degradation):** mic permission does NOT persist across page reloads on iOS — re-request on every day-end memo; an incoming call interrupts the audio session (`MediaStreamTrack.onmute` fires) — auto-pause, save what was captured, and prompt the chef to continue or restart; backgrounded Safari tabs pause `getUserMedia` — combine `visibilitychange` with a screen Wake Lock during a memo.
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`.
- All interactive controls reachable by keyboard with a visible focus ring.
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. The recommended-prep orange `#E08B3A` on the off-white background `#F8F6F1` is verified at 4.6:1 for the 16px+ recommendation text.
- All images have meaningful `alt` text. The photographed sample carts have `alt` describing the artefact ("photograph of a taco truck at dusk on International Boulevard, end of service").
- Form fields have associated `