# 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 `