diff --git a/.llm/README.md b/.llm/README.md
new file mode 100644
index 0000000..c374b12
--- /dev/null
+++ b/.llm/README.md
@@ -0,0 +1,20 @@
+# LLM Workflow Documentation
+
+Documentation for AI assistants working on this project.
+
+## Structure
+
+- **procedure/** - Reusable workflows
+- **features/** - Completed: `done_F-{n}.md`, `done_ISSUE-{n}.md`
+- **tasks/todo/** - Planned: `plan_F-{n}.md`, `plan_ISSUE-{n}.md`
+
+## Workflows
+
+Two separate workflows that never mix:
+
+**F-n (Manual):** `plan_F-6.md` → `done_F-6.md`
+**ISSUE-n (GitHub):** `plan_ISSUE-42.md` → `done_ISSUE-42.md`
+
+- Numbers stay forever (F-6 never becomes F-5 or ISSUE-42)
+- Gaps in numbering expected
+- ISSUE-n imported via `procedure{source_github}` (label: `ready_for_dev`)
diff --git a/.llm/bootstrap-llm-wow.md b/.llm/bootstrap-llm-wow.md
new file mode 100644
index 0000000..3627743
--- /dev/null
+++ b/.llm/bootstrap-llm-wow.md
@@ -0,0 +1,135 @@
+# Bootstrap: LLM Ways of Working
+
+**Invocation:** `bootstrap ways of working from bootstrap-llm-wow.md`
+
+## Creates
+
+```
+.llm/
+├── README.md
+├── bootstrap-llm-wow.md (self-copy)
+├── procedure/import-tasks-github.md
+├── features/ (empty)
+└── tasks/todo/ (empty)
+CLAUDE.md (root)
+```
+
+## Steps
+
+### 1. Check Existing
+`ls -la .llm/` → if exists, ask: Merge | Override | Cancel
+
+### 2. Create Structure
+```bash
+mkdir -p .llm/{procedure,features,tasks/todo}
+```
+
+### 3. Create .llm/README.md
+```markdown
+# LLM Workflow Documentation
+
+## Structure
+- **procedure/** - Reusable workflows
+- **features/** - Completed: `done_F-{n}.md`, `done_ISSUE-{n}.md`
+- **tasks/todo/** - Planned: `plan_F-{n}.md`, `plan_ISSUE-{n}.md`
+
+## Workflows
+**F-n (Manual):** `plan_F-6.md` → `done_F-6.md`
+**ISSUE-n (GitHub):** `plan_ISSUE-42.md` → `done_ISSUE-42.md`
+
+Numbers stay forever. Gaps expected. ISSUE-n via `procedure{source_github}` (label: `ready_for_dev`).
+```
+
+### 4. Self-Replicate
+Copy this file to `.llm/bootstrap-llm-wow.md`
+
+### 5. Create .llm/procedure/import-tasks-github.md
+```markdown
+# Procedure: Import GitHub Issues as Tasks
+
+**Invocation:** `procedure{source_github}`
+
+## Steps
+1. Get repo: `git remote get-url origin` → parse owner/name
+2. Fetch: `gh issue list --label "ready_for_dev" --state open --json number,title,body,labels`
+3. Create: `.llm/tasks/todo/plan_ISSUE-{n}.md` for each
+
+## Template
+# ISSUE-{n}: {Title}
+**GitHub:** #{n} | {url}
+**Created:** {date}
+## Problem/Solution/Steps/Notes
+{from issue or TBD}
+
+## Rules
+- `plan_ISSUE-42.md` → `done_ISSUE-42.md`
+- Skip if exists
+- ISSUE-n ≠ F-n
+- Report: count, files, skipped
+```
+
+### 6. CLAUDE.md
+
+**If missing:** Create with template below + `[PROJECT-SPECIFIC]` placeholders
+**If exists:** Check for "Feature Documentation Process" → add if missing, preserve rest
+
+```markdown
+# CLAUDE.md
+
+## Project Overview
+[PROJECT-SPECIFIC]
+
+## Feature Documentation Process
+
+### Completed Features
+1. Document in `.llm/features/done_F-{n}.md` (same n as plan)
+2. Include: overview, files changed, tests, commits, migrations
+3. Delete plan from `.llm/tasks/todo/`
+
+### Planned Features
+1. Create `.llm/tasks/todo/plan_F-{n}.md`
+2. Include: problem, solution, steps, benefits, effort
+3. Move to `done_F-{n}.md` when done (KEEP NUMBER)
+
+**Plan mode:** Write to `.llm/tasks/todo/plan_F-{n}.md` (NOT `~/.claude/plans/`)
+
+### Feature Numbering
+- Numbers = plan date, not implement date
+- `plan_F-6.md` → `done_F-6.md` (never renumber)
+- Gaps expected
+
+### GitHub Integration
+- ISSUE-n: `plan_ISSUE-42.md` → `done_ISSUE-42.md`
+- Invoke: `procedure{source_github}` (label: `ready_for_dev`)
+- F-n ≠ ISSUE-n (never mix)
+
+## LLM Procedures
+See `.llm/procedure/` for workflows (e.g., `import-tasks-github.md`)
+
+## [PROJECT-SPECIFIC SECTIONS]
+[Add: Best Practices, Commands, Structure, Architecture, Patterns]
+```
+
+### 7. Optional .gitignore
+Ask user to add:
+```
+# .claude/
+# .agents/
+```
+(`.llm/` stays tracked)
+
+### 8. Report
+```
+✅ Bootstrap Complete!
+
+Created: .llm/{README,bootstrap,procedure/import-tasks-github,features/,tasks/todo/}
+Updated: CLAUDE.md [new/merged]
+
+Next:
+- Customize CLAUDE.md
+- Create plan_F-1.md or run procedure{source_github}
+- Commit .llm/
+```
+
+## Replication
+Copy this file to new repo → run invocation → done
diff --git a/docs/features/done_F-1.md b/.llm/features/done_F-1.md
similarity index 100%
rename from docs/features/done_F-1.md
rename to .llm/features/done_F-1.md
diff --git a/docs/features/done_F-6.md b/.llm/features/done_F-6.md
similarity index 100%
rename from docs/features/done_F-6.md
rename to .llm/features/done_F-6.md
diff --git a/.llm/procedure/import-tasks-github.md b/.llm/procedure/import-tasks-github.md
new file mode 100644
index 0000000..53756d6
--- /dev/null
+++ b/.llm/procedure/import-tasks-github.md
@@ -0,0 +1,42 @@
+# Procedure: Import GitHub Issues as Tasks
+
+**Invocation:** `procedure{source_github}`
+
+## Steps
+
+1. **Get repo info:** `git remote get-url origin` → parse owner/name
+2. **Fetch issues:** `gh issue list --label "ready_for_dev" --state open --json number,title,body,labels`
+3. **Create plans:** For each issue → `.llm/tasks/todo/plan_ISSUE-{n}.md`
+
+## Plan Template
+
+```markdown
+# ISSUE-{n}: {Title}
+
+**GitHub:** #{n} | https://github.com/{owner}/{repo}/issues/{n}
+**Created:** {date}
+
+## Problem
+{from issue body or TBD}
+
+## Solution
+{from issue body or TBD}
+
+## Steps
+{from issue body or TBD}
+
+## Notes
+{additional context}
+```
+
+## Rules
+
+- Naming: `plan_ISSUE-42.md` → `done_ISSUE-42.md`
+- Skip if plan exists
+- ISSUE-n separate from F-n (never mix)
+- Report: count, titles, files created, skipped
+
+## Edge Cases
+- No `gh` CLI: provide install instructions
+- Not authenticated: run `gh auth login`
+- Empty issue body: use TBD placeholders
\ No newline at end of file
diff --git a/docs/tasks/todo/plan_F-2.md b/.llm/tasks/todo/plan_F-2.md
similarity index 100%
rename from docs/tasks/todo/plan_F-2.md
rename to .llm/tasks/todo/plan_F-2.md
diff --git a/docs/tasks/todo/plan_F-3.md b/.llm/tasks/todo/plan_F-3.md
similarity index 100%
rename from docs/tasks/todo/plan_F-3.md
rename to .llm/tasks/todo/plan_F-3.md
diff --git a/docs/tasks/todo/plan_F-4.md b/.llm/tasks/todo/plan_F-4.md
similarity index 100%
rename from docs/tasks/todo/plan_F-4.md
rename to .llm/tasks/todo/plan_F-4.md
diff --git a/docs/tasks/todo/plan_F-5.md b/.llm/tasks/todo/plan_F-5.md
similarity index 100%
rename from docs/tasks/todo/plan_F-5.md
rename to .llm/tasks/todo/plan_F-5.md
diff --git a/CLAUDE.md b/CLAUDE.md
index 65ebee6..836a0a5 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,483 +1,218 @@
# CLAUDE.md
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+Guidance for Claude Code working on this repository.
## Project Overview
-Film Photography Tracker is a Progressive Web App (PWA) for tracking film photography metadata. Built with React 18, TypeScript, Vite, and Material-UI, it helps photographers record and organize shots with exposure details, location data, and camera information.
+Film Photography Tracker - PWA for tracking film photography metadata. React 18, TypeScript, Vite, Material-UI. Records shots with exposure details, location, camera info.
## Feature Documentation Process
-When completing work on a feature or making significant changes:
-
### Completed Features
-1. **Document the feature** in `docs/features/done_F-{n}.md` where `n` matches the original plan number
-2. **Include in the documentation:**
- - Feature overview and key components
- - Technical details (files changed, new APIs, data models)
- - User benefits and use cases
- - Testing coverage
- - Commits included
- - Migration notes (if applicable)
- - Future enhancement ideas
-3. **Naming convention:** `done_F-1.md`, `done_F-6.md`, etc. (keeps original plan number)
-4. **Delete task plans** from `docs/tasks/todo/` once implemented
+1. Document in `.llm/features/done_F-{n}.md` (same n as plan)
+2. Include: overview, files changed, APIs, tests, commits, migrations
+3. Delete plan from `.llm/tasks/todo/`
### Planned Features
-1. **Create plan** in `docs/tasks/todo/plan_F-{n}.md` for future features
-2. **Include in the plan:**
- - Problem statement
- - Proposed solution
- - Implementation steps
- - Benefits and trade-offs
- - Effort estimate
-3. **Naming convention:** `plan_F-5.md`, `plan_F-6.md`, etc.
-4. **Move to done_F-{n}.md** once implemented (KEEP THE SAME NUMBER)
-
-**IMPORTANT for Claude Code plan mode:** When entering plan mode, ALWAYS write the final plan to `docs/tasks/todo/plan_F-{n}.md` (NOT to `~/.claude/plans/`). This ensures plans are tracked in the repository.
+1. Create `.llm/tasks/todo/plan_F-{n}.md`
+2. Include: problem, solution, steps, benefits, effort
+3. Move to `done_F-{n}.md` when done (KEEP NUMBER)
+
+**Plan mode:** Write to `.llm/tasks/todo/plan_F-{n}.md` (NOT `~/.claude/plans/`)
### Feature Numbering
-**IMPORTANT:** Feature numbers are assigned when planned and stay with the feature forever:
-- Numbers reflect when features were **planned**, not when they were **implemented**
-- `plan_F-6.md` becomes `done_F-6.md` (NOT `done_F-2.md`)
-- Features can be implemented in any order
-- Gaps in numbering are expected (e.g., `done_F-1.md`, `done_F-6.md`, `plan_F-2.md`, `plan_F-5.md`)
-- Never renumber features - each feature keeps its original plan number
-- Completed features use `done_` prefix
-- Planned features use `plan_` prefix
-
-**Example:**
-- `plan_F-2.md` → implemented later → becomes `done_F-2.md`
-- `plan_F-6.md` → implemented first → becomes `done_F-6.md`
-- Result: `docs/features/` has `done_F-1.md`, `done_F-6.md` (F-2 through F-5 still planned or not yet created)
+- Numbers = plan date, not implement date
+- `plan_F-6.md` → `done_F-6.md` (never renumber)
+- Gaps expected (e.g., `done_F-1.md`, `done_F-6.md`, `plan_F-2.md`, `plan_F-5.md`)
-## Coding Best Practices
+### GitHub Integration
+- ISSUE-n: `plan_ISSUE-42.md` → `done_ISSUE-42.md`
+- Invoke: `procedure{source_github}` (label: `ready_for_dev`)
+- F-n ≠ ISSUE-n (never mix)
-### Component Composition
+## LLM Procedures
+See `.llm/procedure/` for workflows (e.g., `import-tasks-github.md`)
-**DO:**
-- ✅ Use shared components from `src/components/common/` for repeated patterns
-- ✅ Check if a shared component exists before creating duplicate UI (DialogHeader, EmptyStateDisplay, ConfirmationDialog, etc.)
-- ✅ Use selector components (LensSelector, ApertureSelector, ShutterSpeedSelector) instead of TextField with select prop
-- ✅ Extract components when the same pattern appears 2+ times
-
-**DON'T:**
-- ❌ Copy-paste dialog headers, empty states, or confirmation dialogs
-- ❌ Use window.confirm() or window.alert() - use ConfirmationDialog instead
-- ❌ Create new selector components without checking common/ directory first
-- ❌ Over-engineer - don't extract components for patterns that appear only 1-2 times and not very common, or can't be easily extracted
+## Coding Best Practices
-### MUI Component Accessibility
+### Component Composition
+**DO:** Use shared from `src/components/common/` (DialogHeader, EmptyStateDisplay, ConfirmationDialog, EntityContextMenu, LensSelector, ApertureSelector, ShutterSpeedSelector). Extract when pattern appears 2+ times.
-When creating new MUI Select components:
+**DON'T:** Copy-paste dialog headers, empty states, confirmation dialogs. Use window.confirm/alert. Over-engineer for 1-2 instances.
-**ALWAYS:**
+### MUI Select Accessibility
+Always use `useId()` to connect InputLabel/Select:
```typescript
-import { useId } from 'react';
-
-const MySelector = () => {
- const id = useId(); // Generate unique ID for accessibility
-
- return (
-
- My Label
-
-
- );
-};
+const id = useId();
+Label
+
```
-**Why:** Properly connecting InputLabel and Select via `labelId` ensures:
-- Screen readers can announce the label
-- Tests using `getByLabel()` work correctly
-- Material-UI styling and animations work properly
-
### TypeScript Imports
-
-Use `type` keyword for type-only imports (required by verbatimModuleSyntax):
-
+Use `type` keyword (verbatimModuleSyntax):
```typescript
-// ✅ CORRECT
import type { Lens, Camera } from '../types';
import { type ReactNode } from 'react';
-
-// ❌ WRONG
-import { Lens, Camera } from '../types';
-import { ReactNode } from 'react';
-```
-
-### Confirmation Dialogs
-
-**DO:**
-```typescript
-// ✅ Use ConfirmationDialog component
- setDeleteConfirmOpen(false)}
-/>
-```
-
-**DON'T:**
-```typescript
-// ❌ Don't use window.confirm()
-const confirmed = window.confirm('Delete camera?');
-if (confirmed) handleDelete();
-```
-
-### Empty States
-
-**DO:**
-```typescript
-// ✅ Use EmptyStateDisplay component
-}
- title="No Cameras Added Yet"
- description="Add your camera equipment to track metadata."
- actionLabel="Add Camera"
- onAction={() => setShowDialog(true)}
-/>
-```
-
-**DON'T:**
-```typescript
-// ❌ Don't create custom empty state markup
-
-
- No Cameras Yet
-
-
```
-### Code Duplication
-
-**When you see the same pattern 2+ times:**
-1. Check if a shared component exists in `src/components/common/`
-2. If not, consider extracting to a new shared component
-3. Update CLAUDE.md to document the new pattern
-4. Avoid over-engineering - some duplication is acceptable for 1-2 instances
-
-**Red flags that indicate duplication:**
-- Multiple `DialogTitle` with `IconButton` + `Close` icon
-- Multiple empty state implementations with similar structure
-- Multiple FormControl + InputLabel + Select blocks
-- Repeated Edit/Delete context menus
-- Copy-pasted confirmation dialogs
+### Patterns
+- **Confirmations:** Use `` not `window.confirm()`
+- **Empty states:** Use `` not custom markup
+- **Duplication:** Check `common/` before creating. Update CLAUDE.md when adding new patterns.
## Key Commands
### Development
```bash
-npm run dev # Start development server (http://localhost:5173 or https if certs exist)
-npm run build # Build for production (outputs to dist/)
-npm run preview # Preview production build
-npm run lint # Run ESLint
+npm run dev # Start dev server (https if certs exist)
+npm run build # Build for production
+npm run preview # Preview build
+npm run lint # ESLint
```
-### Version Management
+### Version
```bash
-npm run version:patch # Bump patch version
-npm run version:minor # Bump minor version
-npm run version:major # Bump major version
+npm run version:{patch|minor|major}
```
### Testing
```bash
-npm run test:e2e # Run Playwright E2E tests (all 80 tests across 5 browsers)
-npm run test:e2e:ui # Run E2E tests with Playwright UI (interactive mode)
-npm run test:e2e:headed # Run E2E tests in headed mode (see browser)
-npm run test:e2e:debug # Debug E2E tests with inspector
-npm run test:e2e:report # Show test report (opens HTML report in browser)
+npm run test:e2e # 80 tests across 5 browsers
+npm run test:e2e:ui # Interactive mode
+npm run test:e2e:headed # See browser
+npm run test:e2e:debug # Inspector
+npm run test:e2e:report # HTML report
```
-**Test Coverage:**
-- **app-navigation.spec.ts**: Basic app functionality, tab navigation, settings, responsive design
-- **camera-management.spec.ts**: Camera CRUD operations, special characters handling
-- **film-roll-management.spec.ts**: Film roll creation, validation, navigation
-- **photography-workflow.spec.ts**: Complete photography flow, camera settings, aperture/shutter options
-
-**Test Browsers:**
-- Desktop: Chromium, Firefox, WebKit
-- Mobile: Mobile Chrome, Mobile Safari
-
-**Page Objects:** Located in `e2e/utils/page-objects.ts` for maintainable test selectors
+**Test files:** app-navigation, camera-management, film-roll-management, photography-workflow
+**Browsers:** Chromium, Firefox, WebKit, Mobile Chrome, Mobile Safari
+**Page Objects:** `e2e/utils/page-objects.ts`
### Python Metadata Script
```bash
-python apply_filmroll_metadata.py # Apply film roll metadata to TIF files using exiftool
-# Or on Windows:
-apply.cmd # Windows wrapper for Python script
+python apply_filmroll_metadata.py # Apply metadata to TIFs using exiftool
+apply.cmd # Windows wrapper
```
-
-The Python script reads exported JSON metadata and applies it to scanned TIF files using exiftool. It matches exposures to TIF files in order and writes EXIF tags including:
-- Aperture, shutter speed, ISO/EI
-- GPS location (lat/long)
-- Camera make/model
-- **Lens info** (uses `lensName` from JSON if available)
-- **Focal length** (uses exposure-specific value if set)
-- Capture date/time
-- User notes
-
-**v2.0.0 Support**: Script now uses EI if set (overrides film ISO), lens name per exposure, and focal length per exposure.
+Writes EXIF: aperture, shutter, ISO/EI, GPS, camera, lens (v2.0.0: uses EI, lens per exposure, focal length)
## Project Structure
```
src/
-├── components/ # React components
-│ ├── common/ # Shared/reusable components
-│ │ ├── DialogHeader.tsx # Standard dialog header with close button
-│ │ ├── EmptyStateDisplay.tsx # Empty state pattern (icon, title, description, action)
-│ │ ├── ConfirmationDialog.tsx # Reusable confirmation dialog
-│ │ ├── EntityContextMenu.tsx # Edit/Delete context menu
-│ │ ├── LensSelector.tsx # Lens selection dropdown
-│ │ ├── ApertureSelector.tsx # Aperture selection (with maxAperture filtering)
-│ │ └── ShutterSpeedSelector.tsx # Shutter speed selection
-│ ├── MainScreen.tsx # Tabbed interface (Film Rolls & Cameras tabs)
-│ ├── FilmRollListScreen.tsx # Film roll list and management
-│ ├── CameraManagementScreen.tsx # Camera equipment management
-│ ├── LensManagementScreen.tsx # Lens library management
-│ ├── SetupScreen.tsx # Film roll configuration
-│ ├── CameraScreen.tsx # Photo capture interface
-│ ├── GalleryScreen.tsx # Exposure list view with import/export
-│ ├── DetailsScreen.tsx # Individual exposure details/editing
-│ ├── SettingsModal.tsx # App settings (Google Drive, etc.)
-│ ├── ItemCard.tsx # Reusable card component
-│ └── FocalLengthSlider.tsx # Focal length slider for zoom lenses
-├── utils/ # Utility functions
-│ ├── storage.ts # Storage facade (async API wrapper)
-│ ├── indexedDBStorage.ts # IndexedDB implementation with migration
-│ ├── exportImport.ts # Export/Import functionality
-│ ├── googleDriveService.ts # Google Drive integration (placeholder)
-│ └── camera.ts # Camera, geolocation, and file utilities
-├── types.ts # TypeScript type definitions
-├── App.tsx # Main application component (state management)
-└── main.tsx # React entry point
+├── components/
+│ ├── common/ # Shared: Dialog/Empty/Confirmation/Menu/Selectors
+│ ├── *Screen.tsx # Main/FilmRollList/Camera/Lens/Setup/Camera/Gallery/Details/Settings
+│ ├── ItemCard.tsx
+│ └── FocalLengthSlider.tsx
+├── utils/
+│ ├── storage.ts # Async API facade
+│ ├── indexedDBStorage.ts # IndexedDB + migration
+│ ├── exportImport.ts
+│ ├── googleDriveService.ts
+│ └── camera.ts # Camera/geolocation/file utilities
+├── types.ts
+├── App.tsx # State management
+└── main.tsx
```
## Architecture
-### Shared Components Pattern
-
-The app uses a **common components library** (`src/components/common/`) to eliminate code duplication and ensure consistency:
-
-**Core UI Components:**
-- **DialogHeader** - Standard dialog header with title, icon, and close button. Used in all modal dialogs for consistent UX.
-- **EmptyStateDisplay** - Empty state pattern with icon, title, description, and action button. Used in all management screens.
-- **ConfirmationDialog** - Reusable confirmation dialog with customizable severity. Replaces window.confirm() for better UX.
-- **EntityContextMenu** - Standard Edit/Delete context menu for entity management screens.
-
-**Form Components:**
-- **LensSelector** - Lens selection dropdown with proper MUI accessibility (labelId/id via useId())
-- **ApertureSelector** - Aperture selection with automatic filtering based on lens maxAperture
-- **ShutterSpeedSelector** - Shutter speed selection dropdown
-
-**Key Principles:**
-- All MUI Select components use `useId()` hook to properly connect InputLabel and Select for accessibility
-- Components accept standard props (label, fullWidth, required) for flexibility
-- Type imports use `type` keyword for TypeScript's verbatimModuleSyntax compliance
-- Components are self-contained with no external state dependencies
+### Shared Components (`src/components/common/`)
+- **DialogHeader** - Title, icon, close button
+- **EmptyStateDisplay** - Icon, title, description, action
+- **ConfirmationDialog** - Customizable severity, replaces window.confirm
+- **EntityContextMenu** - Edit/Delete menu
+- **LensSelector/ApertureSelector/ShutterSpeedSelector** - MUI selects with useId() accessibility
-**Usage Example:**
-```typescript
-import { ApertureSelector } from './common/ApertureSelector';
-
- setFormData(prev => ({ ...prev, maxAperture: value }))}
- label="Maximum Aperture (widest)"
- maxAperture={currentLens?.maxAperture} // Optional: filters available apertures
-/>
-```
-
-### Storage System
-
-The app uses a **two-layer storage architecture**:
+All use `type` imports, accept standard props, self-contained.
-1. **IndexedDB Layer** (`src/utils/indexedDBStorage.ts`)
- - Primary storage with high capacity (50MB+)
- - Object stores: filmRolls, exposures, cameras, settings, currentFilmRoll
- - Handles automatic migration from localStorage on first run
- - All date fields are serialized/deserialized properly
+### Storage (Two-Layer)
+1. **IndexedDB** (`indexedDBStorage.ts`) - 50MB+, stores: filmRolls, exposures, cameras, settings, currentFilmRoll. Auto-migrates from localStorage. Serializes dates.
+2. **Facade** (`storage.ts`) - Async wrapper. Call `storage.initialize()` in App.tsx.
-2. **Storage Facade** (`src/utils/storage.ts`)
- - Simple async API wrapper around IndexedDB
- - No caching or fallbacks - pure IndexedDB operations
- - All methods are async and return Promises
- - Must call `storage.initialize()` before use (done in App.tsx on mount)
+### State (App.tsx)
+React hooks (no Redux). `AppState`: currentFilmRoll, filmRolls, cameras, exposures, currentScreen, selectedExposure, settings.
+Screens: 'filmrolls' | 'setup' | 'camera' | 'gallery' | 'details'
-**Important**: The app previously used localStorage but migrated to IndexedDB. The migration happens automatically on first load via `indexedDBStorage.migrateFromLocalStorage()`.
+### Data Model (`types.ts`)
+- **Camera** - make, model, lens, auto-name
+- **FilmRoll** - name, ISO, totalExposures, cameraId
+- **Exposure** - filmRollId, number, aperture, shutter, location, imageData (base64), capturedAt
+- **AppSettings** - Google Drive, version
-### Application State
-
-State is managed in `App.tsx` using React hooks (no Redux or external state management):
-
-- **AppState** contains: currentFilmRoll, filmRolls, cameras, exposures, currentScreen, selectedExposure, settings
-- Screen navigation via `currentScreen` enum: 'filmrolls' | 'setup' | 'camera' | 'gallery' | 'details'
-- All storage operations go through async `storage` API and update local state on success
-- Settings include Google Drive integration (currently disabled during storage migration)
+### Screen Flow
+MainScreen (tabs) → SetupScreen → CameraScreen → GalleryScreen → DetailsScreen
-### Data Model (src/types.ts)
+### PWA
+- `vite-plugin-pwa` with injectManifest (custom SW: `public/sw.js`)
+- Manual update (registerType: 'prompt')
+- NetworkFirst caching
+- HTTPS via local certs (localhost+4.pem)
-Core entities:
-- **Camera**: Equipment definition (make, model, lens, auto-generated name)
-- **FilmRoll**: Film configuration (name, ISO, totalExposures, cameraId link)
-- **Exposure**: Individual shots (filmRollId link, exposure number, aperture, shutter speed, location, imageData as base64, capturedAt timestamp)
-- **AppSettings**: Google Drive settings, version
+### Import/Export (`exportImport.ts`)
+**v2.0.0 Format:**
+- `metadata.json` - FilmRoll (ei, currentLensId), Exposures (aperture, shutter, notes, location, capturedAt, ei, lensId, lensName, focalLength)
+- `exposure_X_ID.jpg` - Numbered photos
-### Screen Flow
+**Methods:** Local download (500ms stagger), JSON only (Web Share on mobile), Google Drive (placeholder)
-1. **MainScreen** - Tabbed interface with Film Rolls and Cameras management tabs
-2. **SetupScreen** - Create new film roll with camera selection
-3. **CameraScreen** - Photo capture with camera API or file picker, exposure settings chips (aperture, shutter, notes)
-4. **GalleryScreen** - Grid view of all exposures for current film roll, import/export functionality
-5. **DetailsScreen** - Full-screen photo view with editable metadata
-
-### PWA Configuration
-
-- Uses `vite-plugin-pwa` with **injectManifest** strategy (custom service worker at `public/sw.js`)
-- **Manual update prompt** - registerType: 'prompt' prevents automatic updates to preserve user data
-- Service worker uses NetworkFirst caching strategy for offline support
-- Update notification shown in App.tsx via Snackbar with user confirmation
-- HTTPS support via local certificates (localhost+4.pem) for camera API access
-
-### Import/Export System
-
-Located in `src/utils/exportImport.ts`:
-
-**Export Format (v2.0.0):**
-- `metadata.json`: Contains film roll info, exposures array, exportedAt timestamp, version
- - FilmRoll includes: `ei` (Exposure Index), `currentLensId`
- - Each exposure includes:
- - Core: aperture, shutterSpeed, additionalInfo, location, capturedAt
- - **New in v2.0.0**: `ei`, `lensId`, `lensName` (denormalized), `focalLength`
-- `exposure_X_ID.jpg`: Individual photos with numbered naming (X = exposure number)
-- All files organized in a single folder for easy management
-
-**Export Methods:**
-- **Local Download**: Downloads all files to device (staggered by 500ms to avoid browser limits)
-- **JSON Only**: Exports metadata.json with Web Share API on mobile, regular download on desktop
-- **Google Drive**: Placeholder implementation (requires API setup)
-
-**Import Methods:**
-- **Local Files**: Select folder contents via file input, reads metadata.json and reconstructs exposures
-- **Google Drive**: Placeholder implementation (requires API setup)
-
-**Important Details:**
-- Images stored as base64 data URLs in IndexedDB
-- Export creates separate files; import reads them back
-- FileReader used for file-to-base64 conversion with 10MB size limit
-- Dates converted between Date objects and ISO strings during export/import
-- **Lens name denormalized** in export for easier external tool usage (Python script)
+Images as base64 in IndexedDB. FileReader with 10MB limit. Dates ↔ ISO strings. Lens name denormalized for Python script.
## Key Features
-### Photo Capture & Settings
-- Live camera view with MediaDevices API
-- Fallback camera constraints (tries rear camera first, then front, then basic)
-- Gallery file picker for existing photos
-- Exposure settings: aperture (f/1.4 to f/22), shutter speed (1/4000 to BULB), notes
-- Automatic location capture with Geolocation API (10min cache, high accuracy)
-- Exposure counter with remaining shots display
-
-### Camera & Lens Management
-- **Three top-level tabs**: Film Rolls, Cameras, Lenses (flat navigation structure)
-- **Camera Management**: Define equipment (make, model), auto-generated names
-- **Lens Management**: Separate lens library with max aperture and focal length specs
- - Prime lenses: Single focal length (e.g., 50mm)
- - Zoom lenses: Min/max focal length range (e.g., 24-70mm)
- - Max aperture limits available aperture values in camera settings
-- **Mid-roll lens changes**: Select different lens per exposure
-- **EI (Exposure Index)**: Override film ISO per exposure
-- CRUD operations for both cameras and lenses
-
-### Data Persistence & Privacy
-- **100% Local Storage**: All data stays on device via IndexedDB
-- **No Server Communication**: App works completely offline
-- **No Tracking**: No analytics or data collection
-- **Permission-Based**: Camera and location require user consent
-- **Offline-First PWA**: Installation, offline support, service worker caching
-
-## Development Notes
+### Photo Capture
+- MediaDevices API (fallback constraints: rear → front → basic)
+- Gallery picker
+- Settings: aperture (f/1.4-f/22), shutter (1/4000-BULB), notes
+- Geolocation (10min cache, high accuracy)
+- Exposure counter
-### HTTPS for Camera Access
-Modern browsers require HTTPS for camera API access. The project includes localhost SSL certificates (localhost+4.pem, localhost+4-key.pem). Vite detects these and enables HTTPS automatically.
+### Management
+- Three tabs: Film Rolls, Cameras, Lenses
+- **Cameras** - make, model, auto-names
+- **Lenses** - max aperture, focal length (prime/zoom 24-70mm)
+- Mid-roll lens changes
+- EI (Exposure Index) per exposure
+- CRUD for cameras/lenses
-### Camera Utilities (src/utils/camera.ts)
-- **camera.isSupported()**: Checks MediaDevices API and secure context
-- **camera.getMediaStream()**: Tries multiple constraint sets for maximum compatibility
-- **camera.captureImage()**: Captures from video element, limits to 1280px, JPEG quality 0.7-0.8
-- **geolocation.getCurrentPosition()**: High accuracy, 10s timeout, 10min cache
-- **fileUtils.fileToBase64()**: Converts File to base64 with 10MB limit and type validation
+### Privacy
+100% local (IndexedDB), offline, no tracking, no server, permission-based.
-### Date Handling
-All Date fields (createdAt, capturedAt, lastSyncTime) are stored as Date objects in memory but serialized as ISO strings in IndexedDB. The storage layer handles conversion automatically.
+## Development Notes
-### Exposure Settings State
-Current aperture, shutter speed, and additionalInfo are maintained separately in App.tsx (`exposureSettings` state) and passed to CameraScreen. This allows settings to persist across multiple shots within the same session.
+### Camera Utilities (`camera.ts`)
+- `camera.isSupported()` - Check API + secure context
+- `camera.getMediaStream()` - Multiple constraint attempts
+- `camera.captureImage()` - Max 1280px, JPEG 0.7-0.8
+- `geolocation.getCurrentPosition()` - 10s timeout, 10min cache
+- `fileUtils.fileToBase64()` - 10MB limit
-### Material-UI Theme
-Global theme defined in App.tsx with primary color #1976d2 (blue) and secondary #dc004e (pink).
+### Patterns
+**Creating entities:** ID = `Date.now().toString()` → `await storage.save*()` → update state immutably
+**Updating exposures:** Save first → map state: `exposures.map(e => e.id === id ? newE : e)`
+**Navigation:** `navigateToScreen(screen, exposure?)` updates currentScreen + selectedExposure
-### TypeScript Configuration
-- Strict mode enabled
-- Separate configs: tsconfig.app.json (app code), tsconfig.node.json (Vite config)
-- React 19 type definitions included
+### Config
+- **Dates:** Objects in memory, ISO strings in IndexedDB (auto-converted)
+- **Exposure settings:** Separate state in App.tsx, persists across shots
+- **MUI theme:** Primary #1976d2 (blue), secondary #dc004e (pink)
+- **TypeScript:** Strict mode, tsconfig.app.json + tsconfig.node.json, React 19
## External Dependencies
-
-- **Aperture/Shutter Constants**: Pre-defined values in types.ts (APERTURE, SHUTTER_SPEED enums)
-- **Geolocation API**: Browser API for GPS coordinates
-- **MediaDevices API**: Browser API for camera access
-- **Web Share API**: For mobile sharing (in GalleryScreen)
-- **exiftool**: External tool (v13.38) required for Python metadata script
-
-## Common Patterns
-
-### Creating New Entities
-1. Generate unique ID (Date.now().toString())
-2. Call storage method (e.g., `await storage.saveFilmRoll(filmRoll)`)
-3. Update local state immutably
-4. Handle errors with try/catch and user alerts
-
-### Updating Exposures
-Exposures are identified by ID. When updating, always:
-1. Save to storage first: `await storage.saveExposure(exposure)`
-2. Update state by mapping over array: `exposures.map(e => e.id === exposure.id ? exposure : e)`
-
-### Screen Navigation
-Use `navigateToScreen(screen, exposure?)` helper in App.tsx which updates both currentScreen and selectedExposure.
+- Aperture/shutter constants in `types.ts`
+- Browser APIs: Geolocation, MediaDevices, Web Share
+- exiftool v13.38 for Python script
## Browser Compatibility
-
-- **Camera Access**: Chrome 53+, Firefox 36+, Safari 11+
-- **PWA Features**: Chrome 40+, Firefox 44+, Safari 11.1+
-- **IndexedDB**: All modern browsers
-- **Geolocation**: All modern browsers
-- **Web Share API**: Mobile browsers (Chrome Android 61+, Safari iOS 12.2+)
-
+- Camera: Chrome 53+, Firefox 36+, Safari 11+
+- PWA: Chrome 40+, Firefox 44+, Safari 11.1+
+- IndexedDB/Geolocation: All modern
+- Web Share: Mobile (Chrome Android 61+, Safari iOS 12.2+)
## Deployment
-
-The app is configured for static hosting (Vercel, Netlify, GitHub Pages). Build output goes to `dist/`:
-
+Static hosting (Vercel/Netlify/GitHub Pages). Build to `dist/`:
```bash
-npm run build # Build for production
-npx vercel --prod # Deploy to Vercel
-npx netlify deploy --prod --dir=dist # Deploy to Netlify
+npm run build
+npx vercel --prod
+npx netlify deploy --prod --dir=dist
```
-
-PWA manifest and service worker are automatically generated during build.