================ ONE-SHOT BUILD CONTRACT (read first) ================
Build this in Google AI Studio "Build" in ONE shot — a complete, working app,
no follow-up turns. These are hard rules, not suggestions:
1. TARGET = Full-Stack Web (Node server runtime, secrets, Firebase allowed).
If you target Native Android instead, you MUST drop all server/DB/Workspace/
secrets and persist locally (Room / SharedPreferences) only.
2. PIN EVERY MODEL CALL — never let the agent auto-select (it downgrades on quota):
- Reasoning / text -> gemini-3.5-flash (thinkingLevel: minimal|low|medium|high)
- 4K image + legible text -> gemini-3-pro-image (image_size "4K", up to 14 refs)
- High-volume image -> gemini-3.1-flash-image
- Expressive TTS -> gemini-3.1-flash-tts-preview (inline tags e.g. [whispers])
- Realtime audio/video (WebSocket) -> gemini-3.1-flash-live-preview
- Sandboxed agent -> antigravity-preview-05-2026
3. DIVISION OF LABOR — the model ONLY parses/extracts to a strict responseSchema.
ALL math, money (store currency as integer minor units / cents), sorting,
balancing and graph logic run in deterministic TypeScript/Python. The model
must never compute totals, splits or balances itself.
4. responseSchema sanitation — no regex patterns, no fixed-length tuples, no
format validators in the schema (they crash the OpenAPI engine). Enforce those
in server-side code AFTER parsing the JSON.
5. responseSchema and google_search grounding are MUTUALLY EXCLUSIVE in one call.
6. CODEGEN — split large output into modular, single-responsibility files so no
file is truncated by the output-token cap.
7. Every external call gets a graceful fallback (e.g. manual paste if a Workspace
read fails). Never a silent dead end.
8. ROBUST STORAGE & CANVAS — Wrap all `localStorage`/`sessionStorage` operations (especially JSON parsing and writes) in `try-catch` blocks to prevent crashes in private windows or quota overflows. Canvas drawing elements must dynamically handle window resize and scale pixel density (`window.devicePixelRatio`) to avoid blurry graphics on retina displays.
=====================================================================
# MUST OBEY — Mobile-first build requirements
This app's PRIMARY surface is a mobile phone. Build it impeccably on mobile FIRST, then verify on tablet and desktop. Treat the rules below as non-negotiable hard constraints, not suggestions.
## Viewports to verify (every screen, every state)
- 320 px, 360 px, 375 px, 390 px, 414 px, 480 px
- 768 px, 834 px (iPad portrait / Pro 11)
- 1024 px, 1280 px, 1440 px, 1920 px, 2560 px
- Plus: 200% browser zoom, landscape orientation on every mobile width, iPhone with safe-area insets visible
## Hard layout rules
- Mobile-first CSS. Default styles target mobile; `@media (min-width: ...)` for larger viewports.
- Use `dvh` and `svh` instead of `vh` for full-height surfaces (iOS Safari URL-bar bug).
- Use `clamp()` for fluid typography across all viewports.
- Prefer container queries (`@container`) over media queries for component-level responsiveness.
- Use `min(100%, ...)` widths so content never overflows. Zero horizontal overflow at any viewport.
- Add `` to every page.
- Apply `padding: max(safe-area-inset-X, fallback)` on every edge-bleeding container so notched iPhones in landscape never clip content.
- Wide tables and code blocks scroll INSIDE their container (`overflow-x: auto`), never push the body.
- Use `background-attachment: scroll` on mobile, not `fixed` (iOS Safari repaint bug).
- Avoid `backdrop-filter` on animated elements. Use it sparingly on static surfaces only.
- **Canvas Scaling**: Canvases must dynamically scale with window resize events and properly handle high-DPI screens (`window.devicePixelRatio`). Set physical dimensions (`canvas.width`/`canvas.height`) using pixel ratio and render relative to this grid, using CSS to control responsive viewport scaling.
- **Robust Storage**: Every access to `localStorage`/`sessionStorage` (especially `JSON.parse` of loaded state or writes) MUST be wrapped in a `try-catch` block to handle disabled storage, private browsing mode, quota limits, or corrupted JSON gracefully. Fall back to a robust in-memory object store.
## Touch & accessibility
- Tap targets ≥ 44 × 44 px on touch (Apple HIG). Increase to 48 px under `@media (hover: none) and (pointer: coarse)`.
- All interactive controls reachable by keyboard with a visible focus ring; respect `:focus-visible`.
- Color contrast ≥ 4.5:1 for body text, 3:1 for UI components.
- All images have meaningful `alt`. Decorative images use `alt=""`.
- Respect `prefers-reduced-motion: reduce` — zero animation durations under that query.
- Forms validate inline; error messages are specific, not "Invalid input".
- Modals: focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, focus restored on close.
## Performance bar (Lighthouse mobile, throttled 3G/4G)
- LCP < 2.5 s · INP < 200 ms · CLS < 0.1
- JS bundle gzip < 200 KB mobile-first; lazy-load non-critical screens via `React.lazy` / dynamic imports.
- No render-blocking resources above the fold.
- Images: WebP/AVIF preferred, `loading="lazy"`, explicit `width`/`height` attributes (zero CLS), `srcset` for retina.
- Videos: `preload="metadata"`, low-resolution poster, max 720p mobile fallback. Never autoplay with audio.
- Fonts: `font-display: swap`; preload only the one used above the fold.
- Smooth scroll honoured via CSS `scroll-behavior: smooth` with reduced-motion fallback.
## Pre-ship mobile checklist (the deployer MUST verify before declaring done)
1. Open at 375 px in DevTools — every screen scrolls vertically only; zero horizontal scroll.
2. Browser zoom 200% — layout reflows without overlap.
3. iPhone Safari with the URL bar visible AND landscape — no content under the home indicator; no notch clipping.
4. iPad portrait (768 px) and landscape (1024 px) — no awkward gaps; tablet-specific breakpoints land cleanly.
5. Tap every interactive element with a thumb at real-device size — every target is easy to hit.
6. `prefers-reduced-motion: reduce` — every transition / animation skips cleanly, scroll-behavior becomes instant.
7. Lighthouse mobile score ≥ 90 across all 4 categories.
8. Zero `console.error` and zero CLS shift in real-device testing on a mid-tier Android (e.g. Pixel 6a) and an iPhone SE.
---
The original template starts below. All rules above apply on TOP of whatever this template specifies.
---
# Tackle Box
## 1. Project
**Tackle Box** is a personal catalogue builder for fly-tiers — the
people who sit at a vice for an hour after dinner and produce, by
hand, the small confections of feather, fur, thread, tinsel, and
hook that they will fish with next season. The user photographs a
tray of finished flies, one tray at a time, and the app produces a
structured catalogue: each fly named by pattern, sized by hook,
tagged by intended water type and the months it tends to fish well,
with notes on the materials it appears to use, and the photograph
of the actual fly preserved beside the record forever.
This is the kind of app a fly-tier builds at the kitchen table on a
quiet evening in March, the week before the season opens, after
realising that the four foam boxes and seven tin trays on the shelf
above the vice contain four hundred flies tied across eleven winters
and that he can no longer remember which box holds the small olive
dries he tied last February for the chalk-stream, or where the heavy
articulated streamers ended up. It is also the kind of app a woman
in rural Hokkaidō builds for her late father's tackle box — six
trays of tenkara kebari her father tied across thirty years on the
Sorachi river, each one a small dark hackle-forward fly she does
not yet know how to read but wants to inherit properly. Same shape
of moment, different water, different century of tradition.
The single demo that proves the magic: photograph one open tray of
forty finished flies → in under thirty seconds the user sees a
typeset catalogue page. Each fly has its own row with a cropped
square photograph at the left, a pattern name in the middle ("Pheasant
Tail Nymph, size 16, gold-bead head, hare's-ear thorax"), and three
chips on the right: a hook-size chip (`#16`), a water-type chip
(`chalk-stream · slow riffles`), and a season chip (`Mar – Jun`).
A new entry appears in the catalogue and the tray photograph
itself is preserved at upload resolution in the corner of the row,
so the user can always tap back to the actual fly in its actual
tray.
And in the harder cases — patterns that the tier invented himself,
patterns from a regional tradition the tier no longer fishes,
patterns inherited from another tier whose intent the user can only
guess at — the app reads what is morphologically present in the
fly and refuses to invent intent. A wet-fly with a dyed-red tag and
a partridge hackle reads as "soft-hackle wet, size 14, dyed-red tag,
partridge hackle, hare's-ear body — morphology consistent with
North-Country spider tradition" and never as "fly the tier ties for
greyling in October". The interpretive layer — when this fly fishes
well, what water it suits, what the tier was thinking when he tied
it — belongs to the tier, not to the model.
**Tagline:** _Turn a drawer of home-tied flies into a catalogue — in any tradition, any hook size, with the photograph of every fly preserved beside its record._
## 2. Target audience
- Hobby fly-tiers with multi-decade tackle drawers — chalk-stream dry-fly tiers in southern England, blue-line brookie tiers in West Virginia, steelhead-spey tiers in Oregon, sea-trout tiers on the west coast of Ireland
- Tenkara and traditional Japanese kebari tiers in Hokkaidō, Nagano, and the Tōhoku mountain rivers, cataloguing patterns inherited from a teacher or father
- Patagonian and Tierra del Fuego guides who tie thousands of streamers and dries each off-season and need a working inventory before the southern-hemisphere season opens in November
- Scottish ghillies and gillies-in-training cataloguing salmon-tube flies and traditional fully-dressed classics, often inherited from a head ghillie now retired
- Adult children of a deceased fly-tier inheriting trays of finished flies they themselves do not yet know how to read, but want to honour and preserve
- Fly-fishing clubs and small-town fishing-shop owners building a shared library of regional patterns — the Catskill museum, the Tay Foundation, a Coromandel hut-book
- Custom-tie sellers on Etsy / Instagram / Patreon-style markets who need a clean inventory of what's currently in stock, in what sizes, photographed once and never re-shot
- Fly-fishing instructors and guides who carry a teaching tray of one of every pattern and need that tray itself catalogued for their students
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **Reads a whole tray at once** — point the camera at an open tray of forty finished flies and the app returns forty rows, each with a cropped square photograph, a pattern reading, a hook-size estimate, and a materials list. One photograph; one Gemini 3.5 Flash multimodal call; one trip from the vice to the catalogue.
- **Identifies by what's on the hook, not what the tier was thinking** — the model reads morphology: hook shape and approximate gape, body material colour and segmentation, tail composition, hackle style, wing or no wing, weight indicators (bead head, lead wraps showing through dubbing, tungsten cone). It names patterns by their visible features and the named tradition they sit in. It never claims to know which river the fly was tied for, which fish it was tied to catch, or which month the tier intends to fish it. That interpretive layer lives in the tier's notes.
- **Hook size is estimated, never asserted** — the app gives an estimate (e.g. "size 14, ±1") based on hook-gape pixels relative to a calibration reference the user sets up once per tray (a coin, a ruler, a known-size hook). The tier confirms or corrects in one tap. The reading is honest about its uncertainty.
- **Water-type and season tags are suggestions, not classifications** — the model suggests `chalk-stream`, `freestone-river`, `still-water`, `salt`, `tailwater`, `mountain-stream`, `salmon-river` based on pattern conventions, never on the fly itself "knowing" where it will be fished. The tier accepts or overrides. The same suggestion logic applies to season chips. The honest UI label is "suggested, tap to confirm".
- **Tradition-aware reading without flattening** — North-Country soft-hackles, Catskill dries, Comparaduns, tenkara sakasa-kebari, fully-dressed salmon classics, Patagonian Wooly Buggers, Spanish bonefish patterns, Bahamian crab flies — each tradition is read on its own terms and named in its own vocabulary. A reverse-hackled fly is named "sakasa-kebari (reverse-hackle)" not "weird-looking soft-hackle".
- **The original photograph is sacred** — the photograph of the actual fly is always one tap away. The structured record sits beside it, never replaces it. If the tier disagrees with the reading, the photograph remains; the record can be edited; the photograph cannot be retroactively changed.
- **Inheritance mode** — when a tier inherits flies from a deceased parent, mentor, or club elder, the app supports a separate "Inherited" archive with extra caution flags: no water-type suggestions until the user opts in, an "I don't yet know what this is for" note attached automatically to each entry, and a `tied_by` field that defaults to the named source rather than the current user.
- **A real working tackle inventory** — at the box-level view, see which boxes hold which patterns at which sizes. Filter "size 16 olive dries" → see the three boxes that hold them and the count in each. Plan a trip → mark a virtual selection box and the app shows what's missing relative to a target list the tier defines.
## 4. Features to build
- Camera capture for a whole tray of flies (mobile-first), with crop guides for the tray edges and a one-time per-tray calibration step (place a coin or a ruler in the corner of the first photograph)
- Upload from photo library or scanner (some tiers have already scanned trays on a flat-bed at 600 dpi)
- Automatic tray segmentation — distinguishes individual fly slots, even in foam-grid boxes, tin trays, magnetic boxes, plastic compartmented trays
- Per-fly cropping — each detected fly receives its own square thumbnail at the highest available resolution; the original tray photograph is preserved separately
- Multimodal parse — pattern identification + materials list + hook-size estimate + tradition tagging, in a single Gemini 3.5 Flash call per tray (batched per-fly outputs)
- Pattern naming with tradition awareness — names follow the convention of the inferred tradition ("Partridge & Orange", "Sakasa Kebari (reverse-hackle)", "Royal Coachman, fully dressed", "Sex Dungeon, articulated streamer")
- Hook-size estimation with explicit ±1 confidence — never asserted as exact
- Materials extraction — `body_material`, `tail_material`, `hackle_material`, `wing_material`, `weight_indicator`, each a verbatim morphological note ("pheasant tail fibres, natural", "hot-orange chenille body, gold rib", "CDC feather wing, two slips")
- Suggested water-type chips (multi-select, suggestions only) — `chalk-stream`, `freestone-river`, `still-water`, `tailwater`, `salmon-river`, `salt`, `mountain-stream`, `spring-creek`, `bonefish-flat`, `tenkara-headwater`. User taps to confirm or override.
- Suggested seasonality chips (suggestions only) — based on pattern tradition (mayfly emergers suggest May-June in temperate north, etc.). Honest label: "suggested by pattern, confirm with your local season".
- Box / tray management — a fly belongs to a named box, with a position (grid-cell or visual hotspot on the original tray photograph). The catalogue can filter, sort, count by box.
- Inheritance mode — separate archive type with the `tied_by` defaulted to the inherited tier; water-type and season suggestions hidden by default; an "I don't yet know what this is for" annotation slot per entry
- Verbatim materials list preserved separately from the pattern reading — a tier may disagree with the pattern name but agree with the materials reading
- Trip-planning view — define a target list ("July trip to the Test: 6× size 18 olive emergers, 12× size 14 PT, 4× ginger sedge size 14") and the app shows what is in inventory and what is missing
- Search across the catalogue — semantic ("show me everything with a CDC wing", "show me my tenkara flies under size 14") and structured ("from the Hardy tin, size 16 or smaller, tied since 2024")
- Voice annotation — record a 10-second voice note attached to a fly ("this is the one I tied for Robbie's first trip to the Test"), transcribed for searchability, played back on tap
- Box-photograph hotspot view — tap any fly in the original tray photograph to jump to its record; tap any record to highlight its position in the tray
- Sharing — invite a fishing partner or co-tier to see the catalogue read-only; co-tiers can be granted edit on a per-box basis
- Print-export — a typeset "tackle book" PDF, organised by box → by tradition → by hook size, with the photograph and materials list of every fly, suitable for printing and keeping in the tackle bag for the season
- Club / shop export — anonymised regional-pattern CSV for fly-fishing clubs and museums (opt-in)
- Time-bounded inventory mode — a quick "what did I tie this winter?" filter that scopes to the user's tying-session log
## 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)
- **Multimodal image input** (Gemini 3.5 Flash) — reads a whole tray photograph and returns one structured record per detected fly. Handles foam grids, tin trays, magnetic boxes, and the deep open shadow boxes some tiers display their classics in. Reads in low evening light, on a wooden bench, with a head-lamp's warm cast across the tray. One API call per tray; the response is an array of `Fly` records indexed by tray position.
- **Structured output / JSON Schema** — the response matches the `TrayParse` schema below. Every field is typed; the schema is included verbatim in the system instruction and as `responseSchema`.
- **Tradition-aware pattern naming** (built into Gemini 3.5 Flash's general reasoning) — names patterns using the conventional vocabulary of the inferred tradition: North-Country spiders use the historical name ("Partridge & Orange", "Snipe & Purple"); tenkara patterns use Japanese romanisation ("Sakasa Kebari", "Ishigaki Kebari"); fully-dressed salmon classics use their Victorian names ("Jock Scott", "Silver Doctor"); modern streamers use the modern names ("Sex Dungeon", "Game Changer", "Wooly Bugger"). Spanish bonefish patterns use Spanish-language names where the tradition is Spanish; Patagonian dry-fly patterns are named in the regional conventions; Argentine sea-trout patterns are named in Spanish.
- **Long context (1M tokens)** — the catalogue grows. For a four-hundred-fly archive, the search call (semantic search across the whole catalogue: "show me everything with a CDC wing tied since 2024") loads every `Fly` record at once. **Guardrail**: a parsed Fly record averages ~600 tokens; a 400-fly archive ≈ ~240k tokens (comfortable). For archives larger than 1,200 flies, chunk by box or by tradition before the cross-catalogue call — the 1M ceiling is real.
- **Search grounding** (Gemini 3.5 Flash) — for the optional "is this pattern in the published literature?" enrichment. When a tier confirms a pattern name, an optional grounded search can return a single citation URL (Wikipedia, a recognised fly-tying site, the Catskill museum, a regional fly-fishing federation page) that the user can choose to attach to the record. Off by default — privacy-respecting; the tier may not want to broadcast which patterns they tie.
- **Gemini TTS** (`gemini-3.1-flash-tts-preview`) — narrates a record aloud ("Pheasant Tail Nymph, size sixteen, gold-bead head, hare's-ear thorax, pheasant-tail tail, copper rib") for the tier who wants to listen to the catalogue at the vice without taking his hands off the thread. Pace is unhurried.
- **Nano Banana 2** (`gemini-3.1-flash-image`) — generates a small reference "tradition card" illustration for the box covers (one illustrated card per tradition the tier has flies in: a North-Country spider, a tenkara kebari, a Patagonian streamer). Never used to alter or "improve" the actual photographs of the user's flies.
- **Thinking levels** — `medium` for the primary tray-parse call (pattern identification + materials extraction + hook-size estimate together is the hardest reasoning). `low` for semantic search, voice-note transcription, and the optional grounded pattern-citation call.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used for fishing-partner invitations and inheritance-archive shares) requires the sender domain to be authorised in Firebase Auth. Catalogues are private to the owner and explicitly-invited co-tiers / partners. No public-by-default.
- **Database — Required.** Firestore for `users`, `catalogues`, `boxes`, `flies`, `voice_notes`, `tying_sessions`, `target_lists`, `catalogue_members`.
- **File storage — Required.** Firebase Storage for original tray photographs (preserved at upload resolution, forever), per-fly crops, and voice-note audio. **Storage is NOT auto-provisioned by AI Studio Build today** — enable it in the Firebase console and wire the bucket name into the AIS Build project before first tray upload. Pre-signed URLs only; the photographs are never publicly addressable.
- **Email — Required (transactional).** Fishing-partner invitations via email link (Firebase Auth magic links). Inheritance-archive share notifications. Optional weekly digest ("you tied 14 new flies this week, here's the catalogue update").
- **Payments — Not needed for v1.** Free for personal use. A future "printed tackle book" tier could pipe to a print-on-demand partner and charge for that physical artefact only.
- **External APIs:** Gemini API for all intelligence. No third-party fly-pattern database — the model's tradition-aware naming is the load-bearing capability; pinning a paid database would dilute the demo's "this came from one Gemini call" honesty.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Stripe key if printing tier added) lives in environment variables — never in client bundle. Include a `.env.example`.
**Auth + data privacy reminders:** never log secrets · never store passwords in plain text · use HTTPS everywhere · honour 'delete my account' inside the UI · explicit opt-in for any analytics · the user's catalogue is 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) · the optional grounded pattern-citation call is opt-in per record, not global.
**Read this first — prompt-craft rules that apply to every call in this template:**
1. **Name the model variant explicitly** in every Gemini API call. Do not let the agent pick the model. See the per-call matrix below.
2. **Pin `thinkingLevel` explicitly** per call. See the matrix.
3. **Seed the JSON Schema as a fenced TypeScript / Zod block** in the system instruction or `responseSchema` field. The literal schema is below. **Convert the Zod schema to Gemini's `Schema` type via the SDK helper** before passing to `responseSchema` — do NOT pass raw Zod. **Numeric `min`/`max` constraints are documentation only inside `responseSchema`; clamp on the server after the response arrives.**
4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for user input. Never concatenate.
5. **Pre-declare tools as an enable/disable list** per call. The matrix below names which tools are enabled per call. Tools NOT listed for a call should be disabled.
6. **State negative constraints explicitly** — they are listed below. They are NOT "be careful" suggestions; they are hard rules the model must follow.
7. **Grounded responses can wrap JSON in ```json fences or add prose preamble.** Server-side, strip fences and brace-extract:
```typescript
function safeExtractJSON(raw: string): T {
const clean = raw.replace(/```json\s*|```/gi, '').trim();
const s = clean.indexOf('{'); const e = clean.lastIndexOf('}');
if (s === -1 || e === -1) throw new Error('No JSON boundaries in grounded response');
return JSON.parse(clean.slice(s, e + 1)) as T;
}
```
8. **Strip unsupported Zod modifiers before passing to `responseSchema`** — Gemini's OpenAPI subset rejects `.regex()` / `pattern`, fixed-length `z.tuple()`, and other custom validators. Use a sanitizer that flattens tuples to length-2 arrays and removes regex patterns before serializing. Validate those constraints in middleware AFTER parsing.
### Per-call model + tools matrix
| Call | Model | thinkingLevel | Tools enabled |
|------|-------|---------------|---------------|
| Parse tray photograph → `TrayParse` (array of `Fly` records) | `gemini-3.5-flash` | medium | (none) |
| Generate per-fly cropped thumbnail prompts (internal) | `gemini-3.5-flash` | low | (none) |
| Suggest water-type + seasonality chips per pattern | `gemini-3.5-flash` | low | (none) |
| Pattern-citation enrichment (opt-in, per record) | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| Semantic search across the catalogue (long-context) | `gemini-3.5-flash` | low | (none) — long-context over the whole catalogue |
| Transcribe voice note attached to a fly | `gemini-3.5-flash` | low | (none) |
| TTS — read a catalogue entry aloud | `gemini-3.1-flash-tts-preview` | n/a | n/a |
| Tradition-card illustration for a box cover | `gemini-3.1-flash-image` | n/a | n/a |
*Note for builders:* on TTS and image-generation calls, omit `thinkingConfig` entirely — the field is not supported on those models. The `n/a` cells in this matrix are documentation only; do not serialise them into the request body.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const TraditionTag = z.enum([
"north-country-spider",
"catskill-dry",
"comparadun",
"klinkhammer",
"cdc-emerger",
"soft-hackle-wet",
"fully-dressed-salmon",
"spey-salmon",
"tube-fly",
"modern-streamer",
"articulated-streamer",
"wooly-bugger-family",
"tenkara-sakasa-kebari",
"tenkara-jun-kebari",
"tenkara-futsu-kebari",
"japanese-keiryu",
"patagonian-dry",
"patagonian-streamer",
"saltwater-bonefish",
"saltwater-tarpon",
"saltwater-permit-crab",
"stillwater-buzzer",
"stillwater-blob",
"tailwater-midge",
"atlantic-salmon-shrimp",
"steelhead-intruder",
"scandinavian-tube",
"regional-other",
"unidentified-morphology",
]);
const WaterTypeSuggestion = z.enum([
"chalk-stream",
"freestone-river",
"spring-creek",
"tailwater",
"mountain-stream",
"tenkara-headwater",
"still-water",
"salmon-river",
"salt-flat",
"salt-coast",
"estuary",
"lake",
]);
const SeasonSuggestion = z.object({
hemisphere: z.enum(["northern", "southern", "tropical"]),
months: z.array(z.enum([
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
])),
reasoning_short: z.string(), // "mayfly emerger pattern; northern hemisphere May-Jun"
});
const Material = z.object({
role: z.enum([
"body", "rib", "tail", "tail-fibres", "wing", "wing-post",
"hackle", "thorax", "head", "bead-or-cone", "weight-underbody",
"tag", "butt", "shoulder", "cheeks", "topping", "throat",
"egg-sac", "trailing-shuck", "antenna", "eyes",
]),
description_verbatim: z.string(), // "pheasant tail fibres, natural reddish-brown"
appears_synthetic: z.boolean().nullable(), // null when unclear
appears_dyed: z.boolean().nullable(), // null when unclear
});
const HookEstimate = z.object({
size_estimate: z.number().int(), // 16 (positive ints; saltwater uses negative scale below)
size_estimate_alt_scale: z.string().nullable(), // "2/0" for saltwater large
uncertainty_plus_minus: z.number().int(), // ±1 typical
shape: z.enum([
"standard-dry", "standard-wet", "1xl-nymph", "2xl-streamer",
"3xl-streamer", "4xl-streamer", "scud-curved", "klinkhammer-curved",
"jig", "saltwater-stainless", "tube-fly-no-hook", "egg-yarn-hook",
"barbless", "barbed", "uncertain",
]),
bead_or_cone: z.enum([
"none", "brass-bead", "tungsten-bead", "tungsten-cone",
"brass-cone", "metal-eye-bead-chain", "metal-eye-dumbbell",
"uncertain",
]),
calibration_used: z.string().nullable(), // "coin in upper-left of tray photo"
});
const TyingNote = z.object({
text_verbatim: z.string(),
source: z.enum(["user-typed", "voice-transcribed"]),
date_iso: z.string().nullable(),
});
const PatternCitation = z.object({
source_url: z.string(), // populated only when grounded search is opted in
source_title: z.string(),
excerpt_short: z.string(),
});
const Fly = z.object({
fly_id: z.string(),
tray_position: z.object({
row: z.number().int(),
column: z.number().int(),
bbox_normalised: z.object({
x: z.number(), // 0..1 within tray photo
y: z.number(),
w: z.number(),
h: z.number(),
}),
}),
thumbnail_uri: z.string(), // per-fly crop, Firebase Storage signed URL
tray_photo_uri: z.string(), // the original tray, preserved
pattern_name_primary: z.string(), // "Pheasant Tail Nymph, gold-bead"
pattern_name_alternatives: z.array(z.string()), // ["PTN", "Sawyer-style PT"]
tradition_tag: TraditionTag,
tradition_confidence: z.number().min(0).max(1),
hook: HookEstimate,
materials: z.array(Material),
morphology_summary_short: z.string(), // one sentence the user reads first
visible_features: z.array(z.string()), // ["gold tungsten bead-head", "hare's-ear dubbed thorax", ...]
weight_class: z.enum([
"dry-floating", "emerger-film", "nymph-light",
"nymph-medium", "nymph-heavy", "streamer-floating",
"streamer-sinking", "wet-traditional", "uncertain",
]),
suggested_water_types: z.array(WaterTypeSuggestion),
suggested_seasonality: z.array(SeasonSuggestion),
suggestions_disclaimer: z.string(), // standard: "Suggested by pattern only — confirm with your water and your season."
tied_by_default: z.string().nullable(), // "me" / "Inherited from [name]"
tying_notes_user: z.array(TyingNote),
pattern_citations_optional: z.array(PatternCitation),
reading_confidence: z.number().min(0).max(1),
flagged_for_user_review: z.array(z.object({
field_path: z.string(), // e.g. "hook.size_estimate"
reason: z.string(), // e.g. "no calibration object visible in tray photo; size estimate ±2"
})),
});
const TrayParse = z.object({
tray_id: z.string(),
tray_photo_uri: z.string(),
tray_layout_detected: z.enum([
"foam-grid", "tin-tray", "magnetic-box", "compartmented-plastic",
"open-display-tray", "leaf-binder", "stuck-on-card", "uncertain",
]),
calibration_object_detected: z.object({
present: z.boolean(),
description: z.string().nullable(), // "1-euro coin in upper-left corner"
bbox_normalised: z.object({
x: z.number(), y: z.number(), w: z.number(), h: z.number(),
}).nullable(),
}),
fly_count_detected: z.number().int(),
flies: z.array(Fly),
tray_level_notes: z.array(z.string()), // e.g. "two slots empty (upper-left, lower-right)"
});
type Fly = z.infer;
type TrayParse = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` on the the tray parse call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash misreads hook shapes (jig vs. standard-wet), confuses CDC with hen-hackle, and silently collapses materials lists into one sentence.
- Hook size asserted with false precision ("exactly size 14") — always populate `uncertainty_plus_minus` and surface it in UI. The model must never assert an exact size unless a calibration object is detected in the photograph.
- Pattern name claimed where morphology is genuinely ambiguous — when the model cannot confidently name a pattern, set `tradition_tag` to `unidentified-morphology` and put the morphology in `pattern_name_primary` ("size 16, olive-dubbed, hen-hackle, no wing — morphology suggests soft-hackle wet, unnamed pattern"). Do not invent a name to fill the field.
- Water-type / season suggestions presented as classifications — always carry the `suggestions_disclaimer` string verbatim into the UI; render the chips with a distinct visual treatment ("tap to confirm") that is clearly suggestion-state, not fact-state.
- Tradition tagging biased toward European conventions — the system instruction explicitly enumerates tenkara, keiryu, Patagonian, Atlantic salmon, steelhead, saltwater, stillwater traditions and instructs the model to read by tradition. Verify with a tenkara test tray that no kebari is misread as a "soft-hackle wet".
- Materials list flattened into one prose sentence — enforce the `Material[]` array. Each role-tagged material is a separate object. Test by checking that a Pheasant Tail Nymph produces at least four `Material` entries: body, rib, tail-fibres, bead.
- Tray segmentation merges adjacent flies into one — if `fly_count_detected` is wildly different from the user's expectation, prompt the user with the original tray photograph and let them tap the missed slots. Do not silently under-count.
- Per-fly crops too tight, cutting off the tail — the cropping prompt specifies a 20% padding margin around the morphological bounding box.
- Calibration object misidentified — the system instruction explicitly says "if you see a coin, ruler, or a known hook in the photograph, name it in `calibration_object_detected.description`. If you do not see one, set `present: false` and increase `hook.uncertainty_plus_minus` to at least ±2."
- TTS reads "PTN" letter-by-letter as "P-T-N" — pre-expand abbreviations server-side before sending to TTS ("Pheasant Tail Nymph").
### Negative constraints (hard rules)
- Do NOT assert the tier's intent. The fly is a physical object. The model may describe what is on the hook. The model must NOT claim "this fly is for [river / fish / month]" except as a chip-suggestion with the standard disclaimer.
- Do NOT name a pattern with false confidence. If `tradition_confidence` would be under 0.6, set `tradition_tag` to `unidentified-morphology` and describe the morphology in `pattern_name_primary` instead of guessing a famous name.
- Do NOT translate tradition vocabulary. "Sakasa Kebari" stays "Sakasa Kebari" — never "reverse-hackle Japanese wet fly" in `pattern_name_primary`. The translation can appear as a parenthetical in `pattern_name_alternatives` once: "Sakasa Kebari (reverse-hackle kebari)".
- Do NOT modernise pattern names. "Jock Scott" stays "Jock Scott", never "Atlantic salmon classic wet fly". "Wooly Bugger" stays "Wooly Bugger", never "marabou streamer".
- Do NOT invent materials. If you cannot tell whether a body is CDC or hen-hackle, set `appears_synthetic: null` and describe what you can see ("light-grey soft fibres, possibly CDC"). Do not assert.
- Do NOT extrapolate to the tier's intentions. If a fly is articulated and four inches long, do not write "for big predatory trout". The morphology is enough; the fishery interpretation belongs to the tier.
- Do NOT silently alter the tier's photograph. The original tray photograph is preserved at upload resolution. Per-fly thumbnails are crops, never colour-corrected, never sharpened, never AI-restored.
- Do NOT use the user's catalogue to train or fine-tune any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The capabilities-info panel says this in plain English.
- Do NOT auto-publish or auto-share. Catalogues are private by default. Sharing is explicit, per-catalogue, per-partner.
- Do NOT auto-claim provenance. A fly in an `Inherited` archive is tagged `tied_by_default: "Inherited from [name]"` only when the user has explicitly set the inheritance source. Otherwise the field is null and the UI says "source unknown — tap to attribute".
### 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: Parse tray photograph → `TrayParse`
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are reading a photograph of an open tray of finished, hand-tied
fishing flies. The tier has placed the tray on a flat surface and
photographed it from roughly directly above. Lighting may be a
warm desk lamp, an overhead kitchen light, a north-window grey,
or the harsh down-cast of a head-lamp. The tray may be a foam
grid, a tin tray (Wheatley, Hardy, Richard Wheatley styles), a
magnetic box, a compartmented plastic box (a salt-water purpose
box), an open display tray, a binder leaf with flies stuck through
a foam strip, or a card with flies pinned to it. Detect the layout.
Each tray contains between one and ~64 flies. The flies span a
wide range of fishing traditions; you must read each fly on the
terms of its tradition. Traditions you must recognise and name
correctly include:
- North-Country spiders (Yorkshire and Northumbrian soft-hackles —
Partridge & Orange, Snipe & Purple, Waterhen Bloa, Greenwell's
Glory)
- Catskill-style dries (high-floating divided-wing dries — Royal
Coachman, Light Cahill, Quill Gordon)
- Comparaduns and CDC emergers (modern dry-fly conventions)
- Klinkhammers (Hans van Klinken's parachute-emerger family)
- Fully-dressed Atlantic salmon classics (Jock Scott, Silver
Doctor, Durham Ranger, Green Highlander — full Victorian
dressing)
- Spey-style salmon flies and modern salmon tube flies
(Scandinavian-style tubes too)
- Steelhead intruders and articulated modern streamers (Pacific
Northwest tradition)
- Modern American streamers (Sex Dungeon, Game Changer, Wooly
Bugger family, Murdich Minnow)
- Tenkara kebari from the Japanese tradition — explicitly
Sakasa Kebari (reverse-hackle), Jun Kebari (forward-hackle), and
Futsū Kebari (standard hackle). Use Japanese romanisation; do not
flatten to "soft-hackle wet".
- Japanese keiryū patterns more broadly (genryū-zaō, mountain stream
patterns from Hokkaidō, Nagano, Tōhoku rivers)
- Stillwater patterns — buzzers (chironomid pupae), blobs, FAB
patterns, washing-line setups
- Tailwater midge patterns (Zebra Midges, RS2s)
- Patagonian dry-flies and streamers — Argentine and Chilean
conventions, including Mosca Caballero, Mosca Cigarro,
Patagonia-style Wooly Buggers
- Saltwater bonefish (Gotcha, Crazy Charlie, Bonefish Bitters)
- Saltwater tarpon flies (Tarpon Bunny, Black Death)
- Saltwater permit crab patterns (Merkin, Avalon)
- Atlantic salmon shrimp patterns (Ally's Shrimp, Cascade)
If a fly does not fit any of these, set tradition_tag to
"regional-other" and describe what you can see. If you cannot
identify even the tradition, set tradition_tag to
"unidentified-morphology" and describe the morphology in
pattern_name_primary. Do NOT guess a famous name to fill the field.
For each fly detected:
1. Locate it precisely on the tray photograph. Populate
tray_position.bbox_normalised in 0..1 coordinates of the source
image. Detect row and column index relative to the tray layout.
2. Name the pattern in pattern_name_primary using the conventional
vocabulary of the inferred tradition. Use Japanese romanisation
for tenkara. Use Spanish-language names for Patagonian patterns
tied in the Spanish-language tradition. Preserve Victorian
classic names verbatim.
3. Populate pattern_name_alternatives with the abbreviation or
alternative name (e.g. "PTN" for Pheasant Tail Nymph, "GRHE"
for Gold-Ribbed Hare's Ear).
4. Estimate hook.size_estimate as an integer on the standard fly-hook
scale (smaller number = larger hook for freshwater 1-22; positive
X/0 scale for saltwater encoded in hook.size_estimate_alt_scale
as a string "2/0", "4/0"). If a calibration object is visible in
the tray photo (a coin, a ruler, a known-size hook), name it in
calibration_object_detected.description and use it. If not,
estimate from relative proportions and set
hook.uncertainty_plus_minus to at least 2.
5. Read morphology and extract Material[]. Each material is one
object with role and verbatim description. Mark
appears_synthetic and appears_dyed as null when unclear. Common
materials you must distinguish: pheasant tail fibres, CDC,
hen-hackle, cock-hackle (genetic), partridge, snipe, woodcock,
grouse, marabou (turkey marabou), bucktail, calf body, deer
hair (spinning vs. compressed), elk hair, moose mane, peacock
herl, ostrich herl, ice-dub and synthetic flash, chenille,
tinsel (flat vs. oval, gold vs. silver vs. coloured), holographic
tinsel, foam (closed-cell vs. evazote), CDC dubbing, hare's-ear
dubbing.
6. Populate visible_features as an array of short noun phrases
("gold tungsten bead-head", "hare's-ear dubbed thorax", "two-tone
tail fibres", "hot-orange tag", "Spey-style flowing hackle").
7. Choose weight_class from the closed enum. Use uncertain only
when you genuinely cannot tell.
8. Suggest suggested_water_types and suggested_seasonality as
suggestions only — never assertions. Set suggestions_disclaimer
to: "Suggested by pattern only — confirm with your water and
your season."
9. Populate flagged_for_user_review for any field where
reading_confidence is below 0.7, naming the specific field path
and the reason in one sentence.
Hard rules:
- Do NOT assert the tier's intent. Describe morphology. Suggest
water-type and season as chips with the disclaimer. Never write
"this fly is for [river / fish]".
- Do NOT translate tradition vocabulary. Sakasa Kebari stays Sakasa
Kebari in pattern_name_primary. A translation may appear in
pattern_name_alternatives once: "Sakasa Kebari (reverse-hackle
kebari)".
- Do NOT modernise classical pattern names. "Jock Scott" stays
"Jock Scott".
- Do NOT invent materials. If a body looks like it could be CDC
or hen-hackle and you cannot tell, set appears_synthetic and
appears_dyed to null and describe what you can see in
description_verbatim ("light-grey soft fibres, exact origin
unclear").
- Do NOT pretend to know an exact hook size without a calibration
object. Without calibration, uncertainty_plus_minus is at least 2.
- Do NOT extrapolate beyond what is morphologically visible. No
"for big predatory trout"; no "tied for steelhead"; no "this is
a winter fly".
Output ONLY the TrayParse JSON matching the provided schema.
No commentary outside JSON.
```
---
### Call: Suggest water-type + seasonality chips per pattern
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a `Fly` record produced by the tray-parse call. Your job
is to refine suggested_water_types and suggested_seasonality based
on the pattern, the tradition tag, and the materials, and to
populate suggestions_disclaimer with the canonical string.
Hard rules:
- Suggestions are suggestions. Never write the chips as if they
were classifications. The UI will render them as tap-to-confirm
pills.
- Use the tradition convention as the suggestion frame:
- North-Country spiders → freestone-river, mountain-stream, often
Apr-Sep in northern hemisphere
- Catskill dries → freestone-river, spring-creek, May-Sep in
northern hemisphere
- Tenkara kebari → tenkara-headwater, mountain-stream, Apr-Oct in
Japan
- Stillwater buzzers → still-water, lake; Mar-Oct in temperate
north
- Patagonian streamers → freestone-river, salmon-river-equivalent;
Nov-Apr in southern hemisphere
- Saltwater bonefish patterns → salt-flat; year-round in tropical
- For seasonality, ALWAYS populate hemisphere first. A pattern
conventionally fished in May in temperate north is fished in
November in temperate south.
- For Patagonian, Tierra del Fuego, New Zealand, Tasmanian, Cape /
South African patterns: assume southern hemisphere. For Hokkaidō
tenkara: northern. For Florida bonefish: tropical.
- Do NOT pretend to know which river / which fish / which exact
week. The chips are conventional suggestions only.
- Set suggestions_disclaimer to: "Suggested by pattern only —
confirm with your water and your season."
Output the updated arrays of suggested_water_types,
suggested_seasonality, and the suggestions_disclaimer string as a
single JSON object. No commentary.
```
---
### Call: Pattern-citation enrichment (opt-in)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: search grounding
```
You receive a pattern name and tradition tag from a `Fly` record.
The user has explicitly opted-in to attach a citation. Return ONE
authoritative reference for the pattern: a Wikipedia entry, a
recognised fly-tying museum page, a regional fly-fishing federation
page, or an established fly-tying instructor's reference site.
Hard rules:
- Use `google_search` grounding. Do not invent URLs or paste URLs
from memory.
- Prefer institutional and museum sources where they exist (Catskill
Fly Fishing Center & Museum for Catskill dries; the Salmon &
Trout Conservation reference pages for British salmon classics;
Tenkara USA's pattern primer for tenkara kebari; regional clubs
for Patagonian patterns).
- Return ONLY ONE citation. Do not pad with multiple sources.
- If no authoritative source is found, return an empty
pattern_citations_optional array. Do not invent.
Output the response as JSON in the text body (NOT via
`responseSchema` — `responseSchema` and `google_search` cannot be
combined in the same Gemini call today). Server-side: parse the
JSON, then read the citation URL from the response's
`groundingMetadata.groundingChunks[].web.uri` — do NOT ask the
model to include the URL in the JSON body; it will hallucinate
the URL. Use the model's body only for excerpt_short and
source_title; substitute the URL from groundingMetadata before
persisting.
No commentary outside the JSON.
```
---
### Call: Semantic search across the catalogue (long-context)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none, long context)
```
You receive every `Fly` record in a catalogue at once (long-context)
and a natural-language query from the user ("show me everything
with a CDC wing", "show me my tenkara flies under size 14 I tied
since the start of this year", "what would I take to the Test in
late May?").
Your task: return the array of fly_id values matching the query,
ordered by relevance. Include a short relevance_note per result
explaining why it matched ("CDC wing-post; size 16; tied 2026-02-11").
Hard rules:
- Match on what is actually in the records — pattern_name,
tradition_tag, materials[].description_verbatim, hook.size_estimate,
visible_features. Do NOT match on what the model thinks the fly
is for unless the user's tying_notes or suggested_water_types
explicitly say so.
- For "what would I take to [river] in [month]" queries: surface the
suggested_water_types and suggested_seasonality that fit, and
caveat at the top that these are suggestions, not classifications.
- If the catalogue is larger than 1,200 records, you will receive
it chunked by box; combine results across chunks server-side and
re-rank in a second pass.
- Do NOT invent fly_ids. Every fly_id in your response must appear
in the input.
Output: { results: [{ fly_id, relevance_note }], caveat?: string }.
No commentary outside JSON.
```
---
### Call: Transcribe voice note attached to a fly
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive an audio file (a 5-30 second voice note recorded by the
tier at the vice or in the field, attached to a single Fly record).
Transcribe verbatim.
Hard rules:
- Preserve fly-tying and fly-fishing vocabulary correctly: CDC,
Pheasant Tail Nymph, hackle, dubbing, herl, marabou, parachute,
spinner, dun, emerger, soft-hackle, Sakasa Kebari, Spey, Klinkhammer,
Wooly Bugger, articulated, sink-tip, dead-drift, swing, nymph,
midge.
- Preserve river names verbatim when audible (Test, Itchen, Wye,
Dee, Madison, Henry's Fork, Sorachi, Limay, Río Grande).
- If the audio contains a date reference ("this Saturday", "last
March"), transcribe verbatim. Do NOT resolve to ISO.
- If the tier names a pattern, transcribe the pattern name verbatim
even if it differs from what the model would have called the fly.
The tier's name overrides.
Output: { text_verbatim: string }. No commentary.
```
---
### Call: TTS — read a catalogue entry aloud
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, unhurried. Pick the Gemini 2.5 Flash TTS voice whose
`languageCode` matches the user's preferred locale (en-GB and en-US
both have native voices; for Japanese tenkara entries, ja-JP is
appropriate for the pattern name and any Japanese vocabulary;
for Patagonian-tradition entries with Spanish-language pattern
names, es-AR or es-CL is appropriate). Prefer a warm mid-range
voice; fall back to whichever is available rather than blocking.
Pre-process the text before sending it to TTS:
- Construct a reading from the Fly record:
pattern_name_primary, then a short pause, then hook size and
shape, then a short pause, then a short morphology summary
("hare's-ear thorax, pheasant-tail tail, copper rib, gold tungsten
bead head"), then a short pause, then the weight class.
- Pre-expand abbreviations: "PTN" → "Pheasant Tail Nymph",
"GRHE" → "Gold-Ribbed Hare's Ear", "CDC" → "see-dee-see" (read
as the three letters) OR "cul-de-canard" (the full French) —
prefer "see-dee-see" as that is how most tiers say it. Hook size
numbers are spoken as the number ("size sixteen", not
"size one six").
- For tenkara Japanese pattern names, render the Japanese name
first ("Sakasa Kebari"), then a short pause, then the English
gloss in parentheses if present.
- At each sentence break, insert a single ellipsis (`…`) so the
TTS model produces a natural pause. At a section change
(pattern name → hook → materials), insert a blank line plus an
em-dash (`—`). Gemini 2.5 TTS does not support SSML
`` — these textual cues are how you signal pace.
- Mid-call voice switching is not supported. If the entry mixes
English and Japanese pattern vocabulary, keep one voice throughout
and rely on the chosen voice's locale for pronunciation. The
user sees the script switch in the on-screen subtitle.
- Target rate: ~120 words per minute — at-the-vice reading pace,
not podcast pace.
Style direction: prepend ONE short directive sentence to the
text input, exactly like: "Read warmly and unhurriedly, as a
fly-tier reading their own catalogue at the vice. …". There is
no separate `style` API field on Gemini 2.5 TTS; the directive
sentence inside the input is how style is conveyed.
Phoneme overrides (Japanese romanisation, Spanish pronunciation
of "Mosca Cigarro", French "CDC" full form) are NOT exposed by
Gemini 2.5 TTS — no SSML `` tag. Pronunciation comes
from the chosen voice's native locale.
```
---
### Call: Tradition-card illustration for a box cover
Model: `gemini-3.1-flash-image` · n/a · n/a
```
You are generating a small, restrained illustration of a single
representative fly from a named fly-fishing tradition, to serve
as a box-cover card in the user's tackle catalogue. Style is
honest reference-illustration, like a hand-drawn entomology plate
or an early-twentieth-century fly-tying manual frontispiece — not
a glossy AI render and not a stock photograph.
For the named tradition, generate ONE fly:
- "north-country-spider" → a Partridge & Orange in profile, on a
curved wet-fly hook, hackle flowing back, soft pencil-and-wash
feel.
- "catskill-dry" → a Light Cahill in profile, divided wood-duck
wing standing upright, ginger hackle wound forward, on a standard
dry-fly hook.
- "tenkara-sakasa-kebari" → a Sakasa Kebari in profile, soft
partridge hackle leaning forward over the eye, dark thread body,
on a barbless hook.
- "fully-dressed-salmon" → a Jock Scott or Silver Doctor in
profile, full Victorian dressing, on a curved salmon-iron, in a
warm sepia tone.
- "patagonian-streamer" → a marabou streamer in profile with a
flowing tail, in restrained ochre and brown tones.
- "saltwater-bonefish" → a Gotcha in profile, pale tan body, bead-
chain eyes, on a stainless saltwater hook, against a sand-pale
background.
Hard rules:
- Always profile view. The fly faces left, head to the left.
- Restrained palette. No bright digital colours unless the
tradition genuinely uses them (e.g. the fully-dressed salmon
classics can show their reds and yellows).
- No human hands, no vices, no scenes, no rivers in the background.
- No text, no captions, no labels in the image. The catalogue
layout adds the caption.
- Do NOT generate an image that purports to be a photograph of
the user's own fly. This image is a tradition-card illustration,
clearly stylised, never confused with the photographs in the
catalogue.
Output: one image, square, 1024x1024.
```
## 5. Use cases & content to include
Build dedicated UI sections or flows for each of these — they tell you what content the app must support.
- **The shelf above the vice.** A user is closing out a winter of tying and the four foam boxes and seven tin trays on the shelf above his vice now hold four hundred finished flies he can no longer remember reading. The empty state offers a "one tray at a time" capture flow optimised for an unhurried afternoon: lay the tray on the kitchen table under the warm overhead light, lay the calibration coin in the corner, photograph from above, repeat. The queue processes overnight; the catalogue is ready in the morning.
- **The inherited tackle box.** A woman in Hokkaidō is cataloguing her late father's six trays of tenkara kebari from the Sorachi river. She does not yet know how to read them. The Inheritance mode hides the water-type and season suggestions by default and attaches an "I don't yet know what this is for" annotation slot to every entry. She sets `tied_by` to her father's name once at the archive level. Later, when a family friend who fishes the Sorachi visits, he reads through her catalogue at the kitchen table and fills in what he knows.
- **The Patagonian guide.** An off-season guide in San Martín de los Andes ties two thousand flies between April and October. The catalogue handles the daily output: every evening, one tray photographed, recorded, dated. By November, when the season opens, he has a working inventory of Mosca Caballero, Mosca Cigarro, Wooly Bugger, and articulated streamer counts, organised by size, ready to load his daily fly-boxes against client trip lists.
- **The Scottish ghillie.** A young ghillie inheriting a head ghillie's tackle drawer on retirement — three boxes of fully-dressed salmon classics, two boxes of modern tube flies, one tin of Spey patterns. The app reads each tradition on its own terms. Jock Scott stays Jock Scott. The Cascade tube stays the Cascade. A pre-war shrimp pattern the head ghillie tied at fourteen and still tied at seventy-six is read as morphology-first and flagged for the head ghillie himself to name when the apprentice visits him in hospital.
- **The OFW tier in the Cordillera.** An Ifugao-born guide in Banaue ties flies for the trout that were stocked in the cold streams of the rice-terrace country. The catalogue reads his patterns without flattening them — local-tradition tags appear as `regional-other` with morphology summaries the model is honest about, never forced into a famous-name pattern.
- **The chalk-stream amateur.** A retired teacher in Stockbridge, England, who fishes only the Test and Itchen, ties exclusively size 16–20 dry-flies and emergers. The catalogue's filter view lets him plan a Saturday's box by tapping a target list ("July 14th, late morning, late hatches, 8 olive duns, 6 emergers, 2 PTs as droppers") and seeing exactly which boxes hold the candidates and which sizes he is short.
- **The Catskill museum volunteer.** A volunteer cataloguing a donated collection of fully-dressed salmon classics at the Catskill Fly Fishing Center. The opt-in pattern-citation feature attaches a museum-page citation to each named classic, and the museum-export CSV is part of a regional-pattern donation flow.
- **The Etsy custom-tie seller.** A tier in West Virginia who sells small-batch trout flies online needs a live inventory of what's in stock by pattern and size. The catalogue's box-level view is the live inventory; a private CSV export feeds her shop's stock count.
- **The husband-and-wife tying club.** A husband and a wife who share a tying bench but each tie different patterns. She invites him to the catalogue; he gets edit access on his own boxes and view access on hers. Their voice-notes are clearly attributed.
- **The Cuban-American saltwater tier.** A tier in Miami who ties saltwater patterns for the Florida flats — bonefish, tarpon, permit — and inherited a small box of his abuelo's Cuban-tradition coast patterns. The Inheritance mode preserves the abuelo's box as a separate archive; the app refuses to map his abuelo's morphologically-unclear coast patterns onto the dominant US-published vocabulary.
## 6. Page structure
Build the following screens / sections in this order. Adjust copy to fit the voice, but keep the structural intent.
1. **Welcome / sign-in.** A photographed-looking image of a wooden vice on a tying bench at evening, with a tin tray of half-tied flies on the desk and a head-lamp's warm cone of light across the bench. One paragraph: "Tackle Box turns a tray of finished flies into a catalogue — with every photograph preserved beside its record, every tradition read on its own terms, and every suggestion honestly labelled as a suggestion." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample drawer" → loads the demo catalogue in section 8a.
2. **Empty state — "Start a catalogue".** Three big input methods: 📷 Photograph a tray · 🖼 Upload scans · 🧰 Set up inheritance archive. A short explainer below each ("Best for one tray at a time, kitchen-table afternoon", "Best if you've already scanned trays on a flat-bed", "For flies you inherited and don't yet want auto-tagged").
3. **Tray capture flow** (mobile-first). Live viewfinder with crop guides for the tray edges; a one-time per-tray calibration step ("place a coin or a ruler in the corner of the first tray photograph"). Capture, review, retake. The flow keeps the camera at fixed focus and white-balance so colours stay consistent across the night's trays.
4. **Processing queue.** A vertical list of the night's trays. Each item shows the tray thumbnail, the detected fly count, and a step-by-step honest progress bar: "Detecting the tray layout…" → "Reading each fly…" → "Identifying patterns…" → "Estimating hook sizes…" → "Drafting suggested chips…". Each step takes 4-12 seconds per tray. The user can close the app and come back.
5. **Tray detail view.** A two-panel layout on desktop, stacked on mobile. Top panel: the photograph of the original tray (zoomable, with each fly's bounding box overlaid; tap a box → highlights the corresponding row below). Bottom panel: a list of detected flies, one row each, with the per-fly thumbnail at left, the pattern reading in the middle, three suggestion chips (hook size with ±, water types, seasonality) on the right, and a small `(i)` icon for "show how the AI read this" that opens the materials list and morphology summary. The tier confirms or overrides each row in one tap.
6. **Catalogue view.** A magazine-grid of flies. Filter by box, by tradition, by hook-size range, by water-type, by season, by tied-by. A toggle: "Show me everything with a CDC wing", "Show me my tenkara flies under size 14", "Show me what I tied this winter". Default sort: by box, then by tradition, then by hook size descending (largest first, the tradition's reading direction).
7. **Box view.** Each box has a tradition-card illustration on its cover (generated once by the Nano Banana 2 call), the box's name, the count of flies it holds, the size range, and a "load this box for a trip" button. Inside the box: a magazine-grid of its flies, plus an empty-slot map matching the original tray photograph.
8. **Trip-planning view.** Define a target list ("July trip to the Test: 6× size 18 olive emergers, 12× size 14 PT, 4× ginger sedge size 14"). The app shows what is in inventory and what is missing, with a one-tap "tie this winter's missing" reminder that drops into the tying-session log.
9. **Tying-session log.** A simple calendar view of tying sessions — date, count tied, patterns. Tap a date → all flies tied that session.
10. **Voice-notes view.** All voice notes across the catalogue, transcribed and searchable. Each row plays back the audio and shows the attached fly's thumbnail and pattern name.
11. **Sharing & invitations.** Modal: "Invite a fishing partner or co-tier to see this catalogue". Magic-link email; per-box edit / view permissions on arrival.
12. **Print / export.** Side-by-side typeset preview. Choose: by box, by tradition, by hook size. Toggle: include suggested water-types, include voice-note transcripts, include morphology summaries, include tradition-card illustrations. Export PDF; opt-in CSV for club / shop use.
13. **Footer.** "Made for the shelf above the vice." Privacy: "Your catalogue is yours. We never train on it. The photographs of your flies stay in your Firebase project." 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 Tackle Box."
- Subhead: "Turn a drawer of home-tied flies into a catalogue — in any tradition, any hook size, with the photograph of every fly preserved beside its record."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic photo-cataloguing app: it reads what's on the hook, it names patterns in their own tradition (North-Country soft-hackles in their English vocabulary, tenkara kebari in Japanese romanisation, Patagonian streamers in Spanish where appropriate), it estimates hook size honestly with ±, and it never claims to know the tier's intent.
- Visual: a small annotated illustration of a tin tray of six flies with the relevant labels (pattern name, hook size, tradition tag, materials chips) — not a generic tackle icon.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample drawer".
- A live demo input pre-loaded with three sample trays from the seed content in section 8a.
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the calibration coin → ±1 hook-size estimate, the Sakasa Kebari named in Japanese, the saltwater Gotcha read as bonefish-flat).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample drawer in `/data/seed-catalogue/` for your own tray photographs."
- "Adjust the tradition enum in `/server/schemas/fly.ts` if you tie a regional tradition we missed."
- "Wire up your Gemini API key and Firebase project via the env-var list in the capabilities panel."
- Primary CTA: "Use this template" → links to AI Studio Build remix entry point.
- Secondary: "Just exploring — close" (sets localStorage flag, never auto-shows again).
**Accessibility:** focus trap, `Esc` closes, `role="dialog"`, `aria-modal="true"`, `aria-labelledby`, focus restored to trigger on close. Respect `prefers-reduced-motion`.
**Don't:**
- Don't gate content behind the modal. The page beneath must be fully usable.
- Don't auto-reshow on return visits. Use `localStorage['onboarding-seen-v1']`.
- Don't include unrelated CTAs (newsletter signup, social follow). Keep it about the template only.
## 6c. Capabilities info button (persistent in header)
Add a persistent `(i)` icon in the top-right of the header (next to the primary nav). Click → opens a modal/panel titled **"What powers this app"**.
**Panel contents (in this order):**
**Gemini capabilities used (the hero list):**
- **Gemini 3.5 Flash (multimodal)** — reads a tray of forty flies in one photograph and returns one structured record per fly. Handles foam grids, tin trays, magnetic boxes, and open display trays in warm desk-lamp light or grey north-window light.
- **Gemini 3.5 Flash (tradition-aware naming)** — names patterns in the conventional vocabulary of the inferred tradition: North-Country spiders in their English names, tenkara kebari in Japanese romanisation, fully-dressed salmon classics preserved verbatim, Patagonian streamers in Spanish where the tradition is Spanish.
- **Gemini 3.5 Flash (long context)** — once your catalogue grows past a few hundred flies, the semantic-search view sees the whole catalogue at once. "Show me everything with a CDC wing tied since the start of this year" reads every record together.
- **Gemini 3.5 Flash + grounded search** — opt-in, per record. Attaches a single authoritative citation (museum, club, federation) to a confirmed pattern.
- **Gemini TTS** — reads a catalogue entry aloud at the vice, at the tier's reading pace, with abbreviations pre-expanded ("PTN" → "Pheasant Tail Nymph").
- **Nano Banana 2** — generates a small restrained tradition-card illustration for each box cover. Never used to alter or "improve" the photographs of your own flies.
- **Firebase Auth** — Google and Apple sign-in, fishing-partner invitations via magic links. **Apple sign-in requires you to wire up an Apple Developer Service ID in Firebase Auth.** **Magic-link email requires sender-domain authorisation in Firebase Auth** — both are one-time configuration in the Firebase console.
- **Firestore** — stores your catalogue, syncs across devices in real time.
- **Firebase Storage** — keeps the original tray photographs and per-fly crops at upload resolution, forever. **You must enable Firebase Storage in your Firebase console before first upload — AI Studio Build does not auto-provision it.**
- **Cost note** — see the detailed breakdown in 6d. A typical drawer of 400 flies, photographed across 12 trays, costs about $0.40 of Gemini API spend, total, processed once.
- **Privacy note** — your catalogue is private to you and the partners you invite. This app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The opt-in pattern-citation feature is opt-in per record, not global.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b
- Email: see section 4b
- Payments: see section 4b (not used in v1)
- External APIs: see section 4b
**Environment variables you'll need to configure:**
- `GEMINI_API_KEY` — your Google AI Studio API key
- `FIREBASE_PROJECT_ID` — your Firebase project id
- `FIREBASE_SERVICE_ACCOUNT` — service-account JSON (server-side only)
- `FIREBASE_STORAGE_BUCKET` — the storage bucket you enabled in the Firebase console
- `STRIPE_API_KEY` — only if you wire up the future printed-tackle-book tier
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: long-context semantic-search calls are billed per token of input — a 400-fly catalogue semantic-search call costs about $0.04 per query.
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this catalogue forever" — gone in 60 seconds), what is never sent for training.
**Documentation links:**
- AI Studio Build docs
- Gemini API multimodal, long-context, TTS, Nano Banana 2 docs
- 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)
- **Parse tray photograph (Gemini 3.5 Flash, medium thinking)** — typical tray of 32-40 flies as one image, ~1,800 output tokens. ~$0.012 per tray. A drawer of 12 trays ≈ $0.15.
- **Suggested chips refinement (Gemini 3.5 Flash, low thinking)** — runs per fly after parse, ~$0.0003/fly. A 400-fly catalogue ≈ $0.12 total, one-time.
- **Pattern-citation enrichment (Gemini 3.5 Flash + grounded search)** — opt-in, per record. ~$0.001 per record. If the tier enables it on 50 records ≈ $0.05.
- **Semantic search (Gemini 3.5 Flash, low thinking, long-context, archive-wide)** — runs per query. ~$0.04 per query on a 400-fly catalogue. A heavy week ≈ 20 queries ≈ $0.80/week.
- **Voice-note transcription (Gemini 3.5 Flash, low thinking)** — ~$0.0003 per 30-second note.
- **TTS narration (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A 60-word read-aloud per fly ≈ $0.001 per fly. Cached per fly; charged once.
- **Tradition-card illustration (Nano Banana 2)** — ~$0.03 per image. A catalogue with 6 distinct traditions ≈ $0.18 total, one-time.
- **Expected per-fly cost on first ingest:** ~$0.0008. **Drawer of 400 flies total:** ~$0.32. **Ongoing weekly query budget:** ~$0.80/week heavy use, well under $0.10/week light use.
- **Image storage:** Firebase Storage standard tier, ~$0.026/GB/month. A high-res tray photograph is ~4 MB; per-fly crops are ~150 KB each; a 400-fly catalogue uses ~600 MB ≈ ~$0.016/month.
## 7. Design language
- **Mood:** A tying bench at evening, the head-lamp on, a mug of tea cooling beside the vice, a tin tray on the table, a small notebook open to a page of pencilled notes. Not a tech product. Not an outdoor brand store. The kind of considered, honest tool a careful person keeps within reach.
- **Typography:** Display serif for catalogue headings and pattern names (Source Serif Pro or Adobe Caslon Pro). A handwriting-styled accent (sparingly) only for the tier's own annotations and voice-note transcript captions — never for the parsed pattern name itself, which sits in serif. Clean grotesque for app chrome and chip labels (Inter or Geist).
- **Palette:** Bench-wood background `#F1EAD9` for the catalogue background, deep ink `#1E1813` for body text, antique brass `#8A6A2A` for hook-size chips and tradition badges, faded sage `#5E6E54` for confirmed water-type chips, warm rust `#9B4A2C` only for "needs your review" callouts. A muted slate `#3F4A57` for app chrome. Borrowed from a tying-bench's lamplit wood and a fishing tackle catalogue from 1958, not from SaaS design systems.
- **Imagery:** The photographs of the flies are the hero. Never replace them; never crop them tighter than the user did. Tradition-card illustrations are clearly stylised and pencil-toned — never confused with the photographs in the catalogue. Tray photographs sit in the layout as the artefacts they are: corners, edges, tray-rim shadows, all preserved.
- **Hand-feel touches:** A barely-visible paper grain on the catalogue background. The "show original tray" expandable panel slides the tray photograph in with a thin shadow — like lifting a tray off the bench. Hover on a fly's bounding box in the tray view reveals its row; never aggressively glow.
- **Spacing:** consistent 4-px base. Generous whitespace — the catalogue needs air.
- **Radius:** consistent token set (e.g. 6 / 12 / 20 px). Fly rows use 6; box covers use 12; the welcome card uses 20.
- **Shadows:** subtle, layered, warm-toned. Avoid heavy drop-shadows.
- **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. No bouncing splash animations. The tray-to-row highlight (tap a fly's bounding box in the tray photo → its row glows below) is the one place where motion carries meaning; respect reduced-motion by jumping rather than animating.
- **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Photograph the first tray to start").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible pattern names, hook sizes, materials, voice-note transcripts, and tier names that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on the conventions of each tradition — North-Country names for English soft-hackles, Japanese romanisation for tenkara, Spanish for Patagonian, classical Victorian names for fully-dressed salmon — but never claim that a fictional fly is a real, photographable specimen from a real collection.
- Tone: warm, direct, free of corporate language. This template is for a person at a vice, not a brand.
- Headlines: punchy and concrete. No 'Empower your X' filler. No 'Revolutionize'. No 'Seamless'.
- Body copy: short paragraphs (2-4 sentences). Use lists where appropriate.
- Plain language. Avoid jargon — except where the user already speaks the jargon (the chalk-stream user wants to see "PT, gold-bead, #16"; the tenkara user wants to see "Sakasa Kebari, futsū hackle").
- Where the app outputs AI-generated content, never label it as "AI says" — let it speak naturally. Use small uncertainty cues only where epistemic honesty requires them (a low-confidence hook size shows with the ± visible; suggested chips show with a "tap to confirm" treatment).
## 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 catalogues (sidebar):**
- "The shelf above the vice" (401 flies across 12 trays, contributor: me) — primarily chalk-stream dries and PTNs, with one tin of North-Country spiders and one box of articulated streamers. England, 2014–2026.
- "Otō-san no Sorachi-bako" (148 kebari across 6 trays, contributor: me) — inherited from a father who fished the Sorachi river in Hokkaidō for thirty years. Tenkara Sakasa Kebari and Futsū Kebari, dark thread bodies, partridge and pheasant hackles. Inheritance mode on.
- "Cordillera Patagonia, off-season 2025" (2,140 flies across 64 trays, contributor: me + apprentice Tomás) — a guide's working inventory from San Martín de los Andes, primarily Mosca Caballero, Wooly Buggers, articulated streamers, with a smaller tin of dries for the late-season hatches.
- "Florida flats" (188 flies across 8 trays, contributor: me + my wife) — saltwater patterns for bonefish, tarpon, permit. Gotchas, Crazy Charlies, Tarpon Bunnies, Merkin crabs.
- "Abuelo's Cuban coast box" (24 flies across 1 tray, contributor: me) — inherited from a grandfather who tied for the Cuban coast pre-1962. Morphology-only, no water-type suggestions, no season suggestions, until my father visits and reads them with me.
**Sample tray in detail view (this is what the demo should show):**
- **Tray ID:** `tray-001-shelf-above-vice`
- **Tray layout detected:** `foam-grid`
- **Calibration object detected:** present — "1-pound coin in upper-left corner of tray photo"
- **Fly count detected:** 40
- **Tray-level notes:** "two slots empty in lower-right, possibly recently fished"
- **Sample fly #1 in the tray:**
- **Pattern name (primary):** "Pheasant Tail Nymph, gold-bead head"
- **Pattern name alternatives:** ["PTN, gold-bead", "Sawyer-style PT with bead"]
- **Tradition tag:** `regional-other` (modern bead-head variant of a British classic)
- **Tradition confidence:** 0.88
- **Hook:** size 16, ±1, shape `1xl-nymph`, bead `tungsten-bead`, calibration "1-pound coin"
- **Materials:**
- body — "pheasant tail fibres wrapped tightly, natural reddish-brown", appears_synthetic false, appears_dyed false
- rib — "fine copper wire", appears_synthetic true, appears_dyed false
- tail-fibres — "pheasant tail fibres, four", appears_synthetic false, appears_dyed false
- thorax — "hare's-ear dubbing, natural mottled", appears_synthetic false, appears_dyed false
- bead-or-cone — "gold tungsten bead, 2.5mm approx", appears_synthetic true, appears_dyed false
- **Visible features:** ["gold tungsten bead-head", "tightly-wrapped pheasant-tail body", "hare's-ear thorax", "copper rib"]
- **Morphology summary (one sentence):** "Size 16 weighted nymph with a gold tungsten bead-head, tightly-wrapped pheasant-tail body, fine copper rib, and a mottled hare's-ear thorax."
- **Weight class:** `nymph-medium`
- **Suggested water types:** `freestone-river`, `chalk-stream`, `tailwater`
- **Suggested seasonality:** northern hemisphere, Mar–Sep, reasoning "general-purpose nymph; year-round below ground but most-fished in spring through autumn"
- **Suggestions disclaimer:** "Suggested by pattern only — confirm with your water and your season."
- **Reading confidence:** 0.91
- **Flagged for user review:** [] (none)
- **Sample fly #2 in the tray (a Sakasa Kebari from the Otō-san archive):**
- **Pattern name (primary):** "Sakasa Kebari"
- **Pattern name alternatives:** ["Sakasa Kebari (reverse-hackle kebari)"]
- **Tradition tag:** `tenkara-sakasa-kebari`
- **Tradition confidence:** 0.95
- **Hook:** size 12, ±1, shape `standard-wet`, bead `none`, calibration "1-yen coin lower-right"
- **Materials:**
- body — "dark olive thread, segmented with tying thread itself", appears_synthetic true
- hackle — "soft partridge hackle, leaning forward over the eye, reverse-mounted", appears_synthetic false, appears_dyed false
- head — "small thread head, dark olive, lacquered", appears_synthetic true
- **Visible features:** ["reverse-mounted partridge hackle leaning forward", "dark olive thread body", "no tail", "no rib", "lacquered thread head"]
- **Morphology summary:** "Size 12 reverse-hackle tenkara kebari with a dark olive thread body and soft partridge hackle wound to lean forward over the hook eye."
- **Weight class:** `wet-traditional`
- **Suggested water types:** `tenkara-headwater`, `mountain-stream`, `freestone-river`
- **Suggested seasonality:** northern hemisphere, Apr–Oct, reasoning "tenkara general-purpose kebari, mountain-stream season"
- **Tied-by default:** "Inherited from Otō-san (Sorachi river)"
- **Reading confidence:** 0.94
- **Sample fly #3 in the tray (a Gotcha from the Florida flats archive):**
- **Pattern name (primary):** "Gotcha"
- **Pattern name alternatives:** ["Gotcha bonefish fly, pale tan"]
- **Tradition tag:** `saltwater-bonefish`
- **Tradition confidence:** 0.91
- **Hook:** size 6, alt-scale null, ±1, shape `saltwater-stainless`, bead `metal-eye-bead-chain`
- **Materials:**
- body — "pearl mylar tinsel wrapped over hook shank", appears_synthetic true
- wing — "pale tan craft fur over the hook, sparse", appears_synthetic true, appears_dyed false
- eyes — "small bead-chain eyes, silver", appears_synthetic true
- head — "tan thread head, lacquered", appears_synthetic true
- **Visible features:** ["pearl mylar body", "pale tan craft-fur wing", "bead-chain eyes", "sparse profile"]
- **Morphology summary:** "Size 6 saltwater bonefish fly with a pearl mylar body, sparse pale tan wing, and bead-chain eyes."
- **Weight class:** `streamer-sinking`
- **Suggested water types:** `salt-flat`
- **Suggested seasonality:** tropical, year-round, reasoning "Caribbean / Florida flats pattern, fished year-round"
- **Reading confidence:** 0.93
**Sample input artefacts (for the build to demonstrate):**
- A foam-grid box tray of 40 chalk-stream nymphs and emergers, photographed on a wooden bench under warm overhead light, with a 1-pound coin in the upper-left corner of the tray photograph as a calibration reference.
- A tin tray (Wheatley-style) of 24 tenkara kebari from an inherited Hokkaidō archive, with a 1-yen coin in the lower-right corner.
- An open display tray of 12 fully-dressed Atlantic salmon classics in a Scottish ghillie's drawer, lit by a window's grey north light.
- A compartmented plastic saltwater box of 32 bonefish patterns photographed on a varnished dock-side bench.
- A binder leaf of 8 Patagonian streamers stuck through a foam strip, photographed from above against a kitchen tablecloth.
**Sample voice copy:**
- Onboarding: "Photograph one of your trays. We'll read every fly — even the ones you tied a decade ago and can no longer name."
- Processing: "Detecting the tray layout…" / "Reading each fly…" / "Identifying patterns…" / "Estimating hook sizes…" / "Drafting suggested chips…"
- Empty catalogue: "This catalogue is waiting for its first tray. Photograph a tray, with a coin in one corner for scale, to start."
- Error (couldn't read): "We couldn't make out enough of this tray to read it confidently. Want to try a clearer photo, or upload one tray at a time?"
- Save confirmation: "Added to The shelf above the vice — 40 flies catalogued from tray 1."
- Suggested chips: "Suggested by pattern only — tap to confirm with your water and your season."
- Inheritance flag: "This fly is from an Inherited archive. Water-type and season suggestions are hidden by default until you're ready."
- Low confidence note: "Hook size is an estimate (±2). Add a calibration coin to your next tray photo for tighter estimates."
**Sample fishing-partner invitation email subject + body:**
- Subject: "Tomás — adding you to the Cordillera 2025 catalogue. Boxes 14–22 are yours."
- Body: "Tomás — I've added you as a co-tier on the Cordillera 2025 catalogue. Boxes 14 through 22 (your streamers and Patagonia dries) are yours to edit. I can see your counts in the trip-planning view. Tap to join." [Open Catalogue]
**Sample voice notes:**
- Attached to a Pheasant Tail Nymph: "This is one of the dozen I tied for opening day on the Itchen, last February."
- Attached to a Sakasa Kebari: "Otō-san tied this. The hackle is from the partridge he hunted near the Sorachi the autumn before he died."
- Attached to a Mosca Caballero: "Five-second clip — my apprentice Tomás's first attempt at a Caballero. The proportions are off but the materials are right."
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a tying vice on a wooden bench at evening, with a tin tray of half-tied flies on the desk, a head-lamp's warm cone of light across the bench, and the dark grain of the wood visible. Generate via Nano Banana 2 with a prompt emphasising "wooden tying bench, warm head-lamp light from upper-left, tin tray with a row of finished olive nymphs, no people in frame, late evening, soft shadow under the vice".
- **App icon / wordmark:** Set in the display serif. Slightly worn-bench texture behind it. No icon — just type.
- **Empty-state illustration:** A simple line drawing of an empty foam-grid tray with one finished fly in the upper-left slot. Hand-drawn aesthetic, not a flat icon.
- **Demo tray photographs:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "directly-above view, warm desk-lamp light, foam-grid tray of forty finished trout flies in even rows, a 1-pound coin in the upper-left corner of the tray, wooden bench background, no human hands in frame, no glossy AI render". Each demo tray should look photographed, not rendered.
- **Tradition-card illustrations:** One per tradition the user has flies in, generated by the dedicated Nano Banana 2 call per the system instruction in 4b. Pencil-and-wash style, always profile view, head to the left, no captions in the image.
- **Stock fallbacks:** If image generation fails, fall back to the photographed sample tray from `/public/samples/sample-tray.jpg`. Never to a "🎣" emoji.
- **Generated imagery:** prefer Nano Banana 2 over stock photography. Prompt for warmth, asymmetry, and slight imperfection — avoid the glossy 'AI render' look.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift.
- **Icons:** `lucide-react` for UI. Use sparingly — never decorative-only.
### Build-time asset manifest (explicit specs)
Every image, illustration, and visual reference mentioned above must resolve to ONE of the three buckets below — runtime-generated, seed-shipped, or user-supplied. Do NOT ship `` tags whose `src` is not listed here. Do NOT depend on bare "section 8a prompts" without binding them to explicit paths and model IDs.
**Bucket 1 — Runtime-generated (Nano Banana Pro `gemini-3-pro-image` for hero/demo photographs; Nano Banana 2 `gemini-3.1-flash-image` for in-app illustrations and reference-conditioned variants).** Cached to Firebase Storage; served via signed URL. Every reference above to "Nano Banana 2" or "Nano Banana Pro" MUST be wired to one of these specific calls with an explicit model id:
- `/public/generated/hero.webp` (2400×1500, WebP) — model `gemini-3-pro-image` — uses the literal prompt described as "Hero image (landing screen)" above. Run once at build; commit a `/public/samples/hero-fallback.webp` (1600×1000) generated from the same prompt with `gemini-3.1-flash-image` so the page renders if quota is exhausted.
- `/public/generated/demo/{demo-slug}-{NN}.webp` (1600×1200, WebP) — model `gemini-3.1-flash-image` (reference-conditioned where the prior frame is passed as input) — one path per "Demo X" image referenced above. The slug derives from the seed example in section 8a; the NN index covers each frame in the demo sequence.
- `/public/generated/illustrations/{name}.webp` (1024×1024, WebP) — model `gemini-3.1-flash-image` — one path per named illustration above ("Empty-state illustration", "Recipe-card hero illustrations", "Curriculum picker imagery", "Period-style frames", etc.). Each illustration's prompt is the literal description above; ship a deterministic seed in the request so re-runs are reproducible.
**Bucket 2 — Seed assets shipped with the deliverable.** Every "Stock fallback" path referenced above (e.g. `/public/samples/sample-X.jpg`) is generated once via Nano Banana 2 (`gemini-3.1-flash-image`) at 1024×1024 WebP using the same prompt as its Bucket-1 counterpart, then committed to the repo so the page renders identically if Gemini quota is exhausted or the user is offline. Replace any `.jpg` extension above with `.webp` to match the optimisation rule. Also commit these empty-state seeds (1024×1024 WebP, single-stroke hand-drawn line, no colour fill):
- `/public/samples/empty-state-primary.webp` — line drawing of the app's primary empty surface (the named "Empty-state illustration" above), generated from that exact prompt.
- `/public/samples/empty-state-archive.webp` — line drawing of an empty saved/archive view, single-stroke outline.
- `/public/samples/empty-state-error.webp` — line drawing of a hand placing a single object aside with care, used when an AI call fails.
**Bucket 3 — User-supplied.** Uploads from the user's camera / file picker land at the Firebase Storage path conventional for this template (named in section 4b). The build ships with Bucket-1 + Bucket-2 only; no user-supplied images at first paint.
**Hard rules**
- Every `` tag MUST have a `src` that resolves to a path listed in Bucket 1, Bucket 2, or a Bucket 3 upload path. Anything else is a build error.
- No bare `image.jpg` / `hero.jpg` / `placeholder.png` references anywhere in the code.
- Model IDs: `gemini-3-pro-image` for hero-quality photographic generation; `gemini-3.1-flash-image` for in-app illustrations, reference-conditioned variants, empty-state seeds, and stock fallbacks. Never use a legacy model id (no `imagen-*`, no `gemini-1.5-*-image`).
- File format: WebP everywhere (AVIF acceptable where the target browsers support it). No `.jpg` / `.jpeg` / `.png` in `/public/samples/`.
## 10. Interactivity & states
- Every interactive element has hover, focus, active, and disabled states.
- Forms validate inline and show specific error messages (not "Invalid input").
- Loading states use skeletons that match the eventual layout, not spinners.
- Empty states explain the next action with a button whose label fits THIS app's domain: "Photograph the first tray", "Drop a previously-scanned tray PDF", "Invite a fishing partner" — never a generic "Add your first item".
- Smooth scroll for in-page anchors.
- All AI-generated content streams in token-by-token where supported, with a clear "thinking…" indicator before content starts arriving.
- If an AI call fails, show a calm, specific error ("We couldn't make out enough of this tray to read confidently — try a clearer photo, with a coin or a ruler in one corner for scale?") and offer retry.
- Suggested chips (water-type, season) are rendered in a distinct visual state — a soft dashed outline and a "tap to confirm" affordance — that is clearly suggestion, not classification.
- Hook-size chips always show the ± explicitly when it is greater than 0. A confirmed size shows without ±.
- The tray-to-row highlight (tap a fly's bounding box in the tray photo → its row highlights below) takes 400 ms with `prefers-reduced-motion` falling back to instant.
## 11. Tech & responsive requirements
- **TTS markdown-stripping preprocessor:** before sending any user-authored markdown to `gemini-3.1-flash-tts-preview`, strip non-spoken markdown: `#`/`##`/`###` headings (keep the title text), `**bold**` (keep the inner text), `[label](url)` (keep `label`, drop URL), `` ``` `` fenced code blocks (skip entirely), `>` block-quote markers (keep the text), and `|` table pipes (read row-by-row as sentences). Insert `…` between sentences for a short pause and a blank line plus `—` between paragraphs for a long pause. The model does not understand markdown; raw markdown will be read aloud as literal characters ("asterisk asterisk").
- **File downloads on Safari / Firefox:** when offering local-disk save of any export (PDF, CSV, MP3, ZIP, JSON, image), fall back to `` with a blob URL — the File System Access API (`showSaveFilePicker()`) is Chromium-only. Detect with `'showSaveFilePicker' in window`; otherwise use the anchor-download path.
- **Stack:** React + TypeScript + Tailwind CSS. Functional components + hooks. Use Shadcn UI primitives where appropriate.
- **Build runtime:** AI Studio Build — full-stack with Cloud Run server-side functions. All Gemini API calls happen server-side; API key lives in Secrets Manager, never in client bundle.
- **Model selection:** explicitly pin `gemini-3.5-flash` for tray-parse / chip-refine / semantic-search and `gemini-3.5-flash` for voice-transcribe / pattern-citation. Set `thinkingLevel` explicitly per call.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed catalogue on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as fallback (requires sender-domain authorisation as noted in 4b).
- **Storage:** Firebase Storage for original tray photographs and per-fly crops. Pre-signed URLs only. Enable manually in Firebase console before first upload.
- **Mobile-first.** Verify layouts at 375 px (iPhone SE), 768 px (iPad), 1024 px, 1440 px+.
- Use `clamp()` for fluid typography. Prefer container queries over media queries for component-level responsiveness.
- Use `dvh` / `svh` instead of `vh`. Respect safe-area insets on iOS.
- Zero horizontal overflow at any width. Zero layout shift on load.
- Persist user data in Firestore. Use real-time listeners on the catalogue view.
- Optimistic UI on writes; reconcile on response.
- Tray-capture flow uses the Web Camera API with fixed focus/exposure where supported; falls back to native camera otherwise.
- **iOS Safari gotchas (graceful degradation):** camera permission does NOT persist across page reloads on iOS — re-request on every tray scan; backgrounded Safari tabs pause `getUserMedia` — re-acquire the stream on `visibilitychange`; on Low Power Mode iOS may degrade resolution — always offer `` as a fallback so a tray photo still uploads when WebRTC is denied; rotation drops the camera track on iOS — re-bind on `orientationchange`.
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`.
- All interactive controls reachable by keyboard with a visible focus ring.
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components.
- All images have meaningful `alt` text. The original tray photographs have `alt` describing the artefact ("photograph of a foam-grid tray of 40 finished trout flies on a wooden bench, with a 1-pound coin in the upper-left corner for scale").
- Form fields have associated `