================ 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.
---
# Telescope Log
## 1. Project
**Telescope Log** is a voice-first observing notebook for amateur
astronomers — the kind of person who drives two hours to a dark-sky
site, sets up a Dobsonian on the cooling grass, and watches the
Magellanic Clouds wheel until the dew shorts out the mount. The user
talks to the app at the eyepiece — "M31, 23:14, eight point oh
magnitude, fifteen mil eyepiece, conditions six out of ten" — and the
app turns that ramble into a structured observation, slots it into a
per-object log next to the previous five times the user looked at the
same galaxy, draws it on a sky chart, and on a separate page predicts
when every catalogue object the user has ever logged will next be
above the horizon from their coordinates, on a clear night, after
astronomical twilight.
This is the kind of app a South African retired engineer builds the
weekend after he gets his first six-inch reflector and realises a
Moleskine doesn't survive ground-level humidity at the Sutherland
Karoo site. It is also the kind of app a hospice nurse in Ulaanbaatar
keeps on her phone because the only thing that holds her steady after
a long shift is forty minutes alone on the steppe with a pair of
fifteen-by-seventy binoculars and the certainty that ω Centauri will
clear the southern ridge at 04:12 tomorrow morning. Same shape of
moment, different sky, different stakes.
The single demo that proves the magic: the user holds the phone in
the cradle of one palm at the eyepiece, taps once, and says: "Em
thirty-one, twenty-three fourteen, eight magnitude, fifteen mil
eyepiece, conditions six out of ten, faint dust lane visible with
averted vision, no companion galaxies tonight, dew starting on the
corrector plate." Three seconds later the screen shows a structured
observation — object: M31 (Andromeda Galaxy), local time 23:14, NELM
estimated 5.4 from the user's conditions rating, eyepiece 15 mm
(yielding 80× on the user's pinned 1200 mm scope), seeing/transparency
sliders pre-filled at 6/10, and a quote-card: *"faint dust lane
visible with averted vision"* — verbatim, in the user's own words,
because the rule of this app is that the writing in the log is the
user's writing, not a model's. A new row appears in M31's per-object
timeline: this is the seventh time the user has logged this galaxy
since 2024-08-19. And on the "Next clear night" page, a card slides
to the top: *"M42 (Orion Nebula) rises tomorrow 21:08 local, transit
00:34, sets 04:01 — clear forecast through 02:00, then high cirrus."*
And in the harder cases — Southern Hemisphere objects no Western field
guide indexes well, a Mongolian observer logging a transient she
chased across four sites in October, a Patagonian astrophotographer
whose only window is the three hours between her shifts at the SALT
remote-control desk — the app respects the catalogue conventions of
each tradition (NGC/IC, Caldwell, Melotte, Bennett's catalogue of
southern objects, the Sharpless and Gum H-α catalogues, Patrick
Moore's late lists), and the ephemeris page works from the user's
real coordinates and real horizon profile — not a Northern-Hemisphere
default that hides everything south of −30°.
**Tagline:** _A voice-first observing log for any sky, any catalogue, any latitude — every object you've ever seen, and when you'll see it again._
## 2. Target audience
- Amateur astronomers who attend dark-sky weekends (Tivoli in Namibia, Aoraki Mackenzie in New Zealand, Cherry Springs in Pennsylvania, La Palma, the Atacama, the Karoo, the Tibetan plateau, the Mongolian steppe, Mauna Kea visitor area, Spruce Knob)
- Southern-Hemisphere observers under-served by Northern-default planetarium apps — South African, Namibian, Chilean, Argentine, Australian, New Zealand, Mauritian, Reunionnais, Madagascan amateurs
- Caldwell + Bennett + Patrick Moore catalogue completionists working through deep-sky lists across years of observing
- Astrophotographers logging exposures, filters, and conditions per object across multiple nights
- Variable-star observers reporting to AAVSO who need a structured log they can later export to WebObs
- Lunar and planetary observers tracking Jupiter belts, Mars apparitions, Saturn ring tilt across opposition seasons
- Visiting-eyepiece observers borrowing scopes at star parties, who need the log to capture which eyepiece on which scope per observation
- Outreach astronomers leading public nights at universities, planetariums, and amateur societies, who keep a log of every object they've shown the public
- Professional astronomers between shifts at remote-control desks (SALT, Gemini-South, Las Campanas, SAAO Sutherland) who still keep a personal eyepiece log for love
- Astronomy teachers who use a student's logged observations as the basis for a term project
## 3. Core value propositions
Surface these clearly through copy, visual emphasis, and section ordering — they are the reasons users pick this app.
- **Voice-first at the eyepiece, hands-free in the cold.** Tap once, talk, walk away. The app extracts object, time, eyepiece, conditions, and quote-worthy descriptions from one rambling sentence. Glove-compatible big buttons. No squinting at a glaring screen — the entire UI defaults to deep-red night mode and the brightness slider goes down to 1%.
- **Never invents an observation.** If you didn't say it, it isn't in the log. The app surfaces what it *heard* with a verbatim transcript next to the structured fields — and any field it couldn't fill from your voice is left empty, with a quiet underline, not auto-completed from a guess. This rule is hard-coded into the system instruction.
- **Speaks every catalogue, every script.** Says "M31" and gets M31. Says "Caldwell 14" and gets the Double Cluster. Says "Bennett 95" and the app knows you mean the Southern Pinwheel. Says "ω Cen" and gets the largest globular in the sky. Reads object names in Mandarin, Tamil, Khmer, Tagalog, Vietnamese, Korean, Farsi, Arabic, Hindi, Bengali, Urdu, Amharic, Swahili — and disambiguates from your other context (date, hemisphere, what you've logged before).
- **Per-object lifetime timeline.** Every time you've ever looked at M31 — first time as a teenager with a 60 mm refractor, the night you saw it from the Atacama with a 16-inch, the misty Tuesday from your back garden when you barely glimpsed it — all stacked in chronological order with the conditions, eyepiece, and your verbatim quote each time.
- **Next clear night, from *your* coordinates.** The ephemeris page predicts rise / transit / set times for every catalogue object you've ever logged, from your real latitude/longitude and your local horizon profile (you teach the app the trees, the neighbour's roof, the southern ridge). Weather grounding from the user's locale provides the clear-sky forecast.
- **Real horizon, not a flat one.** Tilt the phone around the sky once during setup; the app records your local horizon angle in 16 azimuth bins. Objects that "rise at 19:14" but don't actually clear your neighbour's roof until 19:47 say so — the math is right.
- **Sky charts that respect the dark.** Per-object finder charts render in true monochrome at the user's chosen night-mode tint. Star magnitudes scale with the user's NELM. No marketing-glow gradients, no neon constellation art.
- **Outreach mode** — when you're at a public-night session, a single tap switches the log into outreach mode: it records what you showed, in what order, the public's questions ("is that a real planet?"), and produces a one-page handout you can email to the school the next morning.
- **AAVSO-ready export** — variable-star observations export to AAVSO's WebObs format on demand. Visual magnitude estimates are stored with the comparison-star magnitudes you used, not just a single number.
- **Yours, forever.** The log is private to you. Exports as JSON, CSV, AAVSO, or a typeset PDF observing journal you can print at the end of the year.
## 4. Features to build
- One-tap voice capture at the eyepiece, with a 2-second pre-roll buffer so the first syllable isn't clipped
- Glove-mode UI — all primary buttons ≥ 64 × 64 px, no hover-only controls, the camera-button-as-shutter on the phone case can be re-bound to "start observation"
- Night-mode shader — deep red background `#1B0405`, lower brightness slider clamps to 1%, monochrome-red rendering for finder charts
- Voice parse → structured `Observation` schema in one Gemini 3.5 Flash audio call (audio reasoning, not transcription-only)
- Catalogue resolver — Messier, NGC, IC, Caldwell, Bennett, Melotte, Sharpless, Gum, Collinder, Trumpler, Berkeley, Stock, Patrick Moore's lists, common-name index ("ring nebula", "tarantula", "47 Tuc")
- Per-object timeline view — every time the user logged that object, ordered, with eyepiece, conditions, and verbatim quote on each row
- "Next clear night" page — for each object the user has logged, predicts rise / transit / set from their pinned coordinates and horizon profile, grounded against local clear-sky forecast
- Local horizon profile capture — tilt the phone around the sky once during setup; record altitude angle at each of 16 azimuth bins; rise/set predictions clip to this profile
- Sky-chart finder per observation — generates a black-on-red star field at appropriate FOV (eyepiece-derived) centred on the object, showing brighter stars and the catalogue object
- Conditions parser — extracts NELM estimates from "six out of ten" with the user's calibration; stores seeing on the Antoniadi I-V scale and transparency 1-10 separately
- Eyepiece + scope library — pinned scopes (focal length, aperture), pinned eyepieces (focal length, AFOV), magnification + true FOV computed automatically per observation
- Variable-star mode — voice can name comparison stars ("AAVSO chart X25467, fainter than the 7.4, brighter than the 7.9") and the app stores the bracket
- Outreach mode — toggles a faster capture flow optimised for public-night sessions; records each object shown, the queue, and the public's questions in a quotes panel
- Dark-sky weekend mode — bulk-records overnight; in the morning the app processes the queue once the user is back on grid power and Wi-Fi
- Multi-site support — the user has home, dark-sky-A, dark-sky-B sites; each has its own horizon profile and Bortle estimate
- Multilingual UI and voice — UI in 24 languages; voice recognition + catalogue resolution across Tagalog, Vietnamese, Mandarin, Cantonese, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer, plus the European set
- Sky-conditions grounding — the "next clear night" page uses grounded search to pull clear-sky forecasts from the user's locale's preferred service (NOAA GFS, Meteoblue, Astrospheric, Yr.no, KNMI, JMA, BoM, MetService NZ, SAWS)
- AAVSO export — JSON observations matching variable-star reports export to AAVSO WebObs CSV
- Annual observing journal — typeset PDF of the year's observations, organised by object, with finder charts and conditions, suitable for printing
- Astrophotography exposure log — for the user who's not at the eyepiece but at a guided rig, capture per-frame exposure metadata via voice ("M42, 180-second sub, OIII filter, ZWO 2600, guide RMS 0.6")
- Re-listen — every observation keeps its raw audio (the user's actual voice), playable from the timeline; the verbatim transcript and the parsed fields sit next to it
## 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)
- **Audio reasoning (Gemini 3.5 Flash)** — the hero capability. The user's voice memo is parsed end-to-end as audio: object, time, eyepiece, magnification, conditions, and free-form description are extracted in a single call. The model never re-narrates the user; the description quote is the user's own words, verbatim. Audio runs through Gemini Files API (`files/*` resource name) for any clip > 20 seconds; shorter clips can go inline as base64.
- **Structured output / JSON Schema (Gemini 3.5 Flash)** — the response matches the `Observation` schema below. Every field is typed. The schema is included verbatim in the system instruction and as `responseSchema`. Fields the audio didn't supply remain `null`, never auto-completed.
- **Multilingual support (Gemini 3.5 Flash)** — voice input handled across Tagalog, Vietnamese, Mandarin, Cantonese, Korean, Tamil, Hindi, Urdu, Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer, French, Spanish, Portuguese, Italian, German, Polish, Russian, Greek, Arabic, English. Catalogue identifiers ("M31", "NGC 1232", "Caldwell 49", "Bennett 95") are language-neutral and resolved consistently regardless of the input language.
- **Search grounding (Gemini 3.5 Flash)** — used for the ephemeris/weather grounding call. Real clear-sky forecasts come from the user's locale's preferred service via grounded search. Used also for catalogue disambiguation when voice gives an ambiguous identifier ("two oh seventeen" = NGC 2017, or "Bennett 41"? — context decides).
- **Long context (Gemini 3.5 Flash, 1M tokens)** — once the user has logged hundreds of observations across years, the per-object timeline + "what have I never seen?" analysis reads the whole log at once. **Guardrail**: average observation including transcript ≈ ~600 tokens; 1,000-observation log ≈ ~600k tokens (comfortable). For 2,000+ observations, chunk by year before the long-context call.
- **Thinking levels** — `medium` for the primary audio-parse-and-resolve call (audio reasoning + catalogue resolution + conditions inference). `low` for the per-object summary, the catalogue lookup, and the chart generator. `high` only when the user explicitly asks "what did I miss this season?" — that runs once, archive-wide.
- **Nano Banana 2 (`gemini-3.1-flash-image`)** — generates the finder chart per object: black-on-red monochrome, eyepiece-derived field of view, dimmed star field, the object at centre. Generated once per object + eyepiece pair, then cached in Firebase Storage.
- **Gemini TTS (`gemini-3.1-flash-tts-preview`)** — reads upcoming-object alerts at the user's chosen pace. Used sparingly: a soft voice saying "ω Centauri clears the southern ridge in four minutes" while the user is still at the eyepiece on a previous object. Pronunciation follows the chosen voice's native locale; Greek-letter object names (ω Cen, β Sgr) use the voice's pronunciation natively.
### Backend services
- **Auth — Required.** Firebase Auth with Google sign-in (auto-provisioned by AI Studio Build). **Apple sign-in is optional but user-configured**: it requires an Apple Developer account, Service ID, Key ID, and private key wired into the Firebase Auth console. **Magic-link email** (used for sharing an observing log with a club mate) also requires the sender domain to be authorised in Firebase Auth. Logs are private to the owner by default; sharing is per-object or per-night, never archive-wide by default.
- **Database — Required.** Firestore for `users`, `sites`, `observations`, `objects` (catalogue cache), `scopes`, `eyepieces`, `horizon_profiles`. Auto-provisioned by AI Studio Build.
- **File storage — Required.** Firebase Storage for raw voice clips (kept forever — the user's voice at the eyepiece is part of the log) + cached finder-chart images. **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 the first observation is recorded.
- **Email — Optional.** Magic-link sharing of a single night's observations or a single object's timeline with a club mate or AAVSO observer. Requires sender-domain authorisation in Firebase Auth.
- **Payments — Not needed for v1.** Free for personal use. A future "printed annual observing journal" tier could pipe to a print-on-demand partner (Lulu, Blurb) and charge for the bound book only.
- **External APIs:** Gemini API for all intelligence. **Ephemerides math runs on-device** using a vendored Skyfield / pyephem-equivalent JS port (Astronomy Engine by Don Cross — MIT licensed, vendored into `/lib/`); this is not a Gemini call. The clear-sky forecast layer comes via grounded search on the user's preferred regional service.
**Environment variables:** every secret (Gemini API key, Firebase service-account JSON, optional Mapbox token for the all-sky base layer, optional AAVSO API key for direct WebObs submission) 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 voice memos are never sent to Gemini for model training (use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms) · raw voice clips never leave the user's Firebase Storage bucket except for the one Gemini call that parses them, which goes via the Files API and is deleted from Gemini's side after the call per the Files API retention policy.
**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** (especially for `seeing_antoniadi` which must be 1-5, `transparency_1_10` which must be 1-10, and `nelm_estimate` which must be in [3.0, 7.5]).
4. **Pin the system instruction separately** from user input. Use the `systemInstruction` field for persona + behavioural rules; use `contents` for the audio 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 voice memo → `Observation` schema | `gemini-3.5-flash` | medium | (none) |
| Resolve a catalogue identifier ("Bennett 95" → object) | `gemini-3.5-flash` | low | (none) |
| Disambiguate ambiguous catalogue speech | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| Generate clear-sky forecast for the user's site | `gemini-3.5-flash` | low | `google_search` grounding (no `responseSchema` on this call — see note) |
| Generate per-object timeline summary (long-context) | `gemini-3.5-flash` | low | (none) — long-context over the user's log |
| "What did I miss this season?" archive analysis | `gemini-3.5-flash` | high | (none) — long-context |
| Generate monochrome finder chart | `gemini-3.1-flash-image` | n/a | n/a |
| Generate spoken upcoming-object alert | `gemini-3.1-flash-tts-preview` | 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. On grounded calls, `responseSchema` and `google_search` are mutually exclusive — instruct the model to emit JSON in the text body and parse server-side; pull citation URLs from `response.groundingMetadata.groundingChunks[].web.uri`.
### Primary structured-output schema (seed this verbatim in the prompt)
```typescript
import { z } from "zod";
const CatalogueId = z.object({
catalogue: z.enum([
"messier", "ngc", "ic", "caldwell", "bennett", "melotte",
"sharpless", "gum", "collinder", "trumpler", "berkeley",
"stock", "patrick_moore", "common_name", "bayer_designation",
"flamsteed", "hd", "hr", "hip", "gcvs", "tycho",
"other_user_spoken",
]),
identifier: z.string(), // "31", "1232", "C49", "B95", "ω Cen", "47 Tuc"
spoken_verbatim: z.string(), // what the user actually said: "em thirty-one"
});
const InstrumentSnapshot = z.object({
scope_id: z.string().nullable(), // FK to user's pinned scopes
scope_label_verbatim: z.string().nullable(), // "the eight-inch dob", "the GP-DX with the 80ED"
eyepiece_id: z.string().nullable(),
eyepiece_label_verbatim: z.string().nullable(), // "fifteen mil"
eyepiece_focal_length_mm: z.number().nullable(), // 15
scope_focal_length_mm: z.number().nullable(), // 1200
magnification_x: z.number().nullable(), // computed: 80
true_fov_deg: z.number().nullable(), // computed from eyepiece AFOV
filter_verbatim: z.string().nullable(), // "OIII", "UHC", "none"
filter_resolved: z.string().nullable(), // canonical: "OIII"
binoviewer_used: z.boolean().nullable(),
});
const Conditions = z.object({
seeing_antoniadi: z.number().min(1).max(5).nullable(), // I-V scale → 1-5
seeing_verbatim: z.string().nullable(), // "seeing about three out of ten"
transparency_1_10: z.number().min(1).max(10).nullable(),
transparency_verbatim: z.string().nullable(),
nelm_estimate: z.number().min(3.0).max(7.5).nullable(), // naked-eye limiting magnitude
nelm_method: z.enum([
"user_stated", "user_calibration_from_conditions_rating",
"comparison_pair", "not_stated",
]),
wind_kph: z.number().nullable(),
temp_c: z.number().nullable(),
dew_state: z.enum([
"none", "forming", "heavy", "frosted", "not_stated",
]),
moon_phase: z.string().nullable(), // resolved server-side from time + site
moon_altitude_deg: z.number().nullable(), // computed
moon_distance_to_target_deg: z.number().nullable(), // computed
});
const ComparisonStar = z.object({
label: z.string(), // "the 7.4 comp"
magnitude: z.number().nullable(), // 7.4
brighter_than_target: z.boolean(),
});
const VariableStarEstimate = z.object({
is_variable_star_observation: z.boolean(),
aavso_chart_id: z.string().nullable(), // "X25467"
comparison_stars: z.array(ComparisonStar),
bracketed_magnitude_low: z.number().nullable(), // brighter bound (lower mag)
bracketed_magnitude_high: z.number().nullable(), // fainter bound
midpoint_estimate: z.number().nullable(), // simple midpoint, never invented
});
const PublicNightCapture = z.object({
is_outreach_observation: z.boolean(),
public_questions: z.array(z.string()), // verbatim questions overheard
show_order_index: z.number().nullable(),
});
const Observation = z.object({
observation_id: z.string(),
recorded_at_utc: z.string(), // ISO, when the voice memo was recorded
site_id: z.string().nullable(), // FK to user's sites
site_label_verbatim: z.string().nullable(), // "the Karoo site", "back garden"
observer_user_id: z.string(),
raw_audio_uri: z.string(), // gs:// path to the kept voice clip
transcript_verbatim: z.string(), // word-for-word, the user's actual words
catalogue_ids: z.array(CatalogueId), // primary first; others if the user used multiple names
resolved_object_id: z.string().nullable(), // canonical, after catalogue lookup
resolved_object_name: z.string().nullable(), // "M31 (Andromeda Galaxy)"
resolved_ra_j2000_hours: z.number().nullable(),
resolved_dec_j2000_deg: z.number().nullable(),
resolved_apparent_magnitude: z.number().nullable(),
resolved_object_type: z.enum([
"galaxy", "globular_cluster", "open_cluster", "planetary_nebula",
"emission_nebula", "reflection_nebula", "dark_nebula", "supernova_remnant",
"double_star", "variable_star", "carbon_star", "asterism",
"moon", "planet", "comet", "asteroid", "satellite", "transient",
"unresolved", "user_spoken_only",
]).nullable(),
instrument: InstrumentSnapshot,
conditions: Conditions,
user_description_verbatim: z.string().nullable(), // the user's free-form quote, never paraphrased
user_added_tags: z.array(z.string()), // "first time", "lifer", "dust lane visible", "averted vision"
variable_star: VariableStarEstimate.nullable(),
public_night: PublicNightCapture.nullable(),
azimuth_deg_at_observation: z.number().nullable(), // computed from RA/Dec + time + site
altitude_deg_at_observation: z.number().nullable(), // computed
hour_angle_at_observation_h: z.number().nullable(), // computed
fields_present_in_audio: z.array(z.string()), // which of the above came from the user's voice
fields_left_null: z.array(z.string()), // explicit list of what was NOT supplied — never silently filled
parse_confidence: z.number().min(0).max(1),
flagged_for_user_review: z.array(z.object({
field_path: z.string(),
reason: z.string(),
})),
});
type Observation = z.infer;
```
### Common failure modes (and how to avoid them)
- The model "fills in" the eyepiece because the user usually uses the 15 mm. Hard rule: if the user did not say it in this clip, leave `eyepiece_focal_length_mm` null. The presence of a field in the log is *never* inferred from past behaviour.
- The model writes a polished description in its own voice instead of preserving the user's words. Pin in the system instruction: `user_description_verbatim` is verbatim from the audio; if the user did not give a free-form description, set it null. Never re-narrate.
- "M thirty-one" is mis-resolved to "M 31" but the model invents a non-existent NGC alias. Hard rule: resolve only to catalogue entries that exist; if uncertain, list all candidates with `parse_confidence` < 0.8 and flag for review.
- The model picks the agent default `gemini-3.5-flash` to save quota on the primary audio call. Pin `gemini-3.5-flash` explicitly. Flash audio reasoning misses faint catalogue identifiers and confuses "ωCen" with "Wǔ-cén".
- Conditions parsed from "six out of ten" silently converted to a transparency score without the user's calibration. Store both: the user's verbatim phrase in `transparency_verbatim` ("six out of ten") *and* the numeric `transparency_1_10: 6` only if the user has set their calibration in settings. Otherwise leave the numeric null.
- Antoniadi seeing reported as 1-10 instead of I-V. Antoniadi is 1 (perfect) to 5 (terrible). If the user says "seeing two", it's Antoniadi II; if the user says "seeing six out of ten" it's a transparency-style guess. Distinguish.
- Variable-star bracket inverted. "Fainter than the 7.4, brighter than the 7.9" → bracket between 7.4 (brighter bound, lower magnitude number) and 7.9 (fainter bound). Many users say it brighter-to-fainter; some say it fainter-to-brighter. The schema's `brighter_than_target` flag forces clarity; never collapse to a single magnitude unless the user explicitly stated a single estimate.
- "47 Tuc" is parsed as a Flamsteed designation. It is NGC 104 / Caldwell 106 / Bennett 2 — a globular cluster. Wire the common-name index to disambiguate before falling back to numeric parsing.
- Moon phase / altitude computed for the wrong site. Always use the observation's `site_id` (or fall back to the user's pinned home site) — never the device's current geolocation, which may have moved since the observation.
- Audio > 20 sec passed as inline base64. Use the Gemini Files API (`files/*` resource name) for any clip > 20 seconds. Inline base64 for shorter clips only.
- Forecast call mixes `responseSchema` and `google_search`. They are mutually exclusive in one call. Instruct the model to emit JSON in the text body and parse server-side; pull citation URLs from `groundingMetadata.groundingChunks[].web.uri`.
### Negative constraints (hard rules)
- Do NOT invent any field. If the user did not say it, the field is `null` and listed in `fields_left_null`. No "intelligent defaults". No "based on your usual setup". The user came outside in the cold to look at the sky; the log is what they actually said tonight, not a smoothed average.
- Do NOT re-narrate `user_description_verbatim`. The free-form description is the user's words, exactly. If the user said "kinda fuzzy on the south side but the dust lane was clean tonight", that is what goes in the field — not "the southern side appeared diffuse while the dust lane was distinctly visible". Same rule across all languages.
- Do NOT translate catalogue identifiers or proper-name objects. "ω Cen" stays "ω Cen" regardless of input language. "47 Tuc" stays "47 Tuc". "ζ Ursae Majoris" stays "ζ UMa". Localised display labels appear in the UI layer only.
- Do NOT auto-modernise spoken antique designations. If the user says "Lacaille two oh fifty-one", record that and resolve it; do not silently rewrite as the modern NGC equivalent.
- Do NOT collapse Antoniadi (I-V) and transparency (1-10) into one rating. They measure different things. Keep both fields, leave one null if the user only said one.
- Do NOT compute or store information about objects the user did not log. The "next clear night" page is constrained to objects in the user's catalogue history.
- Do NOT use the user's voice clips or observation data to train any model. Use the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms.
- Do NOT auto-share. Logs are private. Sharing is explicit, per-object or per-night, per-recipient.
- Do NOT replace the user's raw audio with a transcript. Both are kept. The raw audio is part of the log and is playable from the timeline.
### 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 voice memo → `Observation` schema
Model: `gemini-3.5-flash` · thinkingLevel: medium · Tools: (none)
```
You are parsing a voice memo from an amateur astronomer at the
eyepiece, on a dark-sky night, in the cold. The memo is short —
typically 10 to 90 seconds. The astronomer is reporting a single
observation of a single celestial object.
Languages encountered include English (UK, US, Australian, NZ, South
African, Indian English, Singaporean), Spanish (Iberian and
Latin-American variants), Portuguese (European and Brazilian),
French, German, Italian, Polish, Russian, Greek, Arabic (MSA and
regional), Hebrew, Farsi, Urdu, Hindi, Bengali, Punjabi, Tamil,
Telugu, Tagalog (Filipino), Vietnamese, Mandarin, Cantonese,
Korean, Japanese, Amharic, Swahili, Khmer, Mongolian, and
Indonesian. The astronomer may switch languages mid-sentence,
especially for catalogue identifiers ("Em treinta y uno") and
technical vocabulary.
Catalogue identifiers you must recognise:
- Messier (M1-M110, spoken as "M thirty-one", "em-thirty-one",
"messier thirty-one", "Em treinta y uno")
- NGC (1-7840, spoken with or without leading zeros)
- IC (1-5386)
- Caldwell (C1-C109)
- Bennett's catalogue of southern objects (B1-B158) — common in
South African amateur usage
- Patrick Moore's later lists (occasional)
- Melotte, Collinder, Trumpler, Berkeley, Stock — for open clusters
- Sharpless (Sh2-N), Gum (Gum N) — for H-α emission nebulae
- Bayer designations (α Cen, β Sgr, ω Cen) — Greek letter + IAU
three-letter constellation abbreviation
- Flamsteed (61 Cyg)
- Common names ("47 Tucanae" / "47 Tuc", "Tarantula", "Ring",
"Dumbbell", "Andromeda", "Pinwheel", "Sombrero", "Helix",
"Veil", "Carina Nebula", "Eta Carinae", "Lagoon", "Trifid",
"Jewel Box", "Eagle", "Heart", "Soul")
Output ONLY the Observation JSON matching the provided schema.
Hard rules:
- Preserve the user's verbatim words in `transcript_verbatim`. Every
filler word, every false start, every diacritic. Render Greek
letters (ω, β, α) as the actual Unicode character.
- Preserve the user's free-form description in
`user_description_verbatim`. Do not paraphrase. Do not polish.
Do not translate. If the user said "kinda fuzzy on the south
side", that is what goes in the field.
- Do NOT invent fields. If the user did not state the eyepiece,
leave `eyepiece_focal_length_mm` null AND add the field to
`fields_left_null`. Do not infer "they usually use the 15 mil".
- Resolve catalogue identifiers conservatively. Only resolve to a
catalogue entry that demonstrably exists. If the spoken
identifier is ambiguous (e.g. "two oh seventeen" could be NGC
2017 or Bennett 41 or a misspoken Caldwell), populate
`catalogue_ids` with all plausible candidates and set
`parse_confidence` below 0.8; leave `resolved_object_id` null
and flag for user review.
- Conditions: if the user says "six out of ten conditions", store
`transparency_verbatim: "six out of ten"`. Do NOT auto-populate
`transparency_1_10: 6` — that requires the user's calibration
setting and is handled on the server. Leave the numeric null.
- Antoniadi seeing is I-V. "Seeing II" → `seeing_antoniadi: 2`.
If the user says "seeing four out of ten" or similar
non-Antoniadi phrasing, that is a transparency-style guess —
leave Antoniadi null, store the phrase in
`seeing_verbatim`, and let the server flag for review.
- Variable-star bracket: if the user says "fainter than the 7.4,
brighter than the 7.9", store both comparison stars with
`brighter_than_target` correctly set. Do NOT collapse to a
midpoint unless the user explicitly stated a single estimate
("I make it about 7.6").
- Eyepiece focal length: spoken in millimetres. "Fifteen mil" =
15. "Two and a half mil" = 2.5. "Thirty-two" = 32. Do NOT
convert to inches.
- `fields_left_null` must be the complete list of schema fields
the audio did not supply. This is the contract that prevents
silent invention.
- `flagged_for_user_review` names any field where confidence is
below 0.7 with a one-sentence reason.
No commentary. JSON only.
```
---
### Call: Resolve a catalogue identifier ("Bennett 95" → object)
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none)
```
You receive a catalogue identifier (catalogue + identifier string)
and return the canonical object record:
- resolved_object_id (the app's canonical id)
- resolved_object_name (e.g. "NGC 3521 (the Southern Pinwheel
Galaxy)")
- ra_j2000_hours, dec_j2000_deg
- apparent_magnitude
- object_type (see schema)
- alternate_designations[] (every catalogue this object appears in)
Hard rules:
- Resolve only to known catalogue entries. Do NOT invent. If the
identifier does not match any catalogue, return
`resolved_object_id: null` with a one-sentence reason.
- Bennett's catalogue numbers map to specific NGC/IC objects in
the Southern Hemisphere — wire the lookup table; do not
guess from numeric proximity.
- "47 Tuc" is the common name for NGC 104 / Caldwell 106 /
Bennett 2 — a globular cluster in Tucana. Always resolve via
the common-name index before falling back to numeric parsing.
- Greek-letter Bayer designations resolve to the IAU three-letter
constellation abbreviation, e.g. "ω Cen" → ω Centauri (the
globular cluster, NGC 5139, Caldwell 80, Bennett 70).
- Common names map to the most widely-accepted catalogue entry.
"Ring Nebula" → M57 / NGC 6720.
No commentary outside the structured output.
```
---
### Call: Disambiguate ambiguous catalogue speech
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding
```
You receive a spoken catalogue phrase that did not resolve
uniquely. Your task: surface plausible candidates with one-line
context for each, so the user can pick.
Given the spoken phrase, the user's hemisphere, the date and
local time, and the user's recent observing context (last 10
objects), return:
- candidates[] — each with catalogue + identifier + canonical
name + one-sentence why-this-might-be-what-they-meant
- recommended_default — the most likely candidate given context
- one citation URL per candidate from grounded search
Hard rules:
- Prefer objects above the horizon at the user's site at the
observation time.
- Prefer objects in the user's hemisphere; for southern
observers, prefer Bennett catalogue when the spoken identifier
could be Bennett or Caldwell.
- Do NOT silently pick. Surface every reasonable candidate.
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 citation URLs from the response's
`groundingMetadata.groundingChunks[].web.uri` — do NOT ask the
model to include URLs in the JSON body; it will hallucinate them.
No commentary outside the JSON.
```
---
### Call: Generate clear-sky forecast for the user's site
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: `google_search` grounding
```
You receive the user's site coordinates, local timezone, and the
date window the user is asking about (default: tonight + next 7
nights). Your task: return a per-night summary of cloud cover,
seeing/transparency estimates, moon phase + altitude, and
astronomical twilight times.
Prefer the user's locale's authoritative service in this order:
- South Africa: SAWS
- Namibia / Botswana: SAWS regional or Meteoblue
- Australia: BoM
- New Zealand: MetService NZ
- Japan: JMA
- USA: NOAA GFS + Astrospheric
- UK / Europe: Yr.no + Meteoblue
- South America: Servicio Meteorológico Nacional (AR), DMC (CL)
- South Asia: IMD (IN) + local services
- East Asia: KMA (KR), CMA (CN)
- South-East Asia: BMKG (ID), PAGASA (PH), MetMalaysia (MY)
- Middle East: IMO (IL), local services
- North Africa: services where authoritative
Hard rules:
- Always ground via search; never fabricate a forecast.
- Return JSON in the text body (responseSchema + google_search
is not supported in a single call).
- Astronomical twilight (sun -18°) is computed on-device from the
vendored ephemerides library; you only return cloud, seeing,
and transparency.
- If the forecast service does not publish a seeing/transparency
metric, return null for that field — do not invent.
Output the response as JSON in the text body. Server-side parses
and pulls citation URLs from
`groundingMetadata.groundingChunks[].web.uri`.
No commentary outside the JSON.
```
---
### Call: Generate per-object timeline summary
Model: `gemini-3.5-flash` · thinkingLevel: low · Tools: (none, long context)
```
You receive every observation in the user's log for a single
object (e.g. every time the user has logged M31). Your task:
produce a structured timeline summary suitable for the per-object
view, with each entry quoting the user's own description.
Hard rules:
- Quote, do not summarise. Each timeline entry contains the
user's verbatim description, the date, the site, the
instrument, and the conditions.
- Order chronologically, oldest first.
- Surface patterns ONLY if they are supported by every
observation that addresses them. "User has noted the dust lane
on 4 of 7 observations" is acceptable; "User often sees the
dust lane" is not.
- Do NOT add observations the user did not make. If the user has
never seen this object from the Karoo site, do not say
anything about how it might look from there.
Output: a structured timeline JSON. No commentary.
```
---
### Call: "What did I miss this season?" archive analysis
Model: `gemini-3.5-flash` · thinkingLevel: high · Tools: (none, long context)
```
You receive the user's full observation log AND a list of
catalogue objects above the user's horizon during the current
season at the user's site. Your task: identify objects the user
has historically observed in this season in past years but has
not yet observed in the current season.
Hard rules:
- Compare seasons by the user's local astronomical year, not the
calendar year (a Southern-Hemisphere observer's "winter Milky
Way season" is May-August).
- "Missed" means "observed in past years in this same season,
but not yet observed in the current season". Do NOT include
objects the user has never observed; this is not a
recommendation engine, it is a memory aid.
- Include the user's verbatim last description per missed object,
so the user remembers what they liked about it.
- Order by altitude × clear-sky-forecast in the next 7 nights.
Output: a structured "missed objects" list. No commentary.
```
---
### Call: Generate monochrome finder chart
Model: `gemini-3.1-flash-image` · n/a · n/a
```
Generate a finder chart image for a single deep-sky object,
intended to be displayed under the user's night-mode shader. The
output must be:
- Monochrome — black background, white star field, white catalogue
object glyph. The night-mode shader handles the deep-red tint
in the UI; the source asset is pure greyscale.
- Star magnitudes scaled so the user's pinned NELM is the faintest
visible star.
- Field of view matching the user's selected eyepiece + scope true
FOV (passed in as a parameter).
- Catalogue object glyph at field centre, with the object name and
apparent magnitude in a small label.
- No constellation art, no marketing decoration, no gradients.
- 1024 × 1024 px, PNG, transparent background.
The chart is cached per (object, scope, eyepiece) tuple in
Firebase Storage and re-used for every observation with that
combination.
```
---
### Call: Generate spoken upcoming-object alert
Model: `gemini-3.1-flash-tts-preview` · n/a · n/a
```
Voice: warm, very quiet, unhurried. Pick the Gemini 2.5 Flash TTS
voice whose `languageCode` matches the user's UI language —
pronunciation will follow that locale automatically. Prefer a
darker, quieter voice option where available (the user is at the
eyepiece in the dark; a bright voice ruins night-adapted vision
even through earbuds).
The alert text is a single short sentence, e.g.:
- "Omega Centauri clears your southern ridge in four minutes."
- "Eta Carinae transits in six minutes."
- "M42 is now thirty degrees above your eastern horizon."
Pre-process the text before sending it to TTS:
- Expand catalogue identifiers to their spoken form ("M42" →
"Em forty-two", "ω Cen" → "Omega Centauri", "ζ UMa" → "Zeta
Ursae Majoris"). The pre-processor lives in
`/lib/tts-preprocess.ts`.
- Insert a single ellipsis (`…`) at any internal pause. Gemini
2.5 TTS does not support SSML `` — these textual
cues are how you signal pace.
- Target rate: ~120 words per minute — calm, not urgent.
Style direction: prepend ONE short directive sentence to the text
input, exactly like: "Speak softly, as if the listener is at the
eyepiece in the dark. …". 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 (Greek letter pronunciations, IAU constellation
abbreviations) are NOT exposed by Gemini 2.5 TTS — no SSML
`` tag. Use the pre-processor to write out the spoken
form ("Omega Centauri" not "ω Cen"), and trust the chosen voice's
native pronunciation for everything else.
```
## 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 dark-sky weekend.** Pieter, a retired engineer from Cape Town, drives to the SAAO Sutherland visitor's site for the new-moon weekend with his 10-inch Dobsonian. Friday night he logs 23 objects between 21:00 and 03:30. The app stores raw audio + parsed observations entirely offline; Sunday afternoon, back on his home Wi-Fi, the queue uploads and reconciles. He scrolls through the log over coffee, plays the voice memo from his sighting of ω Cen at zenith, and remembers exactly how he described it.
- **The remote-control observatory between shifts.** Tlolomi works the SALT remote-control desk in Cape Town three nights a week — gigantic professional-grade research telescope, photons going to peer-reviewed papers. But she keeps a personal log for the eyepiece time she carves out at the visitor scope between shifts. The app understands she has two sites — SALT-visitor-scope and home — and never confuses them.
- **The southern observer underserved by Northern apps.** Mereana, a high-school physics teacher in Tekapo, New Zealand, has the entire Mackenzie Basin dark-sky reserve as her back garden. Every Northern app she has ever tried defaults to a horizon centred on +40° latitude and pushes the LMC and SMC off-screen. Telescope Log starts from her actual −44° latitude and her actual horizon profile. The Magellanic Clouds and ω Cen are the home page, not an edge case.
- **The Mongolian steppe observer.** Naranbaatar takes a battered Russian 80 mm refractor onto the open steppe outside Ulaanbaatar twice a month. He logs in Mongolian, in voice. The app parses the Mongolian audio, resolves the catalogue identifiers (he says them in English: "em thirty-one"), and stores his free-form description in Mongolian, in his own words.
- **The Filipino backyard astronomer in Manila.** Aileen, an OFW who came back to Quezon City after a decade in Hong Kong, logs Jupiter every clear week from her rooftop. She talks in Tagalog, slips into English for the catalogue ("Jupiter, GRS transit, eleven point six magnitude"), and the app parses both.
- **The variable-star contributor.** Asha in Bengaluru observes 14 variables most clear nights and reports them to AAVSO. She uses the comparison-star bracket workflow ("fainter than the 9.2, brighter than the 9.6"). The app stores the bracket; she exports a WebObs CSV at the end of the month.
- **The public-night outreach session.** Lior runs the monthly public night at the Tel Aviv University rooftop scope. 80 visitors queue up, each gets 30 seconds at the eyepiece. He toggles outreach mode; the log captures the show order, the public's questions ("is that a real planet, or a picture?"), and produces a one-page handout for the secondary-school group that came as a class trip.
- **The Patagonian astrophotographer.** Camila in El Chaltén logs guided exposures, not visual observations. Per sub-frame voice memos ("M83, 300 sec sub, OIII filter, ZWO 2600, guide RMS 0.8") parse into a structured log she can later cross-reference with her PixInsight project files.
- **The Kerala monsoon observer.** Joseph in Thiruvananthapuram has nine clear nights in a year. The "next clear night" page is his lifeline — when the southwest monsoon breaks and a forecast cell opens for a single Tuesday night, the app surfaces every object he has logged that will be above 30° at that exact window.
## 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 hand on a Dobsonian focuser at the eyepiece, deep red headlamp light, the southern Milky Way out of focus in the background. One paragraph: "Telescope Log is a voice-first observing notebook — for the eyepiece, in the cold, on any sky." Single Google sign-in button; Apple sign-in next to it. Below: "Try with the sample log" → loads a demo log of 14 observations from the seed content in section 8a.
2. **First-run setup — site + horizon + scopes.** A three-step setup, skippable but recommended: (a) Add your home site (latitude/longitude from device GPS, with manual override; Bortle estimate; preferred catalogue). (b) Walk through the sky once: tilt the phone around 16 azimuth bins to record your local horizon angle. (c) Add your scopes and eyepieces (focal length, AFOV, label). Each step takes under a minute.
3. **The eyepiece — primary capture screen.** Deep-red full-screen background. One enormous record button (>= 96 × 96 px) in the centre. Above it, three small chips: the current site (default home), the current scope (default last-used), the current eyepiece (default last-used) — tap any to swap before recording. Below: the last observation's verbatim quote, faint, as a reminder that the previous one was captured. The whole screen is glove-compatible — no small touch targets, no swipe-only gestures.
4. **Processing queue.** Vertical list of pending observations. Each shows a thumbnail of the catalogue object, the spoken identifier, and an honest step-by-step bar: "Listening to the audio…" → "Resolving catalogue…" → "Computing altitude…" → "Done". Each step takes 3-10 seconds. The user can keep recording; new captures queue behind.
5. **Observation detail view.** A two-column layout on desktop, stacked on mobile. Left column: the catalogue object's basic data (RA/Dec, magnitude, type, altitude at observation, hour angle), the finder chart at the user's selected FOV, the audio player. Right column: the structured fields (date, site, instrument, conditions), the user's verbatim description in a quoted card with large serif type, the transcript expandable below. A small `(i)` icon next to any field reveals "this came from your audio at 0:14".
6. **Per-object timeline.** Every time the user has logged this object, in chronological order. Each row: date, site, scope/eyepiece, conditions, the user's verbatim quote. Filter by site, by year, by conditions. The top of the page shows the canonical object: catalogue identifiers, RA/Dec, apparent magnitude, type, a high-quality reference image (Hubble/ESO) and the user's-eyepiece finder chart side by side.
7. **Archive view.** Magazine-grid of objects (not observations) — every distinct catalogue object the user has ever logged. Each card shows the object's reference image, the canonical name, the count of times observed, and the user's most recent quote. Filter by catalogue, by type, by hemisphere. Sort by recency or by lifetime count.
8. **Next clear night.** A timeline-graph of the next 7 nights, with a clear-sky forecast strip per night. Below the strip: for each night, the ranked list of the user's catalogue objects that will be above their horizon during astronomical darkness — rise / transit / set times, peak altitude, moon distance. Tap an object to see its previous observations.
9. **Sites + horizons.** The user's pinned sites with their horizon profiles. Edit horizons by re-walking the sky or by manual adjustment of the 16 azimuth bins. Add new sites for travel.
10. **Scopes + eyepieces.** Pinned instruments with focal lengths and AFOVs. Computed magnification and true FOV per scope+eyepiece pair. Add new instruments by typing focal length + name; the app does not require a model database.
11. **Outreach mode.** A simplified capture flow for public nights: the record button is replaced by a "show this object now" button and a "next visitor" button. The log captures the show order and any overheard public questions. Toggle from the header.
12. **Exports.** Download as JSON, CSV, AAVSO WebObs CSV (variable-star observations only), or PDF (typeset annual observing journal). Each export is a single click; the PDF generation runs server-side and takes 20-60 seconds for a year of observations.
13. **Settings + capabilities.** Account, sites, scopes, eyepieces, calibration (NELM-from-conditions-rating), language, units (mm/inches for eyepiece labels, °C/°F for temperature), reduced-motion preference, the persistent `(i)` capabilities panel.
14. **Footer.** "Made for the eyepiece, not the phone." Privacy: "Your log is yours." 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 Telescope Log."
- Subhead: "A voice-first observing notebook for any sky, any catalogue, any latitude."
- One paragraph (≤ 60 words) explaining who this is for and what makes it different from a generic planetarium app: it never invents an observation, it speaks every catalogue including Bennett and Patrick Moore's lists for the Southern Hemisphere, and the "next clear night" page works from the user's real coordinates and real horizon — not a Northern-default that hides everything south of −30°.
- Visual: a small annotated illustration of the eyepiece-capture screen with the record button, the site chip, the scope chip, and the eyepiece chip labelled.
**Slide 2 — Try it now.**
- One short prompt: "Try with the sample log."
- A live demo input pre-loaded with the sample audio from section 8a — a 22-second voice memo of a user logging M31 from the Karoo.
- 1-2 sentences pointing at *the specific page elements* where the Gemini magic happens (the verbatim quote preservation, the `fields_left_null` honesty, the finder chart at the user's chosen FOV).
**Slide 3 — How to remix this.**
- Headline: "Make this yours."
- Three short bullets:
- "Swap the sample log in `/data/seed-log/` for your own observations."
- "Adjust the system prompts in `/server/prompts/` for your hemisphere or catalogue preferences."
- "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 (audio reasoning)** — parses your voice memo at the eyepiece end-to-end. Catalogue identifier, time, eyepiece, conditions, free-form description — all from one short clip. The free-form description is your words, verbatim, never re-narrated.
- **Gemini 3.5 Flash (multilingual)** — voice input in 24+ languages including Tagalog, Vietnamese, Mandarin, Korean, Tamil, Hindi, Bengali, Punjabi, Amharic, Swahili, Farsi, Khmer, Mongolian, alongside the European set. Catalogue identifiers stay catalogue identifiers regardless of input language.
- **Gemini 3.5 Flash (structured output)** — every observation is typed against a schema. Fields the audio didn't supply remain null. This is the contract that prevents the app from inventing observations.
- **Gemini 3.5 Flash (long context)** — once your log grows past a few hundred observations, the per-object timeline and "what did I miss this season?" analysis read your whole log at once. Up to ~1,500 observations comfortably; beyond that we chunk by year.
- **Gemini 3.5 Flash + grounded search** — the "next clear night" page pulls real cloud-cover and seeing forecasts from your locale's authoritative service (SAWS, BoM, MetService NZ, NOAA, Yr.no, JMA, IMD, KMA, BMKG, PAGASA, etc.). Sources are cited.
- **Gemini 3.5 Flash + grounded search (catalogue disambiguation)** — when your spoken identifier could match two catalogue entries, the app surfaces both with one-line context rather than guessing.
- **Nano Banana 2 (`gemini-3.1-flash-image`)** — generates a monochrome finder chart per object at your selected eyepiece FOV. Cached per (object, scope, eyepiece) tuple.
- **Gemini TTS** — soft spoken upcoming-object alerts ("Omega Centauri clears your southern ridge in four minutes"), in the language of your UI, at a calm pace.
- **Firebase Auth** — Google and Apple sign-in, magic-link sharing for the rare case you want a club mate to see one night.
- **Firestore** — stores your log, syncs across devices in real time.
- **Firebase Storage** — keeps your raw voice clips at upload quality, forever.
- **On-device ephemerides** — rise / transit / set times are computed locally from the vendored Astronomy Engine library; this is not a Gemini call. Predictions work offline.
- **Cost note** — see the detailed breakdown in 6d. A typical dark-sky weekend of 25 observations costs about $0.40 of Gemini API spend total, processed once. Ongoing clear-sky-forecast refresh: a few cents per week.
- **Privacy note** — your log is private to you. This app uses the Gemini API on the paid tier, where Google does not use your content for model training, per the Gemini API Additional Terms. Raw voice clips go to Gemini via the Files API for the single parse call and are deleted from Gemini's side per the Files API retention policy.
**Backend services this app depends on:**
- Auth: see section 4b
- Database: see section 4b
- Storage: see section 4b — note that Firebase Storage is NOT auto-provisioned by AI Studio Build and must be enabled in the Firebase console before the first observation is recorded
- Email: see section 4b — optional, only if you want magic-link sharing; requires sender-domain authorisation
- Payments: see section 4b (not used in v1)
- External APIs: see section 4b — Gemini only; ephemerides on-device
**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)
- `MAPBOX_TOKEN` — optional, only if you want an all-sky base map richer than the vendored one
- `AAVSO_API_KEY` — optional, only if you want direct WebObs submission rather than CSV export
**Cost + privacy notes:**
- One short paragraph per cost-sensitive capability: long-context calls are billed per token of input — a 500-observation log archive analysis costs about $0.20 each time it runs (default: weekly, on demand).
- One short paragraph on privacy: where the data lives (your Firebase project), how to delete it (Settings → "Delete this log forever" — gone in 60 seconds), what is never sent for training, and the Files API retention policy for voice clips.
**Documentation links:**
- AI Studio Build docs
- Gemini API audio-reasoning, multilingual, long-context, image-generation, TTS docs
- Firebase Auth, Firestore, Firebase Storage docs
- Astronomy Engine (vendored ephemerides library) docs
- AAVSO WebObs submission format 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 voice memo (Gemini 3.5 Flash, medium thinking, audio input)** — typical 30-second clip ≈ ~3,000 input tokens (audio is billed at ~32 tokens/sec of audio in 2026 pricing) + ~600 output tokens. ~$0.007/observation.
- **Catalogue resolver (Gemini 3.5 Flash, low thinking)** — small lookup call, ~200 tokens in/out. ~$0.0001/object resolution. Most resolutions hit the local cache after first lookup.
- **Disambiguation (Gemini 3.5 Flash + grounded search)** — only triggers on ambiguous spoken identifiers, ~5% of observations. ~$0.002/disambiguation.
- **Clear-sky forecast (Gemini 3.5 Flash + grounded search)** — runs once per site per refresh window (default: every 6 hours). ~$0.001/refresh. Negligible.
- **Per-object timeline summary (Gemini 3.5 Flash, low thinking, long-context per object)** — runs on-demand when the user opens a per-object view. ~$0.005 per timeline of 10 observations.
- **"What did I miss this season?" archive analysis (Gemini 3.5 Flash, high thinking, long-context)** — long-context input of full archive, runs once per request. ~$0.20 per archive of 500 observations.
- **Finder chart (Nano Banana 2)** — ~$0.03 per chart. Cached per (object, scope, eyepiece) tuple, so a user observing 20 distinct objects with 3 eyepieces ≈ 60 charts ≈ ~$1.80 lifetime. Negligible on an ongoing basis.
- **TTS upcoming-object alert (Gemini 2.5 Flash TTS)** — billed per output token (~$10/M output tokens), effectively ~$0.000003/character. A typical 12-word alert ≈ $0.0002. Used sparingly: a typical session triggers 1-3 alerts.
- **Expected per-observation cost on first ingest:** ~$0.008. **Dark-sky weekend of 25 observations:** ~$0.40 total (including a finder-chart cache fill for new objects). **Ongoing weekly archive refresh** (500-observation log): ~$0.20/week.
- **Audio storage:** Firebase Storage standard tier, ~$0.026/GB/month. A 30-second AAC voice clip ≈ 0.5 MB; a 1,000-observation log ≈ 500 MB ≈ ~$0.013/month.
## 7. Design language
- **Mood:** A field notebook that happens to live on your phone. Not a planetarium app, not a SaaS dashboard. The damp folding table next to the Dobsonian at 03:00 with the red headlamp and the cold flask of coffee. The pages of the notebook are pencil and paper; the app is what you bring when the paper goes wet.
- **Typography:** A geometric monospace for catalogue identifiers, coordinates, and numeric fields (Berkeley Mono or JetBrains Mono — the numbers carry weight). A warm serif for the user's verbatim quote in the observation detail view (Source Serif Pro or Adobe Caslon Pro) — the quote is the heart of the entry and it deserves serif. A clean grotesque for app chrome (Inter or Geist).
- **Palette:** **Night-mode default** — deep red background `#1B0405` for the eyepiece screen and dark-sky weekend, near-black `#0A0A0A` for the rest. Body text in dim red `#A02828` at the eyepiece, in warm bone `#E8DCC0` elsewhere. Accent: faint amber `#C9883A` for the "next clear night" timeline strip (the colour of a fading sunset). Faint green `#3E7C5A` for the "above horizon now" status chip. Faint blue `#3A5773` for the user-added tags and the AAVSO export chip. Daylight-mode is an explicit toggle, never the default — most users open the app in or near darkness.
- **Imagery:** The reference images of catalogue objects are the hero on the per-object pages (use ESO/Hubble public-domain imagery via API where available, with attribution). At the eyepiece, no imagery at all — just the red record button and the chips. The finder charts are pure monochrome line work. Period star atlases (Norton's, Sky Atlas 2000.0) are referenced visually in the chart style but never copied.
- **Hand-feel touches:** A barely-visible grid texture on the eyepiece screen, like the grid on the back of a notebook page. The record button "absorbs" the tap with a brief inward press, then exhales as recording begins. The "next clear night" timeline strip has a gentle horizontal gradient that hints at the path of the sun without ever rendering a literal sun.
- **Spacing:** consistent 4-px base. Generous whitespace — the eye comes from a dark sky and the screen should not crowd it.
- **Radius:** consistent token set (e.g. 8 / 16 / 24 px). Catalogue-id chips use 8; observation cards use 16; the record button uses 48 (almost circular).
- **Shadows:** none at the eyepiece (the red background and the dim screen make shadows redundant). Subtle, layered, warm-toned shadows in daylight mode.
- **Motion:** purposeful — entrance fades, the record button's exhale, the per-object timeline's vertical scroll, the "next clear night" page's horizontal scroll-snap between nights. Respect `prefers-reduced-motion` everywhere; reduced-motion users see instant transitions. No bouncing splash animations. The reduced-motion check on this template is the "above-horizon" status chip — verify that, with reduced motion, it does not pulse.
- **States:** every interactive element has hover, focus, active, disabled. Loading uses skeletons not spinners where possible. Empty states have helpful next-action guidance ("Tap the record button. Say the object's name and what you see.").
## 8. Content generation rules
- Write **realistic, specific copy**. NO Lorem Ipsum. NO generic placeholders like 'Your tagline here'.
- Invent plausible names, dates, locations, catalogue objects, quotes, sample voice memos that fit the domain (use the seed content in section 8a as a starting point). When inventing, lean on real astronomical practice — Antoniadi seeing, NELM estimation, the way amateurs actually describe what they see at the eyepiece — but never claim that a fictional observation is an actual archived AAVSO report.
- Tone: warm, direct, free of corporate language. This template is for a person, not a company. No "Empower your astronomy". No "Revolutionize your observing".
- Headlines: punchy and concrete. No "Empower your X" filler.
- Body copy: short paragraphs (2-4 sentences). Use lists where appropriate.
- Plain language. Avoid jargon — except where the user already speaks the jargon (Antoniadi I-V, Bortle, AAVSO, NELM, transit time, hour angle). The astronomer wants the jargon to be honoured, not hidden.
- Where the app outputs AI-generated content (a per-object timeline summary, a "missed this season" hint), never label it as "AI says". Use small uncertainty cues only where epistemic honesty requires them (a low-confidence catalogue resolution shows the candidate list rather than a single answer).
## 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 logs (sidebar):**
- "Pieter's Karoo Log" (174 observations, 2024-08 to current) — South African Dobsonian observer, mostly Bennett-catalogue Southern objects, primary site Sutherland Visitor Area; secondary site, his back garden in Stellenbosch.
- "Mereana's Tekapo Log" (312 observations, 2023-04 to current) — New Zealand high-school physics teacher, mostly Magellanic-Cloud field objects and Southern globulars, single site at home (Lake Tekapo Dark Sky Reserve).
- "Naranbaatar's Steppe Log" (88 observations, 2025-01 to current) — Mongolian observer on the open steppe outside Ulaanbaatar, mostly Caldwell list, voice in Mongolian.
- "Asha's Variable Stars" (1,420 observations, 2021-03 to current) — Bengaluru AAVSO contributor, almost entirely variable-star observations, structured bracket workflow.
- "Lior's Public Nights" (96 sessions, 1,840 individual public observations) — Tel Aviv University rooftop outreach archive, every monthly public night since 2024-09.
- "Camila's Astrophotography Log" (412 sub-exposures, 38 distinct targets) — El Chaltén imaging archive, ZWO ASI 2600MM + ASI 533MC, dual rig.
**Sample observation in detail view (this is what the demo should show):**
- **Recorded at (verbatim audio):** 22 seconds. The user, in soft English, says: "Em thirty-one, twenty-three fourteen, eight magnitude, fifteen mil eyepiece, conditions six out of ten, faint dust lane visible with averted vision, no companion galaxies tonight, dew starting on the corrector plate."
- **Transcript verbatim:** "em thirty-one twenty-three fourteen eight magnitude fifteen mil eyepiece conditions six out of ten faint dust lane visible with averted vision no companion galaxies tonight dew starting on the corrector plate"
- **Recorded at UTC:** 2025-08-19T21:14:00Z (the user's local 23:14 in CAT)
- **Site label verbatim:** "Karoo"
- **Resolved site:** Pieter's Karoo site (latitude −32.376°, longitude 20.811°, Bortle 1)
- **Catalogue IDs:**
- catalogue: messier, identifier: "31", spoken_verbatim: "em thirty-one"
- **Resolved object:** M31 (Andromeda Galaxy), NGC 224, type galaxy, apparent magnitude 3.4, RA 00h 42m 44s, Dec +41° 16′ 09″
- **Instrument:**
- scope label verbatim: null (user did not state — using last-used: "10-inch Dobsonian, 1200 mm focal length")
- eyepiece label verbatim: "fifteen mil"
- eyepiece focal length: 15 mm
- magnification: 80×
- true FOV: ~1.0° (assuming 80° AFOV eyepiece)
- filter: none stated
- **Conditions:**
- transparency verbatim: "six out of ten"
- transparency 1-10: null (user has not set calibration)
- seeing antoniadi: null
- seeing verbatim: null
- NELM estimate: null
- NELM method: not_stated
- dew state: "forming" (parsed from "dew starting on the corrector plate")
- moon phase: computed server-side — new moon −2 days (waning crescent)
- moon altitude: −34° (below horizon)
- **User description verbatim:** "faint dust lane visible with averted vision, no companion galaxies tonight, dew starting on the corrector plate"
- **User added tags (after the fact, by Pieter):** "dust lane", "averted vision"
- **Variable star:** null
- **Public night:** null
- **Computed at observation:** altitude 28° above eastern horizon, azimuth 67° (ENE), hour angle −4.1h
- **Fields left null:** ["scope_id (used last-used)", "scope_label_verbatim", "transparency_1_10", "seeing_antoniadi", "seeing_verbatim", "nelm_estimate", "wind_kph", "temp_c", "variable_star", "public_night"]
- **Parse confidence:** 0.94
**Sample variable-star observation (Asha's log):**
- **Audio (verbatim):** "RR Lyrae, oh-two-thirty, AAVSO chart X25467, fainter than the 7.4, brighter than the 7.9, comp pair clean, no haze."
- **Resolved object:** RR Lyrae (the prototype RR Lyrae variable, in Lyra), GCVS RR LYR
- **Variable star:**
- is_variable_star_observation: true
- aavso_chart_id: "X25467"
- comparison_stars: [{label: "the 7.4 comp", magnitude: 7.4, brighter_than_target: true}, {label: "the 7.9 comp", magnitude: 7.9, brighter_than_target: false}]
- bracketed_magnitude_low: 7.4
- bracketed_magnitude_high: 7.9
- midpoint_estimate: null (user did not state a single value)
**Sample public-night observation (Lior's log):**
- **Audio (verbatim):** "M57 ring nebula, eyepiece thirteen mil, fifth visitor, the kid asked if it's a real picture or a drawing."
- **Public night:**
- is_outreach_observation: true
- public_questions: ["is that a real picture or a drawing?"]
- show_order_index: 5
**Sample input artefacts (for the build to demonstrate):**
- A 22-second voice memo of a Southern Hemisphere amateur logging M31 from the Karoo, in English.
- A 35-second voice memo in Mongolian logging C14 (the Double Cluster) — the catalogue identifier said in English, the description in Mongolian.
- A 14-second voice memo in Tagalog logging Jupiter from a Quezon City rooftop — switching to English for "GRS transit, eleven point six".
- A 41-second AAVSO-format voice memo logging RR Lyrae with comparison-star bracket.
- A 12-second outreach voice memo: "M57 ring nebula, eyepiece thirteen mil, fifth visitor, the kid asked if it's a real picture or a drawing."
**Sample voice copy:**
- Onboarding: "Tap the red button. Say the object's name and what you see."
- Processing: "Listening to the audio…" / "Resolving Em thirty-one…" / "Computing altitude from your site…" / "Drawing the finder chart…"
- Empty log: "This log is waiting for its first observation. Tap the record button and say what you're looking at."
- Error (couldn't resolve catalogue): "I heard a catalogue identifier but couldn't pin it down. Was it M31, NGC 31, or Caldwell 31?"
- Low-confidence resolution: "I think you said Caldwell 14 (the Double Cluster) — tap to confirm or pick another."
- Save confirmation: "Added M31 to Pieter's Karoo Log — seventh time you've logged this galaxy."
- Field left null: "I didn't catch the eyepiece — tap to add it later if you want."
- Above horizon now: "ω Centauri is 31° up, transiting in 42 minutes."
- Below horizon: "M42 rises at 21:08 tomorrow night."
**Sample magic-link share email:**
- Subject: "Pieter — one night from my Karoo trip. M83 in the new dob."
- Body: "Asha, you'd have loved this one. Here's the single observation from Tuesday — M83 from −32°, dust lanes clean. Tap to view." [Open Observation]
## 9. Media & assets
- **Hero image (landing screen):** A photographed-looking shot of a hand on a Dobsonian focuser at the eyepiece, deep-red headlamp light, the southern Milky Way faintly visible behind. Generate via Nano Banana 2 with a prompt emphasising "Dobsonian focuser detail, red headlamp glow on the hands, southern Milky Way arc in the background, real cold night-time air, no people in frame, no logos, no constellation art overlay".
- **App icon / wordmark:** Set in the geometric monospace. A single small red dot (the M31 marker) anchors the wordmark. No telescope icon — the word is the icon.
- **Empty-state illustration:** A single line drawing of an eyepiece end-on, with the central field circle empty. Hand-drawn aesthetic, not a flat icon.
- **Finder charts:** Generated per the prompts in section 4b — Nano Banana 2 monochrome line work, the night-mode shader handles the deep-red tint. Each chart is 1024 × 1024 px, cached in Firebase Storage.
- **Reference imagery for catalogue objects:** Source from ESO/Hubble public-domain APIs with attribution. Never use AI-generated images for the canonical reference picture of a real catalogue object — too easy to drift into the "looks pretty but is not what you'll see" trap.
- **Stock fallbacks:** If image generation fails, fall back to the photographed sample finder chart from `/public/samples/sample-finder-m31.png`. Never to a "🔭" emoji.
- **Generated imagery:** prefer Nano Banana 2 for the finder charts and hero. Prompt for restraint and astronomical accuracy — never decorative star bursts or fictional nebula colours.
- **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"). Example: "Eyepiece focal length must be between 2 mm and 100 mm — typed 250".
- 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: "Tap to record an observation", "Add your home site", "Walk through the sky to record your horizon" — 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 ("I couldn't hear what catalogue you said — tap to retype the identifier") and offer retry. The raw audio is always preserved so the user can keep trying.
- Low-confidence catalogue resolutions show all candidates with a clear "pick one" affordance — never a silent default.
- The night-mode shader is the **default** on first launch between local civil sunset and civil sunrise (computed from device location); daylight mode is an explicit toggle in the header.
- The record button has a 2-second pre-roll buffer so the first syllable is never clipped.
- Camera-button-as-shutter: if the device exposes a hardware shutter key (Web Camera API + key binding), users can re-bind it to "start observation" in settings.
## 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 the audio parse / per-object timeline / archive-wide analysis, `gemini-3.5-flash` for catalogue resolution / disambiguation / forecast, `gemini-3.1-flash-image` for finder charts, `gemini-3.1-flash-tts-preview` for alerts. Set `thinkingLevel` explicitly per call.
- **Database:** Firestore (auto-provisioned by AI Studio Build). Show the seed log on first launch.
- **Auth:** Firebase Auth — Google sign-in by default; Apple sign-in next to it; magic-link email as fallback for cross-device sign-in.
- **Storage:** Firebase Storage for raw voice clips and cached finder charts. Pre-signed URLs only.
- **Ephemerides:** vendored Astronomy Engine JS library in `/lib/` (MIT licensed). All rise/transit/set calculations are on-device and offline.
- **Audio capture:** Web Audio API + MediaRecorder, recording in `audio/webm; codecs=opus` at 48 kHz where available. Falls back to `audio/mp4; codecs=mp4a.40.2` on Safari iOS.
- **iOS Safari gotchas (graceful degradation):** mic permission does NOT persist across page reloads on iOS — re-request on every observation; an incoming call interrupts the audio session (`MediaStreamTrack.onmute` fires) — auto-pause and prompt resume; backgrounded Safari tabs pause `getUserMedia` and throttle the offline queue — keep the screen Wake Lock on while observations are being voiced; iOS in dark/cold conditions may also throttle CPU — surface a "saving locally — will sync when we have signal" note rather than a hard fail.
- **Audio upload:** clips > 20 sec upload to Firebase Storage first, then are passed to Gemini via the Files API (`files/*` resource name). Clips ≤ 20 sec can go inline as base64.
- **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 — the record button sits comfortably above the home indicator.
- Zero horizontal overflow at any width. Zero layout shift on load.
- Persist user data in Firestore. Use real-time listeners on the log view; offline queue with optimistic UI for the dark-sky-weekend case (no signal at the site).
- Service Worker caches the eyepiece screen, the catalogue resolver, and the on-device ephemerides for full offline operation. Sync happens when connectivity returns.
- Wake Lock API holds the screen on during a recording session (the user is at the eyepiece, the phone is in a cradle, the screen must not sleep).
## 12. Accessibility (WCAG 2.2 AA)
- Semantic HTML — `header`, `nav`, `main`, `section`, `article`, `footer`.
- All interactive controls reachable by keyboard with a visible focus ring. The night-mode focus ring is a dim red 2-px outline; the daylight-mode focus ring is a 2-px amber outline.
- Color contrast ≥ 4.5:1 for body, 3:1 for large text and UI components. **Note:** the night-mode palette uses dim red on near-black, which is the standard astronomical convention but does not pass WCAG 2.2 AA contrast as written. The app exposes a "high-contrast night mode" toggle (dim red on pure black, with thicker stroke weights) that meets AA; the default night mode warns the user on first activation that it prioritises dark adaptation over WCAG contrast and offers the toggle.
- All images have meaningful `alt` text. Finder charts have `alt` describing the artefact ("monochrome finder chart for M31 at 80× magnification, 1° true field of view, M31 at centre, surrounding stars to NELM 8.5").
- Form fields have associated `