================ 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.
---
# Library Cart
## 1. Project
**Library Cart** is a community-library cataloguing app for volunteers
who have inherited a mountain of donated books and no system. The
volunteer wheels a cart up to the shelf, holds the phone in landscape,
and photographs a row of spines. The app reads each spine, resolves
each book to its real ISBN (or flags the ones it can't), looks up
title, author, year of publication, language, and existing Library of
Congress / OCLC subject headings, and assigns a Dewey Decimal
classification down to two decimal places. Then it prints the catalogue
card and the spine label, both in a font and size that match the
shelves already on the wall. The volunteer pushes the cart, photographs
the next shelf, and at the end of the afternoon the new shelf has
labels — and the corner of the back room has a stack of cards in a
real library tray, ready to be filed.
This is the kind of app a Pakistani-Canadian volunteer at the Newton
branch in Surrey builds on a Saturday morning when her board has
asked her to make sense of two thousand donated paperbacks before
the grand reopening. It is also the kind of app a Korean-American
retired teacher at a community centre in Flushing builds when the
local seniors group has given her three banker's boxes of Korean
fiction, English ESL workbooks, and Spanish-language children's
picture books — and she wants the kids who walk in after school to
find what they're looking for. Same shape of moment, different city,
different stack of books.
The single demo that proves the magic: the Surrey volunteer holds her
phone in landscape over the second cart-row of the morning — spines
up, twenty-eight books across, mostly English paperbacks with three
Punjabi religious texts at the right end. Forty-five seconds later
the screen fills with twenty-eight catalogue rows. Each row shows
the spine crop, the resolved ISBN, the title and author from the
verified record, the language, the Dewey number (`813.54` for the
adult fiction near the middle, `J Fic` short codes for the children's
chapter books at the left, `294.6` with the branch's two-decimal
sub-precision for the Punjabi religious texts), and a small green
tick or amber flag. Three books are flagged: one English paperback
because the spine photograph was angled and the OCR is uncertain,
one Punjabi text because the spine has no ISBN visible and no clean
match was found in the metadata catalogue, and one English novel
because two candidate ISBNs both look plausible — a U.S. first
hardcover and a U.K. first trade paperback — and the volunteer needs
to pick. She taps the flagged rows, fixes them in under a minute,
and prints the labels.
The same shelf elsewhere proves the breadth: the Queens volunteer's
fourteen-book Korean fiction shelf, with Hangul vertical spines and
two sun-faded older paperbacks the app flags for grounded-search
disambiguation; the Pilsen volunteer's row of nine Spanish-language
children's picture books shelved cover-up, every one flagged
`cover-up-no-spine-text` so the volunteer photographs each front
cover and the picture-book catalogue assembles itself row by row.
**Tagline:** _Photograph a shelf, get a Dewey-coded catalogue — in any branch, any language, with every uncertain match flagged, never guessed._
## 2. Target audience
- Volunteer librarians at community libraries that grew out of donation drives — neighbourhood, faith-community, immigrant-services, prison, hospital, refugee-camp, and rural-village libraries with no MARC-record budget
- Newly-arrived diaspora communities building a community library in a borrowed church basement or community centre — Pakistani, Korean, Tagalog, Vietnamese, Mandarin, Amharic, Tamil, Punjabi, Bengali, Urdu, Khmer, Farsi, Swahili, Spanish, Portuguese, Polish, French collections
- School librarians inheriting a classroom library from a retiring colleague who never catalogued anything
- Little Free Library stewards and book-swap organisers who want to track what's coming and going across a year
- Prison-library volunteers and hospital-library cart pushers who need fast, low-budget cataloguing on a shared tablet
- Estate executors cataloguing a deceased relative's personal library for donation, sale, or division among heirs
- Rural and remote village libraries in low-connectivity regions where a one-shot photograph in town with offline-queue sync at home is the practical workflow
- Faith-community archivists cataloguing a mosque library of Arabic religious commentary, a temple library of Tamil and Sanskrit texts, a synagogue library of Hebrew and Yiddish, a parish library of Polish and Spanish liturgy
- Children's librarians cataloguing a multilingual children's collection where the spine photograph and the cover photograph carry equal weight (picture books often have no spine title)
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **Reads any spine, any script** — modern paperbacks, sun-faded clothbound hardcovers, library-rebinding tape with handwritten titles, Korean Hangul vertical spines, Tamil and Bengali bottom-to-top, Arabic right-reading, Cyrillic, Devanagari, Hebrew, Japanese vertical kanji, Chinese vertical traditional and simplified, Amharic Ge'ez, Khmer. Gemini 3.5 Flash reads the spine, identifies the script, transliterates if helpful, and emits a structured record per book in a single multimodal call per shelf.
- **Verifies, never invents** — every spine read is checked against the Open Library, Library of Congress, Google Books, and ISBNdb metadata catalogues before the row is marked confident. A spine that reads "John Updike — Rabbit, Run" is matched to ISBN `9780449911655` only because the title + author + edition + publisher signals all align. When they do not align, the row is **flagged for the volunteer**, never silently mapped to a best guess.
- **Assigns Dewey down to the right precision** — the app picks the precision the branch actually uses. Some branches catalogue to two decimals (`813.54`); some use short branch codes (`Fic UPD`); some use Dewey to one decimal plus an author cutter; children's collections use a simpler `J Fic` plus the author's last three letters. The branch's convention is configured once in Settings and applied everywhere.
- **Multilingual collections, no second-class treatment** — the Korean-language fiction shelf is catalogued with the same precision as the English-language shelf. The app's UI is bilingual where the volunteer prefers, and the print labels render the title in the book's original script, with a romanised transliteration below when the branch's convention requires it.
- **Spine labels and catalogue cards that match what's already on the shelves** — the app's print output uses the branch's existing label stock (Avery 5160, 5161, or the branch's custom die-cut), the font and size already on the older shelves, and the catalogue-card layout the branch's tray already holds. New labels and old labels are visually indistinguishable.
- **Flagged rows are first-class citizens** — uncertain ISBNs, mismatched candidates, missing-from-catalogue books, and damaged spines all sit in a "Needs Your Eyes" tab. The volunteer's afternoon is structured around clearing flags, not around scanning. The app earns trust by telling the volunteer when it isn't sure.
- **Offline-first, sync-later** — a Saturday morning at a community centre with patchy wifi works. The photograph queues; the parse happens when a connection returns. The print output works against the local cache.
- **Donation log and weeding log** — every book added is logged with a donation date and (optionally) the donor's name (donor consent gated). Every book weeded is logged with a reason ("duplicate", "damaged beyond repair", "out of scope for our collection"). The annual report writes itself.
## 4. Features to build
- Camera capture in landscape with a shelf-shaped crop guide; the viewfinder shows a per-spine bounding-box overlay in real time
- Multi-shelf batch mode — photograph the wall, the app segments the rows; for stacks-of-books-on-a-cart, photograph in tall portrait and the app segments the stack
- Spine-up vs cover-up vs front-cover capture modes — children's picture books typically have no usable spine and are photographed cover-up
- Upload from photo library, scanner-app PDF, or a backup of previously-exported catalogue CSV
- Multimodal parse of a shelf photograph in one Gemini 3.5 Flash call — script detection, spine segmentation, OCR per spine, language detection per book, candidate-title proposal per book
- Verification pass — each candidate book is resolved against Open Library, LoC, Google Books, ISBNdb; the strongest match wins only when multiple catalogues agree
- Dewey assignment — the Gemini 3.5 Flash call uses the book's verified subject headings (LCSH, BISAC, Dewey-Edition-23 ranges) to propose a Dewey number; the branch's precision setting trims it
- Branch convention configuration — Dewey precision, label stock, font, catalogue-card layout, language order on bilingual labels, romanisation system per script (Revised Romanisation for Korean, Hepburn for Japanese, ALA-LC for Arabic, etc.)
- Flagged row UI — every uncertain row sits in a "Needs Your Eyes" inbox; the volunteer can fix the ISBN, accept an alternate candidate, mark the book "no catalogue record, original record me", or weed it
- Original-record-me flow — for books with no catalogue record anywhere, the volunteer enters title / author / publication year / Dewey by hand, with the app's suggestions one tap away
- Donation log — each batch records donor name (optional, consent-gated), donation date, accession number; donor receives a thank-you email if they consented
- Weeding log — books removed from the collection are logged with reason and an optional photograph of the damaged copy
- Donor opt-in flow — donors can sign a single-page consent form that says how their name will appear (or not) in the public catalogue
- Spine label printing — Avery and custom die-cut formats; preview-before-print; reprint a label any time
- Catalogue card printing — 3×5 traditional and modern minimal layouts; bilingual cards where required
- Public catalogue export — the branch's collection rendered as a searchable static site (for the branch's web page) and as a printable shelf list
- Library-system handoff — export records as MARC21 XML for branches that later move to Koha, Evergreen, or a vendor ILS
- Search across the collection — by title, author, language, Dewey range, subject heading, donor, accession date, branch, "books I weeded last year"
- Volunteer roster — track who catalogued what; useful for branches who want to credit specific volunteers in their annual report
- Reading-level overlay (children's collections) — the app reads the back cover or the publisher's metadata for the reading level (Lexile, AR, DRA) and adds it to the label
## 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 shelf photograph end-to-end: segments the spines, OCRs each one in its native script (Hangul, Tamil, Devanagari, Arabic, Hebrew, traditional and simplified Chinese, Japanese, Cyrillic, Latin, Ge'ez, Khmer), detects the language of each, and proposes a candidate title + author. One API call per shelf photograph; stacks-of-books-on-a-cart are submitted as a single tall image.
- **Structured output / JSON Schema** — the response matches the `ShelfParse` schema below. Every field is typed; the schema is included verbatim in the system instruction and passed as `responseSchema`.
- **Search grounding** (Gemini 3.5 Flash with `google_search`) — used for two things: (1) verifying the candidate title + author against Open Library and Library of Congress for the long-tail books that the structured-catalogues call misses, and (2) resolving a sun-faded spine where only "abbey" and "1962" are legible. Grounded search prevents hallucinated ISBNs. **Grounding citations come from `response.groundingMetadata.groundingChunks[].web.uri` — not the JSON body. `responseSchema` and `google_search` are mutually exclusive in one call.**
- **Long context (1M tokens)** — the "weed our collection" call runs over the entire branch's catalogue at once to find duplicates, near-duplicates (same book, different edition), and out-of-scope holdings. **Guardrail:** a typical book record is ~600 tokens; a 2,000-book branch ≈ ~1.2M tokens — already over the 1M ceiling. For branches above 1,500 books, chunk by Dewey range (000-099, 100-199, …) before the weeding call. The app shows the builder a warning when the branch crosses 1,500.
- **Thinking levels** — `medium` for the primary shelf-parse call (segmentation + multi-script OCR + candidate proposal). `low` for the ISBN-verification call (it's a structured lookup with a clear right answer). `medium` for Dewey assignment (the subject-heading-to-Dewey mapping benefits from reasoning, especially in the 200s religion ranges and the 800s literature ranges where the precision matters). `low` for the donation-log and weeding-log calls.
- **Negative space — what we do NOT use Gemini for** — we do not use Gemini to look up the ISBN's bibliographic record itself; that comes from the Open Library / LoC / Google Books / ISBNdb APIs, which are authoritative. Gemini reads the spine, proposes the candidate, and arbitrates when multiple catalogues disagree. Using Gemini to *generate* an ISBN is the failure mode this template most carefully avoids.
### 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 volunteer invitations and donor consent emails) requires the sender domain to be authorised in Firebase Auth. Branch catalogues are private to the branch members; volunteer roles (admin, cataloguer, viewer) are scoped per branch.
- **Database — Required.** Firestore for `users`, `branches`, `books`, `shelves`, `donations`, `weeding_log`, `volunteer_roster`, `flags`.
- **File storage — Required.** Firebase Storage for shelf photographs, individual spine crops, label PDF previews, and printed-catalogue archives. **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 photograph upload. Pre-signed URLs only.
- **Email — Required (transactional).** Donor thank-you emails (when consented), volunteer invitations (magic links), monthly-progress digests to the branch board. Sender domain authorisation needed.
- **Payments — Not needed for v1.** Free for community libraries. A future "managed branch" tier could charge for printed catalogue books or vendor ILS migration assistance.
- **External APIs:**
- Open Library API (`openlibrary.org/api/`) — primary bibliographic source, free, no key required
- Library of Congress catalog (`id.loc.gov`) — for LCSH subject headings and authoritative author records
- Google Books API — secondary verification, requires an API key
- ISBNdb — tertiary, paid, optional, used only for hard-to-find indie and self-published titles
- WorldCat search API — optional, requires OCLC credentials
- All four are queried with a 250 ms total timeout per book; the strongest concordant match wins
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, Google Books API key, ISBNdb key if used, OCLC WorldCat credentials if used) lives in environment variables — never in the 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 · donor names appear in the public catalogue only with explicit consent · the Gemini API is used on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms · spine photographs that contain readable personal handwriting (donor inscriptions inside the front cover) are kept private to the branch unless the donor explicitly consents to public display.
**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 shelf photograph → `ShelfParse` schema | `gemini-3.5-flash` | medium | (none) |
| Resolve ambiguous spine via grounded search | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| Arbitrate between conflicting catalogue records | `gemini-3.5-flash` | low | (none) |
| Assign Dewey from subject headings | `gemini-3.5-flash` | medium | (none) |
| Find duplicates / weeding candidates across collection | `gemini-3.5-flash` | medium | (none) — long-context, chunked by Dewey range above 1,500 books |
| Generate spine-label print copy (transliteration etc.) | `gemini-3.5-flash` | low | (none) |
| Donation-log thank-you email body | `gemini-3.5-flash` | low | (none) |
*Note for builders:* there are no TTS or image-generation calls in v1 of this template; if you later add a "narrate this catalogue card" feature, omit `thinkingConfig` entirely on TTS calls — the field is not supported on those models. The matrix's documentation columns are advisory; do not serialise `n/a` strings into the request body.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const SpineRead = z.object({
spine_id: z.string(),
bounding_box_in_shelf_image: z.object({
x: z.number(), y: z.number(),
width: z.number(), height: z.number(),
}), // pixel coords in original
spine_orientation: z.enum([
"horizontal-title-readable",
"vertical-title-top-to-bottom", // common in English
"vertical-title-bottom-to-top", // common in French, Tamil, Bengali
"stacked-flat-on-cart",
"cover-up-no-spine-text", // children's picture book on shelf
]),
script_detected: z.string(), // BCP-47-style, "ko-Hang", "ar-Arab", "en-Latn"
language_detected: z.string(), // BCP-47 "ko-KR", "ar", "en"
title_ocr_verbatim: z.string(), // exactly what's on the spine
title_transliterated: z.string().nullable(), // ALA-LC, Hepburn, Revised Romanisation, etc.
author_ocr_verbatim: z.string().nullable(),
publisher_ocr_verbatim: z.string().nullable(),
year_ocr_verbatim: z.string().nullable(),
ocr_confidence: z.number().min(0).max(1),
ocr_legibility_notes: z.string().nullable(), // "sun-faded; only 'abbey' and '1962' clearly legible"
});
const CatalogueMatch = z.object({
source: z.enum(["openlibrary", "loc", "google_books", "isbndb", "worldcat"]),
isbn_13: z.string().nullable(), // null if catalogue has no ISBN (pre-1970, ephemeral)
isbn_10: z.string().nullable(),
title_canonical: z.string(),
authors: z.array(z.string()),
publisher: z.string().nullable(),
publication_year: z.string().nullable(), // string because "[1962]", "ca. 1955" are valid
language_canonical: z.string(), // BCP-47
edition_note: z.string().nullable(), // "first paperback edition"
lc_subject_headings: z.array(z.string()),
bisac_subject_codes: z.array(z.string()),
dewey_canonical: z.string().nullable(), // catalogue's own Dewey if present
match_confidence: z.number().min(0).max(1),
match_signals: z.array(z.string()), // ["title fuzzy 0.94", "author exact", "year ±2"]
});
const FlagReason = z.enum([
"ocr_low_confidence",
"no_catalogue_match",
"multiple_strong_candidates",
"publisher_or_year_mismatch",
"language_mismatch_between_spine_and_catalogue",
"spine_orientation_unclear",
"cover_up_no_spine_text",
"duplicate_of_existing_holding",
"damaged_spine",
"donor_inscription_visible_consent_needed",
]);
const Book = z.object({
book_id: z.string(),
branch_id: z.string(),
// What we saw on the shelf
spine_read: SpineRead,
// What we found in catalogues
candidate_matches: z.array(CatalogueMatch),
chosen_match: CatalogueMatch.nullable(), // null until a flag is resolved
chosen_match_method: z.enum([
"single_unanimous_match",
"majority_concordant_match",
"volunteer_chose_from_candidates",
"volunteer_entered_original_record",
"no_match_no_record_yet",
]),
// Final catalogue record
final_isbn_13: z.string().nullable(),
final_title: z.string().nullable(),
final_authors: z.array(z.string()),
final_language: z.string().nullable(),
final_publication_year: z.string().nullable(),
// Classification
dewey_full: z.string().nullable(), // "813.54"
dewey_branch_format: z.string().nullable(), // "Fic UPD" or "J Fic CIS"
dewey_assignment_rationale: z.string().nullable(), // 1-2 sentence explanation for audit
shelving_section: z.string().nullable(), // "Adult Fiction", "J Picture Books"
// Operational metadata
accession_number: z.string(), // branch-issued, monotonically incrementing
added_at_iso: z.string(),
added_by_volunteer_id: z.string(),
donor_id: z.string().nullable(),
donor_consent_to_public_display: z.boolean(),
condition: z.enum([
"as_new", "very_good", "good", "fair", "poor", "ex_library_rebound",
]),
// Flags
flags: z.array(z.object({
reason: FlagReason,
detail: z.string(),
raised_at_iso: z.string(),
resolved: z.boolean(),
resolved_by_volunteer_id: z.string().nullable(),
resolved_at_iso: z.string().nullable(),
})),
});
const ShelfParse = z.object({
shelf_photo_id: z.string(),
shelf_image_uri: z.string(), // gs:// URI; do NOT pass Firebase Storage public URLs
branch_id: z.string(),
captured_at_iso: z.string(),
capture_orientation: z.enum(["landscape", "portrait", "cart_stack"]),
spines_detected: z.array(SpineRead),
books_proposed: z.array(Book), // one Book per detected spine, pre-verification
shelf_legibility_notes: z.string().nullable(), // overall: "warm afternoon glare upper half"
parse_confidence_overall: z.number().min(0).max(1),
});
type Book = z.infer;
type ShelfParse = z.infer;
```
### Common failure modes (and how to avoid them)
- Agent silently downgrades `thinkingLevel` on the the shelf parse call to save quota — pin `gemini-3.5-flash` with the matrix-specified `thinkingLevel` explicitly. Flash undercounts spines on busy shelves, misses bottom-to-top vertical titles in Tamil and Bengali, and confidently misreads sun-faded clothbound spines.
- ISBN hallucinated by the model — the model should NEVER propose an ISBN. It proposes a title + author + year + publisher; the server verifies against catalogues. Pin this hard in the system instruction.
- Title silently translated — "백범일지" silently returned as "The Diary of Baekbeom". Pin: `title_ocr_verbatim` is exactly what is on the spine, in the original script. A romanisation goes in `title_transliterated`, never in `title_ocr_verbatim`.
- Vertical bottom-to-top spines (Tamil, Bengali, much French) read top-to-bottom and reversed — pin orientation detection as a required field, separate from OCR. The system instruction names the four orientations explicitly and gives examples.
- Children's picture books with no spine text classified as "untitled" — these must be flagged `cover_up_no_spine_text`, not parsed as books with an empty title.
- Multiple catalogues disagree (Open Library says 1962, Google Books says 1963, LoC says 1962) — the arbitration call picks the LoC year over the Google Books year when LoC is one of the concordant sources; never silently average.
- Dewey assigned without consulting subject headings — the assignment call must cite at least one LCSH or BISAC code in `dewey_assignment_rationale`. A Dewey number with no citation is a failure mode.
- Branch convention ignored — the app proposes `813.54` for a branch whose convention is `Fic UPD`. The Dewey-assignment call always reads the branch's convention from Firestore before proposing a `dewey_branch_format`.
- Religion-shelf precision flattened — a Christian-Bible commentary, an Islamic-jurisprudence text, and a Sikh-prayer-book all wind up in 200; for a faith-community library this is unusable. The branch's "preferred 200s sub-Dewey precision" setting (e.g. "always 220.x for our parish library", "always 297.x for our mosque library") is honoured.
- Duplicate detection too aggressive — a different edition of the same book is not a duplicate for a library that lends multiple copies of popular titles. Duplicate logic compares ISBN + publisher + edition note; near-duplicates surface as "you already hold a different edition; merge or keep both?".
- Spine bounding boxes overlap — when two thin paperbacks share a sliver of spine, the parse mis-attributes title text. The system instruction asks the model to be conservative: when two spines overlap or the boundary is unclear, raise a flag and leave the ambiguous slice unassigned.
### Negative constraints (hard rules)
- Do NOT invent ISBNs. The model proposes title + author + year + publisher only. ISBNs come from authoritative bibliographic catalogues (Open Library, LoC, Google Books, ISBNdb, WorldCat). If no catalogue match, the row is flagged `no_catalogue_match` and queued for an original record entered by the volunteer.
- Do NOT translate titles or author names. `title_ocr_verbatim` and `author_ocr_verbatim` are the script and characters on the spine, exactly. Romanisations go in `_transliterated` fields; English glosses go in the label-print step if the branch requests them, never in the canonical record.
- Do NOT assign Dewey without naming the subject signal that drove the assignment. `dewey_assignment_rationale` must cite a specific LCSH heading, a BISAC code, or the catalogue-provided Dewey. A bare assignment is a flag.
- Do NOT silently pick between conflicting candidate matches when their confidence is within 0.05 of each other. Two strong candidates means a `multiple_strong_candidates` flag and the volunteer chooses.
- Do NOT silently weed. The duplicate-detection call surfaces candidates; the volunteer decides. Weeding is logged with a reason every time.
- Do NOT display a donor's name in the public catalogue without explicit per-donation consent. Default is private; consent flows through a single-page form the donor signs.
- Do NOT cache shelf photographs to a public bucket. Storage URIs are server-signed and short-lived; public catalogue exports use the canonical bibliographic record only, never the shelf-photograph crop.
- Do NOT use the branch's catalogue or donor data to train or fine-tune any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. The capabilities-info panel says this in plain English.
- Do NOT proceed past a flag silently in batch mode. A row with an unresolved flag is not added to the public catalogue; it sits in "Needs Your Eyes" until the volunteer clears it.
### 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 shelf photograph → `ShelfParse` schema
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are reading a photograph of a shelf or cart of books for a
community-library volunteer. Your task: detect every spine in the
photograph, OCR its title and author in the original script, detect
the language and script, and emit a structured ShelfParse object.
The photograph may show:
- A horizontal shelf with spines up, twenty to forty books across
- A vertical stack of books on a library cart (portrait orientation)
- A multi-shelf wall captured in one frame
- A children's-collection shelf with picture books cover-up, no
usable spine text
- A mixed collection (English paperbacks, Korean Hangul vertical
spines, Tamil and Bengali bottom-to-top vertical spines, Arabic
right-to-left, Cyrillic, Devanagari, Hebrew, traditional and
simplified Chinese, Japanese vertical kanji, Amharic Ge'ez,
Khmer, Farsi Nastaliq)
- Books with sun-faded spines, library-rebinding tape with
handwritten titles, donor-inscription stickers, prior-library
call-number labels still in place
Hard rules:
- Detect spines as bounding boxes in the original image's pixel
coordinates. Be conservative on overlapping boundaries — if two
thin spines blur into each other, raise the `spine_orientation`
uncertainty and let the volunteer split them.
- Identify the script per spine. Use BCP-47-style codes:
"ko-Hang", "ja-Jpan", "zh-Hant", "zh-Hans", "ar-Arab",
"fa-Arab", "ur-Arab", "he-Hebr", "ta-Taml", "bn-Beng",
"pa-Guru", "hi-Deva", "am-Ethi", "km-Khmr", "ru-Cyrl",
"en-Latn", "es-Latn", "pt-Latn", "fr-Latn", "pl-Latn",
"sw-Latn", and so on. Get this right before OCRing.
- OCR in the original script. Do NOT translate. Do NOT romanise into
the `title_ocr_verbatim` field. The verbatim field is the literal
glyphs on the spine.
- If a romanisation is useful, put it in `title_transliterated` using
the standard system for that script (ALA-LC for Arabic and
Cyrillic, Hepburn for Japanese, Revised Romanisation for Korean,
Pinyin for Mandarin, ISO 15919 for South Asian scripts).
- Detect spine orientation as one of the five enum values. Tamil,
Bengali, French, Spanish spines are very often bottom-to-top
vertical; English spines are almost always top-to-bottom vertical
or horizontal. Do not assume the English pattern across all
scripts.
- For children's picture books shelved cover-up with no spine text,
set spine_orientation = "cover-up-no-spine-text" and leave
title_ocr_verbatim empty. Do NOT invent a title. The volunteer
will photograph the front cover next.
- DO NOT propose an ISBN. ISBNs come from the bibliographic-catalogue
resolution step on the server, not from this call. Your job is
to propose title + author + (publisher) + (year) only.
- ocr_confidence is honest, not generous. A sun-faded clothbound
spine where you can only read "abbey" and "1962" is confidence
0.2 or below, with ocr_legibility_notes naming what was legible.
- shelf_legibility_notes describes the photograph as a whole — glare,
angle, focus, lighting — in one sentence, so the volunteer can
decide whether to retake.
- parse_confidence_overall is a single 0-1 number reflecting your
overall trust in the parse.
Output ONLY the ShelfParse JSON matching the provided schema. No
prose, no commentary. JSON only.
```
---
### Call: Resolve ambiguous spine via grounded search
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding
```
You are helping resolve a single book spine where the in-app
bibliographic-catalogue lookup (Open Library, Library of Congress,
Google Books, ISBNdb) returned no concordant match.
Inputs: the spine's OCR text, transliteration, detected language,
detected script, and any partial publisher / year signals.
Use `google_search` grounding to look for the book on the open web —
publisher pages, second-hand bookstore listings, scholarly catalogue
listings, library bibliographies, the author's official site.
Hard rules:
- Do NOT invent an ISBN. If you find an ISBN on a credible source,
return it with the source URL. If you do not find one, return null
for isbn_13 and isbn_10.
- Prefer publisher pages, university library catalogues, and
national-library catalogues over consumer e-commerce listings.
- For non-Latin-script books, search in the original script AND in
the standard romanisation; rank original-script results higher.
- If multiple plausible candidates exist, return all of them as an
array; the volunteer chooses.
- Output the response as JSON in the text body. Do NOT use
`responseSchema` — `responseSchema` and `google_search` cannot be
combined in the same Gemini call today.
- Citation URLs come from the response's
`groundingMetadata.groundingChunks[].web.uri` — server-side. Do
NOT include URLs in the JSON body; they will hallucinate. Cite
the source in a `source_note` string instead.
Output JSON shape:
{
"candidates": [
{
"title_canonical": "...",
"authors": ["..."],
"publisher": "...",
"publication_year": "...",
"isbn_13": "..." | null,
"isbn_10": "..." | null,
"language_canonical": "...",
"source_note": "found on the Yonsei University Press catalogue page"
}
],
"no_match_found": false
}
No commentary outside the JSON.
```
---
### Call: Arbitrate between conflicting catalogue records
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive the candidate matches returned by Open Library, Library
of Congress, Google Books, and (optionally) ISBNdb and WorldCat for
one spine. Your task: pick the strongest candidate, explain why, or
flag the spine for the volunteer if no single candidate dominates.
Hard rules:
- Library of Congress is authoritative on titles, authors, and
publication years for U.S.-published books. Prefer LoC when it
is one of the concordant sources.
- Open Library is the broadest source but has crowd-sourced edits;
trust it less than LoC for canonical records.
- Google Books is good for ISBNs and edition notes but sometimes
carries OCR'd metadata of variable quality. Use it for ISBN
confirmation, not as the single source of truth.
- ISBNdb and WorldCat are good for indie, self-published, and
non-U.S. titles where the other three are thin.
- For non-U.S. titles, weight the national library of the book's
origin (Korean National Library, Bibliothèque nationale de
France, Deutsche Nationalbibliothek, National Library of
Pakistan, etc.) when its data is available; otherwise weight
Open Library and Google Books.
- A candidate "dominates" when (a) at least two sources concord on
ISBN, title, author, and publication year ±1, OR (b) one
authoritative source (LoC or national library) provides a
complete record AND no other source contradicts it.
- A spine should be flagged `multiple_strong_candidates` when two
candidates have match_confidence within 0.05 of each other AND
differ on a load-bearing field (ISBN, edition, year).
- match_signals is a list of short strings ("title fuzzy 0.94",
"author exact", "year ±2", "publisher mismatch"). Be specific.
Output: the chosen CatalogueMatch object, OR an object
`{ "needs_volunteer": true, "reason": "...", "candidates": [...] }`.
No commentary.
```
---
### Call: Assign Dewey from subject headings
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You assign a Dewey Decimal Classification number to a book whose
authoritative bibliographic record is known. The record includes
LCSH headings, BISAC codes, the catalogue-provided Dewey (if any),
the title, the author, the language, and the publication year.
Use Dewey Decimal Classification, Edition 23, as the reference frame.
Hard rules:
- If the bibliographic record provides a Dewey number, START from it.
Do not overrule it without cause. Confirm it fits the book by
reading the title + LCSH + BISAC together.
- If the bibliographic record provides no Dewey, derive one from the
LCSH + BISAC. The `dewey_assignment_rationale` field must cite at
least one specific LCSH heading or BISAC code that drove the
decision. "I picked 641.5972 because LCSH 'Cooking, Mexican'
maps to 641.5972 in DDC 23."
- Honour the branch's Dewey precision setting. Some branches use
two-decimal Dewey (813.54). Some use one-decimal plus author
cutter (813.5 UPD). Some use short codes (Fic UPD). Some children's
collections use J Fic + author trigraph (J Fic CIS). Read the
branch's setting before proposing `dewey_branch_format`.
- Religion (200s) requires care for faith-community libraries. A
parish library shelving exclusively Christian sources may
configure "200s preference: 220.x for biblical, 230.x for
theology, 250.x for parish work"; an Islamic centre may
configure "200s preference: 297.x" exclusively. Honour the
configured preference.
- Literature (800s) requires care for diaspora libraries. A Korean
fiction collection may use 895.7 (Korean literature) by default;
the branch may configure "literature preference: by-author-cutter"
or "by-original-language".
- Children's fiction is "J Fic" in most North American branches;
it is "J Picture Book", "JE", "Easy", or a Lexile-based grade
band in others. Honour the branch's setting.
- If the book genuinely fits two Dewey ranges with comparable weight
(a poetry collection by an academic about her grandmother's
cooking is 811 AND 641; you choose ONE), explain the choice in
the rationale.
Output: dewey_full, dewey_branch_format, dewey_assignment_rationale,
shelving_section. JSON only. No commentary.
```
---
### Call: Find duplicates / weeding candidates across the collection
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none — long context over the collection)
```
You receive the branch's full catalogue (or a Dewey-range chunk of it
above the 1,500-book chunking threshold) as a JSON array of Book
records. Your task: surface duplicates, near-duplicates (same book,
different edition), and books that look out of scope for the
branch's stated collection focus.
Inputs include:
- The full catalogue chunk
- The branch's collection focus statement ("Korean fiction and ESL
workbooks for after-school programmes", "neighbourhood lending
library, broad fiction + non-fiction, no academic", etc.)
- The branch's policy: "we lend multiple copies of popular titles"
vs "single copy per title"
Hard rules:
- An EXACT duplicate is same ISBN. Surface as
`duplicate_exact` with both accession numbers.
- A NEAR duplicate is same title + same author, different ISBN
(different edition). Surface as `duplicate_near_edition` with both
records and the volunteer chooses.
- An OUT-OF-SCOPE candidate is one whose subject headings sit
outside the branch's collection focus. Be cautious here —
surface it as a soft flag with the LCSH heading and the
collection-focus mismatch named in plain English. Do NOT
recommend weeding. The volunteer decides.
- A DAMAGED-CONDITION candidate is any book whose condition field is
`poor` and whose acquisition date is more than two years ago.
- Do NOT recommend weeding based on age alone. A 1962 first edition
is not a weeding candidate. A 1992 paperback in `poor` condition
is.
- Do NOT recommend weeding non-English-language books for an
English-speaking volunteer. The branch's collection focus is
the only authority on scope.
Output JSON shape:
{
"duplicates_exact": [...],
"duplicates_near_edition": [...],
"out_of_scope_candidates": [...],
"damaged_candidates": [...]
}
No commentary outside the JSON.
```
---
### Call: Generate spine-label print copy (transliteration etc.)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You generate the print-ready text for a single spine label. The
branch's label-format setting determines the lines:
- Line 1: Dewey or branch format ("813.54", "Fic UPD", "J Fic CIS")
- Line 2: author cutter ("UPD", "CIS"), if not already in line 1
- Line 3: optional transliteration if branch wants bilingual labels
Hard rules:
- Title and author appear on the catalogue card, NOT on the spine
label. Spine labels carry classification only.
- If the branch's setting is "bilingual labels for non-Latin
scripts", add a third line with the transliteration using the
standard system for the script.
- Match the branch's font and point size; the print step renders
these. Your job is the text content.
Output: a single object with line_1, line_2, line_3 strings, each
trimmed to the character limits in the branch's label format. No
commentary.
```
---
### Call: Donation-log thank-you email body
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You draft a thank-you email to a library donor who has consented to
be contacted. The branch's voice setting controls tone (warm-
neighbourhood, formal-faith-community, brief-and-bureaucratic).
Hard rules:
- Address the donor by the name they provided on the consent form.
If they consented anonymously, omit the name and say "thank you
for the recent donation".
- Name the number of books donated, the date, and the branch.
- Say in one sentence what happens next ("we are cataloguing your
books; once they are on the shelves, we will send you a short
list of titles you might enjoy borrowing back").
- If the donor consented to public attribution, mention it: "with
your permission, we will list you in our donor wall".
- Keep the email under 120 words.
- Do NOT promise a tax receipt unless the branch has configured the
tax-receipt template. If the branch has a tax-receipt template,
use the configured language verbatim.
Output: a single string, plain text, no HTML. No commentary.
```
## 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 Saturday-morning donation surge.** A volunteer at the Newton branch in Surrey arrives at the library to find seven banker's boxes of donated paperbacks on the floor of the back room. She wheels a cart over, sorts the boxes onto the cart spine-up, and photographs each cart row. The app processes overnight; on Sunday morning she clears 18 flags and prints labels for 220 books before the doors open.
- **The Korean fiction shelf at a Queens community centre.** A retired teacher in Flushing has three boxes of Korean-language paperbacks donated by the seniors group, ranging from a 1990s edition of 백범일지 to a brand-new 한강 paperback. The app reads Hangul vertical spines, matches against the Korean National Library catalogue, romanises titles to Revised Romanisation, and assigns 895.7 with author cutter — the branch's literature-shelving convention.
- **The Spanish-language children's section in Pilsen.** A volunteer at a Mexican-American community library in Chicago is cataloguing donated picture books — many with no spine text, photographed cover-up. The app correctly flags `cover-up-no-spine-text` and prompts the volunteer to photograph the front cover; the resulting catalogue uses J Picture Book + author trigraph and a Lexile-equivalent reading-level overlay.
- **The faith-community library at a Surrey gurdwara.** Punjabi religious texts in Gurmukhi script, English-language children's stories about Sikh history, and academic books on Punjabi literature — three shelving sections, three different Dewey conventions configured (294.6 for Sikhism with sub-Dewey precision, J Fic + author cutter for children's, 891.42 for Punjabi literature).
- **The estate library.** A volunteer is helping an executor catalogue a deceased professor's 800-volume personal library before donation to the university. The app handles a 1962 clothbound first edition (no ISBN, original-record-me flow), a sun-faded paperback that requires grounded-search disambiguation, and the duplicate-detection pass that surfaces three copies of the same textbook the professor used to teach with.
- **The prison library cart.** A volunteer pushes a cart on a Tuesday afternoon. The branch's catalogue lives entirely on a shared tablet that goes through one security checkpoint each visit. The app's offline-first mode handles the day: photograph carts in the morning, process when the tablet reconnects to the unit's wifi at 11am, print labels at the branch office in the afternoon.
- **The Little Free Library steward.** A neighbour in Pittsburgh photographs the contents of her front-yard library twice a month. The app tracks what came in and what walked away, summarises the year for her annual neighbourhood newsletter, and lets her see which books had the shortest shelf life.
- **The refugee-camp reading tent.** A volunteer at a UNHCR-supported reading tent at a camp in northern Kenya catalogues a shipment of Arabic, Tigrinya, Amharic, and Somali books donated by a diaspora organisation. The offline-first capture flow holds the day's photographs; sync happens on the staff laptop's evening connection. The branch's collection focus is "Tigrinya children's literature and Arabic adult fiction"; the weeding logic respects it.
- **The temple library in Chennai.** A volunteer catalogues Tamil and Sanskrit religious texts in a temple's small lending room. Tamil spines are bottom-to-top vertical; the OCR call gets the orientation right because the system instruction names the four orientations explicitly. The 200s precision is configured for the temple's tradition (294.5 for Hindu sources, sub-precision to two decimals).
- **The community fridge library exchange in Lisbon.** A neighbourhood collective runs a small bookshelf next to the community fridge — Portuguese, Brazilian Portuguese, Cape Verdean Crioulo, English language exchange. The volunteer photographs the shelf weekly; the app tracks the flow.
- **The annual report.** The branch board meets in January. The volunteer pulls up the year's stats: 1,847 books added, 412 weeded, 138 donors thanked, top three languages in the collection. The app exports a one-page PDF the chair reads aloud.
- **The handoff to a vendor ILS.** Three years in, the branch has grown and is migrating to Koha. The volunteer exports the catalogue as MARC21 XML; the migration consultant imports it; nothing is lost.
## 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 library cart with a row of donated paperbacks spine-up under a warm afternoon window, a clipboard and a printed catalogue card visible at the edge of frame. One paragraph: "Library Cart turns a photograph of a shelf into a Dewey-coded catalogue — every flagged row clearly marked, every uncertain match in your hands." Single Google sign-in button; Apple sign-in next to it; magic-link email below. "Try with the sample branch" → loads the demo branch in section 8a.
2. **Empty state — "Start a branch."** Branch setup wizard: branch name, collection focus statement, label stock, font, Dewey precision, languages, romanisation systems, bilingual-label preference, religion sub-Dewey preferences, children's-shelving convention. Defaults that work for "neighbourhood lending library" are pre-filled; the wizard takes under three minutes.
3. **Capture flow** (mobile-first, landscape). Live viewfinder with a shelf-shaped guide and per-spine bounding-box overlay as the user pans. Capture a shelf → "another shelf?" → "cart stack?" → "cover-up children's row?". Each capture clusters as a single batch in the queue. Capture in offline mode queues for processing when connection returns.
4. **Processing queue.** A vertical list of the day's batches. Each item shows the shelf thumbnail, the number of spines detected, and a step-by-step honest progress bar: "Reading the spines…" → "Looking up the ISBNs…" → "Picking Dewey numbers…" → "Generating labels…". Each step takes 6-20 seconds per shelf depending on shelf size. The user can close the app and come back.
5. **Shelf detail view.** A two-pane layout on desktop, stacked on mobile. Left pane: the photograph of the shelf with bounding boxes around each spine (tap a box → the corresponding row scrolls into view on the right). Right pane: the list of books with title, author, language, Dewey, status chip (green tick, amber flag, red unresolved), and a small "(i)" icon that reveals the catalogue concordance for that book.
6. **Needs Your Eyes (flag inbox).** A dedicated tab listing every unresolved flag across the branch. Filter by flag reason (OCR low confidence, no catalogue match, multiple strong candidates, etc.). The volunteer's afternoon happens here; each flag has a one-tap resolution path.
7. **Catalogue view.** Magazine-grid of books. Filter by language, Dewey range, donor, accession date, condition, "books I catalogued last week". Toggle: "Show me only the flagged rows", "Show me only the books donated last month". Default sort: most recently added.
8. **Book detail.** Single book record. Shows the spine crop, the photographed cover (if uploaded), the resolved bibliographic record, the catalogue concordance (which of the four sources agreed on which fields), the Dewey assignment rationale, the donor (if consented to attribution), and the accession number with a small QR for the spine label.
9. **Print run.** Pick a batch (or a date range, or "all unprinted labels"). Preview the labels in the branch's stock layout. Print to local printer or export PDF. Catalogue cards print in parallel; toggle on / off.
10. **Donations.** Donor list with consent status, attribution preference, total books donated, last donation date. Single-page donor consent form (PDF + web link) the volunteer hands to the donor; donor can update consent any time via a link in the thank-you email.
11. **Weeding log.** Books removed from the collection: title, reason, condition photo (optional), date, who weeded. Filter by reason and by year for the board report.
12. **Volunteer roster & branch settings.** Volunteer list with role (admin, cataloguer, viewer). Branch settings panel: every choice from the setup wizard, editable.
13. **Public catalogue.** A static-site preview of the branch's public catalogue (title, author, language, Dewey, shelving section, attributed donor). Searchable. Linkable from the branch's website. Spine photographs are not displayed publicly.
14. **Footer.** "Built for the volunteer who is making the shelves make sense." Privacy: "Your catalogue is yours. We never train on it." Capabilities `(i)` icon in header.
## 6b. First-visit onboarding
Show a **first-visit onboarding** the first time a visitor lands on the app (detect via `localStorage` flag; do not show on return visits). Three slides, dismissible at any time. Persistent re-entry: a `?` icon in the header reopens it.
**Slide 1 — What this is.**
- Headline: "Welcome to Library Cart."
- Subhead: "Photograph a shelf, get a Dewey-coded catalogue — in any branch, any language, with every uncertain match flagged, never guessed."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic OCR-and-search app: it reads the spine, verifies against four bibliographic catalogues before committing, flags every uncertain row instead of guessing, and honours the branch's own shelving conventions on font, Dewey precision, and bilingual labels.
- Visual: a small annotated illustration of a single book spine with the relevant elements labelled (title region, author region, publisher mark, prior call-number sticker, sun-fade gradient).
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample branch."
- A live demo input pre-loaded with three shelf photographs from the seed content in section 8a (one English fiction shelf, one Korean fiction shelf, one children's picture-book row).
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the vertical Hangul OCR, the cover-up-no-spine-text flag on the picture-book row, the multi-candidate flag on the third book of the fiction shelf).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample branch's settings in `/data/seed-branch/` for your own collection focus and label conventions."
- "Adjust the prompts in `/server/prompts/` to fit your collection's languages."
- "Wire up your Gemini API key, Google Books 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 spines in any script (Hangul, Tamil, Bengali, Arabic, Hebrew, Devanagari, Chinese, Japanese, Cyrillic, Ge'ez, Khmer, Latin), segments the shelf, OCRs each book in its original characters, detects orientation and language. One call per shelf photograph.
- **Gemini 3.5 Flash (structured output)** — every shelf parse returns a typed `ShelfParse` record. Every book has its candidate matches, its chosen match, its catalogue concordance, and its Dewey rationale stored explicitly.
- **Gemini 3.5 Flash + grounded search** — disambiguates the long-tail books where the four standard bibliographic catalogues return no concordant match. Citations come from grounded-search metadata, never from the model's text body.
- **Gemini 3.5 Flash (long context)** — once your branch passes 200 books, the duplicates and weeding pass runs over the whole collection at once (chunked by Dewey range above 1,500 books).
- **Firebase Auth** — Google and Apple sign-in for branch members; magic-link email for volunteer invitations and donor consent.
- **Firestore** — stores your branch settings, your books, your flags, your roster, your donations, your weeding log.
- **Firebase Storage** — keeps the original shelf photographs and spine crops, available privately for re-OCR if you ever need it.
- **Cost note** — see the detailed breakdown in 6d. A typical donation surge of 200 books costs about $0.80 of Gemini API spend, total, processed once.
- **Privacy note** — your branch's catalogue is private to the branch members you invite. Donor names appear in the public catalogue only with explicit per-donation consent. 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.
**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 — Open Library, Library of Congress, Google Books, ISBNdb, optionally WorldCat
**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)
- `GOOGLE_BOOKS_API_KEY` — for the secondary catalogue verification path
- `ISBNDB_KEY` — optional, only if you want indie / self-published coverage beyond the three free catalogues
- `WORLDCAT_KEY` — optional, OCLC credentials if you want WorldCat parity
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: the long-context weeding call is billed per token of input — a 1,000-book branch weeding pass costs about $0.60 each time it runs (default: monthly).
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this branch forever" — gone in 60 seconds), what is never sent for training, and the donor-consent gate that controls public attribution.
**Documentation links:**
- AI Studio Build docs
- Gemini API multimodal, structured-output, long-context, grounded-search docs
- Firebase Auth, Firestore, Firebase Storage docs
- Open Library API, Library of Congress Linked Data Service, Google Books API
- MARC21 XML reference (for the ILS handoff export)
- Dewey Decimal Classification Edition 23 reference
**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 shelf photograph (Gemini 3.5 Flash, medium thinking)** — one shelf photograph ≈ one large image, ~1,200 output tokens for a 28-spine shelf. ~$0.018/shelf, or roughly $0.0006/book.
- **Arbitrate catalogue records (Gemini 3.5 Flash, low thinking)** — only runs when at least two catalogues return candidates; ~150 output tokens. ~$0.001/book on the books that need it (typically 60% of books).
- **Grounded search disambiguation (Gemini 3.5 Flash + `google_search`)** — only runs on the books where the four catalogues returned no concordant match (typically 5-15% of a typical branch). ~$0.002/book on the books that need it.
- **Dewey assignment (Gemini 3.5 Flash, medium thinking)** — runs once per resolved book; ~200 output tokens. ~$0.002/book.
- **Duplicate / weeding pass (Gemini 3.5 Flash, medium thinking, long-context)** — runs monthly. ~$0.60 per 1,000-book branch per run. Chunked by Dewey range for branches above 1,500 books; cost scales linearly with collection size.
- **Spine-label print copy (Gemini 3.5 Flash, low thinking)** — ~$0.0002/label.
- **Donor thank-you email (Gemini 3.5 Flash, low thinking)** — ~$0.0005/email.
- **Expected per-book cost on first ingest:** ~$0.004 (lower than A1 because no TTS and no per-image generation). **A 200-book donation surge total:** ~$0.80. **Ongoing monthly weeding pass** (1,000-book branch): ~$0.60/month.
- **Image storage:** Firebase Storage standard tier, ~$0.026/GB/month. A high-quality shelf photograph at 4032×3024 ≈ 4 MB; spine crops ≈ 100 KB each. A 1,000-book branch with one shelf photograph per ~20 books uses ~250 MB ≈ ~$0.007/month.
- **External APIs:** Open Library and LoC are free. Google Books has a generous free quota (1,000 requests/day); a typical branch stays well within. ISBNdb is paid (~$50/month for a small-volume plan) and only worth it if you have a long tail of indie titles. WorldCat requires OCLC institutional credentials.
## 7. Design language
- **Mood:** The back room of a small library on a Saturday morning. A cart, a clipboard, a window with afternoon light. Not a corporate SaaS dashboard. Not a "scan-anything" consumer app. A volunteer's tool that respects the volunteer's time and tells them the truth about what it's unsure of.
- **Typography:** A clean library-card serif for catalogue rows and book titles (Source Serif Pro or Sentinel). The Dewey number itself sits in a monospaced typeface (IBM Plex Mono or JetBrains Mono) — librarians read Dewey numbers in monospaced columns; honour that. Clean grotesque for app chrome (Inter or Geist). Non-Latin titles render in the appropriate native typeface — Noto Sans Korean, Noto Sans Arabic, Noto Sans Devanagari, Noto Sans Bengali, Noto Sans Tamil, Noto Sans Punjabi (Gurmukhi), Noto Sans Ethiopic, Noto Sans Khmer.
- **Palette:** Library-paper background `#F6F2EA` for the catalogue view, deep ink `#1A1A1A` for body text, library-stamp navy `#2C3E5E` for Dewey numbers and primary actions, warm amber `#B7791F` for flags-that-need-attention, soft green `#3C8A52` for "confidently catalogued, ready to print", muted red `#9B2C2C` only for "this book has no catalogue match anywhere and you need to enter an original record". Borrowed from real library card stock and labels, not from product-marketing palettes.
- **Imagery:** The photographs of the shelves are the hero. Never crop them tighter than the volunteer did. Spine bounding boxes overlay the photograph with a 1-pixel sepia border that thickens to amber when a flag is attached. Period-correct library aesthetics in chrome: a hint of card-catalogue drawer in the navigation, an echo of the rubber-stamped accession number in the book-detail badge.
- **Hand-feel touches:** Catalogue cards in the print preview have the faint shadow of being slightly lifted off the page; spine-label previews have the texture of the actual label stock they will print onto. The flag-resolution flow has a satisfying "stamped" feel when a flag clears — a small green tick eases in over 200 ms, never bounces.
- **Spacing:** consistent 4-px base. Generous whitespace in the catalogue view; tight rows in the print-run preview where librarians want density.
- **Radius:** consistent token set (e.g. 4 / 8 / 16 px). Catalogue rows use 4; the spine-label preview uses 8; the welcome card uses 16. Dewey-number chips are pill-shaped (`border-radius: 999px`).
- **Shadows:** subtle, layered, neutral-cool. Avoid heavy drop-shadows.
- **Motion:** purposeful — entrance fades, hover lifts, page transitions. Respect `prefers-reduced-motion`. No bouncing splash animations. The processing-queue progress bar advances honestly per step (not a smooth gradient pretending things are happening). The flag-resolution tick is the one place where motion carries meaning; respect reduced-motion by appearing instantly.
- **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons that match the catalogue row layout. Empty states have helpful next-action guidance ("Photograph one shelf to start").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible branches, donors, book titles, Dewey assignments, and shelf photographs that fit the domain (use the seed content in section 8a as a starting point). When inventing book titles, lean on plausible real-world publishing patterns — a 1990s Korean paperback edition of a real-life-pattern Korean memoir, a Mexican-American cookbook from the 2010s, an English mid-century clothbound hardcover — but never reproduce verbatim a real copyrighted text. Cover and spine designs in seed images are illustrative compositions, not facsimiles.
- Tone: warm, direct, free of corporate language. This template is for a volunteer, not a vendor.
- 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 librarian user wants to see "LCSH heading", "DDC 23", "MARC21" in the right places; the print run reads "Avery 5160" without an explainer).
- Where the app outputs AI-generated content, never label it as "AI says" — let it speak naturally. The flag-reason names are themselves the honesty signal.
## 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 branches (sidebar):**
- "Newton Community Library, Surrey" (1,847 books, contributors: me, two co-volunteers, the board secretary) — primarily English fiction and Punjabi religious texts; Dewey precision two-decimal; label stock Avery 5160; bilingual labels for Punjabi (Gurmukhi + romanisation).
- "Flushing Seniors' Library Corner, Queens" (412 books, contributors: me, three retired teachers) — Korean fiction and ESL workbooks; Dewey precision one-decimal plus author cutter; label stock Avery 5161; Korean titles in Hangul with Revised Romanisation below.
- "Pilsen Children's Reading Room, Chicago" (680 books, contributors: me, four parent volunteers) — Spanish-language children's books and bilingual ESL picture books; J Picture Book + author trigraph for picture books; J Fic + author cutter for chapter books; Lexile reading-level overlay enabled.
- "Eastleigh Community Reading Room, Nairobi" (920 books, contributors: me, the imam's wife, two college students) — Somali-language poetry and prose, Swahili newspapers bound by quarter, English-language secondary-school texts; Dewey precision two-decimal; bilingual labels for Arabic-script Somali (Wadaad) and Latin-script Somali (Af Soomaali).
- "Centro Vecinal Almagro, Buenos Aires" (1,150 books, contributors: me, the building's portera, three retired neighbours) — Spanish-language fiction, a long shelf of Cortázar, Borges, and Aira inherited from the previous super, plus a children's corner; Dewey precision two-decimal; the branch's literature preference is "by-author-cutter" within 86x.
- "Sample Branch — Demo" — a small, mixed-language demo branch the onboarding loads.
**Sample shelf in the detail view (this is what the demo should show):**
- **Branch:** Newton Community Library, Surrey
- **Shelf photo capture date:** 2026-05-09, 10:43 local
- **Capture orientation:** landscape
- **Spines detected:** 28
- **Books proposed:** 28
- **Confidently catalogued:** 25
- **Flagged for volunteer review:** 3
**Sample row (book #14 of 28, confidently catalogued):**
- **Spine OCR verbatim:** "RABBIT, RUN / JOHN UPDIKE / FAWCETT"
- **Script:** en-Latn
- **Language:** en-US
- **Spine orientation:** vertical-title-top-to-bottom
- **OCR confidence:** 0.96
- **Candidate matches (3):**
- Open Library: ISBN 9780449911655, title "Rabbit, Run", author "John Updike", publisher "Fawcett", year 1991, language en, match_confidence 0.93, match_signals ["title exact", "author exact", "publisher exact", "year recent edition"]
- Library of Congress: ISBN 9780449911655, title "Rabbit, Run", author "John Updike", publisher "Fawcett", year 1991, language en, match_confidence 0.95, match_signals ["title exact", "author exact", "LoC canonical record"]
- Google Books: ISBN 9780449911655, title "Rabbit, Run", author "John Updike", publisher "Fawcett Crest", year 1991, language en, match_confidence 0.92, match_signals ["title exact", "author exact", "publisher fuzzy 0.96"]
- **Chosen match method:** single_unanimous_match
- **Final ISBN-13:** 9780449911655
- **Final title:** Rabbit, Run
- **Final authors:** ["John Updike"]
- **Dewey full:** 813.54
- **Dewey branch format:** Fic UPD
- **Dewey assignment rationale:** "LCSH heading 'American fiction—20th century' + LoC catalogue's own Dewey 813.54; branch format uses author cutter."
- **Shelving section:** Adult Fiction
- **Accession number:** NEW-2026-1429
- **Donor:** anonymous (consent: private)
**Sample flagged row (book #18 of 28, flagged `multiple_strong_candidates`):**
- **Spine OCR verbatim:** "THE LITTLE FRIEND / DONNA TARTT"
- **Candidate matches:** two ISBNs both confidence ≥ 0.88 — the 2003 first U.S. hardcover (ISBN 9780679439387) and the 2003 first U.K. trade paperback (ISBN 9780747573579). Spine alone cannot disambiguate; the volunteer is asked to check the copyright page or take a photograph of it.
- **Flag detail:** "Two strong candidates differ on publisher and territory. Tap to view both copyright pages and choose."
**Sample flagged row (book #22 of 28, flagged `cover-up-no-spine-text`):**
- **Spine read:** title_ocr_verbatim empty; spine_orientation cover-up-no-spine-text; ocr_legibility_notes "this row is shelved cover-up; no spine text visible. Photograph the front cover to proceed."
- **Flag detail:** "This appears to be a children's picture book shelved cover-up. Tap to photograph the cover."
**Sample flagged row (Korean shelf, flagged `ocr_low_confidence`):**
- **Spine OCR verbatim:** "한강 / [partial glyph illegible due to library-rebinding tape]"
- **Script:** ko-Hang
- **Language:** ko-KR
- **OCR confidence:** 0.42
- **Flag detail:** "The author's name is clear (한강) but the title is obscured by the library's rebinding tape. Tap to enter the title manually, or photograph the title page."
**Sample input shelves (for the build to demonstrate):**
- A 28-book English-language adult-fiction shelf at Newton Community Library, with three flagged rows (one OCR low confidence, one multiple strong candidates, one cover-up-no-spine-text).
- A 14-book Korean-language fiction shelf at the Flushing Seniors' Library Corner, with mixed Hangul vertical spines and two flagged rows for sun-faded older paperbacks.
- A 9-book Spanish-language children's picture-book row at the Pilsen Children's Reading Room, photographed cover-up, with the front-cover photograph flow demonstrated.
- A 12-book Punjabi religious-text shelf at Newton, with Gurmukhi spines and a configured 200s sub-Dewey precision of 294.6.
- A 22-book mixed English / Tamil / Bengali shelf at a fictional Toronto community library used in the deeper demo path, with bottom-to-top vertical Tamil and Bengali spines.
**Sample voice copy:**
- Onboarding: "Photograph the first shelf. We'll read it — and tell you the truth about every uncertain row."
- Processing: "Reading the spines…" / "Looking up the ISBNs…" / "Picking Dewey numbers…" / "Generating labels…"
- Empty branch: "This branch is waiting for its first shelf. Photograph one row to start."
- Error (couldn't read): "We couldn't make out three of the spines on this shelf. Want to retake the shelf, or work through them one by one?"
- Save confirmation: "Added to Newton Community Library — 25 books catalogued, 3 flagged for your eyes."
- Flag detected: "3 books need your eyes before this shelf is ready to print."
- Low confidence note: "Some spines were hard to read. Tap any amber row to see what the model considered."
- ISBN unresolved: "We couldn't find this book in any of the four catalogues. Tap to enter an original record, or weed."
**Sample donor consent form copy (single page, PDF + web):**
- "I, [name], donate the following books to [branch] on [date]. I understand: my name may [☐ appear / ☐ remain private] in the branch's catalogue. I may withdraw consent any time by writing to [branch contact]."
- Trim to one page. Use the branch's letterhead if configured. Volunteer prints, donor signs, volunteer scans back; consent state persists.
**Sample volunteer invitation email subject + body:**
- Subject: "Aisha — would you help catalogue Saturday's donations at Newton Library?"
- Body: "Hi Aisha — the board has asked me to get the donation room down before the reopening. Could you join the branch on the app and help me work through the flagged rows on Sunday morning? Tap to join." [Open Branch]
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a library cart with a row of donated paperbacks spine-up under a warm afternoon window, a clipboard with a half-filled form visible at the edge of frame, a printed catalogue card resting on top of the spines. Generate via Nano Banana 2 with a prompt emphasising "wooden floor, afternoon window light, real worn paperbacks, soft shadow under the cart, clipboard with a printed form, no people in frame".
- **App icon / wordmark:** Set in the library-card serif. A small Dewey number "813.54" beneath the wordmark in monospace, as a quiet identity cue. No icon — just type.
- **Empty-state illustration:** A simple line drawing of a single book spine with a paper label stuck on. Hand-drawn aesthetic, not a flat icon.
- **Demo shelf photographs:** Generated per the prompts in section 8a — Nano Banana 2 prompts that specifically request "shelf of paperbacks spine-up, afternoon window light, soft shadow, real worn paper, no people in frame, no logos visible on spines, spine titles in [appropriate script] in plausible fictional book-cover designs". Each demo shelf should look photographed, not rendered.
- **Spine-label preview swatches:** Avery 5160, Avery 5161, custom 25 × 12 mm die-cut. Real proportions; show the volunteer how a label will look before it prints.
- **Catalogue-card swatches:** 3 × 5 inch traditional, modern minimal, bilingual two-column. Show real proportions.
- **Stock fallbacks:** If image generation fails, fall back to the photographed sample shelf from `/public/samples/sample-shelf.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. Never depict a real, recognisable book cover or spine design; the seed shelves carry plausible fictional cover designs.
- **Optimisation:** WebP/AVIF, `loading="lazy"`, explicit `width`/`height` to prevent layout shift. Original shelf photographs preserved at upload resolution in Firebase Storage; serve resized variants in the UI.
- **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 catalogue-row layout, not spinners.
- Empty states explain the next action with a button whose label fits THIS app's domain: "Photograph the first shelf", "Import a previously-scanned PDF", "Invite a co-volunteer" — never a generic "Add your first item".
- Smooth scroll for in-page anchors and from a spine bounding box to its catalogue row.
- 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 reach the catalogue services right now. The parse is saved; we'll finish verifying when you're back online.") and offer retry.
- Low-confidence OCR rows are highlighted in amber; tapping reveals what the model read versus what it considered.
- The flag-resolution tick animation takes 200 ms with `prefers-reduced-motion` falling back to instant.
- The processing-queue progress steps advance honestly per step — the bar does not pretend to be smooth when it is discrete.
- Bounding-box overlays on the shelf photograph are tappable on touch; hovering on desktop highlights the corresponding catalogue row in the right pane.
## 11. Tech & responsive requirements
- **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 shelf parse / arbitration / Dewey assignment / weeding pass and `gemini-3.5-flash` for grounded-search disambiguation / label copy / donor email. Set `thinkingLevel` explicitly per call.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed branch on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email for invitations.
- **Storage:** Firebase Storage for shelf photographs and spine crops. Pre-signed URLs only.
- **Offline-first:** the capture flow queues to IndexedDB; the parse happens when connectivity returns. The catalogue view reads from the local cache and reconciles on reconnect.
- **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 and flag-inbox views.
- Optimistic UI on writes; reconcile on response.
- 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 reloads on iOS — re-request on every shelf-scan session; backgrounded tabs pause `getUserMedia` — re-acquire the stream on `visibilitychange`; on Low Power Mode iOS may degrade resolution — always offer `` as a fallback so a spine-row photo still uploads when WebRTC is denied.
- Long-context weeding call guards against >1M tokens. Chunk by Dewey range (000-099, 100-199, …) at the 1,500-book threshold; show the builder a warning when a branch is about to cross it.
## 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. Amber flag chip on the bone-paper background is verified against AA.
- All images have meaningful `alt` text. The shelf photographs have `alt` describing the artefact ("photograph of a 28-book English-language fiction shelf at Newton Community Library, captured 9 May 2026, with three rows flagged for review").
- Form fields have associated `