# 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. --- # Birthday Card Designer ## 1. Project **Birthday Card Designer** turns the ninety seconds before you forget about your friend's birthday into a card that looks like you spent an hour on it. Type the recipient's name. Describe them in one line — the colours they live in, the things on their kitchen counter, the hobby they fell in love with last year, the inside joke from your last trip. Optionally upload one to three reference photos: their bookshelf, the mug they always drink from, the colour palette of their flat, the dog. The app takes the description and the references to **Nano Banana Pro** (`gemini-3-pro-image`) and returns four versions of a card whose cover art reads as **specifically about this person**, with their **actual name rendered as legible typography at 4K**, and your written message printed verbatim on the inside. A print-ready 5×7 PDF with bleed and crop marks comes back in the same call as the Instagram-square version, a subtle animated version, and the same card with a slot to record a voiceover greeting. This is the kind of app a friend taps on the bus ride home from work, on the day of a birthday they almost forgot, after realising the generic e-card from Punchbowl will read like exactly what it is. It is also the kind of app a partner uses the night before an anniversary when the gift hasn't arrived yet, a parent uses on the morning of their kid's birthday, a colleague uses for a goodbye card the team will sign, a long-distance grandchild uses to send a card their grandmother can actually print and put on the mantelpiece. The job is the same in every case: an hour of Canva, a $14 Etsy listing with a four-day wait, a hand-drawn card the sender thinks looks clumsy — replaced by ninety seconds and a result that does not look like a template. The thirty-second demo that proves the magic: a user types **"Priya, forty-one tomorrow, runs every morning before sunrise, drinks black coffee in a green ceramic mug, the kind of person who reads three books a month and remembers every one"**, drags in three reference photos (her bookshelf, her green mug on the windowsill, the bridge she runs across at dawn), pastes a message — "Happy birthday from the Berlin crew, save us a chapter of whatever you're reading next week" — and selects "print" as the primary output. Twenty-eight seconds later, four card variations come back. Variation A is a warm-grey cover with the green of the mug pulled into a serif title that reads **"Happy Birthday, Priya"** in legible 4K typography; the silhouette of the bridge sits underneath, dawn light bleeding into the type. Variation B is the same scene at a different time of day, with a stack of books layered into the foreground. Variation C is a quieter, more typographic take, almost a poster. Variation D is the Instagram-square version cropped to lead with the mug. The print PDF has 0.125" bleed, crop marks at the trim, and the user's verbatim message printed inside in a serif body face. The card never reproduces a real character, a real brand, or a real person's likeness. The recipient's name is the only proper noun rendered as in-image text. The user's interior message is verbatim, never paraphrased, never autocompleted, never "improved" by the model. **Tagline:** _Type a name, drop three photos, get a card that looks made-for-them — print-ready in ninety seconds._ ## 2. Target audience - Friends who almost forgot a birthday and want to send something better than an e-card at 11:47 pm - Partners and parents the morning of, when the gift hasn't arrived yet and a real card on the table changes everything - Long-distance family sending a print-ready card across an ocean, in the script the recipient actually reads — Latin, Devanagari, Cyrillic, Arabic, Hebrew, Malayalam, Korean, kana - Colleagues running the office goodbye card who want one cover the whole team can sign, not a Hallmark cliché - Teachers who send each of twenty-two kids a personalised card on the last day of term - Couples on anniversaries — the same engine, "happy anniversary" swapped for "happy birthday" - Bridesmaids and best men assembling the joint card the rest of the wedding party will write on - Wedding planners who want a save-the-date that pulls from the couple's colour palette - Sympathy cards, get-well cards, congratulations cards — the same primitive, different occasion picker - Diaspora friends sending a card in a script the recipient reads natively — Devanagari, Cyrillic, Arabic, Hangul, kanji, Tifinagh - Anyone who has bought a card at a petrol station while running late and felt the embarrassment of handing it over ## 3. Core value propositions Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons a visitor stops scrolling and taps. - **The card looks made for this person, not picked off a shelf.** Nano Banana Pro reads your one-line description and one-to-three reference photos and pulls the recipient's actual aesthetic — the colour of their flat, the texture of their bookshelf, the bridge they run across — into the cover art. The result reads as personal the second the recipient opens it. - **Their actual name, rendered as 4K typography in the image.** This is the post-I/O 2026 leap that makes the difference. The card cover says "Happy Birthday, Priya" or "С Днём Рождения, Алина" or "お誕生日おめでとう、ハルキ" with legible, designed letterforms — not smeared, not hallucinated, not "happy birrthdy". Nano Banana Pro finally does this at production quality. - **Your message, verbatim, on the inside.** What you typed is what the recipient reads. The model never paraphrases your message, never autocompletes it, never adds emoji you did not type. This is a hard rule, not a setting. - **Four variations, ninety seconds, one call.** You do not pick a template, fill a form, and pray. You get four cover variations back at once: print-quality A, alternate-mood B, typographic C, Instagram-square D. You pick. Each rendered at 4K and ready to download, send, or print. - **Print-ready 5×7 PDF with bleed and crop marks.** The print variation comes back as a proper PDF — 5×7 trim, 0.125" bleed, crop marks at the trim line, CMYK-tagged for desktop printers, 300 DPI. Drop it on a Brother MFC at home, or upload it to a local print shop. It works on day one. - **Animated and voiceover versions when you want them.** Tick the animation box and the cover gets a subtle 4-second loop — wind through the bridge silhouette, steam rising from the mug, a leaf drifting through frame. Tick the voiceover box and you can record a 30-second greeting on top of the image; the recipient gets a URL that plays the card. - **Multi-reference style guide (up to fourteen photos).** Power users can drop in fourteen photos — the recipient's flat, their wardrobe palette, their hobby gear, their bookshelf — and the model treats the set as a style guide for the card. The cover feels like it could have been art-directed by someone who has been in their living room. - **Universal, in any language and any script.** The recipient's name renders in their native script. The interior message renders in whatever language you typed. Hindi, Arabic, Mandarin, Korean, Russian, Greek, Hebrew, Amharic, Tamil — all handled at 4K. - **Never imitates a real person, brand, or trademarked character.** The model is instructed never to reproduce copyrighted likenesses or trademarked characters. If you ask for a card "in the style of Pixar" or "with Hello Kitty on the front" the app declines with a one-line note and proposes a non-infringing alternative. ## 4. Features to build - One-screen authoring flow — name, one-line description, optional references, your verbatim message, occasion picker, output format - Reference photo dropzone — accepts up to 14 images via drag-and- drop, paste, or camera; previews each one with a remove control - Description field — 280-character one-liner describing the recipient; live character count; placeholder example rotates - Verbatim message field — 600-character interior message, the field the model is forbidden to paraphrase - Occasion picker — Birthday, Anniversary, Congratulations, Sympathy, Get Well Soon, Goodbye, Save the Date, Custom; affects cover tone, never the user's verbatim message - Output-mix picker — Print PDF (5×7, default on), Instagram square (1080×1080), Animated (subtle 4s loop), Voiceover (record 30s on top of the card) - Four-variation grid — A (the recommended), B (alternate mood), C (typographic), D (Instagram square); each rendered at 4K, each downloadable individually - "Regenerate just variation B" — fine-tune one variation without recomputing the others; the rest of the grid stays put - Annotate-and-fix mode — circle a part of the cover, type the fix ("more green in the title", "remove the second mug"), the model re-renders that region only. Preserves cost and fidelity. - Print PDF builder — 5×7 trim, 0.125" bleed, crop marks at the trim, 300 DPI, RGB → CMYK conversion server-side - Voiceover recorder — in-browser audio capture; saved alongside the card; recipient lands on a page that auto-plays the audio over the card image - Sendable card URL — every saved card gets a shareable URL the recipient opens to see the image, the verbatim message, optionally hear the voiceover; expires after 90 days or when the sender deletes - Recipient-name script picker — auto-detected from the name, but overridable (Priya in Devanagari, Priya in Latin) - Cover-text legibility check — server-side computes contrast ratio between the rendered name and the area of the cover behind it; if below 4.5:1, re-renders with a typographic adjustment - Card-history sidebar — every card the user has made, with the recipient name, the occasion, the date sent, and a small thumbnail - Send via email — magic-link delivery to the recipient's email, with the print PDF attached and the URL inline - Send via SMS link — short URL the user texts to the recipient - Resend / reprint — every saved card stays in the user's library, re-printable on any future birthday - Trademark / likeness refusal — explicit refusal flow with a one-line explanation and a non-infringing alternative suggested - Mobile-first capture — the user can take a reference photo from the phone camera directly into the dropzone - Reduced-motion fallback — the animated variation has a static equivalent users land on when `prefers-reduced-motion: reduce` - Free-tier ceiling — 3 cards per month free; beyond that, the user is prompted to bring their own Gemini API key ## 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) - **Nano Banana Pro — 4K cover generation with legible in-image text** (`gemini-3-pro-image`) — the load-bearing capability of this template. Accepts a structured `CoverPrompt` plus zero-to- fourteen reference images and emits a 4K cover image with the recipient's name rendered as legible designed typography. This is what makes the card feel made-for-them; this is what was not possible before I/O 2026. The prior generation of image models smeared or hallucinated in-image text; Nano Banana Pro renders it cleanly at production quality. - **Multi-reference style ingestion (up to 14 images)** — the same Nano Banana Pro call accepts up to fourteen reference images and treats them as a style guide. The cover pulls colour palette, texture, mood, and compositional cues from the references without copying any specific image. Used for power users who want the card to feel art-directed. - **Description → CoverPrompt structuring** (`gemini-3.5-flash`, thinkingLevel `low`) — takes the user's one-line description and the user's recipient name and emits a structured `CoverPrompt` JSON that names the colour palette, the compositional cues, the mood, the suggested type style, and three optional cover concept alternatives. The image model never sees the raw one-liner; it sees the structured prompt. This indirection is what makes the four variations feel coherent rather than random. - **Trademark / likeness screen** (`gemini-3.5-flash`, thinkingLevel `medium`) — runs once over the description + every reference filename + every reference image's content. Returns a `RefusalDecision` object with `is_refused`, `reason`, and a non-infringing alternative suggestion. Reasons include: explicit request for a real person's likeness, explicit request for a trademarked character, explicit request for a brand logo, reference image clearly depicts a copyrighted character. Triggers a UI refusal banner before the cover call is made. - **Cover-text legibility verifier** (`gemini-3.5-flash`, thinkingLevel `low`) — after Nano Banana Pro returns a cover, this call inspects the rendered image (multimodal) and the intended name string, and emits a `LegibilityReport` with `is_legible`, `contrast_pass`, `name_spelled_correctly` and a short reason. If the name was hallucinated or smeared (rare on Pro, but possible in non-Latin scripts on the first attempt), this triggers a single re-render with a tighter prompt. Bounded retry: maximum one re-render per variation. - **Voiceover transcript (Gemini 3.1 Flash TTS)** (`gemini-3.1-flash-tts-preview`) — optional. If the user does NOT want to record their own voice but wants a spoken greeting, this call reads the user's verbatim interior message aloud in the user's chosen voice. Gemini 3.1 Flash TTS handles English, the major European languages, Mandarin, Cantonese, Korean, Japanese, Hindi, Arabic, Hebrew, Portuguese (both), Turkish. Pace cues encoded as `…` and blank-line `—` — no SSML. - **Optional: animated cover via Veo 3.1** — animation is currently generated as a subtle 4-second loop using Veo 3.1 (`veo-3.1`). Gemini Omni Flash is the announced replacement but has no developer API as of June 2026; we ship with Veo 3.1 fallback and the capabilities panel flags "Omni Flash coming soon". The animation prompt is constrained to motion that preserves the typography legibility — wind, drift, steam, light; never camera movement that distorts the name. - **Thinking levels** — `low` for description-structuring, legibility check, occasion suggestion. `medium` for trademark screen (it has to reason about likeness, brand, and reference content). `high` is not used. Image and TTS calls take no `thinkingConfig`. - **Structured output** — every text call uses `responseSchema` with the Zod schemas below. Image and TTS calls do not use `responseSchema`. ### Backend services - **Auth — Required.** Firebase Auth with Google sign-in (auto- provisioned by AI Studio Build, post-I/O 2026). Apple sign-in is optional and requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. Anonymous browsing of the marketing landing is allowed; sending a card requires sign-in so the card library persists. - **Database — Required.** Firestore for `users`, `cards`, `references`, `recipients`, `share_links`, `voiceovers`. Auto-provisioned. - **File storage — Required.** Firebase Storage for reference photographs (preserved for 30 days then auto-purged), generated 4K covers (preserved indefinitely under the user's account), and voiceover recordings. **Storage is NOT auto-provisioned by AI Studio Build** — enable it in the Firebase console and wire the bucket name before the first upload. - **Cloud Run server-side functions — Required.** All Gemini API calls happen server-side via Cloud Run (auto-provisioned, free for the first two Build apps post-I/O 2026). The Gemini API key lives in Google Cloud Secrets Manager and never reaches the client bundle. Image generation, trademark screen, and PDF builder all run server-side. - **Email — Required (transactional).** Magic-link delivery of cards to recipients; share-link emails to the sender. Sender domain must be authorised in Firebase Auth. - **Payments — Optional in v1.** Free tier: 3 cards/month. Beyond the free tier, two paths: (1) the user supplies their own Gemini API key (free for them; spend on their own Google Cloud account), (2) a small monthly subscription via Stripe (not built in v1; capabilities panel flags it as coming soon). - **External APIs:** Gemini API for all intelligence; a CMYK- conversion library on the server (the LittleCMS bindings) for print PDF colour profile conversion. No required external API beyond Gemini. **Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Stripe key if paid tier added, sender email domain credentials) 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 · recipient names, reference photos, and interior messages 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. Reference photos auto-purge after 30 days. Cards in the user's library remain until the user deletes them. **Read this first — prompt-craft rules that apply to every call in this template:** 1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below. 2. **Pin `thinkingLevel` explicitly** per call. See the matrix. Image and TTS calls have no `thinkingConfig` — omit the field entirely; do not serialise `n/a`. 3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schemas are below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.** 4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate. 5. **Pre-declare tools as an enable/disable list** per call. None of this template's calls use grounded search; do not enable `google_search`. Tools NOT listed for a call should be disabled. 6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow. 7. **Reference images are passed via the Gemini Developer API Files API** — after Firebase Storage upload, the server-side function re-uploads to the Files API and passes the resulting `files/*` resource name (e.g. `files/abc123xyz`) via `fileData: { fileUri, mimeType }`. Do NOT pass Firebase Storage public URLs to `generateContent` — the API does not fetch them server-side. `gs://` URIs belong to Vertex AI / Cloud Storage — a different surface, not accepted here. Use the Files API for reference photos > 1 MB; use `inlineData` (base64) for the small thumbnails. 8. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing. ### Per-call model + tools matrix | Call | Model | thinkingLevel | Tools enabled | |------|-------|---------------|---------------| | Description → `CoverPrompt` | `gemini-3.5-flash` | low | (none) | | Trademark / likeness screen | `gemini-3.5-flash` | medium | (none) | | Cover generation (4 variations) | `gemini-3-pro-image` | n/a | n/a | | Cover-text legibility verifier | `gemini-3.5-flash` | low | (none) | | Targeted region re-render (annotate-and-fix) | `gemini-3-pro-image` | n/a | n/a | | Optional voiceover TTS | `gemini-3.1-flash-tts-preview` | n/a | n/a | | Optional animated cover (4s loop) | `veo-3.1` | n/a | n/a | | Empty-state / how-it-works hero illustration | `gemini-3.1-flash-image` | n/a | n/a | *Note for builders:* on image, video, and TTS 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. There are no grounded search calls in this template — every call is either structured JSON over text or a direct media generation. ### Primary structured-output schemas (seed verbatim in the prompt) ```typescript import { z } from "zod"; const Script = z.enum([ "latin", "cyrillic", "devanagari", "bengali", "tamil", "malayalam", "telugu", "kannada", "sinhala", "gurmukhi", "gujarati", "arabic", "hebrew", "han_simplified", "han_traditional", "hiragana_katakana", "hangul", "thai", "ge_ez", "tifinagh", "greek", "armenian", "georgian", "other", ]); const RecipientName = z.object({ text_as_typed: z.string(), // verbatim user input script_detected: Script, // server-detected, overridable script_chosen: Script, // final script for cover bidirectional: z.boolean(), // true for Hebrew, Arabic }); const Occasion = z.enum([ "birthday", "anniversary", "congratulations", "sympathy", "get_well", "goodbye", "save_the_date", "thinking_of_you", "custom", ]); const CoverPrompt = z.object({ cover_prompt_id: z.string(), recipient_name: RecipientName, recipient_one_liner_verbatim: z.string().max(280), occasion: Occasion, custom_occasion_phrase: z.string().nullable(), // only if occasion === "custom" palette_named: z.array(z.string()), // ["warm dawn grey", "ceramic green"] palette_hex: z.array(z.string()), // ["#A7A096", "#7A9F7A"] mood_descriptors: z.array(z.string()), // ["quiet", "early-morning", "warm"] composition_hint: z.string(), // "horizon line low; type top-third" type_style_hint: z.string(), // "serif, warm, slightly worn" subject_motifs: z.array(z.string()), // ["green ceramic mug", "bridge at dawn"] forbid_motifs: z.array(z.string()), // ["candles", "balloons", "confetti"] reference_image_uris: z.array(z.string()).max(14), variation_concepts: z.array(z.object({ label: z.enum(["A_recommended", "B_alt_mood", "C_typographic", "D_square"]), one_line_concept: z.string(), })).length(4), cover_text: z.string(), // EXACT text to render on cover cover_text_script: Script, // script the cover_text is in refusal_check_required: z.boolean(), // always true; for audit log prompt_confidence: z.number().min(0).max(1), }); const RefusalDecision = z.object({ is_refused: z.boolean(), reason_code: z.enum([ "ok", "real_person_likeness", "trademarked_character", "brand_logo", "explicit_or_violent_content", "reference_image_depicts_copyrighted_work", "other", ]), reason_one_line: z.string(), // user-facing alternative_suggestion: z.string().nullable(), // non-infringing pivot reference_indices_flagged: z.array(z.number()), // which references tripped the check }); const CardVariation = z.object({ variation_label: z.enum([ "A_recommended", "B_alt_mood", "C_typographic", "D_square", ]), image_gs_uri: z.string(), // Files API `files/*` resource name for the 4K render image_aspect: z.enum(["5x7_portrait", "1080_square"]), image_pixel_width: z.number(), image_pixel_height: z.number(), cover_text_rendered: z.string(), // verbatim text on cover generation_seed: z.number().nullable(), }); const LegibilityReport = z.object({ variation_label: z.enum([ "A_recommended", "B_alt_mood", "C_typographic", "D_square", ]), is_legible: z.boolean(), contrast_ratio_estimate: z.number(), // 4.5 minimum for pass contrast_pass: z.boolean(), name_spelled_correctly: z.boolean(), name_rendered_verbatim: z.string(), // what the model sees on the cover reason_one_line: z.string(), re_render_recommended: z.boolean(), }); const InteriorMessage = z.object({ text_verbatim: z.string().max(600), // EXACT user input; never paraphrased language: z.string(), // BCP-47, server-detected script: Script, }); const Card = z.object({ card_id: z.string(), sender_user_id: z.string(), recipient_name: RecipientName, recipient_email: z.string().nullable(), occasion: Occasion, cover_prompt: CoverPrompt, refusal_decision: RefusalDecision, variations: z.array(CardVariation), // 4 entries legibility_reports: z.array(LegibilityReport), // 4 entries interior_message: InteriorMessage, voiceover_gs_uri: z.string().nullable(), animated_cover_gs_uri: z.string().nullable(), print_pdf_gs_uri: z.string().nullable(), share_url: z.string().nullable(), share_expires_at_iso: z.string().nullable(), created_at_iso: z.string(), }); type CoverPrompt = z.infer; type RefusalDecision = z.infer; type CardVariation = z.infer; type LegibilityReport = z.infer; type Card = z.infer; ``` ### Common failure modes (and how to avoid them) - Agent picks `gemini-3.1-flash-image` (Nano Banana 2) for the cover call to save on cost — pin `gemini-3-pro-image` explicitly. Nano Banana 2 cannot render legible in-image text at 4K; using it silently breaks the headline value prop and the recipient's name will look smeared or wrong, especially in non-Latin scripts. - Cover-text passed as a generic prompt string ("write 'happy birthday Priya' on the cover") — Nano Banana Pro renders text most reliably when the exact glyphs to render are pinned in a dedicated `cover_text` field of the structured prompt, with the script explicitly named. The `CoverPrompt.cover_text` field is the source of truth; the image-gen system instruction quotes it verbatim. - Non-Latin script mis-rendered on first attempt — Nano Banana Pro is strong here but not perfect on Bengali, Tamil, Ge'ez, Khmer. The legibility verifier catches this; one bounded re-render with the script explicitly named in the prompt usually fixes it. Never silently retry more than once — bound to a single retry to cap cost. - Interior message paraphrased by the model — this is the bug that loses trust. The interior-message text is NOT sent to any text generation call; it is rendered on the inside of the PDF server-side by the PDF builder using the verbatim string from `InteriorMessage.text_verbatim`. Gemini never sees it for editorial purposes. - Cover text legibility test fails because the model placed the name over a busy area — the legibility verifier returns `re_render_recommended: true` with a tighter `composition_hint`. The retry uses the same seed but a different composition hint pushing the type into a clean area. - Real-person likeness slips through — the trademark screen runs BEFORE the cover call, sees both the text description and the reference image contents (via multimodal). If a reference photo is a poster of a public figure, the screen catches it. The screen is a hard gate, not a soft warning. - Trademark / character request reaches the image model — the trademark screen is a HARD GATE that runs before image generation. If `is_refused === true`, the cover call never fires; the UI shows the user a one-line refusal and the suggested alternative. - Reference photo > 7 MB rejected by Files API — server-side compress to ≤ 2 MB JPEG at 92% quality before upload to the Files API; preserve the original separately in Firebase Storage for the user's record. - Print PDF colour shift — RGB → CMYK is done server-side with the LittleCMS bindings against a US SWOP v2 profile; the user can download both an RGB PDF (for digital viewing) and a CMYK PDF (for print shop). - Animation distorts the typography — the Veo 3.1 prompt explicitly bans camera motion and bans any motion that crosses the recipient-name region. Only ambient motion (wind, light, steam, drift) is allowed. - Voiceover audio clipped — the in-browser recorder caps at 30 seconds; longer recordings are truncated with a warning, not silently cut. - Free-tier exhausted mid-flow — the cover call is gated; if the user has used their three monthly cards, the flow stops at the CoverPrompt step and offers either bring-your-own-key or subscription. Never wastes API spend on a card that can't be finished. ### Negative constraints (hard rules) - Do NOT reproduce trademarked characters, brand logos, or copyrighted artwork on the cover. The trademark screen is a hard gate. If the user asks for "a card with Mickey Mouse on it" the flow stops with a one-line refusal and a non-infringing pivot ("we can do a friendly mouse character with similar warmth — want me to try that instead?"). - Do NOT reproduce a real person's likeness. If the user uploads a photo of a celebrity as a reference, or describes the cover as "looking like [public figure]", the trademark screen catches it and refuses. Photos of the recipient themselves are fine — the recipient is the user's friend, not a public figure being impersonated. - Do NOT paraphrase the interior message. The user's verbatim text is rendered exactly as typed. No autocompletion, no "improved grammar", no emoji added, no emoji removed unless the user removed them. The interior message NEVER goes through a text generation call. - Do NOT translate the recipient's name. "Priya" stays "Priya". "Алина" stays "Алина". "ハルキ" stays "ハルキ". - Do NOT auto-suggest emoji in the interior message. The user types what they type. - Do NOT generate cards that include the user's identity claims ("from your favourite cousin") unless the user typed that themselves in the verbatim message. - Do NOT render a misspelled name on the cover. The legibility verifier compares the rendered name against the intended name string. If the spelling differs, force a single re-render with the name pinned more aggressively. If the second attempt also mis-spells, return three variations instead of four and surface a one-line note: "Your fourth variation didn't render the name cleanly — we shipped three instead." Never ship a card with a misspelled name. - Do NOT include any text on the cover other than the recipient's name and (where the occasion calls for it) the occasion phrase. No watermarks, no app branding, no "designed with Birthday Card Designer" footer. The card belongs to the sender. - Do NOT auto-publish cards. Cards are private to the sender's library until the sender explicitly sends them. - Do NOT use the sender's reference photos, recipient names, or interior messages to train 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 show four nearly-identical variations. The CoverPrompt enforces meaningful diversity: A is the recommended take, B is an alternate mood (different time of day, different season, or different motif foregrounding), C leans typographic (the title carries more of the design), D is the Instagram-square composition. Diversity is a constraint on the prompt, not a hope. - Do NOT save reference photos for longer than 30 days. They auto-purge from Firebase Storage on day 30 unless the user explicitly extends. The card library keeps the generated cover art indefinitely (it is the user's creative work) but not the original references (they are personal data of the recipient, often). - Do NOT show a generic "AI is thinking" spinner. The UI surfaces meaningful step labels: "Reading your reference photos…", "Sketching four cover variations…", "Checking the recipient's name is rendered correctly…", "Building your print-ready PDF…". ### 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: Description → `CoverPrompt` Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You convert a sender's one-line description of a card recipient into a structured CoverPrompt JSON object. The prompt you produce drives Nano Banana Pro (gemini-3-pro-image), which renders four 4K cover variations of a greeting card with the recipient's name as legible in-image typography. Inputs you receive in `contents`: - recipient_name_text_as_typed: the verbatim name the sender entered (may be Latin, Cyrillic, Devanagari, Bengali, Tamil, Arabic, Hebrew, Han, Hangul, Hiragana/Katakana, etc.) - recipient_one_liner_verbatim: a 1-280 character description the sender wrote about the recipient (their colours, their hobbies, the inside joke, the things on their counter) - occasion: one of birthday, anniversary, congratulations, sympathy, get_well, goodbye, save_the_date, thinking_of_you, custom; if custom, also custom_occasion_phrase - reference_count: integer 0-14 (the images themselves are passed in a separate Files API call; you do not see them here — but you know how many there are) - preferred_aesthetic_hint: an optional sender-supplied phrase ("warm and quiet" / "playful and bright" / "elegant and understated"); may be empty Hard rules: - Detect the script of recipient_name_text_as_typed and set script_detected on RecipientName. Default script_chosen to script_detected unless the sender's description explicitly contradicts (rare). - cover_text MUST be the exact glyphs to render on the cover. For birthday in English: "Happy Birthday, ". For other occasions, use the natural phrase for that occasion (do NOT invent flowery alternatives). For non-English / non-Latin scripts, render the occasion phrase in the language of the recipient's name script unless the sender's one-liner obviously implies English (e.g. an Indian-American friend whose description is in English). - Translate the occasion phrase faithfully for the four most common scripts: Latin English "Happy Birthday, ", Devanagari "जन्मदिन मुबारक, ", Cyrillic Russian "С Днём Рождения, ", Hangul Korean ", 생일 축하해", Hiragana/Katakana Japanese "お誕生日おめでとう、 ", Han Simplified ", 生日快乐", Arabic "عيد ميلاد سعيد، ", Hebrew ", יום הולדת שמח". These are common, non-trademarked phrases; render verbatim. - palette_named and palette_hex MUST agree (each named colour has a hex). 2-5 colours total. Pull from the description: "green ceramic mug" → ceramic green, "bridge at dawn" → warm dawn grey, etc. - mood_descriptors: 2-4 short adjectives that match the description's emotional register. NOT generic ("nice", "beautiful") — specific ("quiet", "early-morning", "celebratory but restrained"). - composition_hint: one sentence describing where the typography goes and where the imagery goes (e.g. "horizon line low, type top-third, primary motif bottom-third"). - subject_motifs: 2-5 concrete things to depict, drawn directly from the description. NEVER add motifs the description does not mention. - forbid_motifs: 3-6 items the cover should NOT include. Default to: candles, balloons, confetti, cake, gift boxes, party hats. (These are the generic "AI birthday card" motifs the sender is trying to avoid.) Add anything the description explicitly opposes ("hates pink" → add "pink"). - variation_concepts: EXACTLY 4 entries, one per label: - A_recommended: the strongest, most direct interpretation of the description - B_alt_mood: a different time of day, season, or foregrounded motif - C_typographic: the title carries more of the design; imagery is secondary or abstract - D_square: the Instagram-square crop and composition; motif placement adjusted for 1:1 - type_style_hint: a short phrase (e.g. "serif, warm, slightly worn"; "modern grotesque, generous tracking"; "elegant italic with restrained flourishes"). Tied to the mood. - refusal_check_required: ALWAYS true. This is a hard gate. - prompt_confidence reflects: clarity of description (clear vs vague), specificity of palette cues, presence of references. Below 0.6 → flag this in the variation_concepts rather than guessing. DO NOT include cover_text variations of "Happy 41st Birthday" or "Happy Birthday to the best friend ever" — the cover text is short and clean. Age is included only if the sender explicitly typed it (e.g. "forty-one tomorrow"). DO NOT translate, expand, or improve the recipient's one_liner_verbatim. You read it; you do not echo it. The one_liner is for the prompt's variation_concepts, not for the cover. Output ONLY the CoverPrompt JSON matching the schema. No commentary. JSON only. ``` --- ### Call: Trademark / likeness screen Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none) ``` You decide whether a card-generation request must be refused because it asks for content that infringes on a trademark, a copyrighted character, a brand logo, or a real person's likeness. Inputs you receive in `contents`: - recipient_name_text_as_typed - recipient_one_liner_verbatim - occasion - reference_image_inline_descriptions[]: short server-generated descriptions of each uploaded reference photo (e.g. "a green ceramic mug on a windowsill, soft morning light", "a wooden bookshelf with paperback novels", "a poster of the Star Wars film franchise on a wall"). The descriptions themselves were produced by an earlier multimodal call; you reason over the text descriptions here. You decide which of these categories the request falls into: - ok: nothing in the description or references infringes - real_person_likeness: the description or a reference photo centres on a recognisable public figure (musician, athlete, politician, actor, fictional well-known persona) - trademarked_character: the description or a reference asks for a recognised character (Mickey Mouse, Hello Kitty, Pikachu, the Pixar lamp, etc.) - brand_logo: the description or a reference is centred on a brand mark (Coca-Cola, Apple, Nike, the Mercedes star, etc.) - explicit_or_violent_content: anything sexual, gory, or hateful - reference_image_depicts_copyrighted_work: a reference photo is itself a copyrighted artwork (a Van Gogh print on a wall is fine; a clearly-captured copyrighted poster as the main subject is not) - other: any other clear infringement; explain in reason_one_line A recipient who happens to wear a Liverpool FC shirt in a reference photo is fine — the photo is of the recipient, the shirt is incidental, the cover will be about the recipient, not about the shirt. Use judgement. If a reference photo is OF the recipient themselves, that is fine. The recipient is a private friend, not a public figure being impersonated. Flag only if the recipient's name + description suggest the sender is impersonating a public figure with the same name (rare). When you refuse: - reason_one_line is the message the sender will read. Keep it short, friendly, non-judgemental. Example: "We can't put copyrighted characters on cards — but I'd love to do a friendly mouse with similar warmth. Want me to try that instead?" - alternative_suggestion is a single concrete pivot the sender can accept with one tap. Example: "a friendly cartoon mouse in soft pastels" instead of Mickey. - reference_indices_flagged lists the 0-based indices of any reference images that tripped the check; empty list if the text alone tripped it. If is_refused === false, reason_code MUST be "ok" and reason_one_line, alternative_suggestion, and reference_indices_flagged are empty/null. Output ONLY the RefusalDecision JSON. JSON only. ``` --- ### Call: Cover generation (Nano Banana Pro) Model: `gemini-3-pro-image` · n/a · n/a (image generation, no thinkingConfig) ``` You render ONE 4K cover image for a greeting card. The card's cover features: - The recipient's name, rendered as legible in-image typography in the specified script - The occasion phrase (e.g. "Happy Birthday") in the same script and language as the name, where the CoverPrompt specified one - The visual subject_motifs drawn from the sender's description and reference photos - The palette specified in palette_hex - The composition specified in composition_hint - The type style specified in type_style_hint Hard rules for the cover_text: - Render cover_text EXACTLY as specified. Do not paraphrase. Do not abbreviate. Do not add or remove glyphs. - The script of cover_text matches cover_text_script. - The cover_text occupies a clean area of the composition (per composition_hint); contrast against the background area MUST exceed 4.5:1 (estimated). - Typography is designed, not stock. The user expects a card that looks art-directed, not a font dropped onto a photograph. - ⚠️ Strict Token Preservation: when the recipient name contains non-Latin scripts (Devanagari, Malayalam, Hangul, Katakana, Hebrew, Cyrillic, Tamil, Telugu, Kannada, Sinhala, Gurmukhi, Gujarati, Arabic, Han, Thai, Ge'ez, Tifinagh, Greek, Armenian, Georgian, etc.), preserve the EXACT Unicode codepoints from text_as_typed. Do NOT transliterate. Do NOT substitute visually similar glyphs from another script. If any character is ambiguous or you cannot render it faithfully, return `confidence: 'low'` + `flagged_for_user_review: true` rather than guessing an alternative. Hard rules for the imagery: - Photographic or painterly, depending on type_style_hint. Never cartoon-style unless the prompt explicitly says "cartoon" or "illustrated". - No people in frame unless the sender's description explicitly names a person (and even then, render a silhouette or anonymous figure — never a portrait of a real person). - No commercial branding, no brand logos, no trademarked characters. - No candles, balloons, confetti, cake, gift boxes, or party hats UNLESS the description specifically asks for one of them. These are the "generic AI birthday card" tells the sender is trying to avoid. - Subject motifs drawn from subject_motifs only. Do not invent motifs. - Reference images (passed as Files API URIs in the request) are a style guide: pull palette, texture, mood, and composition cues. Never copy a reference image's exact composition; the cover is a new image inspired by the set. Hard rules for variation diversity: - The four variations differ meaningfully. The variation_label in the CoverPrompt specifies which label this call is for: A_recommended (strongest direct interpretation), B_alt_mood (different time of day / season / foreground), C_typographic (title carries more of the design), D_square (1080x1080 composition for Instagram). - For D_square, the aspect is 1:1; for A, B, C, the aspect is 5:7 portrait at 2400x3360 (4K-equivalent on the long edge, 300 DPI at 8 inches print). Negative constraints: - No watermarks. No app branding. No "designed with X" footers. - No misspelled names. Render cover_text glyph-for-glyph as specified. - No reproduction of trademarked characters, brand marks, or recognisable public figures. (The trademark screen runs first as a hard gate; if you receive a request here, the screen has already cleared it — but still: no.) - No text other than cover_text. The interior message is rendered separately by the PDF builder; it never appears on the cover. Output: a single PNG/JPEG 4K image. The caller will read it back, run the legibility verifier, and decide whether to re-render. ``` --- ### Call: Cover-text legibility verifier Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none) ``` You verify that a Nano Banana Pro render placed the recipient's name on the cover legibly and spelled it correctly. Inputs you receive in `contents`: - the rendered cover image (passed via Files API `files/*` resource name via `fileData.fileUri`, or inlineData) - intended_cover_text: the exact string the cover was supposed to render - intended_cover_text_script - the variation_label Read the cover image. Find the rendered text. Compare it to intended_cover_text. Hard rules: - name_rendered_verbatim: the literal glyphs you see on the cover. Read carefully — Bengali conjuncts, Tamil ligatures, Arabic letter-position forms, Hangul syllable blocks must match. - name_spelled_correctly: true only if name_rendered_verbatim matches intended_cover_text exactly. Whitespace differences do not matter; glyph differences do. - contrast_ratio_estimate: estimate the contrast between the rendered text and the area immediately behind it. Below 4.5 fails. - contrast_pass: true if contrast_ratio_estimate >= 4.5. - is_legible: true only if name_spelled_correctly AND contrast_pass AND the text is clearly readable at thumbnail size. - re_render_recommended: true if is_legible is false. - reason_one_line: a short note the server logs and (if re_render_recommended) the prompt-tightening call reads. Examples: "name rendered correctly but contrast against the dawn-grey sky is borderline (estimated 3.8)", "Bengali conjunct ক্ষ rendered as ক্স — re-render with the script explicitly named". Output ONLY the LegibilityReport JSON. JSON only. ``` --- ### Call: Targeted region re-render (annotate-and-fix) Model: `gemini-3-pro-image` · n/a · n/a ``` You re-render a single region of an existing cover. The user has circled a part of the previous cover and described what they want changed. Inputs you receive in `contents`: - the previous cover image (Files API `files/*` resource name via `fileData.fileUri`) - the region mask (a black-and-white image where white marks the region to re-render) - the change description (e.g. "more green in the title", "remove the second mug", "warmer light on the horizon") - the original CoverPrompt for context (so palette and composition stay consistent) Hard rules: - Re-render ONLY the masked region. The rest of the image must remain pixel-identical to the previous cover. - Honour palette_hex from the CoverPrompt — do not introduce colours outside the agreed palette unless the change description explicitly asks for a new colour. - If the masked region overlaps cover_text, preserve the text exactly as it was. Do NOT re-render the typography unless the change description explicitly addresses it. - Do not introduce motifs from the forbid_motifs list. - Honour the negative constraints from the base cover call. Output: the modified PNG/JPEG at the same resolution and aspect as the previous cover. ``` --- ### Call: Voiceover TTS (sender-typed message read aloud) Model: `gemini-3.1-flash-tts-preview` · n/a · n/a ``` You read the sender's verbatim interior message aloud as an optional voiceover greeting for the card. This call ONLY fires when the sender has explicitly opted in ("read this aloud for me") AND has NOT recorded their own voice. If the sender recorded their own voice, that audio is used directly — this call does not fire. Pick the voice whose languageCode matches the InteriorMessage.language. Default voice character: warm, conversational, unhurried. The directive sentence prepended to the text is the only style control Gemini 3.1 Flash TTS exposes — there is no separate `style` API field. Pre-process the verbatim text before sending to TTS: - At sentence boundaries, insert an ellipsis ("…") for a natural pause. Do NOT alter the text otherwise. - At paragraph boundaries (blank lines), insert a blank line plus an em-dash ("—"). Gemini 3.1 Flash TTS does not support SSML — these textual cues are how pace is conveyed. - Do NOT add words. Do NOT rephrase. Do NOT correct typos. The verbatim text is the source of truth. Style direction: prepend ONE short directive sentence to the text input, exactly like: "Read this aloud as if reading a warm greeting card to a friend. …". Phoneme overrides are NOT exposed by Gemini 3.1 Flash TTS — no SSML tag. Pronunciation comes from the chosen voice's native locale. If the recipient's name is in a script the chosen voice does not natively speak, the model will read it phonetically; flag this in the UI so the sender can re-record themselves if it matters. Output: the audio bytes. ``` --- ### Call: Animated cover (Veo 3.1, 4-second subtle loop) Model: `veo-3.1` · n/a · n/a ``` You generate a 4-second seamless loop of motion over a card cover image. The loop is intended to play silently in a recipient-facing web preview; it is NOT intended to replace the static print cover. Inputs you receive in `contents`: - the static cover image (Files API `files/*` resource name via `fileData.fileUri`) from variation A - the original CoverPrompt for motif context Motion you may add: - ambient air motion (a leaf drifting, hair lifting at the edge of a silhouette, dust in a sunbeam) - water motion (a slow ripple at the edge of a pond, river, or coffee mug — subtle) - light motion (a soft pulse of dawn light, a candle flicker ONLY if the composition has a candle, a slow sun-warm glow) - steam motion (rising from a coffee mug, a kettle, a bowl of soup) — slow, lazy, no acceleration Motion you may NOT add: - camera motion. The frame does not pan, zoom, dolly, or rotate. The cover is a card, not a film clip. - motion through the recipient-name region of the composition. The typography stays pixel-locked. - motion through the occasion phrase, if rendered. - motion that distorts the typography in any way. - people moving. Silhouettes are okay; faces are not. - text appearing or disappearing. The loop is seamless: frame 0 and frame 96 (at 24 fps × 4 s) are identical. Plan motion that returns to the start. Output: a 4-second MP4 loop at the same aspect as the cover. ``` --- ### Call: Empty-state / how-it-works hero illustration Model: `gemini-3.1-flash-image` · n/a · n/a ``` You generate a single image for the marketing landing or empty-state of the Birthday Card Designer app. The image is photographic-looking and depicts a real-world card-sending scene: an envelope on a wooden table, a card half-pulled out of the envelope, warm afternoon light from a window, a ceramic mug beside it; or a print-quality 5×7 card freshly out of a desktop printer, still slightly curled, on a clean work surface. Hard rules: - Photographic, not flat illustration, not cartoon. - No people in frame. - No commercial branding, no brand logos. - No legible text on the rendered card in the scene — the card-within-the-scene is suggested, not detailed (this image is decorative, not a sample card). - Warm lighting, real-paper texture, slight imperfection. Avoid the glossy AI-render look. - Aspect ratios: 3:2 for hero, 1:1 for empty states. Prompt anchors that work well: - "warm afternoon light through a kitchen window, a 5x7 card with a softly painted cover on a worn wooden table, a ceramic mug just out of focus behind it, a single envelope leaning against the mug" - "a hand reaching to pull a card out of a cream-coloured envelope, soft natural light, no faces, focus on the paper texture" - "a desktop printer in a home office at golden hour, a 5x7 card freshly printed and still slightly curled on the output tray, warm overhead lamp light" ``` ## 5. Use cases & content to include Build dedicated UI flows / content blocks for each — they tell you what content the app must support. - **A friend almost forgot a birthday.** It's 11:47 pm, the birthday is tomorrow, the recipient is Priya. The sender types the description: "Priya, forty-one tomorrow, runs every morning before sunrise, drinks black coffee in a green ceramic mug, the kind of person who reads three books a month and remembers every one." Three reference photos go in: her bookshelf, her green mug on a windowsill, the dawn bridge from her last Instagram post. Twenty-eight seconds later, four variations. The sender picks Variation A, taps print, and the 5×7 PDF lands in the printer tray. Done by midnight. - **A partner the morning of an anniversary.** No reference photos. Just a description: "Mateo, ten years together, he laughs the loudest in the room, the colour blue of the Pacific at noon, the guitar he never quite learned but keeps in the corner anyway." Occasion picker: Anniversary. The cover comes back with a Pacific-blue palette, a soft silhouette of a guitar in the bottom-right, the title "Happy Anniversary, Mateo" set in a warm serif. The interior message is the partner's verbatim letter, three paragraphs, in their handwriting-styled body face. - **A parent the morning of a kid's eighth birthday.** Description: "Hari, turning eight tomorrow, completely obsessed with dinosaurs, draws raptors all over his maths notebook, wears the red rain boots even when it's sunny." Reference photo: a page of his notebook with raptor drawings. The cover is playful but not cartoonish — a hand-drawn-looking raptor in red, "Happy 8th Birthday, Hari" in a friendly sans serif. Print PDF goes on the fridge before he wakes up. - **The office goodbye card the whole team will sign.** Description: "Jenna, leaving the team after six years, designer's eye for pale-pink and forest-green, the one who always made the office playlist." Occasion: Goodbye. No reference photos needed. The cover comes back in Jenna's palette — pale pink and forest green — with subtle musical-note flourishes in the type. The card prints at 5×7, gets passed around for signatures, and arrives at her desk on her last Friday. - **A card whose recipient name is in a non-Latin script.** The sender types the description in English and picks the script the recipient actually reads — Malayalam, Hebrew, Cyrillic, Arabic, Devanagari, kana. Nano Banana Pro renders the recipient name legibly in the chosen script at 4K (e.g. "ജന്മദിനാശംസകൾ" in a warm serif Malayalam face), with the interior message kept in the sender's language verbatim. The print PDF can be emailed to a local print shop near the recipient so the physical card arrives in their hand on the day. - **A wedding save-the-date.** A couple uploads ten reference photos — their flat, the venue, the dahlia colours from the florist's portfolio, the mountain view from the engagement spot. Nano Banana Pro reads all ten as a style guide and emits four cover variations with the couple's names — "Sara & Tomas" — in a typographic-led design with the mountain silhouette in soft warm tones. The print PDF and the Instagram-square version both come back; the couple uses the print for mailed cards and the square for the Instagram announcement. - **A sympathy card, no clichés.** The occasion picker switches the whole register. Description: a recent loss, the person who is grieving, one or two grounding details (a colour they love, a place that meant something). The cover comes back quietly — a restrained palette, a soft horizon or a single object, no flowers, no platitudes. The interior message is the sender's verbatim letter, never paraphrased, never "improved". Available in every supported script for the recipient name. - **A bring-your-own-key power user.** A designer who sends ten birthday cards a month plugs in her own Gemini API key in Settings. The free-tier limit lifts; she can render unlimited cards on her own GCP spend (~$0.45 per card at current Nano Banana Pro pricing). Her library carries every card she's ever sent. - **The trademark-screen refusal.** A sender types: "for my nephew, he loves Hello Kitty, make a Hello Kitty card." The trademark screen returns `is_refused: true` with a one-line: "We can't put copyrighted characters on cards — but I'd love to do a friendly pink cat with similar warmth. Want me to try that instead?" The sender taps the alternative and the flow continues with the non-infringing pivot. - **The annotate-and-fix moment.** Variation A comes back nearly perfect but the typography is slightly too pink against the warm-grey sky. The user circles the title and types "make it the green of the mug instead". The targeted-region re-render fires; the rest of the cover stays pixel-identical; nine seconds later, the title is in the ceramic green. The user taps download. - **The Instagram-square variation as a story background.** A user sends the 5×7 print PDF in the mail but also pushes Variation D — the Instagram-square — to their phone, and the recipient gets a story tag the next morning. ## 6. Page structure Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent. 1. **Marketing landing (anonymous-friendly).** Above-the-fold hero showing a print-ready 5×7 card half-pulled from a cream envelope on a warm wooden table, with the demo card's title ("Happy Birthday, Priya") clearly legible. One paragraph: "Type a name. Describe them in one line. Optionally drop three reference photos. Get back a print-ready 5×7 card whose cover reads as made-for-them — not picked off a shelf — in ninety seconds." Primary CTA: "Make a card now". Secondary: "See how it works" → scrolls to a 30-second narrated demo strip. 2. **The 30-second demo strip.** Three frames showing the Priya example: (1) the typed description and three reference photos, (2) the four-variation grid coming in, (3) the print PDF downloading. No video required — three carefully designed still frames tell the story. 3. **Sign-in / start.** Single Google sign-in button; Apple sign-in next to it. Below: "Try the sample card first" → loads the Priya demo with reference photos preloaded. 4. **The authoring screen (the heart of the app).** One screen, five fields: - Recipient name (with a small script auto-detect indicator) - One-line description (280 chars, live count, placeholder rotates through real examples) - Reference photos dropzone (0-14, drag/paste/camera/upload) - Occasion picker (Birthday default; full list in dropdown) - Interior message (600 chars, the field the model NEVER paraphrases — labelled "Your message, exactly as you type it") Output-mix selector below: Print PDF (default on), Instagram square (default on), Animated (off by default), Voiceover (off by default). Primary CTA: "Generate four covers" → fires the structuring call, trademark screen, then the four parallel Nano Banana Pro calls. Progress UI streams meaningful step labels. 5. **The four-variation grid.** A 2×2 grid (or 1×4 on narrow screens) showing variations A, B, C, D as they arrive. Each tile shows the rendered 4K cover at thumbnail, the variation label, and a small "regenerate just this one" control. Tap a tile → variation detail view. 6. **Variation detail view.** Full-bleed 4K render, the cover above the interior layout side-by-side. Download Print PDF / Download Instagram Square / Download Animated MP4 / Record Voiceover. Annotate-and-fix mode: tap "edit this cover" → a masking tool overlays the image, the user circles a region and types the fix, the targeted-region re-render fires. 7. **Send screen.** Once the user picks a variation, the send screen offers: Download (just for me), Email (magic link with PDF attached + URL), SMS (short URL only), Print at a local shop (downloads the CMYK print PDF with a tooltip naming three common print services). Share URL is per-card, revocable, and expires after 90 days unless extended. 8. **Recipient-facing card view.** The URL the recipient opens. The cover image fills the viewport. Below: the user's verbatim interior message in a clean body face. If a voiceover exists, a play button. If an animated cover exists, the animation plays once on first view, then loops on demand. No app branding visible to the recipient. 9. **Card library.** Sidebar list of every card the user has ever made: recipient name, occasion, date sent, small thumbnail. Tap a card → variation detail view; re-print, re-send, or copy to a new card. 10. **Settings & privacy.** Bring-your-own-key field (with a "where to get a Gemini API key" link), default-output preferences (always print on, animated off, etc.), language preferences, "Delete my account" with a 60-second cool-off and a list of what gets deleted (cards, references already purged at 30 days, voiceovers). Privacy panel restates the not-trained-on policy in plain English. 11. **Footer.** "Made for the person who almost forgot." Privacy: "Your messages, your references, your recipient names 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 Birthday Card Designer." - Subhead: "Type a name, drop three photos, get a card that looks made-for-them — print-ready in ninety seconds." - One paragraph (≤ 60 words): the difference from e-cards and Etsy and Canva is that this app reads a one-line description and a handful of reference photos through Nano Banana Pro — the post-I/O 2026 image model that finally renders legible typography at 4K — and returns four card variations with the recipient's actual name as designed type, the user's verbatim interior message unchanged, and a print-ready 5×7 PDF in the same call. - Visual: a small annotated illustration showing the structure of a card (cover with name in legible type, interior with verbatim message, the 5×7 trim with bleed and crop marks) — not a generic envelope icon. **Slide 2 — Try it now.** - One short prompt: "Try the sample card". - A live demo input pre-loaded with the Priya example from the seed content in section 8a. - 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the structuring of the description, the four parallel Nano Banana Pro calls, the legibility verifier, the print-ready PDF assembly). **Slide 3 — How to remix this.** - Headline: "Make this yours." - Three short bullets: - "Swap the demo seed in `/data/seed-card/` for your own starter content." - "Adjust the prompts in `/server/prompts/` to change the occasion catalogue or the script support." - "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):** - **Nano Banana Pro — `gemini-3-pro-image`** — the post-I/O 2026 image model that renders 4K covers with legible in-image typography, accepts up to 14 reference photos as a style guide, and produces production-quality designed type. This is the capability that makes the cards look art-directed rather than templated. Released GA on 2026-05-28. - **Gemini 3.5 Flash** — the default text model for description structuring, trademark screening, and legibility verification. Outperforms last-gen Pro on most benchmarks; thinkingLevel `low` for routine calls, `medium` for the trademark screen. - **Gemini 3.1 Flash TTS** — optional voiceover for senders who want their verbatim message read aloud rather than recording themselves. Expressive, multilingual, no SSML. - **Veo 3.1** — optional 4-second subtle motion loop on the cover. Used only for the recipient-facing web view, never for print. *(Gemini Omni Flash is the announced replacement at I/O 2026; developer API expected in the coming weeks. We will swap to Omni Flash when the API ships.)* - **Nano Banana 2 — `gemini-3.1-flash-image`** — the cheaper image model used for marketing illustrations and empty-state visuals, not for the card covers themselves (those go through Nano Banana Pro for the typography legibility). - **Structured output (Zod → Gemini Schema)** — every text call emits typed JSON matching the schemas in `/server/schemas/`. The CoverPrompt is the contract that turns a free-text description into a four-variation art direction. - **Firebase Auth** — Google and Apple sign-in. Recipient-facing card view requires no sign-in. - **Firestore** — stores cards, references, recipients, share links, voiceovers. Real-time sync across devices. - **Firebase Storage** — keeps reference photos for 30 days (auto-purged), generated covers indefinitely, voiceovers indefinitely. - **Cloud Run** — every Gemini call happens server-side. Auto-provisioned, first 2 apps free post-I/O 2026. - **Cost note** — see the detailed breakdown in 6d. A single card with four 4K Nano Banana Pro variations + legibility checks + print PDF costs about $0.42 of Gemini API spend. Free tier: three cards per month. - **Privacy note** — your interior messages, recipient names, and reference photos are private to you. This app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. Reference photos auto-purge after 30 days. **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 built in v1; bring-your-own-key recommended for power users in the meantime) - External APIs: Gemini API only **Environment variables you'll need to configure:** - `GEMINI_API_KEY` — your Google AI Studio API key - `FIREBASE_PROJECT_ID` — your Firebase project id - `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only) - `FIREBASE_STORAGE_BUCKET` — your Firebase Storage bucket - `STRIPE_SECRET_KEY` — optional, only if you wire up the paid tier - `SENDGRID_API_KEY` (or equivalent) — for transactional email of card share links **Cost + privacy notes:** - One short paragraph on cost: Nano Banana Pro at $2/M input, $12/M output, and a 4K cover is approximately ~$0.10 per render (Google has not pinned an exact public per-image figure; verify before shipping). Four covers per card plus the text-side calls = ~$0.42 per card on the working estimate. Most senders make 1-3 cards per month; the free tier covers the typical case. - One short paragraph on privacy: where the data lives (your Firebase project), how long references are kept (30 days max), how to delete a card (Settings → Card library → swipe to delete), what is never sent for training. Bring-your-own-key is the strongest privacy mode — your card content goes through your Google Cloud account directly. **Documentation links:** - AI Studio Build docs (post-I/O 2026 surface upgrade page) - Nano Banana Pro docs (multi-reference, 4K typography, video-to-image) - Gemini 3.5 Flash docs - Veo 3.1 docs (and the Omni Flash announcement) - Firebase Auth / Firestore / Firebase Storage docs **Accessibility:** same standards as the onboarding modal — focus trap, `Esc`, ARIA, restored focus. **Behaviour:** - Always available — single click from anywhere in the app. - Tooltip on the `(i)` icon: "How this app is built". - Mobile: opens as a full-screen sheet that slides up. - Should be the most honest part of the app — never hand-wave service requirements; never say "AI" without naming the specific Gemini model and capability. ## 6d. Detailed cost breakdown (deployer reads this BEFORE shipping) - **Description → CoverPrompt (Gemini 3.5 Flash, low thinking)** — ~1,500 input tokens (system instruction + user description) and ~800 output tokens (the CoverPrompt JSON). At Gemini 3.5 Flash pricing ($1.50/M input, $9/M output) that is ~$0.0095 per card. - **Trademark / likeness screen (Gemini 3.5 Flash, medium thinking)** — ~2,500 input tokens (system instruction + user inputs + reference descriptions) and ~300 output tokens (RefusalDecision JSON). With medium thinking the effective cost is roughly ~$0.012 per card. - **Cover generation (Nano Banana Pro, 4 variations)** — at ~$2/M input and ~$12/M output, a 4K cover averages ~$0.10 per render. Four variations per card = ~$0.40. *This is the dominant cost line; everything else is a rounding error.* - **Cover-text legibility verifier (Gemini 3.5 Flash, low thinking, multimodal)** — ~1,200 input tokens (system instruction + image inline thumbnail) and ~200 output tokens. ~$0.0023 per variation; 4 variations = ~$0.0092. - **Targeted region re-render (Nano Banana Pro)** — only fires when the user uses annotate-and-fix; ~$0.10 per re-render. Most cards don't trigger this. - **Voiceover TTS (Gemini 3.1 Flash TTS)** — approximate ~$10/M output tokens ≈ ~$0.000003/character (the exact TTS character-token price was not pinned at I/O 2026; treat as a working estimate and verify before shipping). A typical 200-character interior message read aloud is ~$0.001 on the working estimate. Optional, often skipped. - **Animated cover (Veo 3.1)** — approximate ~$0.15 per 4-second clip (verify Veo pricing on the current Gemini API pricing page before shipping). Optional; off by default. - **Hero / empty-state illustration (Nano Banana 2)** — approximate ~$0.03/image (verify before shipping). Generated once per app install (cached client-side). - **Expected per-card cost:** ~$0.42 for the default flow (text + 4 covers + legibility checks + print PDF). With animation + voiceover: ~$0.57. With one annotate-and-fix retry: ~$0.52. - **Free-tier monthly cost per active user (3 cards/month):** ~$1.26. Free-tier ceiling makes sense at 3 cards. - **Bring-your-own-key flow:** the user's Google Cloud account absorbs all Gemini spend; the app's only marginal cost per byok-user is Firebase Storage for references (~$0.001/month per active user) and Firestore reads (~$0.0001/card). - **Reference photo storage:** Firebase Storage standard tier ~$0.026/GB/month. A typical 3-reference card is ~6 MB at ingest; compressed to ~1.5 MB for the Files API. After 30 days the references auto-purge. - **Generated cover storage:** four 4K covers ≈ 20 MB per card. Kept indefinitely. At 100 cards in the library that's 2 GB ≈ ~$0.052/month per power user. ## 7. Design language - **Mood:** the moment a card is half-pulled from its envelope. A paper-and-ink object on a warm wooden table at golden hour. Not a Canva-ish marketing tool. Not a SaaS dashboard. Not festive confetti and balloons. The aesthetic of a well-designed letterpress card you'd buy at a stationer's, scaled into a product you can use in ninety seconds. Generous whitespace, real paper texture, restrained typography, warm light. - **Typography:** A modern grotesque for app chrome and form labels (Inter or Geist). A warm display serif for headlines and for the in-product preview of the card title (Source Serif Pro, Fraunces, or GT Sectra). A monospaced face for the verbatim message field — to mark that what you type is what gets printed, unchanged. - **Palette:** Warm paper background `#F6F1E8` for the canvas, deep ink `#1A1614` for body text, envelope-cream `#EDE3D2` for card surfaces in the UI, postcard-blue `#3E6F8A` for primary actions, golden-hour amber `#C58A3A` for the legibility-pass badge, soft ribbon-red `#A8362C` for refusal banners and for the print-PDF crop-mark colour, ceramic-green `#7A9F7A` accent used sparingly. Borrowed from a stationer's drawer, not a design system. - **Imagery:** Photographic. Envelopes on tables. Cards on printer trays. Wax seals on paper. No flat illustrations of abstract gifts. No emoji. Generated via Nano Banana 2 for marketing surfaces; Nano Banana Pro for the actual cards. - **Hand-feel touches:** When a new card variation arrives, it slides into the grid with a paper-shadow lift, as if a card were being laid down on a desk. The cover-text legibility-pass badge appears next to each variation in golden amber; failures appear in ribbon red with a one-line reason. The interior message field has a real-paper texture in the background, so the user feels they are writing on paper, not in an input box. - **Spacing:** consistent 4-px base. Generous whitespace — the variation grid and the authoring form need air. - **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Form fields use 6; card-variation tiles use 12; the welcome card uses 20. - **Shadows:** subtle, layered, warm-tinted. Avoid heavy drop-shadows. The card-variation tiles use a soft warm shadow that suggests a sheet of paper lying on a desk. - **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. The variation- arrives "lay down on the desk" animation is the one place where motion carries meaning; respect reduced-motion by appearing instantly. No bouncing splash animations. No theatrical hero animations. - **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons that match the eventual layout. Empty states have helpful next-action guidance ("Type a name to start your first card"). ## 8. Content generation rules - Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'. - Invent plausible recipient names, descriptions, interior messages, and palette names that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on the specific — a green ceramic mug, not "a favourite mug"; the bridge she runs across at dawn, not "her morning routine". - Tone: warm, direct, free of corporate language. This is an app for someone making a thing for someone they love, not for a brand-engagement campaign. - 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 user already speaks it (the print user wants to see "5×7", "bleed", "crop marks", "CMYK" because those words mean exactly what they need to mean). - Where the app outputs AI-generated content, never label it as "AI says" — let the variations speak. Use small uncertainty cues only where epistemic honesty requires them (a legibility check that fails surfaces a one-line reason in muted text; a re-render fires automatically, the user sees the resolution, not the problem). ## 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 demo card (this is what the demo should show):** - **Recipient name:** Priya - **Recipient name script:** Latin - **Occasion:** Birthday - **One-line description (verbatim):** "Priya, forty-one tomorrow, runs every morning before sunrise, drinks black coffee in a green ceramic mug, the kind of person who reads three books a month and remembers every one" - **Reference photos (3):** her bookshelf (a wooden shelf with paperback novels and a few hardcovers, soft afternoon light); the green ceramic mug on a windowsill (steam rising, dawn light, the colour `#7A9F7A`); the bridge she runs across at sunrise (a wide stone bridge, dawn fog rising off the river, long shadows) - **Interior message (verbatim, will be rendered EXACTLY as typed):** "Happy birthday from the Berlin crew, Priya. Save us a chapter of whatever you're reading next week — we want to hear about it over dinner. With love, Tomas, Lena, Sam, and Anika." - **Output mix selected:** Print PDF (on), Instagram square (on), Animated (off), Voiceover (off) - **CoverPrompt produced (excerpt):** - palette_named: ["warm dawn grey", "ceramic green", "soft paperback cream"] - palette_hex: ["#A7A096", "#7A9F7A", "#EFE6D4"] - mood_descriptors: ["quiet", "early-morning", "warm", "patient"] - composition_hint: "horizon line low; type top-third; bridge silhouette as horizon" - type_style_hint: "warm display serif with restrained flourishes, slight worn quality" - subject_motifs: ["green ceramic mug with steam", "stone bridge silhouette at dawn", "a small stack of paperback books"] - forbid_motifs: ["candles", "balloons", "confetti", "cake", "gift boxes", "party hats", "running shoes"] - cover_text: "Happy Birthday, Priya" - cover_text_script: latin - **Four variation labels:** - A_recommended: dawn-grey cover with the bridge silhouette at the horizon, the ceramic-green mug in the bottom-left, "Happy Birthday, Priya" in the warm display serif at the top-third - B_alt_mood: dusk version — the same scene at golden hour with deeper amber light and a more saturated green on the mug - C_typographic: the title fills two-thirds of the cover in a layered serif; the bridge and mug are line-drawn in soft grey - D_square: Instagram-square crop foregrounding the mug, the bridge fades into the back, the title sits along the right edge **Sample voice memos with multilingual scripts:** - Maricel (Filipino-American, San Francisco) typing for her Ammachi in Kerala: name "അമ്മച്ചി" (Malayalam), description "Ammachi, eighty-one, the smell of curry leaves and coconut oil, the saree colour she always wears for festivals — that deep red, the temple bell outside her window", interior message in English. - Alina (Russian-Canadian, Toronto) typing for a sister in Moscow: name "Алина" (Cyrillic), description "Алина, thirty-five yesterday, paints in the kitchen at 6am, three cats, the colour of the Neva in October", interior message in Russian. - Tomas (Czech-Spanish, Madrid) typing for a brother in Prague: name "Tomáš" (Latin with diacritics), description "Tomáš, fifty, climbs every Saturday in the Český ráj rocks, beer in pint glasses, the dark green of a fir forest", interior message in Czech. - Haruki (Japanese-American, Seattle) typing for a friend in Osaka: name "ハルキ" (Katakana), description "Haruki, forty next week, jazz vinyl on a turntable in the corner, indigo shibori, the lantern light in the alleys of Namba", interior message in Japanese. - Noa (Israeli, Tel Aviv) typing a sympathy card for a friend whose father died last week: name "נועה" (Hebrew), description "Noa, lost her father last week, he was a fisherman in Akko all his life, she is the gentlest person I know", occasion sympathy, interior message in Hebrew. **Sample input artefacts (for the build to demonstrate):** - A reference photograph of a wooden bookshelf with paperback novels, soft afternoon light, taken on an iPhone. - A reference photograph of a green ceramic mug on a windowsill with steam rising, dawn light. - A reference photograph of a stone bridge at dawn, fog rising off the river. - A finished 5×7 print PDF showing the four-variation A cover, bleed and crop marks visible, ready to drop on a desktop printer. - A finished Instagram-square 1080×1080 PNG showing the variation D crop. - A 4-second silent MP4 loop of variation A with subtle steam rising from the mug — for the recipient-facing web view. **Sample voice copy:** - Onboarding: "Type a name. Describe them in one line. We'll do the rest." - Empty authoring screen: "Who is this card for? Start by typing their name." - Processing (streamed step labels): "Reading your description…" / "Checking your references…" / "Sketching four cover variations…" / "Checking the name is rendered correctly…" / "Building your print-ready PDF…" - Save confirmation: "Card for Priya saved — print PDF and Instagram square ready to download." - Legibility-pass badge: "Name rendered cleanly. Contrast passes." - Legibility re-render note: "We caught a typography hiccup on variation C — re-rendered once with a tighter prompt." - Trademark refusal: "We can't put copyrighted characters on cards — but I'd love to do a friendly pink cat with similar warmth. Want me to try that instead?" - Free-tier ceiling: "You've made three cards this month — that's the free-tier limit. Add your own Gemini API key to keep going (it's free for you; you'll pay Google directly for the API spend, roughly 42 cents a card)." - Interior message label: "Your message — exactly as you type it. We won't change a word." - Recipient-facing card view: "Priya — open this on your birthday." - Share email subject: "Happy birthday from the Berlin crew." - Share email body (auto): "Open this card to see Priya's birthday card from Tomas, Lena, Sam, and Anika. [Open card]" ## 9. Media & assets - **Hero image (landing screen):** A photographed-looking shot of a 5×7 card half-pulled from a cream envelope on a warm wooden table, the cover legibly reading "Happy Birthday, Priya" in a warm display serif against a dawn-grey palette, a green ceramic mug just out of focus behind it. Generate via Nano Banana 2 for the marketing surface (this is a decorative image, not a real card render). Use Nano Banana Pro only for the actual card generation. The hero prompt: "warm afternoon light through a kitchen window, a 5x7 card with a softly painted cover on a worn wooden table, the cover reads 'Happy Birthday, Priya' in a warm display serif at the top, a horizon-line bridge silhouette below, a green ceramic mug just out of focus behind it, a single cream envelope leaning against the mug, real paper texture, soft shadow under the card". - **App icon / wordmark:** Set in the display serif. Slight worn-paper texture behind it. No envelope icon — just type. - **Empty-state illustration:** A simple drawing of a single blank card on a desk with a pen beside it. Hand-drawn aesthetic, not a flat icon. Generate once at build time via Nano Banana 2 (`gemini-3.1-flash-image`), 1:1 WebP at 1024×1024, prompt: "single hand-drawn ink line illustration of a blank rectangular greeting card lying on a wooden desk surface with a ballpoint pen resting diagonally beside it, slight perspective, off-white paper background, slight pen imperfection, no shading, no colour fill, no text, no commercial branding". Ship as a seed asset at `/public/samples/empty-state-blank-card.webp`. - **Demo cards:** Generated per the prompts in section 8a — Nano Banana Pro renders the four variations of the Priya demo at 4K, the build caches them client-side so the demo is fast on repeat visits. Each demo card should look art-directed, not rendered. - **Print-PDF preview overlays:** Vector overlays showing the 5×7 trim, the 0.125" bleed, and the crop marks for users who want to verify the print spec before downloading. - **Stock fallbacks:** If image generation fails, fall back to the pre-rendered demo cards from `/public/samples/`. Never to a "🎂" emoji. - **Generated imagery:** Nano Banana Pro for card covers (4K, legible typography, 14-reference style guide). Nano Banana 2 for marketing and empty-state visuals (cheaper, no in-image text required). Veo 3.1 for the optional animated cover loop. - **Optimisation:** WebP/AVIF for the recipient-facing card view; the 4K master PNG/JPEG stays in Firebase Storage for download. `loading="lazy"`, explicit `width`/`height` to prevent layout shift. - **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only. ## 10. Interactivity & states - Every interactive element has hover, focus, active, and disabled states. - Forms validate inline and show specific error messages (not "Invalid input"). "Recipient name looks empty — at least one character is needed for the cover typography" is the right shape. - Loading states use skeletons that match the eventual layout, not spinners. The four-variation grid shows four skeleton tiles while the parallel Nano Banana Pro calls run; each tile flips to the rendered cover as it arrives. - Empty states explain the next action with a button whose label fits THIS app's domain: "Make your first card", "Add a reference photo", "Write your message" — never a generic "Add your first item". - Smooth scroll for in-page anchors. - All AI-generated text content streams in token-by-token where supported, with a clear meaningful step label before content starts arriving (not "thinking…", but "Reading your reference photos…"). - If a Gemini call fails, show a calm, specific error ("We couldn't read reference photo #2 — it was too large. Try one under 7 MB, or skip it.") and offer retry. - Cover-text legibility check failures surface inline below the affected variation tile, with a one-line reason and a "re- render fired automatically — back in 8 seconds" note. - Trademark refusals surface as a calm inline banner above the generate button, with a clearly tappable alternative suggestion. - The variation-grid arrival animation is a 300 ms "lay-down" motion; respects `prefers-reduced-motion` by appearing instantly. - The print-PDF download confirms the spec inline ("5×7 trim, 0.125" bleed, crop marks, CMYK, 300 DPI") so the user knows what they got. - Free-tier exhaustion surfaces as a calm inline notice on the generate button, not a blocking modal — the user sees the ceiling, taps once to add their key, and continues. ## 11. Tech & responsive requirements - **Stack:** React + TypeScript + Tailwind CSS. Functional components + hooks. Use Shadcn UI primitives where appropriate. Image rendering via plain `` with explicit width/height. - **Build runtime:** AI Studio Build — full-stack with Cloud Run server-side functions (auto-provisioned, first 2 apps free post-I/O 2026). All Gemini API calls happen server-side; API key lives in Secrets Manager, never in client bundle. - **Model selection:** explicitly pin `gemini-3-pro-image` for cover generation; `gemini-3.5-flash` for description- structuring, trademark-screen, and legibility-verifier; `gemini-3.1-flash-tts-preview` for optional voiceover; `veo-3.1` for optional animated cover; `gemini-3.1-flash-image` for marketing and empty-state visuals. Set `thinkingLevel` explicitly per call; OMIT `thinkingConfig` entirely on image, video, and TTS calls. - **Mutual exclusion:** no call in this template combines `responseSchema` with `google_search` — there are no grounded search calls. The structured-output schemas are wired via the `responseSchema` field for every text call. - **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed demo card on first launch. - **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as fallback for recipient share links. - **Storage:** Firebase Storage for reference photos (30-day auto-purge), generated covers (kept indefinitely), voiceovers, print PDFs. Pre-signed URLs only. - **Files API for reference photos:** every reference photo is uploaded to Firebase Storage, then server-side re-uploaded to the Gemini Developer API Files API; the resulting `files/*` resource name (e.g. `files/abc123xyz`) is passed via `fileData: { fileUri, mimeType }` to Nano Banana Pro. Do NOT pass Firebase Storage public URLs to `generateContent` — the API does not fetch them server-side. `gs://` URIs are Vertex AI / Cloud Storage and are not accepted on the Developer API. - **Long-context guardrail:** the CoverPrompt + 14 reference photo inline descriptions stay well under 100k tokens; no chunking is needed in v1. If a future "year of cards" feature reads the user's full card history, the >500k token guardrail applies and we will summarise older cards into stubs. - **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+. The authoring screen is designed for one-thumb use on a phone bus-ride home. - 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 variation grid (the four covers stream in as they finish). - Optimistic UI on writes; reconcile on response. - Card library uses pagination if the user has > 50 cards; never loads them all at once. - **Local save fallback (FSA non-Chromium):** the "download print PDF / cover" affordance uses `showSaveFilePicker()` on Chromium; on Safari / Firefox, fall back to an `` element pointed at a blob URL — FSA `showSaveFilePicker()` is not supported there. ## 12. Accessibility (WCAG 2.2 AA) - Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`. - All interactive controls reachable by keyboard with a visible focus ring (the focus ring is the postcard-blue `#3E6F8A` at 2 px, with a 2 px offset against the warm paper background for contrast). - Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. - All images have meaningful `alt` text. The generated card covers have `alt` describing the artefact ("4K cover for Priya's birthday card — dawn-grey background, bridge silhouette at horizon, green ceramic mug bottom-left, 'Happy Birthday, Priya' in a warm display serif at the top"). The recipient-facing view repeats this `alt` so screen reader users hear the cover described. - Form fields have associated `