================ 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. --- # Brewing Notebook ## 1. Project **Brewing Notebook** is a per-batch fermentation journal for the home brewer who has decided to dial in one specific beer over months. The user records short voice notes on fermentation day 1, day 4, day 7, day 14 — temperature in the closet, smell at the airlock, gravity reading off the refractometer, hop additions, what the kveik did overnight — and the app builds the per-batch graph. Each batch keeps its recipe header, its measured-and-inferred gravity curve, its temperature trace, every hop addition, every dry-hop bag, every diacetyl-rest decision, every off-flavour the brewer noticed at bottling. The app predicts the final gravity from the data so far and proposes exactly one specific tweak for the next batch. This is the kind of app a homebrewer builds for himself in a Daly City garage after batch seven of a hazy IPA still drops too clear on day ten — because he has a stack of paper notebooks, voice memos on his phone going back fourteen months, and no way to compare batch four to batch six side by side. It is also the kind of app a microbrewery research lead in Plzeň keeps open on a tablet next to her pilot fermenter — twelve experimental batches across three yeasts and two malt bills, each batch logged in the same shape, each one charted against the others, the long-context model reading the whole history to suggest which variable to isolate next. Same shape of attention, different scale, different stakes. The single demo that proves the magic: the user opens the batch called "Hazy v8", taps record, and says "day four, nineteen Celsius, gravity one-point-zero-five-eight measured this morning, smell is heavy on the Citra, krausen has dropped about a centimetre overnight" — and in under fifteen seconds the batch graph updates. The gravity curve gets a new measured point at SG 1.058 on day 4. The dashed prediction line extends to a forecast final gravity of 1.014 ± 0.003, showing the airlock smell weighing toward the lower end of the range. A single tweak surfaces in plain English, anchored to the data: "the gravity drop from day 1 to day 4 is slower than your last three batches at this temperature — consider raising to 20 °C for the next 48 h to finish primary cleanly, then dry-hop a day earlier than v7." And in the harder cases — wild fermentation, mixed-culture sours, lambic-style batches that run for eighteen months — the app reads the long arc too: a kettle sour where the Lactobacillus dropped pH to 3.4 in 36 hours, a Brett-driven saison whose gravity refused to move below 1.012 for three weeks before crashing on week five, a solera-style fruited sour that has been blended across four batches of base beer and needs the whole lineage carried forward. **Tagline:** _A per-batch brewing notebook that listens — in any style, any scale, any vessel — and tells you the one thing to tweak next time._ ## 2. Target audience - Home brewers dialling in a single style over many batches — hazy IPA, Czech pilsner, English bitter, West Coast IPA, Belgian dubbel, Berliner Weisse, hefeweizen, kveik pale, sake, mead - Diaspora brewers recreating the beers they grew up drinking that the local market does not stock — Filipino lambanog enthusiasts in Daly City, a Czech-British homebrewer in Manchester chasing the pilsner his grandfather drank in Plzeň, a Mexican homebrewer in Mexico City brewing a Vienna lager closer to a Mexican-import amber than the modern American interpretation - Cider, mead, and kombucha makers — fermentation is the same shape even when malt is not involved; the graph, the temperature trace, the gravity curve, the one-tweak suggestion all generalise - Small commercial breweries running pilot batches before scaling — the head brewer logs the experimental vessels in the same shape as the homebrewer logs his closet fermenter - Brewing-club members who run blind tastings of each other's beers and want to track which variable each member is iterating on - Sake brewers running koji and moromi stages with two parallel temperature schedules — the per-batch graph supports two curves on the same chart so the rice and the yeast read together - Sommelier-level beer judges (BJCP, Cicerone) who taste 200 beers a year and want a per-style notebook of what they noticed - Homebrew teachers running a class of twelve students each brewing the same recipe — the app shows every student's batch on the same axes so the teacher can point at the one that fermented warmest ## 3. Core value propositions Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app. - **Voice is the input** — fermentation logging happens in the closet, the garage, the brewhouse, the cool-room. Hands are wet, the brewer is leaning over a carboy, the kettle is hot. Voice notes — five to thirty seconds, in the brewer's natural shorthand — parse into the structured `BatchObservation` schema. Day, temperature, gravity reading, hop addition, smell, airlock activity, krausen height, off-flavour suspicion, decision taken — all extracted, all charted, never re-typed. - **The graph is the brewer's working surface** — each batch has one graph showing the measured gravity points, the predicted FG range, the temperature trace, every hop addition as a vertical marker, every dry-hop bag as a labelled chip. Predicted FG is a shaded range, not a single line, and shrinks as more data points arrive. The brewer reads the graph the way a pilot reads the panel. - **One tweak per batch, not ten** — the app proposes exactly one specific change for the next batch, anchored in the data of this batch and the long-context history. "Dry-hop on day six instead of day seven" is concrete. "Try a different yeast" is not. The recommendation is grounded: it cites the batches that justify it. - **Long-context across the brewer's whole history** — once the notebook has eight batches of hazy IPA, the next suggestion sees all eight at once. The model reads the whole lineage and proposes the smallest meaningful next experiment, not a wholesale rewrite. - **Fermentation grounding, honestly used** — the app grounds yeast attenuation ranges, hop variety descriptors, water chemistry defaults, and BJCP style targets via search. When it cites a yeast manufacturer's attenuation range it shows the source URL on hover. When the model lacks data ("how does this Voss kveik attenuate at 35 °C in a wort with 30 % flaked oats?") it says so rather than guessing. - **Predicted FG, with an honest error bar** — the prediction is a range and the range shrinks as the curve fills in. The brewer sees confidence drop after an unusual reading and rise as the prediction lines up with the next measurement. The prediction is the model's guess, not a guarantee — and is labelled that way. - **Side-by-side batch overlay** — pick any two batches of the same style and the gravity curves and temperature traces overlay on the same axes. The differences are quantified ("batch v7 fermented 1.4 °C warmer through primary; finished 1.5 °SG higher"). This is the feature the brewer uses on Sunday morning when he is deciding what to change for batch v9. - **Hard rule on food safety** — the app never gives food-safety advice as if it were a substitute for real knowledge. Every time a user asks "is this batch safe to drink" or "is this mould or krausen", the app flags the answer prominently as not a substitute for an experienced brewer, a brewing forum, or a food-safety authority; the relevant local agency is named where surfaced via grounded search. ## 4. Features to build - Voice capture (mobile-first) — one big record button on the active batch page; speech-to-structured-observation in a single Gemini 3.5 Flash call - Manual entry — every field the voice flow extracts is editable; voice and keyboard fully interchangeable - Recipe header per batch — style, OG target, IBU target, water source, mash schedule, hops, yeast, ferment vessel, batch volume, parent recipe version - Per-batch gravity-curve graph — measured points, dashed predicted curve to FG, shaded FG-confidence band, temperature line on a secondary axis, hop additions as vertical markers, dry-hop bags as labelled chips, krausen / airlock notes as annotations - Side-by-side batch overlay — pick two or more batches of the same style; gravity curves and temperature traces overlay on one chart - Long-context tweak proposer — exactly one concrete tweak per batch, citing the prior batches that justify it - Predicted-FG line with shrinking error band as the curve fills in - Yeast-strain library — per-strain attenuation ranges, ideal temperature ranges, flavour profiles, manufacturer source - Water-chemistry calculator with adjustable salt additions — predicted mash pH, sulfate-to-chloride ratio, residual alkalinity - Hop addition timeline — bittering, flavour, aroma, whirlpool, dry- hop bags; each addition timestamped to the fermentation day or the boil minute - Off-flavour log — diacetyl, acetaldehyde, DMS, autolysis, hot- alcohol, oxidation — each as a chip the user taps at any stage, with an optional voice note attached - Style-target overlay — BJCP target ranges for OG, FG, IBU, SRM, ABV plotted as shaded regions on each respective chart - Brewing-day mode — full-screen kettle-side checklist: strike water temp, mash-in, vorlauf, sparge, pre-boil gravity, boil start, hop additions countdown, whirlpool, chill, transfer - Photo capture — wort colour at boil end, krausen at peak, beer at packaging, bottle conditioning — attached to the per-day log - Voice playback — every voice note is preserved and replayable; the parsed structured fields sit beside them - Family-of-batches view — show every version of "Hazy v" in one list with key parameters as columns - Export — recipe export to BeerXML / BeerJSON 2 for use in other brewing software; per-batch PDF brewlog - Brewing-club share — share one batch (recipe + curve + notes) with another user; their tasting feedback comes back as a structured comment thread - Calendar reminders — "day 4 gravity reading", "diacetyl rest warm-up window", "dry-hop drop in two days" — local reminders, never push from a server - Food-safety flag mode — any question that smells like a food- safety question is always answered with the not-a-substitute warning visible ## 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) - **Voice → structured observation** (Gemini 3.5 Flash, multimodal audio input) — accepts a 5–60 second voice memo of a brewer's natural shorthand and emits a `BatchObservation` JSON object. Handles imperial and metric ("nineteen Celsius", "sixty-six Fahrenheit", "one-point-zero-five-eight", "ten Plato", "1.058 OG", "fifteen Brix") with explicit unit normalisation. Handles English, Czech, German, Spanish, French, Tagalog, Portuguese-PT and BR, Vietnamese, Mandarin, Cantonese, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer — every language used in this template's seed library plus the rest of the BCP-47 catalogue the Gemini API supports. The voice locale is passed via the audio's accompanying `language_hint` in the system instruction. - **Long-context across the brewer's history** (Gemini 3.5 Flash, up to 1M tokens) — for the tweak-proposer call. Once a brewer has more than four batches of the same style the proposer reads every observation across every batch and proposes the smallest meaningful experiment for the next batch. **Guardrail**: a parsed batch averages ~6,000 tokens (recipe + 12 observations + tasting notes + photo descriptions). A brewer with 200 batches in one style ≈ ~1.2M tokens, which exceeds the ceiling. Chunk strategy for >100 batches in one style: send the last 30 batches in full, plus structured per-batch summaries (~200 tokens each) for older batches; never send more than 800k tokens of batch data per call. - **Structured output / JSON Schema** — the voice-parse call returns the `BatchObservation` schema. The tweak-proposer call returns the `BatchTweakProposal` schema. The schemas are seeded verbatim in the system instruction and as `responseSchema`. - **Fermentation grounding via search** (Gemini 3.5 Flash + grounded search) — yeast-strain attenuation ranges, hop-variety descriptors, water-chemistry standards, BJCP 2021 style guidelines. Used when the brewer asks "what is the published attenuation range for Lallemand Voss kveik?" or when the tweak-proposer needs a sourced fact. **Critical**: grounded calls do NOT use `responseSchema`; the model emits JSON in the text body and the server parses it, while citation URLs come from `response.groundingMetadata.groundingChunks[].web.uri`. - **Predicted FG (Gemini 3.5 Flash, low thinking)** — a deterministic- feel call that takes the current gravity curve plus recipe and emits a predicted FG range, the confidence band, and a one- sentence rationale. The actual arithmetic should be done in TypeScript on the server (an exponential decay fit pinned to the yeast's attenuation range); the Gemini call's job is the rationale and the sanity check, not the arithmetic itself. - **Thinking levels** — `medium` for the tweak-proposer call (it reads the long-context history and needs to justify its tweak). `low` for voice-parse, predicted-FG sanity check, BatchObservation extraction. `high` is not used in this template; brewing is not a reasoning task that benefits from high. - **Photo description** (Gemini 3.5 Flash, multimodal image) — wort colour at boil end, krausen at peak, beer at packaging — each photo is described into the `photo_description` field of the associated observation. Used to surface "your krausen height looked half what it was on v7 at the same point" later, via the long-context tweak-proposer. ### Backend services - **Auth — Required.** Firebase Auth with Google sign-in (auto- provisioned by AI Studio Build). **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used for sharing a batch with another brewer or with a brewing-club mate) requires the sender domain to be authorised in Firebase Auth. Brewing notebooks are private to the owner by default; sharing is explicit per batch. - **Database — Required.** Firestore for `users`, `recipes`, `batches`, `observations`, `tasting_notes`, `tweaks_proposed`, `shared_links`. - **File storage — Required.** Firebase Storage for voice memos (preserved at original audio quality, forever) and photographs (wort, krausen, bottle conditioning). **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 voice memo is uploaded. Pre-signed URLs only; no public-by-default access. - **Email — Required (transactional).** Magic-link share emails; optional per-batch summary email at packaging time. Sender domain must be authorised in Firebase Auth. - **Payments — Not needed for v1.** Free for personal use. A future "club" tier (shared club library, blind-tasting tournaments) might charge a small subscription via Stripe; not built in v1. - **External APIs:** Gemini API for all intelligence; optional OpenWeather (for "ambient temperature at brew day" annotation, useful for outdoor brewers and for fermentation prediction in non- temperature-controlled environments). No required external API beyond Gemini. **Environment variables:** every secret (Gemini API key, Firebase service-account JSON, OpenWeather key if used, Stripe key if club tier added) 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 brewer's recipes, voice memos, and batch data 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) · sharing a batch with another brewer is per-batch and revocable. **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 schemas are below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.** 4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate. 5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled. 6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow. ### Per-call model + tools matrix | Call | Model | thinkingLevel | Tools enabled | |------|-------|---------------|---------------| | Voice memo → `BatchObservation` | `gemini-3.5-flash` | low | (none) | | Photo (wort/krausen/beer) → photo_description | `gemini-3.5-flash` | low | (none) | | Predicted FG range + rationale (per batch, per new observation) | `gemini-3.5-flash` | low | (none) | | Long-context tweak proposer → `BatchTweakProposal` | `gemini-3.5-flash` | medium | (none) — long-context across the style lineage | | Yeast / hop / water fact grounding (e.g. "Voss kveik attenuation range") | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) | | Off-flavour explainer (e.g. "what is diacetyl, in one paragraph?") | `gemini-3.5-flash` | low | (none) | | TTS playback of the brewer's own voice memo transcript (accessibility) | `gemini-3.1-flash-tts-preview` | n/a | n/a | | Hero / empty-state illustration generation | `gemini-3.1-flash-image` (Nano Banana 2) | n/a | n/a | *Note for builders:* on TTS and image-generation calls, omit `thinkingConfig` entirely — the field is not supported on those models. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body. Grounded search calls emit JSON in the text body — `responseSchema` and `google_search` cannot be combined in the same Gemini call; parse the JSON server-side and read citation URLs from `response.groundingMetadata.groundingChunks[].web.uri`. ### Primary structured-output schemas (seed verbatim in the prompt) ```typescript import { z } from "zod"; const Unit = z.enum([ "celsius", "fahrenheit", "sg", "plato", "brix", "grams", "ounces", "pounds", "kilograms", "litres", "us_gallons", "imperial_gallons", "ibu", "srm", "ebc", "ph", "ppm", "minutes", "hours", "days", ]); const Measurement = z.object({ value: z.number(), unit: Unit, is_normalised: z.boolean(), // true once server-clamped to canonical unit }); const HopAddition = z.object({ variety: z.string(), // "Citra", "Saaz", "Nelson Sauvin" variety_normalised: z.string().nullable(), // canonical name once grounded amount_grams: z.number().nullable(), alpha_acid_percent: z.number().nullable(), addition_stage: z.enum([ "first_wort", "boil_60", "boil_30", "boil_15", "boil_10", "boil_5", "boil_0", "whirlpool", "dry_hop_active_ferment", "dry_hop_after_primary", "dry_hop_cold_crash", "dry_hop_other", ]), addition_day: z.number().nullable(), // fermentation day, if dry-hop addition_minute: z.number().nullable(), // boil minute, if boil-stage duration_days: z.number().nullable(), // for dry-hop bags notes: z.string().nullable(), }); const OffFlavourFlag = z.object({ flavour: z.enum([ "diacetyl", "acetaldehyde", "dms", "autolysis", "hot_alcohol", "oxidation", "phenolic", "metallic", "astringent", "vegetal", "lightstruck", "other", ]), intensity_note: z.string(), // "faint", "borderline", "obvious" stage_observed: z.string(), // "krausen drop", "packaging", "week 3 bottle" voice_memo_uri: z.string().nullable(), }); const BatchObservation = z.object({ observation_id: z.string(), batch_id: z.string(), fermentation_day: z.number(), // day since pitch; 0 = brew day observed_at_iso: z.string(), // wall-clock timestamp voice_memo_uri: z.string().nullable(), // gs:// URI to the original audio voice_memo_transcript_verbatim: z.string().nullable(), voice_memo_language: z.string().nullable(), // BCP-47, e.g. "en-US", "cs-CZ" temperature: Measurement.nullable(), // beer or ambient — see field below temperature_target: z.enum(["beer", "ambient", "unknown"]), gravity: Measurement.nullable(), // SG or Plato or Brix; normalised to SG gravity_method: z.enum([ "hydrometer", "refractometer_uncorrected", "refractometer_corrected", "tilt", "ispindel", "other", "unknown", ]).nullable(), ph: z.number().nullable(), hop_additions_today: z.array(HopAddition), airlock_activity: z.enum([ "vigorous", "steady", "slow", "stopped", "unknown", ]).nullable(), krausen_height_cm: z.number().nullable(), krausen_state: z.enum([ "forming", "peaked", "dropping", "fallen", "absent", ]).nullable(), smell_notes_verbatim: z.string().nullable(), // "heavy on the Citra, slight sulfur" smell_descriptors_extracted: z.array(z.string()), // ["citrus", "tropical", "sulfur"] off_flavour_flags: z.array(OffFlavourFlag), decisions_taken: z.array(z.string()), // ["raised to 20C for diacetyl rest"] photo_uris: z.array(z.string()), photo_description: z.string().nullable(), // model-described parse_confidence: z.number().min(0).max(1), flagged_for_user_review: z.array(z.object({ field_path: z.string(), reason: z.string(), })), }); const TasteRating = z.object({ category: z.enum([ "aroma", "appearance", "flavour", "mouthfeel", "overall_impression", ]), score_out_of_5: z.number().min(0).max(5), notes_verbatim: z.string(), }); const BatchRecipe = z.object({ recipe_id: z.string(), parent_recipe_id: z.string().nullable(), // for batch v8 → links to v7 style: z.string(), // "American IPA - Hazy/NEIPA" style_bjcp_code: z.string().nullable(), // "21B" batch_size_litres: z.number(), og_target: z.number(), // SG fg_target: z.number(), // SG ibu_target: z.number(), srm_target: z.number(), abv_target_percent: z.number(), mash_schedule: z.array(z.object({ step_name: z.string(), // "saccharification" temperature_c: z.number(), duration_minutes: z.number(), })), grain_bill: z.array(z.object({ grain_name: z.string(), amount_grams: z.number(), percentage_of_bill: z.number(), })), hop_schedule: z.array(HopAddition), yeast_strain_name: z.string(), yeast_strain_manufacturer: z.string().nullable(), yeast_published_attenuation_low: z.number().nullable(), yeast_published_attenuation_high: z.number().nullable(), yeast_published_temp_low_c: z.number().nullable(), yeast_published_temp_high_c: z.number().nullable(), water_profile_name: z.string().nullable(), // "Burton-on-Trent", "soft Pilsen" salt_additions: z.array(z.object({ salt: z.string(), // "gypsum", "calcium chloride" amount_grams: z.number(), target_water_volume_litres: z.number(), })), }); const BatchTweakProposal = z.object({ batch_id: z.string(), style_lineage_batches_considered: z.array(z.string()), // batch_ids the proposer read tweak_one_line: z.string(), // the single concrete tweak tweak_full_rationale: z.string(), // 3-6 sentences citing batches cited_batch_ids: z.array(z.string()), // batches that justify the tweak expected_effect: z.string(), // "should finish ~2 SG lower" confidence: z.number().min(0).max(1), risk_note: z.string().nullable(), // "may reduce hop aroma slightly" food_safety_flag: z.boolean(), // true if a safety concern was raised food_safety_disclaimer_required: z.boolean(), // true if the tweak touches anything safety-adjacent }); type BatchObservation = z.infer; type BatchRecipe = z.infer; type BatchTweakProposal = z.infer; ``` ### Common failure modes (and how to avoid them) - Agent silently downgrades `thinkingLevel` for the voice-parse call to save quota — pin `gemini-3.5-flash` explicitly. Flash mis-hears "ten Plato" as "tin plate" in noisy garage environments and silently drops the `gravity_method: refractometer_corrected` distinction. - Gravity units conflated — "10" might be 10 Plato (≈ SG 1.040) or SG 1.010. The voice-parse system instruction must require explicit unit elicitation and the server must clamp via the unit enum. If the brewer says only "ten", flag for review. - Refractometer reading not corrected for alcohol — once fermentation starts, refractometer SG readings are higher than actual because alcohol distorts refractive index. If `gravity_method` is `refractometer_uncorrected` and `fermentation_day >= 2`, apply the standard Sean Terrill correction on the server before charting and flag the observation for user review. - Temperature target ambiguous — beer temperature inside the vessel is not ambient room temperature. If the brewer says "nineteen" without specifying, set `temperature_target: "unknown"` and ask in the UI; do not silently chart it as beer temperature. - Predicted-FG arithmetic done by Gemini — the model is poor at exponential decay fitting and will hallucinate plausible-but-wrong numbers. Do the arithmetic in TypeScript on the server (a simple exponential fit clamped to the yeast's published attenuation range); the Gemini call only sanity-checks the result. - The "one tweak" call returns multiple tweaks — the system instruction must say "exactly one tweak". If the model returns more than one, the server picks the first and discards the rest, and logs the violation so the prompt can be tightened. - Tweak proposer fabricates a batch in its rationale — the system instruction must require `cited_batch_ids` to be a subset of the batches sent in context. The server validates this set membership; if any cited batch_id is not in the context, the rationale is rejected and re-requested. - Food-safety question slips through unflagged — the voice-parse call must set `off_flavour_flags` aggressively when the brewer asks something like "is this mould or krausen" or "is this safe to drink". The tweak proposer must set `food_safety_disclaimer_required: true` whenever the rationale touches sanitation, off-flavour suspicion, fermentation safety, or anything the brewer might interpret as medical advice. - Hop-addition timeline conflates boil and ferment day — a boil- minute addition is during the boil; a dry-hop addition is during fermentation. The schema separates them via `addition_stage` and the two timestamp fields; the system instruction must require the correct one to be set. - Multilingual brewer mid-recording switches between languages ("today is day vier, Temperatur neunzehn Celsius") — the voice- parse system instruction must allow code-switching and store the verbatim transcript with the language tag for the dominant language. - BJCP style targets pulled from a stale cache — yeast manufacturers and BJCP update their guidelines; the grounding call must use `google_search` and surface the source URL on hover. - Long-context call exceeds 1M tokens — for a brewer with >100 batches in one style, send the last 30 batches in full plus per-batch summary stubs for older batches; never raw-dump the whole history. ### Negative constraints (hard rules) - Do NOT give food-safety advice as if it were authoritative. Every food-safety-adjacent answer must include the not-a-substitute disclaimer ("this is not a substitute for an experienced brewer's judgement or your local food-safety authority"), prominently surfaced in the UI and in the structured response via `food_safety_disclaimer_required: true`. - Do NOT invent batches, gravity readings, or observations. The tweak proposer's `cited_batch_ids` must be a strict subset of the batches actually sent in context. If the model wants to cite a pattern it has not been shown, it must instead say so in the `risk_note` ("rationale would be stronger with one more batch at this temperature"). - Do NOT do exponential decay fitting in the model. Do the arithmetic in TypeScript on the server, using the yeast's published attenuation range as a clamp, and use Gemini for the rationale only. - Do NOT correct refractometer readings silently. If the `gravity_method` is uncorrected and fermentation has begun, the server corrects and flags the observation for user review, and the chart shows both the raw and corrected points faintly. - Do NOT propose more than one tweak per batch. The system instruction requires exactly one. Multiple suggestions overwhelm the brewer and break the discipline that makes per-batch iteration work. - Do NOT propose tweaks that change two variables at once. "Switch to a different yeast at a different temperature" is two changes; reject and re-request a single-variable tweak. - Do NOT translate yeast-strain names, hop-variety names, or BJCP category names. "Saaz" is "Saaz" in every language. "Hefeweizen" is "Hefeweizen". - Do NOT auto-publish batches. Sharing is explicit, per-batch, and revocable. - Do NOT use the brewer's recipes, voice memos, or batch data 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 chart predicted FG as a single line. It is a range with a confidence band, and the band must shrink visibly as more data arrives. A single predicted-FG line gives a false sense of certainty. - Do NOT prompt the brewer to drink the beer. Drinking decisions are the brewer's. The app charts, suggests, warns; it does not encourage. - Do NOT auto-claim a batch is "infected" from one observation. An off-flavour flag is a flag, not a diagnosis; the language in the UI is always "you noticed X — that often indicates Y, sometimes indicates Z; consider checking N". ### 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: Voice memo → `BatchObservation` Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You are parsing a homebrewer's voice memo about an ongoing fermentation batch into a structured BatchObservation JSON object. Memos are typically 5 to 60 seconds long, spoken in a garage, a closet, a brewhouse, or a cool-room. Background noise is normal. Languages encountered include English (US, UK, Australian, Indian, Filipino, Singaporean), Czech, German (Hochdeutsch, Bavarian, Austrian), French, Spanish (Iberian, Mexican, Argentinian), Portuguese (Brazilian, European), Italian, Polish, Dutch, Norwegian, Swedish, Finnish, Tagalog (Filipino), Vietnamese, Mandarin, Cantonese, Korean, Japanese, Thai, Tamil, Hindi, Urdu, Bengali, Punjabi, Marathi, Telugu, Amharic, Swahili, Yoruba, Farsi, Arabic, Hebrew, Khmer. Brewers code-switch mid-memo ("day four, Temperatur neunzehn Celsius, gravity ist ten-fifty-eight"). Store the verbatim transcript with the dominant language as `voice_memo_language` (BCP-47); preserve the code-switched phrases verbatim. Audio is provided either as a Gemini Files API `files/*` resource name or as inlineData (base64). Do NOT pass Firebase Storage public URLs to generateContent — the API does not fetch them server-side. The caller supplies a one-line context header before the audio ("Batch v8 Hazy IPA, day 4 expected") so you can disambiguate unit ranges; respect it but do not fabricate fields it does not mention. Read every spoken value carefully. Distinguish: - temperature of the beer inside the vessel vs ambient room temperature — if the brewer says only "nineteen" without specifying, set temperature_target: "unknown" and flag for user review. - gravity reported in SG, Plato, or Brix — if the brewer says "ten Plato" that is SG 1.040, not SG 1.010. If the brewer says only "ten" or only "fifteen" without a unit, flag for user review. - gravity method — hydrometer, refractometer (corrected or uncorrected), Tilt, iSpindel. If unspecified, set to "unknown". - hop additions during fermentation (dry-hop bags) — record variety, weight, the fermentation day they went in, the duration in days they will stay. - airlock activity — vigorous, steady, slow, stopped. - krausen — forming, peaked, dropping, fallen, absent; and a height in centimetres if the brewer mentioned one. - smell descriptors — capture verbatim AND extract a normalised list of descriptors ("heavy on the Citra, slight sulfur" → ["citrus", "tropical", "sulfur"]). - off-flavour flags — diacetyl, acetaldehyde, DMS, autolysis, hot alcohol, oxidation, phenolic, metallic, astringent, vegetal, lightstruck. - decisions the brewer says they have taken or will take ("I'm raising to 20 for the diacetyl rest"). Hard rules: - Preserve every unit exactly as spoken in voice_memo_transcript_verbatim. Server-side will normalise to canonical units; do not silently convert. - Do NOT translate yeast-strain names, hop varieties, malt names, or style names. "Voss kveik", "Citra", "Maris Otter", "Hefeweizen" stay verbatim. - Do NOT invent values. If the brewer did not mention temperature, leave it null. If the brewer mentioned a gravity but no method, set gravity_method: "unknown" rather than guessing. - Do NOT correct refractometer readings yourself. That happens server-side after the response arrives. Just record what was said and the method as stated. - If the memo asks a food-safety question ("is this mould or krausen", "is this safe to drink", "should I throw this out") raise an off_flavour_flag with flavour: "other" and intensity_note containing "user requested food-safety judgement" so the UI surfaces the disclaimer. - parse_confidence reflects audio clarity AND unit unambiguity. Below 0.7 → flag for user review. - flagged_for_user_review names any field with confidence below 0.7 and a one-sentence reason. Output ONLY the BatchObservation JSON matching the provided schema. No commentary. JSON only. ``` --- ### Call: Photo (wort / krausen / beer) → photo_description Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive one photograph attached to a BatchObservation. Photographs are typically taken in the garage, the closet, or the brewhouse under mixed lighting. The brewer uploads the photograph to add visual context to the observation; your job is to describe what is visible in one sensory, specific sentence that will be useful months later when the long-context tweak proposer reads it. Photograph subjects you will see: - wort at boil end (golden through deep amber to dark brown) - krausen on a fermenting beer (foam height, colour, density, hop-trub on top) - beer in a glass at packaging (clarity, head retention, colour) - hydrometer or refractometer reading (gravity scale visible) - a cracked bottle, a contaminated batch, a pellicle on a sour - a stuck mash, a thermometer reading, a kettle clock Hard rules: - One sentence. Concrete and sensory. Avoid art-history vocabulary. - For colour, use SRM-style descriptors ("light gold", "deep amber", "ruby") when relevant. - For krausen, describe height, colour, density. - For a hydrometer or refractometer photo, describe what you can read on the scale verbatim ("hydrometer reading approximately 1.014 on the SG scale; brewer's thumb covers the Plato side"). - Do NOT diagnose. "Visible pellicle on the surface" is a description; "the batch is infected" is a diagnosis — never output a diagnosis. The brewer reads the description and decides. - Do NOT guess off-flavours from a photo. Diacetyl is a flavour and aroma; it is not visible. - If the photo is ambiguous, say so: "image is dark and the fermenter contents are not clearly visible". Examples of good descriptions: - "krausen approximately 4 cm high, dense beige foam with a brown ring of hop trub at the rim, vessel sides clean." - "hydrometer reading approximately 1.014 on the SG scale, liquid is hazy gold, no visible particulates." - "boiled wort at flameout, deep amber colour estimated 14 SRM, light hot-break suspended throughout." Examples of bad descriptions: - "a beautiful golden brew" (vague, art-history vocabulary) - "the batch is contaminated" (diagnosis, not description) - "you probably need to raise your fermentation temperature" (suggestion, not description) Output: a single string in photo_description. No commentary. ``` --- ### Call: Predicted FG + rationale (per batch, on new observation) Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive a batch's recipe header, the yeast's published attenuation range (low and high), the full ordered list of BatchObservations for this batch (including the new one), and a server-computed predicted FG range from a clamped exponential decay fit. Your job is to sanity-check the server's predicted FG range and write a one-sentence rationale a brewer will read. The server-computed prediction is the source of truth for the numeric range. You do NOT recompute it. You evaluate whether the range makes sense given the data and write the rationale. Inputs you receive: - recipe.og_target, recipe.yeast_published_attenuation_low/high - observations[]: an array of BatchObservations in fermentation_day order - server_predicted_fg_low, server_predicted_fg_high - server_predicted_fg_confidence (0..1) Output a structured object: { "predicted_fg_low": , "predicted_fg_high": , "predicted_fg_confidence": , "rationale": "", "concerns": [""] } Hard rules: - Do NOT recompute the numeric range. Copy the server's range verbatim into the output. - If the server's range is outside the yeast's published attenuation range, flag this in concerns with a short reason — but still copy the range verbatim. - The rationale cites the most recent observation and the current trend ("gravity dropped 0.012 SG since day 1, fastest drop of the day-3 readings in this batch's lineage"). - Do NOT extrapolate to off-flavours. If the brewer's last observation mentioned diacetyl, you can mention it as a concern but do not recommend a diacetyl rest here — the tweak proposer call handles recommendations. - Do NOT make promises. The FG is predicted, not guaranteed. Use language like "expected to finish around" not "will finish at". Output: the JSON object exactly as specified. No commentary. ``` --- ### Call: Long-context tweak proposer → `BatchTweakProposal` Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none, long context across the lineage) ``` You receive the full lineage of batches in one style, in chronological order, with every BatchObservation, the recipe headers, the tasting notes for completed batches, and the current batch's curve so far. Your task: propose EXACTLY ONE concrete, single-variable tweak for the brewer's NEXT batch in this lineage. Anchor the tweak in evidence from the lineage. Cite the specific batches that justify it. Inputs you receive: - style_lineage_batches[]: the ordered batches in this style, each one a complete batch record (recipe + observations + tasting notes for completed batches) - current_batch: the most recent batch (which may still be fermenting or already packaged) Hard rules: - EXACTLY ONE tweak. Not two. Not "you could also". One. - ONE variable. Do not propose "switch yeast AND change temperature" — that is two variables. If you want both, pick the one with the strongest evidence and put the other in risk_note as a future iteration. - CITE the batches that justify the tweak. Populate cited_batch_ids as a strict subset of style_lineage_batches you were given. Do NOT cite batches you did not receive in the input. - The tweak must be CONCRETE. "Dry-hop on day six instead of day seven" is concrete. "Try a different hop" is not. "Mash at 65 °C for 75 minutes instead of 67 °C for 60 minutes" is concrete. "Adjust the mash" is not. - The rationale is 3-6 sentences. It names the pattern (e.g. "batches v3 and v6 both finished with the same flabby malt character; both mashed at 67 °C with the same long rest"), the proposed change, and the expected effect. - expected_effect is a single sentence the brewer can read in three seconds: "should produce a drier finish with about 0.003 SG lower terminal gravity". - confidence reflects evidence strength: 0.9 only when 3+ batches in the lineage agree on the pattern; 0.5-0.7 when the pattern shows in 2 batches; below 0.5 when the evidence is one batch and you are extrapolating. - risk_note is required whenever the tweak is non-trivial. Name one realistic downside ("may reduce hop aroma slightly", "may add a perceptible mineral note"). - If the tweak touches sanitation, off-flavour control, fermentation temperature safety, or anything the brewer might read as medical/safety advice, set food_safety_flag: true and food_safety_disclaimer_required: true. The UI will surface the not-a-substitute language. - Do NOT propose tweaks that violate the brewer's stated constraints. If a brewer's recipe header notes "kveik only, garage temperatures only", do not propose switching to a lager yeast at 12 °C. - Do NOT translate yeast, hop, malt, or style names. - Do NOT moralise about beer alcohol content, frequency of drinking, or anything social. The brewer makes those decisions. If the lineage is too short (fewer than 2 batches) to justify a data-driven tweak, return a tweak with confidence ≤ 0.5 and an honest rationale that says so. Do NOT pretend you have evidence you do not have. Output ONLY the BatchTweakProposal JSON matching the provided schema. No commentary. JSON only. ``` --- ### Call: Yeast / hop / water fact grounding Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding ``` You answer a single brewing-fact question with a sourced answer. Example questions: - "What is the published attenuation range for Lallemand Voss kveik?" - "What is the alpha acid range for Nelson Sauvin in the 2025 harvest?" - "What is the BJCP 2021 target ABV range for category 21B (American IPA - Hazy/NEIPA)?" - "What is the typical sulfate-to-chloride ratio for an English bitter water profile?" Use `google_search` grounding for every answer. Do NOT answer from training-data memory alone for any numeric range. Output the answer 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 `response.groundingMetadata.groundingChunks[].web.uri` — do NOT include URLs in the JSON body; the model will hallucinate them. JSON shape: { "answer_one_line": "Voss kveik typically attenuates 75-82% at fermentation temperatures of 25-40 °C.", "answer_detail": "<2-4 sentences of supporting detail>", "numeric_values": [ {"label": "attenuation_low", "value": 75, "unit": "percent"}, {"label": "attenuation_high", "value": 82, "unit": "percent"}, {"label": "temp_low", "value": 25, "unit": "celsius"}, {"label": "temp_high", "value": 40, "unit": "celsius"} ], "source_authority": "" } Hard rules: - The numeric_values array holds every numeric the brewer asked about, with explicit unit fields. Never give a number without a unit. - If sources disagree, surface the disagreement explicitly: "Manufacturer publishes 75-82%; community measurements suggest 70-85% in practice; both ranges are provided". - Do NOT include URLs in the JSON body. Citations come from groundingMetadata server-side. - If the question is food-safety adjacent ("is botulism a risk in low-gravity meads?"), return the answer with the not-a-substitute disclaimer in answer_detail. No commentary outside the JSON. ``` --- ### Call: Off-flavour explainer Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You receive an off-flavour name and return a short, plain- English explanation a homebrewer can read in under thirty seconds. The explainer surfaces when the user taps an off- flavour chip in the UI. Shape: { "flavour": "", "tastes_like": "", "common_causes": [""], "common_fixes": [""], "safety_disclaimer_required": } Hard rules: - One paragraph max in tastes_like. - common_causes and common_fixes are short lists, not paragraphs. - safety_disclaimer_required is true whenever the off-flavour is associated with a real safety concern (e.g. autolysis at extreme levels, contamination in low-acid meads, lightstruck in sealed packaging) AND the user has asked about it directly. - Do NOT recommend tasting a batch the user is uncertain about. Suggest the brewer's normal channels (forum, local brewing club, manufacturer) instead. - Do NOT translate the off-flavour names. ``` --- ### Call: TTS playback of the brewer's own voice memo transcript Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` Voice: clear, calm, unhurried. Pick the Gemini 2.5 Flash TTS voice whose `languageCode` matches the memo's `voice_memo_language` — pronunciation will follow that locale automatically. Use case: accessibility playback for the brewer's own past voice memos, useful when the brewer is hands-busy at the kettle and wants to hear his day-4 notes from the last batch read aloud. Pre-process the text before sending to TTS: - Source the text from `voice_memo_transcript_verbatim`. - At sentence boundaries, insert an ellipsis ("…") for a natural pause. At paragraph boundaries, insert a blank line plus an em-dash ("—"). Gemini 2.5 TTS does not support SSML `` — these textual cues are how pace is conveyed. - Skip metadata that does not belong in spoken playback (the observation_id, the timestamps). - Mid-call voice switching is not supported. If the memo is code-switched (English + German), keep the whole reading in the dominant-language voice. The on-screen subtitle can highlight the inserted phrases visually. - Target rate: ~130 words per minute — slightly faster than letter-reading pace because brewers are listening to their own notes, not narrative prose. Style direction: prepend ONE short directive sentence to the text input, exactly like: "Read clearly and unhurriedly, like reading back a brewing notebook to yourself at the kettle. …". 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 for yeast names, hop varieties, German umlauts, Czech háčky, etc. are NOT exposed by Gemini 2.5 TTS — no SSML `` tag. Pronunciation comes from the chosen voice's native locale. ``` --- ### Call: Hero / empty-state illustration generation Model: `gemini-3.1-flash-image` (Nano Banana 2) · n/a · n/a ``` You generate a single photographic-looking image for the welcome screen or the empty-state of the app. Images depict real brewing surfaces: a garage workbench with a carboy and an airlock bubbling, a stainless conical fermenter, a row of test tiles drying near a kettle, a notebook open beside a hydrometer. Prompt anchors that work well: - "warm afternoon light through a garage window, no people in frame, a clear glass carboy of fermenting hazy IPA with active krausen, a refractometer on a wooden workbench beside it, a paper notebook open with handwritten gravity readings" - "stainless conical fermenter in a small brewery, soft overhead light, a tablet on a stand beside it, no people, shallow depth of field" - "kitchen-table brewing scene at dusk, a row of three small bottles on a tea-stained towel, condensation on the bottles, warm desk lamp" Hard rules: - Photographic, not cartoon, not illustration-style. - No people in frame unless the user prompt asks for one. - No commercial branding visible (no real brewery logos, no beer-brand labels). - Warm lighting, slight imperfection, real-paper texture for notebooks; avoid the glossy AI-render look. - Aspect ratios: 3:2 for hero, 1:1 for empty states. ``` ## 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. - **Dialling in Hazy v8 in a Daly City garage.** A Filipino-American homebrewer is on batch eight of his hazy IPA. He records day 4 in Tagalog-inflected English: "day four, nineteen Celsius, gravity one-oh-five-eight, smell is heavy on the Citra and Mosaic, krausen dropped about a centimetre overnight, going to push to twenty for the diacetyl rest tomorrow". The app updates the curve, predicts FG 1.014 ± 0.003, and at packaging it cites batches v3, v6, v7 and proposes one tweak for v9: "dry-hop on day six instead of day seven — v6 and v7 both showed sharper hop expression when the dry-hop went in during active fermentation". - **Czech pilsner in Manchester, brewed cold.** A Czech-British brewer in Manchester is iterating on a Czech-style pilsner across six batches. Day 1: "den jedna, jedenáct Celsius, OG jedna nula čtyřicet osm" — code-switching between Czech and English. The schema captures both. The tweak proposer cites v3 and v5: "v3 fermented at 9 °C and finished cleaner; v5 fermented at 11 °C and had a faint diacetyl. Try 9 °C for v7 and lengthen primary by three days". - **A Vienna lager in Mexico City.** A homebrewer in Mexico City has been chasing the Vienna lager his grandfather drank in Orizaba. He records day 2 in Spanish: "día dos, doce grados, gravedad uno-cero-cuatro-cinco, espuma hermosa, lleno de carácter de malta". The app charts it. After batch v4 the tweak proposer cites v2 ("OG was 1.052, finished 1.014, attenuation 73%") and proposes raising mash temperature by 1 °C to retain more body for v5. - **A kveik pale brewed at 35 °C in a closet.** Voss kveik runs hot. The user logs day 1 "thirty-five Celsius, krausen forming vigorous", day 2 "thirty-six, vigorous", day 3 "thirty-five, dropping, gravity one-oh-one-eight". The app's predicted FG shrinks from a 1.014 ± 0.004 band on day 1 to 1.010 ± 0.001 on day 3 — the brewer watches the band collapse as kveik delivers. - **A wild fermentation across eighteen months.** A mixed-culture brewer in Brussels is logging a lambic-style aged in oak. Day 1: cool ambient, no pitched yeast. Week 2: Brett character emerging. Month 3: pellicle visible on the surface. Month 8: gravity refused to move from 1.012 for three weeks before crashing to 1.004 on week 35. The schema and the chart handle the long timeline (days as the x-axis, log-scaled to keep the early rapid-change visible). - **The diacetyl rest decision.** Mid-fermentation, a brewer notices a butterscotch note in the airlock vapour at day 5. He asks the app "is this diacetyl?". The off-flavour explainer responds with a short paragraph and surfaces the disclaimer ("this is not a substitute for an experienced brewer's judgement"). The tweak proposer flags `food_safety_disclaimer_required: true`. - **The infected batch question.** A brewer photographs a strange white film on the surface of his beer. The photo-description call describes it neutrally ("a thin off-white film with a textured surface, slight wrinkling visible at the rim"); the brewer asks "is this mould?". The off-flavour explainer responds with a not-a-substitute disclaimer prominently surfaced and suggests his local brewing club, the manufacturer's forum, or a food-safety authority. - **The side-by-side batch overlay.** Sunday morning, brewer is deciding what to change for batch v9. He selects v6 and v7 and the gravity curves overlay on one chart. v7's curve sits 1.5 SG higher than v6's at every point. The overlay surfaces a quantified summary: "v7 fermented 1.4 °C cooler through primary; finished 1.5 SG higher; both batches used the same yeast pitch rate." - **The first share with a brewing-club mate.** A user shares "Czech Pilsner v6" with a friend. The friend opens the link, sees the recipe header, the gravity curve, the tasting notes, and posts back a structured comment: "the Saaz aroma feels muted on day 30 — did you dry-hop?" The comment thread is per-batch and revocable. ## 6. Page structure Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent. 1. **Welcome / sign-in.** A photographed-looking shot of a garage workbench at golden hour with a clear glass carboy of fermenting hazy IPA, a hydrometer floating in a flask beside it, a handwritten notebook open with smudged ink readings. One paragraph: "Brewing Notebook is a per-batch fermentation journal that listens. Record a voice note in the closet, the garage, or the brewhouse, and we'll build the graph and propose the one thing to tweak next time." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample notebook" → loads the demo lineage in section 8a. 2. **Notebook home.** The user's batches grouped by style lineage. Each lineage shows: style name, batch count, most recent batch status (fermenting / conditioning / packaged), and a tiny sparkline of the most recent gravity curve. Tap a lineage → list of batches; tap a batch → detail view. 3. **Active batch — day-by-day log.** Top: recipe header (style, OG target, yeast, vessel). Middle: the gravity-curve graph (full width, measured points, dashed predicted curve to FG, shaded FG- confidence band, temperature line on a secondary axis, hop additions as vertical markers, dry-hop bags as labelled chips, krausen notes as annotations). Bottom: chronological feed of observations (each one a card with date, voice-memo waveform, transcript, parsed fields, photos). One big red record button floats above the feed. 4. **Voice-recording flow.** Tap record → live waveform with a subtle gain meter. Stop → a 3-line preview shows the parsed fields ("day 4, 19 °C, SG 1.058") with a confirm button. Confirm → the observation drops into the feed and the graph updates with a small animation. Reject → the voice memo is preserved but unparsed; the user can edit the parsed fields manually. 5. **Side-by-side overlay.** Pick two or more batches in one lineage → the gravity curves overlay on one chart. A quantified summary below the chart names the differences ("v7 fermented 1.4 °C warmer through primary; finished 1.5 SG higher"). 6. **Tweak proposal view.** When the brewer marks a batch as "packaged" or "ready to plan next batch", the tweak proposer call runs and surfaces the single concrete tweak. The proposal card shows: the tweak one-liner in large type, the rationale in prose (3-6 sentences citing batches with chips that link to them), the expected effect, the confidence as a slim bar, the risk-note below in muted type. The food-safety disclaimer surfaces prominently when the proposal touches anything sanitation- or off-flavour-adjacent. 7. **Recipe editor.** A focused form for the recipe header. Style picker (BJCP 2021 categories), batch size, OG/FG/IBU/SRM/ABV targets, mash schedule, grain bill, hop schedule, yeast strain (with attenuation and temperature ranges resolved via the grounding call), water profile, salt additions, parent-recipe link. BeerXML / BeerJSON 2 import on the side. 8. **Brewing-day mode.** Full-screen kettle-side checklist: strike water temperature, mash-in, vorlauf, sparge, pre-boil gravity, boil start, hop additions countdown (live, audible if user chooses), whirlpool, chill, transfer. Each step gets a timestamp and an optional voice note. Designed for a wet hand and a hot kettle — large tap targets, high contrast, no fine print. 9. **Off-flavour explainer.** Each off-flavour name in the schema has its own page. Plain-English description, common causes, common fixes, food-safety disclaimer where applicable. Each instance the brewer flagged in his history links here for re-reference. 10. **Style lineage history.** The full chronological list of batches in one style, with key parameters as columns and a multi-line chart of OG, FG, and ABV across the lineage. The brewer can see his iteration arc at a glance. 11. **Sharing.** Modal: "Share this batch with a brewing-club mate or another brewer." Magic-link email; the recipient lands on a read-only view of the batch with a comment field. Per-batch, revocable. 12. **Settings & privacy.** Unit preferences (SG vs Plato; Celsius vs Fahrenheit; litres vs gallons), notification preferences (calendar-style local reminders only), export to BeerXML / BeerJSON, "Delete this lineage" / "Delete this account" controls with a 60-second cool-off. Privacy panel restates the not-trained-on policy in plain English. 13. **Footer.** "Made for the brewer dialling in batch v8 of v8." Privacy: "Your recipes, voice memos, and batch data are yours. We never train on them." Capabilities `(i)` icon in header. ## 6b. First-visit onboarding Show a **first-visit onboarding** the first time a visitor lands on the app (detect via `localStorage` flag; do not show on return visits). Three slides, dismissible at any time. Persistent re-entry: a `?` icon in the header reopens it. **Slide 1 — What this is.** - Headline: "Welcome to Brewing Notebook." - Subhead: "A per-batch fermentation journal that listens — in any style, any scale, any vessel — and tells you the one thing to tweak next time." - One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic brewing app: voice is the primary input, the graph is the working surface, the long-context model reads the whole lineage to propose exactly one concrete tweak per batch, and food-safety adjacent advice is always flagged as not-a-substitute. - Visual: a small annotated illustration of a gravity-curve graph with the relevant features labelled (measured points, predicted- FG band, hop addition markers, krausen note annotations) — not a generic beer mug. **Slide 2 — Try it now.** - One short prompt: "Try with the sample notebook". - A live demo input pre-loaded with the eight-batch hazy IPA lineage from the seed content in section 8a. - 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the voice-parse on the day-4 memo, the predicted FG band, the long-context tweak proposal for batch v9). **Slide 3 — How to remix this.** - Headline: "Make this yours." - Three short bullets: - "Swap the sample lineage in `/data/seed-notebook/` for your own batches." - "Adjust the prompts in `/server/prompts/` to fit your style and vocabulary (cider, mead, kombucha, sake)." - "Wire up your Gemini API key and Firebase project via the env-var list in the capabilities panel." - Primary CTA: "Use this template" → links to AI Studio Build remix entry point. - Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again). **Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`. **Don't:** - Don't gate content behind the modal. The page beneath must be fully usable. - Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`. - Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only. ## 6c. Capabilities info button (persistent in header) Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**. **Panel contents (in this order):** **Gemini capabilities used (the hero list):** - **Gemini 3.5 Flash (multimodal audio)** — listens to your voice memos in any language and parses them into structured observations: day, temperature, gravity reading, hop additions, krausen notes, smell descriptors, off-flavour flags, decisions taken. Code-switching between languages mid-memo is supported. - **Gemini 3.5 Flash (multimodal image)** — describes your wort, krausen, hydrometer, and packaging photographs into the observation log. Useful months later when the long-context tweak proposer reads your lineage. - **Gemini 3.5 Flash (long context)** — reads your whole style lineage at once (up to ~100 batches before chunking) to propose exactly one concrete, single-variable tweak for your next batch, anchored in citations from prior batches. - **Gemini 3.5 Flash (structured output)** — every voice memo becomes a typed `BatchObservation`; every tweak proposal is a typed `BatchTweakProposal`. The schemas live in the repo. - **Gemini 3.5 Flash + grounded search** — resolves yeast attenuation ranges, hop variety descriptors, water-chemistry standards, and BJCP 2021 style targets from cited public sources. - **Gemini TTS** — reads your past voice memos aloud when your hands are wet at the kettle. - **Nano Banana 2** — generates the welcome and empty-state imagery (photographic-looking carboys, kettles, workbenches). - **Firebase Auth** — Google and Apple sign-in; per-batch sharing via magic links. - **Firestore** — stores your batches, syncs across devices in real time. - **Firebase Storage** — keeps your original voice memos and photographs at upload quality, forever. - **Cost note** — see the detailed breakdown in 6d. A single brewing day with 6 observations costs about $0.08 of Gemini API spend. A full lineage of 10 batches over a year of dialling in costs about $1.10 of Gemini total. - **Privacy note** — your recipes, voice memos, and batch data are private to you and any brewer you explicitly share a batch with. This app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. - **Food-safety note** — this app charts, suggests, and warns; it does NOT give food-safety advice as if it were authoritative. Whenever a question touches sanitation, contamination, or fermentation safety, the answer is flagged as not a substitute for an experienced brewer's judgement, your local brewing club, or your local food-safety authority. **Backend services this app depends on:** - Auth: see section 4b - Database: see section 4b - Storage: see section 4b — REQUIRES manual enable in Firebase console; AIS Build does not auto-provision Storage today. - Email: see section 4b — magic-link sharing requires the sender domain to be authorised in Firebase Auth. - Apple sign-in: optional, requires an Apple Developer account and Service-ID config. See section 4b. - Payments: see section 4b (not used in v1) - External APIs: see section 4b **Environment variables you'll need to configure:** - `GEMINI_API_KEY` — your Google AI Studio API key - `FIREBASE_PROJECT_ID` — your Firebase project id - `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only) - `OPENWEATHER_API_KEY` — optional, only if you want ambient- temperature annotation for outdoor / non-temperature-controlled brewing **Cost + privacy notes:** - One short paragraph per cost-sensitive capability: long-context tweak proposer calls are billed per token of input — a 10-batch lineage call is about $0.08 per run (default: at packaging time only). - One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this lineage forever" — gone in 60 seconds), what is never sent for training. **Documentation links:** - AI Studio Build docs - Gemini API multimodal audio, multimodal image, long-context, TTS, grounded search docs - Firebase Auth, Firestore, Firebase Storage docs - BJCP 2021 style guidelines (for style-target overlays) - BeerJSON 2 spec (for recipe import / export) **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) - **Voice memo → BatchObservation (Gemini 3.5 Flash, low thinking)** — a 30-second voice memo plus the system instruction averages ~3,000 input tokens and ~600 output tokens. At Gemini 3.5 Flash pricing (~$1.50/M input, ~$9/M output) that is ~$0.0063 per voice memo. A typical brewing day has 6 observations → ~$0.038. - **Photo description (Gemini 3.5 Flash, low thinking)** — ~1 image + short system instruction, ~1,200 input tokens and ~200 output tokens → ~$0.0025 per photo. ~3 photos per batch on average → ~$0.0075/batch. - **Predicted FG sanity-check (Gemini 3.5 Flash, low thinking)** — small input (recipe + last few observations), ~1,500 input tokens and ~300 output tokens → ~$0.0034 per run. Run once per new observation → ~$0.020 per brewing day. - **Long-context tweak proposer (Gemini 3.5 Flash, medium thinking, long-context)** — a 10-batch lineage averages ~60,000 input tokens and ~600 output tokens → ~$0.078 per run. Default frequency: run once at packaging time → ~$0.08 per batch. - **Grounded fact call (Gemini 3.5 Flash + grounded search)** — small input, ~$0.0008 per call. Used a handful of times per recipe (resolving yeast attenuation, hop alpha-acid range, style targets) → ~$0.005/batch. - **Off-flavour explainer (Gemini 3.5 Flash)** — ~$0.0005/call; user-driven, a few calls per batch at most. - **TTS playback (Gemini 2.5 Flash TTS)** — ~$10/M output tokens ≈ ~$0.000003/character. A 90-word memo readback is ~$0.0017. Cached per memo; charged once. - **Hero / empty-state image (Nano Banana 2)** — ~$0.03/image. Generated once per app install (cached client-side), or once per regenerate-empty-state action. - **Expected per-batch cost (across full lifecycle):** ~$0.18 — ~6 voice memos at ~$0.0063, ~3 photo descriptions at ~$0.0025, ~6 predicted-FG sanity checks at ~$0.0034, 1 long-context tweak proposer at ~$0.08, a few grounded facts, a few off-flavour explainers. - **Year of dialling in one style (10 batches):** ~$1.80 total Gemini API spend. - **Voice memo storage:** Firebase Storage standard tier ~$0.026/ GB/month. A 30-second voice memo at 64 kbps is ~240 KB; 60 memos per batch year ≈ ~14 MB ≈ ~$0.0004/month. Photos add another ~50 MB/batch ≈ ~$0.001/month per batch. ## 7. Design language - **Mood:** A brewer's working surface, not a tech product. Not a Untappd-style social feed. Not a SaaS dashboard. The garage at 9 pm with the airlock bubbling, the carboy in the closet, the notebook on the kitchen counter beside the kettle still cooling down, the tablet on a stand in the brewhouse beside the conical fermenter. A workbench at the moment between batches. - **Typography:** Clean grotesque for app chrome and data labels (Inter or Geist). Display serif for the batch name and lineage heading (Source Serif Pro or Fraunces). A handwriting-styled accent (sparingly) for the user's own voice-memo transcript preview — to mark the difference between the brewer's words and the structured fields parsed from them. - **Palette:** Warm paper background `#F4EFE6` for the notebook surface, deep ink `#1B1714` for body text, malt amber `#A37430` for headings and graph axes, wort gold `#D9A646` for the predicted-FG band, hop green `#5B8C3A` for hop-addition markers, diacetyl butter `#E0C065` only for off-flavour flags, faded red `#A33A2C` for the food-safety disclaimer chrome and for the one-tweak proposal card border. Borrowed from a brewer's notebook, not from SaaS design systems. - **Imagery:** Photographic. Carboys in real garages at golden hour. Stainless conicals in small brewhouses. Notebooks open beside hydrometers on wooden workbenches. No flat illustrations. No beer-mug emoji. Generated via Nano Banana 2 with prompts emphasising real working surfaces, warm light, no people, no commercial branding. - **Hand-feel touches:** Each new observation drops into the feed with a thin paper-shadow as if a notebook page were turning. The gravity-curve graph's predicted-FG band has a soft watercolour- wash quality — it is a range, and it looks like a range. Hovering on a measured point reveals the verbatim voice-memo transcript that produced it. The hop-addition markers rise out of the chart with a small lift, as if pegged on a real cork board. - **Spacing:** consistent 4-px base. Generous whitespace — the graph and the feed need air. - **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Observation cards use 6; the tweak-proposal card uses 12; the welcome card uses 20. - **Shadows:** subtle, layered, warm-tinted. Avoid heavy drop- shadows. - **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. The predicted-FG band's "shrinking on new data" animation is the one place where motion carries meaning; respect reduced-motion by jumping instead of animating. No bouncing splash animations. No theatrical hero animations. - **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Record your first day-1 voice note to start the curve"). ## 8. Content generation rules - Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'. - Invent plausible names, batch numbers, voice-memo transcripts, gravity readings, and tweak proposals that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on realistic brewing patterns — hazy IPA finishing 1.014, kveik attenuating 78%, Czech pilsner fermenting at 9 °C — but never claim that a fictional batch came from a real brewery or that a fictional yeast measurement is a real published value. - Tone: warm, direct, free of corporate language. This template is for a brewer, not a brewing company. - Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'. No 'AI-powered'. - Body copy: short paragraphs (2-4 sentences). Use lists where appropriate. - Plain language. Avoid jargon — except where the brewer already speaks the jargon (the home brewer wants to see "SG", "Plato", "IBU", "dry-hop", "diacetyl rest" because they speak that vocabulary daily). - Where the app outputs AI-generated content, never label it as "AI says" — let it speak naturally. Use small uncertainty cues only where epistemic honesty requires them (a low-confidence gravity reading shows as faintly underlined; tapping reveals the alternate units the model considered). ## 8a. Seed content (use these specific examples) Anchor every generated copy + sample data point in the concrete content below. Use these names, numbers, dates, and snippets verbatim where helpful, or generate close variants that sit in the same world. **Sample lineages (sidebar):** - "Hazy v" (8 batches, just me) — primary lineage: a hazy IPA dialled across eight batches in a Daly City garage. OG 1.060– 1.068, FG targets around 1.014, yeast London Ale III at 19 °C, hops Citra + Mosaic + Nelson Sauvin in various proportions. - "Czech Pilsner" (6 batches, contributors: me, club mate Honza in Manchester) — bohemian pilsner across six batches at 9–11 °C in a converted chest freezer with an Inkbird controller. - "Vienna Lager — Orizaba style" (4 batches, just me) — a Vienna lager built to resemble the import-amber style brewer's grandfather drank in Orizaba. - "Voss Kveik Pale" (5 batches, just me) — a pale ale with Voss kveik at 35 °C in a hall closet. Two-day primary, packaged in ten. - "Kettle Sour — Berliner Weisse" (3 batches, just me) — a Berliner Weisse using a Lactobacillus kettle-souring stage before the boil. **Sample batch in detail view (this is what the demo should show):** - **Batch name:** "Hazy v8" - **Style:** American IPA – Hazy/NEIPA (BJCP 21B) - **Batch size:** 19 L - **OG measured:** 1.062 - **FG target:** 1.014 - **IBU target:** 50 - **SRM target:** 5 - **ABV target:** 6.3% - **Yeast:** London Ale III (Imperial A38 Juice), published attenuation 70–75%, published temperature range 17–22 °C - **Grain bill:** 78% 2-row Pale Malt, 12% Flaked Oats, 6% Wheat Malt, 4% Carapils - **Hop schedule:** 14 g Citra at whirlpool (15 min, 80 °C); 14 g Mosaic at whirlpool; 30 g Citra + 30 g Mosaic dry-hop during active fermentation, day 3; 30 g Nelson Sauvin dry- hop on day 7 - **Water profile:** "soft NEIPA" — 4 g calcium chloride and 2 g gypsum into 25 L total brewing water - **Observation v8-day-1 (verbatim voice memo):** "day one, twenty Celsius, pitch is happy, krausen forming, smell is mostly worty, going to drop to nineteen tonight" - **Observation v8-day-3 (verbatim voice memo):** "day three, nineteen Celsius, gravity one-oh-three-eight by refractometer uncorrected, krausen peaked, smell starting to show Citra, dropping the first dry-hop bag now, sixty grams Citra and Mosaic" - **Observation v8-day-4 (verbatim voice memo):** "day four, nineteen Celsius, gravity one-oh-five-eight measured this morning… wait, I meant one-oh-three-zero, sorry, by hydrometer this time, krausen has dropped about a centimetre overnight, going to push to twenty for the diacetyl rest tomorrow" - **Predicted FG (after day-4 observation):** 1.014 ± 0.003, SG; confidence 0.68; rationale: "gravity has dropped 0.032 SG since day 1, consistent with the day-4 readings on v6 and v7 at the same temperature; London Ale III's published 70–75% attenuation range bounds the expected terminal gravity to 1.0124 – 1.0156." - **Tweak proposal (one-line):** "Drop the first dry-hop bag on day 6 instead of day 3 for v9." - **Tweak proposal (full rationale):** "Batches v3 and v6 had the same dry-hop schedule as v8 (day 3 + day 7) and finished with muted hop aroma at packaging; v7 dropped the first dry- hop bag on day 6 with the gravity at 1.020 and showed a noticeably brighter Citra expression at packaging. Three batches in this lineage (v3, v6, v7) are enough evidence to pin the dry-hop timing to active-fermentation completion rather than calendar day. v9 should drop the first dry-hop bag when the gravity hits 1.020 (estimated day 6 at 19 °C) and the second bag at day 9 instead of day 7." - **Cited batches:** v3, v6, v7 - **Expected effect:** "Brighter Citra and Mosaic aroma at packaging; should not affect final gravity by more than 0.001 SG." - **Confidence:** 0.74 - **Risk note:** "May reduce hop bitterness perception slightly; the v7 packaging notes mentioned a slightly leaner finish." - **Food-safety flag:** false - **Food-safety disclaimer required:** false **Sample voice memos with multilingual code-switching:** - Honza (Czech-British, Manchester): "den jedna, jedenáct Celsius, OG one-oh-four-eight, pitched two packets of W-34/70, krausen forming nicely, smell is malty and clean — language Czech with English numeric switching" - Maricel (Filipino-American, Daly City): "day four, nineteen na lang, gravity one-oh-three-zero, krausen peaked yesterday, may amoy ng Citra, going to drop dry-hop in two hours — Tagalog with English brewing vocabulary" - Diego (Mexican, Mexico City): "día dos, doce grados, gravedad uno-cero-cuatro-cinco, espuma hermosa, lleno de carácter de malta — Spanish throughout" - Solomon (Ethiopian-American, Oakland): "day one, twenty-one Celsius, OG one-oh-five-four, የጥራጥሬ rich flavour, krausen forming — English with Amharic descriptors" - Mai (Vietnamese-Australian, Melbourne): "ngày bốn, mười chín độ, gravity một-không-ba-mươi, dry-hop today — Vietnamese with English brewing terms" **Sample input artefacts (for the build to demonstrate):** - A voice memo recorded in a garage with a humming dehumidifier in the background, day 4 of Hazy v8. - A photograph of a clear glass carboy with active krausen at 4 cm height, light hop-trub ring around the rim. - A photograph of a refractometer showing approximately 1.030 on the SG scale (uncorrected), hazy wort drop on the prism. - A photograph of a stainless conical fermenter in a brewhouse with a sample valve and a thermometer reading 19.2 °C. - A photograph of a strange off-white film on the surface of a beer (the brewer is asking "mould or pellicle?"; the app describes neutrally and surfaces the not-a-substitute disclaimer). **Sample voice copy:** - Onboarding: "Record one voice note from your fermenter. We'll build the graph." - Empty active batch: "This batch is waiting for its first day-1 voice note. Tap record." - Processing: "Listening to your memo…" / "Parsing day, gravity, temperature…" / "Updating the graph…" / "Reading your lineage for the next tweak…" - Save confirmation: "Added to Hazy v8 — day 4 observation, SG 1.030, temperature 19 °C." - Predicted FG card: "Predicted final gravity: 1.014 ± 0.003. This is the model's best estimate, not a guarantee." - Tweak card heading: "One tweak for v9 — drop the first dry-hop bag on day 6 instead of day 3." - Food-safety surfacing: "This question touches on fermentation safety. This app is not a substitute for an experienced brewer's judgement, your local brewing club, or your local food-safety authority." - Low-confidence note: "We weren't sure about the gravity unit here. Tap the underlined value to see what we considered." - Share email subject: "Honza — can you look at Czech Pilsner v6 and tell me if I'm chasing a ghost?" - Share email body: "Hey Honza — v6 finished at 1.014 again, same as v5. I think the mash schedule is off. Have a look and ping me back with your read. [Open Batch]" ## 9. Media & assets - **Hero image (landing screen):** A photographed-looking shot of a garage workbench at golden hour, a clear glass carboy of fermenting hazy IPA in the centre with visible active krausen, a refractometer and a paper notebook open beside it, warm evening light. Generate via Nano Banana 2 with a prompt emphasising "warm afternoon light through a garage window, no people in frame, a clear glass carboy of fermenting hazy IPA with active krausen, a refractometer on a wooden workbench beside it, a paper notebook open with handwritten gravity readings, real worn paper, soft shadow under the carboy". - **App icon / wordmark:** Set in the display serif. Slightly worn paper texture behind it. No icon — just type. - **Empty-state illustration:** A simple line drawing of a single carboy with an airlock on top. Hand-drawn aesthetic, not a flat icon. - **Demo photos:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "stainless conical fermenter with sample valve, soft overhead light", "hydrometer in a sample tube, hazy gold liquid, focus on the SG scale reading approximately 1.030", "wort at flameout in a stainless kettle, deep amber colour, slight steam". Each demo photo should look photographed, not rendered. - **Style-target overlay graphics:** Vector overlays for BJCP 2021 ranges on the gravity chart (shaded regions for OG, FG, IBU, SRM, ABV). - **Stock fallbacks:** If image generation fails, fall back to the photographed sample carboy from `/public/samples/sample- carboy.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"). "Gravity reading 0.058 looks low — did you mean 1.058?" is the right shape. - 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: "Record your first day-1 voice note", "Import a BeerJSON 2 recipe", "Add another batch to this lineage" — never a generic "Add your first item". - Smooth scroll for in-page anchors. - All AI-generated content streams in token-by-token where supported, with a clear "thinking…" indicator before content starts arriving. - If an AI call fails, show a calm, specific error ("We couldn't parse the audio — the dehumidifier was loud. Want to try a shorter clip, or type the day and gravity into the side panel?") and offer retry. - Low-confidence gravity readings in the chart are faintly underlined; tapping reveals the alternate units (SG vs Plato vs Brix) the model considered. - The predicted-FG band's "shrinking on new data" animation takes 400 ms with `prefers-reduced-motion` falling back to instant. - The food-safety disclaimer surfaces as a non-dismissable inline banner above any tweak proposal that touches sanitation or off-flavour control — not a toast that scrolls off-screen. ## 11. Tech & responsive requirements - **TTS markdown-stripping preprocessor:** before sending any user-authored markdown to `gemini-3.1-flash-tts-preview`, strip non-spoken markdown: `#`/`##`/`###` headings (keep the title text), `**bold**` (keep the inner text), `[label](url)` (keep `label`, drop URL), `` ``` `` fenced code blocks (skip entirely), `>` block-quote markers (keep the text), and `|` table pipes (read row-by-row as sentences). Insert `…` between sentences for a short pause and a blank line plus `—` between paragraphs for a long pause. The model does not understand markdown; raw markdown will be read aloud as literal characters ("asterisk asterisk"). - **File downloads on Safari / Firefox:** when offering local-disk save of any export (PDF, CSV, MP3, ZIP, JSON, image), fall back to `` with a blob URL — the File System Access API (`showSaveFilePicker()`) is Chromium-only. Detect with `'showSaveFilePicker' in window`; otherwise use the anchor-download path. - **Stack:** React + TypeScript + Tailwind CSS. Functional components + hooks. Use Shadcn UI primitives where appropriate. Graph rendering via Recharts or Visx (both work; Visx gives more control over the shrinking-confidence-band animation). - **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 voice- parse / photo-describe / predicted-FG / tweak-proposer and `gemini-3.5-flash` for grounded facts and off-flavour explainer. Set `thinkingLevel` explicitly per call. - **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed lineage on first launch. - **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as fallback (for share links). - **Storage:** Firebase Storage for voice memos and photos. Pre-signed URLs only. - **Audio handling:** the web Recorder API for voice capture; capture as `audio/webm` or `audio/mp4` (depending on browser support), upload to Firebase Storage, then pass the resulting `files/*` resource name to the voice-parse Gemini call via the Files API. Do NOT pass Firebase Storage public URLs to `generateContent` — the API does not fetch them server-side. - **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. - Use `clamp()` for fluid typography. Prefer container queries over media queries for component-level responsiveness. - Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS. - Zero horizontal overflow at any width. Zero layout shift on load. - Persist user data in Firestore. Use real-time listeners on the active batch view (so a brewer recording in the closet sees the graph update on the tablet in the kitchen). - Optimistic UI on writes; reconcile on response. - Brewing-day mode uses the screen-wake-lock API to keep the display on at the kettle; falls back gracefully if unsupported. ## 12. Accessibility (WCAG 2.2 AA) - Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`. - All interactive controls reachable by keyboard with a visible focus ring. - Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. - All images have meaningful `alt` text. The photographed observation images have `alt` describing the artefact ("photograph of a hydrometer reading approximately 1.030 SG in hazy gold wort, taken in a Daly City garage at golden hour"). - Form fields have associated `