diff --git a/ios/ChronoMind/Shared/Notifications/NotificationScheduler.swift b/ios/ChronoMind/Shared/Notifications/NotificationScheduler.swift
new file mode 100644
index 0000000..faac9c6
--- /dev/null
+++ b/ios/ChronoMind/Shared/Notifications/NotificationScheduler.swift
@@ -0,0 +1,188 @@
+// ── Notification Scheduling ────────────────────────────────────
+// UNUserNotificationCenter wrapper for pre-warning cascade + timer fire
+// iOS-native notification system
+
+import Foundation
+import UserNotifications
+
+// MARK: - Notification Manager
+
+@MainActor
+final class CMNotificationManager: ObservableObject {
+ static let shared = CMNotificationManager()
+
+ @Published var isAuthorized = false
+
+ private init() {}
+
+ // MARK: - Permission
+
+ func requestPermission() async {
+ do {
+ let granted = try await UNUserNotificationCenter.current()
+ .requestAuthorization(options: [.alert, .sound, .badge, .criticalAlert])
+ isAuthorized = granted
+ } catch {
+ isAuthorized = false
+ }
+ }
+
+ func checkPermission() async {
+ let settings = await UNUserNotificationCenter.current().notificationSettings()
+ isAuthorized = settings.authorizationStatus == .authorized
+ }
+
+ // MARK: - Schedule Timer Notifications
+
+ /// Schedule all cascade warnings + final fire notification for a timer
+ func scheduleNotifications(for timer: CMTimer) {
+ // Remove existing notifications for this timer
+ removeNotifications(for: timer.id)
+
+ // Schedule cascade warnings
+ for warning in timer.warnings where !warning.fired {
+ scheduleWarning(timer: timer, warning: warning)
+ }
+
+ // Schedule fire notification
+ scheduleFireNotification(timer: timer)
+ }
+
+ /// Remove all scheduled notifications for a timer
+ func removeNotifications(for timerId: String) {
+ let center = UNUserNotificationCenter.current()
+ // Remove all notifications with this timer's ID prefix
+ center.getPendingNotificationRequests { requests in
+ let ids = requests
+ .filter { $0.identifier.hasPrefix("cm-\(timerId)") }
+ .map { $0.identifier }
+ center.removePendingNotificationRequests(withIdentifiers: ids)
+ }
+ }
+
+ /// Remove all ChronoMind notifications
+ func removeAllNotifications() {
+ UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
+ }
+
+ // MARK: - Private
+
+ private func scheduleWarning(timer: CMTimer, warning: CascadeWarning) {
+ let content = UNMutableNotificationContent()
+ let timeStr = formatMinutesBefore(warning.minutesBefore)
+ content.title = "⏰ \(timer.label) in \(timeStr)"
+ content.body = "Pre-warning: \"\(timer.label)\" fires in \(timeStr)"
+ content.sound = soundForUrgency(timer.urgency, isWarning: true)
+ content.categoryIdentifier = "TIMER_WARNING"
+ content.userInfo = [
+ "timerId": timer.id,
+ "warningId": warning.id,
+ "type": "warning",
+ ]
+
+ // Use the warning's scheduled time
+ let trigger = UNTimeIntervalNotificationTrigger(
+ timeInterval: max(1, warning.scheduledTime.timeIntervalSinceNow),
+ repeats: false
+ )
+
+ let request = UNNotificationRequest(
+ identifier: "cm-\(timer.id)-warning-\(warning.id)",
+ content: content,
+ trigger: trigger
+ )
+
+ UNUserNotificationCenter.current().add(request)
+ }
+
+ private func scheduleFireNotification(timer: CMTimer) {
+ let content = UNMutableNotificationContent()
+ content.title = "🔔 \(timer.label) — NOW!"
+ content.body = "Timer \"\(timer.label)\" is firing!"
+ content.sound = soundForUrgency(timer.urgency, isWarning: false)
+ content.categoryIdentifier = "TIMER_FIRE"
+ content.userInfo = [
+ "timerId": timer.id,
+ "type": "fire",
+ ]
+
+ // Interrupt level for important/critical
+ if #available(iOS 15.0, *) {
+ switch timer.urgency {
+ case .critical:
+ content.interruptionLevel = .critical
+ case .important:
+ content.interruptionLevel = .timeSensitive
+ case .standard:
+ content.interruptionLevel = .active
+ case .gentle:
+ content.interruptionLevel = .passive
+ case .passive:
+ content.interruptionLevel = .passive
+ }
+ }
+
+ let trigger = UNTimeIntervalNotificationTrigger(
+ timeInterval: max(1, timer.targetTime.timeIntervalSinceNow),
+ repeats: false
+ )
+
+ let request = UNNotificationRequest(
+ identifier: "cm-\(timer.id)-fire",
+ content: content,
+ trigger: trigger
+ )
+
+ UNUserNotificationCenter.current().add(request)
+ }
+
+ private func soundForUrgency(_ urgency: UrgencyLevel, isWarning: Bool) -> UNNotificationSound {
+ let config = getUrgencyConfig(urgency)
+ guard config.soundEnabled else { return .none }
+
+ if urgency == .critical && !isWarning {
+ // Critical fires get the default critical alert sound
+ if #available(iOS 12.0, *) {
+ return .defaultCritical
+ }
+ }
+
+ return .default
+ }
+
+ // MARK: - Notification Categories
+
+ func registerCategories() {
+ let snooze5 = UNNotificationAction(
+ identifier: "SNOOZE_5",
+ title: "Snooze 5m",
+ options: []
+ )
+ let snooze15 = UNNotificationAction(
+ identifier: "SNOOZE_15",
+ title: "Snooze 15m",
+ options: []
+ )
+ let dismiss = UNNotificationAction(
+ identifier: "DISMISS",
+ title: "Dismiss",
+ options: [.destructive]
+ )
+
+ let fireCategory = UNNotificationCategory(
+ identifier: "TIMER_FIRE",
+ actions: [snooze5, snooze15, dismiss],
+ intentIdentifiers: [],
+ options: []
+ )
+
+ let warningCategory = UNNotificationCategory(
+ identifier: "TIMER_WARNING",
+ actions: [dismiss],
+ intentIdentifiers: [],
+ options: []
+ )
+
+ UNUserNotificationCenter.current().setNotificationCategories([fireCategory, warningCategory])
+ }
+}
diff --git a/ios/ChronoMind/Shared/Theme/ChronoMindTheme.swift b/ios/ChronoMind/Shared/Theme/ChronoMindTheme.swift
new file mode 100644
index 0000000..6b9e05f
--- /dev/null
+++ b/ios/ChronoMind/Shared/Theme/ChronoMindTheme.swift
@@ -0,0 +1,117 @@
+// ── ChronoMind Design Tokens ───────────────────────────────────
+// Colors from PRD Section 9 (--cm-* CSS vars → Swift Color literals)
+// Typography from PRD Section 9.2
+
+import SwiftUI
+
+// MARK: - Colors
+
+enum CMColors {
+ // Backgrounds
+ static let bg = Color(hex: 0x0A0B0F)
+ static let surface = Color(hex: 0x12141D)
+ static let surfaceHover = Color(hex: 0x1A1D2A)
+ static let border = Color(hex: 0x2A2D3A)
+
+ // Text
+ static let text = Color(hex: 0xE8ECF4)
+ static let textSecondary = Color(hex: 0x8A92A6)
+ static let textMuted = Color(hex: 0x5A6178)
+
+ // Urgency
+ static let critical = Color(hex: 0xFF4757)
+ static let important = Color(hex: 0xFF9F43)
+ static let standard = Color(hex: 0xFECA57)
+ static let gentle = Color(hex: 0x2ED573)
+ static let passive = Color(hex: 0x5B8DEE)
+
+ // Accent
+ static let accent = Color(hex: 0x5B8DEE)
+ static let accentGlow = Color(hex: 0x5B8DEE, alpha: 0.2)
+
+ // Semantic
+ static let success = Color(hex: 0x2ED573)
+ static let warning = Color(hex: 0xFECA57)
+ static let error = Color(hex: 0xFF4757)
+
+ // Light theme variants
+ static let lightBg = Color(hex: 0xF8F9FC)
+ static let lightSurface = Color.white
+ static let lightSurfaceHover = Color(hex: 0xF0F2F8)
+ static let lightBorder = Color(hex: 0xE2E5EE)
+ static let lightText = Color(hex: 0x1A1D2A)
+ static let lightTextSecondary = Color(hex: 0x5A6178)
+ static let lightTextMuted = Color(hex: 0x8A92A6)
+
+ /// Get urgency color for a given level
+ static func urgencyColor(_ level: UrgencyLevel) -> Color {
+ switch level {
+ case .critical: return critical
+ case .important: return important
+ case .standard: return standard
+ case .gentle: return gentle
+ case .passive: return passive
+ }
+ }
+
+ /// Get urgency background color (15% opacity)
+ static func urgencyBg(_ level: UrgencyLevel) -> Color {
+ urgencyColor(level).opacity(0.15)
+ }
+
+ /// Get urgency border color (40% opacity)
+ static func urgencyBorder(_ level: UrgencyLevel) -> Color {
+ urgencyColor(level).opacity(0.4)
+ }
+}
+
+// MARK: - Typography
+
+enum CMFonts {
+ /// Display font — Space Grotesk (clock face, timer digits)
+ static func display(size: CGFloat, weight: Font.Weight = .bold) -> Font {
+ .system(size: size, weight: weight, design: .rounded)
+ }
+
+ /// Body font — Inter/SF Pro equivalent
+ static func body(size: CGFloat, weight: Font.Weight = .regular) -> Font {
+ .system(size: size, weight: weight, design: .default)
+ }
+
+ /// Mono font — JetBrains Mono (countdown digits, time displays)
+ static func mono(size: CGFloat, weight: Font.Weight = .regular) -> Font {
+ .system(size: size, weight: weight, design: .monospaced)
+ }
+}
+
+// MARK: - Spacing
+
+enum CMSpacing {
+ static let xxs: CGFloat = 2
+ static let xs: CGFloat = 4
+ static let sm: CGFloat = 8
+ static let md: CGFloat = 12
+ static let lg: CGFloat = 16
+ static let xl: CGFloat = 24
+ static let xxl: CGFloat = 32
+ static let xxxl: CGFloat = 48
+}
+
+// MARK: - Corner Radius
+
+enum CMRadius {
+ static let sm: CGFloat = 8
+ static let md: CGFloat = 12
+ static let lg: CGFloat = 16
+ static let xl: CGFloat = 20
+ static let full: CGFloat = 999
+}
+
+// MARK: - Shadows
+
+enum CMShadow {
+ static let sm = Color.black.opacity(0.1)
+ static let md = Color.black.opacity(0.2)
+ static let lg = Color.black.opacity(0.3)
+ static let glow = CMColors.accent.opacity(0.3)
+}
diff --git a/web/src/components/Dashboard.tsx b/web/src/components/Dashboard.tsx
index 854f379..877cb0f 100644
--- a/web/src/components/Dashboard.tsx
+++ b/web/src/components/Dashboard.tsx
@@ -11,7 +11,8 @@ import { CreateTimerModal } from './CreateTimerModal';
import { AlarmOverlay } from './AlarmOverlay';
import { requestNotificationPermission } from '@/lib/notifications';
import { formatTime, formatDate } from '@/lib/format';
-import { Plus, Clock, Bell, Keyboard, Sun, Moon } from 'lucide-react';
+import { Plus, Clock, Bell, Keyboard, Sun, Moon, Settings } from 'lucide-react';
+import Link from 'next/link';
import { FeedbackButton } from './FeedbackButton';
import { useTheme } from '@/lib/use-theme';
@@ -134,6 +135,14 @@ export function Dashboard() {
>
{theme === 'dark' ? : }
+
+
+