diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json
new file mode 100644
index 00000000..eaa1539a
--- /dev/null
+++ b/.agents/plugins/marketplace.json
@@ -0,0 +1,20 @@
+{
+ "name": "hack-dance",
+ "interface": {
+ "displayName": "Hack Dance"
+ },
+ "plugins": [
+ {
+ "name": "hack",
+ "source": {
+ "source": "local",
+ "path": "./plugins/hack"
+ },
+ "policy": {
+ "installation": "AVAILABLE",
+ "authentication": "ON_INSTALL"
+ },
+ "category": "Developer Tools"
+ }
+ ]
+}
diff --git a/.agents/skills/macos-app-design/SKILL.md b/.agents/skills/macos-app-design/SKILL.md
deleted file mode 100644
index 0a40fa58..00000000
--- a/.agents/skills/macos-app-design/SKILL.md
+++ /dev/null
@@ -1,70 +0,0 @@
----
-name: macos-app-design
-description: Use when designing or building native macOS applications with SwiftUI or AppKit. Triggers on menu bar structure, keyboard shortcuts, multi-window behavior, Liquid Glass design system, macOS Tahoe/Sequoia, sidebar navigation, toolbar design, app icons, SF Symbols, or making an app feel like a "good Mac citizen."
----
-
-# macOS App Design & Development
-
-Guide for designing and implementing native-feeling, "good Mac citizen" apps: fast, elegant, accessible, and deeply integrated with macOS workflows.
-
-## Two Rules That Beat Everything Else
-
-1. **Prefer system components and conventions** over bespoke UI—fastest path to "feels right on Mac"
-2. **If you customize bars, backgrounds, borders, or control chrome**: stop and justify it
-
-## Quick Reference: Mac Citizen Checklist
-
-| Area | Requirement |
-|------|-------------|
-| **Menu Bar** | Standard layout (App/File/Edit/View/Window/Help), ⌘, for Settings |
-| **Keyboard** | Every primary command reachable via keyboard, standard shortcuts work |
-| **Windows** | Resize fluidly, support multiple windows, respect fullscreen/minimize |
-| **Sidebars** | Top-level navigation, scannable items, content extends behind |
-| **Toolbars** | Group by function/frequency, demote secondary to "more" menu |
-| **Text** | Use system text components, standard editing behaviors |
-| **Accessibility** | VoiceOver labels, full keyboard navigation, Reduced Motion support |
-
-## Liquid Glass Quick Rules
-
-**Do:**
-- Use for navigation/controls layer (toolbars, sidebars, bars)
-- Let system components provide built-in behaviors
-
-**Don't:**
-- Apply to content layer (tables, lists, document content)
-- Stack "glass on glass"
-
-## App Archetypes
-
-Identify your app type first:
-- **Document-based**: Files as primary units (open/save/duplicate)
-- **Library + editor**: Sidebar lists items, detail in main area
-- **Utility**: Single window, optional menu bar
-- **Menu-bar app**: Lives in menu bar, minimal UI
-- **Pro tool**: Dense, power-user workflows
-
-## Deliverables Before Building
-
-1. **App archetype** identified
-2. **Information architecture** (sidebar structure, navigation, window model)
-3. **Command map** (menus + keyboard shortcuts for every major feature)
-4. **State + data model** (persistence, undo/redo, concurrency)
-5. **Accessibility plan** (VoiceOver, keyboard, contrast, reduce motion)
-
-## Full Reference
-
-For complete design system details, Icon Composer workflow, SF Symbols guidance, evaluation rubrics, and Definition of Done checklist:
-
-See: [references/macos-design-guide.md](references/macos-design-guide.md)
-
-## Common Mistakes
-
-| Mistake | Fix |
-|---------|-----|
-| Missing menu bar commands | Every feature in menus with keyboard shortcuts |
-| Settings outside App menu | Always ⌘, opening from App menu |
-| Custom text components | Use system text for Mac editing ecosystem |
-| Toolbar overload | Demote secondary actions, group by function |
-| Glass on content | Reserve Liquid Glass for navigation layer only |
-| Breaking standard shortcuts | Never override ⌘C, ⌘V, ⌘Z, etc. |
-| Single-window only | Support multiple windows when it benefits workflows |
diff --git a/.agents/skills/macos-app-design/references/macos-design-guide.md b/.agents/skills/macos-app-design/references/macos-design-guide.md
deleted file mode 100644
index 315abc58..00000000
--- a/.agents/skills/macos-app-design/references/macos-design-guide.md
+++ /dev/null
@@ -1,357 +0,0 @@
-# World-Class macOS App Design & Build Guide
-
-Complete reference for designing and implementing native macOS apps.
-
-## 1) Quality Bar: What "World-Class" Means
-
-### "Good Mac Citizen" Test
-
-Mac excellence is less about toolkit and more about embracing Mac conventions:
-- Menus and keyboard shortcuts
-- Window management and services
-- Text behaviors and preferences
-
-### Performance is Non-Negotiable
-
-**Practical sanity test:** Open a large document, scroll hard, resize—must stay fast and responsive ("Moby Dick" test).
-
-### Apple Design Awards Rubric
-
-Use these categories for evaluation:
-- **Delight and Fun**: Satisfying micro-interactions
-- **Inclusivity**: VoiceOver, keyboard-only, reduced motion
-- **Innovation**: Platform tech for meaningful improvement
-- **Interaction**: Effortless, tailored to macOS workflows
-- **Visuals and Graphics**: Cohesive iconography/typography
-- **Social Impact**: Accessibility, broad user benefit
-
----
-
-## 2) Liquid Glass Design System (macOS Tahoe 26+)
-
-Apple's visual refresh built on **Liquid Glass**—translucent, dynamic material that reflects/refracts surroundings.
-
-### What Liquid Glass Is
-
-A "digital meta-material" that dynamically bends light and behaves fluidly. Reserved primarily for the **navigation/controls layer** floating above content.
-
-### Where to Use Liquid Glass
-
-**Do:**
-- Navigation/controls (toolbars, sidebars, bars, key UI chrome)
-- Let system components provide built-in behaviors (focus changes, shadowing, interaction glow)
-
-**Don't:**
-- Content layer (tables, lists, documents)—muddies hierarchy
-- "Glass on glass" stacking—gets cluttered
-
-### Accessibility Adaptations
-
-Liquid Glass automatically adapts to:
-- **Reduced Transparency**
-- **Increased Contrast**
-- **Reduced Motion**
-
-Your app must remain usable with these settings enabled.
-
-### New Design System Rules
-
-- **Remove decorative backgrounds/borders** you added for emphasis
-- Express hierarchy via **layout and grouping**, not decoration
-- **Crowded toolbar** = signal to remove or demote actions
-- Group bar items **by function and frequency**
-- Avoid grouping text button + icon button (reads as one control)
-
----
-
-## 3) macOS-Native IA & UI Patterns
-
-### 3.1 Menu Bar: Your Command Center
-
-**Required baseline:**
-- Standard menu layout: **App / File / Edit / View / Window / Help** (+ Format if relevant)
-- **Settings…** in App menu with **⌘,** behavior
-- Menu items enable/disable based on focus/selection
-
-**SwiftUI implementation:**
-- Use `commands` in App, add via `CommandMenu` / `CommandGroup`
-- Keyboard shortcuts and discoverability come "for free"
-
-**World-class touches:**
-- Help menu with real help content
-- Menu organization reflecting user workflows
-- Primary tasks near top, destructive tasks grouped and labeled
-
-### 3.2 Keyboard Shortcuts and Full Keyboard Workflows
-
-**Non-negotiable:**
-- Every primary command reachable via keyboard
-- Standard shortcuts behave normally (⌘C, ⌘V, ⌘Z, ⌘F, ⌘N, ⌘O)
-
-**World-class:**
-- Proper tabbing and focus rings on custom controls
-- Command palette *in addition* to menus (not instead)
-
-### 3.3 Windows: Multi-Window Platform
-
-**Choose window model for archetype:**
-- **Document-based**: Files as primary units, open/save/duplicate/revert
-- **Library + detail**: Sidebar lists items, detail/editor in main area
-- **Utility**: Lightweight single window, optional menu bar
-
-**Required:**
-- Window resizing as first-class interaction—layouts fluidly adapt
-- Responsive under resize
-
-**World-class:**
-- Multiple windows showing different content (or views of same)
-- Respect close/minimize/zoom/toolbar/titlebar, tabbing, fullscreen
-
-### 3.4 Sidebars: Structure, Navigation, Context
-
-macOS sidebars refract/reflect content behind them while maintaining context.
-
-**Do:**
-- Top-level navigation with clear hierarchy
-- Scannable, stable items (users build muscle memory)
-- Let content extend beneath/behind sidebars
-
-**Don't:**
-- Overload with dense controls—use inspectors/toolbars for actions
-
-### 3.5 Toolbars and Bar Content
-
-**New system encourages:**
-- Remove custom backgrounds/borders
-- Group by function/frequency
-- Demote secondary actions to "more" menu
-- Primary action visually distinct (often tinted)
-
-### 3.6 Text System Behavior
-
-**Users expect:**
-- Standard text shortcuts and behaviors
-- Correct selection rules
-- Undo/redo
-- Services menu integration
-- Context menu items
-
-**Practical rule:** Use system text components to inherit Mac editing ecosystem.
-
-### 3.7 Services, Share, System Integrations
-
-A "Mac citizen" participates in:
-- **Services menu** integrations
-- Standard share flows
-- Drag & drop, copy/paste
-- Quick Look previews (document/media apps)
-
----
-
-## 4) Visual Language
-
-### 4.1 Typography and Layout
-
-- Prefer system text styles and default spacing
-- Adjust only for hierarchy and readability
-- Design for large displays, resizable windows, dense information, long sessions
-
-### 4.2 Color and Materials
-
-Use system colors/materials for automatic adaptation to:
-- Light/dark environments
-- User customization (accent colors, increased contrast)
-
-Brand through content, iconography, subtle accents—not by replacing system structure.
-
-### 4.3 App Icons: Modern, Layered, Multi-Appearance
-
-New look includes multi-layer Liquid Glass-crafted icons with personalization (light/dark, tints, "clear" look).
-
-#### Icon Composer Workflow
-
-**Tool requirements:** macOS Sequoia 15.3+
-
-**Canvas sizes:**
-- macOS/iPhone/iPad: 1024px
-- Watch: 1088px (optically larger)
-
-**Workflow:**
-1. Design as layers—keep source art flat and controllable
-2. Add dynamic glass properties in Icon Composer (not baked in)
-3. Export vectors as **SVG**, complex assets as **PNG**
-4. Convert text to outlines if needed
-5. Don't export enclosure mask (system applies cropping)
-
-**Layer groups:** Up to **4 groups/layers** of glass complexity (intentional limit)
-
-**Appearances supported:**
-- Default, Dark
-- Clear (light/dark)
-- Tinted (light/dark)
-
-**Mono legibility:** Set at least one key element to white, tune conversion for contrast.
-
-**Pitfalls:**
-- Highly translucent art becomes unclear in monochrome
-- Watch blur/gradients and detail at small sizes
-
-### 4.4 SF Symbols
-
-**SF Symbols 7:**
-- 6,900+ symbols aligned with San Francisco typeface
-- Multiple weights/scales
-- Draw animations and annotation tools
-- Variable rendering, gradients, Magic Replace
-- Requires macOS Sonoma+ to run app
-
-**Guideline:** Use SF Symbols for system concepts. Only design custom symbols when domain requires—keep them SF-like and accessible.
-
-### 4.5 Motion and Micro-Interactions
-
-Motion communicates hierarchy and responsiveness:
-- Clarify state change, focus, hierarchy, transitions
-- Respect Reduced Motion (system adapts Liquid Glass; your custom animations must too)
-- Subtle, high-quality over flashy
-
----
-
-## 5) Accessibility and Inclusive Design
-
-**Design in, not added later.**
-
-### Baseline Requirements
-
-| Area | Requirement |
-|------|-------------|
-| **VoiceOver** | Meaningful labels/values/hints for every interactive element |
-| **Keyboard** | Everything operable without mouse/trackpad |
-| **Contrast** | Readable in light/dark and increased contrast modes |
-| **Reduce Motion** | UI usable and understandable with setting enabled |
-| **Reduce Transparency** | UI remains functional |
-| **Localization** | Layouts handle longer strings, different writing systems |
-
-### Inclusive Craft Moves
-
-- Don't encode meaning by color alone
-- Redundancy: icon + label, shape + color, text + sound
-- Reduce cognitive load: clear IA, consistent terminology, predictable controls
-
----
-
-## 6) Engineering for Excellence
-
-### 6.1 Architecture
-
-- Clear separation between **domain model** and **UI state**
-- Concurrency keeping UI responsive (avoid blocking main thread)
-- Deliberate **undo/redo** strategy, document/versioning, autosave
-
-**Pattern:** Define internal "app intents" (command model) so UI, menus, shortcuts, toolbar all invoke same code paths.
-
-### 6.2 SwiftUI vs AppKit
-
-**Prefer SwiftUI** for new apps—faster iteration, modern patterns.
-
-**Drop to AppKit when:**
-- Advanced text editing matching macOS standards
-- Deep window customization unavailable in SwiftUI
-- Certain pro-workflow integrations
-
-**Rule:** If using AppKit, keep bridging layer thin and well-tested.
-
-### 6.3 Performance Discipline
-
-Repeatable checklist:
-- Large-file open, scroll, resize test
-- Memory sanity: no unbounded caches, retain cycles; windows close cleanly
-- Smooth interaction: scrolling/resizing responsive
-
-### 6.4 Fit and Finish Features
-
-World-class apps include:
-- Fully functional menu structure + shortcuts
-- Native-feeling Settings (⌘,)
-- Window tabbing support and multi-window
-- Help content
-- Scriptability/automation hooks
-- Respect for user preferences (appearance, accent colors)
-
----
-
-## 7) ADA-Inspired Evaluation Rubric
-
-Use as release gate:
-
-### Delight and Fun
-- [ ] Satisfying micro-interactions enhancing understanding?
-- [ ] Feels "alive" in restrained, high-quality way?
-
-### Inclusivity
-- [ ] VoiceOver user can complete core workflow?
-- [ ] Keyboard-only user can do everything?
-- [ ] Reduced motion/transparency preserves usability?
-
-### Interaction
-- [ ] Control effortless and tailored to macOS workflows?
-
-### Visuals and Graphics
-- [ ] UI cohesive with consistent iconography/typography?
-- [ ] SF Symbols used appropriately, custom symbols match system?
-
-### Innovation (Differentiating)
-- [ ] Leveraging platform tech for meaningfully better experience?
-
----
-
-## 8) Definition of Done Checklist
-
-### Mac Citizen Essentials
-
-- [ ] Standard menu bar structure; key commands discoverable
-- [ ] Settings opens via ⌘, with native Mac feel
-- [ ] Undo/redo works reliably
-- [ ] Copy/paste/selection behave normally in text surfaces
-- [ ] Multi-window behavior sensible; resizing robust and fast
-
-### Liquid Glass Readiness
-
-- [ ] Custom bars/backgrounds removed unless strongly justified
-- [ ] Liquid Glass for functional/navigation layers; content clear
-- [ ] "Glass on glass" stacking avoided
-
-### Icon + Branding
-
-- [ ] App icon modern and layered (Icon Composer), tested across appearances
-- [ ] SF Symbols for standard concepts; custom symbols SF-consistent and accessible
-
-### Accessibility
-
-- [ ] VoiceOver labels/hints for all interactive elements
-- [ ] Full keyboard navigation supported
-- [ ] Reduced motion/transparency/increased contrast tested
-
-### Performance & Stability
-
-- [ ] Large-document / heavy-data scenarios responsive
-- [ ] No major UI hitches while scrolling/resizing
-
----
-
-## 9) Source References
-
-**Apple Official:**
-- [Apple Newsroom - Design Update](https://www.apple.com/newsroom/2025/06/apple-introduces-a-delightful-and-elegant-new-software-design/)
-- [WWDC25: Meet Liquid Glass](https://developer.apple.com/videos/play/wwdc2025/219/)
-- [WWDC25: Get to know the new design system](https://developer.apple.com/videos/play/wwdc2025/356/)
-- [WWDC25: New look of app icons](https://developer.apple.com/videos/play/wwdc2025/220/)
-- [WWDC25: Create icons with Icon Composer](https://developer.apple.com/videos/play/wwdc2025/361/)
-- [SF Symbols](https://developer.apple.com/sf-symbols/)
-- [Icon Composer](https://developer.apple.com/icon-composer/)
-- [Apple Design Resources](https://developer.apple.com/design/resources/)
-- [Apple Design Awards](https://developer.apple.com/design/awards/)
-
-**Community:**
-- [App Feel on Mac](https://coyotetracks.org/blog/app-feel-on-mac/)
-- [Best in Class macOS App](https://swiftjectivec.com/What-does-a-best-in-class-macOS-app-look-like/)
-- [The macOS App Icon Book](https://flarup.shop/products/the-macos-app-icon-book)
diff --git a/.agents/skills/macos-design-guidelines/AGENTS.md b/.agents/skills/macos-design-guidelines/AGENTS.md
deleted file mode 100644
index aebfe5a1..00000000
--- a/.agents/skills/macos-design-guidelines/AGENTS.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# macOS Design Guidelines — Agent Instructions
-
-## Purpose
-
-This skill provides Apple Human Interface Guidelines for macOS. Apply these rules when building, reviewing, or designing Mac apps using SwiftUI or AppKit.
-
-## When to Apply
-
-- Building any macOS application
-- Reviewing Mac UI code or designs
-- Implementing menu bars, toolbars, sidebars, or window management
-- Adding keyboard shortcuts or pointer interactions
-- Porting iOS apps to Mac via Catalyst or Designed for iPad
-- Evaluating desktop app usability
-
-## How to Use
-
-1. Read `SKILL.md` for the full rule set with code examples
-2. Read `rules/_sections.md` for the categorized quick-reference
-3. Use the evaluation checklist in SKILL.md before shipping
-
-## Priority
-
-Rules marked CRITICAL must never be skipped. Rules marked HIGH should be followed unless there is a documented reason. Rules marked MEDIUM are strong recommendations.
-
-## Key Principles
-
-- Mac users expect menu bars, keyboard shortcuts, and multi-window support
-- Every destructive action needs Cmd+Z undo
-- Toolbars and sidebars should be user-customizable
-- Respect system appearance (Dark Mode, accent color, font size)
-- Support drag and drop everywhere it makes sense
-- Desktop apps are power-user tools — don't hide functionality behind discoverability walls
diff --git a/.agents/skills/macos-design-guidelines/SKILL.md b/.agents/skills/macos-design-guidelines/SKILL.md
deleted file mode 100644
index 873ef6c3..00000000
--- a/.agents/skills/macos-design-guidelines/SKILL.md
+++ /dev/null
@@ -1,965 +0,0 @@
----
-name: macos-design-guidelines
-description: Apple Human Interface Guidelines for Mac. Use when building macOS apps with SwiftUI or AppKit, implementing menu bars, toolbars, window management, or keyboard shortcuts. Triggers on tasks involving Mac UI, desktop apps, or Mac Catalyst.
-license: MIT
-metadata:
- author: platform-design-skills
- version: "1.0.0"
----
-
-# macOS Human Interface Guidelines
-
-Mac apps serve power users who expect deep keyboard control, persistent menu bars, resizable multi-window layouts, and tight system integration. These guidelines codify Apple's HIG into actionable rules with SwiftUI and AppKit examples.
-
----
-
-## 1. Menu Bar (CRITICAL)
-
-Every Mac app must have a menu bar. It is the primary discovery mechanism for commands. Users who cannot find a feature will look in the menu bar before anywhere else.
-
-### Rule 1.1 — Provide Standard Menus
-
-Every app must include at minimum: **App**, **File**, **Edit**, **View**, **Window**, **Help**. Omit File only if the app is not document-based. Add app-specific menus between Edit and View or between View and Window.
-
-```swift
-// SwiftUI — Standard menu structure
-@main
-struct MyApp: App {
- var body: some Scene {
- WindowGroup {
- ContentView()
- }
- .commands {
- // Adds to existing standard menus
- CommandGroup(after: .newItem) {
- Button("New from Template...") { newFromTemplate() }
- .keyboardShortcut("T", modifiers: [.command, .shift])
- }
- CommandMenu("Canvas") {
- Button("Zoom to Fit") { zoomToFit() }
- .keyboardShortcut("0", modifiers: .command)
- Divider()
- Button("Add Artboard") { addArtboard() }
- .keyboardShortcut("A", modifiers: [.command, .shift])
- }
- }
- }
-}
-```
-
-```swift
-// AppKit — Building menus programmatically
-let editMenu = NSMenu(title: "Edit")
-let undoItem = NSMenuItem(title: "Undo", action: #selector(UndoManager.undo), keyEquivalent: "z")
-let redoItem = NSMenuItem(title: "Redo", action: #selector(UndoManager.redo), keyEquivalent: "Z")
-editMenu.addItem(undoItem)
-editMenu.addItem(redoItem)
-editMenu.addItem(.separator())
-```
-
-### Rule 1.2 — Keyboard Shortcuts for All Menu Items
-
-Every menu item that performs an action must have a keyboard shortcut. Use standard shortcuts for standard actions (Cmd+C, Cmd+V, Cmd+Z, etc.). Custom shortcuts should use Cmd plus a letter. Reserve Cmd+Shift, Cmd+Option, and Cmd+Ctrl combos for secondary actions.
-
-**Standard Shortcut Reference:**
-
-| Action | Shortcut |
-|--------|----------|
-| New | Cmd+N |
-| Open | Cmd+O |
-| Close | Cmd+W |
-| Save | Cmd+S |
-| Save As | Cmd+Shift+S |
-| Print | Cmd+P |
-| Undo | Cmd+Z |
-| Redo | Cmd+Shift+Z |
-| Cut | Cmd+X |
-| Copy | Cmd+C |
-| Paste | Cmd+V |
-| Select All | Cmd+A |
-| Find | Cmd+F |
-| Find Next | Cmd+G |
-| Preferences/Settings | Cmd+, |
-| Hide App | Cmd+H |
-| Quit | Cmd+Q |
-| Minimize | Cmd+M |
-| Fullscreen | Cmd+Ctrl+F |
-
-### Rule 1.3 — Dynamic Menu Updates
-
-Menu items must reflect current state. Disable items that are not applicable. Update titles to match context (e.g., "Undo Typing" not just "Undo"). Toggle checkmarks for on/off states.
-
-```swift
-// SwiftUI — Dynamic menu state
-CommandGroup(replacing: .toolbar) {
- Button(showingSidebar ? "Hide Sidebar" : "Show Sidebar") {
- showingSidebar.toggle()
- }
- .keyboardShortcut("S", modifiers: [.command, .control])
-}
-```
-
-```swift
-// AppKit — Validate menu items
-override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
- if menuItem.action == #selector(delete(_:)) {
- menuItem.title = selectedItems.count > 1 ? "Delete \(selectedItems.count) Items" : "Delete"
- return !selectedItems.isEmpty
- }
- return super.validateMenuItem(menuItem)
-}
-```
-
-### Rule 1.4 — Contextual Menus
-
-Provide right-click context menus on all interactive elements. Context menus should contain the most relevant subset of menu bar actions for the clicked element, plus element-specific actions.
-
-```swift
-// SwiftUI
-Text(item.name)
- .contextMenu {
- Button("Rename...") { rename(item) }
- Button("Duplicate") { duplicate(item) }
- Divider()
- Button("Delete", role: .destructive) { delete(item) }
- }
-```
-
-### Rule 1.5 — App Menu Structure
-
-The App menu (leftmost, bold app name) must contain: About, Preferences/Settings (Cmd+,), Services submenu, Hide App (Cmd+H), Hide Others (Cmd+Option+H), Show All, Quit (Cmd+Q). Never rename or remove these standard items.
-
-```swift
-// SwiftUI — Settings scene
-@main
-struct MyApp: App {
- var body: some Scene {
- WindowGroup { ContentView() }
- Settings { SettingsView() } // Automatically wired to Cmd+,
- }
-}
-```
-
----
-
-## 2. Windows (CRITICAL)
-
-Mac users expect full control over window size, position, and lifecycle. An app that fights window management feels fundamentally broken on the Mac.
-
-### Rule 2.1 — Resizable with Sensible Minimums
-
-All main windows must be freely resizable. Set a minimum size that keeps the UI usable. Never set a maximum size unless the content truly cannot scale (rare).
-
-```swift
-// SwiftUI
-WindowGroup {
- ContentView()
- .frame(minWidth: 600, minHeight: 400)
-}
-.defaultSize(width: 900, height: 600)
-```
-
-```swift
-// AppKit
-window.minSize = NSSize(width: 600, height: 400)
-window.setContentSize(NSSize(width: 900, height: 600))
-```
-
-### Rule 2.2 — Support Fullscreen and Split View
-
-Opt into native fullscreen by setting the appropriate window collection behavior. The green traffic-light button must either enter fullscreen or show the tile picker.
-
-```swift
-// AppKit
-window.collectionBehavior.insert(.fullScreenPrimary)
-```
-
-SwiftUI windows get fullscreen support automatically.
-
-### Rule 2.3 — Multiple Windows
-
-Unless your app is a single-purpose utility, support multiple windows. Document-based apps must allow multiple documents open simultaneously. Use `WindowGroup` or `DocumentGroup` in SwiftUI.
-
-```swift
-// SwiftUI — Document-based app
-@main
-struct TextEditorApp: App {
- var body: some Scene {
- DocumentGroup(newDocument: TextDocument()) { file in
- TextEditorView(document: file.$document)
- }
- }
-}
-```
-
-### Rule 2.4 — Title Bar Shows Document Info
-
-For document-based apps, the title bar must show the document name. Support proxy icon dragging. Show edited state (dot in close button). Support title bar renaming on click.
-
-```swift
-// AppKit
-window.representedURL = document.fileURL
-window.title = document.displayName
-window.isDocumentEdited = document.hasUnsavedChanges
-```
-
-```swift
-// SwiftUI — NavigationSplitView titles
-NavigationSplitView {
- SidebarView()
-} detail: {
- DetailView()
- .navigationTitle(document.name)
-}
-```
-
-### Rule 2.5 — Remember Window State
-
-Persist window position, size, and state across launches. Use `NSWindow.setFrameAutosaveName` or SwiftUI's built-in state restoration.
-
-```swift
-// AppKit
-window.setFrameAutosaveName("MainWindow")
-
-// SwiftUI — Automatic with WindowGroup
-WindowGroup(id: "main") {
- ContentView()
-}
-.defaultPosition(.center)
-```
-
-### Rule 2.6 — Traffic Light Buttons
-
-Never hide or reposition the close (red), minimize (yellow), or zoom (green) buttons. They must remain in the top-left corner. If using a custom title bar, the buttons must still be visible and functional.
-
-```swift
-// AppKit — Custom title bar that preserves traffic lights
-window.titlebarAppearsTransparent = true
-window.styleMask.insert(.fullSizeContentView)
-// Traffic lights remain functional and visible
-```
-
----
-
-## 3. Toolbars (HIGH)
-
-Toolbars are the secondary command surface after the menu bar. They provide quick access to frequent actions and should be customizable.
-
-### Rule 3.1 — Unified Title Bar and Toolbar
-
-Use the unified title bar + toolbar style for a modern appearance. The toolbar sits in the title bar area, saving vertical space.
-
-```swift
-// SwiftUI
-WindowGroup {
- ContentView()
- .toolbar {
- ToolbarItem(placement: .primaryAction) {
- Button(action: compose) {
- Label("Compose", systemImage: "square.and.pencil")
- }
- }
- }
-}
-.windowToolbarStyle(.unified)
-```
-
-```swift
-// AppKit
-window.titleVisibility = .hidden
-window.toolbarStyle = .unified
-```
-
-### Rule 3.2 — User-Customizable Toolbars
-
-Allow users to add, remove, and rearrange toolbar items. Provide a default set and a superset of available items.
-
-```swift
-// SwiftUI — Customizable toolbar
-.toolbar(id: "main") {
- ToolbarItem(id: "compose", placement: .primaryAction) {
- Button(action: compose) {
- Label("Compose", systemImage: "square.and.pencil")
- }
- }
- ToolbarItem(id: "filter", placement: .secondaryAction) {
- Button(action: toggleFilter) {
- Label("Filter", systemImage: "line.3.horizontal.decrease")
- }
- }
-}
-.toolbarRole(.editor)
-```
-
-### Rule 3.3 — Segmented Controls for View Switching
-
-Use a segmented control or picker in the toolbar for switching between content views (e.g., List/Grid/Column). This is a toolbar pattern, not a tab bar.
-
-```swift
-// SwiftUI
-ToolbarItem(placement: .principal) {
- Picker("View Mode", selection: $viewMode) {
- Label("List", systemImage: "list.bullet").tag(ViewMode.list)
- Label("Grid", systemImage: "square.grid.2x2").tag(ViewMode.grid)
- Label("Column", systemImage: "rectangle.split.3x1").tag(ViewMode.column)
- }
- .pickerStyle(.segmented)
-}
-```
-
-### Rule 3.4 — Search Field in Toolbar
-
-Place the search field in the trailing area of the toolbar. Use `.searchable()` in SwiftUI for standard search behavior with suggestions and tokens.
-
-```swift
-// SwiftUI
-NavigationSplitView {
- SidebarView()
-} detail: {
- ContentListView()
- .searchable(text: $searchText, placement: .toolbar, prompt: "Search items")
- .searchSuggestions {
- ForEach(suggestions) { suggestion in
- Text(suggestion.title).searchCompletion(suggestion.title)
- }
- }
-}
-```
-
-### Rule 3.5 — Toolbar Labels and Icons
-
-Toolbar items should have both an icon (SF Symbol) and a text label. In compact mode, show icons only. Prefer labeled icons for discoverability. Use `Label` to supply both.
-
----
-
-## 4. Sidebars (HIGH)
-
-Sidebars are the primary navigation surface for Mac apps. They appear on the leading edge and provide persistent access to top-level sections and content libraries.
-
-### Rule 4.1 — Leading Edge, Collapsible
-
-Place the sidebar on the left (leading) edge. Make it collapsible via the toolbar button or Cmd+Ctrl+S. Persist collapsed state.
-
-```swift
-// SwiftUI
-NavigationSplitView(columnVisibility: $columnVisibility) {
- List(selection: $selection) {
- Section("Library") {
- Label("All Items", systemImage: "tray.full")
- Label("Favorites", systemImage: "star")
- Label("Recent", systemImage: "clock")
- }
- Section("Tags") {
- ForEach(tags) { tag in
- Label(tag.name, systemImage: "tag")
- }
- }
- }
- .navigationSplitViewColumnWidth(min: 180, ideal: 220, max: 320)
-} detail: {
- DetailView(selection: selection)
-}
-.navigationSplitViewStyle(.prominentDetail)
-```
-
-### Rule 4.2 — Source List Style
-
-Use the source list style (`.listStyle(.sidebar)`) for content-library navigation. Source lists have a translucent background that shows the desktop or window behind them with vibrancy effects.
-
-```swift
-// SwiftUI
-List(selection: $selection) {
- ForEach(sections) { section in
- Section(section.name) {
- ForEach(section.items) { item in
- NavigationLink(value: item) {
- Label(item.name, systemImage: item.icon)
- }
- }
- }
- }
-}
-.listStyle(.sidebar)
-```
-
-### Rule 4.3 — Outline Views for Hierarchies
-
-When content is hierarchical (e.g., folder trees, project structures), use disclosure groups or outline views to let users expand and collapse levels.
-
-```swift
-// SwiftUI — Recursive outline
-List(selection: $selection) {
- OutlineGroup(rootNodes, children: \.children) { node in
- Label(node.name, systemImage: node.icon)
- }
-}
-```
-
-### Rule 4.4 — Drag to Reorder
-
-Sidebar items that can be reordered (bookmarks, favorites, custom sections) must support drag-to-reorder. Implement `onMove` or `NSOutlineView` drag delegates.
-
-```swift
-// SwiftUI
-ForEach(favorites) { item in
- Label(item.name, systemImage: item.icon)
-}
-.onMove { source, destination in
- favorites.move(fromOffsets: source, toOffset: destination)
-}
-```
-
-### Rule 4.5 — Badge Counts
-
-Show badge counts on sidebar items for unread counts, pending items, or notifications. Use the `.badge()` modifier.
-
-```swift
-// SwiftUI
-Label("Inbox", systemImage: "tray")
- .badge(unreadCount)
-```
-
----
-
-## 5. Keyboard (CRITICAL)
-
-Mac users rely on keyboard shortcuts more than any other platform. An app without comprehensive keyboard support is a broken Mac app.
-
-### Rule 5.1 — Cmd Shortcuts for Everything
-
-Every action reachable by mouse must have a keyboard equivalent. Primary actions use Cmd+letter. Secondary actions use Cmd+Shift or Cmd+Option. Tertiary actions use Cmd+Ctrl.
-
-**Keyboard Shortcut Conventions:**
-
-| Modifier Pattern | Usage |
-|-----------------|-------|
-| Cmd+letter | Primary actions (New, Open, Save, etc.) |
-| Cmd+Shift+letter | Variant of primary (Save As, Find Previous) |
-| Cmd+Option+letter | Alternative mode (Paste and Match Style) |
-| Cmd+Ctrl+letter | Window/view controls (Fullscreen, Sidebar) |
-| Ctrl+letter | Emacs-style text navigation (acceptable) |
-| Fn+key | System functions (F11 Show Desktop, etc.) |
-
-### Rule 5.2 — Full Keyboard Navigation
-
-Support Tab to move between controls. Support arrow keys within lists, grids, and tables. Support Shift+Tab for reverse navigation. Use `focusable()` and `@FocusState` in SwiftUI.
-
-```swift
-// SwiftUI — Focus management
-struct ContentView: View {
- @FocusState private var focusedField: Field?
-
- var body: some View {
- VStack {
- TextField("Name", text: $name)
- .focused($focusedField, equals: .name)
- TextField("Email", text: $email)
- .focused($focusedField, equals: .email)
- }
- .onSubmit { advanceFocus() }
- }
-}
-```
-
-### Rule 5.3 — Escape to Cancel or Close
-
-Esc must dismiss popovers, sheets, dialogs, and cancel in-progress operations. In text fields, Esc reverts to the previous value. In modal dialogs, Esc is equivalent to clicking Cancel.
-
-```swift
-// SwiftUI — Sheet with Esc support (automatic)
-.sheet(isPresented: $showingSheet) {
- SheetView() // Esc dismisses automatically
-}
-
-// AppKit — Custom responder
-override func cancelOperation(_ sender: Any?) {
- dismiss(nil)
-}
-```
-
-### Rule 5.4 — Return for Default Action
-
-In dialogs and forms, Return/Enter activates the default button (visually emphasized in blue). The default button is always the safest primary action.
-
-```swift
-// SwiftUI
-Button("Save") { save() }
- .keyboardShortcut(.defaultAction) // Enter key
-
-Button("Cancel") { cancel() }
- .keyboardShortcut(.cancelAction) // Esc key
-```
-
-### Rule 5.5 — Delete for Removal
-
-The Delete key (Backspace) must remove selected items in lists, tables, and collections. Cmd+Delete for more destructive removal (move to Trash). Always support Cmd+Z to undo deletion.
-
-### Rule 5.6 — Space for Quick Look
-
-When items support previewing, Space bar should invoke Quick Look. Use the `QLPreviewPanel` API in AppKit or `.quickLookPreview()` in SwiftUI.
-
-```swift
-// SwiftUI
-List(selection: $selection) {
- ForEach(files) { file in
- FileRow(file: file)
- }
-}
-.quickLookPreview($quickLookItem, in: files)
-```
-
-### Rule 5.7 — Arrow Key Navigation
-
-In lists and grids, Up/Down arrow keys move selection. Left/Right collapse/expand disclosure groups or navigate columns. Cmd+Up goes to the beginning, Cmd+Down goes to the end.
-
----
-
-## 6. Pointer and Mouse (HIGH)
-
-Mac is a pointer-driven platform. Every interactive element must respond to hover, click, right-click, and drag.
-
-### Rule 6.1 — Hover States
-
-All interactive elements must have a visible hover state. Buttons highlight, rows show a selection indicator, links change cursor. Use `.onHover` in SwiftUI.
-
-```swift
-// SwiftUI — Hover effect
-struct HoverableRow: View {
- @State private var isHovered = false
-
- var body: some View {
- HStack {
- Text(item.name)
- Spacer()
- if isHovered {
- Button("Edit") { edit() }
- .buttonStyle(.borderless)
- }
- }
- .padding(8)
- .background(isHovered ? Color.primary.opacity(0.05) : .clear)
- .cornerRadius(6)
- .onHover { hovering in isHovered = hovering }
- }
-}
-```
-
-### Rule 6.2 — Right-Click Context Menus
-
-Every interactive element must respond to right-click with a contextual menu. The context menu should contain the most relevant actions for the clicked item.
-
-### Rule 6.3 — Drag and Drop
-
-Support drag and drop for content manipulation: reordering items, moving between containers, importing files from Finder, and exporting content.
-
-```swift
-// SwiftUI — Drag and drop
-ForEach(items) { item in
- ItemView(item: item)
- .draggable(item)
-}
-.dropDestination(for: Item.self) { items, location in
- handleDrop(items, at: location)
- return true
-}
-```
-
-```swift
-// Accepting file drops from Finder
-.dropDestination(for: URL.self) { urls, location in
- importFiles(urls)
- return true
-}
-```
-
-### Rule 6.4 — Scroll Behavior
-
-Support both trackpad (smooth/inertial) and mouse wheel (discrete) scrolling. Use elastic/bounce scrolling at content boundaries. Support horizontal scrolling where appropriate.
-
-### Rule 6.5 — Cursor Changes
-
-Change the cursor to indicate affordances: pointer for clickable elements, I-beam for text, crosshair for drawing, resize handles at window/splitter edges, grab hand for draggable content.
-
-```swift
-// AppKit — Custom cursor
-override func resetCursorRects() {
- addCursorRect(bounds, cursor: .crosshair)
-}
-```
-
-### Rule 6.6 — Multi-Selection
-
-Support Cmd+Click for non-contiguous selection and Shift+Click for range selection in lists, tables, and grids. This is a deeply ingrained Mac interaction pattern.
-
-```swift
-// SwiftUI — Tables with multi-selection
-Table(items, selection: $selectedItems) {
- TableColumn("Name", value: \.name)
- TableColumn("Date", value: \.dateFormatted)
- TableColumn("Size", value: \.sizeFormatted)
-}
-```
-
----
-
-## 7. Notifications and Alerts (MEDIUM)
-
-Mac users are protective of their attention. Only interrupt when truly necessary.
-
-### Rule 7.1 — Use Notification Center Appropriately
-
-Send notifications only for events that happen outside the app or require user action. Never notify for routine operations. Notifications must be actionable.
-
-```swift
-// UserNotifications
-let content = UNMutableNotificationContent()
-content.title = "Download Complete"
-content.body = "project-assets.zip is ready"
-content.categoryIdentifier = "DOWNLOAD"
-content.sound = .default
-
-let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)
-UNUserNotificationCenter.current().add(request)
-```
-
-### Rule 7.2 — Alerts with Suppression Option
-
-For recurring alerts, provide a "Do not show this again" checkbox. Respect the user's choice and persist it.
-
-```swift
-// AppKit — Alert with suppression
-let alert = NSAlert()
-alert.messageText = "Remove from library?"
-alert.informativeText = "The file will be moved to the Trash."
-alert.alertStyle = .warning
-alert.addButton(withTitle: "Remove")
-alert.addButton(withTitle: "Cancel")
-alert.showsSuppressionButton = true
-alert.suppressionButton?.title = "Do not ask again"
-
-let response = alert.runModal()
-if alert.suppressionButton?.state == .on {
- UserDefaults.standard.set(true, forKey: "suppressRemoveAlert")
-}
-```
-
-### Rule 7.3 — Don't Interrupt Unnecessarily
-
-Never show alerts for successful operations. Use inline status indicators, toolbar badges, or subtle animations instead. Reserve modal alerts for destructive or irreversible actions.
-
-### Rule 7.4 — Dock Badge
-
-Show a badge on the Dock icon for notification counts. Clear it promptly when the user addresses the notifications.
-
-```swift
-// AppKit
-NSApp.dockTile.badgeLabel = unreadCount > 0 ? "\(unreadCount)" : nil
-```
-
----
-
-## 8. System Integration (MEDIUM)
-
-Mac apps exist in a rich ecosystem. Deep integration makes an app feel native.
-
-### Rule 8.1 — Dock Icon and Menus
-
-Provide a high-quality 1024x1024 app icon. Support Dock right-click menus for quick actions. Show recent documents in the Dock menu.
-
-```swift
-// AppKit — Dock menu
-override func applicationDockMenu(_ sender: NSApplication) -> NSMenu? {
- let menu = NSMenu()
- menu.addItem(withTitle: "New Window", action: #selector(newWindow(_:)), keyEquivalent: "")
- menu.addItem(withTitle: "New Document", action: #selector(newDocument(_:)), keyEquivalent: "")
- menu.addItem(.separator())
- for doc in recentDocuments.prefix(5) {
- menu.addItem(withTitle: doc.name, action: #selector(openRecent(_:)), keyEquivalent: "")
- }
- return menu
-}
-```
-
-### Rule 8.2 — Spotlight Integration
-
-Index app content for Spotlight search using `CSSearchableItem` and Core Spotlight. Users expect to find app content via Cmd+Space.
-
-```swift
-import CoreSpotlight
-
-let attributeSet = CSSearchableItemAttributeSet(contentType: .text)
-attributeSet.title = document.title
-attributeSet.contentDescription = document.summary
-attributeSet.thumbnailData = document.thumbnail?.pngData()
-
-let item = CSSearchableItem(uniqueIdentifier: document.id, domainIdentifier: "documents", attributeSet: attributeSet)
-CSSearchableIndex.default().indexSearchableItems([item])
-```
-
-### Rule 8.3 — Quick Look Support
-
-Provide Quick Look previews for custom file types via a Quick Look Preview Extension. Users expect Space to preview any file in Finder.
-
-### Rule 8.4 — Share Extensions
-
-Implement the Share menu so users can share content from your app to Messages, Mail, Notes, etc. Also accept shared content from other apps.
-
-```swift
-// SwiftUI
-ShareLink(item: document.url) {
- Label("Share", systemImage: "square.and.arrow.up")
-}
-```
-
-### Rule 8.5 — Services Menu
-
-Register for the Services menu to receive text, URLs, or files from other apps. This is a uniquely Mac integration point that power users rely on.
-
-### Rule 8.6 — Shortcuts and AppleScript
-
-Support the Shortcuts app by providing App Intents. For advanced automation, add AppleScript/JXA scripting support via an `.sdef` scripting dictionary.
-
-```swift
-// App Intents for Shortcuts
-struct CreateDocumentIntent: AppIntent {
- static var title: LocalizedStringResource = "Create Document"
- static var description = IntentDescription("Creates a new document with the given title.")
-
- @Parameter(title: "Title")
- var title: String
-
- func perform() async throws -> some IntentResult {
- let doc = DocumentManager.shared.create(title: title)
- return .result(value: doc.title)
- }
-}
-```
-
----
-
-## 9. Visual Design (HIGH)
-
-Mac apps should look and feel like they belong on the platform. Use system-provided materials, fonts, and colors.
-
-### Rule 9.1 — Use System Fonts
-
-Use SF Pro (the system font) at standard dynamic type sizes. Use SF Mono for code. Never hardcode font sizes; use semantic styles.
-
-```swift
-// SwiftUI — Semantic font styles
-Text("Title").font(.title)
-Text("Headline").font(.headline)
-Text("Body text").font(.body)
-Text("Caption").font(.caption)
-Text("let x = 42").font(.system(.body, design: .monospaced))
-```
-
-### Rule 9.2 — Vibrancy and Materials
-
-Use system materials for sidebar and toolbar backgrounds. Vibrancy lets the desktop or underlying content show through, anchoring the app to the Mac visual language.
-
-```swift
-// SwiftUI
-List { ... }
- .listStyle(.sidebar) // Automatic vibrancy
-
-// Custom vibrancy
-ZStack {
- VisualEffectView(material: .sidebar, blendingMode: .behindWindow)
- Text("Sidebar Content")
-}
-```
-
-```swift
-// AppKit — Visual effect view
-let visualEffect = NSVisualEffectView()
-visualEffect.material = .sidebar
-visualEffect.blendingMode = .behindWindow
-visualEffect.state = .followsWindowActiveState
-```
-
-### Rule 9.3 — Respect System Accent Color
-
-Use the system accent color for selection, emphasis, and interactive elements. Never override it with a fixed brand color for standard controls. Use `.accentColor` or `.tint` only on custom views when appropriate.
-
-```swift
-// SwiftUI — Follows system accent automatically
-Button("Action") { doSomething() }
- .buttonStyle(.borderedProminent) // Uses system accent color
-
-Toggle("Enable feature", isOn: $isEnabled) // Toggle tint follows accent
-```
-
-### Rule 9.4 — Support Dark Mode
-
-Every view must support both Light and Dark appearances. Use semantic colors (`Color.primary`, `Color.secondary`, `.background`) rather than hardcoded colors. Test in both modes.
-
-```swift
-// SwiftUI — Semantic colors
-Text("Title").foregroundStyle(.primary)
-Text("Subtitle").foregroundStyle(.secondary)
-
-RoundedRectangle(cornerRadius: 8)
- .fill(Color(nsColor: .controlBackgroundColor))
-
-// Asset catalog: define colors for Both Appearances
-// Never use Color.white or Color.black for UI surfaces
-```
-
-### Rule 9.5 — Translucency
-
-Respect the "Reduce transparency" accessibility setting. When transparency is reduced, replace translucent materials with solid backgrounds.
-
-```swift
-// SwiftUI
-@Environment(\.accessibilityReduceTransparency) var reduceTransparency
-
-var body: some View {
- if reduceTransparency {
- Color(nsColor: .windowBackgroundColor)
- } else {
- VisualEffectView(material: .sidebar, blendingMode: .behindWindow)
- }
-}
-```
-
-### Rule 9.6 — Consistent Spacing and Layout
-
-Use 20pt standard margins, 8pt spacing between related controls, 20pt spacing between groups. Align controls to a grid. Use SwiftUI's built-in spacing or AppKit's Auto Layout with system spacing constraints.
-
----
-
-## Keyboard Shortcut Quick Reference
-
-### Navigation
-| Shortcut | Action |
-|----------|--------|
-| Cmd+N | New window/document |
-| Cmd+O | Open |
-| Cmd+W | Close window/tab |
-| Cmd+Q | Quit app |
-| Cmd+, | Settings/Preferences |
-| Cmd+Tab | Switch apps |
-| Cmd+` | Switch windows within app |
-| Cmd+T | New tab |
-
-### Editing
-| Shortcut | Action |
-|----------|--------|
-| Cmd+Z | Undo |
-| Cmd+Shift+Z | Redo |
-| Cmd+X / C / V | Cut / Copy / Paste |
-| Cmd+A | Select All |
-| Cmd+D | Duplicate |
-| Cmd+F | Find |
-| Cmd+G | Find Next |
-| Cmd+Shift+G | Find Previous |
-| Cmd+E | Use Selection for Find |
-
-### View
-| Shortcut | Action |
-|----------|--------|
-| Cmd+Ctrl+F | Toggle fullscreen |
-| Cmd+Ctrl+S | Toggle sidebar |
-| Cmd+0 | Show/hide toolbar |
-| Cmd++ / Cmd+- | Zoom in/out |
-| Cmd+0 | Actual size |
-
----
-
-## Evaluation Checklist
-
-Before shipping a Mac app, verify:
-
-### Menu Bar
-- [ ] App has a complete menu bar with standard menus
-- [ ] All actions have keyboard shortcuts
-- [ ] Menu items dynamically update (enable/disable, title changes)
-- [ ] Context menus on all interactive elements
-- [ ] App menu has About, Settings, Hide, Quit
-
-### Windows
-- [ ] Windows are freely resizable with sensible minimums
-- [ ] Fullscreen and Split View work
-- [ ] Multiple windows supported (if appropriate)
-- [ ] Window position and size persist across launches
-- [ ] Traffic light buttons visible and functional
-- [ ] Document title and edited state shown (if document-based)
-
-### Toolbars
-- [ ] Toolbar present with common actions
-- [ ] Toolbar is user-customizable
-- [ ] Search field available in toolbar
-
-### Sidebars
-- [ ] Sidebar for navigation (if app has multiple sections)
-- [ ] Sidebar is collapsible
-- [ ] Source list style with vibrancy
-
-### Keyboard
-- [ ] Full keyboard navigation (Tab, arrows, Enter, Esc)
-- [ ] Cmd+Z undo for all destructive actions
-- [ ] Space for Quick Look previews
-- [ ] Delete key removes selected items
-- [ ] No keyboard traps (user can always Tab out)
-
-### Pointer
-- [ ] Hover states on interactive elements
-- [ ] Right-click context menus everywhere
-- [ ] Drag and drop for content manipulation
-- [ ] Cmd+Click for multi-selection
-- [ ] Appropriate cursor changes
-
-### Notifications
-- [ ] Notifications only for important events
-- [ ] Alerts have suppression option for recurring ones
-- [ ] No modal alerts for routine operations
-
-### System Integration
-- [ ] High-quality Dock icon
-- [ ] Content indexed in Spotlight (if applicable)
-- [ ] Share menu works
-- [ ] App Intents for Shortcuts
-
-### Visual Design
-- [ ] System fonts at semantic sizes
-- [ ] Dark Mode fully supported
-- [ ] System accent color respected
-- [ ] Translucency respects accessibility setting
-- [ ] Consistent spacing on 8pt grid
-
----
-
-## Anti-Patterns
-
-**Do not do these things in a Mac app:**
-
-1. **No menu bar** — Every Mac app needs a menu bar. Period. A Mac app without menus is like a car without a steering wheel.
-
-2. **Hamburger menus** — Never use a hamburger menu on Mac. The menu bar exists for this purpose. Hamburger menus signal a lazy iOS port.
-
-3. **Tab bars at the bottom** — Mac apps use sidebars and toolbars, not iOS-style tab bars. If you need tabs, use actual document tabs in the tab bar (like Safari or Finder).
-
-4. **Large touch-sized targets** — Mac controls should be compact (22-28pt height). Users have precise pointer input. Giant buttons waste space and look out of place.
-
-5. **Floating action buttons** — FABs are a Material Design pattern. On Mac, place primary actions in the toolbar, menu bar, or as inline buttons.
-
-6. **Sheet for every action** — Don't use modal sheets for simple operations. Use popovers, inline editing, or direct manipulation. Sheets should be reserved for multi-step workflows or important decisions.
-
-7. **Custom window chrome** — Don't replace the standard title bar, traffic lights, or window controls with custom implementations. Users expect these to work consistently across all apps.
-
-8. **Ignoring keyboard** — If a power user must reach for the mouse to perform common actions, your keyboard support is insufficient.
-
-9. **Single-window only** — Unless your app is genuinely single-purpose (calculator, timer), support multiple windows. Users expect to Cmd+N for new windows.
-
-10. **Fixed window size** — Non-resizable windows feel broken on Mac. Users have displays ranging from 13" laptops to 32" externals and expect to use that space.
-
-11. **No Cmd+Z undo** — Every destructive or modifying action must be undoable. Users build muscle memory around Cmd+Z as their safety net.
-
-12. **Notification spam** — Mac apps that send excessive notifications get their permissions revoked. Only notify for events that genuinely need attention.
-
-13. **Ignoring Dark Mode** — A Mac app that looks wrong in Dark Mode appears abandoned. Always test both appearances.
-
-14. **Hardcoded colors** — Use semantic system colors, not hardcoded hex values. Your colors should adapt to Light/Dark mode and accessibility settings automatically.
-
-15. **No drag and drop** — Mac is a drag-and-drop platform. If users can see content, they expect to drag it somewhere.
diff --git a/.agents/skills/neon-postgres/SKILL.md b/.agents/skills/neon-postgres/SKILL.md
deleted file mode 100644
index 9ef63207..00000000
--- a/.agents/skills/neon-postgres/SKILL.md
+++ /dev/null
@@ -1,186 +0,0 @@
----
-name: neon-postgres
-description: Guides and best practices for working with Neon Serverless Postgres. Covers getting started, local development with Neon, choosing a connection method, Neon features, authentication (@neondatabase/auth), PostgREST-style data API (@neondatabase/neon-js), Neon CLI, and Neon's Platform API/SDKs. Use for any Neon-related questions.
----
-
-# Neon Serverless Postgres
-
-Neon is a serverless Postgres platform that separates compute and storage to offer autoscaling, branching, instant restore, and scale-to-zero. It's fully compatible with Postgres and works with any language, framework, or ORM that supports Postgres.
-
-## Neon Documentation
-
-The Neon documentation is the source of truth for all Neon-related information. Always verify claims against the official docs before responding. Neon features and APIs evolve, so prefer fetching current docs over relying on training data.
-
-### Fetching Docs as Markdown
-
-Any Neon doc page can be fetched as markdown in two ways:
-
-1. **Append `.md` to the URL** (simplest): `https://neon.com/docs/introduction/branching.md`
-2. **Request `text/markdown`** on the standard URL: `curl -H "Accept: text/markdown" https://neon.com/docs/introduction/branching`
-
-Both return the same markdown content. Use whichever method your tools support.
-
-### Finding the Right Page
-
-The docs index lists every available page with its URL and a short description:
-
-```
-https://neon.com/docs/llms.txt
-```
-
-Common doc URLs are organized in the topic links below. If you need a page not listed here, search the [docs index](https://neon.com/docs/llms.txt) — don't guess URLs.
-
-## What Is Neon
-
-Use this for architecture explanations and terminology (organizations, projects, branches, endpoints) before giving implementation advice.
-
-Link: `references/what-is-neon.md`
-
-## Getting Started
-
-Use this for first-time setup: org/project selection, connection strings, driver installation, optional auth, and initial schema setup.
-
-Link: `references/getting-started.md`
-
-## Connection Methods & Drivers
-
-Use this when you need to pick the correct transport and driver based on runtime constraints (TCP, HTTP, WebSocket, edge, serverless, long-running).
-
-Link: `references/connection-methods.md`
-
-### Serverless Driver
-
-Use this for `@neondatabase/serverless` patterns, including HTTP queries, WebSocket transactions, and runtime-specific optimizations.
-
-Link: `references/neon-serverless.md`
-
-### Neon JS SDK
-
-Use this for combined Neon Auth + Data API workflows with PostgREST-style querying and typed client setup.
-
-Link: `references/neon-js.md`
-
-## Developer Tools
-
-Use this for local development enablement with `npx neonctl@latest init`, VSCode extension setup, and Neon MCP server configuration.
-
-Link: `references/devtools.md`
-
-### Neon CLI
-
-Use this for terminal-first workflows, scripts, and CI/CD automation with `neonctl`.
-
-Link: `references/neon-cli.md`
-
-## Neon Admin API
-
-The Neon Admin API can be used to manage Neon resources programmatically. It is used behind the scenes by the Neon CLI and MCP server, but can also be used directly for more complex automation workflows or when embedding Neon in other applications.
-
-### Neon REST API
-
-Use this for direct HTTP automation, endpoint-level control, API key auth, rate-limit handling, and operation polling.
-
-Link: `references/neon-rest-api.md`
-
-### Neon TypeScript SDK
-
-Use this when implementing typed programmatic control of Neon resources in TypeScript via `@neondatabase/api-client`.
-
-Link: `references/neon-typescript-sdk.md`
-
-### Neon Python SDK
-
-Use this when implementing programmatic Neon management in Python with the `neon-api` package.
-
-Link: `references/neon-python-sdk.md`
-
-## Neon Auth
-
-Use this for managed user authentication setup, UI components, auth methods, and Neon Auth integration pitfalls in Next.js and React apps.
-
-Link: `references/neon-auth.md`
-
-Neon Auth is also embedded in the Neon JS SDK - so depending on your use case, you may want to use the Neon JS SDK instead of Neon Auth. See `references/connection-methods.md` for more details.
-
-## Branching
-
-Use this when the user is planning isolated environments, schema migration testing, preview deployments, or branch lifecycle automation.
-
-Key points:
-
-- Branches are instant, copy-on-write clones (no full data copy).
-- Each branch has its own compute endpoint.
-- Use the neonctl CLI or MCP server to create, inspect, and compare branches.
-
-Link: `references/branching.md`
-
-## Autoscaling
-
-Use this when the user needs compute to scale automatically with workload and wants guidance on CU sizing and runtime behavior.
-
-Link: https://neon.com/docs/introduction/autoscaling.md
-
-## Scale to Zero
-
-Use this when optimizing idle costs and discussing suspend/resume behavior, including cold-start trade-offs.
-
-Key points:
-
-- Idle computes suspend automatically (default 5 minutes, configurable) (unless disabled - launch & scale plan only)
-- First query after suspend typically has a cold-start penalty (around hundreds of ms)
-- Storage remains active while compute is suspended.
-
-Link: https://neon.com/docs/introduction/scale-to-zero.md
-
-## Instant Restore
-
-Use this when the user needs point-in-time recovery or wants to restore data state without traditional backup restore workflows.
-
-Key points:
-
-- Restore windows depend on plan limits.
-- Users can create branches from historical points-in-time.
-- Time Travel queries can be used for historical inspection workflows.
-
-Link: https://neon.com/docs/introduction/branch-restore.md
-
-## Read Replicas
-
-Use this for read-heavy workloads where the user needs dedicated read-only compute without duplicating storage.
-
-Key points:
-
-- Replicas are read-only compute endpoints sharing the same storage.
-- Creation is fast and scaling is independent from primary compute.
-- Typical use cases: analytics, reporting, and read-heavy APIs.
-
-Link: https://neon.com/docs/introduction/read-replicas.md
-
-## Connection Pooling
-
-Use this when the user is in serverless or high-concurrency environments and needs safe, scalable Postgres connection management.
-
-Key points:
-
-- Neon pooling uses PgBouncer.
-- Add `-pooler` to endpoint hostnames to use pooled connections.
-- Pooling is especially important in serverless runtimes with bursty concurrency.
-
-Link: https://neon.com/docs/connect/connection-pooling.md
-
-## IP Allow Lists
-
-Use this when the user needs to restrict database access by trusted networks, IPs, or CIDR ranges.
-
-Link: https://neon.com/docs/introduction/ip-allow.md
-
-## Logical Replication
-
-Use this when integrating CDC pipelines, external Postgres sync, or replication-based data movement.
-
-Key points:
-
-- Neon supports native logical replication workflows.
-- Useful for replicating to/from external Postgres systems.
-
-Link: https://neon.com/docs/guides/logical-replication-guide.md
diff --git a/.agents/skills/neon-postgres/references/branching.md b/.agents/skills/neon-postgres/references/branching.md
deleted file mode 100644
index 4e22987b..00000000
--- a/.agents/skills/neon-postgres/references/branching.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# Branching in Neon
-
-Neon branching gives you instant, copy-on-write database environments. Use this for preview environments, migration testing, branch-based isolation, and restore-style workflows.
-
-## Product Concept and Branching Behavior
-
-Start here for branching mechanics, parent-child model, and practical usage patterns.
-
-Link: https://neon.com/docs/introduction/branching.md
-
-## Branch Operations via REST API
-
-Use this when automating branch creation, listing, or deletion through direct HTTP integration.
-
-Link: `references/neon-rest-api.md`
-
-## Branch Operations via CLI
-
-Use this for terminal workflows and CI jobs that create and clean up temporary branches.
-
-Link: `references/neon-cli.md`
-
-## Branch Operations via MCP Server
-
-Use this when an AI assistant should create, inspect, or compare branches directly from tool calls.
-
-Link: https://neon.com/docs/ai/neon-mcp-server.md
diff --git a/.agents/skills/neon-postgres/references/connection-methods.md b/.agents/skills/neon-postgres/references/connection-methods.md
deleted file mode 100644
index f63b6fd9..00000000
--- a/.agents/skills/neon-postgres/references/connection-methods.md
+++ /dev/null
@@ -1,179 +0,0 @@
-# Connection Methods
-
-Guide to selecting the optimal connection method for your Neon Postgres database based on deployment platform and runtime environment.
-
-See the [official connection guide](https://neon.com/docs/connect/choose-connection.md) for complete details.
-
-## Decision Tree
-
-Follow this flow to determine the right connection approach:
-
-### 1. What Language Are You Using?
-
-**Not TypeScript/JavaScript** → Use **TCP with connection pooling** from a secure server.
-
-For non-TypeScript languages, connect from a secure backend server using your language's native Postgres driver with connection pooling enabled.
-
-| Language/Framework | Documentation |
-| ------------------- | --------------------------------------------- |
-| Django (Python) | https://neon.com/docs/guides/django.md |
-| SQLAlchemy (Python) | https://neon.com/docs/guides/sqlalchemy.md |
-| Elixir Ecto | https://neon.com/docs/guides/elixir-ecto.md |
-| Laravel (PHP) | https://neon.com/docs/guides/laravel.md |
-| Ruby on Rails | https://neon.com/docs/guides/ruby-on-rails.md |
-| Go | https://neon.com/docs/guides/go.md |
-| Rust | https://neon.com/docs/guides/rust.md |
-| Java | https://neon.com/docs/guides/java.md |
-
-**TypeScript/JavaScript** → Continue to step 2.
-
----
-
-### 2. Client-Side App Without Backend?
-
-**Yes** → Use **Neon Data API** via `@neondatabase/neon-js`
-
-This is the only option for client-side apps since browsers cannot make direct TCP connections to Postgres. See `neon-js.md` for setup and the [JavaScript SDK docs](https://neon.com/docs/reference/javascript-sdk.md) for the full reference.
-
-**No** → Continue to step 3.
-
----
-
-### 3. Long-Running Server? (Railway, Render, traditional VPS)
-
-**Yes** → Use **TCP with connection pooling** via `node-postgres`, `postgres.js`, or `bun:pg`
-
-Long-running servers maintain persistent connections, so standard TCP drivers with pooling are optimal.
-
-**No** → Continue to step 4.
-
----
-
-### 4. Edge Environment Without TCP Support?
-
-Some edge runtimes don't support TCP connections. Rarely the case anymore.
-
-**Yes** → Continue to step 5 to check transaction requirements.
-
-**No** → Continue to step 6 to check pooling support.
-
----
-
-### 5. Does Your App Use SQL Transactions?
-
-**Yes** → Use **WebSocket transport** via `@neondatabase/serverless` with `Pool`
-
-WebSocket maintains connection state needed for transactions. See `neon-serverless.md` for setup.
-
-**No** → Use **HTTP transport** via `@neondatabase/serverless`
-
-HTTP is faster for single queries (~3 roundtrips vs ~8 for TCP). See `neon-serverless.md` for setup and the [serverless driver docs](https://neon.com/docs/serverless/serverless-driver.md) for the full reference.
-
----
-
-### 6. Serverless Environment With Connection Pooling Support?
-
-**Vercel (Fluid Compute)** → Use **TCP with `@vercel/functions`**
-
-Vercel's Fluid compute supports connection pooling. Use `attachDatabasePool` for optimal connection management. See the [Vercel connection methods guide](https://neon.com/docs/guides/vercel-connection-methods.md) for details.
-
-**Cloudflare (with Hyperdrive)** → Use **TCP via Hyperdrive**
-
-Cloudflare Hyperdrive provides connection pooling for Workers. Use `node-postgres` or any native TCP driver.
-
-See the [Cloudflare Hyperdrive guide](https://neon.com/docs/guides/cloudflare-hyperdrive.md) for connecting with Cloudflare Workers and Hyperdrive.
-
-Also consider Placement Hints to ensure your Workers are deployed as close as possible to your Neon database - read the [Placement Configuration docs](https://developers.cloudflare.com/workers/configuration/placement/) for details.
-
-**No pooling support (Netlify, Deno Deploy)** → Use `@neondatabase/serverless`
-
-Fall back to the decision in step 5 based on transaction requirements.
-
----
-
-## Quick Reference Table
-
-| Platform | TCP Support | Pooling | Recommended Driver |
-| ----------------------- | ----------- | ------------------- | -------------------------- |
-| Vercel (Fluid) | Yes | `@vercel/functions` | `pg` (node-postgres) |
-| Cloudflare (Hyperdrive) | Yes | Hyperdrive | `pg` (node-postgres) |
-| Cloudflare Workers | No | No | `@neondatabase/serverless` |
-| Netlify Functions | No | No | `@neondatabase/serverless` |
-| Deno Deploy | No | No | `@neondatabase/serverless` |
-| Railway / Render | Yes | Built-in | `pg` (node-postgres) |
-| Client-side (browser) | No | N/A | `@neondatabase/neon-js` |
-
----
-
-## ORM Support
-
-Popular TypeScript/JavaScript ORMs all work with Neon:
-
-| ORM | Drivers Supported | Documentation |
-| ------- | ----------------------------------------------- | --------------------------------------- |
-| Drizzle | `pg`, `postgres.js`, `@neondatabase/serverless` | https://neon.com/docs/guides/drizzle.md |
-| Kysely | `pg`, `postgres.js`, `@neondatabase/serverless` | https://neon.com/docs/guides/kysely.md |
-| Prisma | `pg`, `@neondatabase/serverless` | https://neon.com/docs/guides/prisma.md |
-| TypeORM | `pg` | https://neon.com/docs/guides/typeorm.md |
-
-All ORMs support both TCP drivers and Neon's serverless driver depending on your platform.
-
-For Drizzle ORM integration with Neon, see `neon-drizzle.md`.
-
----
-
-## Vercel Fluid + Drizzle Example
-
-Complete database client setup for Vercel with Drizzle ORM and connection pooling. See `neon-drizzle.md` for more examples.
-
-```typescript
-// src/lib/db/client.ts
-import { attachDatabasePool } from "@vercel/functions";
-import { drizzle } from "drizzle-orm/node-postgres";
-import { Pool } from "pg";
-
-import * as schema from "./schema";
-
-const pool = new Pool({
- connectionString: process.env.DATABASE_URL,
-});
-attachDatabasePool(pool);
-
-export const db = drizzle({ client: pool, schema });
-```
-
-**Why `attachDatabasePool`?**
-
-- First request establishes the TCP connection (~8 roundtrips)
-- Subsequent requests reuse the connection instantly
-- Ensures idle connections close gracefully before function suspension
-- Prevents connection leaks in serverless environments
-
----
-
-## Gathering Requirements
-
-When helping a user choose their connection method, gather this information:
-
-1. **Deployment platform**: Where will the app run? (Vercel, Cloudflare, Netlify, Railway, browser, etc.)
-2. **Runtime type**: Serverless functions, edge functions, or long-running server?
-3. **Transaction requirements**: Does the app need SQL transactions?
-4. **ORM preference**: Using Drizzle, Kysely, Prisma, or raw SQL?
-
-Then provide:
-
-- The recommended driver/package
-- A working code example for their setup
-- The correct npm install command
-
----
-
-## Documentation Resources
-
-| Topic | URL |
-| -------------------------- | --------------------------------------------------------- |
-| Choosing Connection Method | https://neon.com/docs/connect/choose-connection.md |
-| Serverless Driver | https://neon.com/docs/serverless/serverless-driver.md |
-| JavaScript SDK | https://neon.com/docs/reference/javascript-sdk.md |
-| Connection Pooling | https://neon.com/docs/connect/connection-pooling.md |
-| Vercel Connection Methods | https://neon.com/docs/guides/vercel-connection-methods.md |
diff --git a/.agents/skills/neon-postgres/references/devtools.md b/.agents/skills/neon-postgres/references/devtools.md
deleted file mode 100644
index 1f8fa763..00000000
--- a/.agents/skills/neon-postgres/references/devtools.md
+++ /dev/null
@@ -1,109 +0,0 @@
-# Neon Developer Tools
-
-Neon provides developer tools to enhance your local development workflow, including a VSCode extension and MCP server for AI-assisted development.
-
-## Quick Setup with neon init
-
-The fastest way to set up all Neon developer tools:
-
-```bash
-npx neonctl@latest init
-```
-
-This command:
-
-- Installs the Neon VSCode extension
-- Configures the Neon MCP server for AI assistants
-- Sets up your local environment for Neon development
-
-See the [full CLI init reference](https://neon.com/docs/reference/cli-init.md) for all options.
-
-## VSCode Extension
-
-The Neon VSCode extension provides:
-
-- **Database Explorer**: Browse projects, branches, tables, and data
-- **SQL Editor**: Write and execute queries with IntelliSense
-- **Branch Management**: Create, switch, and manage database branches
-- **Connection String Access**: Quick copy of connection strings
-
-**Install from VSCode:**
-
-1. Open Extensions (Cmd/Ctrl+Shift+X)
-2. Search "Neon"
-3. Install "Neon" by Neon
-
-**Or via command line:**
-
-```bash
-code --install-extension neon.neon-vscode
-```
-
-See the [full VSCode extension docs](https://neon.com/docs/local/vscode-extension.md) for all features.
-
-## Neon MCP Server
-
-The Neon MCP (Model Context Protocol) server enables AI assistants like Claude, Cursor, and GitHub Copilot to interact with your Neon databases directly.
-
-### Capabilities
-
-The MCP server provides AI assistants with:
-
-- **Project Management**: List, create, describe, and delete projects
-- **Branch Operations**: Create branches, compare schemas, reset from parent
-- **SQL Execution**: Run queries and transactions
-- **Schema Operations**: Describe tables, get database structure
-- **Migrations**: Prepare and complete database migrations with safety checks
-- **Query Tuning**: Analyze and optimize slow queries
-- **Neon Auth**: Provision authentication for your branches
-
-### Setup
-
-**Option 1: Via neon init (Recommended)**
-
-```bash
-npx neonctl@latest init
-```
-
-**Option 2: Manual Configuration**
-
-Add to your AI assistant's MCP configuration:
-
-```json
-{
- "mcpServers": {
- "neon": {
- "command": "npx",
- "args": ["-y", "@neondatabase/mcp-server-neon"],
- "env": {
- "NEON_API_KEY": "your-api-key"
- }
- }
- }
-}
-```
-
-Get your API key from: https://console.neon.tech/app/settings/api-keys
-
-### Common MCP Operations
-
-| Operation | What It Does |
-| ---------------------------- | ----------------------------- |
-| `list_projects` | Show all Neon projects |
-| `create_project` | Create a new project |
-| `run_sql` | Execute SQL queries |
-| `get_connection_string` | Get database connection URL |
-| `create_branch` | Create a database branch |
-| `prepare_database_migration` | Safely prepare schema changes |
-| `provision_neon_auth` | Set up Neon Auth |
-
-See the [full MCP server docs](https://neon.com/docs/ai/neon-mcp-server.md) for all available operations.
-
-## Documentation Resources
-
-| Topic | URL |
-| ------------------ | ----------------------------------------------- |
-| CLI Init Command | https://neon.com/docs/reference/cli-init.md |
-| VSCode Extension | https://neon.com/docs/local/vscode-extension.md |
-| MCP Server | https://neon.com/docs/ai/neon-mcp-server.md |
-| Neon CLI Reference | https://neon.com/docs/reference/neon-cli.md |
diff --git a/.agents/skills/neon-postgres/references/getting-started.md b/.agents/skills/neon-postgres/references/getting-started.md
deleted file mode 100644
index dd23b0fd..00000000
--- a/.agents/skills/neon-postgres/references/getting-started.md
+++ /dev/null
@@ -1,118 +0,0 @@
-# Getting Started with Neon
-
-Interactive guide for setting up a Neon project and connecting it to code.
-
-## Check Status Quo
-
-Inspect the user's codebase and environment to see if they have already integrated Neon and to better understand their needs and constraints.
-
-Specifically check for:
-
-- Existing database connection code
-- Existing Neon MCP server or Neon CLI configuration
-- Existence of a `.env` file and `DATABASE_URL` environment variable
-- Existing ORM (Prisma, Drizzle, TypeORM) configuration
-
-## Self-Driving Setup With Neon's CLI or MCP Server
-
-You can offer the user to inspect the existing connected Neon projects or create new ones using the Neon CLI or MCP server.
-
-If the MCP server and CLI aren't set up yet, ask the user for permission to run:
-
-```bash
-npx neonctl@latest init
-```
-
-This will install the Neon VSCode extension (if applicable) and the Neon MCP server and `neon-postgres` agent skill. Alternatively, you can offer to install the Neon CLI. Install instructions here: https://neon.com/docs/reference/cli-install.md
-
-Either CLI or MCP server can be used to manage Neon projects and databases on the user's behalf. If the user prefers to manually get started with Neon, then you can guide them through the setup process instead of using the CLI or MCP server directly. See `devtools.md` for details.
-
-Since the Neon CLI and MCP server do interact with database resources, it's important to verify the user is comfortable with the security implications of running these tools.
-
-## Setup Flow
-
-### 1. Select Organization and Project
-
-- Check existing organizations and projects (via MCP server or CLI or manually by the user)
-- **1 organization**: default to it
-- **Multiple organizations**: list all and ask which to use
-- **No projects**: ask if they want to create a new project
-- **1 project**: ask "Would you like to use '{project_name}' or create a new one?"
-- **Multiple projects (<6)**: list all and let them choose
-- **Many projects (6+)**: list recent projects, offer to create new or specify by name/ID
-
-### 2. Get Connection String
-
-- Use MCP server or CLI to get the connection string
-- Store it in `.env` as `DATABASE_URL`:
-
-```
-DATABASE_URL=postgresql://user:password@host/database
-```
-
-**Before modifying `.env`:**
-
-1. Try to read the `.env` file first
-2. If readable: use search/replace to update or append `DATABASE_URL`
-3. If unreadable (permissions): use append command or show the line to add manually
-4. Never overwrite an existing `.env` — always append or update in place
-
-### 3. Pick Connection Method & Pick Driver
-
-Refer to `connection-methods.md` to pick the correct connection method and driver based on your deployment platform.
-
-#### 3.1. User Authentication with Neon Auth (if needed)
-
-Skip for CLI tools, scripts, or apps without user accounts.
-
-If the app needs auth: use MCP server `provision_neon_auth` tool, then see `neon-auth.md` for setup. For auth + database queries, see `neon-js.md`.
-
-#### 3.2. ORM Setup (optional)
-
-Check for existing ORM (Prisma, Drizzle, TypeORM). If none, ask if they want one. For Drizzle integration, see `neon-drizzle.md`.
-
-### 6. Schema Setup
-
-- Check for existing migration files or ORM schemas
-- If none: offer to create an example schema or design one together
-
-## What's Next
-
-After setup is complete, offer to help with:
-
-- Neon-specific features (branching, autoscaling, scale-to-zero)
-- Connection pooling for production
-- Writing queries or building API endpoints
-- Database migrations and schema changes
-- Performance optimization
-
-## Resume Support
-
-If the user says "Continue with Neon setup", check what's already configured:
-
-- MCP server connection
-- `.env` file with `DATABASE_URL`
-- Dependencies installed
-- Schema created
-
-Then resume from where they left off.
-
-## Security Reminders
-
-- Never commit connection strings to version control
-- Use environment variables for all credentials
-- Prefer SSL connections (default in Neon)
-- Use least-privilege database roles
-- Rotate API keys and passwords regularly
-
-## Documentation
-
-| Topic | URL |
-| ------------------ | ----------------------------------------------------- |
-| Getting Started | https://neon.com/docs/get-started/signing-up.md |
-| Connecting to Neon | https://neon.com/docs/connect/connect-intro.md |
-| Connection String | https://neon.com/docs/connect/connect-from-any-app.md |
-| Frameworks Guide | https://neon.com/docs/get-started/frameworks.md |
-| ORMs Guide | https://neon.com/docs/get-started/orms.md |
-| VSCode Extension | https://neon.com/docs/local/vscode-extension.md |
-| MCP Server | https://neon.com/docs/ai/neon-mcp-server.md |
diff --git a/.agents/skills/neon-postgres/references/neon-auth.md b/.agents/skills/neon-postgres/references/neon-auth.md
deleted file mode 100644
index 579d0b27..00000000
--- a/.agents/skills/neon-postgres/references/neon-auth.md
+++ /dev/null
@@ -1,413 +0,0 @@
-# Neon Auth
-
-Neon Auth provides managed authentication that stores users, sessions, and auth configuration directly in your Neon database. When you branch your database, your entire auth state branches with it.
-
-See the [official Neon Auth docs](https://neon.com/docs/auth/overview.md) for complete details.
-
-## Package Selection
-
-| Framework / Use Case | Package | Notes |
-| ----------------------- | ----------------------- | ------------------------------ |
-| Next.js | `@neondatabase/auth` | Server + client SDK |
-| React SPA (Vite, etc) | `@neondatabase/neon-js` | Client SDK + optional Data API |
-| Auth + Database queries | `@neondatabase/neon-js` | Full SDK |
-
-Both packages share auth exports (`@neondatabase/neon-js/auth/*` re-exports `@neondatabase/auth/*`).
-
-```bash
-# Next.js
-npm install @neondatabase/auth@latest
-
-# React SPA / Full SDK
-npm install @neondatabase/neon-js@latest
-```
-
-> **Note:** While these packages are in pre-release (beta), you must use `@latest` with npm. Without it, npm may install an older version. This is not needed with pnpm or yarn.
-
-## Next.js Setup
-
-**1. Server auth instance** (`lib/auth/server.ts`):
-
-```typescript
-import { createNeonAuth } from "@neondatabase/auth/next/server";
-
-export const auth = createNeonAuth({
- baseUrl: process.env.NEON_AUTH_BASE_URL!,
- cookies: {
- secret: process.env.NEON_AUTH_COOKIE_SECRET!,
- },
-});
-```
-
-**2. API route handler** (`app/api/auth/[...path]/route.ts`):
-
-```typescript
-import { auth } from "@/lib/auth/server";
-export const { GET, POST } = auth.handler();
-```
-
-**3. Middleware** (`middleware.ts`):
-
-```typescript
-import { auth } from "@/lib/auth/server";
-
-export default auth.middleware({
- loginUrl: "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/auth/sign-in",
-});
-
-export const config = {
- matcher: ["/account/:path*"],
-};
-```
-
-**4. Client** (`lib/auth/client.ts`):
-
-```typescript
-"use client";
-import { createAuthClient } from "@neondatabase/auth/next";
-export const authClient = createAuthClient();
-```
-
-**5. Server component access** (must set `force-dynamic`):
-
-```typescript
-import { auth } from "@/lib/auth/server";
-
-export const dynamic = "force-dynamic";
-
-export default async function DashboardPage() {
- const { data: session } = await auth.getSession();
- if (!session?.user) return
Not logged in
;
- return
Hello {session.user.name}
;
-}
-```
-
-**6. UI setup** — Add the provider, CSS, and page components. See [UI Components](#ui-components) below for `NeonAuthUIProvider`, CSS imports, `AuthView`, and `AccountView` setup.
-
-See the [Next.js quickstart](https://neon.com/docs/auth/quick-start/nextjs.md) and [server SDK reference](https://neon.com/docs/auth/reference/nextjs-server.md) for the full setup.
-
-### Environment Variables (Next.js)
-
-```bash
-NEON_AUTH_BASE_URL=https://ep-xxx.neonauth.us-east-1.aws.neon.tech/neondb/auth
-NEON_AUTH_COOKIE_SECRET=your-secret-at-least-32-characters-long
-```
-
-Get your Auth URL from the Neon Console: Project -> Branch -> Auth -> Configuration.
-
-Generate a cookie secret: `openssl rand -base64 32`
-
-## React SPA Setup
-
-**1. Auth client** (`lib/auth.ts`):
-
-```typescript
-import { createAuthClient } from "@neondatabase/neon-js/auth";
-
-export const authClient = createAuthClient(import.meta.env.VITE_NEON_AUTH_URL);
-```
-
-If you need `useSession()` in custom components, pass an adapter:
-
-```typescript
-import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react";
-
-const authClient = createAuthClient(import.meta.env.VITE_NEON_AUTH_URL, {
- adapter: BetterAuthReactAdapter(),
-});
-```
-
-UI components (`AuthView`, `SignedIn`, etc.) work without an adapter.
-
-**2. UI setup** — Wrap your app with `NeonAuthUIProvider` and import CSS. See [UI Components](#ui-components) below. In a SPA, the provider and CSS go in your root component (e.g., `App.tsx` or your router layout).
-
-**3. Routing** — Map `AuthView` and `AccountView` to routes in your router (React Router, TanStack Router, etc.). For example, with React Router:
-
-```tsx
-} />
-} />
-```
-
-### Environment Variables (React SPA)
-
-```bash
-VITE_NEON_AUTH_URL=https://ep-xxx.neonauth.us-east-1.aws.neon.tech/neondb/auth
-```
-
-See the [React quickstart with UI components](https://neon.com/docs/auth/quick-start/react-router-components.md) and [React API-only quickstart](https://neon.com/docs/auth/quick-start/react.md) for the full setup.
-
-## UI Components
-
-Use pre-built components instead of building custom auth forms.
-
-| Component | Purpose |
-| ------------------------ | ----------------------------------------- |
-| `AuthView` | Sign-in, sign-up, forgot-password pages |
-| `AccountView` | Account settings, security pages |
-| `UserButton` | User avatar with dropdown menu |
-| `SignedIn` / `SignedOut` | Conditional rendering based on auth state |
-| `RedirectToSignIn` | Redirect unauthenticated users |
-| `RedirectToSignUp` | Redirect to sign-up page |
-
-See the [UI components reference](https://neon.com/docs/auth/reference/ui-components.md) for full props and customization.
-
-### CSS (choose one, never both)
-
-The CSS import path depends on which package you installed:
-
-```typescript
-// Next.js (@neondatabase/auth)
-import "@neondatabase/auth/ui/css";
-
-// React SPA (@neondatabase/neon-js)
-import "@neondatabase/neon-js/ui/css";
-```
-
-```css
-/* With Tailwind v4 — Next.js */
-@import "tailwindcss";
-@import "@neondatabase/auth/ui/tailwind";
-
-/* With Tailwind v4 — React SPA */
-@import "tailwindcss";
-@import "@neondatabase/neon-js/ui/tailwind";
-```
-
-### Provider Setup
-
-Wrap your app with `NeonAuthUIProvider`. Only `authClient` is required.
-
-In Next.js, add `suppressHydrationWarning` to the `` tag in your root layout — the provider injects theme attributes (`className="light"`, `color-scheme`) client-side that don't exist in the server render:
-
-```tsx
-// app/layout.tsx
-import { NeonAuthUIProvider, UserButton } from "@neondatabase/auth/react";
-import { authClient } from "@/lib/auth/client";
-
-export default function RootLayout({ children }) {
- return (
-
-
-
- {children}
-
-
-
- );
-}
-```
-
-**Social login** requires TWO configurations: enable in Neon Console AND add `social` prop to provider.
-
-### AuthView (Next.js)
-
-Create `app/auth/[path]/page.tsx`:
-
-```tsx
-import { AuthView } from "@neondatabase/auth/react";
-
-export const dynamicParams = false;
-
-export default async function AuthPage({
- params,
-}: {
- params: Promise<{ path: string }>;
-}) {
- const { path } = await params;
- return ;
-}
-```
-
-Auth paths: `sign-in`, `sign-up`, `forgot-password`, `reset-password`, `magic-link`, `two-factor`, `callback`, `sign-out`
-
-### AccountView (Next.js)
-
-Create `app/account/[path]/page.tsx`:
-
-```tsx
-import { AccountView } from "@neondatabase/auth/react";
-import { accountViewPaths } from "@neondatabase/auth/react/ui/server";
-
-export const dynamicParams = false;
-
-export function generateStaticParams() {
- return Object.values(accountViewPaths).map((path) => ({ path }));
-}
-
-export default async function AccountPage({
- params,
-}: {
- params: Promise<{ path: string }>;
-}) {
- const { path } = await params;
- return ;
-}
-```
-
-Account paths: `settings`, `security`
-
-### Conditional Rendering
-
-```tsx
-import { SignedIn, SignedOut, UserButton } from "@neondatabase/auth/react";
-
-
- Sign In
-
-
-
-
-```
-
-## Auth Methods Quick Reference
-
-| Method | Usage |
-| ----------------------------------------------- | ---------------------------------------------- |
-| `auth.signUp.email({ email, password, name })` | Create account (server) |
-| `auth.signIn.email({ email, password })` | Sign in (server) |
-| `auth.signIn.social({ provider, callbackURL })` | OAuth sign-in (server) |
-| `auth.signOut()` | Sign out (server) |
-| `auth.getSession()` | Get session (server, requires `force-dynamic`) |
-| `authClient.useSession()` | Session hook (client, needs React adapter) |
-| `authClient.getSession()` | Get session (client, no adapter needed) |
-| `authClient.signIn.email(...)` | Sign in (client) |
-| `authClient.signUp.email(...)` | Create account (client) |
-
-### Session Data
-
-```typescript
-const { data: session } = await auth.getSession();
-// session.user: { id, name, email, image, emailVerified, createdAt, updatedAt }
-// session.session: { id, expiresAt, token, createdAt, updatedAt, userId }
-```
-
-### Error Handling
-
-```typescript
-const { error } = await auth.signIn.email({ email, password });
-if (error) {
- // error.code: "INVALID_EMAIL_OR_PASSWORD", "EMAIL_NOT_VERIFIED",
- // "USER_NOT_FOUND", "TOO_MANY_REQUESTS"
- console.error(error.message);
-}
-```
-
-## Key Imports
-
-```typescript
-// Server (Next.js)
-import { createNeonAuth } from "@neondatabase/auth/next/server";
-
-// Client (Next.js) -- includes React adapter automatically
-import { createAuthClient } from "@neondatabase/auth/next";
-
-// Client (React SPA)
-import { createAuthClient } from "@neondatabase/neon-js/auth";
-
-// React adapter (only needed for useSession() in React SPA)
-import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react";
-
-// UI components
-import {
- NeonAuthUIProvider,
- AuthView,
- AccountView,
- SignedIn,
- SignedOut,
- UserButton,
-} from "@neondatabase/auth/react";
-import { accountViewPaths } from "@neondatabase/auth/react/ui/server";
-
-// CSS (choose one, never both; path matches your package)
-import "@neondatabase/auth/ui/css"; // Next.js
-import "@neondatabase/neon-js/ui/css"; // React SPA
-// or in CSS: @import "@neondatabase/auth/ui/tailwind"; (Next.js)
-// or in CSS: @import "@neondatabase/neon-js/ui/tailwind"; (React SPA)
-```
-
-## Common Mistakes
-
-### Missing NEON_AUTH_COOKIE_SECRET
-
-Required for Next.js, must be 32+ characters for HMAC-SHA256. Generate with `openssl rand -base64 32`.
-
-### Missing force-dynamic on server components
-
-```typescript
-// WRONG -- will error
-export default async function Page() {
- const { data: session } = await auth.getSession();
-}
-
-// CORRECT
-export const dynamic = "force-dynamic";
-export default async function Page() {
- const { data: session } = await auth.getSession();
-}
-```
-
-### Using v0.1 API patterns
-
-Use `createNeonAuth()` + `auth.handler()`, not the old standalone `authApiHandler()`. See the [migration guide](https://neon.com/docs/auth/migrate/from-auth-v0.1.md).
-
-### Using useSession() without adapter in React SPA
-
-`createAuthClient(url)` without an adapter returns a vanilla client with no React hooks. Either pass `BetterAuthReactAdapter()` or use UI components (`SignedIn`, etc.) which don't require an adapter.
-
-### Wrong BetterAuthReactAdapter import
-
-Must use subpath import and call as function:
-
-```typescript
-// WRONG
-import { BetterAuthReactAdapter } from "@neondatabase/neon-js";
-
-// CORRECT
-import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react";
-const client = createAuthClient(url, { adapter: BetterAuthReactAdapter() });
-```
-
-### CSS import conflicts
-
-Choose ONE: `ui/css` (without Tailwind) or `ui/tailwind` (with Tailwind v4). Never import both -- causes ~94KB of duplicate styles.
-
-### Missing "use client" directive
-
-Required for any component using `useSession()` or other React hooks.
-
-### Wrong createAuthClient signature
-
-URL is the first argument, not a property in an options object:
-
-```typescript
-// WRONG
-createAuthClient({ url: myUrl });
-
-// CORRECT (React SPA)
-createAuthClient(url);
-createAuthClient(url, { adapter: BetterAuthReactAdapter() });
-
-// CORRECT (Next.js) -- no arguments, uses proxy
-createAuthClient();
-```
-
-## Documentation
-
-| Topic | URL |
-| -------------------- | ----------------------------------------------------------------- |
-| Auth Overview | https://neon.com/docs/auth/overview.md |
-| Next.js Quickstart | https://neon.com/docs/auth/quick-start/nextjs.md |
-| Next.js API-only | https://neon.com/docs/auth/quick-start/nextjs-api-only.md |
-| React with UI | https://neon.com/docs/auth/quick-start/react-router-components.md |
-| React API Methods | https://neon.com/docs/auth/quick-start/react.md |
-| TanStack Router | https://neon.com/docs/auth/quick-start/tanstack-router.md |
-| Server SDK Reference | https://neon.com/docs/auth/reference/nextjs-server.md |
-| UI Components Ref | https://neon.com/docs/auth/reference/ui-components.md |
-| Client SDK Reference | https://neon.com/docs/reference/javascript-sdk.md |
-| v0.1 Migration Guide | https://neon.com/docs/auth/migrate/from-auth-v0.1.md |
-| OAuth Setup | https://neon.com/docs/auth/guides/setup-oauth.md |
-| Email Verification | https://neon.com/docs/auth/guides/email-verification.md |
-| Branching Auth | https://neon.com/docs/auth/branching-authentication.md |
diff --git a/.agents/skills/neon-postgres/references/neon-cli.md b/.agents/skills/neon-postgres/references/neon-cli.md
deleted file mode 100644
index 6ff7d462..00000000
--- a/.agents/skills/neon-postgres/references/neon-cli.md
+++ /dev/null
@@ -1,154 +0,0 @@
-# Neon CLI
-
-The Neon CLI is a command-line interface for managing Neon Serverless Postgres directly from your terminal. It provides the same capabilities as the Neon Platform API and is ideal for scripting, CI/CD pipelines, and developers who prefer terminal workflows.
-
-## Installation
-
-**macOS (Homebrew):**
-
-```bash
-brew install neonctl
-```
-
-**npm (cross-platform):**
-
-```bash
-npm install -g neonctl
-```
-
-## Authentication
-
-Authenticate with your Neon account:
-
-```bash
-neonctl auth
-```
-
-This opens a browser for OAuth authentication and stores credentials locally.
-
-For CI/CD or non-interactive environments, use an API key:
-
-```bash
-export NEON_API_KEY=your-api-key
-```
-
-Get your API key from: https://console.neon.tech/app/settings/api-keys
-
-## Common Commands
-
-### Project Management
-
-```bash
-# List all projects (org-scoped)
-neonctl projects list --org-id
-
-# Create a new project
-neonctl projects create --name my-project --org-id
-
-# Get project details
-neonctl projects get
-
-# Delete a project
-neonctl projects delete
-```
-
-### Branch Operations
-
-```bash
-# List branches
-neonctl branches list --project-id
-
-# Create a branch
-neonctl branches create --project-id --name dev
-
-# Delete a branch
-neonctl branches delete --project-id
-```
-
-### Connection Strings
-
-```bash
-# Get connection string
-neonctl connection-string --project-id
-
-# Get connection string for specific branch
-neonctl connection-string --project-id --branch-id
-
-# Get pooled connection string
-neonctl connection-string --project-id --pooled
-```
-
-### SQL Execution
-
-```bash
-# Run SQL query
-neonctl sql "SELECT * FROM users LIMIT 10" --project-id
-
-# Run SQL from file
-neonctl sql --file schema.sql --project-id
-```
-
-### Database Management
-
-```bash
-# List databases
-neonctl databases list --project-id --branch-id
-
-# Create database
-neonctl databases create --project-id --name mydb
-
-# List roles
-neonctl roles list --project-id --branch-id
-```
-
-## Output Formats
-
-The CLI supports multiple output formats:
-
-```bash
-# JSON output (default for scripting)
-neonctl projects list --output json
-
-# Table output (human-readable)
-neonctl projects list --output table
-
-# YAML output
-neonctl projects list --output yaml
-```
-
-## CI/CD Integration
-
-Example GitHub Actions workflow:
-
-```yaml
-- name: Create preview branch
- env:
- NEON_API_KEY: ${{ secrets.NEON_API_KEY }}
- run: |
- neonctl branches create \
- --project-id ${{ vars.NEON_PROJECT_ID }} \
- --name preview-${{ github.event.pull_request.number }}
-```
-
-## CLI vs MCP Server vs SDKs
-
-| Tool | Best For |
-| -------------- | ------------------------------------------------- |
-| Neon CLI | Terminal workflows, scripts, CI/CD pipelines |
-| MCP Server | AI-assisted development with Claude, Cursor, etc. |
-| TypeScript SDK | Programmatic access in Node.js/TypeScript apps |
-| Python SDK | Programmatic access in Python applications |
-| REST API | Direct HTTP integration in any language |
-
-## Documentation Resources
-
-| Topic | URL |
-| -------------- | -------------------------------------------------------- |
-| CLI Reference | https://neon.com/docs/reference/neon-cli.md |
-| CLI Install | https://neon.com/docs/reference/cli-install.md |
-| CLI Auth | https://neon.com/docs/reference/cli-auth.md |
-| CLI Projects | https://neon.com/docs/reference/cli-projects.md |
-| CLI Branches | https://neon.com/docs/reference/cli-branches.md |
-| CLI Connection | https://neon.com/docs/reference/cli-connection-string.md |
-
-See the [full CLI docs](https://neon.com/docs/reference/neon-cli.md) for the complete command reference.
diff --git a/.agents/skills/neon-postgres/references/neon-drizzle.md b/.agents/skills/neon-postgres/references/neon-drizzle.md
deleted file mode 100644
index e137b001..00000000
--- a/.agents/skills/neon-postgres/references/neon-drizzle.md
+++ /dev/null
@@ -1,241 +0,0 @@
-# Neon and Drizzle Integration
-
-Integration patterns, configurations, and optimizations for using **Drizzle ORM** with **Neon** Postgres.
-
-See the [official Drizzle guide](https://neon.com/docs/guides/drizzle.md) for complete details.
-
-## Choosing the Right Driver
-
-Drizzle ORM works with multiple Postgres drivers. See `connection-methods.md` for the full decision tree.
-
-| Platform | TCP Support | Pooling | Recommended Driver |
-| ----------------------- | ----------- | ------------------- | -------------------------- |
-| Vercel (Fluid) | Yes | `@vercel/functions` | `pg` (node-postgres) |
-| Cloudflare (Hyperdrive) | Yes | Hyperdrive | `pg` (node-postgres) |
-| Cloudflare Workers | No | No | `@neondatabase/serverless` |
-| Netlify Functions | No | No | `@neondatabase/serverless` |
-| Deno Deploy | No | No | `@neondatabase/serverless` |
-| Railway / Render | Yes | Built-in | `pg` (node-postgres) |
-
-## Connection Setup
-
-### 1. TCP with node-postgres (Long-Running Servers)
-
-Best for Railway, Render, traditional VPS.
-
-```bash
-npm install drizzle-orm pg
-npm install -D drizzle-kit @types/pg dotenv
-```
-
-```typescript
-// src/db.ts
-import { drizzle } from "drizzle-orm/node-postgres";
-import { Pool } from "pg";
-
-const pool = new Pool({ connectionString: process.env.DATABASE_URL });
-export const db = drizzle({ client: pool });
-```
-
-### 2. Vercel Fluid Compute with Connection Pooling
-
-```bash
-npm install drizzle-orm pg @vercel/functions
-npm install -D drizzle-kit @types/pg
-```
-
-```typescript
-// src/db.ts
-import { attachDatabasePool } from "@vercel/functions";
-import { drizzle } from "drizzle-orm/node-postgres";
-import { Pool } from "pg";
-import * as schema from "./schema";
-
-const pool = new Pool({ connectionString: process.env.DATABASE_URL });
-attachDatabasePool(pool);
-
-export const db = drizzle({ client: pool, schema });
-```
-
-### 3. HTTP Adapter (Edge Without TCP)
-
-For Cloudflare Workers, Netlify Edge, Deno Deploy. Does NOT support interactive transactions.
-
-```bash
-npm install drizzle-orm @neondatabase/serverless
-npm install -D drizzle-kit dotenv
-```
-
-```typescript
-// src/db.ts
-import { drizzle } from "drizzle-orm/neon-http";
-import { neon } from "@neondatabase/serverless";
-
-const sql = neon(process.env.DATABASE_URL!);
-export const db = drizzle(sql);
-```
-
-### 4. WebSocket Adapter (Edge with Transactions)
-
-```bash
-npm install drizzle-orm @neondatabase/serverless ws
-npm install -D drizzle-kit dotenv @types/ws
-```
-
-```typescript
-// src/db.ts
-import { drizzle } from "drizzle-orm/neon-serverless";
-import { Pool, neonConfig } from "@neondatabase/serverless";
-import ws from "ws";
-
-neonConfig.webSocketConstructor = ws; // Required for Node.js < v22
-
-const pool = new Pool({ connectionString: process.env.DATABASE_URL });
-export const db = drizzle(pool);
-```
-
-## Drizzle Config
-
-```typescript
-// drizzle.config.ts
-import { config } from "dotenv";
-import { defineConfig } from "drizzle-kit";
-
-config({ path: ".env.local" });
-
-export default defineConfig({
- schema: "./src/schema.ts",
- out: "./drizzle",
- dialect: "postgresql",
- dbCredentials: {
- url: process.env.DATABASE_URL!,
- },
-});
-```
-
-## Migrations
-
-```bash
-# Generate migrations
-npx drizzle-kit generate
-
-# Apply migrations
-npx drizzle-kit migrate
-```
-
-## Schema Definition
-
-```typescript
-// src/schema.ts
-import { pgTable, serial, text, integer, timestamp } from "drizzle-orm/pg-core";
-
-export const usersTable = pgTable("users", {
- id: serial("id").primaryKey(),
- name: text("name").notNull(),
- email: text("email").notNull().unique(),
- role: text("role").default("user").notNull(),
- createdAt: timestamp("created_at").defaultNow().notNull(),
-});
-
-export type User = typeof usersTable.$inferSelect;
-export type NewUser = typeof usersTable.$inferInsert;
-
-export const postsTable = pgTable("posts", {
- id: serial("id").primaryKey(),
- title: text("title").notNull(),
- content: text("content").notNull(),
- userId: integer("user_id")
- .notNull()
- .references(() => usersTable.id, { onDelete: "cascade" }),
- createdAt: timestamp("created_at").defaultNow().notNull(),
-});
-
-export type Post = typeof postsTable.$inferSelect;
-export type NewPost = typeof postsTable.$inferInsert;
-```
-
-## Query Patterns
-
-### Batch Inserts
-
-```typescript
-export async function batchInsertUsers(users: NewUser[]) {
- return db.insert(usersTable).values(users).returning();
-}
-```
-
-### Prepared Statements
-
-```typescript
-import { sql } from "drizzle-orm";
-
-export const getUsersByRolePrepared = db
- .select()
- .from(usersTable)
- .where(sql`${usersTable.role} = $1`)
- .prepare("get_users_by_role");
-
-// Usage: getUsersByRolePrepared.execute(['admin'])
-```
-
-### Transactions
-
-```typescript
-export async function createUserWithPosts(user: NewUser, posts: NewPost[]) {
- return await db.transaction(async (tx) => {
- const [newUser] = await tx.insert(usersTable).values(user).returning();
-
- if (posts.length > 0) {
- await tx.insert(postsTable).values(
- posts.map((post) => ({
- ...post,
- userId: newUser.id,
- })),
- );
- }
-
- return newUser;
- });
-}
-```
-
-## Working with Neon Branches
-
-```typescript
-import { drizzle } from "drizzle-orm/neon-http";
-import { neon } from "@neondatabase/serverless";
-
-const getBranchUrl = () => {
- const env = process.env.NODE_ENV;
- if (env === "development") return process.env.DEV_DATABASE_URL;
- if (env === "test") return process.env.TEST_DATABASE_URL;
- return process.env.DATABASE_URL;
-};
-
-const sql = neon(getBranchUrl()!);
-export const db = drizzle({ client: sql });
-```
-
-## Error Handling
-
-```typescript
-export async function safeNeonOperation(
- operation: () => Promise,
-): Promise {
- try {
- return await operation();
- } catch (error: any) {
- if (error.message?.includes("connection pool timeout")) {
- console.error("Neon connection pool timeout");
- }
- throw error;
- }
-}
-```
-
-## Best Practices
-
-1. **Connection Management** - See `connection-methods.md` for platform-specific guidance
-2. **Neon Features** - Utilize branching for development/testing
-3. **Query Optimization** - Batch operations, use prepared statements
-4. **Schema Design** - Leverage Postgres-specific features, use appropriate indexes
diff --git a/.agents/skills/neon-postgres/references/neon-js.md b/.agents/skills/neon-postgres/references/neon-js.md
deleted file mode 100644
index 6d4d83c4..00000000
--- a/.agents/skills/neon-postgres/references/neon-js.md
+++ /dev/null
@@ -1,451 +0,0 @@
-# Neon JS SDK
-
-The `@neondatabase/neon-js` SDK provides a unified client for Neon Auth and Data API. It combines authentication handling with PostgREST-compatible database queries.
-
-**Auth only?** Use `@neondatabase/auth` instead (see `neon-auth.md`) for smaller bundle size.
-
-See the [official JavaScript SDK docs](https://neon.com/docs/reference/javascript-sdk.md) for complete details.
-
-## Package Selection
-
-| Use Case | Package | Notes |
-| --------------- | ---------------------------- | ------------------- |
-| Auth + Data API | `@neondatabase/neon-js` | Full SDK |
-| Auth only | `@neondatabase/auth` | Smaller bundle |
-| Data API only | `@neondatabase/postgrest-js` | Bring your own auth |
-
-## Installation
-
-```bash
-npm install @neondatabase/neon-js@latest
-```
-
-> **Note:** While this package is in pre-release (beta), you must use `@latest` with npm. Without it, npm may install an older version. This is not needed with pnpm or yarn.
-
-## Quick Setup Patterns
-
-### Next.js
-
-**1. Server Auth Instance:**
-
-```typescript
-// lib/auth/server.ts
-import { createNeonAuth } from "@neondatabase/neon-js/auth/next/server";
-
-export const auth = createNeonAuth({
- baseUrl: process.env.NEON_AUTH_BASE_URL!,
- cookies: {
- secret: process.env.NEON_AUTH_COOKIE_SECRET!,
- },
-});
-```
-
-**2. API Route Handler:**
-
-```typescript
-// app/api/auth/[...path]/route.ts
-import { auth } from "@/lib/auth/server";
-export const { GET, POST } = auth.handler();
-```
-
-**3. Auth Client:**
-
-```typescript
-// lib/auth/client.ts
-"use client";
-import { createAuthClient } from "@neondatabase/neon-js/auth/next";
-export const authClient = createAuthClient();
-```
-
-**4. Database Client:**
-
-```typescript
-// lib/db/client.ts
-import { createClient } from "@neondatabase/neon-js";
-import type { Database } from "./database.types";
-
-export const dbClient = createClient({
- auth: { url: process.env.NEON_AUTH_BASE_URL! },
- dataApi: { url: process.env.NEON_DATA_API_URL! },
-});
-```
-
-**5. Middleware + UI setup** — See [Neon Auth reference](neon-auth.md) for middleware configuration, `NeonAuthUIProvider`, CSS imports, and `AuthView`/`AccountView` page components.
-
-### React SPA
-
-```typescript
-import { createAuthClient } from "@neondatabase/neon-js/auth";
-
-const authClient = createAuthClient(import.meta.env.VITE_NEON_AUTH_URL);
-```
-
-> **Note:** If you need React hooks like `useSession()` in custom components, pass an adapter:
-> `createAuthClient(url, { adapter: BetterAuthReactAdapter() })`.
-> UI components (`AuthView`, `SignedIn`, etc.) do not require an adapter.
-
-### React SPA with Data API
-
-```typescript
-import { createClient } from "@neondatabase/neon-js";
-
-const client = createClient({
- auth: { url: import.meta.env.VITE_NEON_AUTH_URL },
- dataApi: { url: import.meta.env.VITE_NEON_DATA_API_URL },
-});
-
-export const authClient = client.auth;
-```
-
-## Environment Variables
-
-```bash
-# Next.js (.env)
-NEON_AUTH_BASE_URL=https://ep-xxx.neonauth.us-east-1.aws.neon.tech/neondb/auth
-NEON_AUTH_COOKIE_SECRET=your-secret-at-least-32-characters-long
-NEON_DATA_API_URL=https://ep-xxx.apirest.us-east-1.aws.neon.tech/neondb/rest/v1
-
-# Vite/React (.env)
-VITE_NEON_AUTH_URL=https://ep-xxx.neonauth.us-east-1.aws.neon.tech/neondb/auth
-VITE_NEON_DATA_API_URL=https://ep-xxx.apirest.us-east-1.aws.neon.tech/neondb/rest/v1
-```
-
-Get your Auth URL from the Neon Console: Project -> Branch -> Auth -> Configuration.
-
-Generate a cookie secret: `openssl rand -base64 32`
-
-## Database Queries (PostgREST / Data API)
-
-> **Prerequisite:** The Data API must be enabled per branch before making queries. Enable it via the Neon Console (Project → Data API), the MCP server's `provision_neon_data_api` tool, or the [REST API](https://api-docs.neon.tech/reference/createprojectbranchdataapi) (`POST /projects/{project_id}/branches/{branch_id}/data-api/{database_name}`). Without it, requests will return 404.
-
-All query methods follow PostgREST syntax (same as Supabase).
-
-```typescript
-// Select with filters
-const { data } = await client
- .from("items")
- .select("id, name, status")
- .eq("status", "active")
- .order("created_at", { ascending: false })
- .limit(10);
-
-// Select single row
-const { data, error } = await client
- .from("items")
- .select("*")
- .eq("id", 1)
- .single();
-
-// Insert (returns inserted row)
-const { data, error } = await client
- .from("items")
- .insert({ name: "New Item", status: "pending" })
- .select()
- .single();
-
-// Insert multiple
-const { data } = await client
- .from("items")
- .insert([{ name: "A" }, { name: "B" }])
- .select();
-
-// Update
-await client.from("items").update({ status: "completed" }).eq("id", 1);
-
-// Update and return updated row
-const { data } = await client
- .from("items")
- .update({ status: "completed" })
- .eq("id", 1)
- .select()
- .single();
-
-// Delete
-await client.from("items").delete().eq("id", 1);
-
-// Delete and return deleted row
-const { data } = await client
- .from("items")
- .delete()
- .eq("id", 1)
- .select()
- .single();
-
-// Upsert
-await client.from("items").upsert({ id: 1, name: "Updated", status: "active" });
-```
-
-### Filter Operators
-
-| Operator | Example |
-| ------------- | -------------------------------------------- |
-| `.eq()` | `.eq("status", "active")` |
-| `.neq()` | `.neq("status", "archived")` |
-| `.gt()` | `.gt("price", 100)` |
-| `.gte()` | `.gte("price", 100)` |
-| `.lt()` | `.lt("price", 100)` |
-| `.lte()` | `.lte("price", 100)` |
-| `.like()` | `.like("name", "%item%")` |
-| `.ilike()` | `.ilike("name", "%item%")` |
-| `.is()` | `.is("deleted_at", null)` |
-| `.in()` | `.in("status", ["active", "pending"])` |
-| `.contains()` | `.contains("tags", ["important"])` |
-| `.or()` | `.or("status.eq.active,price.gt.100")` |
-| `.not()` | `.not("status", "eq", "archived")` |
-| `.order()` | `.order("created_at", { ascending: false })` |
-| `.limit()` | `.limit(10)` |
-| `.range()` | `.range(0, 9)` (first 10 items) |
-
-**Pagination formula**: `.range((page - 1) * pageSize, page * pageSize - 1)`
-
-### Relationships
-
-```typescript
-// One-to-many
-const { data } = await client
- .from("posts")
- .select("id, title, author:users(name, email)");
-
-// Many-to-many
-const { data } = await client
- .from("posts")
- .select("id, title, tags:post_tags(tag:tags(name))");
-
-// Nested
-const { data } = await client.from("posts").select(`
- id, title,
- author:users(id, name, profile:profiles(bio, avatar))
- `);
-```
-
-### Error Handling
-
-```typescript
-const { data, error } = await client.from("items").select();
-if (error) {
- console.error(error.message, error.code, error.details);
- return;
-}
-```
-
-Common error codes: `PGRST116` (no rows with `.single()`), `23505` (unique violation), `23503` (FK violation), `42P01` (table not found).
-
-### Next.js Usage Examples
-
-**Server Component:**
-
-```typescript
-// app/posts/page.tsx
-import { dbClient } from "@/lib/db/client";
-
-export default async function PostsPage() {
- const { data: posts, error } = await dbClient
- .from("posts")
- .select("id, title, created_at, author:users(name)")
- .order("created_at", { ascending: false })
- .limit(10);
-
- if (error) return
Error loading posts
;
-
- return (
-
- {posts?.map((post) => (
-
-
{post.title}
-
By {post.author?.name}
-
- ))}
-
- );
-}
-```
-
-**API Route:**
-
-```typescript
-// app/api/posts/route.ts
-import { dbClient } from "@/lib/db/client";
-import { NextResponse } from "next/server";
-
-export async function GET() {
- const { data, error } = await dbClient.from("posts").select();
- if (error)
- return NextResponse.json({ error: error.message }, { status: 500 });
- return NextResponse.json(data);
-}
-
-export async function POST(request: Request) {
- const body = await request.json();
- const { data, error } = await dbClient
- .from("posts")
- .insert(body)
- .select()
- .single();
- if (error)
- return NextResponse.json({ error: error.message }, { status: 400 });
- return NextResponse.json(data, { status: 201 });
-}
-```
-
-## Auth Methods
-
-### BetterAuth API (Default)
-
-```typescript
-await client.auth.signIn.email({ email, password });
-await client.auth.signUp.email({ email, password, name });
-await client.auth.signOut();
-const { data: session } = await client.auth.getSession();
-await client.auth.signIn.social({
- provider: "google",
- callbackURL: "/dashboard",
-});
-```
-
-### Supabase-Compatible API
-
-```typescript
-import { createClient, SupabaseAuthAdapter } from "@neondatabase/neon-js";
-
-const client = createClient({
- auth: { adapter: SupabaseAuthAdapter(), url },
- dataApi: { url },
-});
-
-await client.auth.signInWithPassword({ email, password });
-await client.auth.signUp({ email, password });
-const {
- data: { session },
-} = await client.auth.getSession();
-```
-
-## Key Imports
-
-```typescript
-// Main client
-import {
- createClient,
- SupabaseAuthAdapter,
- BetterAuthVanillaAdapter,
-} from "@neondatabase/neon-js";
-
-// Server auth (Next.js) -- unified instance
-import { createNeonAuth } from "@neondatabase/neon-js/auth/next/server";
-
-// Client auth (Next.js) -- auto-includes React adapter
-import { createAuthClient } from "@neondatabase/neon-js/auth/next";
-
-// Client auth (React SPA / vanilla)
-import { createAuthClient } from "@neondatabase/neon-js/auth";
-
-// React adapter (only needed for useSession() in custom components)
-import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react";
-
-// UI components (use /auth/react -- superset of /auth/react/ui and /auth/react/adapters)
-import {
- NeonAuthUIProvider,
- AuthView,
- AccountView,
- SignedIn,
- SignedOut,
- UserButton,
-} from "@neondatabase/neon-js/auth/react";
-import { accountViewPaths } from "@neondatabase/neon-js/auth/react/ui/server";
-
-// CSS (choose one, never both)
-import "@neondatabase/neon-js/ui/css"; // Without Tailwind
-// @import '@neondatabase/neon-js/ui/tailwind'; // With Tailwind v4 (in CSS file)
-```
-
-## Generate Types
-
-```bash
-npx neon-js gen-types --db-url "$DATABASE_URL" --output src/types/database.ts
-```
-
-Use types in the client for autocomplete and compile-time checking:
-
-```typescript
-import type { Database } from "./database.types";
-const client = createClient({ ... });
-```
-
-## Supabase Migration
-
-The Neon JS SDK uses the same PostgREST API as Supabase. Query syntax is identical:
-
-```typescript
-// Before (Supabase)
-import { createClient } from "@supabase/supabase-js";
-const client = createClient(SUPABASE_URL, SUPABASE_KEY);
-
-// After (Neon)
-import { createClient, SupabaseAuthAdapter } from "@neondatabase/neon-js";
-const client = createClient({
- auth: { adapter: SupabaseAuthAdapter(), url: NEON_AUTH_URL },
- dataApi: { url: NEON_DATA_API_URL },
-});
-
-// Queries work the same
-const { data } = await client.from("items").select();
-```
-
-## Common Mistakes
-
-### Using old v0.1 server APIs
-
-Use `createNeonAuth()` + `auth.handler()`, not standalone `authApiHandler()`. See `neon-auth.md` for the v0.2 pattern.
-
-### Missing NEON_AUTH_COOKIE_SECRET
-
-Required for Next.js, must be 32+ characters. Generate with `openssl rand -base64 32`.
-
-### Missing force-dynamic on server components
-
-Server components using `auth.getSession()` need `export const dynamic = 'force-dynamic'`.
-
-### Wrong adapter import path
-
-`BetterAuthReactAdapter` must be imported from a subpath and called as a function:
-
-```typescript
-// WRONG
-import { BetterAuthReactAdapter } from "@neondatabase/neon-js";
-
-// CORRECT
-import { BetterAuthReactAdapter } from "@neondatabase/neon-js/auth/react";
-auth: {
- adapter: BetterAuthReactAdapter();
-} // Don't forget ()
-```
-
-### CSS import conflicts
-
-Choose ONE method. Never import both -- causes duplicate styles:
-
-```css
-/* With Tailwind v4 */
-@import "tailwindcss";
-@import "@neondatabase/neon-js/ui/tailwind";
-```
-
-```typescript
-/* Without Tailwind */
-import "@neondatabase/neon-js/ui/css";
-```
-
-### Missing "use client" directive
-
-Required for any component using `useSession()` or other React hooks:
-
-```typescript
-"use client"; // Required!
-import { authClient } from "@/lib/auth/client";
-```
-
-### Wrong API for adapter type
-
-| Adapter | Sign In | Sign Up |
-| ---------------------- | ----------------------------------------- | ----------------------------------- |
-| BetterAuthReactAdapter | `signIn.email({ email, password })` | `signUp.email({ email, password })` |
-| SupabaseAuthAdapter | `signInWithPassword({ email, password })` | `signUp({ email, password })` |
diff --git a/.agents/skills/neon-postgres/references/neon-python-sdk.md b/.agents/skills/neon-postgres/references/neon-python-sdk.md
deleted file mode 100644
index 76a2c613..00000000
--- a/.agents/skills/neon-postgres/references/neon-python-sdk.md
+++ /dev/null
@@ -1,101 +0,0 @@
-# Neon Python SDK
-
-The `neon-api` Python SDK is a Pythonic wrapper around the Neon REST API for managing Neon resources programmatically.
-
-For core concepts (Organization, Project, Branch, Endpoint, etc.), see `what-is-neon.md`.
-
-See the [official Python SDK docs](https://neon.com/docs/reference/python-sdk.md) for complete details.
-
-## Installation
-
-```bash
-pip install neon-api
-```
-
-## Authentication
-
-```python
-import os
-from neon_api import NeonAPI
-
-neon = NeonAPI(api_key=os.environ["NEON_API_KEY"])
-```
-
-## Org-Aware Workflow
-
-All Neon accounts are organization-based. Discover the user's org first, then pass `org_id` to project operations:
-
-```python
-# 1. Get the user's organizations
-orgs = neon.current_user_organizations()
-org_id = orgs[0].id
-
-# 2. List projects within the org
-projects = neon.projects(org_id=org_id)
-```
-
-## Method Quick Reference
-
-### Projects
-
-| Operation | Method |
-| ------------------ | ------------------------------------------------------------------------------- |
-| List projects | `neon.projects(org_id=...)` |
-| Create project | `neon.project_create(project={ 'name': ..., 'pg_version': 17, 'org_id': ... })` |
-| Get project | `neon.project(project_id=...)` |
-| Update project | `neon.project_update(project_id=..., project={...})` |
-| Delete project | `neon.project_delete(project_id=...)` |
-| Get connection URI | `neon.connection_uri(project_id=..., database_name=..., role_name=...)` |
-
-### Branches
-
-| Operation | Method |
-| ------------- | ------------------------------------------------------------------- |
-| Create branch | `neon.branch_create(project_id=..., branch={...}, endpoints=[...])` |
-| List branches | `neon.branches(project_id=...)` |
-| Get branch | `neon.branch(project_id=..., branch_id=...)` |
-| Update branch | `neon.branch_update(project_id=..., branch_id=..., branch={...})` |
-| Delete branch | `neon.branch_delete(project_id=..., branch_id=...)` |
-
-### Databases
-
-| Operation | Method |
-| --------------- | ---------------------------------------------------------------------- |
-| Create database | `neon.database_create(project_id=..., branch_id=..., database={...})` |
-| List databases | `neon.databases(project_id=..., branch_id=...)` |
-| Delete database | `neon.database_delete(project_id=..., branch_id=..., database_id=...)` |
-
-### Roles
-
-| Operation | Method |
-| ----------- | ---------------------------------------------------------------- |
-| Create role | `neon.role_create(project_id=..., branch_id=..., role_name=...)` |
-| List roles | `neon.roles(project_id=..., branch_id=...)` |
-| Delete role | `neon.role_delete(project_id=..., branch_id=..., role_name=...)` |
-
-### Endpoints
-
-| Operation | Method |
-| ---------------- | ----------------------------------------------------------------------- |
-| Create endpoint | `neon.endpoint_create(project_id=..., endpoint={...})` |
-| Start endpoint | `neon.endpoint_start(project_id=..., endpoint_id=...)` |
-| Suspend endpoint | `neon.endpoint_suspend(project_id=..., endpoint_id=...)` |
-| Update endpoint | `neon.endpoint_update(project_id=..., endpoint_id=..., endpoint={...})` |
-| Delete endpoint | `neon.endpoint_delete(project_id=..., endpoint_id=...)` |
-
-### Organizations
-
-| Operation | Method |
-| -------------- | ----------------------------------- |
-| List user orgs | `neon.current_user_organizations()` |
-| Get org | `neon.organization(org_id=...)` |
-
-### API Keys & Operations
-
-| Operation | Method |
-| --------------- | -------------------------------------------------- |
-| List API keys | `neon.api_keys()` |
-| Create API key | `neon.api_key_create(key_name=...)` |
-| Revoke API key | `neon.api_key_revoke(key_id)` |
-| List operations | `neon.operations(project_id=...)` |
-| Get operation | `neon.operation(project_id=..., operation_id=...)` |
diff --git a/.agents/skills/neon-postgres/references/neon-rest-api.md b/.agents/skills/neon-postgres/references/neon-rest-api.md
deleted file mode 100644
index f6875293..00000000
--- a/.agents/skills/neon-postgres/references/neon-rest-api.md
+++ /dev/null
@@ -1,77 +0,0 @@
-# Neon REST API
-
-Essentials for making direct HTTP requests to the Neon Platform API.
-
-See the [official API reference](https://neon.com/docs/reference/api-reference.md) for complete details.
-
-## OpenAPI Specification
-
-The full [OpenAPI spec](https://neon.com/api_spec/release/v2.json) is available for programmatic lookup of exact endpoints, request/response schemas, and required fields.
-
-## Base URL
-
-```
-https://console.neon.tech/api/v2/
-```
-
-## Authentication
-
-Include a Neon API key in every request:
-
-```
-Authorization: Bearer $NEON_API_KEY
-```
-
-### API Key Types
-
-| Type | Scope | Best For |
-| -------------- | ------------------------------- | ----------------------------- |
-| Personal | All projects user has access to | Individual use, scripting |
-| Organization | Entire organization | CI/CD, org-wide automation |
-| Project-scoped | Single project only | Project-specific integrations |
-
-## Rate Limits
-
-- 700 requests/minute (~11/second)
-- Bursts up to 40 requests/second per route
-- Handle `429 Too Many Requests` with retry + backoff
-
-## Common Endpoints
-
-| Operation | Method | Path |
-| ------------------ | -------- | -------------------------------------------------------- |
-| List projects | `GET` | `/projects?org_id={org_id}` |
-| List user orgs | `GET` | `/users/me/organizations` |
-| Create project | `POST` | `/projects` (include `org_id` in body) |
-| Get connection URI | `GET` | `/projects/{project_id}/connection_uri` |
-| Create branch | `POST` | `/projects/{project_id}/branches` |
-| List branches | `GET` | `/projects/{project_id}/branches` |
-| Delete branch | `DELETE` | `/projects/{project_id}/branches/{branch_id}` |
-| Start endpoint | `POST` | `/projects/{project_id}/endpoints/{endpoint_id}/start` |
-| Suspend endpoint | `POST` | `/projects/{project_id}/endpoints/{endpoint_id}/suspend` |
-| List databases | `GET` | `/projects/{project_id}/branches/{branch_id}/databases` |
-| Create database | `POST` | `/projects/{project_id}/branches/{branch_id}/databases` |
-| List roles | `GET` | `/projects/{project_id}/branches/{branch_id}/roles` |
-| List API keys | `GET` | `/api_keys` |
-| List operations | `GET` | `/projects/{project_id}/operations` |
-
-## Important Constraints
-
-- You **cannot delete** a project's root or default branch
-- You **cannot delete** a branch that has child branches — delete all children first
-- Creating a new role may **drop existing connections** to the active compute endpoint
-- A branch can have only one `read_write` endpoint but multiple `read_only` endpoints
-- Operations are async — poll operation status before starting dependent operations
-- Operations older than 6 months may be deleted from Neon's systems
-- The first API key must be created from the [Neon Console](https://console.neon.tech/app/settings/api-keys); subsequent keys can be created via the API
-
-## Error Codes
-
-| Status | Meaning | Action |
-| ------ | ------------ | ------------------------ |
-| 401 | Unauthorized | Check API key |
-| 404 | Not Found | Verify resource ID |
-| 429 | Rate Limited | Retry with backoff |
-| 500 | Server Error | Retry or contact support |
-
-For TypeScript SDK usage, see `neon-typescript-sdk.md`. For Python SDK, see `neon-python-sdk.md`.
diff --git a/.agents/skills/neon-postgres/references/neon-serverless.md b/.agents/skills/neon-postgres/references/neon-serverless.md
deleted file mode 100644
index e9897d76..00000000
--- a/.agents/skills/neon-postgres/references/neon-serverless.md
+++ /dev/null
@@ -1,250 +0,0 @@
-# Neon Serverless Driver
-
-Patterns and best practices for connecting to Neon databases in serverless environments using the `@neondatabase/serverless` driver. The driver connects over **HTTP** for fast, single queries or **WebSockets** for `node-postgres` compatibility and interactive transactions.
-
-See the [official serverless driver docs](https://neon.com/docs/serverless/serverless-driver.md) for complete details.
-
-## Installation
-
-```bash
-# Using npm
-npm install @neondatabase/serverless
-
-# Using JSR
-bunx jsr add @neon/serverless
-```
-
-**Note:** Version 1.0.0+ requires **Node.js v19 or later**.
-
-For projects that depend on `pg` but want to use Neon's WebSocket-based connection pool:
-
-```json
-"dependencies": {
- "pg": "npm:@neondatabase/serverless@^0.10.4"
-},
-"overrides": {
- "pg": "npm:@neondatabase/serverless@^0.10.4"
-}
-```
-
-## Connection String
-
-Always use environment variables:
-
-```typescript
-// For HTTP queries
-import { neon } from "@neondatabase/serverless";
-const sql = neon(process.env.DATABASE_URL!);
-
-// For WebSocket connections
-import { Pool } from "@neondatabase/serverless";
-const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
-```
-
-**Never hardcode credentials:**
-
-```typescript
-// AVOID
-const sql = neon("postgres://username:password@host.neon.tech/neondb");
-```
-
-## HTTP Queries with `neon` function
-
-Ideal for simple, "one-shot" queries in serverless/edge environments. Uses HTTP `fetch` - fastest method for single queries.
-
-### Parameterized Queries
-
-Use tagged template literals for safe parameter interpolation:
-
-```typescript
-const [post] = await sql`SELECT * FROM posts WHERE id = ${postId}`;
-```
-
-For manually constructed queries:
-
-```typescript
-const [post] = await sql.query("SELECT * FROM posts WHERE id = $1", [postId]);
-```
-
-**Never concatenate user input:**
-
-```typescript
-// AVOID: SQL Injection Risk
-const [post] = await sql("SELECT * FROM posts WHERE id = " + postId);
-```
-
-### Configuration Options
-
-```typescript
-// Return rows as arrays instead of objects
-const sqlArrayMode = neon(process.env.DATABASE_URL!, { arrayMode: true });
-const rows = await sqlArrayMode`SELECT id, title FROM posts`;
-// rows -> [[1, "First Post"], [2, "Second Post"]]
-
-// Get full results including row count and field metadata
-const sqlFull = neon(process.env.DATABASE_URL!, { fullResults: true });
-const result = await sqlFull`SELECT * FROM posts LIMIT 1`;
-// result -> { rows: [...], fields: [...], rowCount: 1, ... }
-```
-
-## WebSocket Connections with `Pool` and `Client`
-
-Use for `node-postgres` compatibility, interactive transactions, or session support.
-
-### WebSocket Configuration
-
-For Node.js v21 and earlier:
-
-```typescript
-import { Pool, neonConfig } from "@neondatabase/serverless";
-import ws from "ws";
-
-// Required for Node.js < v22
-neonConfig.webSocketConstructor = ws;
-
-const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
-```
-
-### Serverless Lifecycle Management
-
-Create, use, and close the pool within the same invocation:
-
-```typescript
-// Vercel Edge Functions example
-export default async (req: Request, ctx: ExecutionContext) => {
- const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
-
- try {
- const { rows } = await pool.query("SELECT * FROM users");
- return new Response(JSON.stringify(rows));
- } catch (err) {
- console.error(err);
- return new Response("Database error", { status: 500 });
- } finally {
- ctx.waitUntil(pool.end());
- }
-};
-```
-
-**Avoid** creating a global `Pool` instance outside the handler.
-
-## Transactions
-
-### HTTP Transactions
-
-For running multiple queries in a single, non-interactive transaction:
-
-```typescript
-const [newUser, newProfile] = await sql.transaction(
- [
- sql`INSERT INTO users(name) VALUES(${name}) RETURNING id`,
- sql`INSERT INTO profiles(user_id, bio) VALUES(${userId}, ${bio})`,
- ],
- {
- isolationLevel: "ReadCommitted",
- readOnly: false,
- },
-);
-```
-
-### Interactive Transactions
-
-For complex transactions with conditional logic:
-
-```typescript
-const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
-const client = await pool.connect();
-try {
- await client.query("BEGIN");
- const {
- rows: [{ id }],
- } = await client.query("INSERT INTO users(name) VALUES($1) RETURNING id", [
- name,
- ]);
- await client.query("INSERT INTO profiles(user_id, bio) VALUES($1, $2)", [
- id,
- bio,
- ]);
- await client.query("COMMIT");
-} catch (err) {
- await client.query("ROLLBACK");
- throw err;
-} finally {
- client.release();
- await pool.end();
-}
-```
-
-## Environment-Specific Optimizations
-
-```javascript
-// For Vercel Edge Functions, specify nearest region
-export const config = {
- runtime: "edge",
- regions: ["iad1"], // Region nearest to your Neon DB
-};
-
-// For Cloudflare Workers, consider using Hyperdrive
-// https://neon.com/blog/hyperdrive-neon-faq
-```
-
-## ORM Integration
-
-For Drizzle ORM integration with the serverless driver, see `neon-drizzle.md`.
-
-### Prisma
-
-```typescript
-import { neonConfig } from "@neondatabase/serverless";
-import { PrismaNeon, PrismaNeonHTTP } from "@prisma/adapter-neon";
-import { PrismaClient } from "@prisma/client";
-import ws from "ws";
-
-const connectionString = process.env.DATABASE_URL;
-neonConfig.webSocketConstructor = ws;
-
-// HTTP adapter
-const adapterHttp = new PrismaNeonHTTP(connectionString!, {});
-export const prismaClientHttp = new PrismaClient({ adapter: adapterHttp });
-
-// WebSocket adapter
-const adapterWs = new PrismaNeon({ connectionString });
-export const prismaClientWs = new PrismaClient({ adapter: adapterWs });
-```
-
-### Kysely
-
-```typescript
-import { Pool } from "@neondatabase/serverless";
-import { Kysely, PostgresDialect } from "kysely";
-
-const dialect = new PostgresDialect({
- pool: new Pool({ connectionString: process.env.DATABASE_URL }),
-});
-
-const db = new Kysely({ dialect });
-```
-
-**NOTE:** Do not pass the `neon()` function to ORMs that expect a `node-postgres` compatible `Pool`.
-
-## Error Handling
-
-```javascript
-// Pool error handling
-const pool = new Pool({ connectionString: process.env.DATABASE_URL });
-pool.on("error", (err) => {
- console.error("Unexpected error on idle client", err);
- process.exit(-1);
-});
-
-// Query error handling
-try {
- const [post] = await sql`SELECT * FROM posts WHERE id = ${postId}`;
- if (!post) {
- return new Response("Not found", { status: 404 });
- }
-} catch (err) {
- console.error("Database query failed:", err);
- return new Response("Server error", { status: 500 });
-}
-```
diff --git a/.agents/skills/neon-postgres/references/neon-typescript-sdk.md b/.agents/skills/neon-postgres/references/neon-typescript-sdk.md
deleted file mode 100644
index 8dbdb8ec..00000000
--- a/.agents/skills/neon-postgres/references/neon-typescript-sdk.md
+++ /dev/null
@@ -1,135 +0,0 @@
-# Neon TypeScript SDK
-
-The `@neondatabase/api-client` TypeScript SDK is a typed wrapper around the Neon REST API for managing Neon resources programmatically.
-
-For core concepts (Organization, Project, Branch, Endpoint, etc.), see `what-is-neon.md`.
-
-See the [official TypeScript SDK docs](https://neon.com/docs/reference/typescript-sdk.md) for complete details.
-
-## Installation
-
-```bash
-npm install @neondatabase/api-client
-```
-
-## Authentication
-
-```typescript
-import { createApiClient } from "@neondatabase/api-client";
-
-const apiClient = createApiClient({ apiKey: process.env.NEON_API_KEY! });
-```
-
-## Org-Aware Workflow
-
-All Neon accounts are organization-based. You must discover the user's org first, then pass `org_id` to project operations:
-
-```typescript
-// 1. Get the user's organizations
-const { data: orgs } = await apiClient.getCurrentUserOrganizations();
-const orgId = orgs.organizations[0].id;
-
-// 2. List projects within the org
-const { data: projects } = await apiClient.listProjects({ org_id: orgId });
-```
-
-## Method Quick Reference
-
-### Projects
-
-| Operation | Method |
-| ------------------ | ------------------------------------------------------------------------------- |
-| List projects | `apiClient.listProjects({ org_id })` |
-| Create project | `apiClient.createProject({ project: { name, pg_version, region_id, org_id } })` |
-| Get project | `apiClient.getProject(projectId)` |
-| Update project | `apiClient.updateProject(projectId, { project: { name } })` |
-| Delete project | `apiClient.deleteProject(projectId)` |
-| Get connection URI | `apiClient.getConnectionUri({ projectId, database_name, role_name, pooled })` |
-
-### Branches
-
-| Operation | Method |
-| ------------- | --------------------------------------------------------------------------------------- |
-| Create branch | `apiClient.createProjectBranch(projectId, { branch: { name }, endpoints: [{ type }] })` |
-| List branches | `apiClient.listProjectBranches({ projectId })` |
-| Get branch | `apiClient.getProjectBranch(projectId, branchId)` |
-| Update branch | `apiClient.updateProjectBranch(projectId, branchId, { branch: { name } })` |
-| Delete branch | `apiClient.deleteProjectBranch(projectId, branchId)` |
-
-### Databases
-
-| Operation | Method |
-| --------------- | ------------------------------------------------------------------------------------------------ |
-| Create database | `apiClient.createProjectBranchDatabase(projectId, branchId, { database: { name, owner_name } })` |
-| List databases | `apiClient.listProjectBranchDatabases(projectId, branchId)` |
-| Delete database | `apiClient.deleteProjectBranchDatabase(projectId, branchId, databaseName)` |
-
-### Roles
-
-| Operation | Method |
-| ----------- | ---------------------------------------------------------------------------- |
-| Create role | `apiClient.createProjectBranchRole(projectId, branchId, { role: { name } })` |
-| List roles | `apiClient.listProjectBranchRoles(projectId, branchId)` |
-| Delete role | `apiClient.deleteProjectBranchRole(projectId, branchId, roleName)` |
-
-### Endpoints
-
-| Operation | Method |
-| ---------------- | ------------------------------------------------------------------------------- |
-| Create endpoint | `apiClient.createProjectEndpoint(projectId, { endpoint: { branch_id, type } })` |
-| List endpoints | `apiClient.listProjectEndpoints(projectId)` |
-| Start endpoint | `apiClient.startProjectEndpoint(projectId, endpointId)` |
-| Suspend endpoint | `apiClient.suspendProjectEndpoint(projectId, endpointId)` |
-| Restart endpoint | `apiClient.restartProjectEndpoint(projectId, endpointId)` |
-| Update endpoint | `apiClient.updateProjectEndpoint(projectId, endpointId, { endpoint: {...} })` |
-| Delete endpoint | `apiClient.deleteProjectEndpoint(projectId, endpointId)` |
-
-### API Keys
-
-| Operation | Method |
-| ---------- | -------------------------------------- |
-| List keys | `apiClient.listApiKeys()` |
-| Create key | `apiClient.createApiKey({ key_name })` |
-| Revoke key | `apiClient.revokeApiKey(keyId)` |
-
-### Operations
-
-| Operation | Method |
-| --------------- | ------------------------------------------------------- |
-| List operations | `apiClient.listProjectOperations({ projectId })` |
-| Get operation | `apiClient.getProjectOperation(projectId, operationId)` |
-
-### Organizations
-
-| Operation | Method |
-| -------------- | ------------------------------------------------------------------------ |
-| List user orgs | `apiClient.getCurrentUserOrganizations()` |
-| Get org | `apiClient.getOrganization(orgId)` |
-| List members | `apiClient.getOrganizationMembers(orgId)` |
-| Create org key | `apiClient.createOrgApiKey(orgId, { key_name, project_id? })` |
-| Invite member | `apiClient.createOrganizationInvitations(orgId, { invitations: [...] })` |
-
-## Error Handling
-
-```typescript
-try {
- const response = await apiClient.getProject(projectId);
- return response.data;
-} catch (error: any) {
- if (error.isAxiosError) {
- const status = error.response?.status;
- // 401 = bad API key, 404 = not found, 429 = rate limited
- console.error("API error:", status, error.response?.data?.message);
- }
- return null;
-}
-```
-
-## Key Types
-
-```typescript
-import { EndpointType, MemberRole } from "@neondatabase/api-client";
-
-// EndpointType.ReadWrite, EndpointType.ReadOnly
-// MemberRole.Admin, MemberRole.Member
-```
diff --git a/.agents/skills/neon-postgres/references/what-is-neon.md b/.agents/skills/neon-postgres/references/what-is-neon.md
deleted file mode 100644
index b2d8ba7c..00000000
--- a/.agents/skills/neon-postgres/references/what-is-neon.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# What is Neon
-
-Neon is a serverless Postgres platform that separates compute and storage to offer autoscaling, branching, instant restore, and scale-to-zero.
-
-See the [official introduction](https://neon.com/docs/introduction.md) for complete details.
-
-## Core Concepts
-
-| Concept | Description | Key Relationship |
-| ---------------- | --------------------------------------------------------------------- | ------------------------- |
-| Organization | Highest-level container for billing, users, and projects | Contains Projects |
-| Project | Primary container for all database resources for an application | Contains Branches |
-| Branch | Lightweight, copy-on-write clone of database state | Contains Databases, Roles |
-| Compute Endpoint | Running PostgreSQL instance (CPU/RAM for queries) | Attached to a Branch |
-| Database | Logical container for data (tables, schemas, views) | Exists within a Branch |
-| Role | PostgreSQL role for authentication and authorization | Belongs to a Branch |
-| Operation | Async action by the control plane (creating branch, starting compute) | Associated with Project |
-
-## Key Differentiators
-
-1. **Serverless Architecture**: Compute scales automatically and can suspend when idle
-2. **Branching**: Create instant database copies without duplicating storage
-3. **Separation of Compute and Storage**: Pay for compute only when active
-4. **Postgres Compatible**: Works with any Postgres driver, ORM, or tool
-
-## When to Use Neon
-
-- **Serverless applications**: Functions that need database access without managing connections
-- **Development workflows**: Branch databases like code for isolated testing
-- **Variable workloads**: Auto-scale during traffic spikes, scale to zero when idle
-- **Cost optimization**: Pay only for active compute time and storage used
-
-## Further Reading
-
-| Topic | URL |
-| ---------------------- | ----------------------------------------------------------- |
-| Architecture | https://neon.com/docs/introduction/architecture-overview.md |
-| Plans & Billing | https://neon.com/docs/introduction/about-billing.md |
-| Regions | https://neon.com/docs/introduction/regions.md |
-| Postgres Compatibility | https://neon.com/docs/reference/compatibility.md |
diff --git a/.agents/skills/swiftui-animation/SKILL.md b/.agents/skills/swiftui-animation/SKILL.md
deleted file mode 100644
index f1cff06a..00000000
--- a/.agents/skills/swiftui-animation/SKILL.md
+++ /dev/null
@@ -1,204 +0,0 @@
----
-name: swiftui-animation
-description: This skill provides comprehensive guidance for implementing advanced SwiftUI animations, transitions, matched geometry effects, and Metal shader integration. Use when building animations, view transitions, hero animations, or GPU-accelerated effects in SwiftUI apps for iOS and macOS.
----
-
-# SwiftUI Animation Expert
-
-Expert guidance for implementing advanced SwiftUI animations and Metal shader integration. Covers animation curves, springs, transitions, matched geometry effects, PhaseAnimator, KeyframeAnimator, and GPU-accelerated shader effects.
-
-## When to Use This Skill
-
-- Understanding motion design principles and when to use animation
-- Making animations accessible and platform-appropriate
-- Implementing animations in SwiftUI (springs, easing, keyframes)
-- Creating view transitions (fade, slide, scale, custom)
-- Building hero animations with matchedGeometryEffect
-- Adding GPU-accelerated effects with Metal shaders
-- Optimizing animation performance
-- Creating multi-phase orchestrated animations
-
-## Quick Reference
-
-### Animation Basics
-
-```swift
-// Explicit animation (preferred)
-withAnimation(.spring(response: 0.4, dampingFraction: 0.75)) {
- isExpanded.toggle()
-}
-
-// iOS 17+ spring presets
-withAnimation(.snappy) { ... } // Fast, small bounce
-withAnimation(.smooth) { ... } // Gentle, no bounce
-withAnimation(.bouncy) { ... } // More bounce
-```
-
-### Common Transitions
-
-```swift
-// Basic
-.transition(.opacity)
-.transition(.scale)
-.transition(.slide)
-.transition(.move(edge: .bottom))
-
-// Combined
-.transition(.move(edge: .trailing).combined(with: .opacity))
-
-// Asymmetric
-.transition(.asymmetric(
- insertion: .move(edge: .bottom),
- removal: .opacity
-))
-```
-
-### Matched Geometry Effect
-
-```swift
-@Namespace var namespace
-
-// Source view
-ThumbnailView()
- .matchedGeometryEffect(id: "hero", in: namespace)
-
-// Destination view
-DetailView()
- .matchedGeometryEffect(id: "hero", in: namespace)
-```
-
-### Metal Shader Effects (iOS 17+)
-
-```swift
-// Color manipulation
-.colorEffect(ShaderLibrary.invert())
-
-// Pixel displacement
-.distortionEffect(
- ShaderLibrary.wave(.float(time)),
- maxSampleOffset: CGSize(width: 20, height: 20)
-)
-
-// Full layer access
-.layerEffect(ShaderLibrary.blur(.float(radius)), maxSampleOffset: .zero)
-```
-
-## Reference Materials
-
-Detailed documentation is available in `references/`:
-
-- **motion-guidelines.md** - HIG Motion design principles
- - Purpose-driven motion philosophy
- - Accessibility requirements
- - Platform-specific considerations (iOS, visionOS, watchOS)
- - Animation anti-patterns to avoid
-
-- **animations.md** - Complete animation API guide
- - Implicit vs explicit animations
- - Spring parameters and presets
- - Animation modifiers (speed, delay, repeat)
- - PhaseAnimator for multi-step sequences
- - KeyframeAnimator for property-specific timelines
- - Custom animatable properties
-
-- **transitions.md** - View transition guide
- - Built-in transitions (opacity, scale, slide, move)
- - Combined and asymmetric transitions
- - Matched geometry effect implementation
- - Hero animation patterns
- - Content transitions (iOS 17+)
- - Custom transition creation
-
-- **metal-shaders.md** - GPU shader integration
- - SwiftUI shader modifiers (colorEffect, distortionEffect, layerEffect)
- - Writing Metal shader functions
- - Embedding MTKView with UIViewRepresentable
- - Cross-platform Metal integration (iOS/macOS)
- - Performance considerations
-
-## Common Patterns
-
-### Expandable Card
-
-```swift
-struct ExpandableCard: View {
- @State private var isExpanded = false
-
- var body: some View {
- VStack {
- RoundedRectangle(cornerRadius: isExpanded ? 20 : 12)
- .fill(.blue)
- .frame(
- width: isExpanded ? 300 : 150,
- height: isExpanded ? 400 : 100
- )
- }
- .onTapGesture {
- withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) {
- isExpanded.toggle()
- }
- }
- }
-}
-```
-
-### List Item Appearance
-
-```swift
-ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
- ItemRow(item: item)
- .transition(.asymmetric(
- insertion: .move(edge: .trailing).combined(with: .opacity),
- removal: .move(edge: .leading).combined(with: .opacity)
- ))
- .animation(.spring().delay(Double(index) * 0.05), value: items)
-}
-```
-
-### Pulsing Indicator
-
-```swift
-Circle()
- .fill(.blue)
- .frame(width: 20, height: 20)
- .scaleEffect(isPulsing ? 1.2 : 1.0)
- .opacity(isPulsing ? 0.6 : 1.0)
- .onAppear {
- withAnimation(.easeInOut(duration: 1.0).repeatForever(autoreverses: true)) {
- isPulsing = true
- }
- }
-```
-
-## Best Practices
-
-1. **Motion should be purposeful** - Don't add animation for its own sake; support the experience without overshadowing it
-2. **Make motion optional** - Supplement with haptics and audio; never use motion as the only way to communicate
-3. **Aim for brevity** - Brief, precise animations feel lightweight and convey information effectively
-4. **Prefer explicit animations** - Use `withAnimation` over `.animation()` modifier for clarity
-5. **Use spring animations** - They feel more natural and iOS-native
-6. **Start with `.spring(response: 0.35, dampingFraction: 0.8)`** - Good default for most interactions
-7. **Keep animations under 400ms** - Longer feels sluggish
-8. **Let people cancel motion** - Don't force users to wait for animations to complete
-9. **Test on device** - Simulator animation timing differs
-10. **Profile shader performance** - GPU time matters for complex effects
-
-## Troubleshooting
-
-### Animation not working
-
-- Ensure state change is wrapped in `withAnimation`
-- Check that the property is animatable
-- Verify the view is actually changing
-
-### Matched geometry jumps
-
-- Both views must use the same ID and namespace
-- Use explicit `withAnimation` when toggling
-- Check `zIndex` for proper layering
-
-### Shader not appearing
-
-- Verify `.metal` file is added to target
-- Check shader function signature matches expected format
-- Ensure `maxSampleOffset` is set correctly for distortion effects
diff --git a/.agents/skills/swiftui-animation/references/animations.md b/.agents/skills/swiftui-animation/references/animations.md
deleted file mode 100644
index e1cddb59..00000000
--- a/.agents/skills/swiftui-animation/references/animations.md
+++ /dev/null
@@ -1,288 +0,0 @@
-# SwiftUI Animation APIs
-
-SwiftUI provides a declarative approach to animations, making it easy to animate view changes with minimal code. Animations in SwiftUI can be implicit or explicit.
-
-## Implicit vs Explicit Animations
-
-**Implicit animations** occur when attaching an animation to a view or state change using the `.animation(_:)` modifier.
-
-**Explicit animations** are triggered by wrapping state changes in a `withAnimation` closure. This is the modern, preferred approach as it provides clearer control over what animates.
-
-```swift
-// Explicit animation (preferred)
-withAnimation {
- isExpanded.toggle()
-}
-
-// Implicit animation
-SomeView()
- .animation(.spring(), value: isExpanded)
-```
-
-When wrapping a state mutation in `withAnimation`, SwiftUI animates any animatable view properties that change by interpolating from their old values to new values over time. SwiftUI automatically computes intermediate frames, producing smooth transitions.
-
-## Basic Animation Curves
-
-SwiftUI includes built-in animation curves and spring dynamics:
-
-### Standard Easing Curves
-
-```swift
-withAnimation(.easeInOut) { ... }
-withAnimation(.easeIn) { ... }
-withAnimation(.easeOut) { ... }
-withAnimation(.linear) { ... }
-```
-
-### Spring Animations
-
-```swift
-// Fully parametric spring
-withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { ... }
-
-// iOS 17+ presets
-withAnimation(.smooth) { ... } // No bounce, gentle transitions
-withAnimation(.snappy) { ... } // Small bounce, modern feel
-withAnimation(.bouncy) { ... } // More bounce
-
-// With custom duration
-withAnimation(.snappy(duration: 0.25)) { ... } // Fast, modern
-withAnimation(.smooth(duration: 0.35)) { ... } // Soft, gentle
-```
-
-### Spring Parameters Explained
-
-- **response** - Speed of reaction (lower = faster/stiffer)
-- **dampingFraction** - Amount of bounce
- - `0.5` = very bouncy
- - `0.8` = gentle bounce (ideal for interactive drags)
- - `0.9` = nearly critically damped
- - `1.0` = no bounce
-
-```swift
-// Quick response with gentle bounce - ideal for pop-ups
-withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
- showPopup = true
-}
-
-// Mimics Apple Music/Wallet feel
-withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
- cardExpanded = true
-}
-```
-
-## Animation Modifiers
-
-### Speed and Delay
-
-```swift
-withAnimation(.spring().speed(2.0)) { ... } // 2x speed
-withAnimation(.easeInOut.delay(0.5)) { ... } // 0.5s delay
-```
-
-### Repeating Animations
-
-```swift
-// Repeat specific count
-withAnimation(.easeInOut.repeatCount(3, autoreverses: true)) {
- scale = 1.2
-}
-
-// Repeat forever (pulsing effect)
-withAnimation(.easeInOut(duration: 1.0).repeatForever(autoreverses: true)) {
- opacity = 0.5
-}
-```
-
-## Animatable Properties
-
-SwiftUI automatically animates properties conforming to the `Animatable` protocol:
-
-- Frame (size, position)
-- Opacity
-- Scale
-- Rotation
-- Offset
-- Corner radius
-- Colors
-- Path shapes
-
-```swift
-struct AnimatedCard: View {
- @State private var isExpanded = false
-
- var body: some View {
- RoundedRectangle(cornerRadius: isExpanded ? 20 : 10)
- .fill(isExpanded ? Color.blue : Color.gray)
- .frame(
- width: isExpanded ? 300 : 150,
- height: isExpanded ? 400 : 200
- )
- .scaleEffect(isExpanded ? 1.0 : 0.9)
- .opacity(isExpanded ? 1.0 : 0.7)
- .onTapGesture {
- withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
- isExpanded.toggle()
- }
- }
- }
-}
-```
-
-## PhaseAnimator (iOS 17+)
-
-PhaseAnimator creates multi-step animations by defining a sequence of phases:
-
-```swift
-enum AnimationPhase: CaseIterable {
- case initial, middle, final
-}
-
-struct PhaseAnimatedView: View {
- @State private var trigger = false
-
- var body: some View {
- PhaseAnimator(AnimationPhase.allCases, trigger: trigger) { phase in
- Circle()
- .fill(.blue)
- .scaleEffect(scaleFor(phase))
- .opacity(opacityFor(phase))
- .rotationEffect(rotationFor(phase))
- } animation: { phase in
- switch phase {
- case .initial: .spring(duration: 0.3)
- case .middle: .easeInOut(duration: 0.5)
- case .final: .bouncy
- }
- }
- .onTapGesture { trigger.toggle() }
- }
-
- func scaleFor(_ phase: AnimationPhase) -> CGFloat {
- switch phase {
- case .initial: 1.0
- case .middle: 1.5
- case .final: 1.0
- }
- }
-
- func opacityFor(_ phase: AnimationPhase) -> Double {
- switch phase {
- case .initial: 1.0
- case .middle: 0.5
- case .final: 1.0
- }
- }
-
- func rotationFor(_ phase: AnimationPhase) -> Angle {
- switch phase {
- case .initial: .zero
- case .middle: .degrees(180)
- case .final: .degrees(360)
- }
- }
-}
-```
-
-**Use PhaseAnimator when:** Orchestrating a series of distinct visual states where all properties animate together per phase change.
-
-## KeyframeAnimator (iOS 17+)
-
-KeyframeAnimator provides fine-grained control with independent timelines for different properties:
-
-```swift
-struct KeyframeValues {
- var scale: Double = 1.0
- var rotation: Angle = .zero
- var verticalOffset: Double = 0.0
-}
-
-struct KeyframeAnimatedView: View {
- @State private var trigger = false
-
- var body: some View {
- KeyframeAnimator(
- initialValue: KeyframeValues(),
- trigger: trigger
- ) { values in
- Circle()
- .fill(.orange)
- .frame(width: 100, height: 100)
- .scaleEffect(values.scale)
- .rotationEffect(values.rotation)
- .offset(y: values.verticalOffset)
- } keyframes: { _ in
- KeyframeTrack(\.scale) {
- LinearKeyframe(1.5, duration: 0.2)
- SpringKeyframe(1.0, duration: 0.3, spring: .bouncy)
- }
-
- KeyframeTrack(\.rotation) {
- LinearKeyframe(.degrees(180), duration: 0.3)
- CubicKeyframe(.degrees(360), duration: 0.2)
- }
-
- KeyframeTrack(\.verticalOffset) {
- SpringKeyframe(-50, duration: 0.25, spring: .snappy)
- SpringKeyframe(0, duration: 0.25, spring: .bouncy)
- }
- }
- .onTapGesture { trigger.toggle() }
- }
-}
-```
-
-### Keyframe Types
-
-- **LinearKeyframe** - Constant velocity interpolation
-- **SpringKeyframe** - Spring-based timing
-- **CubicKeyframe** - Cubic Bezier curve
-- **MoveKeyframe** - Instantaneous jump (no interpolation)
-
-**Use KeyframeAnimator when:** Different properties need to animate on their own independent timelines for rich, orchestrated effects.
-
-## Custom Animatable Properties
-
-Create custom animatable types by conforming to `Animatable`:
-
-```swift
-struct AnimatableProgress: Shape {
- var progress: Double
-
- var animatableData: Double {
- get { progress }
- set { progress = newValue }
- }
-
- func path(in rect: CGRect) -> Path {
- var path = Path()
- let width = rect.width * progress
- path.addRect(CGRect(x: 0, y: 0, width: width, height: rect.height))
- return path
- }
-}
-
-// Usage
-struct ProgressView: View {
- @State private var progress: Double = 0
-
- var body: some View {
- AnimatableProgress(progress: progress)
- .fill(.blue)
- .frame(height: 20)
- .onAppear {
- withAnimation(.easeInOut(duration: 2.0)) {
- progress = 1.0
- }
- }
- }
-}
-```
-
-## Sources
-
-- Apple WWDC23 Session - Explore SwiftUI Animation
-- Apple Developer Documentation - Animation
-- Sebastien Lato, "SwiftUI Animation Masterclass" (Dev.to, 2025)
-- AppCoda - Using PhaseAnimator (Aug 2023)
-- AppCoda - Creating Advanced Animations with KeyframeAnimator (Aug 2023)
diff --git a/.agents/skills/swiftui-animation/references/metal-shaders.md b/.agents/skills/swiftui-animation/references/metal-shaders.md
deleted file mode 100644
index 1d09b926..00000000
--- a/.agents/skills/swiftui-animation/references/metal-shaders.md
+++ /dev/null
@@ -1,1449 +0,0 @@
-# Metal Shaders and SwiftUI Integration
-
-Metal is Apple's low-level graphics and compute shader framework for GPU-accelerated rendering and effects. It can be combined with SwiftUI in two primary ways:
-
-1. **SwiftUI shader effects** - Built-in modifiers for per-view GPU processing (iOS 17+)
-2. **Custom Metal rendering** - Embedding MTKView for fully custom pipelines
-
-Both approaches work cross-platform (iOS, macOS, visionOS) with similar APIs.
-
-## SwiftUI Shader Effects (iOS 17+)
-
-Modern SwiftUI introduces view modifiers that apply custom Metal fragment shaders directly to views:
-
-- `.colorEffect()` - Modify pixel colors only
-- `.distortionEffect()` - Displace pixel positions (warping, ripples)
-- `.layerEffect()` - Full composite effects with original layer access
-
-### How Shaders Work in SwiftUI
-
-Shaders are small programs that run on your device's GPU. SwiftUI uses shaders internally to implement many visual effects like Mesh Gradients. When you apply a shader effect to a view using modifiers like `.layerEffect()`, SwiftUI calls your shader function for every single pixel of your view.
-
-```swift
-// Instantiate a shader from ShaderLibrary
-let shader = ShaderLibrary.ripple(
- .float(time),
- .float2(origin),
- .color(.pink)
-)
-
-// Apply to a view
-myView.layerEffect(shader, maxSampleOffset: CGSize(width: 100, height: 100))
-```
-
-### Metal Shading Language Basics
-
-Shaders are written in Metal Shading Language (not Swift). The shader function name matches the invocation on `ShaderLibrary`.
-
-```metal
-// Shaders.metal
-#include
-#include
-using namespace metal;
-
-[[stitchable]] half4 myEffect(
- float2 position, // Current pixel's location
- SwiftUI::Layer layer, // View's content (for sampling)
- half4 color // SwiftUI Color converted to half4
-) {
- // position: the pixel being processed
- // layer.sample(pos): get color at position (must stay within maxSampleOffset)
- // color: passed-in color parameter
-
- return layer.sample(position);
-}
-```
-
-### Metal Vector Types
-
-Metal uses vector types extensively:
-
-- `float2` - Two-component 32-bit float (2D points, dimensions)
-- `half4` - Four-component 16-bit float (RGBA colors)
-- `float3` - Three-component 32-bit float (RGB, 3D positions)
-- `float4` - Four-component 32-bit float
-
-SwiftUI automatically converts types like `Color` to Metal representations (`half4`).
-
-### Creating a Metal Shader
-
-#### 1. Add a Metal File
-
-Create a `.metal` file in your project:
-
-```metal
-// Shaders.metal
-#include
-#include
-using namespace metal;
-
-// Color effect - modify colors only
-[[stitchable]] half4 pixelate(
- float2 position,
- SwiftUI::Layer layer,
- float size
-) {
- float2 pixelatedPosition = floor(position / size) * size;
- return layer.sample(pixelatedPosition);
-}
-
-// Distortion effect - displace pixels
-[[stitchable]] float2 wave(
- float2 position,
- float time,
- float amplitude,
- float frequency
-) {
- float2 offset = float2(
- sin(position.y * frequency + time) * amplitude,
- cos(position.x * frequency + time) * amplitude
- );
- return position + offset;
-}
-
-// Color manipulation
-[[stitchable]] half4 colorShift(
- float2 position,
- half4 color,
- float hueShift
-) {
- // Convert RGB to HSV, shift hue, convert back
- float3 rgb = float3(color.rgb);
- // ... HSV conversion logic ...
- return half4(rgb.r, rgb.g, rgb.b, color.a);
-}
-```
-
-#### 2. Apply in SwiftUI
-
-```swift
-import SwiftUI
-
-struct ShaderDemoView: View {
- var body: some View {
- Image("photo")
- .resizable()
- .aspectRatio(contentMode: .fit)
- .layerEffect(
- ShaderLibrary.pixelate(.float(10)),
- maxSampleOffset: .zero
- )
- }
-}
-
-struct WaveEffectView: View {
- @State private var time: Float = 0
-
- var body: some View {
- Text("Wavy Text")
- .font(.largeTitle)
- .distortionEffect(
- ShaderLibrary.wave(
- .float(time),
- .float(5), // amplitude
- .float(0.1) // frequency
- ),
- maxSampleOffset: CGSize(width: 10, height: 10)
- )
- .onReceive(Timer.publish(every: 1/60, on: .main, in: .common).autoconnect()) { _ in
- time += 0.05
- }
- }
-}
-```
-
-### Shader Types Explained
-
-#### colorEffect
-
-Changes pixel colors without moving them. The shader receives the current color and position.
-
-```swift
-.colorEffect(ShaderLibrary.invertColors())
-```
-
-```metal
-[[stitchable]] half4 invertColors(float2 position, half4 color) {
- return half4(1.0 - color.rgb, color.a);
-}
-```
-
-#### distortionEffect
-
-Displaces pixels to new positions. Returns the source position to sample from.
-
-```swift
-.distortionEffect(
- ShaderLibrary.ripple(.float(time), .float2(center)),
- maxSampleOffset: CGSize(width: 100, height: 100)
-)
-```
-
-```metal
-[[stitchable]] float2 ripple(
- float2 position,
- float time,
- float2 center
-) {
- float distance = length(position - center);
- float wave = sin(distance * 0.1 - time * 5) * 10;
- float2 direction = normalize(position - center);
- return position + direction * wave;
-}
-```
-
-**Important:** Set `maxSampleOffset` to the maximum distance pixels can move.
-
-#### layerEffect
-
-Full access to the rendered layer, enabling complex composite effects. This is the most powerful effect type and effectively a superset of the other two.
-
-```swift
-.layerEffect(
- ShaderLibrary.blur(.float(radius)),
- maxSampleOffset: CGSize(width: radius, height: radius)
-)
-```
-
-```metal
-[[stitchable]] half4 blur(
- float2 position,
- SwiftUI::Layer layer,
- float radius
-) {
- half4 color = half4(0);
- float samples = 0;
-
- for (float x = -radius; x <= radius; x += 1) {
- for (float y = -radius; y <= radius; y += 1) {
- color += layer.sample(position + float2(x, y));
- samples += 1;
- }
- }
-
- return color / samples;
-}
-```
-
-## Complete Ripple Effect Example (WWDC 2024)
-
-This example from WWDC 2024 shows a touch-responsive ripple effect that spreads from the touch location.
-
-### Metal Shader
-
-```metal
-[[stitchable]] half4 ripple(
- float2 position,
- SwiftUI::Layer layer,
- float2 origin,
- float time,
- float amplitude,
- float frequency,
- float decay,
- float speed
-) {
- // Calculate distance from touch origin
- float distance = length(position - origin);
-
- // Calculate ripple displacement
- float rippleAmount = amplitude * sin(frequency * distance - speed * time);
- rippleAmount *= exp(-decay * distance); // Decay with distance
-
- // Calculate new sample position
- float2 direction = normalize(position - origin);
- float2 newPosition = position + direction * rippleAmount;
-
- // Sample the layer at the distorted position
- half4 color = layer.sample(newPosition);
-
- // Optional: adjust brightness based on distortion strength
- float brightness = 1.0 + rippleAmount * 0.02;
- color.rgb *= brightness;
-
- return color;
-}
-```
-
-### SwiftUI ViewModifier
-
-```swift
-struct RippleModifier: ViewModifier {
- var origin: CGPoint
- var elapsedTime: TimeInterval
- var amplitude: Double = 12
- var frequency: Double = 15
- var decay: Double = 8
- var speed: Double = 1200
-
- func body(content: Content) -> some View {
- content.layerEffect(
- ShaderLibrary.ripple(
- .float2(origin),
- .float(elapsedTime),
- .float(amplitude),
- .float(frequency),
- .float(decay),
- .float(speed)
- ),
- maxSampleOffset: CGSize(width: 100, height: 100)
- )
- }
-}
-```
-
-### Animated Ripple Effect
-
-```swift
-struct RippleEffect: ViewModifier {
- var origin: CGPoint
- var trigger: Bool
- var duration: TimeInterval = 1.5
-
- func body(content: Content) -> some View {
- content.keyframeAnimator(
- initialValue: 0.0,
- trigger: trigger
- ) { view, elapsedTime in
- view.modifier(RippleModifier(
- origin: origin,
- elapsedTime: elapsedTime
- ))
- } keyframes: { _ in
- LinearKeyframe(duration, duration: duration)
- }
- }
-}
-
-extension View {
- func rippleEffect(at origin: CGPoint, trigger: Bool) -> some View {
- modifier(RippleEffect(origin: origin, trigger: trigger))
- }
-}
-
-// Usage
-struct RippleDemo: View {
- @State private var tapLocation: CGPoint = .zero
- @State private var trigger = false
-
- var body: some View {
- Image("photo")
- .resizable()
- .aspectRatio(contentMode: .fit)
- .rippleEffect(at: tapLocation, trigger: trigger)
- .onTapGesture { location in
- tapLocation = location
- trigger.toggle()
- }
- }
-}
-```
-
-### Debug UI for Shader Parameters
-
-Building great shader effects requires experimentation. Create debug UI to iterate quickly:
-
-```swift
-struct ShaderDebugView: View {
- @State private var amplitude: Double = 12
- @State private var frequency: Double = 15
- @State private var decay: Double = 8
- @State private var speed: Double = 1200
- @State private var time: Double = 0
-
- var body: some View {
- VStack {
- // Preview with scrubber
- Image("photo")
- .resizable()
- .aspectRatio(contentMode: .fit)
- .modifier(RippleModifier(
- origin: CGPoint(x: 150, y: 150),
- elapsedTime: time,
- amplitude: amplitude,
- frequency: frequency,
- decay: decay,
- speed: speed
- ))
-
- // Time scrubber
- Slider(value: $time, in: 0...2)
- Text("Time: \(time, specifier: "%.2f")")
-
- // Parameter controls
- Group {
- Slider(value: $amplitude, in: 0...50)
- Text("Amplitude: \(amplitude, specifier: "%.1f")")
-
- Slider(value: $frequency, in: 0...50)
- Text("Frequency: \(frequency, specifier: "%.1f")")
-
- Slider(value: $decay, in: 0...20)
- Text("Decay: \(decay, specifier: "%.1f")")
-
- Slider(value: $speed, in: 0...3000)
- Text("Speed: \(speed, specifier: "%.0f")")
- }
- }
- .padding()
- }
-}
-```
-
-## Scroll Effects with visualEffect
-
-The `visualEffect` modifier provides access to view geometry for position-based effects:
-
-```swift
-struct GroceryListView: View {
- let items: [GroceryItem]
-
- var body: some View {
- ScrollView {
- ForEach(items) { item in
- ItemRow(item: item)
- .visualEffect { content, proxy in
- let frame = proxy.frame(in: .scrollView)
- let yPosition = frame.minY
-
- return content
- .hueRotation(.degrees(yPosition / 3))
- .offset(y: yPosition < 100 ? (100 - yPosition) * 0.3 : 0)
- .scaleEffect(yPosition < 100 ? 0.9 + (yPosition / 1000) : 1)
- .blur(radius: yPosition < 50 ? (50 - yPosition) / 10 : 0)
- .opacity(yPosition < 50 ? yPosition / 50 : 1)
- }
- }
- }
- }
-}
-```
-
-## Mesh Gradients (iOS 18+)
-
-Mesh gradients create beautiful color fills from a grid of control points:
-
-```swift
-struct MeshGradientView: View {
- var body: some View {
- MeshGradient(
- width: 3,
- height: 3,
- points: [
- // Row 0
- SIMD2(0.0, 0.0), SIMD2(0.5, 0.0), SIMD2(1.0, 0.0),
- // Row 1
- SIMD2(0.0, 0.5), SIMD2(0.5, 0.5), SIMD2(1.0, 0.5),
- // Row 2
- SIMD2(0.0, 1.0), SIMD2(0.5, 1.0), SIMD2(1.0, 1.0)
- ],
- colors: [
- .red, .orange, .yellow,
- .green, .blue, .purple,
- .pink, .mint, .cyan
- ]
- )
- .ignoresSafeArea()
- }
-}
-```
-
-### Animated Mesh Gradient
-
-```swift
-struct AnimatedMeshGradient: View {
- @State private var centerPoint = SIMD2(0.5, 0.5)
-
- var body: some View {
- TimelineView(.animation) { timeline in
- let time = timeline.date.timeIntervalSinceReferenceDate
-
- MeshGradient(
- width: 3,
- height: 3,
- points: [
- SIMD2(0.0, 0.0), SIMD2(0.5, 0.0), SIMD2(1.0, 0.0),
- SIMD2(0.0, 0.5),
- SIMD2(
- 0.5 + Float(sin(time)) * 0.2,
- 0.5 + Float(cos(time)) * 0.2
- ),
- SIMD2(1.0, 0.5),
- SIMD2(0.0, 1.0), SIMD2(0.5, 1.0), SIMD2(1.0, 1.0)
- ],
- colors: [
- .red, .orange, .yellow,
- .green, .blue, .purple,
- .pink, .mint, .cyan
- ]
- )
- }
- }
-}
-```
-
-## TextRenderer (iOS 18+)
-
-TextRenderer allows customizing how SwiftUI Text is drawn, enabling per-glyph animations.
-
-### Basic TextRenderer
-
-```swift
-struct AnimatedTextRenderer: TextRenderer {
- var elapsedTime: TimeInterval
- var elementDuration: TimeInterval = 0.1
- var totalDuration: TimeInterval = 0.9
-
- var animatableData: Double {
- get { elapsedTime }
- set { elapsedTime = newValue }
- }
-
- func draw(layout: Text.Layout, in context: inout GraphicsContext) {
- let count = layout.flattenedRunSlices.count
- let delay = elementDelay(count: count)
-
- for (index, slice) in layout.flattenedRunSlices.enumerated() {
- let timeOffset = Double(index) * delay
- let elementTime = max(0, min(elementDuration, elapsedTime - timeOffset))
- let progress = elementTime / elementDuration
-
- var copy = context
-
- // Animate opacity
- copy.opacity = progress
-
- // Animate blur (from blurry to sharp)
- let blurRadius = (1 - progress) * slice.typographicBounds.height / 3
- copy.addFilter(.blur(radius: blurRadius))
-
- // Animate vertical position
- let yOffset = (1 - progress) * -slice.typographicBounds.descent
- copy.translateBy(x: 0, y: yOffset)
-
- copy.draw(slice, options: .disablesSubpixelQuantization)
- }
- }
-
- private func elementDelay(count: Int) -> Double {
- (totalDuration - elementDuration) / Double(max(1, count - 1))
- }
-}
-```
-
-### Using TextRenderer with Transitions
-
-```swift
-struct TextAppearTransition: Transition {
- func body(content: Content, phase: TransitionPhase) -> some View {
- content
- .transaction { transaction in
- if !transaction.animation?.isSpring ?? false {
- transaction.animation = .linear(duration: 0.9)
- }
- }
- .textRenderer(AnimatedTextRenderer(
- elapsedTime: phase.isIdentity ? 0.9 : 0
- ))
- }
-}
-
-extension AnyTransition {
- static var textAppear: AnyTransition {
- .modifier(
- active: TextAppearTransition(),
- identity: TextAppearTransition()
- )
- }
-}
-
-// Usage
-struct TextTransitionDemo: View {
- @State private var showText = false
-
- var body: some View {
- VStack {
- if showText {
- Text("Visual Effects")
- .font(.largeTitle)
- .transition(.textAppear)
- }
-
- Button("Toggle") {
- withAnimation {
- showText.toggle()
- }
- }
- }
- }
-}
-```
-
-### TextAttribute for Selective Animation
-
-Mark specific text ranges for special treatment:
-
-```swift
-struct EmphasisAttribute: TextAttribute {}
-
-extension Text {
- func emphasis() -> Text {
- self.customAttribute(EmphasisAttribute())
- }
-}
-
-// Usage
-Text("Welcome to ") + Text("Visual Effects").emphasis() + Text("!")
-```
-
-Then in your TextRenderer, check for the attribute:
-
-```swift
-func draw(layout: Text.Layout, in context: inout GraphicsContext) {
- for run in layout.flattenedRuns {
- let hasEmphasis = run[EmphasisAttribute.self] != nil
-
- if hasEmphasis {
- // Animate per-glyph
- for slice in run {
- // ... glyph animation
- }
- } else {
- // Simple fade
- context.opacity = progress
- context.draw(run)
- }
- }
-}
-```
-
-## Animated Shader Example
-
-```swift
-struct AnimatedShaderView: View {
- var body: some View {
- TimelineView(.animation) { timeline in
- let time = timeline.date.timeIntervalSinceReferenceDate
-
- Image("background")
- .resizable()
- .aspectRatio(contentMode: .fill)
- .distortionEffect(
- ShaderLibrary.wave(
- .float(Float(time)),
- .float(8),
- .float(0.05)
- ),
- maxSampleOffset: CGSize(width: 20, height: 20)
- )
- }
- }
-}
-```
-
-## Common Shader Effects
-
-### Chromatic Aberration
-
-```metal
-[[stitchable]] half4 chromaticAberration(
- float2 position,
- SwiftUI::Layer layer,
- float amount
-) {
- half4 r = layer.sample(position + float2(amount, 0));
- half4 g = layer.sample(position);
- half4 b = layer.sample(position - float2(amount, 0));
- return half4(r.r, g.g, b.b, g.a);
-}
-```
-
-### Vignette
-
-```metal
-[[stitchable]] half4 vignette(
- float2 position,
- half4 color,
- float2 size,
- float intensity
-) {
- float2 uv = position / size;
- float2 center = float2(0.5, 0.5);
- float dist = distance(uv, center);
- float vignette = 1.0 - smoothstep(0.3, 0.7, dist * intensity);
- return half4(color.rgb * vignette, color.a);
-}
-```
-
-### Noise/Grain
-
-```metal
-[[stitchable]] half4 filmGrain(
- float2 position,
- half4 color,
- float time,
- float intensity
-) {
- float noise = fract(sin(dot(position + time, float2(12.9898, 78.233))) * 43758.5453);
- half3 grain = half3(noise * intensity);
- return half4(color.rgb + grain, color.a);
-}
-```
-
-### Gradient Map
-
-```metal
-[[stitchable]] half4 gradientMap(
- float2 position,
- SwiftUI::Layer layer,
- half4 shadowColor,
- half4 highlightColor
-) {
- half4 original = layer.sample(position);
- float luminance = dot(original.rgb, half3(0.299, 0.587, 0.114));
- half3 mapped = mix(shadowColor.rgb, highlightColor.rgb, luminance);
- return half4(mapped, original.a);
-}
-```
-
-## Embedding Metal with UIViewRepresentable
-
-For full control over Metal rendering (3D content, custom vertex shaders, multi-pass rendering), embed an MTKView in SwiftUI.
-
-### Basic Setup
-
-```swift
-import SwiftUI
-import MetalKit
-
-struct MetalView: UIViewRepresentable {
- func makeCoordinator() -> Renderer {
- Renderer()
- }
-
- func makeUIView(context: Context) -> MTKView {
- let mtkView = MTKView()
- mtkView.device = MTLCreateSystemDefaultDevice()
- mtkView.delegate = context.coordinator
- mtkView.preferredFramesPerSecond = 60
- mtkView.enableSetNeedsDisplay = false
- mtkView.isPaused = false
- mtkView.clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 1)
- context.coordinator.setup(mtkView: mtkView)
- return mtkView
- }
-
- func updateUIView(_ uiView: MTKView, context: Context) {}
-}
-
-class Renderer: NSObject, MTKViewDelegate {
- var device: MTLDevice!
- var commandQueue: MTLCommandQueue!
- var pipelineState: MTLRenderPipelineState!
-
- func setup(mtkView: MTKView) {
- device = mtkView.device
- commandQueue = device.makeCommandQueue()
-
- let library = device.makeDefaultLibrary()!
- let vertexFunction = library.makeFunction(name: "vertexShader")
- let fragmentFunction = library.makeFunction(name: "fragmentShader")
-
- let pipelineDescriptor = MTLRenderPipelineDescriptor()
- pipelineDescriptor.vertexFunction = vertexFunction
- pipelineDescriptor.fragmentFunction = fragmentFunction
- pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat
-
- pipelineState = try! device.makeRenderPipelineState(descriptor: pipelineDescriptor)
- }
-
- func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
-
- func draw(in view: MTKView) {
- guard let drawable = view.currentDrawable,
- let renderPassDescriptor = view.currentRenderPassDescriptor,
- let commandBuffer = commandQueue.makeCommandBuffer(),
- let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
- else { return }
-
- renderEncoder.setRenderPipelineState(pipelineState)
- renderEncoder.endEncoding()
-
- commandBuffer.present(drawable)
- commandBuffer.commit()
- }
-}
-```
-
-### Cross-Platform Version
-
-```swift
-import SwiftUI
-import MetalKit
-
-#if os(iOS) || os(tvOS)
-typealias ViewRepresentable = UIViewRepresentable
-#elseif os(macOS)
-typealias ViewRepresentable = NSViewRepresentable
-#endif
-
-struct MetalView: ViewRepresentable {
- func makeCoordinator() -> Renderer { Renderer() }
-
- #if os(iOS) || os(tvOS)
- func makeUIView(context: Context) -> MTKView { createMTKView(context: context) }
- func updateUIView(_ uiView: MTKView, context: Context) {}
- #elseif os(macOS)
- func makeNSView(context: Context) -> MTKView { createMTKView(context: context) }
- func updateNSView(_ nsView: MTKView, context: Context) {}
- #endif
-
- private func createMTKView(context: Context) -> MTKView {
- let mtkView = MTKView()
- mtkView.device = MTLCreateSystemDefaultDevice()
- mtkView.delegate = context.coordinator
- mtkView.preferredFramesPerSecond = 60
- context.coordinator.setup(mtkView: mtkView)
- return mtkView
- }
-}
-```
-
-## Choosing the Right Approach
-
-### Use SwiftUI Shader Effects When:
-
-- Applying effects to existing SwiftUI views
-- Creating shader-driven transitions
-- Adding image processing (blur, color shifts, distortions)
-- Building particle-like effects on views
-- Simpler implementation is preferred
-
-### Use MTKView Embedding When:
-
-- Rendering custom 3D content
-- Performing custom drawing with vertex shaders
-- Managing multi-phase GPU rendering
-- Using compute kernels
-- Building games or interactive 3D scenes
-- Needing advanced Metal features (multiple render passes, custom blending)
-
-## Performance Considerations
-
-1. **Shader complexity** - Keep shaders efficient; GPU time matters
-2. **maxSampleOffset** - Set accurately; larger values = more GPU work
-3. **Frame rate** - Use `TimelineView(.animation)` for smooth updates
-4. **Memory** - Large textures consume GPU memory
-5. **Profiling** - Use Xcode's GPU profiler for optimization
-6. **Debug UI** - Build parameter scrubbers for rapid iteration
-
-## Best Practices
-
-1. **Experiment boldly** - Turn parameters up to explore boundaries
-2. **Live with effects** - Test over time to ensure they're pleasant, not distracting
-3. **Context matters** - Effects should fit naturally within the larger app
-4. **Build debug tools** - Scrubbers and visualizers accelerate development
-5. **Consider accessibility** - Ensure effects don't impair usability
-
-## Ready-to-Use Shader Effects (from Inferno)
-
-The following shaders are adapted from [Inferno](https://github.com/twostraws/Inferno) by Paul Hudson (MIT License). Inferno is an excellent open-source collection of fragment shaders designed for SwiftUI apps, with comprehensive documentation and beginner-friendly code.
-
-### Water Ripple Effect
-
-A distortion shader that creates animated water ripples.
-
-```metal
-// Water.metal - from Inferno (https://github.com/twostraws/Inferno)
-// MIT License - Copyright (c) 2023 Paul Hudson
-
-[[stitchable]] float2 water(
- float2 position,
- float2 size,
- float time,
- float speed, // 0.5-10, start with 3
- float strength, // 1-5, start with 3
- float frequency // 5-25, start with 10
-) {
- // Normalize to UV space (0..1)
- float2 uv = position / size;
-
- // Adjust parameters
- float adjustedSpeed = time * speed * 0.05f;
- float adjustedStrength = strength / 100.0f;
-
- // Wrap phase to avoid large trig arguments
- const float TWO_PI = 6.28318530718f;
- float phase = fmod(adjustedSpeed * frequency, TWO_PI);
-
- // Apply sine/cosine distortion
- float argX = frequency * uv.x + phase;
- float argY = frequency * uv.y + phase;
- uv.x += fast::sin(argX) * adjustedStrength;
- uv.y += fast::cos(argY) * adjustedStrength;
-
- return uv * size;
-}
-```
-
-```swift
-// SwiftUI Usage
-struct WaterEffectView: View {
- @State private var startTime = Date.now
-
- var body: some View {
- TimelineView(.animation) { timeline in
- let elapsedTime = startTime.distance(to: timeline.date)
-
- Image("photo")
- .resizable()
- .aspectRatio(contentMode: .fit)
- .drawingGroup()
- .visualEffect { content, proxy in
- content.distortionEffect(
- ShaderLibrary.water(
- .float2(proxy.size),
- .float(elapsedTime),
- .float(3), // speed
- .float(3), // strength
- .float(10) // frequency
- ),
- maxSampleOffset: .zero
- )
- }
- }
- }
-}
-```
-
-### Emboss Effect
-
-Creates a 3D relief/embossed appearance.
-
-```metal
-// Emboss.metal - from Inferno (https://github.com/twostraws/Inferno)
-// MIT License - Copyright (c) 2023 Paul Hudson
-
-[[stitchable]] half4 emboss(
- float2 position,
- SwiftUI::Layer layer,
- float strength // How far to read pixels, try 1-20
-) {
- // Read current pixel
- half4 currentColor = layer.sample(position);
- half4 newColor = currentColor;
-
- // Add brightness from one diagonal direction
- newColor += layer.sample(position + 1.0) * strength;
-
- // Subtract brightness from opposite direction
- newColor -= layer.sample(position - 1.0) * strength;
-
- // Preserve original alpha for smooth edges
- return half4(newColor) * currentColor.a;
-}
-```
-
-```swift
-// SwiftUI Usage
-Image("photo")
- .layerEffect(
- ShaderLibrary.emboss(.float(5)),
- maxSampleOffset: .zero
- )
-```
-
-### Color Planes (RGB Glitch)
-
-Separates RGB channels for a glitch effect - great with accelerometer data.
-
-```metal
-// ColorPlanes.metal - from Inferno (https://github.com/twostraws/Inferno)
-// MIT License - Copyright (c) 2023 Paul Hudson
-
-[[stitchable]] half4 colorPlanes(
- float2 position,
- SwiftUI::Layer layer,
- float2 offset // How much to offset colors
-) {
- // Red channel: double offset
- float2 red = position - (offset * 2.0);
-
- // Blue channel: single offset
- float2 blue = position - offset;
-
- // Green from original position
- half4 color = layer.sample(position);
-
- // Replace red and blue channels
- color.r = layer.sample(red).r;
- color.b = layer.sample(blue).b;
-
- // Multiply by alpha for smooth edges
- return color * color.a;
-}
-```
-
-```swift
-// SwiftUI Usage - drag to offset
-struct ColorPlanesView: View {
- @State private var offset = CGSize.zero
-
- var body: some View {
- Image("photo")
- .drawingGroup()
- .layerEffect(
- ShaderLibrary.colorPlanes(.float2(offset)),
- maxSampleOffset: .zero
- )
- .gesture(
- DragGesture(minimumDistance: 0)
- .onChanged { offset = $0.translation }
- )
- }
-}
-```
-
-### Infrared Thermal Effect
-
-Simulates thermal/infrared imaging by mapping brightness to a cold-to-hot color scale.
-
-```metal
-// Infrared.metal - from Inferno (https://github.com/twostraws/Inferno)
-// MIT License - Copyright (c) 2023 Paul Hudson
-
-[[stitchable]] half4 infrared(float2 position, half4 color) {
- if (color.a > 0) {
- // Define temperature colors
- half3 cold = half3(0.0h, 0.0h, 1.0h); // Blue
- half3 medium = half3(1.0h, 1.0h, 0.0h); // Yellow
- half3 hot = half3(1.0h, 0.0h, 0.0h); // Red
-
- // Calculate luminance
- half3 grayValues = half3(0.2125h, 0.7154h, 0.0721h);
- half luma = dot(color.rgb, grayValues);
-
- // Map to temperature colors
- half3 newColor;
- if (luma < 0.5h) {
- newColor = mix(cold, medium, luma / 0.5h);
- } else {
- newColor = mix(medium, hot, (luma - 0.5h) / 0.5h);
- }
-
- return half4(newColor, 1.0h) * color.a;
- }
- return color;
-}
-```
-
-```swift
-// SwiftUI Usage
-Image("photo")
- .colorEffect(ShaderLibrary.infrared())
-```
-
-### White Noise
-
-Generates dynamic grayscale static noise.
-
-```metal
-// WhiteNoise.metal - from Inferno (https://github.com/twostraws/Inferno)
-// MIT License - Copyright (c) 2023 Paul Hudson
-
-float whiteRandom(float offset, float2 position, float time) {
- float2 nonRepeating = float2(12.9898 * time, 78.233 * time);
- float sum = dot(position, nonRepeating);
- float sine = sin(sum);
- float hugeNumber = sine * 43758.5453 * offset;
- return fract(hugeNumber);
-}
-
-[[stitchable]] half4 whiteNoise(float2 position, half4 color, float time) {
- if (color.a > 0.0h) {
- return half4(half3(whiteRandom(1.0, position, time)), 1.0h) * color.a;
- }
- return color;
-}
-```
-
-```swift
-// SwiftUI Usage
-struct NoiseView: View {
- @State private var startTime = Date.now
-
- var body: some View {
- TimelineView(.animation) { timeline in
- let elapsedTime = startTime.distance(to: timeline.date)
-
- Rectangle()
- .colorEffect(ShaderLibrary.whiteNoise(.float(elapsedTime)))
- }
- }
-}
-```
-
-### Loupe (Magnifier)
-
-Creates a circular zoom effect at a touch location.
-
-```metal
-// SimpleLoupe.metal - from Inferno (https://github.com/twostraws/Inferno)
-// MIT License - Copyright (c) 2023 Paul Hudson
-
-[[stitchable]] half4 simpleLoupe(
- float2 position,
- SwiftUI::Layer layer,
- float2 size,
- float2 touch, // Touch location
- float maxDistance, // Loupe size, try 0.05
- float zoomFactor // Zoom amount, try 2
-) {
- // Calculate UV coordinates
- half2 uv = half2(position / size);
- half2 center = half2(touch / size);
- half2 delta = uv - center;
-
- // Calculate distance with aspect ratio correction
- half aspectRatio = size.x / size.y;
- half distance = (delta.x * delta.x) + (delta.y * delta.y) / aspectRatio;
-
- // Apply zoom inside loupe area
- half totalZoom = 1.0h;
- if (distance < maxDistance) {
- totalZoom /= zoomFactor;
- }
-
- // Calculate zoomed position
- half2 newPosition = delta * totalZoom + center;
-
- return layer.sample(float2(newPosition) * size);
-}
-```
-
-```swift
-// SwiftUI Usage
-struct LoupeView: View {
- @State private var touchLocation = CGPoint.zero
-
- var body: some View {
- Image("photo")
- .resizable()
- .aspectRatio(contentMode: .fit)
- .drawingGroup()
- .visualEffect { content, proxy in
- content.layerEffect(
- ShaderLibrary.simpleLoupe(
- .float2(proxy.size),
- .float2(touchLocation),
- .float(0.05), // loupe size
- .float(2) // zoom factor
- ),
- maxSampleOffset: .zero
- )
- }
- .gesture(
- DragGesture(minimumDistance: 0)
- .onChanged { touchLocation = $0.location }
- )
- }
-}
-```
-
-### Shimmer Effect
-
-Animated diagonal highlight sweep - great for loading states.
-
-```metal
-// Shimmer.metal - from Inferno (https://github.com/twostraws/Inferno)
-// MIT License - Copyright (c) 2023 Paul Hudson
-
-// RGB to HSL conversion
-half3 rgbToHSL(half3 rgb) {
- half minVal = min3(rgb.r, rgb.g, rgb.b);
- half maxVal = max3(rgb.r, rgb.g, rgb.b);
- half delta = maxVal - minVal;
-
- half3 hsl = half3(0.0h, 0.0h, 0.5h * (maxVal + minVal));
-
- if (delta > 0.0h) {
- if (maxVal == rgb.r) {
- hsl[0] = fmod((rgb.g - rgb.b) / delta, 6.0h);
- } else if (maxVal == rgb.g) {
- hsl[0] = (rgb.b - rgb.r) / delta + 2.0h;
- } else {
- hsl[0] = (rgb.r - rgb.g) / delta + 4.0h;
- }
- hsl[0] /= 6.0h;
- if (hsl[2] > 0.0h && hsl[2] < 1.0h) {
- hsl[1] = delta / (1.0h - abs(2.0h * hsl[2] - 1.0h));
- }
- }
- return hsl;
-}
-
-// HSL to RGB conversion
-half3 hslToRGB(half3 hsl) {
- half c = (1.0h - abs(2.0h * hsl[2] - 1.0h)) * hsl[1];
- half h = hsl[0] * 6.0h;
- half x = c * (1.0h - abs(fmod(h, 2.0h) - 1.0h));
-
- half3 rgb;
- if (h < 1.0h) rgb = half3(c, x, 0.0h);
- else if (h < 2.0h) rgb = half3(x, c, 0.0h);
- else if (h < 3.0h) rgb = half3(0.0h, c, x);
- else if (h < 4.0h) rgb = half3(0.0h, x, c);
- else if (h < 5.0h) rgb = half3(x, 0.0h, c);
- else rgb = half3(c, 0.0h, x);
-
- return rgb + (hsl[2] - 0.5h * c);
-}
-
-[[stitchable]] half4 shimmer(
- float2 position,
- half4 color,
- float2 size,
- float time,
- float animationDuration, // Loop duration in seconds
- float gradientWidth, // Width of shimmer in UV space
- float maxLightness // Peak brightness boost
-) {
- if (color.a == 0.0h) return color;
-
- // Calculate animation progress
- float loopedProgress = fmod(time, float(animationDuration));
- half progress = loopedProgress / animationDuration;
-
- // Convert to UV space
- half2 uv = half2(position / size);
-
- // Calculate gradient bounds
- half minU = 0.0h - gradientWidth;
- half maxU = 1.0h + gradientWidth;
- half start = minU + maxU * progress + gradientWidth * uv.y;
- half end = start + gradientWidth;
-
- if (uv.x > start && uv.x < end) {
- half gradient = smoothstep(start, end, uv.x);
- half intensity = sin(gradient * M_PI_H);
-
- // Adjust lightness in HSL space
- half3 hsl = rgbToHSL(color.rgb);
- hsl[2] = hsl[2] + half(maxLightness * (maxLightness > 0.0h ? 1 - hsl[2] : hsl[2])) * intensity;
- color.rgb = hslToRGB(hsl);
- }
-
- return color;
-}
-```
-
-```swift
-// SwiftUI Usage
-struct ShimmerView: View {
- @State private var startTime = Date.now
-
- var body: some View {
- TimelineView(.animation) { timeline in
- let elapsedTime = startTime.distance(to: timeline.date)
-
- Text("Loading...")
- .font(.largeTitle)
- .foregroundStyle(.gray)
- .visualEffect { content, proxy in
- content.colorEffect(
- ShaderLibrary.shimmer(
- .float2(proxy.size),
- .float(elapsedTime),
- .float(2.0), // animation duration
- .float(0.3), // gradient width
- .float(0.8) // max lightness
- )
- )
- }
- }
- }
-}
-```
-
-## Transition Shaders (from Inferno)
-
-Inferno also provides shader-based view transitions. These require both the Metal shader and a SwiftUI `AnyTransition` extension.
-
-### Pixellate Transition
-
-Views pixellate while fading between states.
-
-```metal
-// Pixellate.metal - from Inferno (https://github.com/twostraws/Inferno)
-// MIT License - Copyright (c) 2023 Paul Hudson
-
-[[stitchable]] half4 pixellate(
- float2 position,
- SwiftUI::Layer layer,
- float2 size,
- float amount, // Progress 0-1
- float squares, // Number of pixel squares
- float steps // Animation steps (lower = more retro)
-) {
- half2 uv = half2(position / size);
-
- // Direction goes 0 -> 0.5 -> 0
- half direction = min(amount, 1.0 - amount);
-
- // Quantize for stepped animation
- half steppedProgress = ceil(direction * steps) / steps;
- half2 squareSize = 2.0h * steppedProgress / half2(squares);
-
- half2 newPosition;
- if (steppedProgress == 0.0h) {
- newPosition = uv;
- } else {
- newPosition = (floor(uv / squareSize) + 0.5h) * squareSize;
- }
-
- // Blend with transparency as transition progresses
- return mix(layer.sample(float2(newPosition) * size), 0.0h, amount);
-}
-```
-
-### Swirl Transition
-
-Vortex effect that twists views during transition.
-
-```metal
-// Swirl.metal - from Inferno (https://github.com/twostraws/Inferno)
-// MIT License - Copyright (c) 2023 Paul Hudson
-
-[[stitchable]] half4 swirl(
- float2 position,
- SwiftUI::Layer layer,
- float2 size,
- float amount, // Progress 0-1
- float radius // Swirl radius relative to view, try 0.5
-) {
- half2 uv = half2(position / size);
- uv -= 0.5h;
-
- half distanceFromCenter = length(uv);
-
- if (distanceFromCenter < radius) {
- half swirlStrength = (radius - distanceFromCenter) / radius;
-
- // Swirl intensity: 0->1->0 during transition
- half swirlAmount;
- if (amount <= 0.5) {
- swirlAmount = mix(0.0h, 1.0h, half(amount) / 0.5h);
- } else {
- swirlAmount = mix(1.0h, 0.0h, (half(amount) - 0.5h) / 0.5h);
- }
-
- half swirlAngle = swirlStrength * swirlStrength * swirlAmount * 8.0h * M_PI_H;
-
- // Rotate UV coordinates
- half sinAngle = sin(swirlAngle);
- half cosAngle = cos(swirlAngle);
- uv = half2(
- dot(uv, half2(cosAngle, -sinAngle)),
- dot(uv, half2(sinAngle, cosAngle))
- );
- }
-
- uv += 0.5h;
- return mix(layer.sample(float2(uv) * size), 0.0h, amount);
-}
-```
-
-```swift
-// Transition usage example
-struct TransitionDemo: View {
- @State private var showingFirst = true
-
- var body: some View {
- VStack {
- if showingFirst {
- Image(systemName: "star.fill")
- .font(.system(size: 200))
- .foregroundStyle(.yellow)
- .drawingGroup()
- .transition(.swirl(radius: 0.5))
- } else {
- Image(systemName: "heart.fill")
- .font(.system(size: 200))
- .foregroundStyle(.red)
- .drawingGroup()
- .transition(.swirl(radius: 0.5))
- }
-
- Button("Toggle") {
- withAnimation(.easeInOut(duration: 1.5)) {
- showingFirst.toggle()
- }
- }
- }
- }
-}
-```
-
-## Metal Shading Language Reference
-
-### Common Data Types
-
-| Type | Description | Swift Equivalent |
-|------|-------------|------------------|
-| `float` | 32-bit float | `Float`, `CGFloat`, `Double` via `.float()` |
-| `float2` | 2D vector | `CGPoint`, `CGSize` via `.float2()` |
-| `float4` | 4D vector | - |
-| `half` | 16-bit float (faster on GPU) | - |
-| `half3` | RGB color | - |
-| `half4` | RGBA color | `Color` via `.color()` |
-| `uint2` | Integer 2D vector | - |
-
-### Numeric Literals
-
-```metal
-float x = 0.5; // or 0.5f, 0.5F
-half h = 0.5h; // or 0.5H (use h suffix for half precision)
-int i = 42;
-uint u = 42u; // or 42U
-```
-
-### Common Functions
-
-| Function | Description |
-|----------|-------------|
-| `abs(x)` | Absolute value |
-| `ceil(x)` | Round up |
-| `floor(x)` | Round down |
-| `fract(x)` | Fractional part |
-| `fmod(x, y)` | Remainder of x/y |
-| `min(a, b)` | Minimum value |
-| `max(a, b)` | Maximum value |
-| `mix(a, b, t)` | Linear interpolation |
-| `smoothstep(e0, e1, x)` | S-curve interpolation |
-| `sin(x)`, `cos(x)` | Trigonometry (radians) |
-| `fast::sin(x)` | Fast approximation |
-| `pow(x, y)` | x raised to power y |
-| `dot(a, b)` | Dot product |
-| `length(v)` | Vector length |
-| `normalize(v)` | Unit vector |
-| `distance(a, b)` | Distance between points |
-| `layer.sample(pos)` | Sample layer color at position |
-
-### Performance Tips
-
-1. **Prefer `half` over `float`** - Half precision is faster on mobile GPUs
-2. **Use `fast::sin()` and `fast::cos()`** - Good enough for visual effects
-3. **Precompute on CPU** - Calculate constants in Swift, pass as uniforms
-4. **Avoid branching** - Use `mix()` and `step()` instead of `if/else`
-5. **Minimize texture samples** - Each `layer.sample()` is expensive
-
-## Sources
-
-- WWDC 2024 - Create custom visual effects with SwiftUI
-- WWDC 2023 - Create custom visual effects with SwiftUI
-- [Inferno](https://github.com/twostraws/Inferno) by Paul Hudson - MIT License
-- Apple Developer Documentation - Metal Shading Language
-- Apple Developer Documentation - TextRenderer
-- Apple Developer Forums - MetalKit in SwiftUI
-- [The Book of Shaders](https://thebookofshaders.com) - Shader fundamentals
-- [ShaderToy](https://www.shadertoy.com) - Shader inspiration and examples
-- [GL Transitions](https://www.gl-transitions.com) - Transition effects
diff --git a/.agents/skills/swiftui-animation/references/motion-guidelines.md b/.agents/skills/swiftui-animation/references/motion-guidelines.md
deleted file mode 100644
index 25d3c46d..00000000
--- a/.agents/skills/swiftui-animation/references/motion-guidelines.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# Motion Design Guidelines
-
-Beautiful, fluid motions bring the interface to life, conveying status, providing feedback and instruction, and enriching the visual experience of your app or game.
-
-Many system components automatically include motion, letting you offer familiar and consistent experiences throughout your app or game. System components might also adjust their motion in response to factors like accessibility settings or different input methods. For example, the movement of Liquid Glass responds to direct touch interaction with greater emphasis to reinforce the feeling of a tactile experience, but produces a more subdued effect when a person interacts using a trackpad.
-
-If you design custom motion, follow the guidelines below.
-
-## Best Practices
-
-**Add motion purposefully, supporting the experience without overshadowing it.** Don't add motion for the sake of adding motion. Gratuitous or excessive animation can distract people and may make them feel disconnected or physically uncomfortable.
-
-**Make motion optional.** Not everyone can or wants to experience the motion in your app or game, so it's essential to avoid using it as the only way to communicate important information. To help everyone enjoy your app or game, supplement visual feedback by also using alternatives like haptics and audio to communicate.
-
-## Providing Feedback
-
-**Strive for realistic feedback motion that follows people's gestures and expectations.** In nongame apps, accurate, realistic motion can help people understand how something works, but feedback motion that doesn't make sense can make them feel disoriented. For example, if someone reveals a view by sliding it down from the top, they don't expect to dismiss the view by sliding it to the side.
-
-**Aim for brevity and precision in feedback animations.** When animated feedback is brief and precise, it tends to feel lightweight and unobtrusive, and it can often convey information more effectively than prominent animation. For example, when a game displays a succinct animation that's precisely tied to a successful action, players can instantly get the message without being distracted from their gameplay. Another example is in visionOS: When people tap a panorama in Photos, it quickly and smoothly expands to fill the space in front of them, helping them track the transition without making them wait to enjoy the content.
-
-**In apps, generally avoid adding motion to UI interactions that occur frequently.** The system already provides subtle animations for interactions with standard interface elements. For a custom element, you generally want to avoid making people spend extra time paying attention to unnecessary motion every time they interact with it.
-
-**Let people cancel motion.** As much as possible, don't make people wait for an animation to complete before they can do anything, especially if they have to experience the animation more than once.
-
-**Consider using animated symbols where it makes sense.** When you use SF Symbols 5 or later, you can apply animations to SF Symbols or custom symbols.
-
-## Leveraging Platform Capabilities
-
-**Make sure your game's motion looks great by default on each platform you support.** In most games, maintaining a consistent frame rate of 30 to 60 fps typically results in a smooth, visually appealing experience. For each platform you support, use the device's graphics capabilities to enable default settings that let people enjoy your game without first having to change those settings.
-
-**Let people customize the visual experience of your game to optimize performance or battery life.** For example, consider letting people switch between power modes when the system detects the presence of an external power source.
-
-## Platform Considerations
-
-### iOS, iPadOS, macOS, tvOS
-
-No additional considerations. System components automatically include appropriate motion and adjust based on accessibility settings and input methods.
-
-### visionOS
-
-In addition to subtly communicating context, drawing attention to information, and enriching immersive experiences, motion in visionOS can combine with depth to provide essential feedback when people look at interactive elements. Because motion is likely to be a large part of your visionOS experience, it's crucial to avoid causing distraction, confusion, or discomfort.
-
-**Avoid displaying motion at the edges of a person's field of view.** People can be particularly sensitive to motion that occurs in their peripheral vision: in addition to being distracting, such motion can even cause discomfort because it can make people feel like they or their surroundings are moving. If you need to show an object moving in the periphery during an immersive experience, make sure the object's brightness level is similar to the rest of the visible content.
-
-**Help people remain comfortable when showing the movement of large virtual objects.** If an object is large enough to fill a lot of the field of view, occluding most or all of passthrough, people can naturally perceive it as being part of their surroundings. To help people perceive the object's movement without making them think that they or their surroundings are moving, you can increase the object's translucency, helping people see through it, or lower its contrast to make its motion less noticeable.
-
-> **Note:** People can experience discomfort even when they're the ones moving a large virtual object, such as a window. Although adjusting translucency and contrast can help in this scenario, consider also keeping a window's size fairly small.
-
-**Consider using fades when you need to relocate an object.** When an object moves from one location to another, people naturally watch the movement. If such movement doesn't communicate anything useful to people, you can fade the object out before moving it and fade it back in after it's in the new location.
-
-**In general, avoid letting people rotate a virtual world.** When a virtual world rotates, the experience typically upsets people's sense of stability, even when they control the rotation and the movement is subtle. Instead, consider using instantaneous directional changes during a quick fade-out.
-
-**Consider giving people a stationary frame of reference.** It can be easier for people to handle visual movement when it's contained within an area that doesn't move. In contrast, if the entire surrounding area appears to move — for example, in a game that automatically moves a player through space — people can feel unwell.
-
-**Avoid showing objects that oscillate in a sustained way.** In particular, you want to avoid showing an oscillation that has a frequency of around 0.2 Hz because people can be very sensitive to this frequency. If you need to show objects oscillating, aim to keep the amplitude low and consider making the content translucent.
-
-### watchOS
-
-SwiftUI provides a powerful and streamlined way to add motion to your app.
-
-> **Note:** All layout- and appearance-based animations automatically include built-in easing that plays at the start and end of the animation. You can't turn off or customize easing.
-
-## Sources
-
-- [Apple Human Interface Guidelines - Motion](https://developer.apple.com/design/human-interface-guidelines/motion)
diff --git a/.agents/skills/swiftui-animation/references/transitions.md b/.agents/skills/swiftui-animation/references/transitions.md
deleted file mode 100644
index 17eab6dd..00000000
--- a/.agents/skills/swiftui-animation/references/transitions.md
+++ /dev/null
@@ -1,378 +0,0 @@
-# SwiftUI Transition Animations
-
-Transition animations handle the appearance and disappearance of views in SwiftUI, allowing smooth cross-fades or movement when views are inserted or removed from the hierarchy.
-
-## Basic Transitions
-
-By default, SwiftUI instantly inserts or removes views. Apply transitions using the `.transition()` modifier on conditional views:
-
-```swift
-struct BasicTransitionView: View {
- @State private var showDetails = false
-
- var body: some View {
- VStack {
- Button("Toggle") {
- withAnimation(.spring()) {
- showDetails.toggle()
- }
- }
-
- if showDetails {
- DetailsView()
- .transition(.opacity) // Fade in/out
- }
- }
- }
-}
-```
-
-## Built-in Transitions
-
-### Opacity
-
-```swift
-.transition(.opacity) // Fade in/out
-```
-
-### Scale
-
-```swift
-.transition(.scale) // Scale from center
-.transition(.scale(scale: 0.5)) // Scale from 50%
-.transition(.scale(scale: 0, anchor: .top)) // Scale from top
-```
-
-### Slide
-
-```swift
-.transition(.slide) // Slide in from leading, out to leading
-```
-
-### Move
-
-```swift
-.transition(.move(edge: .top)) // From/to top
-.transition(.move(edge: .bottom)) // From/to bottom
-.transition(.move(edge: .leading)) // From/to leading
-.transition(.move(edge: .trailing)) // From/to trailing
-```
-
-### Push (iOS 17+)
-
-```swift
-.transition(.push(from: .bottom)) // Push from bottom
-.transition(.push(from: .trailing)) // Push from trailing
-```
-
-## Combined Transitions
-
-Combine multiple transitions for richer effects:
-
-```swift
-// Slide and fade
-.transition(.move(edge: .leading).combined(with: .opacity))
-
-// Scale, rotate, and fade
-.transition(
- .scale(scale: 0.5)
- .combined(with: .opacity)
- .combined(with: .rotation3DEffect(.degrees(90), axis: (0, 1, 0)))
-)
-```
-
-## Asymmetric Transitions
-
-Different animations for insertion vs removal:
-
-```swift
-.transition(.asymmetric(
- insertion: .move(edge: .trailing).combined(with: .opacity),
- removal: .move(edge: .leading).combined(with: .opacity)
-))
-
-// Card appearing from bottom, disappearing with fade
-.transition(.asymmetric(
- insertion: .move(edge: .bottom),
- removal: .opacity
-))
-```
-
-## Animating Transitions
-
-### With `withAnimation`
-
-```swift
-withAnimation(.spring(response: 0.4, dampingFraction: 0.7)) {
- showDetails.toggle()
-}
-```
-
-### Attached Animation (iOS 17+)
-
-```swift
-.transition(.slide.animation(.easeInOut(duration: 0.5)))
-```
-
-## Matched Geometry Effect
-
-Creates seamless "hero" animations between two distinct views by interpolating position, size, shape, and other geometric properties.
-
-### Implementation Steps
-
-#### 1. Define a Namespace
-
-```swift
-@Namespace var animationNamespace
-```
-
-#### 2. Assign Matching IDs
-
-```swift
-// Source view
-ThumbnailView()
- .matchedGeometryEffect(id: "hero", in: animationNamespace)
-
-// Destination view
-FullScreenView()
- .matchedGeometryEffect(id: "hero", in: animationNamespace)
-```
-
-#### 3. Toggle Views with Animation
-
-```swift
-struct HeroAnimationView: View {
- @Namespace var animationNamespace
- @State private var isExpanded = false
-
- var body: some View {
- VStack {
- if isExpanded {
- // Expanded state
- RoundedRectangle(cornerRadius: 20)
- .fill(.blue)
- .matchedGeometryEffect(id: "card", in: animationNamespace)
- .frame(width: 300, height: 400)
- } else {
- // Collapsed state
- RoundedRectangle(cornerRadius: 10)
- .fill(.blue)
- .matchedGeometryEffect(id: "card", in: animationNamespace)
- .frame(width: 100, height: 100)
- }
- }
- .onTapGesture {
- withAnimation(.spring(response: 0.4, dampingFraction: 0.75)) {
- isExpanded.toggle()
- }
- }
- }
-}
-```
-
-### Full Hero Animation Example
-
-```swift
-struct CardGridView: View {
- @Namespace var namespace
- @State private var selectedCard: Card?
-
- var body: some View {
- ZStack {
- // Grid of cards
- LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))]) {
- ForEach(cards) { card in
- if selectedCard?.id != card.id {
- CardThumbnail(card: card)
- .matchedGeometryEffect(id: card.id, in: namespace)
- .onTapGesture {
- withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
- selectedCard = card
- }
- }
- }
- }
- }
-
- // Expanded card overlay
- if let card = selectedCard {
- CardDetail(card: card)
- .matchedGeometryEffect(id: card.id, in: namespace)
- .zIndex(1)
- .onTapGesture {
- withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) {
- selectedCard = nil
- }
- }
- }
- }
- }
-}
-
-struct CardThumbnail: View {
- let card: Card
-
- var body: some View {
- VStack {
- Image(card.imageName)
- .resizable()
- .aspectRatio(contentMode: .fill)
- Text(card.title)
- .font(.caption)
- }
- .frame(width: 150, height: 200)
- .background(.ultraThinMaterial)
- .clipShape(RoundedRectangle(cornerRadius: 12))
- }
-}
-
-struct CardDetail: View {
- let card: Card
-
- var body: some View {
- VStack {
- Image(card.imageName)
- .resizable()
- .aspectRatio(contentMode: .fit)
- Text(card.title)
- .font(.title)
- Text(card.description)
- .font(.body)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .background(.ultraThinMaterial)
- .clipShape(RoundedRectangle(cornerRadius: 24))
- .padding()
- }
-}
-```
-
-### Matched Geometry Tips
-
-- Both views must exist in code during the transition (SwiftUI handles visibility)
-- Use explicit `withAnimation` for reliable animation triggering
-- The interpolation covers position, size, corner radius, and rotation
-- For complex views, match individual elements with different IDs
-- Use `.zIndex()` to control layering during transitions
-
-## Content Transitions (iOS 17+)
-
-Animate content changes within the same view:
-
-### Numeric Text
-
-```swift
-Text("\(count)")
- .contentTransition(.numericText()) // Animated number flip
-
-// With count direction
-Text("\(count)")
- .contentTransition(.numericText(countsDown: count < previousCount))
-```
-
-### Identity
-
-```swift
-Text(currentText)
- .contentTransition(.identity) // Crossfade between text
-```
-
-### Interpolate
-
-```swift
-Text(currentText)
- .contentTransition(.interpolate) // Morph between content
-```
-
-## Custom Transitions
-
-Create reusable custom transitions:
-
-```swift
-extension AnyTransition {
- static var slideAndFade: AnyTransition {
- .asymmetric(
- insertion: .move(edge: .trailing).combined(with: .opacity),
- removal: .move(edge: .leading).combined(with: .opacity)
- )
- }
-
- static var popIn: AnyTransition {
- .scale(scale: 0.8)
- .combined(with: .opacity)
- .animation(.spring(response: 0.3, dampingFraction: 0.6))
- }
-
- static func flyIn(from edge: Edge) -> AnyTransition {
- .asymmetric(
- insertion: .move(edge: edge).combined(with: .opacity),
- removal: .scale(scale: 0.9).combined(with: .opacity)
- )
- }
-}
-
-// Usage
-SomeView()
- .transition(.slideAndFade)
-
-OtherView()
- .transition(.flyIn(from: .bottom))
-```
-
-## View Modifier-Based Custom Transitions
-
-For more complex transitions, use view modifiers:
-
-```swift
-struct RotateAndScaleModifier: ViewModifier {
- let amount: Double
-
- func body(content: Content) -> some View {
- content
- .rotationEffect(.degrees(amount * 360))
- .scaleEffect(1 - amount)
- .opacity(1 - amount)
- }
-}
-
-extension AnyTransition {
- static var rotateAway: AnyTransition {
- .modifier(
- active: RotateAndScaleModifier(amount: 1),
- identity: RotateAndScaleModifier(amount: 0)
- )
- }
-}
-```
-
-## Navigation Transitions (iOS 16+)
-
-Customize navigation transitions:
-
-```swift
-NavigationStack {
- ContentView()
- .navigationDestination(for: Item.self) { item in
- DetailView(item: item)
- }
-}
-.navigationTransition(.slide) // iOS 18+
-
-// Or using matchedGeometryEffect with NavigationLink
-```
-
-## Best Practices
-
-1. **Always wrap state changes in `withAnimation`** for reliable transition animation
-2. **Use spring animations** for natural, iOS-native feel
-3. **Prefer `.spring(response: 0.35, dampingFraction: 0.8)`** as a starting point
-4. **Use asymmetric transitions** when removal should differ from insertion
-5. **Combine transitions** for richer visual effects
-6. **Leverage matchedGeometryEffect** for hero animations between screens
-7. **Test on device** - animations can feel different in simulator
-
-## Sources
-
-- Create with Swift - Matched Geometry Effect in SwiftUI (Feb 2024)
-- Apple Developer Documentation - Transitions
-- Sebastien Lato, "SwiftUI Animation Masterclass" (Dev.to, 2025)
diff --git a/.agents/skills/swiftui-expert-skill/SKILL.md b/.agents/skills/swiftui-expert-skill/SKILL.md
deleted file mode 100644
index ce1a826a..00000000
--- a/.agents/skills/swiftui-expert-skill/SKILL.md
+++ /dev/null
@@ -1,290 +0,0 @@
----
-name: swiftui-expert-skill
-description: Write, review, or improve SwiftUI code following best practices for state management, view composition, performance, modern APIs, Swift concurrency, and iOS 26+ Liquid Glass adoption. Use when building new SwiftUI features, refactoring existing views, reviewing code quality, or adopting modern SwiftUI patterns.
----
-
-# SwiftUI Expert Skill
-
-## Overview
-Use this skill to build, review, or improve SwiftUI features with correct state management, modern API usage, Swift concurrency best practices, optimal view composition, and iOS 26+ Liquid Glass styling. Prioritize native APIs, Apple design guidance, and performance-conscious patterns. This skill focuses on facts and best practices without enforcing specific architectural patterns.
-
-## Workflow Decision Tree
-
-### 1) Review existing SwiftUI code
-- Check property wrapper usage against the selection guide (see `references/state-management.md`)
-- Verify modern API usage (see `references/modern-apis.md`)
-- Verify view composition follows extraction rules (see `references/view-structure.md`)
-- Check performance patterns are applied (see `references/performance-patterns.md`)
-- Verify list patterns use stable identity (see `references/list-patterns.md`)
-- Check animation patterns for correctness (see `references/animation-basics.md`, `references/animation-transitions.md`)
-- Inspect Liquid Glass usage for correctness and consistency (see `references/liquid-glass.md`)
-- Validate iOS 26+ availability handling with sensible fallbacks
-
-### 2) Improve existing SwiftUI code
-- Audit state management for correct wrapper selection (prefer `@Observable` over `ObservableObject`)
-- Replace deprecated APIs with modern equivalents (see `references/modern-apis.md`)
-- Extract complex views into separate subviews (see `references/view-structure.md`)
-- Refactor hot paths to minimize redundant state updates (see `references/performance-patterns.md`)
-- Ensure ForEach uses stable identity (see `references/list-patterns.md`)
-- Improve animation patterns (use value parameter, proper transitions, see `references/animation-basics.md`, `references/animation-transitions.md`)
-- Suggest image downsampling when `UIImage(data:)` is used (as optional optimization, see `references/image-optimization.md`)
-- Adopt Liquid Glass only when explicitly requested by the user
-
-### 3) Implement new SwiftUI feature
-- Design data flow first: identify owned vs injected state (see `references/state-management.md`)
-- Use modern APIs (no deprecated modifiers or patterns, see `references/modern-apis.md`)
-- Use `@Observable` for shared state (with `@MainActor` if not using default actor isolation)
-- Structure views for optimal diffing (extract subviews early, keep views small, see `references/view-structure.md`)
-- Separate business logic into testable models (see `references/layout-best-practices.md`)
-- Use correct animation patterns (implicit vs explicit, transitions, see `references/animation-basics.md`, `references/animation-transitions.md`, `references/animation-advanced.md`)
-- Apply glass effects after layout/appearance modifiers (see `references/liquid-glass.md`)
-- Gate iOS 26+ features with `#available` and provide fallbacks
-
-## Core Guidelines
-
-### State Management
-- **Always prefer `@Observable` over `ObservableObject`** for new code
-- **Mark `@Observable` classes with `@MainActor`** unless using default actor isolation
-- **Always mark `@State` and `@StateObject` as `private`** (makes dependencies clear)
-- **Never declare passed values as `@State` or `@StateObject`** (they only accept initial values)
-- Use `@State` with `@Observable` classes (not `@StateObject`)
-- `@Binding` only when child needs to **modify** parent state
-- `@Bindable` for injected `@Observable` objects needing bindings
-- Use `let` for read-only values; `var` + `.onChange()` for reactive reads
-- Legacy: `@StateObject` for owned `ObservableObject`; `@ObservedObject` for injected
-- Nested `ObservableObject` doesn't work (pass nested objects directly); `@Observable` handles nesting fine
-
-### Modern APIs
-- Use `foregroundStyle()` instead of `foregroundColor()`
-- Use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`
-- Use `Tab` API instead of `tabItem()`
-- Use `Button` instead of `onTapGesture()` (unless need location/count)
-- Use `NavigationStack` instead of `NavigationView`
-- Use `navigationDestination(for:)` for type-safe navigation
-- Use two-parameter or no-parameter `onChange()` variant
-- Use `ImageRenderer` for rendering SwiftUI views
-- Use `.sheet(item:)` instead of `.sheet(isPresented:)` for model-based content
-- Sheets should own their actions and call `dismiss()` internally
-- Use `ScrollViewReader` for programmatic scrolling with stable IDs
-- Avoid `UIScreen.main.bounds` for sizing
-- Avoid `GeometryReader` when alternatives exist (e.g., `containerRelativeFrame()`)
-
-### Swift Best Practices
-- Use modern Text formatting (`.format` parameters, not `String(format:)`)
-- Use `localizedStandardContains()` for user-input filtering (not `contains()`)
-- Prefer static member lookup (`.blue` vs `Color.blue`)
-- Use `.task` modifier for automatic cancellation of async work
-- Use `.task(id:)` for value-dependent tasks
-
-### View Composition
-- **Prefer modifiers over conditional views** for state changes (maintains view identity)
-- Extract complex views into separate subviews for better readability and performance
-- Keep views small for optimal performance
-- Keep view `body` simple and pure (no side effects or complex logic)
-- Use `@ViewBuilder` functions only for small, simple sections
-- Prefer `@ViewBuilder let content: Content` over closure-based content properties
-- Separate business logic into testable models (not about enforcing architectures)
-- Action handlers should reference methods, not contain inline logic
-- Use relative layout over hard-coded constants
-- Views should work in any context (don't assume screen size or presentation style)
-
-### Performance
-- Pass only needed values to views (avoid large "config" or "context" objects)
-- Eliminate unnecessary dependencies to reduce update fan-out
-- Check for value changes before assigning state in hot paths
-- Avoid redundant state updates in `onReceive`, `onChange`, scroll handlers
-- Minimize work in frequently executed code paths
-- Use `LazyVStack`/`LazyHStack` for large lists
-- Use stable identity for `ForEach` (never `.indices` for dynamic content)
-- Ensure constant number of views per `ForEach` element
-- Avoid inline filtering in `ForEach` (prefilter and cache)
-- Avoid `AnyView` in list rows
-- Consider POD views for fast diffing (or wrap expensive views in POD parents)
-- Suggest image downsampling when `UIImage(data:)` is encountered (as optional optimization)
-- Avoid layout thrash (deep hierarchies, excessive `GeometryReader`)
-- Gate frequent geometry updates by thresholds
-- Use `Self._printChanges()` to debug unexpected view updates
-
-### Animations
-- Use `.animation(_:value:)` with value parameter (deprecated version without value is too broad)
-- Use `withAnimation` for event-driven animations (button taps, gestures)
-- Prefer transforms (`offset`, `scale`, `rotation`) over layout changes (`frame`) for performance
-- Transitions require animations outside the conditional structure
-- Custom `Animatable` implementations must have explicit `animatableData`
-- Use `.phaseAnimator` for multi-step sequences (iOS 17+)
-- Use `.keyframeAnimator` for precise timing control (iOS 17+)
-- Animation completion handlers need `.transaction(value:)` for reexecution
-- Implicit animations override explicit animations (later in view tree wins)
-
-### Liquid Glass (iOS 26+)
-**Only adopt when explicitly requested by the user.**
-- Use native `glassEffect`, `GlassEffectContainer`, and glass button styles
-- Wrap multiple glass elements in `GlassEffectContainer`
-- Apply `.glassEffect()` after layout and visual modifiers
-- Use `.interactive()` only for tappable/focusable elements
-- Use `glassEffectID` with `@Namespace` for morphing transitions
-
-## Quick Reference
-
-### Property Wrapper Selection (Modern)
-| Wrapper | Use When |
-|---------|----------|
-| `@State` | Internal view state (must be `private`), or owned `@Observable` class |
-| `@Binding` | Child modifies parent's state |
-| `@Bindable` | Injected `@Observable` needing bindings |
-| `let` | Read-only value from parent |
-| `var` | Read-only value watched via `.onChange()` |
-
-**Legacy (Pre-iOS 17):**
-| Wrapper | Use When |
-|---------|----------|
-| `@StateObject` | View owns an `ObservableObject` (use `@State` with `@Observable` instead) |
-| `@ObservedObject` | View receives an `ObservableObject` |
-
-### Modern API Replacements
-| Deprecated | Modern Alternative |
-|------------|-------------------|
-| `foregroundColor()` | `foregroundStyle()` |
-| `cornerRadius()` | `clipShape(.rect(cornerRadius:))` |
-| `tabItem()` | `Tab` API |
-| `onTapGesture()` | `Button` (unless need location/count) |
-| `NavigationView` | `NavigationStack` |
-| `onChange(of:) { value in }` | `onChange(of:) { old, new in }` or `onChange(of:) { }` |
-| `fontWeight(.bold)` | `bold()` |
-| `GeometryReader` | `containerRelativeFrame()` or `visualEffect()` |
-| `showsIndicators: false` | `.scrollIndicators(.hidden)` |
-| `String(format: "%.2f", value)` | `Text(value, format: .number.precision(.fractionLength(2)))` |
-| `string.contains(search)` | `string.localizedStandardContains(search)` (for user input) |
-
-### Liquid Glass Patterns
-```swift
-// Basic glass effect with fallback
-if #available(iOS 26, *) {
- content
- .padding()
- .glassEffect(.regular.interactive(), in: .rect(cornerRadius: 16))
-} else {
- content
- .padding()
- .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
-}
-
-// Grouped glass elements
-GlassEffectContainer(spacing: 24) {
- HStack(spacing: 24) {
- GlassButton1()
- GlassButton2()
- }
-}
-
-// Glass buttons
-Button("Confirm") { }
- .buttonStyle(.glassProminent)
-```
-
-## Review Checklist
-
-### State Management
-- [ ] Using `@Observable` instead of `ObservableObject` for new code
-- [ ] `@Observable` classes marked with `@MainActor` (if needed)
-- [ ] Using `@State` with `@Observable` classes (not `@StateObject`)
-- [ ] `@State` and `@StateObject` properties are `private`
-- [ ] Passed values NOT declared as `@State` or `@StateObject`
-- [ ] `@Binding` only where child modifies parent state
-- [ ] `@Bindable` for injected `@Observable` needing bindings
-- [ ] Nested `ObservableObject` avoided (or passed directly to child views)
-
-### Modern APIs (see `references/modern-apis.md`)
-- [ ] Using `foregroundStyle()` instead of `foregroundColor()`
-- [ ] Using `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`
-- [ ] Using `Tab` API instead of `tabItem()`
-- [ ] Using `Button` instead of `onTapGesture()` (unless need location/count)
-- [ ] Using `NavigationStack` instead of `NavigationView`
-- [ ] Avoiding `UIScreen.main.bounds`
-- [ ] Using alternatives to `GeometryReader` when possible
-- [ ] Button images include text labels for accessibility
-
-### Sheets & Navigation (see `references/sheet-navigation-patterns.md`)
-- [ ] Using `.sheet(item:)` for model-based sheets
-- [ ] Sheets own their actions and dismiss internally
-- [ ] Using `navigationDestination(for:)` for type-safe navigation
-
-### ScrollView (see `references/scroll-patterns.md`)
-- [ ] Using `ScrollViewReader` with stable IDs for programmatic scrolling
-- [ ] Using `.scrollIndicators(.hidden)` instead of initializer parameter
-
-### Text & Formatting (see `references/text-formatting.md`)
-- [ ] Using modern Text formatting (not `String(format:)`)
-- [ ] Using `localizedStandardContains()` for search filtering
-
-### View Structure (see `references/view-structure.md`)
-- [ ] Using modifiers instead of conditionals for state changes
-- [ ] Complex views extracted to separate subviews
-- [ ] Views kept small for performance
-- [ ] Container views use `@ViewBuilder let content: Content`
-
-### Performance (see `references/performance-patterns.md`)
-- [ ] View `body` kept simple and pure (no side effects)
-- [ ] Passing only needed values (not large config objects)
-- [ ] Eliminating unnecessary dependencies
-- [ ] State updates check for value changes before assigning
-- [ ] Hot paths minimize state updates
-- [ ] No object creation in `body`
-- [ ] Heavy computation moved out of `body`
-
-### List Patterns (see `references/list-patterns.md`)
-- [ ] ForEach uses stable identity (not `.indices`)
-- [ ] Constant number of views per ForEach element
-- [ ] No inline filtering in ForEach
-- [ ] No `AnyView` in list rows
-
-### Layout (see `references/layout-best-practices.md`)
-- [ ] Avoiding layout thrash (deep hierarchies, excessive GeometryReader)
-- [ ] Gating frequent geometry updates by thresholds
-- [ ] Business logic separated into testable models
-- [ ] Action handlers reference methods (not inline logic)
-- [ ] Using relative layout (not hard-coded constants)
-- [ ] Views work in any context (context-agnostic)
-
-### Animations (see `references/animation-basics.md`, `references/animation-transitions.md`, `references/animation-advanced.md`)
-- [ ] Using `.animation(_:value:)` with value parameter
-- [ ] Using `withAnimation` for event-driven animations
-- [ ] Transitions paired with animations outside conditional structure
-- [ ] Custom `Animatable` has explicit `animatableData` implementation
-- [ ] Preferring transforms over layout changes for animation performance
-- [ ] Phase animations for multi-step sequences (iOS 17+)
-- [ ] Keyframe animations for precise timing (iOS 17+)
-- [ ] Completion handlers use `.transaction(value:)` for reexecution
-
-### Liquid Glass (iOS 26+)
-- [ ] `#available(iOS 26, *)` with fallback for Liquid Glass
-- [ ] Multiple glass views wrapped in `GlassEffectContainer`
-- [ ] `.glassEffect()` applied after layout/appearance modifiers
-- [ ] `.interactive()` only on user-interactable elements
-- [ ] Shapes and tints consistent across related elements
-
-## References
-- `references/state-management.md` - Property wrappers and data flow (prefer `@Observable`)
-- `references/view-structure.md` - View composition, extraction, and container patterns
-- `references/performance-patterns.md` - Performance optimization techniques and anti-patterns
-- `references/list-patterns.md` - ForEach identity, stability, and list best practices
-- `references/layout-best-practices.md` - Layout patterns, context-agnostic views, and testability
-- `references/modern-apis.md` - Modern API usage and deprecated replacements
-- `references/animation-basics.md` - Core animation concepts, implicit/explicit animations, timing, performance
-- `references/animation-transitions.md` - Transitions, custom transitions, Animatable protocol
-- `references/animation-advanced.md` - Transactions, phase/keyframe animations (iOS 17+), completion handlers (iOS 17+)
-- `references/sheet-navigation-patterns.md` - Sheet presentation and navigation patterns
-- `references/scroll-patterns.md` - ScrollView patterns and programmatic scrolling
-- `references/text-formatting.md` - Modern text formatting and string operations
-- `references/image-optimization.md` - AsyncImage, image downsampling, and optimization
-- `references/liquid-glass.md` - iOS 26+ Liquid Glass API
-
-## Philosophy
-
-This skill focuses on **facts and best practices**, not architectural opinions:
-- We don't enforce specific architectures (e.g., MVVM, VIPER)
-- We do encourage separating business logic for testability
-- We prioritize modern APIs over deprecated ones
-- We emphasize thread safety with `@MainActor` and `@Observable`
-- We optimize for performance and maintainability
-- We follow Apple's Human Interface Guidelines and API design patterns
diff --git a/.agents/skills/swiftui-expert-skill/references/animation-advanced.md b/.agents/skills/swiftui-expert-skill/references/animation-advanced.md
deleted file mode 100644
index 6df634d3..00000000
--- a/.agents/skills/swiftui-expert-skill/references/animation-advanced.md
+++ /dev/null
@@ -1,351 +0,0 @@
-# SwiftUI Advanced Animations
-
-Transactions, phase animations (iOS 17+), keyframe animations (iOS 17+), and completion handlers (iOS 17+).
-
-## Table of Contents
-- [Transactions](#transactions)
-- [Phase Animations (iOS 17+)](#phase-animations-ios-17)
-- [Keyframe Animations (iOS 17+)](#keyframe-animations-ios-17)
-- [Animation Completion Handlers (iOS 17+)](#animation-completion-handlers-ios-17)
-
----
-
-## Transactions
-
-The underlying mechanism for all animations in SwiftUI.
-
-### Basic Usage
-
-```swift
-// withAnimation is shorthand for withTransaction
-withAnimation(.default) { flag.toggle() }
-
-// Equivalent explicit transaction
-var transaction = Transaction(animation: .default)
-withTransaction(transaction) { flag.toggle() }
-```
-
-### The .transaction Modifier
-
-```swift
-Rectangle()
- .frame(width: flag ? 100 : 50, height: 50)
- .transaction { t in
- t.animation = .default
- }
-```
-
-**Note:** This behaves like the deprecated `.animation(_:)` without value parameter - it animates on every state change.
-
-### Animation Precedence
-
-**Implicit animations override explicit animations** (later in view tree wins).
-
-```swift
-Button("Tap") {
- withAnimation(.linear) { flag.toggle() }
-}
-.animation(.bouncy, value: flag) // .bouncy wins!
-```
-
-### Disabling Animations
-
-```swift
-// Prevent implicit animations from overriding
-.transaction { t in
- t.disablesAnimations = true
-}
-
-// Remove animation entirely
-.transaction { $0.animation = nil }
-```
-
-### Custom Transaction Keys (iOS 17+)
-
-Pass metadata through transactions.
-
-```swift
-struct ChangeSourceKey: TransactionKey {
- static let defaultValue: String = "unknown"
-}
-
-extension Transaction {
- var changeSource: String {
- get { self[ChangeSourceKey.self] }
- set { self[ChangeSourceKey.self] = newValue }
- }
-}
-
-// Set source
-var transaction = Transaction(animation: .default)
-transaction.changeSource = "server"
-withTransaction(transaction) { flag.toggle() }
-
-// Read in view tree
-.transaction { t in
- if t.changeSource == "server" {
- t.animation = .smooth
- } else {
- t.animation = .bouncy
- }
-}
-```
-
----
-
-## Phase Animations (iOS 17+)
-
-Cycle through discrete phases automatically. Each phase change is a separate animation.
-
-### Basic Usage
-
-```swift
-// GOOD - triggered phase animation
-Button("Shake") { trigger += 1 }
- .phaseAnimator(
- [0.0, -10.0, 10.0, -5.0, 5.0, 0.0],
- trigger: trigger
- ) { content, offset in
- content.offset(x: offset)
- }
-
-// Infinite loop (no trigger)
-Circle()
- .phaseAnimator([1.0, 1.2, 1.0]) { content, scale in
- content.scaleEffect(scale)
- }
-```
-
-### Enum Phases (Recommended for Clarity)
-
-```swift
-// GOOD - enum phases are self-documenting
-enum BouncePhase: CaseIterable {
- case initial, up, down, settle
-
- var scale: CGFloat {
- switch self {
- case .initial: 1.0
- case .up: 1.2
- case .down: 0.9
- case .settle: 1.0
- }
- }
-}
-
-Circle()
- .phaseAnimator(BouncePhase.allCases, trigger: trigger) { content, phase in
- content.scaleEffect(phase.scale)
- }
-```
-
-### Custom Timing Per Phase
-
-```swift
-.phaseAnimator([0, -20, 20], trigger: trigger) { content, offset in
- content.offset(x: offset)
-} animation: { phase in
- switch phase {
- case -20: .bouncy
- case 20: .linear
- default: .smooth
- }
-}
-```
-
-### Good vs Bad
-
-```swift
-// GOOD - use phaseAnimator for multi-step sequences
-.phaseAnimator([0, -10, 10, 0], trigger: trigger) { content, offset in
- content.offset(x: offset)
-}
-
-// BAD - manual DispatchQueue sequencing
-Button("Animate") {
- withAnimation(.easeOut(duration: 0.1)) { offset = -10 }
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
- withAnimation { offset = 10 }
- }
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
- withAnimation { offset = 0 }
- }
-}
-```
-
----
-
-## Keyframe Animations (iOS 17+)
-
-Precise timing control with exact values at specific times.
-
-### Basic Usage
-
-```swift
-Button("Bounce") { trigger += 1 }
- .keyframeAnimator(
- initialValue: AnimationValues(),
- trigger: trigger
- ) { content, value in
- content
- .scaleEffect(value.scale)
- .offset(y: value.verticalOffset)
- } keyframes: { _ in
- KeyframeTrack(\.scale) {
- SpringKeyframe(1.2, duration: 0.15)
- SpringKeyframe(0.9, duration: 0.1)
- SpringKeyframe(1.0, duration: 0.15)
- }
- KeyframeTrack(\.verticalOffset) {
- LinearKeyframe(-20, duration: 0.15)
- LinearKeyframe(0, duration: 0.25)
- }
- }
-
-struct AnimationValues {
- var scale: CGFloat = 1.0
- var verticalOffset: CGFloat = 0
-}
-```
-
-### Keyframe Types
-
-| Type | Behavior |
-|------|----------|
-| `CubicKeyframe` | Smooth interpolation |
-| `LinearKeyframe` | Straight-line interpolation |
-| `SpringKeyframe` | Spring physics |
-| `MoveKeyframe` | Instant jump (no interpolation) |
-
-### Multiple Synchronized Tracks
-
-Tracks run **in parallel**, each animating one property.
-
-```swift
-// GOOD - bell shake with synchronized rotation and scale
-struct BellAnimation {
- var rotation: Double = 0
- var scale: CGFloat = 1.0
-}
-
-Image(systemName: "bell.fill")
- .keyframeAnimator(
- initialValue: BellAnimation(),
- trigger: trigger
- ) { content, value in
- content
- .rotationEffect(.degrees(value.rotation))
- .scaleEffect(value.scale)
- } keyframes: { _ in
- KeyframeTrack(\.rotation) {
- CubicKeyframe(15, duration: 0.1)
- CubicKeyframe(-15, duration: 0.1)
- CubicKeyframe(10, duration: 0.1)
- CubicKeyframe(-10, duration: 0.1)
- CubicKeyframe(0, duration: 0.1)
- }
- KeyframeTrack(\.scale) {
- CubicKeyframe(1.1, duration: 0.25)
- CubicKeyframe(1.0, duration: 0.25)
- }
- }
-
-// BAD - manual timer-based animation
-Image(systemName: "bell.fill")
- .onTapGesture {
- withAnimation(.easeOut(duration: 0.1)) { rotation = 15 }
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
- withAnimation { rotation = -15 }
- }
- // ... more manual timing - error prone
- }
-```
-
-### KeyframeTimeline (iOS 17+)
-
-Query animation values directly for testing or non-SwiftUI use.
-
-```swift
-let timeline = KeyframeTimeline(initialValue: AnimationValues()) {
- KeyframeTrack(\.scale) {
- CubicKeyframe(1.2, duration: 0.25)
- CubicKeyframe(1.0, duration: 0.25)
- }
-}
-
-let midpoint = timeline.value(time: 0.25)
-print(midpoint.scale) // Value at 0.25 seconds
-```
-
----
-
-## Animation Completion Handlers (iOS 17+)
-
-Execute code when animations finish.
-
-### With withAnimation
-
-```swift
-// GOOD - completion with withAnimation
-Button("Animate") {
- withAnimation(.spring) {
- isExpanded.toggle()
- } completion: {
- showNextStep = true
- }
-}
-```
-
-### With Transaction (For Reexecution)
-
-```swift
-// GOOD - completion fires on every trigger change
-Circle()
- .scaleEffect(bounceCount % 2 == 0 ? 1.0 : 1.2)
- .transaction(value: bounceCount) { transaction in
- transaction.animation = .spring
- transaction.addAnimationCompletion {
- message = "Bounce \(bounceCount) complete"
- }
- }
-
-// BAD - completion only fires ONCE (no value parameter)
-Circle()
- .scaleEffect(bounceCount % 2 == 0 ? 1.0 : 1.2)
- .animation(.spring, value: bounceCount)
- .transaction { transaction in // No value!
- transaction.addAnimationCompletion {
- completionCount += 1 // Only fires once, ever
- }
- }
-```
-
----
-
-## Quick Reference
-
-### Transactions (All iOS versions)
-- `withTransaction` is the explicit form of `withAnimation`
-- Implicit animations override explicit (later in view tree wins)
-- Use `disablesAnimations` to prevent override
-- Use `.transaction { $0.animation = nil }` to remove animation
-
-### Custom Transaction Keys (iOS 17+)
-- Pass metadata through animation system via `TransactionKey`
-
-### Phase Animations (iOS 17+)
-- Use for multi-step sequences returning to start
-- Prefer enum phases for clarity
-- Each phase change is a separate animation
-- Use `trigger` parameter for one-shot animations
-
-### Keyframe Animations (iOS 17+)
-- Use for precise timing control
-- Tracks run in parallel
-- Use `KeyframeTimeline` for testing/advanced use
-- Prefer over manual DispatchQueue timing
-
-### Completion Handlers (iOS 17+)
-- Use `withAnimation(.animation) { } completion: { }` for one-shot completion handlers
-- Use `.transaction(value:)` for handlers that should refire on every value change
-- Without `value:` parameter, completion only fires once
diff --git a/.agents/skills/swiftui-expert-skill/references/animation-basics.md b/.agents/skills/swiftui-expert-skill/references/animation-basics.md
deleted file mode 100644
index 859682a9..00000000
--- a/.agents/skills/swiftui-expert-skill/references/animation-basics.md
+++ /dev/null
@@ -1,284 +0,0 @@
-# SwiftUI Animation Basics
-
-Core animation concepts, implicit vs explicit animations, timing curves, and performance patterns.
-
-## Table of Contents
-- [Core Concepts](#core-concepts)
-- [Implicit Animations](#implicit-animations)
-- [Explicit Animations](#explicit-animations)
-- [Animation Placement](#animation-placement)
-- [Selective Animation](#selective-animation)
-- [Timing Curves](#timing-curves)
-- [Animation Performance](#animation-performance)
-- [Disabling Animations](#disabling-animations)
-- [Debugging](#debugging)
-
----
-
-## Core Concepts
-
-State changes trigger view updates. SwiftUI provides mechanisms to animate these changes.
-
-**Animation Process:**
-1. State change triggers view tree re-evaluation
-2. SwiftUI compares new tree to current render tree
-3. Animatable properties are identified and interpolated (~60 fps)
-
-**Key Characteristics:**
-- Animations are additive and cancelable
-- Always start from current render tree state
-- Blend smoothly when interrupted
-
----
-
-## Implicit Animations
-
-Use `.animation(_:value:)` to animate when a specific value changes.
-
-```swift
-// GOOD - uses value parameter
-Rectangle()
- .frame(width: isExpanded ? 200 : 100, height: 50)
- .animation(.spring, value: isExpanded)
- .onTapGesture { isExpanded.toggle() }
-
-// BAD - deprecated, animates all changes unexpectedly
-Rectangle()
- .frame(width: isExpanded ? 200 : 100, height: 50)
- .animation(.spring) // Deprecated!
-```
-
----
-
-## Explicit Animations
-
-Use `withAnimation` for event-driven state changes.
-
-```swift
-// GOOD - explicit animation
-Button("Toggle") {
- withAnimation(.spring) {
- isExpanded.toggle()
- }
-}
-
-// BAD - no animation context
-Button("Toggle") {
- isExpanded.toggle() // Abrupt change
-}
-```
-
-**When to use which:**
-- **Implicit**: Animations tied to specific value changes, precise view tree scope
-- **Explicit**: Event-driven animations (button taps, gestures)
-
----
-
-## Animation Placement
-
-Place animation modifiers after the properties they should animate.
-
-```swift
-// GOOD - animation after properties
-Rectangle()
- .frame(width: isExpanded ? 200 : 100, height: 50)
- .foregroundStyle(isExpanded ? .blue : .red)
- .animation(.default, value: isExpanded) // Animates both
-
-// BAD - animation before properties
-Rectangle()
- .animation(.default, value: isExpanded) // Too early!
- .frame(width: isExpanded ? 200 : 100, height: 50)
-```
-
----
-
-## Selective Animation
-
-Animate only specific properties using multiple animation modifiers or scoped animations.
-
-```swift
-// GOOD - selective animation
-Rectangle()
- .frame(width: isExpanded ? 200 : 100, height: 50)
- .animation(.spring, value: isExpanded) // Animate size
- .foregroundStyle(isExpanded ? .blue : .red)
- .animation(nil, value: isExpanded) // Don't animate color
-
-// iOS 17+ scoped animation
-Rectangle()
- .foregroundStyle(isExpanded ? .blue : .red) // Not animated
- .animation(.spring) {
- $0.frame(width: isExpanded ? 200 : 100, height: 50) // Animated
- }
-```
-
----
-
-## Timing Curves
-
-### Built-in Curves
-
-| Curve | Use Case |
-|-------|----------|
-| `.spring` | Interactive elements, most UI |
-| `.easeInOut` | Appearance changes |
-| `.bouncy` | Playful feedback (iOS 17+) |
-| `.linear` | Progress indicators only |
-
-### Modifiers
-
-```swift
-.animation(.default.speed(2.0), value: flag) // 2x faster
-.animation(.default.delay(0.5), value: flag) // Delayed start
-.animation(.default.repeatCount(3, autoreverses: true), value: flag)
-```
-
-### Good vs Bad Timing
-
-```swift
-// GOOD - appropriate timing for interaction type
-Button("Tap") {
- withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
- isActive.toggle()
- }
-}
-.scaleEffect(isActive ? 0.95 : 1.0)
-
-// BAD - too slow for button feedback
-Button("Tap") {
- withAnimation(.easeInOut(duration: 1.0)) { // Way too slow!
- isActive.toggle()
- }
-}
-
-// BAD - linear feels robotic
-Rectangle()
- .animation(.linear(duration: 0.5), value: isActive) // Mechanical
-```
-
----
-
-## Animation Performance
-
-### Prefer Transforms Over Layout
-
-```swift
-// GOOD - GPU accelerated transforms
-Rectangle()
- .frame(width: 100, height: 100)
- .scaleEffect(isActive ? 1.5 : 1.0) // Fast
- .offset(x: isActive ? 50 : 0) // Fast
- .rotationEffect(.degrees(isActive ? 45 : 0)) // Fast
- .animation(.spring, value: isActive)
-
-// BAD - layout changes are expensive
-Rectangle()
- .frame(width: isActive ? 150 : 100, height: isActive ? 150 : 100) // Expensive
- .padding(isActive ? 50 : 0) // Expensive
-```
-
-### Narrow Animation Scope
-
-```swift
-// GOOD - animation scoped to specific subview
-VStack {
- HeaderView() // Not affected
- ExpandableContent(isExpanded: isExpanded)
- .animation(.spring, value: isExpanded) // Only this
- FooterView() // Not affected
-}
-
-// BAD - animation at root
-VStack {
- HeaderView()
- ExpandableContent(isExpanded: isExpanded)
- FooterView()
-}
-.animation(.spring, value: isExpanded) // Animates everything
-```
-
-### Avoid Animation in Hot Paths
-
-```swift
-// GOOD - gate by threshold
-.onPreferenceChange(ScrollOffsetKey.self) { offset in
- let shouldShow = offset.y < -50
- if shouldShow != showTitle { // Only when crossing threshold
- withAnimation(.easeOut(duration: 0.2)) {
- showTitle = shouldShow
- }
- }
-}
-
-// BAD - animating every scroll change
-.onPreferenceChange(ScrollOffsetKey.self) { offset in
- withAnimation { // Fires constantly!
- self.offset = offset.y
- }
-}
-```
-
----
-
-## Disabling Animations
-
-```swift
-// GOOD - disable with transaction
-Text("Count: \(count)")
- .transaction { $0.animation = nil }
-
-// GOOD - disable from parent context
-DataView()
- .transaction { $0.disablesAnimations = true }
-
-// BAD - hacky zero duration
-Text("Count: \(count)")
- .animation(.linear(duration: 0), value: count) // Hacky
-```
-
----
-
-## Debugging
-
-```swift
-// Slow down for inspection
-#if DEBUG
-.animation(.linear(duration: 3.0).speed(0.2), value: isExpanded)
-#else
-.animation(.spring, value: isExpanded)
-#endif
-
-// Debug modifier to log values
-struct AnimationDebugModifier: ViewModifier, Animatable {
- var value: Double
- var animatableData: Double {
- get { value }
- set {
- value = newValue
- print("Animation: \(newValue)")
- }
- }
- func body(content: Content) -> some View {
- content.opacity(value)
- }
-}
-```
-
----
-
-## Quick Reference
-
-### Do
-- Use `.animation(_:value:)` with value parameter
-- Use `withAnimation` for event-driven animations
-- Prefer transforms over layout changes
-- Scope animations narrowly
-- Choose appropriate timing curves
-
-### Don't
-- Use deprecated `.animation(_:)` without value
-- Animate layout properties in hot paths
-- Apply broad animations at root level
-- Use linear timing for UI (feels robotic)
-- Animate on every frame in scroll handlers
diff --git a/.agents/skills/swiftui-expert-skill/references/animation-transitions.md b/.agents/skills/swiftui-expert-skill/references/animation-transitions.md
deleted file mode 100644
index 29b3f984..00000000
--- a/.agents/skills/swiftui-expert-skill/references/animation-transitions.md
+++ /dev/null
@@ -1,326 +0,0 @@
-# SwiftUI Transitions
-
-Transitions for view insertion/removal, custom transitions, and the Animatable protocol.
-
-## Table of Contents
-- [Property Animations vs Transitions](#property-animations-vs-transitions)
-- [Basic Transitions](#basic-transitions)
-- [Asymmetric Transitions](#asymmetric-transitions)
-- [Custom Transitions](#custom-transitions)
-- [Identity and Transitions](#identity-and-transitions)
-- [The Animatable Protocol](#the-animatable-protocol)
-
----
-
-## Property Animations vs Transitions
-
-**Property animations**: Interpolate values on views that exist before AND after state change.
-
-**Transitions**: Animate views being inserted or removed from the render tree.
-
-```swift
-// Property animation - same view, different properties
-Rectangle()
- .frame(width: isExpanded ? 200 : 100, height: 50)
- .animation(.spring, value: isExpanded)
-
-// Transition - view inserted/removed
-if showDetail {
- DetailView()
- .transition(.scale)
-}
-```
-
----
-
-## Basic Transitions
-
-### Critical: Transitions Require Animation Context
-
-```swift
-// GOOD - animation outside conditional
-VStack {
- Button("Toggle") { showDetail.toggle() }
- if showDetail {
- DetailView()
- .transition(.slide)
- }
-}
-.animation(.spring, value: showDetail)
-
-// GOOD - explicit animation
-Button("Toggle") {
- withAnimation(.spring) {
- showDetail.toggle()
- }
-}
-if showDetail {
- DetailView()
- .transition(.scale.combined(with: .opacity))
-}
-
-// BAD - animation inside conditional (removed with view!)
-if showDetail {
- DetailView()
- .transition(.slide)
- .animation(.spring, value: showDetail) // Won't work on removal!
-}
-
-// BAD - no animation context
-Button("Toggle") {
- showDetail.toggle() // No animation
-}
-if showDetail {
- DetailView()
- .transition(.slide) // Ignored - just appears/disappears
-}
-```
-
-### Built-in Transitions
-
-| Transition | Effect |
-|------------|--------|
-| `.opacity` | Fade in/out (default) |
-| `.scale` | Scale up/down |
-| `.slide` | Slide from leading edge |
-| `.move(edge:)` | Move from specific edge |
-| `.offset(x:y:)` | Move by offset amount |
-
-### Combining Transitions
-
-```swift
-// Parallel - both simultaneously
-.transition(.slide.combined(with: .opacity))
-
-// Chained
-.transition(.scale.combined(with: .opacity).combined(with: .offset(y: 20)))
-```
-
----
-
-## Asymmetric Transitions
-
-Different animations for insertion vs removal.
-
-```swift
-// GOOD - different animations for insert/remove
-if showCard {
- CardView()
- .transition(
- .asymmetric(
- insertion: .scale.combined(with: .opacity),
- removal: .move(edge: .bottom).combined(with: .opacity)
- )
- )
-}
-
-// BAD - same transition when different behaviors needed
-if showCard {
- CardView()
- .transition(.slide) // Same both ways - may feel awkward
-}
-```
-
----
-
-## Custom Transitions
-
-### Pre-iOS 17
-
-```swift
-struct BlurModifier: ViewModifier {
- var radius: CGFloat
- func body(content: Content) -> some View {
- content.blur(radius: radius)
- }
-}
-
-extension AnyTransition {
- static func blur(radius: CGFloat) -> AnyTransition {
- .modifier(
- active: BlurModifier(radius: radius),
- identity: BlurModifier(radius: 0)
- )
- }
-}
-
-// Usage
-.transition(.blur(radius: 10))
-```
-
-### iOS 17+ (Transition Protocol)
-
-```swift
-struct BlurTransition: Transition {
- var radius: CGFloat
-
- func body(content: Content, phase: TransitionPhase) -> some View {
- content
- .blur(radius: phase.isIdentity ? 0 : radius)
- .opacity(phase.isIdentity ? 1 : 0)
- }
-}
-
-// Usage
-.transition(BlurTransition(radius: 10))
-```
-
-### Good vs Bad Custom Transitions
-
-```swift
-// GOOD - reusable transition
-if showContent {
- ContentView()
- .transition(BlurTransition(radius: 10))
-}
-
-// BAD - inline logic (won't animate on removal!)
-if showContent {
- ContentView()
- .blur(radius: showContent ? 0 : 10) // Not a transition
- .opacity(showContent ? 1 : 0)
-}
-```
-
----
-
-## Identity and Transitions
-
-View identity changes trigger transitions, not property animations.
-
-```swift
-// Triggers transition - different branches have different identities
-if isExpanded {
- Rectangle().frame(width: 200, height: 50)
-} else {
- Rectangle().frame(width: 100, height: 50)
-}
-
-// Triggers transition - .id() changes identity
-Rectangle()
- .id(flag) // Different identity when flag changes
- .transition(.scale)
-
-// Property animation - same view, same identity
-Rectangle()
- .frame(width: isExpanded ? 200 : 100, height: 50)
- .animation(.spring, value: isExpanded)
-```
-
----
-
-## The Animatable Protocol
-
-Enables custom property interpolation during animations.
-
-### Protocol Definition
-
-```swift
-protocol Animatable {
- associatedtype AnimatableData: VectorArithmetic
- var animatableData: AnimatableData { get set }
-}
-```
-
-### Basic Implementation
-
-```swift
-// GOOD - explicit animatableData
-struct ShakeModifier: ViewModifier, Animatable {
- var shakeCount: Double
-
- var animatableData: Double {
- get { shakeCount }
- set { shakeCount = newValue }
- }
-
- func body(content: Content) -> some View {
- content.offset(x: sin(shakeCount * .pi * 2) * 10)
- }
-}
-
-extension View {
- func shake(count: Int) -> some View {
- modifier(ShakeModifier(shakeCount: Double(count)))
- }
-}
-
-// Usage
-Button("Shake") { shakeCount += 3 }
- .shake(count: shakeCount)
- .animation(.default, value: shakeCount)
-
-// BAD - missing animatableData (silent failure!)
-struct BadShakeModifier: ViewModifier {
- var shakeCount: Double
- // Missing animatableData! Uses EmptyAnimatableData
-
- func body(content: Content) -> some View {
- content.offset(x: sin(shakeCount * .pi * 2) * 10)
- }
-}
-// Animation jumps to final value instead of interpolating
-```
-
-### Multiple Properties with AnimatablePair
-
-```swift
-// GOOD - AnimatablePair for two properties
-struct ComplexModifier: ViewModifier, Animatable {
- var scale: CGFloat
- var rotation: Double
-
- var animatableData: AnimatablePair {
- get { AnimatablePair(scale, rotation) }
- set {
- scale = newValue.first
- rotation = newValue.second
- }
- }
-
- func body(content: Content) -> some View {
- content
- .scaleEffect(scale)
- .rotationEffect(.degrees(rotation))
- }
-}
-
-// GOOD - nested AnimatablePair for 3+ properties
-struct ThreePropertyModifier: ViewModifier, Animatable {
- var x: CGFloat
- var y: CGFloat
- var rotation: Double
-
- var animatableData: AnimatablePair, Double> {
- get { AnimatablePair(AnimatablePair(x, y), rotation) }
- set {
- x = newValue.first.first
- y = newValue.first.second
- rotation = newValue.second
- }
- }
-
- func body(content: Content) -> some View {
- content
- .offset(x: x, y: y)
- .rotationEffect(.degrees(rotation))
- }
-}
-```
-
----
-
-## Quick Reference
-
-### Do
-- Place transitions outside conditional structures
-- Use `withAnimation` or `.animation` outside the `if`
-- Implement `animatableData` explicitly for custom Animatable
-- Use `AnimatablePair` for multiple animated properties
-- Use asymmetric transitions when insert/remove need different effects
-
-### Don't
-- Put animation modifiers inside conditionals for transitions
-- Forget `animatableData` implementation (silent failure)
-- Use inline blur/opacity instead of proper transitions
-- Expect property animation when view identity changes
diff --git a/.agents/skills/swiftui-expert-skill/references/image-optimization.md b/.agents/skills/swiftui-expert-skill/references/image-optimization.md
deleted file mode 100644
index 4511776b..00000000
--- a/.agents/skills/swiftui-expert-skill/references/image-optimization.md
+++ /dev/null
@@ -1,286 +0,0 @@
-# SwiftUI Image Optimization Reference
-
-## AsyncImage Best Practices
-
-### Basic AsyncImage with Phase Handling
-
-```swift
-// Good - handles loading and error states
-AsyncImage(url: imageURL) { phase in
- switch phase {
- case .empty:
- ProgressView()
- case .success(let image):
- image
- .resizable()
- .aspectRatio(contentMode: .fit)
- case .failure:
- Image(systemName: "photo")
- .foregroundStyle(.secondary)
- @unknown default:
- EmptyView()
- }
-}
-.frame(width: 200, height: 200)
-```
-
-### AsyncImage with Custom Placeholder
-
-```swift
-struct ImageView: View {
- let url: URL?
-
- var body: some View {
- AsyncImage(url: url) { phase in
- switch phase {
- case .empty:
- ZStack {
- Color.gray.opacity(0.2)
- ProgressView()
- }
- case .success(let image):
- image
- .resizable()
- .aspectRatio(contentMode: .fill)
- case .failure:
- ZStack {
- Color.gray.opacity(0.2)
- Image(systemName: "exclamationmark.triangle")
- .foregroundStyle(.secondary)
- }
- @unknown default:
- EmptyView()
- }
- }
- .clipShape(.rect(cornerRadius: 12))
- }
-}
-```
-
-### AsyncImage with Transition
-
-```swift
-AsyncImage(url: imageURL) { phase in
- switch phase {
- case .empty:
- ProgressView()
- case .success(let image):
- image
- .resizable()
- .aspectRatio(contentMode: .fit)
- .transition(.opacity)
- case .failure:
- Image(systemName: "photo")
- @unknown default:
- EmptyView()
- }
-}
-.animation(.easeInOut, value: imageURL)
-```
-
-## Image Decoding and Downsampling (Optional Optimization)
-
-**When you encounter `UIImage(data:)` usage, consider suggesting image downsampling as a potential performance improvement**, especially for large images in lists or grids.
-
-### Current Pattern That Could Be Optimized
-
-```swift
-// Current pattern - decodes full image on main thread
-// Unsafe - force unwrap can crash if imageData is invalid
-Image(uiImage: UIImage(data: imageData)!)
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(width: 200, height: 200)
-```
-
-### Suggested Optimization Pattern
-
-```swift
-// Suggested optimization - decode and downsample off main thread
-struct OptimizedImageView: View {
- let imageData: Data
- let targetSize: CGSize
- @State private var processedImage: UIImage?
-
- var body: some View {
- Group {
- if let processedImage {
- Image(uiImage: processedImage)
- .resizable()
- .aspectRatio(contentMode: .fit)
- } else {
- ProgressView()
- }
- }
- .task {
- processedImage = await decodeAndDownsample(imageData, targetSize: targetSize)
- }
- }
-
- private func decodeAndDownsample(_ data: Data, targetSize: CGSize) async -> UIImage? {
- await Task.detached {
- guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
- return nil
- }
-
- let options: [CFString: Any] = [
- kCGImageSourceThumbnailMaxPixelSize: max(targetSize.width, targetSize.height),
- kCGImageSourceCreateThumbnailFromImageAlways: true,
- kCGImageSourceCreateThumbnailWithTransform: true
- ]
-
- guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
- return nil
- }
-
- return UIImage(cgImage: cgImage)
- }.value
- }
-}
-
-// Usage
-OptimizedImageView(
- imageData: imageData,
- targetSize: CGSize(width: 200, height: 200)
-)
-```
-
-### Reusable Image Downsampling Helper
-
-```swift
-actor ImageProcessor {
- func downsample(data: Data, to targetSize: CGSize) -> UIImage? {
- guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
- return nil
- }
-
- let maxDimension = max(targetSize.width, targetSize.height) * UIScreen.main.scale
-
- let options: [CFString: Any] = [
- kCGImageSourceThumbnailMaxPixelSize: maxDimension,
- kCGImageSourceCreateThumbnailFromImageAlways: true,
- kCGImageSourceCreateThumbnailWithTransform: true,
- kCGImageSourceShouldCache: false
- ]
-
- guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
- return nil
- }
-
- return UIImage(cgImage: cgImage)
- }
-}
-
-// Usage in view
-struct ImageView: View {
- let imageData: Data
- let targetSize: CGSize
- @State private var image: UIImage?
-
- private let processor = ImageProcessor()
-
- var body: some View {
- Group {
- if let image {
- Image(uiImage: image)
- .resizable()
- .aspectRatio(contentMode: .fit)
- } else {
- ProgressView()
- }
- }
- .task {
- image = await processor.downsample(data: imageData, to: targetSize)
- }
- }
-}
-```
-
-### When to Suggest This Optimization
-
-Mention this optimization when you see `UIImage(data:)` usage, particularly in:
-- Scrollable content (List, ScrollView with LazyVStack/LazyHStack)
-- Grid layouts with many images
-- Image galleries or carousels
-- Any scenario where large images are displayed at smaller sizes
-
-**Don't automatically apply it**—present it as an optional improvement for performance-sensitive scenarios.
-
-## SF Symbols
-
-### Using SF Symbols
-
-```swift
-// Basic symbol
-Image(systemName: "star.fill")
- .foregroundStyle(.yellow)
-
-// With rendering mode
-Image(systemName: "heart.fill")
- .symbolRenderingMode(.multicolor)
-
-// With variable color
-Image(systemName: "speaker.wave.3.fill")
- .symbolRenderingMode(.hierarchical)
- .foregroundStyle(.blue)
-
-// Animated symbols (iOS 17+)
-Image(systemName: "antenna.radiowaves.left.and.right")
- .symbolEffect(.variableColor)
-```
-
-### SF Symbol Variants
-
-```swift
-// Circle variant
-Image(systemName: "star.circle.fill")
-
-// Square variant
-Image(systemName: "star.square.fill")
-
-// With badge
-Image(systemName: "folder.badge.plus")
-```
-
-## Image Rendering
-
-### ImageRenderer for Snapshots
-
-```swift
-// Render SwiftUI view to UIImage
-let renderer = ImageRenderer(content: myView)
-renderer.scale = UIScreen.main.scale
-
-if let uiImage = renderer.uiImage {
- // Use the image (save, share, etc.)
-}
-
-// Render to CGImage
-if let cgImage = renderer.cgImage {
- // Use CGImage
-}
-```
-
-### Rendering with Custom Size
-
-```swift
-let renderer = ImageRenderer(content: myView)
-renderer.proposedSize = ProposedViewSize(width: 400, height: 300)
-
-if let uiImage = renderer.uiImage {
- // Image rendered at 400x300 points
-}
-```
-
-## Summary Checklist
-
-- [ ] Use `AsyncImage` with proper phase handling
-- [ ] Handle empty, success, and failure states
-- [ ] Consider downsampling for `UIImage(data:)` in performance-sensitive scenarios
-- [ ] Decode and downsample images off the main thread
-- [ ] Use appropriate target sizes for downsampling
-- [ ] Consider image caching for frequently accessed images
-- [ ] Use SF Symbols with appropriate rendering modes
-- [ ] Use `ImageRenderer` for rendering SwiftUI views to images
-
-**Performance Note**: Image downsampling is an optional optimization. Only suggest it when you encounter `UIImage(data:)` usage in performance-sensitive contexts like scrollable lists or grids.
diff --git a/.agents/skills/swiftui-expert-skill/references/layout-best-practices.md b/.agents/skills/swiftui-expert-skill/references/layout-best-practices.md
deleted file mode 100644
index 57c8f744..00000000
--- a/.agents/skills/swiftui-expert-skill/references/layout-best-practices.md
+++ /dev/null
@@ -1,312 +0,0 @@
-# SwiftUI Layout Best Practices Reference
-
-## Relative Layout Over Constants
-
-**Use dynamic layout calculations instead of hard-coded values.**
-
-```swift
-// Good - relative to actual layout
-GeometryReader { geometry in
- VStack {
- HeaderView()
- .frame(height: geometry.size.height * 0.2)
- ContentView()
- }
-}
-
-// Avoid - magic numbers that don't adapt
-VStack {
- HeaderView()
- .frame(height: 150) // Doesn't adapt to different screens
- ContentView()
-}
-```
-
-**Why**: Hard-coded values don't account for different screen sizes, orientations, or dynamic content (like status bars during phone calls).
-
-## Context-Agnostic Views
-
-**Views should work in any context.** Never assume presentation style or screen size.
-
-```swift
-// Good - adapts to given space
-struct ProfileCard: View {
- let user: User
-
- var body: some View {
- VStack {
- Image(user.avatar)
- .resizable()
- .aspectRatio(contentMode: .fit)
- Text(user.name)
- Spacer()
- }
- .padding()
- }
-}
-
-// Avoid - assumes full screen
-struct ProfileCard: View {
- let user: User
-
- var body: some View {
- VStack {
- Image(user.avatar)
- .frame(width: UIScreen.main.bounds.width) // Wrong!
- Text(user.name)
- }
- }
-}
-```
-
-**Why**: Views should work as full screens, modals, sheets, popovers, or embedded content.
-
-## Own Your Container
-
-**Custom views should own static containers but not lazy/repeatable ones.**
-
-```swift
-// Good - owns static container
-struct HeaderView: View {
- var body: some View {
- HStack {
- Image(systemName: "star")
- Text("Title")
- Spacer()
- }
- }
-}
-
-// Avoid - missing container
-struct HeaderView: View {
- var body: some View {
- Image(systemName: "star")
- Text("Title")
- // Caller must wrap in HStack
- }
-}
-
-// Good - caller owns lazy container
-struct FeedView: View {
- let items: [Item]
-
- var body: some View {
- LazyVStack {
- ForEach(items) { item in
- ItemRow(item: item)
- }
- }
- }
-}
-```
-
-## Layout Performance
-
-### Avoid Layout Thrash
-
-**Minimize deep view hierarchies and excessive layout dependencies.**
-
-```swift
-// Bad - deep nesting, excessive layout passes
-VStack {
- HStack {
- VStack {
- HStack {
- VStack {
- Text("Deep")
- }
- }
- }
- }
-}
-
-// Good - flatter hierarchy
-VStack {
- Text("Shallow")
- Text("Structure")
-}
-```
-
-**Avoid excessive `GeometryReader` and preference chains:**
-
-```swift
-// Bad - multiple geometry readers cause layout thrash
-GeometryReader { outerGeometry in
- VStack {
- GeometryReader { innerGeometry in
- // Layout recalculates multiple times
- }
- }
-}
-
-// Good - single geometry reader or use alternatives (iOS 17+)
-containerRelativeFrame(.horizontal) { width, _ in
- width * 0.8
-}
-```
-
-**Gate frequent geometry updates:**
-
-```swift
-// Bad - updates on every pixel change
-.onPreferenceChange(ViewSizeKey.self) { size in
- currentSize = size
-}
-
-// Good - gate by threshold
-.onPreferenceChange(ViewSizeKey.self) { size in
- let difference = abs(size.width - currentSize.width)
- if difference > 10 { // Only update if significant change
- currentSize = size
- }
-}
-```
-
-## View Logic and Testability
-
-### Separate View Logic from Views
-
-**Place view logic into view models or similar, so it can be tested.**
-
-> **iOS 17+**: Use `@Observable` macro with `@State` for view models.
-
-```swift
-// Good - logic in testable model (iOS 17+)
-@Observable
-@MainActor
-final class LoginViewModel {
- var email = ""
- var password = ""
- var isValid: Bool {
- !email.isEmpty && password.count >= 8
- }
-
- func login() async throws {
- // Business logic here
- }
-}
-
-struct LoginView: View {
- @State private var viewModel = LoginViewModel()
-
- var body: some View {
- Form {
- TextField("Email", text: $viewModel.email)
- SecureField("Password", text: $viewModel.password)
- Button("Login") {
- Task {
- try? await viewModel.login()
- }
- }
- .disabled(!viewModel.isValid)
- }
- }
-}
-```
-
-> **iOS 16 and earlier**: Use `ObservableObject` protocol with `@StateObject`.
-
-```swift
-// Good - logic in testable model (iOS 16 and earlier)
-@MainActor
-final class LoginViewModel: ObservableObject {
- @Published var email = ""
- @Published var password = ""
- var isValid: Bool {
- !email.isEmpty && password.count >= 8
- }
-
- func login() async throws {
- // Business logic here
- }
-}
-
-struct LoginView: View {
- @StateObject private var viewModel = LoginViewModel()
-
- var body: some View {
- Form {
- TextField("Email", text: $viewModel.email)
- SecureField("Password", text: $viewModel.password)
- Button("Login") {
- Task {
- try? await viewModel.login()
- }
- }
- .disabled(!viewModel.isValid)
- }
- }
-}
-```
-
-```swift
-// Bad - logic embedded in view
-struct LoginView: View {
- @State private var email = ""
- @State private var password = ""
-
- var body: some View {
- Form {
- TextField("Email", text: $email)
- SecureField("Password", text: $password)
- Button("Login") {
- // Business logic directly in view - hard to test
- Task {
- if !email.isEmpty && password.count >= 8 {
- // Login logic...
- }
- }
- }
- }
- }
-}
-```
-
-**Note**: This is about separating business logic for testability, not about enforcing specific architectures like MVVM. The goal is to make logic testable while keeping views simple.
-
-## Action Handlers
-
-**Separate layout from logic.** View body should reference action methods, not contain logic.
-
-```swift
-// Good - action references method
-struct PublishView: View {
- @State private var viewModel = PublishViewModel()
-
- var body: some View {
- Button("Publish Project", action: viewModel.handlePublish)
- }
-}
-
-// Avoid - logic in closure
-struct PublishView: View {
- @State private var isLoading = false
- @State private var showError = false
-
- var body: some View {
- Button("Publish Project") {
- isLoading = true
- apiService.publish(project) { result in
- if case .error = result {
- showError = true
- }
- isLoading = false
- }
- }
- }
-}
-```
-
-**Why**: Separating logic from layout improves readability, testability, and maintainability.
-
-## Summary Checklist
-
-- [ ] Use relative layout over hard-coded constants
-- [ ] Views work in any context (don't assume screen size)
-- [ ] Custom views own static containers
-- [ ] Avoid deep view hierarchies (layout thrash)
-- [ ] Gate frequent geometry updates by thresholds
-- [ ] View logic separated into testable models/classes
-- [ ] Action handlers reference methods, not inline logic
-- [ ] Avoid excessive `GeometryReader` usage
-- [ ] Use `containerRelativeFrame()` when appropriate
diff --git a/.agents/skills/swiftui-expert-skill/references/liquid-glass.md b/.agents/skills/swiftui-expert-skill/references/liquid-glass.md
deleted file mode 100644
index fb86c525..00000000
--- a/.agents/skills/swiftui-expert-skill/references/liquid-glass.md
+++ /dev/null
@@ -1,377 +0,0 @@
-# SwiftUI Liquid Glass Reference (iOS 26+)
-
-## Overview
-
-Liquid Glass is Apple's new design language introduced in iOS 26. It provides translucent, dynamic surfaces that respond to content and user interaction. This reference covers the native SwiftUI APIs for implementing Liquid Glass effects.
-
-## Availability
-
-All Liquid Glass APIs require iOS 26 or later. Always provide fallbacks:
-
-```swift
-if #available(iOS 26, *) {
- // Liquid Glass implementation
-} else {
- // Fallback using materials
-}
-```
-
-## Core APIs
-
-### glassEffect Modifier
-
-The primary modifier for applying glass effects to views:
-
-```swift
-.glassEffect(_ style: GlassEffectStyle = .regular, in shape: some Shape = .rect)
-```
-
-#### Basic Usage
-
-```swift
-Text("Hello")
- .padding()
- .glassEffect() // Default regular style, rect shape
-```
-
-#### With Shape
-
-```swift
-Text("Rounded Glass")
- .padding()
- .glassEffect(in: .rect(cornerRadius: 16))
-
-Image(systemName: "star")
- .padding()
- .glassEffect(in: .circle)
-
-Text("Capsule")
- .padding(.horizontal, 20)
- .padding(.vertical, 10)
- .glassEffect(in: .capsule)
-```
-
-### GlassEffectStyle
-
-#### Prominence Levels
-
-```swift
-.glassEffect(.regular) // Standard glass appearance
-.glassEffect(.prominent) // More visible, higher contrast
-```
-
-#### Tinting
-
-Add color tint to the glass:
-
-```swift
-.glassEffect(.regular.tint(.blue))
-.glassEffect(.prominent.tint(.red.opacity(0.3)))
-```
-
-#### Interactivity
-
-Make glass respond to touch/pointer hover:
-
-```swift
-// Interactive glass - responds to user interaction
-.glassEffect(.regular.interactive())
-
-// Combined with tint
-.glassEffect(.regular.tint(.blue).interactive())
-```
-
-**Important**: Only use `.interactive()` on elements that actually respond to user input (buttons, tappable views, focusable elements).
-
-## GlassEffectContainer
-
-Wraps multiple glass elements for proper visual grouping and spacing:
-
-```swift
-GlassEffectContainer {
- HStack {
- Button("One") { }
- .glassEffect()
- Button("Two") { }
- .glassEffect()
- }
-}
-```
-
-### With Spacing
-
-Control the visual spacing between glass elements:
-
-```swift
-GlassEffectContainer(spacing: 24) {
- HStack(spacing: 24) {
- GlassChip(icon: "pencil")
- GlassChip(icon: "eraser")
- GlassChip(icon: "trash")
- }
-}
-```
-
-**Note**: The container's `spacing` parameter should match the actual spacing in your layout for proper glass effect rendering.
-
-## Glass Button Styles
-
-Built-in button styles for glass appearance:
-
-```swift
-// Standard glass button
-Button("Action") { }
- .buttonStyle(.glass)
-
-// Prominent glass button (higher visibility)
-Button("Primary Action") { }
- .buttonStyle(.glassProminent)
-```
-
-### Custom Glass Buttons
-
-For more control, apply glass effect manually:
-
-```swift
-Button(action: { }) {
- Label("Settings", systemImage: "gear")
- .padding()
-}
-.glassEffect(.regular.interactive(), in: .capsule)
-```
-
-## Morphing Transitions
-
-Create smooth transitions between glass elements using `glassEffectID` and `@Namespace`:
-
-```swift
-struct MorphingExample: View {
- @Namespace private var animation
- @State private var isExpanded = false
-
- var body: some View {
- GlassEffectContainer {
- if isExpanded {
- ExpandedCard()
- .glassEffect()
- .glassEffectID("card", in: animation)
- } else {
- CompactCard()
- .glassEffect()
- .glassEffectID("card", in: animation)
- }
- }
- .animation(.smooth, value: isExpanded)
- }
-}
-```
-
-### Requirements for Morphing
-
-1. Both views must have the same `glassEffectID`
-2. Use the same `@Namespace`
-3. Wrap in `GlassEffectContainer`
-4. Apply animation to the container or parent
-
-## Modifier Order
-
-**Critical**: Apply `glassEffect` after layout and visual modifiers:
-
-```swift
-// CORRECT order
-Text("Label")
- .font(.headline) // 1. Typography
- .foregroundStyle(.primary) // 2. Color
- .padding() // 3. Layout
- .glassEffect() // 4. Glass effect LAST
-
-// WRONG order - glass applied too early
-Text("Label")
- .glassEffect() // Wrong position
- .padding()
- .font(.headline)
-```
-
-## Complete Examples
-
-### Toolbar with Glass Buttons
-
-```swift
-struct GlassToolbar: View {
- var body: some View {
- if #available(iOS 26, *) {
- GlassEffectContainer(spacing: 16) {
- HStack(spacing: 16) {
- ToolbarButton(icon: "pencil", action: { })
- ToolbarButton(icon: "eraser", action: { })
- ToolbarButton(icon: "scissors", action: { })
- Spacer()
- ToolbarButton(icon: "square.and.arrow.up", action: { })
- }
- .padding(.horizontal)
- }
- } else {
- // Fallback toolbar
- HStack(spacing: 16) {
- // ... fallback implementation
- }
- }
- }
-}
-
-struct ToolbarButton: View {
- let icon: String
- let action: () -> Void
-
- var body: some View {
- Button(action: action) {
- Image(systemName: icon)
- .font(.title2)
- .frame(width: 44, height: 44)
- }
- .glassEffect(.regular.interactive(), in: .circle)
- }
-}
-```
-
-### Card with Glass Effect
-
-```swift
-struct GlassCard: View {
- let title: String
- let subtitle: String
-
- var body: some View {
- if #available(iOS 26, *) {
- cardContent
- .glassEffect(.regular, in: .rect(cornerRadius: 20))
- } else {
- cardContent
- .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
- }
- }
-
- private var cardContent: some View {
- VStack(alignment: .leading, spacing: 8) {
- Text(title)
- .font(.headline)
- Text(subtitle)
- .font(.subheadline)
- .foregroundStyle(.secondary)
- }
- .padding()
- .frame(maxWidth: .infinity, alignment: .leading)
- }
-}
-```
-
-### Segmented Control
-
-```swift
-struct GlassSegmentedControl: View {
- @Binding var selection: Int
- let options: [String]
- @Namespace private var animation
-
- var body: some View {
- if #available(iOS 26, *) {
- GlassEffectContainer(spacing: 4) {
- HStack(spacing: 4) {
- ForEach(options.indices, id: \.self) { index in
- Button(options[index]) {
- withAnimation(.smooth) {
- selection = index
- }
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 8)
- .glassEffect(
- selection == index ? .prominent.interactive() : .regular.interactive(),
- in: .capsule
- )
- .glassEffectID(selection == index ? "selected" : "option\(index)", in: animation)
- }
- }
- .padding(4)
- }
- } else {
- Picker("Options", selection: $selection) {
- ForEach(options.indices, id: \.self) { index in
- Text(options[index]).tag(index)
- }
- }
- .pickerStyle(.segmented)
- }
- }
-}
-```
-
-## Fallback Strategies
-
-### Using Materials
-
-```swift
-if #available(iOS 26, *) {
- content.glassEffect()
-} else {
- content.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
-}
-```
-
-### Available Materials for Fallback
-
-- `.ultraThinMaterial` - Closest to glass appearance
-- `.thinMaterial` - Slightly more opaque
-- `.regularMaterial` - Standard blur
-- `.thickMaterial` - More opaque
-- `.ultraThickMaterial` - Most opaque
-
-### Conditional Modifier Extension
-
-```swift
-extension View {
- @ViewBuilder
- func glassEffectWithFallback(
- _ style: GlassEffectStyle = .regular,
- in shape: some Shape = .rect,
- fallbackMaterial: Material = .ultraThinMaterial
- ) -> some View {
- if #available(iOS 26, *) {
- self.glassEffect(style, in: shape)
- } else {
- self.background(fallbackMaterial, in: shape)
- }
- }
-}
-```
-
-## Best Practices
-
-### Do
-
-- Use `GlassEffectContainer` for grouped glass elements
-- Apply glass after layout modifiers
-- Use `.interactive()` only on tappable elements
-- Match container spacing with layout spacing
-- Provide material-based fallbacks for older iOS
-- Keep glass shapes consistent within a feature
-
-### Don't
-
-- Apply glass to every element (use sparingly)
-- Use `.interactive()` on static content
-- Mix different corner radii arbitrarily
-- Forget iOS version checks
-- Apply glass before padding/frame modifiers
-- Nest `GlassEffectContainer` unnecessarily
-
-## Checklist
-
-- [ ] `#available(iOS 26, *)` with fallback
-- [ ] `GlassEffectContainer` wraps grouped elements
-- [ ] `.glassEffect()` applied after layout modifiers
-- [ ] `.interactive()` only on user-interactable elements
-- [ ] `glassEffectID` with `@Namespace` for morphing
-- [ ] Consistent shapes and spacing across feature
-- [ ] Container spacing matches layout spacing
-- [ ] Appropriate prominence levels used
diff --git a/.agents/skills/swiftui-expert-skill/references/list-patterns.md b/.agents/skills/swiftui-expert-skill/references/list-patterns.md
deleted file mode 100644
index ef384a29..00000000
--- a/.agents/skills/swiftui-expert-skill/references/list-patterns.md
+++ /dev/null
@@ -1,153 +0,0 @@
-# SwiftUI List Patterns Reference
-
-## ForEach Identity and Stability
-
-**Always provide stable identity for `ForEach`.** Never use `.indices` for dynamic content.
-
-```swift
-// Good - stable identity via Identifiable
-extension User: Identifiable {
- var id: String { userId }
-}
-
-ForEach(users) { user in
- UserRow(user: user)
-}
-
-// Good - stable identity via keypath
-ForEach(users, id: \.userId) { user in
- UserRow(user: user)
-}
-
-// Wrong - indices create static content
-ForEach(users.indices, id: \.self) { index in
- UserRow(user: users[index]) // Can crash on removal!
-}
-
-// Wrong - unstable identity
-ForEach(users, id: \.self) { user in
- UserRow(user: user) // Only works if User is Hashable and stable
-}
-```
-
-**Critical**: Ensure **constant number of views per element** in `ForEach`:
-
-```swift
-// Good - consistent view count
-ForEach(items) { item in
- ItemRow(item: item)
-}
-
-// Bad - variable view count breaks identity
-ForEach(items) { item in
- if item.isSpecial {
- SpecialRow(item: item)
- DetailRow(item: item)
- } else {
- RegularRow(item: item)
- }
-}
-```
-
-**Avoid inline filtering:**
-
-```swift
-// Bad - unstable identity, changes on every update
-ForEach(items.filter { $0.isEnabled }) { item in
- ItemRow(item: item)
-}
-
-// Good - prefilter and cache
-@State private var enabledItems: [Item] = []
-
-var body: some View {
- ForEach(enabledItems) { item in
- ItemRow(item: item)
- }
- .onChange(of: items) { _, newItems in
- enabledItems = newItems.filter { $0.isEnabled }
- }
-}
-```
-
-**Avoid `AnyView` in list rows:**
-
-```swift
-// Bad - hides identity, increases cost
-ForEach(items) { item in
- AnyView(item.isSpecial ? SpecialRow(item: item) : RegularRow(item: item))
-}
-
-// Good - Create a unified row view
-ForEach(items) { item in
- ItemRow(item: item)
-}
-
-struct ItemRow: View {
- let item: Item
-
- var body: some View {
- if item.isSpecial {
- SpecialRow(item: item)
- } else {
- RegularRow(item: item)
- }
- }
-}
-```
-
-**Why**: Stable identity is critical for performance and animations. Unstable identity causes excessive diffing, broken animations, and potential crashes.
-
-## Enumerated Sequences
-
-**Always convert enumerated sequences to arrays. To be able to use them in a ForEach.**
-
-```swift
-let items = ["A", "B", "C"]
-
-// Correct
-ForEach(Array(items.enumerated()), id: \.offset) { index, item in
- Text("\(index): \(item)")
-}
-
-// Wrong - Doesn't compile, enumerated() isn't an array
-ForEach(items.enumerated(), id: \.offset) { index, item in
- Text("\(index): \(item)")
-}
-```
-
-## List with Custom Styling
-
-```swift
-// Remove default background and separators
-List(items) { item in
- ItemRow(item: item)
- .listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
- .listRowSeparator(.hidden)
-}
-.listStyle(.plain)
-.scrollContentBackground(.hidden)
-.background(Color.customBackground)
-.environment(\.defaultMinListRowHeight, 1) // Allows custom row heights
-```
-
-## List with Pull-to-Refresh
-
-```swift
-List(items) { item in
- ItemRow(item: item)
-}
-.refreshable {
- await loadItems()
-}
-```
-
-## Summary Checklist
-
-- [ ] ForEach uses stable identity (never `.indices` for dynamic content)
-- [ ] Constant number of views per ForEach element
-- [ ] No inline filtering in ForEach (prefilter and cache instead)
-- [ ] No `AnyView` in list rows
-- [ ] Don't convert enumerated sequences to arrays
-- [ ] Use `.refreshable` for pull-to-refresh
-- [ ] Custom list styling uses appropriate modifiers
diff --git a/.agents/skills/swiftui-expert-skill/references/modern-apis.md b/.agents/skills/swiftui-expert-skill/references/modern-apis.md
deleted file mode 100644
index 20a54572..00000000
--- a/.agents/skills/swiftui-expert-skill/references/modern-apis.md
+++ /dev/null
@@ -1,400 +0,0 @@
-# Modern SwiftUI APIs Reference
-
-## Overview
-
-This reference covers modern SwiftUI API usage patterns and deprecated API replacements. Always use the latest APIs to ensure forward compatibility and access to new features.
-
-## Styling and Appearance
-
-### foregroundStyle() vs foregroundColor()
-
-**Always use `foregroundStyle()` instead of `foregroundColor()`.**
-
-```swift
-// Modern (Correct)
-Text("Hello")
- .foregroundStyle(.primary)
-
-Image(systemName: "star")
- .foregroundStyle(.blue)
-
-// Legacy (Avoid)
-Text("Hello")
- .foregroundColor(.primary)
-```
-
-**Why**: `foregroundStyle()` supports hierarchical styles, gradients, and materials, making it more flexible and future-proof.
-
-### clipShape() vs cornerRadius()
-
-**Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`.**
-
-```swift
-// Modern (Correct)
-Image("photo")
- .clipShape(.rect(cornerRadius: 12))
-
-VStack {
- // content
-}
-.clipShape(.rect(cornerRadius: 16))
-
-// Legacy (Avoid)
-Image("photo")
- .cornerRadius(12)
-```
-
-**Why**: `cornerRadius()` is deprecated. `clipShape()` is more explicit and supports all shape types.
-
-### fontWeight() vs bold()
-
-**Don't apply `fontWeight()` unless there's a good reason. Always use `bold()` for bold text.**
-
-```swift
-// Correct
-Text("Important")
- .bold()
-
-// Avoid (unless you need a specific weight)
-Text("Important")
- .fontWeight(.bold)
-
-// Acceptable (specific weight needed)
-Text("Semibold")
- .fontWeight(.semibold)
-```
-
-## Navigation
-
-### NavigationStack vs NavigationView
-
-**Always use `NavigationStack` instead of `NavigationView`.**
-
-```swift
-// Modern (Correct)
-NavigationStack {
- List(items) { item in
- NavigationLink(value: item) {
- Text(item.name)
- }
- }
- .navigationDestination(for: Item.self) { item in
- DetailView(item: item)
- }
-}
-
-// Legacy (Avoid)
-NavigationView {
- List(items) { item in
- NavigationLink(destination: DetailView(item: item)) {
- Text(item.name)
- }
- }
-}
-```
-
-### navigationDestination(for:)
-
-**Use `navigationDestination(for:)` for type-safe navigation.**
-
-```swift
-struct ContentView: View {
- var body: some View {
- NavigationStack {
- List {
- NavigationLink("Profile", value: Route.profile)
- NavigationLink("Settings", value: Route.settings)
- }
- .navigationDestination(for: Route.self) { route in
- switch route {
- case .profile:
- ProfileView()
- case .settings:
- SettingsView()
- }
- }
- }
- }
-}
-
-enum Route: Hashable {
- case profile
- case settings
-}
-```
-
-## Tabs
-
-### Tab API vs tabItem()
-
-**For iOS 18 and later, prefer the `Tab` API over `tabItem()` to access modern tab features, and use availability checks or `tabItem()` for earlier OS versions.**
-
-```swift
-// Modern (Correct) - iOS 18+
-TabView {
- Tab("Home", systemImage: "house") {
- HomeView()
- }
-
- Tab("Search", systemImage: "magnifyingglass") {
- SearchView()
- }
-
- Tab("Profile", systemImage: "person") {
- ProfileView()
- }
-}
-
-// Legacy (Avoid)
-TabView {
- HomeView()
- .tabItem {
- Label("Home", systemImage: "house")
- }
-}
-```
-
-**Important**: When using `Tab(role:)` with roles, you must use the new `Tab { } label: { }` syntax for all tabs. Mixing with `.tabItem()` causes compilation errors.
-
-```swift
-// Correct - all tabs use Tab syntax
-TabView {
- Tab(role: .search) {
- SearchView()
- } label: {
- Label("Search", systemImage: "magnifyingglass")
- }
-
- Tab {
- HomeView()
- } label: {
- Label("Home", systemImage: "house")
- }
-}
-
-// Wrong - mixing Tab and tabItem causes errors
-TabView {
- Tab(role: .search) {
- SearchView()
- } label: {
- Label("Search", systemImage: "magnifyingglass")
- }
-
- HomeView() // Error: can't mix with Tab(role:)
- .tabItem {
- Label("Home", systemImage: "house")
- }
-}
-```
-
-## Interactions
-
-### Button vs onTapGesture()
-
-**Never use `onTapGesture()` unless you specifically need tap location or tap count. Always use `Button` otherwise.**
-
-```swift
-// Correct - standard tap action
-Button("Tap me") {
- performAction()
-}
-
-// Correct - need tap location
-Text("Tap anywhere")
- .onTapGesture { location in
- handleTap(at: location)
- }
-
-// Correct - need tap count
-Image("photo")
- .onTapGesture(count: 2) {
- handleDoubleTap()
- }
-
-// Wrong - use Button instead
-Text("Tap me")
- .onTapGesture {
- performAction()
- }
-```
-
-**Why**: `Button` provides proper accessibility, visual feedback, and semantic meaning. Use `onTapGesture()` only when you need its specific features.
-
-### Button with Images
-
-**Always specify text alongside images in buttons for accessibility.**
-
-```swift
-// Correct - includes text label
-Button("Add Item", systemImage: "plus") {
- addItem()
-}
-
-// Also correct - custom label
-Button {
- addItem()
-} label: {
- Label("Add Item", systemImage: "plus")
-}
-
-// Wrong - image only, no text
-Button {
- addItem()
-} label: {
- Image(systemName: "plus")
-}
-```
-
-## Layout and Sizing
-
-### Avoid UIScreen.main.bounds
-
-**Never use `UIScreen.main.bounds` to read available space.**
-
-```swift
-// Wrong - uses UIKit, doesn't respect safe areas
-let screenWidth = UIScreen.main.bounds.width
-
-// Correct - use GeometryReader
-GeometryReader { geometry in
- Text("Width: \(geometry.size.width)")
-}
-
-// Better - use containerRelativeFrame (iOS 17+)
-Text("Full width")
- .containerRelativeFrame(.horizontal)
-
-// Best - let SwiftUI handle sizing
-Text("Auto-sized")
- .frame(maxWidth: .infinity)
-```
-
-### GeometryReader Alternatives
-
-> **iOS 17+**: `containerRelativeFrame` and `visualEffect` require iOS 17 or later.
-
-**Don't use `GeometryReader` if a newer alternative works.**
-
-```swift
-// Modern - containerRelativeFrame
-Image("hero")
- .resizable()
- .containerRelativeFrame(.horizontal) { length, axis in
- length * 0.8
- }
-
-// Modern - visualEffect for position-based effects
-Text("Parallax")
- .visualEffect { content, geometry in
- content.offset(y: geometry.frame(in: .global).minY * 0.5)
- }
-
-// Legacy - only use if necessary
-GeometryReader { geometry in
- Image("hero")
- .frame(width: geometry.size.width * 0.8)
-}
-```
-
-## Type Erasure
-
-### Avoid AnyView
-
-**Avoid `AnyView` unless absolutely required.**
-
-```swift
-// Prefer - use @ViewBuilder
-@ViewBuilder
-func content() -> some View {
- if condition {
- Text("Option A")
- } else {
- Image(systemName: "photo")
- }
-}
-
-// Avoid - type erasure has performance cost
-func content() -> AnyView {
- if condition {
- return AnyView(Text("Option A"))
- } else {
- return AnyView(Image(systemName: "photo"))
- }
-}
-
-// Acceptable - when protocol conformance requires it
-var body: some View {
- // Complex conditional logic that requires type erasure
-}
-```
-
-## Styling Best Practices
-
-### Dynamic Type
-
-**Don't force specific font sizes. Prefer Dynamic Type.**
-
-```swift
-// Correct - respects user's text size preferences
-Text("Title")
- .font(.title)
-
-Text("Body")
- .font(.body)
-
-// Avoid - fixed size doesn't scale
-Text("Title")
- .font(.system(size: 24))
-```
-
-### UIKit Colors
-
-**Avoid using UIKit colors in SwiftUI code.**
-
-```swift
-// Correct - SwiftUI colors
-Text("Hello")
- .foregroundStyle(.blue)
- .background(.gray.opacity(0.2))
-
-// Wrong - UIKit colors
-Text("Hello")
- .foregroundColor(Color(UIColor.systemBlue))
- .background(Color(UIColor.systemGray))
-```
-
-## Static Member Lookup
-
-**Prefer static member lookup to struct instances.**
-
-```swift
-// Correct - static member lookup
-Circle()
- .fill(.blue)
-Button("Action") { }
- .buttonStyle(.borderedProminent)
-
-// Verbose - unnecessary struct instantiation
-Circle()
- .fill(Color.blue)
-Button("Action") { }
- .buttonStyle(BorderedProminentButtonStyle())
-```
-
-## Summary Checklist
-
-- [ ] Use `foregroundStyle()` instead of `foregroundColor()`
-- [ ] Use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`
-- [ ] Use `Tab` API instead of `tabItem()`
-- [ ] Use `Button` instead of `onTapGesture()` (unless need location/count)
-- [ ] Use `NavigationStack` instead of `NavigationView`
-- [ ] Use `navigationDestination(for:)` for type-safe navigation
-- [ ] Avoid `AnyView` unless required
-- [ ] Avoid `UIScreen.main.bounds`
-- [ ] Avoid `GeometryReader` when alternatives exist
-- [ ] Use Dynamic Type instead of fixed font sizes
-- [ ] Avoid hard-coded padding/spacing unless requested
-- [ ] Avoid UIKit colors in SwiftUI
-- [ ] Use static member lookup (`.blue` vs `Color.blue`)
-- [ ] Include text labels with button images
-- [ ] Use `bold()` instead of `fontWeight(.bold)`
diff --git a/.agents/skills/swiftui-expert-skill/references/performance-patterns.md b/.agents/skills/swiftui-expert-skill/references/performance-patterns.md
deleted file mode 100644
index 5fd48643..00000000
--- a/.agents/skills/swiftui-expert-skill/references/performance-patterns.md
+++ /dev/null
@@ -1,377 +0,0 @@
-# SwiftUI Performance Patterns Reference
-
-## Performance Optimization
-
-### 1. Avoid Redundant State Updates
-
-SwiftUI doesn't compare values before triggering updates:
-
-```swift
-// BAD - triggers update even if value unchanged
-.onReceive(publisher) { value in
- self.currentValue = value // Always triggers body re-evaluation
-}
-
-// GOOD - only update when different
-.onReceive(publisher) { value in
- if self.currentValue != value {
- self.currentValue = value
- }
-}
-```
-
-### 2. Optimize Hot Paths
-
-Hot paths are frequently executed code (scroll handlers, animations, gestures):
-
-```swift
-// BAD - updates state on every scroll position change
-.onPreferenceChange(ScrollOffsetKey.self) { offset in
- shouldShowTitle = offset.y <= -32 // Fires constantly during scroll!
-}
-
-// GOOD - only update when threshold crossed
-.onPreferenceChange(ScrollOffsetKey.self) { offset in
- let shouldShow = offset.y <= -32
- if shouldShow != shouldShowTitle {
- shouldShowTitle = shouldShow // Fires only when crossing threshold
- }
-}
-```
-
-### 3. Pass Only What Views Need
-
-**Avoid passing large "config" or "context" objects.** Pass only the specific values each view needs.
-
-```swift
-// Good - pass specific values
-@Observable
-@MainActor
-final class AppConfig {
- var theme: Theme
- var fontSize: CGFloat
- var notifications: Bool
-}
-
-struct SettingsView: View {
- @State private var config = AppConfig()
-
- var body: some View {
- VStack {
- ThemeSelector(theme: config.theme)
- FontSizeSlider(fontSize: config.fontSize)
- }
- }
-}
-
-// Avoid - passing entire config
-struct SettingsView: View {
- @State private var config = AppConfig()
-
- var body: some View {
- VStack {
- ThemeSelector(config: config) // Gets notified of ALL config changes
- FontSizeSlider(config: config) // Gets notified of ALL config changes
- }
- }
-}
-```
-
-**Why**: When using `ObservableObject`, any `@Published` property change triggers updates in all views observing the object. With `@Observable`, views update when properties they access change, but passing entire objects still creates unnecessary dependencies.
-
-### 4. Use Equatable Views
-
-For views with expensive bodies, conform to `Equatable`:
-
-```swift
-struct ExpensiveView: View, Equatable {
- let data: SomeData
-
- static func == (lhs: Self, rhs: Self) -> Bool {
- lhs.data.id == rhs.data.id // Custom equality check
- }
-
- var body: some View {
- // Expensive computation
- }
-}
-
-// Usage
-ExpensiveView(data: data)
- .equatable() // Use custom equality
-```
-
-**Caution**: If you add new state or dependencies to your view, remember to update your `==` function!
-
-### 5. POD Views for Fast Diffing
-
-**POD (Plain Old Data) views use `memcmp` for fastest diffing.** A view is POD if it only contains simple value types and no property wrappers.
-
-```swift
-// POD view - fastest diffing
-struct FastView: View {
- let title: String
- let count: Int
-
- var body: some View {
- Text("\(title): \(count)")
- }
-}
-
-// Non-POD view - uses reflection or custom equality
-struct SlowerView: View {
- let title: String
- @State private var isExpanded = false // Property wrapper makes it non-POD
-
- var body: some View {
- Text(title)
- }
-}
-```
-
-**Advanced Pattern**: Wrap expensive non-POD views in POD parent views:
-
-```swift
-// POD wrapper for fast diffing
-struct ExpensiveView: View {
- let value: Int
-
- var body: some View {
- ExpensiveViewInternal(value: value)
- }
-}
-
-// Internal view with state
-private struct ExpensiveViewInternal: View {
- let value: Int
- @State private var item: Item?
-
- var body: some View {
- // Expensive rendering
- }
-}
-```
-
-**Why**: The POD parent uses fast `memcmp` comparison. Only when `value` changes does the internal view get diffed.
-
-### 6. Lazy Loading
-
-Use lazy containers for large collections:
-
-```swift
-// BAD - creates all views immediately
-ScrollView {
- VStack {
- ForEach(items) { item in
- ExpensiveRow(item: item)
- }
- }
-}
-
-// GOOD - creates views on demand
-ScrollView {
- LazyVStack {
- ForEach(items) { item in
- ExpensiveRow(item: item)
- }
- }
-}
-```
-
-### 7. Task Cancellation
-
-Cancel async work when view disappears:
-
-```swift
-struct DataView: View {
- @State private var data: [Item] = []
-
- var body: some View {
- List(data) { item in
- Text(item.name)
- }
- .task {
- // Automatically cancelled when view disappears
- data = await fetchData()
- }
- }
-}
-```
-
-### 8. Debug View Updates
-
-**Use `Self._printChanges()` to debug unexpected view updates.**
-
-```swift
-struct DebugView: View {
- @State private var count = 0
- @State private var name = ""
-
- var body: some View {
- let _ = Self._printChanges() // Prints what caused body to be called
-
- VStack {
- Text("Count: \(count)")
- Text("Name: \(name)")
- }
- }
-}
-```
-
-**Why**: This helps identify which state changes are causing view updates. Even if a parent updates, a child's body shouldn't be called if the child's dependencies didn't change.
-
-### 9. Eliminate Unnecessary Dependencies
-
-**Narrow state scope to reduce update fan-out.**
-
-```swift
-// Bad - broad dependency
-@Observable
-@MainActor
-final class AppModel {
- var items: [Item] = []
- var settings: Settings = .init()
- var theme: Theme = .light
-}
-
-struct ItemRow: View {
- @Environment(AppModel.self) private var model
- let item: Item
-
- var body: some View {
- // Updates when ANY property of model changes
- Text(item.name)
- .foregroundStyle(model.theme.primaryColor)
- }
-}
-
-// Good - narrow dependency
-struct ItemRow: View {
- let item: Item
- let themeColor: Color // Only depends on what it needs
-
- var body: some View {
- Text(item.name)
- .foregroundStyle(themeColor)
- }
-}
-```
-
-**Why**: With `ObservableObject`, any `@Published` property change triggers all observers. With `@Observable`, views update when accessed properties change, but passing entire models still creates broader dependencies than necessary.
-
-### 10. Common Performance Issues
-
-**Be aware of common performance bottlenecks in SwiftUI:**
-
-- View invalidation storms from broad state changes
-- Unstable identity in lists causing excessive diffing
-- Heavy work in `body` (formatting, sorting, image decoding)
-- Layout thrash from deep stacks or preference chains
-
-**When performance issues arise**, suggest the user profile with Instruments (SwiftUI template) to identify specific bottlenecks.
-
-## Anti-Patterns
-
-### 1. Creating Objects in Body
-
-```swift
-// BAD - creates new formatter every body call
-var body: some View {
- let formatter = DateFormatter()
- formatter.dateStyle = .long
- return Text(formatter.string(from: date))
-}
-
-// GOOD - static or stored formatter
-private static let dateFormatter: DateFormatter = {
- let f = DateFormatter()
- f.dateStyle = .long
- return f
-}()
-
-var body: some View {
- Text(Self.dateFormatter.string(from: date))
-}
-```
-
-### 2. Heavy Computation in Body
-
-**Keep view body simple and pure.** Avoid side effects, dispatching, or complex logic.
-
-```swift
-// BAD - sorts array every body call
-var body: some View {
- List(items.sorted { $0.name < $1.name }) { item in
- Text(item.name)
- }
-}
-
-// GOOD - compute once, store result
-@State private var sortedItems: [Item] = []
-
-var body: some View {
- List(sortedItems) { item in
- Text(item.name)
- }
- .onChange(of: items) { _, newItems in
- sortedItems = newItems.sorted { $0.name < $1.name }
- }
-}
-
-// Better - compute in model
-@Observable
-@MainActor
-final class ItemsViewModel {
- var items: [Item] = []
-
- var sortedItems: [Item] {
- items.sorted { $0.name < $1.name }
- }
-
- func loadItems() async {
- items = await fetchItems()
- }
-}
-
-struct ItemsView: View {
- @State private var viewModel = ItemsViewModel()
-
- var body: some View {
- List(viewModel.sortedItems) { item in
- Text(item.name)
- }
- .task {
- await viewModel.loadItems()
- }
- }
-}
-```
-
-**Why**: Complex logic in `body` slows down view updates and can cause frame drops. The `body` should be a pure structural representation of state.
-
-### 3. Unnecessary State
-
-```swift
-// BAD - derived state stored separately
-@State private var items: [Item] = []
-@State private var itemCount: Int = 0 // Unnecessary!
-
-// GOOD - compute derived values
-@State private var items: [Item] = []
-
-var itemCount: Int { items.count } // Computed property
-```
-
-## Summary Checklist
-
-- [ ] State updates check for value changes before assigning
-- [ ] Hot paths minimize state updates
-- [ ] Pass only needed values to views (avoid large config objects)
-- [ ] Large lists use `LazyVStack`/`LazyHStack`
-- [ ] No object creation in `body`
-- [ ] Heavy computation moved out of `body`
-- [ ] Body kept simple and pure (no side effects)
-- [ ] Derived state computed, not stored
-- [ ] Use `Self._printChanges()` to debug unexpected updates
-- [ ] Equatable conformance for expensive views (when appropriate)
-- [ ] Consider POD view wrappers for advanced optimization
diff --git a/.agents/skills/swiftui-expert-skill/references/scroll-patterns.md b/.agents/skills/swiftui-expert-skill/references/scroll-patterns.md
deleted file mode 100644
index a2342674..00000000
--- a/.agents/skills/swiftui-expert-skill/references/scroll-patterns.md
+++ /dev/null
@@ -1,305 +0,0 @@
-# SwiftUI ScrollView Patterns Reference
-
-## ScrollView Modifiers
-
-### Hiding Scroll Indicators
-
-**Use `.scrollIndicators(.hidden)` modifier instead of initializer parameter.**
-
-```swift
-// Modern (Correct)
-ScrollView {
- content
-}
-.scrollIndicators(.hidden)
-
-// Legacy (Avoid)
-ScrollView(showsIndicators: false) {
- content
-}
-```
-
-## ScrollViewReader for Programmatic Scrolling
-
-**Use `ScrollViewReader` for scroll-to-top, scroll-to-bottom, and anchor-based jumps.**
-
-```swift
-struct ChatView: View {
- @State private var messages: [Message] = []
- private let bottomID = "bottom"
-
- var body: some View {
- ScrollViewReader { proxy in
- ScrollView {
- LazyVStack {
- ForEach(messages) { message in
- MessageRow(message: message)
- .id(message.id)
- }
- Color.clear
- .frame(height: 1)
- .id(bottomID)
- }
- }
- .onChange(of: messages.count) { _, _ in
- withAnimation {
- proxy.scrollTo(bottomID, anchor: .bottom)
- }
- }
- .onAppear {
- proxy.scrollTo(bottomID, anchor: .bottom)
- }
- }
- }
-}
-```
-
-### Scroll-to-Top Pattern
-
-```swift
-struct FeedView: View {
- @State private var items: [Item] = []
- @State private var scrollToTop = false
- private let topID = "top"
-
- var body: some View {
- ScrollViewReader { proxy in
- ScrollView {
- LazyVStack {
- Color.clear
- .frame(height: 1)
- .id(topID)
-
- ForEach(items) { item in
- ItemRow(item: item)
- }
- }
- }
- .onChange(of: scrollToTop) { _, shouldScroll in
- if shouldScroll {
- withAnimation {
- proxy.scrollTo(topID, anchor: .top)
- }
- scrollToTop = false
- }
- }
- }
- }
-}
-```
-
-**Why**: `ScrollViewReader` provides programmatic scroll control with stable anchors. Always use stable IDs and explicit animations.
-
-## Scroll Position Tracking
-
-### Basic Scroll Position
-
-**Avoid** - Storing scroll position directly triggers view updates on every scroll frame:
-
-```swift
-// ❌ Bad Practice - causes unnecessary re-renders
-struct ContentView: View {
- @State private var scrollPosition: CGFloat = 0
-
- var body: some View {
- ScrollView {
- content
- .background(
- GeometryReader { geometry in
- Color.clear
- .preference(
- key: ScrollOffsetPreferenceKey.self,
- value: geometry.frame(in: .named("scroll")).minY
- )
- }
- )
- }
- .coordinateSpace(name: "scroll")
- .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
- scrollPosition = value
- }
- }
-}
-```
-
-**Preferred** - Check scroll position and update a flag based on thresholds for smoother, more efficient scrolling:
-
-```swift
-// ✅ Good Practice - only updates state when crossing threshold
-struct ContentView: View {
- @State private var startAnimation: Bool = false
-
- var body: some View {
- ScrollView {
- content
- .background(
- GeometryReader { geometry in
- Color.clear
- .preference(
- key: ScrollOffsetPreferenceKey.self,
- value: geometry.frame(in: .named("scroll")).minY
- )
- }
- )
- }
- .coordinateSpace(name: "scroll")
- .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
- if value < -100 {
- startAnimation = true
- } else {
- startAnimation = false
- }
- }
- }
-}
-
-struct ScrollOffsetPreferenceKey: PreferenceKey {
- static var defaultValue: CGFloat = 0
- static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
- value = nextValue()
- }
-}
-```
-
-### Scroll-Based Header Visibility
-
-```swift
-struct ContentView: View {
- @State private var showHeader = true
-
- var body: some View {
- VStack(spacing: 0) {
- if showHeader {
- HeaderView()
- .transition(.move(edge: .top))
- }
-
- ScrollView {
- content
- .background(
- GeometryReader { geometry in
- Color.clear
- .preference(
- key: ScrollOffsetPreferenceKey.self,
- value: geometry.frame(in: .named("scroll")).minY
- )
- }
- )
- }
- .coordinateSpace(name: "scroll")
- .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
- if offset < -50 { // Scrolling down
- withAnimation { showHeader = false }
- } else if offset > 50 { // Scrolling up
- withAnimation { showHeader = true }
- }
- }
- }
- }
-}
-```
-
-## Scroll Transitions and Effects
-
-> **iOS 17+**: All APIs in this section require iOS 17 or later.
-
-### Scroll-Based Opacity
-
-```swift
-struct ParallaxView: View {
- var body: some View {
- ScrollView {
- LazyVStack(spacing: 20) {
- ForEach(items) { item in
- ItemCard(item: item)
- .visualEffect { content, geometry in
- let frame = geometry.frame(in: .scrollView)
- let distance = min(0, frame.minY)
- return content
- .opacity(1 + distance / 200)
- }
- }
- }
- }
- }
-}
-```
-
-### Parallax Effect
-
-```swift
-struct ParallaxHeader: View {
- var body: some View {
- ScrollView {
- VStack(spacing: 0) {
- Image("hero")
- .resizable()
- .aspectRatio(contentMode: .fill)
- .frame(height: 300)
- .visualEffect { content, geometry in
- let offset = geometry.frame(in: .scrollView).minY
- return content
- .offset(y: offset > 0 ? -offset * 0.5 : 0)
- }
- .clipped()
-
- ContentView()
- }
- }
- }
-}
-```
-
-## Scroll Target Behavior
-
-> **iOS 17+**: All APIs in this section require iOS 17 or later.
-
-### Paging ScrollView
-
-```swift
-struct PagingView: View {
- var body: some View {
- ScrollView(.horizontal) {
- LazyHStack(spacing: 0) {
- ForEach(pages) { page in
- PageView(page: page)
- .containerRelativeFrame(.horizontal)
- }
- }
- .scrollTargetLayout()
- }
- .scrollTargetBehavior(.paging)
- }
-}
-```
-
-### Snap to Items
-
-```swift
-struct SnapScrollView: View {
- var body: some View {
- ScrollView(.horizontal) {
- LazyHStack(spacing: 16) {
- ForEach(items) { item in
- ItemCard(item: item)
- .frame(width: 280)
- }
- }
- .scrollTargetLayout()
- }
- .scrollTargetBehavior(.viewAligned)
- .contentMargins(.horizontal, 20)
- }
-}
-```
-
-## Summary Checklist
-
-- [ ] Use `.scrollIndicators(.hidden)` instead of initializer parameter
-- [ ] Use `ScrollViewReader` with stable IDs for programmatic scrolling
-- [ ] Always use explicit animations with `scrollTo()`
-- [ ] Use `.visualEffect` for scroll-based visual changes
-- [ ] Use `.scrollTargetBehavior(.paging)` for paging behavior
-- [ ] Use `.scrollTargetBehavior(.viewAligned)` for snap-to-item behavior
-- [ ] Gate frequent scroll position updates by thresholds
-- [ ] Use preference keys for custom scroll position tracking
diff --git a/.agents/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md b/.agents/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md
deleted file mode 100644
index 467ffca5..00000000
--- a/.agents/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md
+++ /dev/null
@@ -1,292 +0,0 @@
-# SwiftUI Sheet and Navigation Patterns Reference
-
-## Sheet Patterns
-
-### Item-Driven Sheets (Preferred)
-
-**Use `.sheet(item:)` instead of `.sheet(isPresented:)` when presenting model-based content.**
-
-```swift
-// Good - item-driven
-@State private var selectedItem: Item?
-
-var body: some View {
- List(items) { item in
- Button(item.name) {
- selectedItem = item
- }
- }
- .sheet(item: $selectedItem) { item in
- ItemDetailSheet(item: item)
- }
-}
-
-// Avoid - boolean flag requires separate state
-@State private var showSheet = false
-@State private var selectedItem: Item?
-
-var body: some View {
- List(items) { item in
- Button(item.name) {
- selectedItem = item
- showSheet = true
- }
- }
- .sheet(isPresented: $showSheet) {
- if let selectedItem {
- ItemDetailSheet(item: selectedItem)
- }
- }
-}
-```
-
-**Why**: `.sheet(item:)` automatically handles presentation state and avoids optional unwrapping in the sheet body.
-
-### Sheets Own Their Actions
-
-**Sheets should handle their own dismiss and actions internally.**
-
-```swift
-// Good - sheet owns its actions
-struct EditItemSheet: View {
- @Environment(\.dismiss) private var dismiss
- @Environment(DataStore.self) private var store
-
- let item: Item
- @State private var name: String
- @State private var isSaving = false
-
- init(item: Item) {
- self.item = item
- _name = State(initialValue: item.name)
- }
-
- var body: some View {
- NavigationStack {
- Form {
- TextField("Name", text: $name)
- }
- .navigationTitle("Edit Item")
- .toolbar {
- ToolbarItem(placement: .cancellationAction) {
- Button("Cancel") {
- dismiss()
- }
- }
- ToolbarItem(placement: .confirmationAction) {
- Button(isSaving ? "Saving..." : "Save") {
- Task { await save() }
- }
- .disabled(isSaving || name.isEmpty)
- }
- }
- }
- }
-
- private func save() async {
- isSaving = true
- await store.updateItem(item, name: name)
- dismiss()
- }
-}
-
-// Avoid - parent manages sheet actions via closures
-struct ParentView: View {
- @State private var selectedItem: Item?
-
- var body: some View {
- List(items) { item in
- Button(item.name) {
- selectedItem = item
- }
- }
- .sheet(item: $selectedItem) { item in
- EditItemSheet(
- item: item,
- onSave: { newName in
- // Parent handles save
- },
- onCancel: {
- selectedItem = nil
- }
- )
- }
- }
-}
-```
-
-**Why**: Sheets that own their actions are more reusable and don't require callback prop-drilling.
-
-## Navigation Patterns
-
-### Type-Safe Navigation with NavigationStack
-
-```swift
-struct ContentView: View {
- var body: some View {
- NavigationStack {
- List {
- NavigationLink("Profile", value: Route.profile)
- NavigationLink("Settings", value: Route.settings)
- }
- .navigationDestination(for: Route.self) { route in
- switch route {
- case .profile:
- ProfileView()
- case .settings:
- SettingsView()
- }
- }
- }
- }
-}
-
-enum Route: Hashable {
- case profile
- case settings
-}
-```
-
-### Programmatic Navigation
-
-```swift
-struct ContentView: View {
- @State private var navigationPath = NavigationPath()
-
- var body: some View {
- NavigationStack(path: $navigationPath) {
- List {
- Button("Go to Detail") {
- navigationPath.append(DetailRoute.item(id: 1))
- }
- }
- .navigationDestination(for: DetailRoute.self) { route in
- switch route {
- case .item(let id):
- ItemDetailView(id: id)
- }
- }
- }
- }
-}
-
-enum DetailRoute: Hashable {
- case item(id: Int)
-}
-```
-
-### Navigation with State Restoration
-
-```swift
-struct ContentView: View {
- @State private var navigationPath = NavigationPath()
-
- var body: some View {
- NavigationStack(path: $navigationPath) {
- RootView()
- .navigationDestination(for: Route.self) { route in
- destinationView(for: route)
- }
- }
- }
-
- @ViewBuilder
- private func destinationView(for route: Route) -> some View {
- switch route {
- case .profile:
- ProfileView()
- case .settings:
- SettingsView()
- }
- }
-}
-```
-
-## Presentation Modifiers
-
-### Full Screen Cover
-
-```swift
-struct ContentView: View {
- @State private var showFullScreen = false
-
- var body: some View {
- Button("Show Full Screen") {
- showFullScreen = true
- }
- .fullScreenCover(isPresented: $showFullScreen) {
- FullScreenView()
- }
- }
-}
-```
-
-### Popover
-
-```swift
-struct ContentView: View {
- @State private var showPopover = false
-
- var body: some View {
- Button("Show Popover") {
- showPopover = true
- }
- .popover(isPresented: $showPopover) {
- PopoverContentView()
- .presentationCompactAdaptation(.popover) // Don't adapt to sheet on iPhone
- }
- }
-}
-```
-
-### Alert with Actions
-
-```swift
-struct ContentView: View {
- @State private var showAlert = false
-
- var body: some View {
- Button("Show Alert") {
- showAlert = true
- }
- .alert("Delete Item?", isPresented: $showAlert) {
- Button("Delete", role: .destructive) {
- deleteItem()
- }
- Button("Cancel", role: .cancel) { }
- } message: {
- Text("This action cannot be undone.")
- }
- }
-}
-```
-
-### Confirmation Dialog
-
-```swift
-struct ContentView: View {
- @State private var showDialog = false
-
- var body: some View {
- Button("Show Options") {
- showDialog = true
- }
- .confirmationDialog("Choose an option", isPresented: $showDialog) {
- Button("Option 1") { handleOption1() }
- Button("Option 2") { handleOption2() }
- Button("Cancel", role: .cancel) { }
- }
- }
-}
-```
-
-## Summary Checklist
-
-- [ ] Use `.sheet(item:)` for model-based sheets
-- [ ] Sheets own their actions and dismiss internally
-- [ ] Use `NavigationStack` with `navigationDestination(for:)` for type-safe navigation
-- [ ] Use `NavigationPath` for programmatic navigation
-- [ ] Use appropriate presentation modifiers (sheet, fullScreenCover, popover)
-- [ ] Alerts and confirmation dialogs use modern API with actions
-- [ ] Avoid passing dismiss/save callbacks to sheets
-- [ ] Navigation state can be saved/restored when needed
diff --git a/.agents/skills/swiftui-expert-skill/references/state-management.md b/.agents/skills/swiftui-expert-skill/references/state-management.md
deleted file mode 100644
index 5dc503f5..00000000
--- a/.agents/skills/swiftui-expert-skill/references/state-management.md
+++ /dev/null
@@ -1,449 +0,0 @@
-# SwiftUI State Management Reference
-
-## Property Wrapper Selection Guide
-
-| Wrapper | Use When | Notes |
-|---------|----------|-------|
-| `@State` | Internal view state that triggers updates | Must be `private` |
-| `@Binding` | Child view needs to modify parent's state | Don't use for read-only |
-| `@Bindable` | iOS 17+: View receives `@Observable` object and needs bindings | For injected observables |
-| `let` | Read-only value passed from parent | Simplest option |
-| `var` | Read-only value that child observes via `.onChange()` | For reactive reads |
-
-**Legacy (Pre-iOS 17):**
-| Wrapper | Use When | Notes |
-|---------|----------|-------|
-| `@StateObject` | View owns an `ObservableObject` instance | Use `@State` with `@Observable` instead |
-| `@ObservedObject` | View receives an `ObservableObject` from outside | Never create inline |
-
-## @State
-
-Always mark `@State` properties as `private`. Use for internal view state that triggers UI updates.
-
-```swift
-// Correct
-@State private var isAnimating = false
-@State private var selectedTab = 0
-```
-
-**Why Private?** Marking state as `private` makes it clear what's created by the view versus what's passed in. It also prevents accidentally passing initial values that will be ignored (see "Don't Pass Values as @State" below).
-
-### iOS 17+ with @Observable (Preferred)
-
-**Always prefer `@Observable` over `ObservableObject`.** With iOS 17's `@Observable` macro, use `@State` instead of `@StateObject`:
-
-```swift
-@Observable
-@MainActor // Always mark @Observable classes with @MainActor
-final class DataModel {
- var name = "Some Name"
- var count = 0
-}
-
-struct MyView: View {
- @State private var model = DataModel() // Use @State, not @StateObject
-
- var body: some View {
- VStack {
- TextField("Name", text: $model.name)
- Stepper("Count: \(model.count)", value: $model.count)
- }
- }
-}
-```
-
-**Note**: You may want to mark `@Observable` classes with `@MainActor` to ensure thread safety with SwiftUI, unless your project or package uses Default Actor Isolation set to `MainActor`—in which case, the explicit attribute is redundant and can be omitted.
-
-## @Binding
-
-Use only when child view needs to **modify** parent's state. If child only reads the value, use `let` instead.
-
-```swift
-// Parent
-struct ParentView: View {
- @State private var isSelected = false
-
- var body: some View {
- ChildView(isSelected: $isSelected)
- }
-}
-
-// Child - will modify the value
-struct ChildView: View {
- @Binding var isSelected: Bool
-
- var body: some View {
- Button("Toggle") {
- isSelected.toggle()
- }
- }
-}
-```
-
-### When NOT to use @Binding
-
-```swift
-// Bad - child only displays, doesn't modify
-struct DisplayView: View {
- @Binding var title: String // Unnecessary
- var body: some View {
- Text(title)
- }
-}
-
-// Good - use let for read-only
-struct DisplayView: View {
- let title: String
- var body: some View {
- Text(title)
- }
-}
-```
-
-## @StateObject vs @ObservedObject (Legacy - Pre-iOS 17)
-
-**Note**: These are legacy patterns. Always prefer `@Observable` with `@State` for iOS 17+.
-
-The key distinction is **ownership**:
-
-- `@StateObject`: View **creates and owns** the object
-- `@ObservedObject`: View **receives** the object from outside
-
-```swift
-// Legacy pattern - use @Observable instead
-class MyViewModel: ObservableObject {
- @Published var items: [String] = []
-}
-
-// View creates it → @StateObject
-struct OwnerView: View {
- @StateObject private var viewModel = MyViewModel()
-
- var body: some View {
- ChildView(viewModel: viewModel)
- }
-}
-
-// View receives it → @ObservedObject
-struct ChildView: View {
- @ObservedObject var viewModel: MyViewModel
-
- var body: some View {
- List(viewModel.items, id: \.self) { Text($0) }
- }
-}
-```
-
-### Common Mistake
-
-Never create an `ObservableObject` inline with `@ObservedObject`:
-
-```swift
-// WRONG - creates new instance on every view update
-struct BadView: View {
- @ObservedObject var viewModel = MyViewModel() // BUG!
-}
-
-// CORRECT - owned objects use @StateObject
-struct GoodView: View {
- @StateObject private var viewModel = MyViewModel()
-}
-```
-
-### @StateObject instantiation in View's initializer (if it's a Parent view)
-
-This approach is an anti-pattern in general. Prefer storing the StateObject in the parent view or wherever the model is actually owned, then pass it down (use @ObservedObject, @EnvironmentObject, or @Bindable (for @Observable)) to keep ownership and lifecycle explicit.
-If you need to create a @StateObject with initialization parameters in your view's custom initializer, be aware of redundant allocations and hidden side effects.
-
-```swift
-// WRONG - creates a new ViewModel instance each time the view's initializer is called
-// (which can happen multiple times during SwiftUI's structural identity evaluation)
-struct MovieDetailsView: View {
-
- @StateObject private var viewModel: MovieDetailsViewModel
-
- init(movie: Movie) {
- let viewModel = MovieDetailsViewModel(movie: movie)
- _viewModel = StateObject(wrappedValue: viewModel)
- }
-
- var body: some View {
- // ...
- }
-}
-
-// CORRECT - creation in @autoclosure prevents multiple instantiations
-struct MovieDetailsView: View {
-
- @StateObject private var viewModel: MovieDetailsViewModel
-
- init(movie: Movie) {
- _viewModel = StateObject(
- wrappedValue: MovieDetailsViewModel(movie: movie)
- )
- }
-
- var body: some View {
- // ...
- }
-}
-```
-
-**Modern Alternative**: Use `@Observable` with `@State` instead of `ObservableObject` patterns.
-
-## Don't Pass Values as @State
-
-**Critical**: Never declare passed values as `@State` or `@StateObject`. The value you provide is only an initial value and won't update.
-
-```swift
-// Parent
-struct ParentView: View {
- @State private var item = Item(name: "Original")
-
- var body: some View {
- ChildView(item: item)
- Button("Change") {
- item.name = "Updated" // Child won't see this!
- }
- }
-}
-
-// Wrong - child ignores updates from parent
-struct ChildView: View {
- @State var item: Item // Accepts initial value only!
-
- var body: some View {
- Text(item.name) // Shows "Original" forever
- }
-}
-
-// Correct - child receives updates
-struct ChildView: View {
- let item: Item // Or @Binding if child needs to modify
-
- var body: some View {
- Text(item.name) // Updates when parent changes
- }
-}
-```
-
-**Why**: `@State` and `@StateObject` retain values between view updates. That's their purpose. When a parent passes a new value, the child reuses its existing state.
-
-**Prevention**: Always mark `@State` and `@StateObject` as `private`. This prevents them from appearing in the generated initializer.
-
-## @Bindable (iOS 17+)
-
-Use when receiving an `@Observable` object from outside and needing bindings:
-
-```swift
-@Observable
-final class UserModel {
- var name = ""
- var email = ""
-}
-
-struct ParentView: View {
- @State private var user = UserModel()
-
- var body: some View {
- EditUserView(user: user)
- }
-}
-
-struct EditUserView: View {
- @Bindable var user: UserModel // Received from parent, needs bindings
-
- var body: some View {
- Form {
- TextField("Name", text: $user.name)
- TextField("Email", text: $user.email)
- }
- }
-}
-```
-
-## let vs var for Passed Values
-
-### Use `let` for read-only display
-
-```swift
-struct ProfileHeader: View {
- let username: String
- let avatarURL: URL
-
- var body: some View {
- HStack {
- AsyncImage(url: avatarURL)
- Text(username)
- }
- }
-}
-```
-
-### Use `var` when reacting to changes with `.onChange()`
-
-```swift
-struct ReactiveView: View {
- var externalValue: Int // Watch with .onChange()
- @State private var displayText = ""
-
- var body: some View {
- Text(displayText)
- .onChange(of: externalValue) { oldValue, newValue in
- displayText = "Changed from \(oldValue) to \(newValue)"
- }
- }
-}
-```
-
-## Environment and Preferences
-
-### @Environment
-
-Access environment values provided by SwiftUI or parent views:
-
-```swift
-struct MyView: View {
- @Environment(\.colorScheme) private var colorScheme
- @Environment(\.dismiss) private var dismiss
-
- var body: some View {
- Button("Done") { dismiss() }
- .foregroundStyle(colorScheme == .dark ? .white : .black)
- }
-}
-```
-
-### @Environment with @Observable (iOS 17+ - Preferred)
-
-**Always prefer this pattern** for sharing state through the environment:
-
-```swift
-@Observable
-@MainActor
-final class AppState {
- var isLoggedIn = false
-}
-
-// Inject
-ContentView()
- .environment(AppState())
-
-// Access
-struct ChildView: View {
- @Environment(AppState.self) private var appState
-}
-```
-
-### @EnvironmentObject (Legacy - Pre-iOS 17)
-
-Legacy pattern for sharing observable objects through the environment:
-
-```swift
-// Legacy pattern - use @Observable with @Environment instead
-class AppState: ObservableObject {
- @Published var isLoggedIn = false
-}
-
-// Inject at root
-ContentView()
- .environmentObject(AppState())
-
-// Access in child
-struct ChildView: View {
- @EnvironmentObject var appState: AppState
-}
-```
-
-## Decision Flowchart
-
-```
-Is this value owned by this view?
-├─ YES: Is it a simple value type?
-│ ├─ YES → @State private var
-│ └─ NO (class):
-│ ├─ Use @Observable → @State private var (mark class @MainActor)
-│ └─ Legacy ObservableObject → @StateObject private var
-│
-└─ NO (passed from parent):
- ├─ Does child need to MODIFY it?
- │ ├─ YES → @Binding var
- │ └─ NO: Does child need BINDINGS to its properties?
- │ ├─ YES (@Observable) → @Bindable var
- │ └─ NO: Does child react to changes?
- │ ├─ YES → var + .onChange()
- │ └─ NO → let
- │
- └─ Is it a legacy ObservableObject from parent?
- └─ YES → @ObservedObject var (consider migrating to @Observable)
-```
-
-## State Privacy Rules
-
-**All view-owned state should be `private`:**
-
-```swift
-// Correct - clear what's created vs passed
-struct MyView: View {
- // Created by view - private
- @State private var isExpanded = false
- @State private var viewModel = ViewModel()
- @AppStorage("theme") private var theme = "light"
- @Environment(\.colorScheme) private var colorScheme
-
- // Passed from parent - not private
- let title: String
- @Binding var isSelected: Bool
- @Bindable var user: User
-
- var body: some View {
- // ...
- }
-}
-```
-
-**Why**: This makes dependencies explicit and improves code completion for the generated initializer.
-
-## Avoid Nested ObservableObject
-
-**Note**: This limitation only applies to `ObservableObject`. `@Observable` fully supports nested observed objects.
-
-```swift
-// Avoid - breaks animations and change tracking
-class Parent: ObservableObject {
- @Published var child: Child // Nested ObservableObject
-}
-
-class Child: ObservableObject {
- @Published var value: Int
-}
-
-// Workaround - pass child directly to views
-struct ParentView: View {
- @StateObject private var parent = Parent()
-
- var body: some View {
- ChildView(child: parent.child) // Pass nested object directly
- }
-}
-
-struct ChildView: View {
- @ObservedObject var child: Child
-
- var body: some View {
- Text("\(child.value)")
- }
-}
-```
-
-**Why**: SwiftUI can't track changes through nested `ObservableObject` properties. Manual workarounds break animations. With `@Observable`, this isn't an issue.
-
-## Key Principles
-
-1. **Always prefer `@Observable` over `ObservableObject`** for new code
-2. **Mark `@Observable` classes with `@MainActor` for thread safety (unless using default actor isolation)`**
-3. Use `@State` with `@Observable` classes (not `@StateObject`)
-4. Use `@Bindable` for injected `@Observable` objects that need bindings
-5. **Always mark `@State` and `@StateObject` as `private`**
-6. **Never declare passed values as `@State` or `@StateObject`**
-7. With `@Observable`, nested objects work fine; with `ObservableObject`, pass nested objects directly to child views
diff --git a/.agents/skills/swiftui-expert-skill/references/text-formatting.md b/.agents/skills/swiftui-expert-skill/references/text-formatting.md
deleted file mode 100644
index 9bce545c..00000000
--- a/.agents/skills/swiftui-expert-skill/references/text-formatting.md
+++ /dev/null
@@ -1,285 +0,0 @@
-# SwiftUI Text Formatting Reference
-
-## Modern Text Formatting
-
-**Never use C-style `String(format:)` with Text. Always use format parameters.**
-
-## Number Formatting
-
-### Basic Number Formatting
-
-```swift
-let value = 42.12345
-
-// Modern (Correct)
-Text(value, format: .number.precision(.fractionLength(2)))
-// Output: "42.12"
-
-Text(abs(value), format: .number.precision(.fractionLength(2)))
-// Output: "42.12" (absolute value)
-
-// Legacy (Avoid)
-Text(String(format: "%.2f", abs(value)))
-```
-
-### Integer Formatting
-
-```swift
-let count = 1234567
-
-// With grouping separator
-Text(count, format: .number)
-// Output: "1,234,567" (locale-dependent)
-
-// Without grouping
-Text(count, format: .number.grouping(.never))
-// Output: "1234567"
-```
-
-### Decimal Precision
-
-```swift
-let price = 19.99
-
-// Fixed decimal places
-Text(price, format: .number.precision(.fractionLength(2)))
-// Output: "19.99"
-
-// Significant digits
-Text(price, format: .number.precision(.significantDigits(3)))
-// Output: "20.0"
-
-// Integer-only
-Text(price, format: .number.precision(.integerLength(1...)))
-// Output: "19"
-```
-
-## Currency Formatting
-
-```swift
-let price = 19.99
-
-// Correct - with currency code
-Text(price, format: .currency(code: "USD"))
-// Output: "$19.99"
-
-// With locale
-Text(price, format: .currency(code: "EUR").locale(Locale(identifier: "de_DE")))
-// Output: "19,99 €"
-
-// Avoid - manual formatting
-Text(String(format: "$%.2f", price))
-```
-
-## Percentage Formatting
-
-```swift
-let percentage = 0.856
-
-// Correct - with precision
-Text(percentage, format: .percent.precision(.fractionLength(1)))
-// Output: "85.6%"
-
-// Without decimal places
-Text(percentage, format: .percent.precision(.fractionLength(0)))
-// Output: "86%"
-
-// Avoid - manual calculation
-Text(String(format: "%.1f%%", percentage * 100))
-```
-
-## Date and Time Formatting
-
-### Date Formatting
-
-```swift
-let date = Date()
-
-// Date only
-Text(date, format: .dateTime.day().month().year())
-// Output: "Jan 23, 2026"
-
-// Full date
-Text(date, format: .dateTime.day().month(.wide).year())
-// Output: "January 23, 2026"
-
-// Short date
-Text(date, style: .date)
-// Output: "1/23/26"
-```
-
-### Time Formatting
-
-```swift
-let date = Date()
-
-// Time only
-Text(date, format: .dateTime.hour().minute())
-// Output: "2:30 PM"
-
-// With seconds
-Text(date, format: .dateTime.hour().minute().second())
-// Output: "2:30:45 PM"
-
-// 24-hour format
-Text(date, format: .dateTime.hour(.defaultDigits(amPM: .omitted)).minute())
-// Output: "14:30"
-```
-
-### Relative Date Formatting
-
-```swift
-let futureDate = Date().addingTimeInterval(3600)
-
-// Relative formatting
-Text(futureDate, style: .relative)
-// Output: "in 1 hour"
-
-Text(futureDate, style: .timer)
-// Output: "59:59" (counts down)
-```
-
-## String Searching and Comparison
-
-### Localized String Comparison
-
-**Use `localizedStandardContains()` for user-input filtering, not `contains()`.**
-
-```swift
-let searchText = "café"
-let items = ["Café Latte", "Coffee", "Tea"]
-
-// Correct - handles diacritics and case
-let filtered = items.filter { $0.localizedStandardContains(searchText) }
-// Matches "Café Latte"
-
-// Wrong - exact match only
-let filtered = items.filter { $0.contains(searchText) }
-// Might not match "Café Latte" depending on normalization
-```
-
-**Why**: `localizedStandardContains()` handles case-insensitive, diacritic-insensitive matching appropriate for user-facing search.
-
-### Case-Insensitive Comparison
-
-```swift
-let text = "Hello World"
-let search = "hello"
-
-// Correct - case-insensitive
-if text.localizedCaseInsensitiveContains(search) {
- // Match found
-}
-
-// Also correct - for exact comparison
-if text.lowercased() == search.lowercased() {
- // Equal
-}
-```
-
-### Localized Sorting
-
-```swift
-let names = ["Zoë", "Zara", "Åsa"]
-
-// Correct - locale-aware sorting
-let sorted = names.sorted { $0.localizedStandardCompare($1) == .orderedAscending }
-// Output: ["Åsa", "Zara", "Zoë"]
-
-// Wrong - byte-wise sorting
-let sorted = names.sorted()
-// Output may not be correct for all locales
-```
-
-## Attributed Strings
-
-### Basic Attributed Text
-
-```swift
-// Using Text concatenation
-Text("Hello ")
- .foregroundStyle(.primary)
-+ Text("World")
- .foregroundStyle(.blue)
- .bold()
-
-// Using AttributedString
-var attributedString = AttributedString("Hello World")
-attributedString.foregroundColor = .primary
-if let range = attributedString.range(of: "World") {
- attributedString[range].foregroundColor = .blue
- attributedString[range].font = .body.bold()
-}
-Text(attributedString)
-```
-
-### Markdown in Text
-
-```swift
-// Simple markdown
-Text("This is **bold** and this is *italic*")
-
-// With links
-Text("Visit [Apple](https://apple.com) for more info")
-
-// Multiline markdown
-Text("""
-# Title
-This is a paragraph with **bold** text.
-- Item 1
-- Item 2
-""")
-```
-
-## Text Measurement
-
-### Measuring Text Height
-
-```swift
-// Wrong (Legacy) - GeometryReader trick
-struct MeasuredText: View {
- let text: String
- @State private var textHeight: CGFloat = 0
-
- var body: some View {
- Text(text)
- .background(
- GeometryReader { geometry in
- Color.clear
- .onAppear {
- textWidth = geometry.size.height
- }
- }
- )
- }
-}
-
-// Modern (correct)
-struct MeasuredText: View {
- let text: String
- @State private var textHeight: CGFloat = 0
-
- var body: some View {
- Text(text)
- .onGeometryChange(for: CGFloat.self) { geometry in
- geometry.size.height
- } action: { newValue in
- textHeight = newValue
- }
- }
-}
-```
-
-## Summary Checklist
-
-- [ ] Use `.format` parameters with Text instead of `String(format:)`
-- [ ] Use `.currency(code:)` for currency formatting
-- [ ] Use `.percent` for percentage formatting
-- [ ] Use `.dateTime` for date/time formatting
-- [ ] Use `localizedStandardContains()` for user-input search
-- [ ] Use `localizedStandardCompare()` for locale-aware sorting
-- [ ] Use Text concatenation or AttributedString for styled text
-- [ ] Use markdown syntax for simple text formatting
-- [ ] All formatting respects user's locale and preferences
-
-**Why**: Modern format parameters are type-safe, localization-aware, and integrate better with SwiftUI's text rendering.
diff --git a/.agents/skills/swiftui-expert-skill/references/view-structure.md b/.agents/skills/swiftui-expert-skill/references/view-structure.md
deleted file mode 100644
index 43e88617..00000000
--- a/.agents/skills/swiftui-expert-skill/references/view-structure.md
+++ /dev/null
@@ -1,276 +0,0 @@
-# SwiftUI View Structure Reference
-
-## View Structure Principles
-
-SwiftUI's diffing algorithm compares view hierarchies to determine what needs updating. Proper view composition directly impacts performance.
-
-## Prefer Modifiers Over Conditional Views
-
-**Prefer "no-effect" modifiers over conditionally including views.** When you introduce a branch, consider whether you're representing multiple views or two states of the same view.
-
-### Use Opacity Instead of Conditional Inclusion
-
-```swift
-// Good - same view, different states
-SomeView()
- .opacity(isVisible ? 1 : 0)
-
-// Avoid - creates/destroys view identity
-if isVisible {
- SomeView()
-}
-```
-
-**Why**: Conditional view inclusion can cause loss of state, poor animation performance, and breaks view identity. Using modifiers maintains view identity across state changes.
-
-### When Conditionals Are Appropriate
-
-Use conditionals when you truly have **different views**, not different states:
-
-```swift
-// Correct - fundamentally different views
-if isLoggedIn {
- DashboardView()
-} else {
- LoginView()
-}
-
-// Correct - optional content
-if let user {
- UserProfileView(user: user)
-}
-```
-
-## Extract Subviews, Not Computed Properties
-
-### The Problem with @ViewBuilder Functions
-
-When you use `@ViewBuilder` functions or computed properties for complex views, the entire function re-executes on every parent state change:
-
-```swift
-// BAD - re-executes complexSection() on every tap
-struct ParentView: View {
- @State private var count = 0
-
- var body: some View {
- VStack {
- Button("Tap: \(count)") { count += 1 }
- complexSection() // Re-executes every tap!
- }
- }
-
- @ViewBuilder
- func complexSection() -> some View {
- // Complex views that re-execute unnecessarily
- ForEach(0..<100) { i in
- HStack {
- Image(systemName: "star")
- Text("Item \(i)")
- Spacer()
- Text("Detail")
- }
- }
- }
-}
-```
-
-### The Solution: Separate Structs
-
-Extract to separate `struct` views. SwiftUI can skip their `body` when inputs don't change:
-
-```swift
-// GOOD - ComplexSection body SKIPPED when its inputs don't change
-struct ParentView: View {
- @State private var count = 0
-
- var body: some View {
- VStack {
- Button("Tap: \(count)") { count += 1 }
- ComplexSection() // Body skipped during re-evaluation
- }
- }
-}
-
-struct ComplexSection: View {
- var body: some View {
- ForEach(0..<100) { i in
- HStack {
- Image(systemName: "star")
- Text("Item \(i)")
- Spacer()
- Text("Detail")
- }
- }
- }
-}
-```
-
-### Why This Works
-
-1. SwiftUI compares the `ComplexSection` struct (which has no properties)
-2. Since nothing changed, SwiftUI skips calling `ComplexSection.body`
-3. The complex view code never executes unnecessarily
-
-## When @ViewBuilder Functions Are Acceptable
-
-Use for small, simple sections that don't affect performance:
-
-```swift
-struct SimpleView: View {
- @State private var showDetails = false
-
- var body: some View {
- VStack {
- headerSection() // OK - simple, few views
- if showDetails {
- detailsSection()
- }
- }
- }
-
- @ViewBuilder
- private func headerSection() -> some View {
- HStack {
- Text("Title")
- Spacer()
- Button("Toggle") { showDetails.toggle() }
- }
- }
-
- @ViewBuilder
- private func detailsSection() -> some View {
- Text("Some details here")
- .font(.caption)
- }
-}
-```
-
-## When to Extract Subviews
-
-Extract complex views into separate subviews when:
-- The view has multiple logical sections or responsibilities
-- The view contains reusable components
-- The view body becomes difficult to read or understand
-- You need to isolate state changes for performance
-- The view is becoming large (keep views small for better performance)
-
-## Container View Pattern
-
-### Avoid Closure-Based Content
-
-Closures can't be compared, causing unnecessary re-renders:
-
-```swift
-// BAD - closure prevents SwiftUI from skipping updates
-struct MyContainer: View {
- let content: () -> Content
-
- var body: some View {
- VStack {
- Text("Header")
- content() // Always called, can't compare closures
- }
- }
-}
-
-// Usage forces re-render on every parent update
-MyContainer {
- ExpensiveView()
-}
-```
-
-### Use @ViewBuilder Property Instead
-
-```swift
-// GOOD - view can be compared
-struct MyContainer: View {
- @ViewBuilder let content: Content
-
- var body: some View {
- VStack {
- Text("Header")
- content // SwiftUI can compare and skip if unchanged
- }
- }
-}
-
-// Usage - SwiftUI can diff ExpensiveView
-MyContainer {
- ExpensiveView()
-}
-```
-
-## ZStack vs overlay/background
-
-Use `ZStack` to **compose multiple peer views** that should be layered together and jointly define layout.
-
-Prefer `overlay` / `background` when you’re **decorating a primary view**.
-Not primarily because they don’t affect layout size, but because they **express intent and improve readability**: the view being modified remains the clear layout anchor.
-
-A key difference is **size proposal behavior**:
-- In `overlay` / `background`, the child view implicitly adopts the size proposed to the parent when it doesn’t define its own size, making decorative attachments feel natural and predictable.
-- In `ZStack`, each child participates independently in layout, and no implicit size inheritance exists. This makes it better suited for peer composition, but less intuitive for simple decoration.
-
-Use `ZStack` (or another container) when the “decoration” **must explicitly participate in layout sizing**—for example, when reserving space, extending tappable/visible bounds, or preventing overlap with neighboring views.
-
-### Examples: Choosing Between overlay/background and ZStack
-
-```swift
-// GOOD - correct usage
-// Decoration that should not change layout sizing belongs in overlay/background
-Button("Continue") {
- // action
-}
-.overlay(alignment: .trailing) {
- Image(systemName: "lock.fill")
- .padding(.trailing, 8)
-}
-
-// BAD - incorrect usage
-// Using ZStack when overlay/background is enough and layout sizing should remain anchored to the button
-ZStack(alignment: .trailing) {
- Button("Continue") {
- // action
- }
- Image(systemName: "lock.fill")
- .padding(.trailing, 8)
-}
-
-// GOOD - correct usage
-// Capsule is taking a parent size for rendering
-HStack(spacing: 12) {
- HStack {
- Image(systemName: "tray")
- Text("Inbox")
- }
- Text("Next")
-}
-.background {
- Capsule()
- .strokeBorder(.blue, lineWidth: 2)
-}
-
-// BAD - incorrect usage
-// overlay does not contribute to measured size, so the Capsule is taking all available space if no explicit size is set
-ZStack(alignment: .topTrailing) {
- HStack(spacing: 12) {
- HStack {
- Image(systemName: "tray")
- Text("Inbox")
- }
- Text("Next")
- }
-
- Capsule()
- .strokeBorder(.blue, lineWidth: 2)
-}
-```
-
-## Summary Checklist
-
-- [ ] Prefer modifiers over conditional views for state changes
-- [ ] Complex views extracted to separate subviews
-- [ ] Views kept small for better performance
-- [ ] `@ViewBuilder` functions only for simple sections
-- [ ] Container views use `@ViewBuilder let content: Content`
-- [ ] Extract views when they have multiple responsibilities or become hard to read
diff --git a/.agents/skills/swiftui-performance-audit/SKILL.md b/.agents/skills/swiftui-performance-audit/SKILL.md
deleted file mode 100644
index c28123fc..00000000
--- a/.agents/skills/swiftui-performance-audit/SKILL.md
+++ /dev/null
@@ -1,203 +0,0 @@
----
-name: swiftui-performance-audit
-description: Audit and improve SwiftUI runtime performance from code review and architecture. Use for requests to diagnose slow rendering, janky scrolling, high CPU/memory usage, excessive view updates, or layout thrash in SwiftUI apps, and to provide guidance for user-run Instruments profiling when code review alone is insufficient.
----
-
-# SwiftUI Performance Audit
-
-## Overview
-
-Audit SwiftUI view performance end-to-end, from instrumentation and baselining to root-cause analysis and concrete remediation steps.
-
-## Workflow Decision Tree
-
-- If the user provides code, start with "Code-First Review."
-- If the user only describes symptoms, ask for minimal code/context, then do "Code-First Review."
-- If code review is inconclusive, go to "Guide the User to Profile" and ask for a trace or screenshots.
-
-## 1. Code-First Review
-
-Collect:
-- Target view/feature code.
-- Data flow: state, environment, observable models.
-- Symptoms and reproduction steps.
-
-Focus on:
-- View invalidation storms from broad state changes.
-- Unstable identity in lists (`id` churn, `UUID()` per render).
-- Top-level conditional view swapping (`if/else` returning different root branches).
-- Heavy work in `body` (formatting, sorting, image decoding).
-- Layout thrash (deep stacks, `GeometryReader`, preference chains).
-- Large images without downsampling or resizing.
-- Over-animated hierarchies (implicit animations on large trees).
-
-Provide:
-- Likely root causes with code references.
-- Suggested fixes and refactors.
-- If needed, a minimal repro or instrumentation suggestion.
-
-## 2. Guide the User to Profile
-
-Explain how to collect data with Instruments:
-- Use the SwiftUI template in Instruments (Release build).
-- Reproduce the exact interaction (scroll, navigation, animation).
-- Capture SwiftUI timeline and Time Profiler.
-- Export or screenshot the relevant lanes and the call tree.
-
-Ask for:
-- Trace export or screenshots of SwiftUI lanes + Time Profiler call tree.
-- Device/OS/build configuration.
-
-## 3. Analyze and Diagnose
-
-Prioritize likely SwiftUI culprits:
-- View invalidation storms from broad state changes.
-- Unstable identity in lists (`id` churn, `UUID()` per render).
-- Top-level conditional view swapping (`if/else` returning different root branches).
-- Heavy work in `body` (formatting, sorting, image decoding).
-- Layout thrash (deep stacks, `GeometryReader`, preference chains).
-- Large images without downsampling or resizing.
-- Over-animated hierarchies (implicit animations on large trees).
-
-Summarize findings with evidence from traces/logs.
-
-## 4. Remediate
-
-Apply targeted fixes:
-- Narrow state scope (`@State`/`@Observable` closer to leaf views).
-- Stabilize identities for `ForEach` and lists.
-- Move heavy work out of `body` (precompute, cache, `@State`).
-- Use `equatable()` or value wrappers for expensive subtrees.
-- Downsample images before rendering.
-- Reduce layout complexity or use fixed sizing where possible.
-
-## Common Code Smells (and Fixes)
-
-Look for these patterns during code review.
-
-### Expensive formatters in `body`
-
-```swift
-var body: some View {
- let number = NumberFormatter() // slow allocation
- let measure = MeasurementFormatter() // slow allocation
- Text(measure.string(from: .init(value: meters, unit: .meters)))
-}
-```
-
-Prefer cached formatters in a model or a dedicated helper:
-
-```swift
-final class DistanceFormatter {
- static let shared = DistanceFormatter()
- let number = NumberFormatter()
- let measure = MeasurementFormatter()
-}
-```
-
-### Computed properties that do heavy work
-
-```swift
-var filtered: [Item] {
- items.filter { $0.isEnabled } // runs on every body eval
-}
-```
-
-Prefer precompute or cache on change:
-
-```swift
-@State private var filtered: [Item] = []
-// update filtered when inputs change
-```
-
-### Sorting/filtering in `body` or `ForEach`
-
-```swift
-List {
- ForEach(items.sorted(by: sortRule)) { item in
- Row(item)
- }
-}
-```
-
-Prefer sort once before view updates:
-
-```swift
-let sortedItems = items.sorted(by: sortRule)
-```
-
-### Inline filtering in `ForEach`
-
-```swift
-ForEach(items.filter { $0.isEnabled }) { item in
- Row(item)
-}
-```
-
-Prefer a prefiltered collection with stable identity.
-
-### Unstable identity
-
-```swift
-ForEach(items, id: \.self) { item in
- Row(item)
-}
-```
-
-Avoid `id: \.self` for non-stable values; use a stable ID.
-
-### Top-level conditional view swapping
-
-```swift
-var content: some View {
- if isEditing {
- editingView
- } else {
- readOnlyView
- }
-}
-```
-
-Prefer one stable base view and localize conditions to sections/modifiers (for example inside `toolbar`, row content, `overlay`, or `disabled`). This reduces root identity churn and helps SwiftUI diffing stay efficient.
-
-### Image decoding on the main thread
-
-```swift
-Image(uiImage: UIImage(data: data)!)
-```
-
-Prefer decode/downsample off the main thread and store the result.
-
-### Broad dependencies in observable models
-
-```swift
-@Observable class Model {
- var items: [Item] = []
-}
-
-var body: some View {
- Row(isFavorite: model.items.contains(item))
-}
-```
-
-Prefer granular view models or per-item state to reduce update fan-out.
-
-## 5. Verify
-
-Ask the user to re-run the same capture and compare with baseline metrics.
-Summarize the delta (CPU, frame drops, memory peak) if provided.
-
-## Outputs
-
-Provide:
-- A short metrics table (before/after if available).
-- Top issues (ordered by impact).
-- Proposed fixes with estimated effort.
-
-## References
-
-Add Apple documentation and WWDC resources under `references/` as they are supplied by the user.
-- Optimizing SwiftUI performance with Instruments: `references/optimizing-swiftui-performance-instruments.md`
-- Understanding and improving SwiftUI performance: `references/understanding-improving-swiftui-performance.md`
-- Understanding hangs in your app: `references/understanding-hangs-in-your-app.md`
-- Demystify SwiftUI performance (WWDC23): `references/demystify-swiftui-performance-wwdc23.md`
diff --git a/.agents/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md b/.agents/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md
deleted file mode 100644
index 18de77c5..00000000
--- a/.agents/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Demystify SwiftUI Performance (WWDC23) (Summary)
-
-Context: WWDC23 session on building a mental model for SwiftUI performance and triaging hangs/hitches.
-
-## Performance loop
-
-- Measure -> Identify -> Optimize -> Re-measure.
-- Focus on concrete symptoms (slow navigation, broken animations, spinning cursor).
-
-## Dependencies and updates
-
-- Views form a dependency graph; dynamic properties are a frequent source of updates.
-- Use `Self._printChanges()` in debug only to inspect extra dependencies.
-- Eliminate unnecessary dependencies by extracting views or narrowing state.
-- Consider `@Observable` for more granular property tracking.
-
-## Common causes of slow updates
-
-- Expensive view bodies (string interpolation, filtering, formatting).
-- Dynamic property instantiation and state initialization in `body`.
-- Slow identity resolution in lists/tables.
-- Hidden work: bundle lookups, heap allocations, repeated string construction.
-
-## Avoid slow initialization in view bodies
-
-- Don’t create heavy models synchronously in view bodies.
-- Use `.task` to fetch async data and keep `init` lightweight.
-
-## Lists and tables identity rules
-
-- Stable identity is critical for performance and animation.
-- Ensure a constant number of views per element in `ForEach`.
-- Avoid inline filtering in `ForEach`; pre-filter and cache collections.
-- Avoid `AnyView` in list rows; it hides identity and increases cost.
-- Flatten nested `ForEach` when possible to reduce overhead.
-
-## Table specifics
-
-- `TableRow` resolves to a single row; row count must be constant.
-- Prefer the streamlined `Table` initializer to enforce constant rows.
-- Use explicit IDs for back deployment when needed.
-
-## Debugging aids
-
-- Use Instruments for hangs and hitches.
-- Use `_printChanges` to validate dependency assumptions during debug.
diff --git a/.agents/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md b/.agents/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md
deleted file mode 100644
index 3080feb7..00000000
--- a/.agents/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Optimizing SwiftUI Performance with Instruments (Summary)
-
-Context: WWDC session introducing the next-generation SwiftUI Instrument in Instruments 26 and how to diagnose SwiftUI-specific bottlenecks.
-
-## Key takeaways
-
-- Profile SwiftUI issues with the SwiftUI template (SwiftUI instrument + Time Profiler + Hangs/Hitches).
-- Long view body updates are a common bottleneck; use "Long View Body Updates" to identify slow bodies.
-- Set inspection range on a long update and correlate with Time Profiler to find expensive frames.
-- Keep work out of `body`: move formatting, sorting, image decoding, and other expensive work into cached or precomputed paths.
-- Use Cause & Effect Graph to diagnose *why* updates occur; SwiftUI is declarative, so backtraces are often unhelpful.
-- Avoid broad dependencies that trigger many updates (e.g., `@Observable` arrays or global environment reads).
-- Prefer granular view models and scoped state so only the affected view updates.
-- Environment values update checks still cost time; avoid placing fast-changing values (timers, geometry) in environment.
-- Profile early and often during feature development to catch regressions.
-
-## Suggested workflow (condensed)
-
-1. Record a trace in Release mode using the SwiftUI template.
-2. Inspect "Long View Body Updates" and "Other Long Updates."
-3. Zoom into a long update, then inspect Time Profiler for hot frames.
-4. Fix slow body work by moving heavy logic into precomputed/cache paths.
-5. Use Cause & Effect Graph to identify unintended update fan-out.
-6. Re-record and compare the update counts and hitch frequency.
-
-## Example patterns from the session
-
-- Caching formatted distance strings in a location manager instead of computing in `body`.
-- Replacing a dependency on a global favorites array with per-item view models to reduce update fan-out.
diff --git a/.agents/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md b/.agents/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md
deleted file mode 100644
index 2dab5e13..00000000
--- a/.agents/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Understanding Hangs in Your App (Summary)
-
-Context: Apple guidance on identifying hangs caused by long-running main-thread work and understanding the main run loop.
-
-## Key concepts
-
-- A hang is a noticeable delay in a discrete interaction (typically >100 ms).
-- Hangs almost always come from long-running work on the main thread.
-- The main run loop processes UI events, timers, and main-queue work sequentially.
-
-## Main-thread work stages
-
-- Event delivery to the correct view/handler.
-- Your code: state updates, data fetch, UI changes.
-- Core Animation commit to the render server.
-
-## Why the main run loop matters
-
-- Only the main thread can update UI safely.
-- The run loop is the foundation that executes main-queue work.
-- If the run loop is busy, it can’t handle new events; this causes hangs.
-
-## Diagnosing hangs
-
-- Observe the main run loop’s busy periods: healthy loops sleep most of the time.
-- Hang detection typically flags busy periods >250 ms.
-- The Hangs instrument can be configured to lower thresholds.
-
-## Practical takeaways
-
-- Keep main-thread work short; offload heavy work from event handlers.
-- Avoid long-running tasks on the main dispatch queue or main actor.
-- Use run loop behavior as a proxy for user-perceived responsiveness.
diff --git a/.agents/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md b/.agents/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md
deleted file mode 100644
index 2281f06b..00000000
--- a/.agents/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# Understanding and Improving SwiftUI Performance (Summary)
-
-Context: Apple guidance on diagnosing SwiftUI performance with Instruments and applying design patterns to reduce long or frequent updates.
-
-## Core concepts
-
-- SwiftUI is declarative; view updates are driven by state, environment, and observable data dependencies.
-- View bodies must compute quickly to meet frame deadlines; slow or frequent updates lead to hitches.
-- Instruments is the primary tool to find long-running updates and excessive update frequency.
-
-## Instruments workflow
-
-1. Profile via Product > Profile.
-2. Choose the SwiftUI template and record.
-3. Exercise the target interaction.
-4. Stop recording and inspect the SwiftUI track + Time Profiler.
-
-## SwiftUI timeline lanes
-
-- Update Groups: overview of time SwiftUI spends calculating updates.
-- Long View Body Updates: orange >500us, red >1000us.
-- Long Platform View Updates: AppKit/UIKit hosting in SwiftUI.
-- Other Long Updates: geometry/text/layout and other SwiftUI work.
-- Hitches: frame misses where UI wasn’t ready in time.
-
-## Diagnose long view body updates
-
-- Expand the SwiftUI track; inspect module-specific subtracks.
-- Set Inspection Range and correlate with Time Profiler.
-- Use call tree or flame graph to identify expensive frames.
-- Repeat the update to gather enough samples for analysis.
-- Filter to a specific update (Show Calls Made by `MySwiftUIView.body`).
-
-## Diagnose frequent updates
-
-- Use Update Groups to find long active groups without long updates.
-- Set inspection range on the group and analyze update counts.
-- Use Cause graph ("Show Causes") to see what triggers updates.
-- Compare causes with expected data flow; prioritize the highest-frequency causes.
-
-## Remediation patterns
-
-- Move expensive work out of `body` and cache results.
-- Use `Observable()` macro to scope dependencies to properties actually read.
-- Avoid broad dependencies that fan out updates to many views.
-- Reduce layout churn; isolate state-dependent subtrees from layout readers.
-- Avoid storing closures that capture parent state; precompute child views.
-- Gate frequent updates (e.g., geometry changes) by thresholds.
-
-## Verification
-
-- Re-record after changes to confirm reduced update counts and fewer hitches.
diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
new file mode 100644
index 00000000..d87b0fbb
--- /dev/null
+++ b/.claude-plugin/marketplace.json
@@ -0,0 +1,16 @@
+{
+ "name": "hack-dance",
+ "description": "Official Hack plugins for local-first development workflows.",
+ "owner": {
+ "name": "Hack Dance LLC"
+ },
+ "plugins": [
+ {
+ "name": "hack",
+ "source": "./plugins/hack",
+ "description": "Operate and onboard Hack-managed local development environments.",
+ "category": "development",
+ "tags": ["local-development", "docker", "developer-tools", "agents"]
+ }
+ ]
+}
diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
deleted file mode 100644
index 0a7bb422..00000000
--- a/.claude/CLAUDE.md
+++ /dev/null
@@ -1,123 +0,0 @@
-# Ultracite Code Standards
-
-This project uses **Ultracite**, a zero-config preset that enforces strict code quality standards through automated formatting and linting.
-
-## Quick Reference
-
-- **Format code**: `bun x ultracite fix`
-- **Check for issues**: `bun x ultracite check`
-- **Diagnose setup**: `bun x ultracite doctor`
-
-Biome (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable.
-
----
-
-## Core Principles
-
-Write code that is **accessible, performant, type-safe, and maintainable**. Focus on clarity and explicit intent over brevity.
-
-### Type Safety & Explicitness
-
-- Use explicit types for function parameters and return values when they enhance clarity
-- Prefer `unknown` over `any` when the type is genuinely unknown
-- Use const assertions (`as const`) for immutable values and literal types
-- Leverage TypeScript's type narrowing instead of type assertions
-- Use meaningful variable names instead of magic numbers - extract constants with descriptive names
-
-### Modern JavaScript/TypeScript
-
-- Use arrow functions for callbacks and short functions
-- Prefer `for...of` loops over `.forEach()` and indexed `for` loops
-- Use optional chaining (`?.`) and nullish coalescing (`??`) for safer property access
-- Prefer template literals over string concatenation
-- Use destructuring for object and array assignments
-- Use `const` by default, `let` only when reassignment is needed, never `var`
-
-### Async & Promises
-
-- Always `await` promises in async functions - don't forget to use the return value
-- Use `async/await` syntax instead of promise chains for better readability
-- Handle errors appropriately in async code with try-catch blocks
-- Don't use async functions as Promise executors
-
-### React & JSX
-
-- Use function components over class components
-- Call hooks at the top level only, never conditionally
-- Specify all dependencies in hook dependency arrays correctly
-- Use the `key` prop for elements in iterables (prefer unique IDs over array indices)
-- Nest children between opening and closing tags instead of passing as props
-- Don't define components inside other components
-- Use semantic HTML and ARIA attributes for accessibility:
- - Provide meaningful alt text for images
- - Use proper heading hierarchy
- - Add labels for form inputs
- - Include keyboard event handlers alongside mouse events
- - Use semantic elements (`