diff --git a/.planning/phases/03-images/03-RESEARCH.md b/.planning/phases/03-images/03-RESEARCH.md
new file mode 100644
index 0000000..7dd408f
--- /dev/null
+++ b/.planning/phases/03-images/03-RESEARCH.md
@@ -0,0 +1,521 @@
+# Phase 3: Images - Research
+
+**Researched:** 2026-01-29
+**Domain:** Image attachments with file upload, camera capture, storage, and display
+**Confidence:** HIGH
+
+## Summary
+
+This phase implements image attachments for entries with support for file upload (desktop), camera capture (mobile), thumbnail generation, and lightbox viewing. The research covers the complete image lifecycle: capture/upload on the client, transfer to server via form actions, storage on filesystem with thumbnail generation, and retrieval via API endpoints.
+
+The established approach for SvelteKit image handling is:
+1. **Client:** HTML5 File API for uploads, MediaDevices API for camera capture, canvas.toBlob() for photo capture
+2. **Server:** Form actions with multipart/form-data, write files via Node.js fs/promises
+3. **Processing:** Sharp for thumbnail generation (dramatically faster than alternatives)
+4. **Storage:** Filesystem-based with API endpoints serving images (not static folder)
+5. **Display:** svelte-lightbox for fullscreen viewing, native img tags for thumbnails
+
+**Primary recommendation:** Use Sharp for server-side thumbnail generation, native browser APIs for camera/upload, and svelte-lightbox for the lightbox component. Store images outside static/ and serve via +server.ts endpoints for better control.
+
+## Standard Stack
+
+The established libraries/tools for this domain:
+
+### Core
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| sharp | latest | Server-side image processing, thumbnail generation | 4-5x faster than ImageMagick, 20x faster than Jimp. Native C++ bindings via libvips |
+| svelte-lightbox | 1.1.x | Fullscreen image lightbox | Purpose-built for Svelte/SvelteKit, keyboard navigation, mobile support |
+
+### Supporting
+| Library | Version | Purpose | When to Use |
+|---------|---------|---------|-------------|
+| nanoid | 5.1.x | Generate unique filenames | Already in project, use for image IDs |
+| Node.js fs/promises | built-in | File system operations | Write uploaded images to disk |
+
+### Alternatives Considered
+| Instead of | Could Use | Tradeoff |
+|------------|-----------|----------|
+| sharp | jimp | Pure JS (no native deps), but 20x slower - not recommended |
+| svelte-lightbox | bigger-picture | Smaller bundle, but needs Svelte 4 compatibility config for Svelte 5 |
+| Filesystem storage | SQLite blob | Simpler queries, but larger DB, slower, harder to backup |
+
+**Installation:**
+```bash
+npm install sharp svelte-lightbox
+```
+
+## Architecture Patterns
+
+### Recommended Project Structure
+```
+data/
+ uploads/
+ originals/ # Full-size images (user uploaded)
+ thumbnails/ # Generated thumbnails (150x150)
+src/
+ lib/
+ server/
+ images/
+ storage.ts # File system operations
+ thumbnails.ts # Sharp thumbnail generation
+ components/
+ ImageUpload.svelte # Upload button + drag-drop zone
+ CameraCapture.svelte # Camera modal with preview
+ ImageGallery.svelte # Horizontal scroll gallery
+ ImageLightbox.svelte # Fullscreen viewer wrapper
+ routes/
+ api/
+ images/
+ [id]/
+ +server.ts # Serve original image
+ thumbnail/
+ +server.ts # Serve thumbnail
+```
+
+### Pattern 1: Form Action File Upload
+**What:** Handle multipart form data in SvelteKit form actions
+**When to use:** File uploads from forms
+**Example:**
+```typescript
+// +page.server.ts
+import { writeFile } from 'node:fs/promises';
+import { nanoid } from 'nanoid';
+import sharp from 'sharp';
+
+export const actions = {
+ uploadImage: async ({ request }) => {
+ const formData = await request.formData();
+ const file = formData.get('image') as File;
+ const entryId = formData.get('entryId') as string;
+
+ if (!file || file.size === 0) {
+ return fail(400, { error: 'No file uploaded' });
+ }
+
+ const imageId = nanoid();
+ const ext = file.name.split('.').pop() || 'jpg';
+ const buffer = Buffer.from(await file.arrayBuffer());
+
+ // Save original
+ await writeFile(`data/uploads/originals/${imageId}.${ext}`, buffer);
+
+ // Generate thumbnail with EXIF auto-rotation
+ await sharp(buffer)
+ .rotate() // Auto-orient based on EXIF
+ .resize(150, 150, { fit: 'cover', position: 'center' })
+ .jpeg({ quality: 80 })
+ .toFile(`data/uploads/thumbnails/${imageId}.jpg`);
+
+ // Save to database
+ // imageRepository.create({ id: imageId, entryId, ext });
+
+ return { success: true, imageId };
+ }
+};
+```
+
+### Pattern 2: Camera Capture with getUserMedia
+**What:** Access device camera directly in browser
+**When to use:** Mobile photo capture, desktop webcam
+**Example:**
+```typescript
+// CameraCapture.svelte
+
+```
+
+### Pattern 3: API Endpoint for Serving Images
+**What:** Serve uploaded images via +server.ts endpoints
+**When to use:** Serving user-uploaded files not in static/
+**Example:**
+```typescript
+// src/routes/api/images/[id]/+server.ts
+import { readFile } from 'node:fs/promises';
+import { error } from '@sveltejs/kit';
+
+export async function GET({ params }) {
+ const { id } = params;
+
+ // Look up image metadata from database
+ const image = imageRepository.getById(id);
+ if (!image) {
+ throw error(404, 'Image not found');
+ }
+
+ const filePath = `data/uploads/originals/${id}.${image.ext}`;
+
+ try {
+ const data = await readFile(filePath);
+ return new Response(data, {
+ headers: {
+ 'Content-Type': `image/${image.ext === 'jpg' ? 'jpeg' : image.ext}`,
+ 'Content-Length': data.length.toString(),
+ 'Cache-Control': 'public, max-age=31536000, immutable'
+ }
+ });
+ } catch {
+ throw error(404, 'Image file not found');
+ }
+}
+```
+
+### Pattern 4: Drag and Drop Zone
+**What:** HTML5 drag-and-drop file upload
+**When to use:** Desktop file upload experience
+**Example:**
+```typescript
+// ImageUpload.svelte
+
+
+
fileInput.click()}
+ onkeydown={(e) => e.key === 'Enter' && fileInput.click()}
+>
+
+ Drop image here or click to upload
+
+```
+
+### Anti-Patterns to Avoid
+- **Storing images in SQLite:** Bloats database, slows queries, complicates backups
+- **Storing in static/ folder:** Files overwritten on deploy, not recommended for user uploads
+- **Blocking UI during upload:** Always show optimistic preview, upload in background
+- **Missing EXIF rotation:** Images from phones will appear rotated; always use sharp.rotate()
+- **Using jimp for thumbnails:** 20x slower than Sharp, poor choice for any volume
+
+## Don't Hand-Roll
+
+Problems that look simple but have existing solutions:
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| Thumbnail generation | Canvas resize on client | Sharp on server | Server handles EXIF rotation, consistent quality, proper color management |
+| Image EXIF orientation | Manual rotation logic | sharp.rotate() | Complex orientation matrix, mirroring cases, Sharp handles all 8 orientations |
+| Lightbox gallery | Custom modal + gestures | svelte-lightbox | Keyboard navigation, pinch-zoom, swipe, accessibility baked in |
+| Camera permissions | Custom prompts | getUserMedia native prompts | Browser handles permission UI, remembers choice, shows camera indicator |
+| File type validation | Regex on extension | File API + accept attribute | MIME type checking, browser validates before upload |
+
+**Key insight:** Image handling has many edge cases (EXIF, color profiles, format variations). Sharp and browser APIs handle these; custom solutions will miss them.
+
+## Common Pitfalls
+
+### Pitfall 1: Missing EXIF Auto-Rotation
+**What goes wrong:** Photos from mobile devices appear rotated 90 or 180 degrees
+**Why it happens:** Mobile cameras embed rotation in EXIF metadata rather than rotating pixels
+**How to avoid:** Always call `sharp.rotate()` without arguments before any resize operation
+**Warning signs:** Portrait photos appearing landscape, images "sideways"
+
+### Pitfall 2: Blocking UI During Upload
+**What goes wrong:** User clicks upload, UI freezes until complete
+**Why it happens:** Not showing optimistic preview, waiting for server response
+**How to avoid:** Use URL.createObjectURL() for instant preview, upload via background fetch
+**Warning signs:** "Nothing happened" user feedback, perceived slowness
+
+### Pitfall 3: Camera Not Available Handling
+**What goes wrong:** App crashes or shows blank when camera unavailable
+**Why it happens:** Not catching getUserMedia errors, not checking navigator.mediaDevices
+**How to avoid:** Feature detection first, catch NotAllowedError and NotFoundError, show graceful fallback
+**Warning signs:** White screen on desktop without webcam, permission denied with no feedback
+
+### Pitfall 4: Memory Leaks from Object URLs
+**What goes wrong:** Memory usage grows over time as user uploads images
+**Why it happens:** Not calling URL.revokeObjectURL() after preview no longer needed
+**How to avoid:** Revoke URLs when component unmounts or when upload completes
+**Warning signs:** Browser tab memory increasing, eventual slowdown
+
+### Pitfall 5: Form Without enctype
+**What goes wrong:** File upload appears to work but server receives empty file
+**Why it happens:** Missing `enctype="multipart/form-data"` on form element
+**How to avoid:** Always set enctype for file upload forms, or use FormData with fetch directly
+**Warning signs:** formData.get('file') returns null or 0-byte file
+
+### Pitfall 6: Camera Stream Not Stopped
+**What goes wrong:** Camera indicator stays on, battery drain, privacy concern
+**Why it happens:** Not calling track.stop() when closing camera modal
+**How to avoid:** Always stop all tracks when camera not in use: `stream.getTracks().forEach(t => t.stop())`
+**Warning signs:** Browser camera indicator remains active after closing
+
+## Code Examples
+
+Verified patterns from official sources:
+
+### Sharp Thumbnail Generation
+```typescript
+// Source: https://sharp.pixelplumbing.com/api-resize
+import sharp from 'sharp';
+
+async function generateThumbnail(
+ inputBuffer: Buffer,
+ outputPath: string,
+ size = 150
+): Promise {
+ await sharp(inputBuffer)
+ .rotate() // Auto-orient based on EXIF Orientation tag
+ .resize(size, size, {
+ fit: 'cover', // Crop to fill dimensions
+ position: 'center', // Center the crop
+ withoutEnlargement: true // Don't upscale small images
+ })
+ .jpeg({ quality: 80 })
+ .toFile(outputPath);
+}
+```
+
+### getUserMedia Camera Access
+```typescript
+// Source: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
+async function startCamera(preferBack = true): Promise {
+ // Feature detection
+ if (!navigator.mediaDevices?.getUserMedia) {
+ throw new Error('Camera not supported in this browser');
+ }
+
+ try {
+ return await navigator.mediaDevices.getUserMedia({
+ video: {
+ facingMode: preferBack ? 'environment' : 'user',
+ width: { ideal: 1920 },
+ height: { ideal: 1080 }
+ },
+ audio: false
+ });
+ } catch (err) {
+ if (err instanceof DOMException) {
+ if (err.name === 'NotAllowedError') {
+ throw new Error('Camera permission denied');
+ }
+ if (err.name === 'NotFoundError') {
+ throw new Error('No camera found');
+ }
+ }
+ throw err;
+ }
+}
+```
+
+### Canvas to Blob for Photo Capture
+```typescript
+// Source: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
+function canvasToBlob(
+ canvas: HTMLCanvasElement,
+ type = 'image/jpeg',
+ quality = 0.9
+): Promise {
+ return new Promise((resolve, reject) => {
+ canvas.toBlob(
+ (blob) => {
+ if (blob) resolve(blob);
+ else reject(new Error('Failed to create blob'));
+ },
+ type,
+ quality
+ );
+ });
+}
+```
+
+### SvelteKit Form Action File Upload
+```typescript
+// Source: https://travishorn.com/uploading-and-saving-files-with-sveltekit
+import { writeFile, mkdir } from 'node:fs/promises';
+import { existsSync } from 'node:fs';
+
+export const actions = {
+ upload: async ({ request }) => {
+ const formData = await request.formData();
+ const file = formData.get('file') as File;
+
+ if (!file || file.size === 0) {
+ return fail(400, { error: 'No file provided' });
+ }
+
+ // Ensure upload directory exists
+ const uploadDir = 'data/uploads/originals';
+ if (!existsSync(uploadDir)) {
+ await mkdir(uploadDir, { recursive: true });
+ }
+
+ // Convert to buffer and write
+ const buffer = Buffer.from(await file.arrayBuffer());
+ const filename = `${crypto.randomUUID()}.${file.name.split('.').pop()}`;
+ await writeFile(`${uploadDir}/${filename}`, buffer);
+
+ return { success: true, filename };
+ }
+};
+```
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| navigator.getUserMedia | navigator.mediaDevices.getUserMedia | 2017+ | Old API deprecated, use MediaDevices interface |
+| readFileSync for serving | SvelteKit read() helper | SvelteKit 2.4 | Better streaming, works in serverless |
+| Base64 data URLs | Blob + Object URLs | Always preferred | 33% smaller, faster parsing |
+| jimp for Node.js images | Sharp | Ongoing | Sharp 20x faster, better format support |
+
+**Deprecated/outdated:**
+- `navigator.getUserMedia()`: Deprecated, use `navigator.mediaDevices.getUserMedia()`
+- Canvas `toDataURL()` for uploads: Use `toBlob()` for better performance
+- Storing files in static/: Build process overwrites; use data/ directory
+
+## Open Questions
+
+Things that couldn't be fully resolved:
+
+1. **Thumbnail dimensions**
+ - What we know: 150x150 is common for list thumbnails
+ - What's unclear: Exact dimensions depend on UI design
+ - Recommendation: Start with 150x150 cover crop, adjust based on visual testing
+
+2. **Camera button visibility on desktop**
+ - What we know: Can detect camera with enumerateDevices()
+ - What's unclear: Whether to show camera button proactively or only after first use
+ - Recommendation: Show camera button, handle no-camera gracefully with error message
+
+3. **Upload progress indication**
+ - What we know: fetch() doesn't support upload progress natively
+ - What's unclear: Whether optimistic preview is sufficient UX
+ - Recommendation: Show optimistic preview immediately; if progress needed, use XMLHttpRequest
+
+## Sources
+
+### Primary (HIGH confidence)
+- [Sharp resize API](https://sharp.pixelplumbing.com/api-resize) - Thumbnail generation, fit modes
+- [Sharp operations API](https://sharp.pixelplumbing.com/api-operation/) - EXIF auto-rotation
+- [MDN getUserMedia](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) - Camera access
+- [MDN Taking still photos](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API/Taking_still_photos) - Canvas capture
+- [MDN toBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob) - Canvas to blob conversion
+- [MDN File drag and drop](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop) - Drag-drop API
+
+### Secondary (MEDIUM confidence)
+- [SvelteKit file upload tutorial](https://travishorn.com/uploading-and-saving-files-with-sveltekit) - Form actions pattern
+- [svelte-lightbox docs](https://svelte-lightbox.js.org/docs/introduction) - Lightbox component usage
+- [Sharp vs Jimp benchmark](https://www.peterbe.com/plog/sharp-vs-jimp) - Performance comparison
+
+### Tertiary (LOW confidence)
+- [SvelteKit serving uploads discussion](https://github.com/sveltejs/kit/discussions/10162) - Custom server pattern for uploads outside static/
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH - Sharp and browser APIs are well-documented, widely used
+- Architecture: HIGH - SvelteKit patterns verified from official sources
+- Pitfalls: HIGH - Based on MDN documentation and established best practices
+- Lightbox choice: MEDIUM - svelte-lightbox is well-maintained but alternatives exist
+
+**Research date:** 2026-01-29
+**Valid until:** 2026-03-01 (Sharp and browser APIs are stable)