import React, { useEffect, useState, useRef, useCallback, useMemo } from "react"
import {
  StyleSheet,
  Text,
  View,
  Pressable,
  ScrollView,
  Modal,
  Alert,
  ActivityIndicator,
  LayoutChangeEvent,
  Linking,
  Platform,
} from "react-native"
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withSequence,
  withTiming,
  Easing,
  runOnJS,
  interpolate,
  Extrapolation,
} from "react-native-reanimated"
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context"
import { StatusBar } from "expo-status-bar"
import { SymbolView } from "expo-symbols"
import * as Haptics from "expo-haptics"
import { useAudioPlayer } from "expo-audio"
import { initWhisper, releaseAllWhisper, type WhisperContext } from "whisper.rn"
import { RealtimeTranscriber, type RealtimeTranscribeEvent } from "whisper.rn/src/realtime-transcription"
import { AudioPcmStreamAdapter } from "whisper.rn/src/realtime-transcription/adapters/AudioPcmStreamAdapter"
import { AudioManager } from "react-native-audio-api"
import * as FileSystem from "expo-file-system/legacy"
import { fetch as expoFetch } from "expo/fetch"
import { unregisterRelayDevice } from "@/lib/relay-client"
import { useMonitoring, type MonitorJob } from "@/hooks/use-monitoring"
import { looksLikeLocalHost, useServerSessions } from "@/hooks/use-server-sessions"
import { ensureNotificationPermissions, getDevicePushToken } from "@/notifications/monitoring-notifications"

const CONTROL_HEIGHT = 86
const SEND_SETTLE_MS = 240
const WAVEFORM_ROWS = 5
const WAVEFORM_CELL_SIZE = 8
const WAVEFORM_CELL_GAP = 2
const DROPDOWN_VISIBLE_ROWS = 6
// If the press duration is shorter than this, treat it as a tap (toggle)
const TAP_THRESHOLD_MS = 300
const SERVER_STATE_FILE = `${FileSystem.documentDirectory}mobile-voice-servers.json`
const WHISPER_SETTINGS_FILE = `${FileSystem.documentDirectory}mobile-voice-whisper-settings.json`
const ONBOARDING_STATE_FILE = `${FileSystem.documentDirectory}mobile-voice-onboarding.json`
const WHISPER_MODELS_DIR = `${FileSystem.documentDirectory}whisper-models`
const WHISPER_REPO = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main"
const WHISPER_MODELS = [
  "ggml-tiny.en-q5_1.bin",
  "ggml-tiny.en-q8_0.bin",
  "ggml-tiny.en.bin",
  "ggml-tiny-q5_1.bin",
  "ggml-tiny-q8_0.bin",
  "ggml-tiny.bin",
  "ggml-base.en-q5_1.bin",
  "ggml-base.en-q8_0.bin",
  "ggml-base.en.bin",
  "ggml-base-q5_1.bin",
  "ggml-base-q8_0.bin",
  "ggml-base.bin",
  "ggml-small.en-q5_1.bin",
  "ggml-small.en-q8_0.bin",
  "ggml-small.en.bin",
  "ggml-small-q5_1.bin",
  "ggml-small-q8_0.bin",
  "ggml-small.bin",
  "ggml-medium.en-q5_0.bin",
  "ggml-medium.en-q8_0.bin",
  "ggml-medium.en.bin",
  "ggml-medium-q5_0.bin",
  "ggml-medium-q8_0.bin",
  "ggml-medium.bin",
  "ggml-large-v1.bin",
  "ggml-large-v2-q5_0.bin",
  "ggml-large-v2-q8_0.bin",
  "ggml-large-v2.bin",
  "ggml-large-v3-q5_0.bin",
  "ggml-large-v3-turbo-q5_0.bin",
  "ggml-large-v3-turbo-q8_0.bin",
  "ggml-large-v3-turbo.bin",
  "ggml-large-v3.bin",
] as const

type WhisperModelID = (typeof WHISPER_MODELS)[number]
type TranscriptionMode = "bulk" | "realtime"
type PermissionPromptState = "idle" | "pending" | "granted" | "denied"
const DEFAULT_WHISPER_MODEL: WhisperModelID = "ggml-small-q8_0.bin"
const DEFAULT_TRANSCRIPTION_MODE: TranscriptionMode = "bulk"

const WHISPER_MODEL_LABELS: Record<WhisperModelID, string> = {
  "ggml-tiny.en-q5_1.bin": "tiny.en q5_1",
  "ggml-tiny.en-q8_0.bin": "tiny.en q8_0",
  "ggml-tiny.en.bin": "tiny.en",
  "ggml-tiny-q5_1.bin": "tiny q5_1",
  "ggml-tiny-q8_0.bin": "tiny q8_0",
  "ggml-tiny.bin": "tiny",
  "ggml-base.en-q5_1.bin": "base.en q5_1",
  "ggml-base.en-q8_0.bin": "base.en q8_0",
  "ggml-base.en.bin": "base.en",
  "ggml-base-q5_1.bin": "base q5_1",
  "ggml-base-q8_0.bin": "base q8_0",
  "ggml-base.bin": "base",
  "ggml-small.en-q5_1.bin": "small.en q5_1",
  "ggml-small.en-q8_0.bin": "small.en q8_0",
  "ggml-small.en.bin": "small.en",
  "ggml-small-q5_1.bin": "small q5_1",
  "ggml-small-q8_0.bin": "small q8_0",
  "ggml-small.bin": "small",
  "ggml-medium.en-q5_0.bin": "medium.en q5_0",
  "ggml-medium.en-q8_0.bin": "medium.en q8_0",
  "ggml-medium.en.bin": "medium.en",
  "ggml-medium-q5_0.bin": "medium q5_0",
  "ggml-medium-q8_0.bin": "medium q8_0",
  "ggml-medium.bin": "medium",
  "ggml-large-v1.bin": "large-v1",
  "ggml-large-v2-q5_0.bin": "large-v2 q5_0",
  "ggml-large-v2-q8_0.bin": "large-v2 q8_0",
  "ggml-large-v2.bin": "large-v2",
  "ggml-large-v3-q5_0.bin": "large-v3 q5_0",
  "ggml-large-v3-turbo-q5_0.bin": "large-v3 turbo q5_0",
  "ggml-large-v3-turbo-q8_0.bin": "large-v3 turbo q8_0",
  "ggml-large-v3-turbo.bin": "large-v3 turbo",
  "ggml-large-v3.bin": "large-v3",
}

const WHISPER_MODEL_SIZES: Record<WhisperModelID, number> = {
  "ggml-tiny.en-q5_1.bin": 32166155,
  "ggml-tiny.en-q8_0.bin": 43550795,
  "ggml-tiny.en.bin": 77704715,
  "ggml-tiny-q5_1.bin": 32152673,
  "ggml-tiny-q8_0.bin": 43537433,
  "ggml-tiny.bin": 77691713,
  "ggml-base.en-q5_1.bin": 59721011,
  "ggml-base.en-q8_0.bin": 81781811,
  "ggml-base.en.bin": 147964211,
  "ggml-base-q5_1.bin": 59707625,
  "ggml-base-q8_0.bin": 81768585,
  "ggml-base.bin": 147951465,
  "ggml-small.en-q5_1.bin": 190098681,
  "ggml-small.en-q8_0.bin": 264477561,
  "ggml-small.en.bin": 487614201,
  "ggml-small-q5_1.bin": 190085487,
  "ggml-small-q8_0.bin": 264464607,
  "ggml-small.bin": 487601967,
  "ggml-medium.en-q5_0.bin": 539225533,
  "ggml-medium.en-q8_0.bin": 823382461,
  "ggml-medium.en.bin": 1533774781,
  "ggml-medium-q5_0.bin": 539212467,
  "ggml-medium-q8_0.bin": 823369779,
  "ggml-medium.bin": 1533763059,
  "ggml-large-v1.bin": 3094623691,
  "ggml-large-v2-q5_0.bin": 1080732091,
  "ggml-large-v2-q8_0.bin": 1656129691,
  "ggml-large-v2.bin": 3094623691,
  "ggml-large-v3-q5_0.bin": 1081140203,
  "ggml-large-v3-turbo-q5_0.bin": 574041195,
  "ggml-large-v3-turbo-q8_0.bin": 874188075,
  "ggml-large-v3-turbo.bin": 1624555275,
  "ggml-large-v3.bin": 3095033483,
}

function isWhisperModelID(value: unknown): value is WhisperModelID {
  return typeof value === "string" && (WHISPER_MODELS as readonly string[]).includes(value)
}

function isEnglishOnlyWhisperModel(modelID: WhisperModelID): boolean {
  return modelID.includes(".en")
}

function isTranscriptionMode(value: unknown): value is TranscriptionMode {
  return value === "bulk" || value === "realtime"
}

function formatWhisperModelSize(bytes: number): string {
  const mib = bytes / (1024 * 1024)
  if (mib >= 1024) {
    return `${(mib / 1024).toFixed(1)} GB`
  }

  return `${Math.round(mib)} MB`
}

function cleanTranscriptText(text: string): string {
  return text.replace(/[ \t]+$/gm, "").trimEnd()
}

function cleanSessionText(text: string): string {
  return cleanTranscriptText(text).trimStart()
}

function normalizeTranscriptSessions(text: string): string {
  const cleaned = cleanTranscriptText(text)
  if (!cleaned) {
    return ""
  }

  return cleaned
    .split(/\n\n+/)
    .map((session) => cleanSessionText(session))
    .filter((session) => session.length > 0)
    .join("\n\n")
}

function mergeTranscriptChunk(previous: string, chunk: string): string {
  const cleanPrevious = cleanTranscriptText(previous)
  const cleanChunk = cleanSessionText(chunk)

  if (!cleanChunk) {
    return cleanPrevious
  }

  if (!cleanPrevious) {
    return cleanChunk
  }

  const normalizedChunk = cleanChunk
  if (!normalizedChunk) {
    return cleanPrevious
  }

  if (/^[,.;:!?)]/.test(normalizedChunk)) {
    return `${cleanPrevious}${normalizedChunk}`
  }

  return `${cleanPrevious} ${normalizedChunk}`
}

function formatSessionUpdated(updatedMs: number): string {
  if (!updatedMs) return ""

  const now = Date.now()
  const deltaMs = Math.max(0, now - updatedMs)
  const deltaMin = Math.floor(deltaMs / 60000)

  if (deltaMin < 60) {
    return `${Math.max(1, deltaMin)} min`
  }

  const date = new Date(updatedMs)
  try {
    return new Intl.DateTimeFormat(undefined, {
      hour: "numeric",
      minute: "2-digit",
    }).format(date)
  } catch {
    return date.toLocaleTimeString()
  }
}

type DropdownMode = "none" | "server" | "session"

type Pair = {
  v: 1
  serverID?: string
  relayURL: string
  relaySecret: string
  hosts: string[]
}

type Scan = {
  data: string
}

type WhisperSavedState = {
  defaultModel: WhisperModelID
  mode: TranscriptionMode
}

type OnboardingSavedState = {
  completed: boolean
}

type Cam = {
  CameraView: (typeof import("expo-camera"))["CameraView"]
  requestCameraPermissionsAsync: () => Promise<{ granted: boolean }>
}

function parsePairShape(data: unknown): Pair | undefined {
  if (!data || typeof data !== "object") return
  if ((data as { v?: unknown }).v !== 1) return
  if (typeof (data as { relayURL?: unknown }).relayURL !== "string") return
  if (typeof (data as { relaySecret?: unknown }).relaySecret !== "string") return
  if (!Array.isArray((data as { hosts?: unknown }).hosts)) return
  const hosts = (data as { hosts: unknown[] }).hosts.filter((item): item is string => typeof item === "string")
  if (!hosts.length) return
  const serverIDRaw = (data as { serverID?: unknown }).serverID
  const serverID = typeof serverIDRaw === "string" && serverIDRaw.length > 0 ? serverIDRaw : undefined
  return {
    v: 1,
    serverID,
    relayURL: (data as { relayURL: string }).relayURL,
    relaySecret: (data as { relaySecret: string }).relaySecret,
    hosts,
  }
}

function parsePair(input: string): Pair | undefined {
  const raw = input.trim()
  if (!raw) return

  const candidates: string[] = [raw]

  try {
    const url = new URL(raw)
    const query = url.searchParams.get("pair") ?? url.searchParams.get("payload")
    if (query) {
      candidates.unshift(query)
    }
  } catch {
    // Raw JSON payload is still supported.
  }

  const seen = new Set<string>()
  for (const candidate of candidates) {
    if (!candidate || seen.has(candidate)) continue
    seen.add(candidate)

    try {
      const parsed = JSON.parse(candidate)
      const pair = parsePairShape(parsed)
      if (pair) {
        return pair
      }
    } catch {
      // keep trying fallbacks
    }
  }
}

function isLoopback(hostname: string): boolean {
  return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "0.0.0.0" || hostname === "::1"
}

/**
 * Probe all non-loopback hosts in parallel by hitting /health, then
 * choose the first reachable host based on the original ordering.
 * This preserves server-side preference (e.g. tailnet before LAN).
 */
async function pickHost(list: string[]): Promise<string | undefined> {
  const candidates = list.filter((item) => {
    try {
      return !isLoopback(new URL(item).hostname)
    } catch {
      return false
    }
  })

  if (!candidates.length) return list[0]

  const probes = candidates.map(async (host) => {
    const controller = new AbortController()
    const timeout = setTimeout(() => controller.abort(), 3000)
    try {
      const res = await fetch(`${host.replace(/\/+$/, "")}/health`, {
        method: "GET",
        signal: controller.signal,
      })
      return res.ok
    } catch {
      return false
    } finally {
      clearTimeout(timeout)
    }
  })

  for (let index = 0; index < candidates.length; index += 1) {
    const reachable = await probes[index]
    if (reachable) {
      return candidates[index]
    }
  }

  // none reachable — keep first candidate as deterministic fallback
  try {
    return candidates[0]
  } catch {
    return list[0]
  }
}

export default function DictationScreen() {
  const insets = useSafeAreaInsets()
  const [camera, setCamera] = useState<Cam | null>(null)
  const [defaultWhisperModel, setDefaultWhisperModel] = useState<WhisperModelID>(DEFAULT_WHISPER_MODEL)
  const [onboardingReady, setOnboardingReady] = useState(false)
  const [onboardingComplete, setOnboardingComplete] = useState(false)
  const [onboardingStep, setOnboardingStep] = useState(0)
  const [microphonePermissionState, setMicrophonePermissionState] = useState<PermissionPromptState>("idle")
  const [notificationPermissionState, setNotificationPermissionState] = useState<PermissionPromptState>("idle")
  const [localNetworkPermissionState, setLocalNetworkPermissionState] = useState<PermissionPromptState>("idle")
  const [activeWhisperModel, setActiveWhisperModel] = useState<WhisperModelID | null>(null)
  const [installedWhisperModels, setInstalledWhisperModels] = useState<WhisperModelID[]>([])
  const [whisperSettingsOpen, setWhisperSettingsOpen] = useState(false)
  const [downloadingModelID, setDownloadingModelID] = useState<WhisperModelID | null>(null)
  const [downloadProgress, setDownloadProgress] = useState(0)
  const [isPreparingWhisperModel, setIsPreparingWhisperModel] = useState(true)
  const [transcriptionMode, setTranscriptionMode] = useState<TranscriptionMode>(DEFAULT_TRANSCRIPTION_MODE)
  const [isTranscribingBulk, setIsTranscribingBulk] = useState(false)
  const [whisperError, setWhisperError] = useState("")
  const [transcribedText, setTranscribedText] = useState("")
  const [isRecording, setIsRecording] = useState(false)
  const [permissionGranted, setPermissionGranted] = useState(false)
  const [controlsWidth, setControlsWidth] = useState(0)
  const [hasCompletedSession, setHasCompletedSession] = useState(false)
  const [isSending, setIsSending] = useState(false)
  const [agentStateDismissed, setAgentStateDismissed] = useState(false)
  const [dropdownMode, setDropdownMode] = useState<DropdownMode>("none")
  const [dropdownRenderMode, setDropdownRenderMode] = useState<Exclude<DropdownMode, "none">>("server")
  const [scanOpen, setScanOpen] = useState(false)
  const [camGranted, setCamGranted] = useState(false)
  const [waveformLevels, setWaveformLevels] = useState<number[]>(Array.from({ length: 24 }, () => 0))
  const [waveformTick, setWaveformTick] = useState(0)
  const waveformLevelsRef = useRef<number[]>(Array.from({ length: 24 }, () => 0))
  const lastWaveformCommitRef = useRef(0)
  const sendPlayer = useAudioPlayer(require("../../assets/sounds/send-whoosh.mp3"))
  const completePlayer = useAudioPlayer(require("../../assets/sounds/complete.wav"))

  const isRecordingRef = useRef(false)
  const isStartingRef = useRef(false)
  const activeSessionRef = useRef(0)
  const scrollViewRef = useRef<ScrollView>(null)
  const isHoldingRef = useRef(false)
  const pressInTimeRef = useRef(0)
  const accumulatedRef = useRef("")
  const baseTextRef = useRef("")
  const whisperContextRef = useRef<WhisperContext | null>(null)
  const whisperContextModelRef = useRef<WhisperModelID | null>(null)
  const whisperTranscriberRef = useRef<RealtimeTranscriber | null>(null)
  const bulkAudioStreamRef = useRef<AudioPcmStreamAdapter | null>(null)
  const bulkAudioChunksRef = useRef<Uint8Array[]>([])
  const bulkTranscriptionJobRef = useRef(0)
  const downloadProgressRef = useRef(0)
  const waveformPulseIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
  const sendSettleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const scanLockRef = useRef(false)
  const whisperRestoredRef = useRef(false)

  const closeDropdown = useCallback(() => {
    setDropdownMode("none")
  }, [])

  const {
    servers,
    serversRef,
    activeServerId,
    setActiveServerId,
    activeServerIdRef,
    activeSessionId,
    setActiveSessionId,
    activeSessionIdRef,
    restoredRef,
    refreshServerStatusAndSessions,
    refreshAllServerHealth,
    selectServer,
    selectSession,
    removeServer,
    addServer,
    findServerForSession,
  } = useServerSessions()

  const {
    beginMonitoring,
    devicePushToken,
    latestAssistantResponse,
    monitorJob,
    monitorStatus,
    setDevicePushToken,
    setMonitorStatus,
  } = useMonitoring({
    completePlayer,
    closeDropdown,
    findServerForSession,
    refreshServerStatusAndSessions,
    servers,
    serversRef,
    restoredRef,
    activeServerId,
    activeSessionId,
    activeServerIdRef,
    activeSessionIdRef,
    setActiveServerId,
    setActiveSessionId,
    setAgentStateDismissed,
    setNotificationPermissionState,
  })

  useEffect(() => {
    let mounted = true

    void (async () => {
      let complete = false

      try {
        const data = await FileSystem.readAsStringAsync(ONBOARDING_STATE_FILE)
        if (data) {
          const parsed = JSON.parse(data) as Partial<OnboardingSavedState>
          complete = Boolean(parsed.completed)
        }
      } catch {
        // No onboarding state file yet.
      }

      if (!complete) {
        try {
          const [serverInfo, whisperInfo] = await Promise.all([
            FileSystem.getInfoAsync(SERVER_STATE_FILE),
            FileSystem.getInfoAsync(WHISPER_SETTINGS_FILE),
          ])

          if (serverInfo.exists || whisperInfo.exists) {
            complete = true
          }
        } catch {
          // Keep first-install behavior if metadata check fails.
        }

        if (complete) {
          void FileSystem.writeAsStringAsync(ONBOARDING_STATE_FILE, JSON.stringify({ completed: true })).catch(() => {})
        }
      }

      if (mounted) {
        setOnboardingComplete(complete)
        setOnboardingReady(true)
      }
    })()

    return () => {
      mounted = false
    }
  }, [])

  const modelPath = useCallback((modelID: WhisperModelID) => `${WHISPER_MODELS_DIR}/${modelID}`, [])

  const refreshInstalledWhisperModels = useCallback(async () => {
    const next: WhisperModelID[] = []

    for (const modelID of WHISPER_MODELS) {
      try {
        const info = await FileSystem.getInfoAsync(modelPath(modelID))
        if (info.exists) {
          next.push(modelID)
        }
      } catch {
        // Ignore model metadata read errors.
      }
    }

    setInstalledWhisperModels(next)
    return next
  }, [modelPath])

  const stopWaveformPulse = useCallback(() => {
    if (waveformPulseIntervalRef.current) {
      clearInterval(waveformPulseIntervalRef.current)
      waveformPulseIntervalRef.current = null
    }
  }, [])

  const clearWaveform = useCallback(() => {
    const cleared = new Array(waveformLevelsRef.current.length).fill(0)
    waveformLevelsRef.current = cleared
    setWaveformLevels(cleared)
    setWaveformTick(Date.now())
  }, [])

  useEffect(() => {
    return () => {
      if (sendSettleTimeoutRef.current) {
        clearTimeout(sendSettleTimeoutRef.current)
      }
      stopWaveformPulse()
    }
  }, [stopWaveformPulse])

  const ensureAudioInputRoute = useCallback(async () => {
    try {
      const devices = await AudioManager.getDevicesInfo()
      if (devices.currentInputs.length === 0 && devices.availableInputs.length > 0) {
        const pick = devices.availableInputs[0]
        await AudioManager.setInputDevice(pick.id)
      }
    } catch {
      // Input route setup is best-effort.
    }
  }, [])

  // Set up audio session and check microphone permissions on mount.
  useEffect(() => {
    void (async () => {
      try {
        AudioManager.setAudioSessionOptions({
          iosCategory: "playAndRecord",
          iosMode: "spokenAudio",
          iosOptions: ["allowBluetoothHFP", "defaultToSpeaker"],
        })

        await AudioManager.setAudioSessionActivity(true)

        const permission = await AudioManager.checkRecordingPermissions()
        const granted = permission === "Granted"
        setPermissionGranted(granted)
        setMicrophonePermissionState(granted ? "granted" : permission === "Denied" ? "denied" : "idle")

        if (granted) {
          await ensureAudioInputRoute()
        }
      } catch (e) {
        console.error("Failed to set up audio session:", e)
      }
    })()
  }, [ensureAudioInputRoute])

  const loadWhisperContext = useCallback(
    async (modelID: WhisperModelID) => {
      if (whisperContextRef.current && whisperContextModelRef.current === modelID) {
        setActiveWhisperModel(modelID)
        return whisperContextRef.current
      }

      setIsPreparingWhisperModel(true)
      setWhisperError("")

      try {
        const existing = whisperContextRef.current
        whisperContextRef.current = null
        whisperContextModelRef.current = null
        if (existing) {
          await existing.release().catch(() => {})
        }

        const context = await initWhisper({
          filePath: modelPath(modelID),
          useGpu: Platform.OS === "ios",
        })

        whisperContextRef.current = context
        whisperContextModelRef.current = modelID
        setActiveWhisperModel(modelID)
        return context
      } catch (error) {
        const message = error instanceof Error ? error.message : "Failed to load Whisper model"
        setWhisperError(message)
        throw error
      } finally {
        setIsPreparingWhisperModel(false)
      }
    },
    [modelPath],
  )

  const downloadWhisperModel = useCallback(
    async (modelID: WhisperModelID) => {
      if (downloadingModelID && downloadingModelID !== modelID) {
        return false
      }

      setDownloadingModelID(modelID)
      downloadProgressRef.current = 0
      setDownloadProgress(0)
      setWhisperError("")

      try {
        await FileSystem.makeDirectoryAsync(WHISPER_MODELS_DIR, { intermediates: true }).catch(() => {})

        const targetPath = modelPath(modelID)
        await FileSystem.deleteAsync(targetPath, { idempotent: true }).catch(() => {})

        const download = FileSystem.createDownloadResumable(
          `${WHISPER_REPO}/${modelID}`,
          targetPath,
          {},
          (event: FileSystem.DownloadProgressData) => {
            const total = event.totalBytesExpectedToWrite
            if (!total) return
            const rawProgress = Math.max(0, Math.min(1, event.totalBytesWritten / total))
            const progress = Math.max(downloadProgressRef.current, rawProgress)
            downloadProgressRef.current = progress
            setDownloadProgress(progress)
          },
        )

        const result = await download.downloadAsync()
        if (!result?.uri) {
          throw new Error("Whisper model download did not complete")
        }

        await refreshInstalledWhisperModels()
        return true
      } catch (error) {
        const message = error instanceof Error ? error.message : "Failed to download Whisper model"
        setWhisperError(message)
        return false
      } finally {
        setDownloadingModelID((current) => (current === modelID ? null : current))
      }
    },
    [downloadingModelID, modelPath, refreshInstalledWhisperModels],
  )

  const ensureWhisperModelReady = useCallback(
    async (modelID: WhisperModelID) => {
      const info = await FileSystem.getInfoAsync(modelPath(modelID))
      if (!info.exists) {
        const downloaded = await downloadWhisperModel(modelID)
        if (!downloaded) {
          throw new Error(`Unable to download ${modelID}`)
        }
      }
      return loadWhisperContext(modelID)
    },
    [downloadWhisperModel, loadWhisperContext, modelPath],
  )

  useEffect(() => {
    let mounted = true

    void (async () => {
      await FileSystem.makeDirectoryAsync(WHISPER_MODELS_DIR, { intermediates: true }).catch(() => {})

      let nextDefaultModel: WhisperModelID = DEFAULT_WHISPER_MODEL
      let nextMode: TranscriptionMode = DEFAULT_TRANSCRIPTION_MODE
      try {
        const data = await FileSystem.readAsStringAsync(WHISPER_SETTINGS_FILE)
        if (data) {
          const parsed = JSON.parse(data) as Partial<WhisperSavedState>
          if (isWhisperModelID(parsed.defaultModel)) {
            nextDefaultModel = parsed.defaultModel
          }
          if (isTranscriptionMode(parsed.mode)) {
            nextMode = parsed.mode
          }
        }
      } catch {
        // Use default settings if state file is missing or invalid.
      }

      if (!mounted) return

      whisperRestoredRef.current = true
      setDefaultWhisperModel(nextDefaultModel)
      setTranscriptionMode(nextMode)

      await refreshInstalledWhisperModels()

      try {
        await ensureWhisperModelReady(nextDefaultModel)
      } catch (error) {
        console.error("[Whisper] Failed to initialize default model:", error)
      } finally {
        if (mounted) {
          setIsPreparingWhisperModel(false)
        }
      }
    })()

    return () => {
      mounted = false
    }
  }, [ensureWhisperModelReady, refreshInstalledWhisperModels])

  useEffect(() => {
    if (!whisperRestoredRef.current) return
    const payload: WhisperSavedState = { defaultModel: defaultWhisperModel, mode: transcriptionMode }
    void FileSystem.writeAsStringAsync(WHISPER_SETTINGS_FILE, JSON.stringify(payload)).catch(() => {})
  }, [defaultWhisperModel, transcriptionMode])

  useEffect(() => {
    return () => {
      const transcriber = whisperTranscriberRef.current
      whisperTranscriberRef.current = null
      if (transcriber) {
        void (async () => {
          await transcriber.stop().catch(() => {})
          await transcriber.release().catch(() => {})
        })()
      }

      const bulkStream = bulkAudioStreamRef.current
      bulkAudioStreamRef.current = null
      if (bulkStream) {
        void (async () => {
          await bulkStream.stop().catch(() => {})
          await bulkStream.release().catch(() => {})
        })()
      }

      const context = whisperContextRef.current
      whisperContextRef.current = null
      whisperContextModelRef.current = null

      if (context) {
        void context.release().catch(() => {})
      }

      void releaseAllWhisper().catch(() => {})
    }
  }, [])

  const startWaveformPulse = useCallback(() => {
    if (waveformPulseIntervalRef.current) return

    waveformPulseIntervalRef.current = setInterval(() => {
      if (!isRecordingRef.current) return

      const next = waveformLevelsRef.current.map((value) => {
        const decay = value * 0.45
        const lift = Math.random() * 0.95
        return Math.max(0.08, Math.min(1, decay + lift * 0.55))
      })

      waveformLevelsRef.current = next

      const now = Date.now()
      if (now - lastWaveformCommitRef.current > 45) {
        setWaveformLevels(next)
        setWaveformTick(now)
        lastWaveformCommitRef.current = now
      }
    }, 70)
  }, [])

  const finalizeRecordingState = useCallback(() => {
    isRecordingRef.current = false
    activeSessionRef.current = 0
    isStartingRef.current = false
    setIsRecording(false)
    stopWaveformPulse()
    clearWaveform()
  }, [clearWaveform, stopWaveformPulse])

  const startRecording = useCallback(async () => {
    if (isRecordingRef.current || isStartingRef.current || downloadingModelID || isTranscribingBulk) return

    isStartingRef.current = true
    const sessionID = Date.now()
    activeSessionRef.current = sessionID
    accumulatedRef.current = ""
    baseTextRef.current = normalizeTranscriptSessions(transcribedText)
    if (baseTextRef.current !== transcribedText) {
      setTranscribedText(baseTextRef.current)
    }
    isRecordingRef.current = true
    setIsRecording(true)
    setWhisperError("")

    const cancelled = () => !isRecordingRef.current || activeSessionRef.current !== sessionID

    try {
      const context = await ensureWhisperModelReady(defaultWhisperModel)
      if (cancelled()) {
        isStartingRef.current = false
        return
      }

      const previousTranscriber = whisperTranscriberRef.current
      whisperTranscriberRef.current = null
      if (previousTranscriber) {
        await previousTranscriber.stop().catch(() => {})
        await previousTranscriber.release().catch(() => {})
      }

      const previousBulkStream = bulkAudioStreamRef.current
      bulkAudioStreamRef.current = null
      if (previousBulkStream) {
        await previousBulkStream.stop().catch(() => {})
        await previousBulkStream.release().catch(() => {})
      }

      bulkAudioChunksRef.current = []
      bulkTranscriptionJobRef.current = 0

      startWaveformPulse()

      const englishOnlyModel = isEnglishOnlyWhisperModel(defaultWhisperModel)

      if (transcriptionMode === "bulk") {
        const audioStream = new AudioPcmStreamAdapter()
        audioStream.onData((packet: unknown) => {
          if (activeSessionRef.current !== sessionID) return
          const data = (packet as { data?: unknown }).data
          if (!(data instanceof Uint8Array) || data.length === 0) return
          bulkAudioChunksRef.current.push(new Uint8Array(data))
        })
        audioStream.onError((error: string) => {
          if (activeSessionRef.current !== sessionID) return
          setWhisperError(error)
          console.error("[Dictation] Bulk audio stream error:", error)
        })

        await audioStream.initialize({
          sampleRate: 16000,
          channels: 1,
          bitsPerSample: 16,
          bufferSize: 16 * 1024,
          audioSource: 6,
        })
        await audioStream.start()

        bulkAudioStreamRef.current = audioStream

        if (cancelled()) {
          await audioStream.stop().catch(() => {})
          await audioStream.release().catch(() => {})
          if (bulkAudioStreamRef.current === audioStream) {
            bulkAudioStreamRef.current = null
          }
          finalizeRecordingState()
          return
        }

        isStartingRef.current = false
        return
      }

      const transcriber = new RealtimeTranscriber(
        {
          whisperContext: context,
          audioStream: new AudioPcmStreamAdapter(),
        },
        {
          audioSliceSec: 4,
          audioMinSec: 0.8,
          maxSlicesInMemory: 6,
          transcribeOptions: {
            language: englishOnlyModel ? "en" : "auto",
            translate: !englishOnlyModel,
            maxLen: 1,
          },
          logger: () => {},
        },
        {
          onTranscribe: (event: RealtimeTranscribeEvent) => {
            if (activeSessionRef.current !== sessionID) return
            if (event.type !== "transcribe") return

            const nextSessionText = mergeTranscriptChunk(accumulatedRef.current, event.data?.result ?? "")
            accumulatedRef.current = nextSessionText

            const base = normalizeTranscriptSessions(baseTextRef.current)
            const separator = base.length > 0 && nextSessionText.length > 0 ? "\n\n" : ""
            setTranscribedText(normalizeTranscriptSessions(base + separator + nextSessionText))

            if (nextSessionText.length > 0) {
              setHasCompletedSession(true)
            }
          },
          onError: (error: string) => {
            if (activeSessionRef.current !== sessionID) return
            console.error("[Dictation] Whisper realtime error:", error)
            setWhisperError(error)
          },
          onStatusChange: (active: boolean) => {
            if (activeSessionRef.current !== sessionID) return
            if (!active) {
              if (whisperTranscriberRef.current === transcriber) {
                whisperTranscriberRef.current = null
              }
              finalizeRecordingState()
            }
          },
        },
      )

      whisperTranscriberRef.current = transcriber
      await transcriber.start()

      if (cancelled()) {
        await transcriber.stop().catch(() => {})
        await transcriber.release().catch(() => {})
        if (whisperTranscriberRef.current === transcriber) {
          whisperTranscriberRef.current = null
        }
        finalizeRecordingState()
        return
      }

      isStartingRef.current = false
    } catch (error) {
      console.error("[Dictation] Failed to start realtime transcription:", error)
      const message = error instanceof Error ? error.message : "Unable to start transcription"
      setWhisperError(message)

      const activeTranscriber = whisperTranscriberRef.current
      whisperTranscriberRef.current = null
      if (activeTranscriber) {
        void (async () => {
          await activeTranscriber.stop().catch(() => {})
          await activeTranscriber.release().catch(() => {})
        })()
      }

      finalizeRecordingState()
      void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {})
    }
  }, [
    defaultWhisperModel,
    downloadingModelID,
    ensureWhisperModelReady,
    finalizeRecordingState,
    isTranscribingBulk,
    startWaveformPulse,
    transcriptionMode,
    transcribedText,
  ])

  const stopRecording = useCallback(() => {
    if (!isRecordingRef.current && !isStartingRef.current) return

    void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {})

    const baseAtStop = normalizeTranscriptSessions(baseTextRef.current)
    const englishOnlyModel = isEnglishOnlyWhisperModel(defaultWhisperModel)

    const transcriber = whisperTranscriberRef.current
    whisperTranscriberRef.current = null
    if (transcriber) {
      void (async () => {
        await transcriber.stop().catch((error: unknown) => {
          console.warn("[Dictation] Failed to stop realtime transcription:", error)
        })
        await transcriber.release().catch(() => {})
      })()
    }

    const bulkStream = bulkAudioStreamRef.current
    bulkAudioStreamRef.current = null
    const bulkChunks = bulkAudioChunksRef.current
    bulkAudioChunksRef.current = []

    finalizeRecordingState()

    if (transcriptionMode !== "bulk") {
      return
    }

    const runID = Date.now()
    bulkTranscriptionJobRef.current = runID

    void (async () => {
      if (bulkStream) {
        await bulkStream.stop().catch((error: unknown) => {
          console.warn("[Dictation] Failed to stop bulk audio stream:", error)
        })
        await bulkStream.release().catch(() => {})
      }

      if (bulkChunks.length === 0) {
        return
      }

      const totalLength = bulkChunks.reduce((sum, chunk) => sum + chunk.length, 0)
      if (totalLength === 0) {
        return
      }

      const merged = new Uint8Array(totalLength)
      let offset = 0
      for (const chunk of bulkChunks) {
        merged.set(chunk, offset)
        offset += chunk.length
      }

      const context = whisperContextRef.current
      if (!context) {
        setWhisperError("Whisper model is not loaded")
        return
      }

      setIsTranscribingBulk(true)

      try {
        const { promise } = context.transcribeData(merged.buffer, {
          language: englishOnlyModel ? "en" : "auto",
          translate: !englishOnlyModel,
          maxLen: 1,
        })

        const result = await promise
        if (bulkTranscriptionJobRef.current !== runID) {
          return
        }

        const sessionText = cleanSessionText(result.result ?? "")
        if (!sessionText) {
          return
        }

        const separator = baseAtStop.length > 0 ? "\n\n" : ""
        setTranscribedText(normalizeTranscriptSessions(baseAtStop + separator + sessionText))
        setHasCompletedSession(true)
      } catch (error) {
        if (bulkTranscriptionJobRef.current !== runID) {
          return
        }
        const message = error instanceof Error ? error.message : "Bulk transcription failed"
        setWhisperError(message)
        console.error("[Dictation] Bulk transcription failed:", error)
      } finally {
        if (bulkTranscriptionJobRef.current === runID) {
          setIsTranscribingBulk(false)
        }
      }
    })()
  }, [defaultWhisperModel, finalizeRecordingState, transcriptionMode])

  const clearIconRotation = useSharedValue(0)
  const sendOutProgress = useSharedValue(0)

  const handleClearTranscript = useCallback(() => {
    void Haptics.selectionAsync().catch(() => {})

    clearIconRotation.value = withSequence(
      withTiming(-30, { duration: 90 }),
      withTiming(30, { duration: 120 }),
      withTiming(0, { duration: 90 }),
    )

    if (isRecordingRef.current) {
      stopRecording()
    }
    accumulatedRef.current = ""
    baseTextRef.current = ""
    setTranscribedText("")
    setHasCompletedSession(false)
    clearWaveform()
    sendOutProgress.value = 0
    setIsSending(false)
  }, [clearIconRotation, clearWaveform, sendOutProgress, stopRecording])

  const handleHideAgentState = useCallback(() => {
    void Haptics.selectionAsync().catch(() => {})
    setAgentStateDismissed(true)
  }, [])

  const resetTranscriptState = useCallback(() => {
    if (isRecordingRef.current) {
      stopRecording()
    }
    accumulatedRef.current = ""
    baseTextRef.current = ""
    setTranscribedText("")
    setHasCompletedSession(false)
    clearWaveform()
  }, [clearWaveform, stopRecording])

  const handleOpenWhisperSettings = useCallback(() => {
    void Haptics.selectionAsync().catch(() => {})
    setDropdownMode("none")
    setWhisperSettingsOpen(true)
  }, [])

  const handleDownloadWhisperModel = useCallback(
    async (modelID: WhisperModelID) => {
      const ok = await downloadWhisperModel(modelID)
      if (ok) {
        void Haptics.selectionAsync().catch(() => {})
      }
    },
    [downloadWhisperModel],
  )

  const handleSelectWhisperModel = useCallback(
    async (modelID: WhisperModelID) => {
      if (isRecordingRef.current || isStartingRef.current) {
        stopRecording()
      }

      try {
        await ensureWhisperModelReady(modelID)
        setDefaultWhisperModel(modelID)
        setWhisperError("")
        void Haptics.selectionAsync().catch(() => {})
      } catch (error) {
        const message = error instanceof Error ? error.message : "Unable to switch Whisper model"
        setWhisperError(message)
      }
    },
    [ensureWhisperModelReady, stopRecording],
  )

  const handleDeleteWhisperModel = useCallback(
    async (modelID: WhisperModelID) => {
      if (downloadingModelID === modelID) return

      if (isRecordingRef.current || isStartingRef.current) {
        stopRecording()
      }

      if (whisperContextModelRef.current === modelID && whisperContextRef.current) {
        const activeContext = whisperContextRef.current
        whisperContextRef.current = null
        whisperContextModelRef.current = null
        setActiveWhisperModel(null)
        await activeContext.release().catch(() => {})
      }

      await FileSystem.deleteAsync(modelPath(modelID), { idempotent: true }).catch(() => {})
      const nextInstalled = await refreshInstalledWhisperModels()

      if (defaultWhisperModel === modelID) {
        const fallbackModel = nextInstalled[0] ?? DEFAULT_WHISPER_MODEL
        setDefaultWhisperModel(fallbackModel)
        try {
          await ensureWhisperModelReady(fallbackModel)
        } catch {
          // Keep UI responsive if fallback init fails.
        }
      } else if (activeWhisperModel == null && nextInstalled.includes(defaultWhisperModel)) {
        try {
          await ensureWhisperModelReady(defaultWhisperModel)
        } catch {
          // Keep UI responsive if default model init fails.
        }
      }

      void Haptics.selectionAsync().catch(() => {})
    },
    [
      activeWhisperModel,
      defaultWhisperModel,
      downloadingModelID,
      ensureWhisperModelReady,
      modelPath,
      refreshInstalledWhisperModels,
      stopRecording,
    ],
  )

  const handleRequestNotificationPermission = useCallback(async () => {
    if (notificationPermissionState === "pending") return

    setNotificationPermissionState("pending")

    try {
      const granted = await ensureNotificationPermissions()
      setNotificationPermissionState(granted ? "granted" : "denied")

      if (!granted) {
        return
      }

      const token = await getDevicePushToken()
      if (token) {
        setDevicePushToken(token)
      }
    } catch {
      setNotificationPermissionState("denied")
    }
  }, [notificationPermissionState, setDevicePushToken])

  const handleRequestMicrophonePermission = useCallback(async () => {
    if (microphonePermissionState === "pending") return

    setMicrophonePermissionState("pending")

    try {
      const permission = await AudioManager.requestRecordingPermissions()
      const granted = permission === "Granted"
      setPermissionGranted(granted)
      setMicrophonePermissionState(granted ? "granted" : "denied")

      if (granted) {
        await ensureAudioInputRoute()
      }
    } catch {
      setPermissionGranted(false)
      setMicrophonePermissionState("denied")
    }
  }, [ensureAudioInputRoute, microphonePermissionState])

  const handleRequestLocalNetworkPermission = useCallback(async () => {
    if (localNetworkPermissionState === "pending") return

    setLocalNetworkPermissionState("pending")

    const localProbes = new Set<string>([
      "http://192.168.1.1",
      "http://192.168.0.1",
      "http://10.0.0.1",
      "http://100.100.100.100",
    ])

    for (const server of serversRef.current) {
      try {
        const url = new URL(server.url)
        if (looksLikeLocalHost(url.hostname)) {
          localProbes.add(`${url.protocol}//${url.host}`)
        }
      } catch {
        // Skip malformed saved server URL.
      }
    }

    const controller = new AbortController()
    const timeout = setTimeout(() => {
      controller.abort()
    }, 1800)

    try {
      await Promise.allSettled(
        [...localProbes].map((base) =>
          expoFetch(`${base.replace(/\/+$/, "")}/health`, {
            method: "GET",
            signal: controller.signal,
          }),
        ),
      )
      setLocalNetworkPermissionState("granted")
    } catch {
      setLocalNetworkPermissionState("denied")
    } finally {
      clearTimeout(timeout)
    }
  }, [localNetworkPermissionState, serversRef])

  const completeSend = useCallback(() => {
    if (sendSettleTimeoutRef.current) {
      clearTimeout(sendSettleTimeoutRef.current)
    }

    sendSettleTimeoutRef.current = setTimeout(() => {
      resetTranscriptState()
      sendOutProgress.value = 0
      setIsSending(false)
      sendSettleTimeoutRef.current = null
    }, SEND_SETTLE_MS)
  }, [resetTranscriptState, sendOutProgress])

  const handleSendTranscript = useCallback(async () => {
    const text = transcribedText.trim()
    if (text.length === 0 || isSending || !activeServerId || !activeSessionId) return

    const server = serversRef.current.find((item) => item.id === activeServerId)
    if (!server) return

    const session = server.sessions.find((item) => item.id === activeSessionId)
    if (!session) return

    const base = server.url.replace(/\/+$/, "")

    setIsSending(true)
    setMonitorStatus("Sending prompt…")

    try {
      const response = await fetch(`${base}/session/${session.id}/prompt_async`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          parts: [
            {
              type: "text",
              text,
            },
          ],
        }),
      })

      if (!response.ok) {
        throw new Error(`Prompt request failed (${response.status})`)
      }

      const nextJob: MonitorJob = {
        id: `job-${Date.now()}`,
        sessionID: session.id,
        opencodeBaseURL: base,
        startedAt: Date.now(),
      }

      await beginMonitoring(nextJob)

      if (server.relaySecret.trim().length === 0) {
        setMonitorStatus("Monitoring (foreground only)")
      }

      void sendPlayer.seekTo(0)
      void sendPlayer.play()

      void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy).catch(() => {})
      setTimeout(() => {
        void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {})
      }, 70)

      sendOutProgress.value = withTiming(
        1,
        {
          duration: 320,
          easing: Easing.bezier(0.2, 0.8, 0.2, 1),
        },
        (finished) => {
          if (finished) {
            runOnJS(completeSend)()
          }
        },
      )
    } catch {
      setMonitorStatus("Failed to send prompt")
      void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {})
      setIsSending(false)
      sendOutProgress.value = 0
    }
  }, [
    activeServerId,
    activeSessionId,
    beginMonitoring,
    completeSend,
    isSending,
    serversRef,
    setMonitorStatus,
    sendOutProgress,
    sendPlayer,
    transcribedText,
  ])

  // --- Gesture handling: tap vs hold ---

  const handlePressIn = useCallback(() => {
    pressInTimeRef.current = Date.now()

    if (isRecordingRef.current) return

    setDropdownMode("none")
    void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {})
    isHoldingRef.current = true
    void startRecording()
  }, [startRecording])

  const handlePressOut = useCallback(() => {
    const pressDuration = Date.now() - pressInTimeRef.current

    if (pressDuration < TAP_THRESHOLD_MS) {
      if (isHoldingRef.current) {
        // Tap started recording on pressIn -- keep it running (toggle ON)
        isHoldingRef.current = false
      } else {
        // Already recording from a previous tap -- this tap stops it
        stopRecording()
      }
    } else {
      // Long press = hold-to-record, stop on release
      isHoldingRef.current = false
      stopRecording()
    }
  }, [stopRecording])

  const modelDownloading = downloadingModelID !== null
  const modelLoading = isPreparingWhisperModel || activeWhisperModel == null || modelDownloading || isTranscribingBulk
  let modelLoadingState: "downloading" | "loading" | "ready" = "ready"
  if (modelDownloading) {
    modelLoadingState = "downloading"
  } else if (modelLoading) {
    modelLoadingState = "loading"
  }
  const pct = Math.round(Math.max(0, Math.min(1, downloadProgress)) * 100)
  const loadingModelLabel = downloadingModelID
    ? WHISPER_MODEL_LABELS[downloadingModelID]
    : WHISPER_MODEL_LABELS[defaultWhisperModel]
  const hasTranscript = transcribedText.trim().length > 0
  const hasAssistantResponse = latestAssistantResponse.trim().length > 0
  const hasAgentActivity = hasAssistantResponse || monitorStatus.trim().length > 0 || monitorJob !== null
  const shouldShowAgentStateCard = hasAgentActivity && !agentStateDismissed
  const showsCompleteState = monitorStatus.toLowerCase().includes("complete")
  let agentStateIcon: "loading" | "done" = "loading"
  if (monitorJob === null && (hasAssistantResponse || showsCompleteState)) {
    agentStateIcon = "done"
  }
  const agentStateText = hasAssistantResponse ? latestAssistantResponse : "Waiting for agent…"
  const shouldShowSend = hasCompletedSession && hasTranscript
  const activeServer = servers.find((s) => s.id === activeServerId) ?? null
  const activeSession = activeServer?.sessions.find((s) => s.id === activeSessionId) ?? null
  const canSendToSession = !!activeServer && activeServer.status === "online" && !!activeSession
  const isDropdownOpen = dropdownMode !== "none"
  const effectiveDropdownMode = isDropdownOpen ? dropdownMode : dropdownRenderMode
  const headerTitle = activeServer?.name ?? "No server configured"
  let headerDotStyle = styles.serverStatusOffline
  if (activeServer?.status === "online") {
    headerDotStyle = styles.serverStatusActive
  } else if (activeServer?.status === "checking") {
    headerDotStyle = styles.serverStatusChecking
  }

  const recordingProgress = useSharedValue(0)
  const sendVisibility = useSharedValue(hasTranscript ? 1 : 0)
  const waveformVisibility = useSharedValue(0)
  const serverMenuProgress = useSharedValue(0)

  useEffect(() => {
    recordingProgress.value = withSpring(isRecording ? 1 : 0, {
      damping: 14,
      stiffness: 140,
      mass: 0.8,
    })
  }, [isRecording, recordingProgress])

  useEffect(() => {
    const isGenerating = isRecording
    waveformVisibility.value = withTiming(isGenerating ? 1 : 0, {
      duration: isGenerating ? 180 : 240,
      easing: Easing.inOut(Easing.quad),
    })
  }, [isRecording, waveformVisibility])

  useEffect(() => {
    serverMenuProgress.value = withTiming(isDropdownOpen ? 1 : 0, {
      duration: isDropdownOpen ? 240 : 240,
      easing: isDropdownOpen ? Easing.bezier(0.2, 0.8, 0.2, 1) : Easing.bezier(0.4, 0, 0.2, 1),
    })
  }, [isDropdownOpen, serverMenuProgress])

  useEffect(() => {
    if (dropdownMode !== "none") {
      setDropdownRenderMode(dropdownMode)
    }
  }, [dropdownMode])

  useEffect(() => {
    sendVisibility.value = shouldShowSend
      ? withTiming(1, {
          duration: 320,
          easing: Easing.bezier(0.2, 0.8, 0.2, 1),
        })
      : withTiming(0, {
          duration: 360,
          easing: Easing.bezier(0.22, 0.61, 0.36, 1),
        })
  }, [shouldShowSend, sendVisibility])

  // Parent clips outer half of center-stroke, so only inner half is visible.
  // borderWidth 6 → 3px visible inward, borderWidth 12 → 6px visible inward.
  const animatedBorderStyle = useAnimatedStyle(() => {
    const progress = recordingProgress.value
    // Width: 3 → ~1.5px visible inward at rest (matches other cards),
    // 12 → ~6px visible inward when active
    const bw = interpolate(progress, [0, 1], [3, 12], Extrapolation.CLAMP)
    return {
      borderWidth: bw,
      borderColor: "#FF2E3F",
    }
  })

  const animatedDotStyle = useAnimatedStyle(() => ({
    borderRadius: interpolate(recordingProgress.value, [0, 1], [19, 2], Extrapolation.CLAMP),
  }))

  const animatedClearIconStyle = useAnimatedStyle(() => ({
    transform: [{ rotate: `${clearIconRotation.value}deg` }],
  }))

  const animatedSendStyle = useAnimatedStyle(() => ({
    width: interpolate(sendVisibility.value, [0, 1], [0, Math.max((controlsWidth - 8) / 2, 0)], Extrapolation.CLAMP),
    marginLeft: interpolate(sendVisibility.value, [0, 1], [0, 8], Extrapolation.CLAMP),
    opacity: sendVisibility.value,
    transform: [
      {
        translateX: interpolate(sendVisibility.value, [0, 1], [14, 0], Extrapolation.CLAMP),
      },
      {
        scale: interpolate(sendVisibility.value, [0, 1], [0.98, 1], Extrapolation.CLAMP),
      },
    ],
  }))

  const animatedTranscriptSendStyle = useAnimatedStyle(() => ({
    opacity: interpolate(sendOutProgress.value, [0, 1], [1, 0], Extrapolation.CLAMP),
    transform: [
      {
        translateY: interpolate(sendOutProgress.value, [0, 1], [0, -44], Extrapolation.CLAMP),
      },
    ],
  }))

  const animatedWaveformRowStyle = useAnimatedStyle(() => ({
    opacity: waveformVisibility.value,
    transform: [
      {
        translateY: interpolate(waveformVisibility.value, [0, 1], [6, 0], Extrapolation.CLAMP),
      },
    ],
  }))

  const menuRows =
    effectiveDropdownMode === "server" ? Math.max(servers.length, 1) : Math.max(activeServer?.sessions.length ?? 0, 1)
  const expandedRowsHeight = Math.min(menuRows, DROPDOWN_VISIBLE_ROWS) * 42
  const addServerExtraHeight = effectiveDropdownMode === "server" ? 38 : 8
  const expandedHeaderHeight = 51 + 12 + expandedRowsHeight + addServerExtraHeight

  const animatedHeaderStyle = useAnimatedStyle(() => ({
    height: interpolate(serverMenuProgress.value, [0, 1], [51, expandedHeaderHeight], Extrapolation.CLAMP),
  }))

  const animatedServerMenuStyle = useAnimatedStyle(() => ({
    opacity: serverMenuProgress.value,
    transform: [
      {
        translateY: interpolate(serverMenuProgress.value, [0, 1], [-8, 0], Extrapolation.CLAMP),
      },
    ],
  }))

  const animatedHeaderShadowStyle = useAnimatedStyle(() => ({
    shadowOpacity: interpolate(serverMenuProgress.value, [0, 1], [0, 0.35], Extrapolation.CLAMP),
    shadowRadius: interpolate(serverMenuProgress.value, [0, 1], [0, 18], Extrapolation.CLAMP),
    elevation: interpolate(serverMenuProgress.value, [0, 1], [0, 16], Extrapolation.CLAMP),
  }))

  const waveformColumnMeta = useMemo(
    () =>
      Array.from({ length: waveformLevels.length }, () => ({
        delay: Math.random() * 1.5,
        duration: 1 + Math.random(),
        phase: Math.random() * Math.PI * 2,
      })),
    [waveformLevels.length],
  )

  const getWaveformCellStyle = useCallback(
    (row: number, col: number) => {
      const level = waveformLevels[col] ?? 0
      const rowFromBottom = WAVEFORM_ROWS - 1 - row
      const intensity = Math.max(0, Math.min(1, level * WAVEFORM_ROWS - rowFromBottom))

      const meta = waveformColumnMeta[col]
      const t = waveformTick / 1000
      const basePhase = (Math.max(0, t - meta.delay) / meta.duration) * Math.PI * 2 + meta.phase + row * 0.35
      const pulse = 0.5 + 0.5 * Math.sin(basePhase)

      let alpha = 0.08
      if (intensity > 0) {
        alpha = (0.4 + intensity * 0.6) * (0.85 + pulse * 0.15)
      } else if (isRecording) {
        alpha = 0.1 + pulse * 0.07
      }

      // Base palette around #78839A, with brighter/lower variants by intensity.
      const baseR = 120
      const baseG = 131
      const baseB = 154
      const lift = Math.round(intensity * 28)
      const r = Math.min(255, baseR + lift)
      const g = Math.min(255, baseG + lift)
      const b = Math.min(255, baseB + lift)

      return {
        backgroundColor: `rgba(${r}, ${g}, ${b}, ${alpha})`,
        borderColor: `rgba(${Math.min(255, r + 8)}, ${Math.min(255, g + 8)}, ${Math.min(255, b + 8)}, ${Math.min(1, alpha + 0.16)})`,
      }
    },
    [isRecording, waveformColumnMeta, waveformLevels, waveformTick],
  )

  const handleControlsLayout = useCallback((event: LayoutChangeEvent) => {
    setControlsWidth(event.nativeEvent.layout.width)
  }, [])

  const handleWaveformLayout = useCallback((event: LayoutChangeEvent) => {
    const width = event.nativeEvent.layout.width
    const columns = Math.max(14, Math.floor((width + WAVEFORM_CELL_GAP) / (WAVEFORM_CELL_SIZE + WAVEFORM_CELL_GAP)))

    if (columns === waveformLevelsRef.current.length) return

    const next = Array.from({ length: columns }, () => 0)
    waveformLevelsRef.current = next
    setWaveformLevels(next)
  }, [])

  const toggleServerMenu = useCallback(() => {
    void Haptics.selectionAsync().catch(() => {})
    setDropdownMode((prev) => {
      const next = prev === "server" ? "none" : "server"
      if (next === "server") {
        setDropdownRenderMode("server")
      }
      if (next === "server") {
        refreshAllServerHealth()
      }
      return next
    })
  }, [refreshAllServerHealth])

  const toggleSessionMenu = useCallback(() => {
    if (!activeServer || activeServer.status !== "online") return
    void Haptics.selectionAsync().catch(() => {})
    void refreshServerStatusAndSessions(activeServer.id)
    setDropdownRenderMode("session")
    setDropdownMode((prev) => (prev === "session" ? "none" : "session"))
  }, [activeServer, refreshServerStatusAndSessions])

  const handleSelectServer = useCallback(
    (id: string) => {
      selectServer(id)
      setDropdownMode("none")
      void refreshServerStatusAndSessions(id)
    },
    [refreshServerStatusAndSessions, selectServer],
  )

  const handleSelectSession = useCallback(
    (id: string) => {
      selectSession(id)
      setDropdownMode("none")
    },
    [selectSession],
  )

  const handleDeleteServer = useCallback(
    (id: string) => {
      const server = serversRef.current.find((s) => s.id === id)
      if (server && devicePushToken && server.relaySecret.trim().length > 0) {
        unregisterRelayDevice({
          relayBaseURL: server.relayURL,
          secret: server.relaySecret.trim(),
          deviceToken: devicePushToken,
        }).catch(() => {})
      }

      removeServer(id)
    },
    [devicePushToken, removeServer, serversRef],
  )

  const handleStartScan = useCallback(async () => {
    scanLockRef.current = false
    const current =
      camera ??
      (await import("expo-camera")
        .catch(() => null)
        .then((mod) => {
          if (!mod) return null

          const direct = (mod as { requestCameraPermissionsAsync?: unknown }).requestCameraPermissionsAsync
          const fromCamera = (mod as { Camera?: { requestCameraPermissionsAsync?: unknown } }).Camera
            ?.requestCameraPermissionsAsync
          let requestCameraPermissionsAsync: (() => Promise<{ granted: boolean }>) | null = null
          if (typeof direct === "function") {
            requestCameraPermissionsAsync = direct as () => Promise<{ granted: boolean }>
          } else if (typeof fromCamera === "function") {
            requestCameraPermissionsAsync = fromCamera as () => Promise<{ granted: boolean }>
          }

          if (!requestCameraPermissionsAsync) {
            return null
          }

          const next = {
            CameraView: mod.CameraView,
            requestCameraPermissionsAsync,
          }
          setCamera(next)
          return next
        }))
    if (!current) {
      Alert.alert("Scanner unavailable", "This build does not include camera support. Reinstall the latest dev build.")
      return
    }
    if (camGranted) {
      setScanOpen(true)
      return
    }
    const res = await current.requestCameraPermissionsAsync()
    if (!res.granted) return
    setCamGranted(true)
    setScanOpen(true)
  }, [camGranted, camera])

  const completeOnboarding = useCallback(
    (openScanner: boolean) => {
      setOnboardingComplete(true)
      void FileSystem.writeAsStringAsync(ONBOARDING_STATE_FILE, JSON.stringify({ completed: true })).catch(() => {})

      if (openScanner) {
        void handleStartScan()
      }
    },
    [handleStartScan],
  )

  const handleReplayOnboarding = useCallback(() => {
    setWhisperSettingsOpen(false)
    setScanOpen(false)
    setDropdownMode("none")
    setOnboardingStep(0)
    setMicrophonePermissionState(permissionGranted ? "granted" : "idle")
    setNotificationPermissionState("idle")
    setLocalNetworkPermissionState("idle")
    setOnboardingReady(true)
    setOnboardingComplete(false)
    void FileSystem.deleteAsync(ONBOARDING_STATE_FILE, { idempotent: true }).catch(() => {})
  }, [permissionGranted])

  const connectPairPayload = useCallback(
    (rawData: string, source: "scan" | "link") => {
      const fromScan = source === "scan"
      if (fromScan && scanLockRef.current) return

      if (fromScan) {
        scanLockRef.current = true
      }

      const pair = parsePair(rawData)
      if (!pair) {
        if (fromScan) {
          void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {})
          setTimeout(() => {
            scanLockRef.current = false
          }, 750)
        }
        return
      }

      void pickHost(pair.hosts)
        .then((host) => {
          if (!host) {
            if (fromScan) {
              scanLockRef.current = false
            }
            return
          }

          const ok = addServer(host, pair.relayURL, pair.relaySecret, pair.serverID)
          if (!ok) {
            if (fromScan) {
              scanLockRef.current = false
            }
            return
          }

          setScanOpen(false)
          void Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {})
        })
        .catch(() => {
          if (fromScan) {
            scanLockRef.current = false
          }
        })
    },
    [addServer],
  )

  const handleScan = useCallback(
    (event: Scan) => {
      connectPairPayload(event.data, "scan")
    },
    [connectPairPayload],
  )

  useEffect(() => {
    if (scanOpen) return
    scanLockRef.current = false
  }, [scanOpen])

  useEffect(() => {
    let active = true

    const handleURL = async (url: string | null) => {
      if (!url) return
      if (!parsePair(url)) return

      if (!restoredRef.current) {
        for (let attempt = 0; attempt < 20; attempt += 1) {
          await new Promise((resolve) => setTimeout(resolve, 100))
          if (restoredRef.current || !active) {
            break
          }
        }
      }

      if (!active) return
      connectPairPayload(url, "link")
    }

    void Linking.getInitialURL()
      .then((url) => handleURL(url))
      .catch(() => {})

    const sub = Linking.addEventListener("url", (event) => {
      void handleURL(event.url)
    })

    return () => {
      active = false
      sub.remove()
    }
  }, [connectPairPayload, restoredRef])

  useEffect(() => {
    if (!activeServerId) return
    void refreshServerStatusAndSessions(activeServerId)
    const timer = setInterval(() => {
      void refreshServerStatusAndSessions(activeServerId)
    }, 15000)
    return () => clearInterval(timer)
  }, [activeServerId, refreshServerStatusAndSessions])

  const defaultModelInstalled = installedWhisperModels.includes(defaultWhisperModel)
  let onboardingProgressRaw = 0
  if (downloadingModelID) {
    onboardingProgressRaw = downloadProgress
  } else if (defaultModelInstalled || activeWhisperModel === defaultWhisperModel) {
    onboardingProgressRaw = 1
  } else if (isPreparingWhisperModel) {
    onboardingProgressRaw = 0.12
  }
  const onboardingProgress = Math.max(0, Math.min(1, onboardingProgressRaw))
  const onboardingProgressPct = Math.round(onboardingProgress * 100)
  let onboardingModelStatus = "Downloading model in background"
  if (downloadingModelID) {
    onboardingModelStatus = `Downloading model in background ${onboardingProgressPct}%`
  } else if (onboardingProgress >= 1) {
    onboardingModelStatus = "Model ready in background"
  }
  const onboardingSteps = [
    {
      title: "Allow mic access.",
      body: "Control only listens while you hold the record button.",
      primaryLabel: microphonePermissionState === "pending" ? "Requesting microphone..." : "Allow microphone",
      primaryDisabled: microphonePermissionState === "pending",
      secondaryLabel: "Continue without granting",
      visualTag: "MIC",
      visualSurfaceStyle: styles.onboardingVisualSurfaceMic,
      visualOrbStyle: styles.onboardingVisualOrbMic,
      visualTagStyle: styles.onboardingVisualTagMic,
    },
    {
      title: "Turn on notifications.",
      body: "Get alerts when your OpenCode run finishes, fails, or needs your attention.",
      primaryLabel: notificationPermissionState === "pending" ? "Requesting notifications..." : "Allow notifications",
      primaryDisabled: notificationPermissionState === "pending",
      secondaryLabel: "Continue without granting",
      visualTag: "PUSH",
      visualSurfaceStyle: styles.onboardingVisualSurfaceNotifications,
      visualOrbStyle: styles.onboardingVisualOrbNotifications,
      visualTagStyle: styles.onboardingVisualTagNotifications,
    },
    {
      title: "Enable local network.",
      body: "This lets Control discover your machine on the same network.",
      primaryLabel: localNetworkPermissionState === "pending" ? "Requesting local network..." : "Allow local network",
      primaryDisabled: localNetworkPermissionState === "pending",
      secondaryLabel: "Continue without granting",
      visualTag: "LAN",
      visualSurfaceStyle: styles.onboardingVisualSurfaceNetwork,
      visualOrbStyle: styles.onboardingVisualOrbNetwork,
      visualTagStyle: styles.onboardingVisualTagNetwork,
    },
    {
      title: "Pair your computer.",
      body: "Start `opencode serve` on your computer, then scan the QR code to pair.",
      primaryLabel: "Scan OpenCode QR",
      primaryDisabled: false,
      secondaryLabel: "I will do this later",
      visualTag: "PAIR",
      visualSurfaceStyle: styles.onboardingVisualSurfacePair,
      visualOrbStyle: styles.onboardingVisualOrbPair,
      visualTagStyle: styles.onboardingVisualTagPair,
    },
  ] as const
  const onboardingStepCount = onboardingSteps.length
  const clampedOnboardingStep = Math.max(0, Math.min(onboardingStep, onboardingStepCount - 1))
  const onboardingCurrentStep = onboardingSteps[clampedOnboardingStep]
  const {
    title: onboardingTitle,
    body: onboardingBody,
    primaryLabel: onboardingPrimaryLabel,
    primaryDisabled: onboardingPrimaryDisabled,
    secondaryLabel: onboardingSecondaryLabel,
    visualTag: onboardingVisualTag,
    visualSurfaceStyle: onboardingVisualSurfaceStyle,
    visualOrbStyle: onboardingVisualOrbStyle,
    visualTagStyle: onboardingVisualTagStyle,
  } = onboardingCurrentStep
  const onboardingSafeStyle = useMemo(
    () => [styles.onboardingRoot, { paddingTop: insets.top + 8, paddingBottom: Math.max(insets.bottom, 16) }],
    [insets.bottom, insets.top],
  )

  if (!onboardingReady) {
    return (
      <SafeAreaView style={onboardingSafeStyle} edges={["left", "right"]}>
        <StatusBar style="light" />
      </SafeAreaView>
    )
  }

  if (!onboardingComplete) {
    return (
      <SafeAreaView style={onboardingSafeStyle} edges={["left", "right"]}>
        <StatusBar style="light" />

        <View style={styles.onboardingShell}>
          <View style={styles.onboardingTopRail}>
            <View style={styles.onboardingModelRow}>
              <Text style={styles.onboardingModelText}>{onboardingModelStatus}</Text>
              <View style={styles.onboardingModelTrack}>
                <View
                  style={[
                    styles.onboardingModelFill,
                    { width: `${Math.max(onboardingProgressPct, onboardingProgress > 0 ? 6 : 0)}%` },
                  ]}
                />
              </View>
            </View>
          </View>

          <View style={styles.onboardingContent}>
            <View style={[styles.onboardingVisualSurface, onboardingVisualSurfaceStyle]}>
              <View style={[styles.onboardingVisualOrb, styles.onboardingVisualOrbOne, onboardingVisualOrbStyle]} />
              <View style={[styles.onboardingVisualOrb, styles.onboardingVisualOrbTwo, onboardingVisualOrbStyle]} />
              <View style={[styles.onboardingVisualTag, onboardingVisualTagStyle]}>
                <Text style={styles.onboardingVisualTagText}>{onboardingVisualTag}</Text>
              </View>
            </View>

            <View style={styles.onboardingCopyBlock}>
              <Text
                style={styles.onboardingEyebrow}
              >{`STEP ${clampedOnboardingStep + 1} OF ${onboardingStepCount}`}</Text>
              <Text style={styles.onboardingTitle}>{onboardingTitle}</Text>
              <Text style={styles.onboardingBody}>{onboardingBody}</Text>
            </View>
          </View>

          <View style={styles.onboardingFooter}>
            <Pressable
              onPress={() => {
                if (clampedOnboardingStep === 0) {
                  void (async () => {
                    await handleRequestMicrophonePermission()
                    setOnboardingStep(1)
                  })()
                  return
                }

                if (clampedOnboardingStep === 1) {
                  void (async () => {
                    await handleRequestNotificationPermission()
                    setOnboardingStep(2)
                  })()
                  return
                }

                if (clampedOnboardingStep === 2) {
                  void (async () => {
                    await handleRequestLocalNetworkPermission()
                    setOnboardingStep(3)
                  })()
                  return
                }

                completeOnboarding(true)
              }}
              style={({ pressed }) => [
                styles.onboardingPrimaryButton,
                onboardingPrimaryDisabled && styles.onboardingPrimaryButtonDisabled,
                pressed && styles.clearButtonPressed,
              ]}
              disabled={onboardingPrimaryDisabled}
            >
              <Text style={styles.onboardingPrimaryButtonText}>{onboardingPrimaryLabel}</Text>
              <SymbolView
                name={{ ios: "arrow.right", android: "arrow_forward", web: "arrow_forward" }}
                size={20}
                weight="semibold"
                tintColor="#FFFFFF"
              />
            </Pressable>

            <Pressable
              onPress={() => {
                if (clampedOnboardingStep < onboardingStepCount - 1) {
                  setOnboardingStep((step) => Math.min(step + 1, onboardingStepCount - 1))
                  return
                }

                completeOnboarding(false)
              }}
              style={({ pressed }) => [styles.onboardingSecondaryButton, pressed && styles.clearButtonPressed]}
            >
              <Text style={styles.onboardingSecondaryText}>{onboardingSecondaryLabel}</Text>
            </Pressable>
          </View>
        </View>
      </SafeAreaView>
    )
  }

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar style="light" />

      {isDropdownOpen ? <Pressable style={styles.dismissOverlay} onPress={closeDropdown} /> : null}

      {/* Workspace header */}
      <View style={styles.headerAnchor}>
        <Animated.View style={[styles.statusBar, animatedHeaderStyle, animatedHeaderShadowStyle]}>
          {activeServer ? (
            <View style={styles.headerSplitRow}>
              <Pressable
                onPress={toggleServerMenu}
                style={({ pressed }) => [styles.headerSplitLeft, pressed && styles.clearButtonPressed]}
              >
                <View style={styles.headerServerLabel}>
                  <View style={[styles.serverStatusDot, headerDotStyle]} />
                  <Text
                    style={[styles.workspaceHeaderText, styles.headerServerText]}
                    numberOfLines={1}
                    ellipsizeMode="tail"
                  >
                    {activeServer.name}
                  </Text>
                </View>
              </Pressable>

              <View style={styles.headerSplitDivider} />

              <Pressable
                onPress={toggleSessionMenu}
                style={({ pressed }) => [styles.headerSplitRight, pressed && styles.clearButtonPressed]}
              >
                <Text
                  style={[styles.workspaceHeaderText, styles.headerSessionText]}
                  numberOfLines={1}
                  ellipsizeMode="tail"
                >
                  {activeSession?.title ?? "Select session"}
                </Text>
              </Pressable>
            </View>
          ) : (
            <Pressable
              onPress={toggleServerMenu}
              style={({ pressed }) => [styles.statusBarTapArea, pressed && styles.clearButtonPressed]}
            >
              <View style={styles.headerServerLabel}>
                <View style={[styles.serverStatusDot, headerDotStyle]} />
                <Text style={styles.workspaceHeaderText}>{headerTitle}</Text>
              </View>
            </Pressable>
          )}

          <Animated.View
            style={[styles.serverMenuInline, animatedServerMenuStyle]}
            pointerEvents={isDropdownOpen ? "auto" : "none"}
          >
            <ScrollView
              style={styles.dropdownListViewport}
              contentContainerStyle={styles.dropdownListContent}
              showsVerticalScrollIndicator={false}
              bounces={false}
            >
              {effectiveDropdownMode === "server" ? (
                servers.length === 0 ? (
                  <Text style={styles.serverEmptyText}>No servers yet</Text>
                ) : (
                  servers.map((server) => (
                    <Pressable
                      key={server.id}
                      onPress={() => handleSelectServer(server.id)}
                      style={({ pressed }) => [styles.serverRow, pressed && styles.serverRowPressed]}
                    >
                      <View
                        style={[
                          styles.serverStatusDot,
                          server.status === "online" ? styles.serverStatusActive : styles.serverStatusOffline,
                        ]}
                      />
                      <Text style={styles.serverNameText}>{server.name}</Text>
                      <Pressable onPress={() => handleDeleteServer(server.id)} hitSlop={8}>
                        <Text style={styles.serverDeleteIcon}>✕</Text>
                      </Pressable>
                    </Pressable>
                  ))
                )
              ) : activeServer ? (
                activeServer.sessions.length === 0 ? (
                  activeServer.sessionsLoading ? null : (
                    <Text style={styles.serverEmptyText}>No sessions available</Text>
                  )
                ) : (
                  activeServer.sessions.map((session, index) => (
                    <Pressable
                      key={session.id}
                      onPress={() => handleSelectSession(session.id)}
                      style={({ pressed }) => [
                        styles.serverRow,
                        index === activeServer.sessions.length - 1 && styles.serverRowLast,
                        pressed && styles.serverRowPressed,
                      ]}
                    >
                      <View style={[styles.serverStatusDot, styles.serverStatusActive]} />
                      <Text style={styles.serverNameText} numberOfLines={1}>
                        {session.title}
                      </Text>
                      <Text style={styles.sessionUpdatedText}>{formatSessionUpdated(session.updated)}</Text>
                    </Pressable>
                  ))
                )
              ) : (
                <Text style={styles.serverEmptyText}>Select a server first</Text>
              )}
            </ScrollView>

            {effectiveDropdownMode === "server" ? (
              <Pressable onPress={() => void handleStartScan()} style={styles.addServerButton}>
                <Text style={styles.addServerButtonText}>Add server by scanning QR code</Text>
              </Pressable>
            ) : null}
          </Animated.View>
        </Animated.View>
      </View>

      {/* Transcription area */}
      <View style={styles.transcriptionArea}>
        {shouldShowAgentStateCard ? (
          <View style={styles.splitCardStack}>
            <View style={[styles.splitCard, styles.replyCard]}>
              <View style={styles.agentStateHeaderRow}>
                <View style={styles.agentStateTitleWrap}>
                  <View style={styles.agentStateIconWrap}>
                    {agentStateIcon === "loading" ? (
                      <ActivityIndicator size="small" color="#91A0C0" />
                    ) : (
                      <SymbolView
                        name={{ ios: "checkmark.circle.fill", android: "check_circle", web: "check_circle" }}
                        size={16}
                        tintColor="#91C29D"
                      />
                    )}
                  </View>
                  <Text style={styles.replyCardLabel}>Agent</Text>
                </View>
                <Pressable onPress={handleHideAgentState} hitSlop={8}>
                  <Text style={styles.agentStateClose}>✕</Text>
                </Pressable>
              </View>
              <ScrollView style={styles.replyScroll} contentContainerStyle={styles.replyContent}>
                <Text style={styles.replyText}>{agentStateText}</Text>
              </ScrollView>
            </View>

            <View style={styles.transcriptionPanel}>
              <View style={styles.transcriptionTopActions} pointerEvents="box-none">
                <Pressable
                  onPress={handleOpenWhisperSettings}
                  style={({ pressed }) => [styles.clearButton, pressed && styles.clearButtonPressed]}
                  hitSlop={8}
                >
                  <SymbolView
                    name={{ ios: "gearshape.fill", android: "settings", web: "settings" }}
                    size={18}
                    weight="semibold"
                    tintColor="#B8BDC9"
                  />
                </Pressable>
                <Pressable
                  onPress={handleClearTranscript}
                  style={({ pressed }) => [styles.clearButton, pressed && styles.clearButtonPressed]}
                  hitSlop={8}
                >
                  <Animated.Text style={[styles.clearIcon, animatedClearIconStyle]}>↻</Animated.Text>
                </Pressable>
              </View>

              {whisperError ? (
                <View style={styles.modelErrorBadge}>
                  <Text style={styles.modelErrorText}>{whisperError}</Text>
                </View>
              ) : null}

              <ScrollView
                ref={scrollViewRef}
                style={styles.transcriptionScroll}
                contentContainerStyle={styles.transcriptionContent}
                onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
              >
                <Animated.View style={animatedTranscriptSendStyle}>
                  {transcribedText ? (
                    <Text style={styles.transcriptionText}>{transcribedText}</Text>
                  ) : (
                    <Text style={styles.placeholderText}>Your transcription will appear here…</Text>
                  )}
                </Animated.View>
              </ScrollView>

              <Animated.View
                style={[styles.waveformBoxesRow, animatedWaveformRowStyle]}
                pointerEvents="none"
                onLayout={handleWaveformLayout}
              >
                {Array.from({ length: WAVEFORM_ROWS }).map((_, row) => (
                  <View key={`row-${row}`} style={styles.waveformGridRow}>
                    {waveformLevels.map((_, col) => (
                      <View key={`cell-${row}-${col}`} style={[styles.waveformBox, getWaveformCellStyle(row, col)]} />
                    ))}
                  </View>
                ))}
              </Animated.View>
            </View>
          </View>
        ) : (
          <View style={styles.transcriptionPanel}>
            <View style={styles.transcriptionTopActions} pointerEvents="box-none">
              <Pressable
                onPress={handleOpenWhisperSettings}
                style={({ pressed }) => [styles.clearButton, pressed && styles.clearButtonPressed]}
                hitSlop={8}
              >
                <SymbolView
                  name={{ ios: "gearshape.fill", android: "settings", web: "settings" }}
                  size={18}
                  weight="semibold"
                  tintColor="#B8BDC9"
                />
              </Pressable>
              <Pressable
                onPress={handleClearTranscript}
                style={({ pressed }) => [styles.clearButton, pressed && styles.clearButtonPressed]}
                hitSlop={8}
              >
                <Animated.Text style={[styles.clearIcon, animatedClearIconStyle]}>↻</Animated.Text>
              </Pressable>
            </View>

            {whisperError ? (
              <View style={styles.modelErrorBadge}>
                <Text style={styles.modelErrorText}>{whisperError}</Text>
              </View>
            ) : null}

            <ScrollView
              ref={scrollViewRef}
              style={styles.transcriptionScroll}
              contentContainerStyle={styles.transcriptionContent}
              onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
            >
              <Animated.View style={animatedTranscriptSendStyle}>
                {transcribedText ? (
                  <Text style={styles.transcriptionText}>{transcribedText}</Text>
                ) : (
                  <Text style={styles.placeholderText}>Your transcription will appear here…</Text>
                )}
              </Animated.View>
            </ScrollView>

            <Animated.View
              style={[styles.waveformBoxesRow, animatedWaveformRowStyle]}
              pointerEvents="none"
              onLayout={handleWaveformLayout}
            >
              {Array.from({ length: WAVEFORM_ROWS }).map((_, row) => (
                <View key={`row-${row}`} style={styles.waveformGridRow}>
                  {waveformLevels.map((_, col) => (
                    <View key={`cell-${row}-${col}`} style={[styles.waveformBox, getWaveformCellStyle(row, col)]} />
                  ))}
                </View>
              ))}
            </Animated.View>
          </View>
        )}
      </View>

      {/* Record button */}
      <View style={styles.controlsRow} onLayout={handleControlsLayout}>
        <Pressable
          onPressIn={handlePressIn}
          onPressOut={handlePressOut}
          disabled={!permissionGranted || modelLoading}
          style={[styles.recordPressable, !permissionGranted && styles.recordButtonDisabled]}
        >
          <View style={styles.recordButton}>
            {isTranscribingBulk ? (
              <View style={styles.recordBusyCenter}>
                <ActivityIndicator color="#FF2E3F" size="small" />
              </View>
            ) : modelLoadingState !== "ready" ? (
              <>
                <View
                  style={[
                    styles.loadFill,
                    modelLoadingState === "loading" && styles.loadFillPending,
                    { width: modelLoadingState === "downloading" ? `${Math.max(pct, 3)}%` : "100%" },
                  ]}
                />
                <View style={styles.loadOverlay} pointerEvents="none">
                  <Text style={styles.loadText}>
                    {modelLoadingState === "downloading"
                      ? `Downloading ${loadingModelLabel} ${pct}%`
                      : `Loading ${loadingModelLabel}`}
                  </Text>
                </View>
              </>
            ) : (
              <>
                <Animated.View style={[styles.recordBorder, animatedBorderStyle]} pointerEvents="none" />
                <Animated.View style={[styles.recordDot, animatedDotStyle]} />
              </>
            )}
          </View>
        </Pressable>

        <Animated.View style={[styles.sendSlot, animatedSendStyle]} pointerEvents={shouldShowSend ? "auto" : "none"}>
          <Pressable
            onPress={handleSendTranscript}
            style={({ pressed }) => [
              styles.sendButton,
              (isSending || !hasTranscript || !canSendToSession) && styles.sendButtonDisabled,
              pressed && styles.clearButtonPressed,
            ]}
            disabled={isSending || !hasTranscript || !canSendToSession}
            hitSlop={8}
          >
            <Text style={styles.sendIcon}>↑</Text>
          </Pressable>
        </Animated.View>
      </View>

      <Modal
        visible={whisperSettingsOpen}
        animationType="slide"
        presentationStyle="formSheet"
        onRequestClose={() => setWhisperSettingsOpen(false)}
      >
        <SafeAreaView style={styles.settingsRoot}>
          <View style={styles.settingsTop}>
            <View style={styles.settingsTitleBlock}>
              <Text style={styles.settingsTitle}>Settings</Text>
              <Text style={styles.settingsSubtitle}>Default: {WHISPER_MODEL_LABELS[defaultWhisperModel]}</Text>
            </View>
            <Pressable onPress={() => setWhisperSettingsOpen(false)}>
              <Text style={styles.settingsClose}>Done</Text>
            </Pressable>
          </View>

          <ScrollView style={styles.settingsScroll} contentContainerStyle={styles.settingsContent}>
            <View style={styles.settingsSection}>
              <Text style={styles.settingsSectionLabel}>DEVELOPMENT:</Text>
              {__DEV__ ? (
                <Pressable
                  onPress={handleReplayOnboarding}
                  style={({ pressed }) => [styles.settingsTextRow, pressed && styles.clearButtonPressed]}
                >
                  <Text style={styles.settingsTextRowTitle}>Replay onboarding</Text>
                  <Text style={styles.settingsTextRowAction}>Run</Text>
                </Pressable>
              ) : (
                <View style={styles.settingsTextRow}>
                  <Text style={styles.settingsMutedText}>Available in development builds.</Text>
                </View>
              )}
            </View>

            <View style={styles.settingsSection}>
              <Text style={styles.settingsSectionLabel}>GENERAL:</Text>
              <View style={styles.settingsTextRow}>
                <Text style={styles.settingsTextRowTitle}>Default model</Text>
                <Text style={styles.settingsTextRowValue}>{WHISPER_MODEL_LABELS[defaultWhisperModel]}</Text>
              </View>

              <Pressable
                onPress={() => setTranscriptionMode("bulk")}
                disabled={isRecording || isTranscribingBulk}
                style={({ pressed }) => [
                  styles.settingsTextRow,
                  (isRecording || isTranscribingBulk) && styles.settingsInlinePressableDisabled,
                  pressed && styles.clearButtonPressed,
                ]}
              >
                <View style={styles.settingsOptionCopy}>
                  <Text style={styles.settingsTextRowTitle}>On Release</Text>
                  <Text style={styles.settingsTextRowMeta}>Transcribe after release</Text>
                </View>
                <Text
                  style={[
                    styles.settingsTextRowAction,
                    transcriptionMode === "bulk" && styles.settingsTextRowActionActive,
                  ]}
                >
                  {transcriptionMode === "bulk" ? "Selected" : "Use"}
                </Text>
              </Pressable>

              <Pressable
                onPress={() => setTranscriptionMode("realtime")}
                disabled={isRecording || isTranscribingBulk}
                style={({ pressed }) => [
                  styles.settingsTextRow,
                  (isRecording || isTranscribingBulk) && styles.settingsInlinePressableDisabled,
                  pressed && styles.clearButtonPressed,
                ]}
              >
                <View style={styles.settingsOptionCopy}>
                  <Text style={styles.settingsTextRowTitle}>Realtime</Text>
                  <Text style={styles.settingsTextRowMeta}>Transcribe while you speak</Text>
                </View>
                <Text
                  style={[
                    styles.settingsTextRowAction,
                    transcriptionMode === "realtime" && styles.settingsTextRowActionActive,
                  ]}
                >
                  {transcriptionMode === "realtime" ? "Selected" : "Use"}
                </Text>
              </Pressable>
            </View>

            <View style={styles.settingsSection}>
              <Text style={styles.settingsSectionLabel}>MODELS:</Text>
              {WHISPER_MODELS.map((modelID) => {
                const installed = installedWhisperModels.includes(modelID)
                const isDefault = defaultWhisperModel === modelID
                const isDownloading = downloadingModelID === modelID
                const actionDisabled = (downloadingModelID !== null && !isDownloading) || isTranscribingBulk
                const downloadPct = Math.round(Math.max(0, Math.min(1, downloadProgress)) * 100)
                const actionLabel = isDownloading
                  ? `${downloadPct}%`
                  : installed
                    ? isDefault
                      ? "Selected"
                      : "Select"
                    : "Download"
                const sizeLabel = formatWhisperModelSize(WHISPER_MODEL_SIZES[modelID])
                const rowMeta = [sizeLabel, installed ? "installed" : null, isDefault ? "default" : null]
                  .filter(Boolean)
                  .join(" · ")

                return (
                  <View key={modelID} style={styles.settingsInlineRow}>
                    <Pressable
                      onPress={() => {
                        if (installed) {
                          void handleSelectWhisperModel(modelID)
                        }
                      }}
                      onLongPress={() => {
                        if (!installed || isDownloading) return
                        Alert.alert("Delete model?", `Remove ${modelID} from this device?`, [
                          { text: "Cancel", style: "cancel" },
                          {
                            text: "Delete",
                            style: "destructive",
                            onPress: () => {
                              void handleDeleteWhisperModel(modelID)
                            },
                          },
                        ])
                      }}
                      delayLongPress={350}
                      disabled={!installed || actionDisabled || isPreparingWhisperModel}
                      style={({ pressed }) => [
                        styles.settingsInlineLabelPressable,
                        (!installed || actionDisabled || isPreparingWhisperModel) &&
                          styles.settingsInlinePressableDisabled,
                        pressed && styles.clearButtonPressed,
                      ]}
                    >
                      <Text style={styles.settingsInlineName}>{modelID}</Text>
                      <Text style={styles.settingsInlineMeta}>{rowMeta}</Text>
                    </Pressable>

                    <Pressable
                      onPress={() => {
                        if (isDownloading) return
                        if (installed) {
                          void handleSelectWhisperModel(modelID)
                          return
                        }
                        void handleDownloadWhisperModel(modelID)
                      }}
                      disabled={actionDisabled || (installed && isPreparingWhisperModel)}
                      accessibilityLabel={actionLabel}
                      style={({ pressed }) => [
                        styles.settingsInlineTextActionPressable,
                        (actionDisabled || (installed && isPreparingWhisperModel)) &&
                          styles.settingsInlinePressableDisabled,
                        pressed && styles.clearButtonPressed,
                      ]}
                    >
                      <Text
                        style={[
                          styles.settingsInlineTextAction,
                          installed && styles.settingsInlineTextActionInstalled,
                          isDownloading && styles.settingsInlineTextActionDownloading,
                        ]}
                      >
                        {actionLabel}
                      </Text>
                    </Pressable>
                  </View>
                )
              })}
            </View>
          </ScrollView>
        </SafeAreaView>
      </Modal>

      <Modal
        visible={scanOpen}
        animationType="slide"
        presentationStyle="formSheet"
        onRequestClose={() => setScanOpen(false)}
      >
        <SafeAreaView style={styles.scanRoot}>
          <View style={styles.scanTop}>
            <Text style={styles.scanTitle}>Scan server QR</Text>
            <Pressable onPress={() => setScanOpen(false)}>
              <Text style={styles.scanClose}>Close</Text>
            </Pressable>
          </View>
          {camGranted && camera ? (
            <camera.CameraView
              style={styles.scanCam}
              barcodeScannerSettings={{ barcodeTypes: ["qr"] }}
              onBarcodeScanned={handleScan}
            />
          ) : (
            <View style={styles.scanEmpty}>
              <Text style={styles.scanHint}>Camera permission is required to scan setup QR codes.</Text>
            </View>
          )}
        </SafeAreaView>
      </Modal>
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#121212",
    position: "relative",
  },
  onboardingRoot: {
    flex: 1,
    backgroundColor: "#121212",
    paddingHorizontal: 16,
  },
  onboardingShell: {
    flex: 1,
  },
  onboardingTopRail: {
    gap: 8,
    marginBottom: 10,
  },
  onboardingContent: {
    flex: 1,
    justifyContent: "center",
    alignItems: "stretch",
    gap: 22,
    paddingHorizontal: 2,
  },
  onboardingModelRow: {
    gap: 6,
  },
  onboardingModelText: {
    color: "#A9A9A9",
    fontSize: 11,
    fontWeight: "700",
    letterSpacing: 0.35,
    textTransform: "uppercase",
  },
  onboardingModelTrack: {
    height: 4,
    width: "100%",
    borderRadius: 999,
    backgroundColor: "#2C2C2C",
    overflow: "hidden",
  },
  onboardingModelFill: {
    height: "100%",
    borderRadius: 999,
    backgroundColor: "#FF5B47",
  },
  onboardingVisualSurface: {
    width: "100%",
    minHeight: 176,
    borderRadius: 26,
    borderWidth: 1,
    alignItems: "center",
    justifyContent: "center",
    overflow: "hidden",
    backgroundColor: "#171717",
    borderColor: "#2B2B2B",
  },
  onboardingVisualSurfaceMic: {
    backgroundColor: "#1A2118",
    borderColor: "#2F3D2D",
  },
  onboardingVisualSurfaceNotifications: {
    backgroundColor: "#1A1D2A",
    borderColor: "#303A5A",
  },
  onboardingVisualSurfaceNetwork: {
    backgroundColor: "#1A2218",
    borderColor: "#344930",
  },
  onboardingVisualSurfacePair: {
    backgroundColor: "#1F1A27",
    borderColor: "#413157",
  },
  onboardingVisualOrb: {
    position: "absolute",
    borderRadius: 999,
    opacity: 0.22,
  },
  onboardingVisualOrbOne: {
    width: 130,
    height: 130,
    top: -28,
    left: -22,
  },
  onboardingVisualOrbTwo: {
    width: 160,
    height: 160,
    bottom: -52,
    right: -44,
  },
  onboardingVisualOrbMic: {
    backgroundColor: "#61C372",
  },
  onboardingVisualOrbNotifications: {
    backgroundColor: "#4A6EE0",
  },
  onboardingVisualOrbNetwork: {
    backgroundColor: "#78B862",
  },
  onboardingVisualOrbPair: {
    backgroundColor: "#9B6CDC",
  },
  onboardingVisualTag: {
    borderRadius: 20,
    paddingHorizontal: 24,
    paddingVertical: 12,
    borderWidth: 1,
    shadowColor: "#000000",
    shadowOffset: { width: 0, height: 8 },
    shadowOpacity: 0.12,
    shadowRadius: 16,
    elevation: 3,
  },
  onboardingVisualTagMic: {
    backgroundColor: "#253A25",
    borderColor: "#3A5C3A",
  },
  onboardingVisualTagNotifications: {
    backgroundColor: "#223561",
    borderColor: "#38518C",
  },
  onboardingVisualTagNetwork: {
    backgroundColor: "#284122",
    borderColor: "#3D6835",
  },
  onboardingVisualTagPair: {
    backgroundColor: "#3B2859",
    borderColor: "#5A3D86",
  },
  onboardingVisualTagText: {
    color: "#F6F7F8",
    fontSize: 22,
    fontWeight: "800",
    letterSpacing: 1.8,
  },
  onboardingCopyBlock: {
    alignItems: "flex-start",
    gap: 10,
    width: "100%",
  },
  onboardingEyebrow: {
    color: "#7F7F7F",
    fontSize: 11,
    fontWeight: "700",
    letterSpacing: 1.3,
  },
  onboardingTitle: {
    color: "#F1F1F1",
    fontSize: 34,
    fontWeight: "800",
    textAlign: "left",
    letterSpacing: -1,
    lineHeight: 38,
  },
  onboardingBody: {
    color: "#B4B4B4",
    fontSize: 18,
    lineHeight: 25,
    textAlign: "left",
    paddingHorizontal: 0,
  },
  onboardingFooter: {
    gap: 10,
    paddingTop: 6,
  },
  onboardingPrimaryButton: {
    height: 56,
    borderRadius: 14,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#1D6FF4",
    borderWidth: 2,
    borderColor: "#1557C3",
    flexDirection: "row",
    gap: 10,
    shadowColor: "#000000",
    shadowOffset: { width: 0, height: 8 },
    shadowOpacity: 0.2,
    shadowRadius: 18,
    elevation: 4,
  },
  onboardingPrimaryButtonDisabled: {
    opacity: 0.6,
  },
  onboardingPrimaryButtonText: {
    color: "#FFFFFF",
    fontSize: 17,
    fontWeight: "700",
    letterSpacing: 0.2,
  },
  onboardingSecondaryButton: {
    alignSelf: "flex-start",
    paddingVertical: 8,
    paddingHorizontal: 2,
  },
  onboardingSecondaryText: {
    color: "#959CAA",
    fontSize: 14,
    fontWeight: "600",
    textAlign: "left",
  },
  dismissOverlay: {
    ...StyleSheet.absoluteFillObject,
    zIndex: 15,
  },
  headerAnchor: {
    marginHorizontal: 6,
    marginTop: 5,
    height: 51,
    zIndex: 30,
  },
  statusBar: {
    position: "absolute",
    top: 0,
    left: 0,
    right: 0,
    backgroundColor: "#151515",
    borderRadius: 20,
    borderWidth: 3,
    borderColor: "#282828",
    paddingHorizontal: 14,
    paddingTop: 0,
    overflow: "hidden",
    shadowColor: "#000000",
  },
  statusBarInner: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    minHeight: 30,
  },
  statusBarTapArea: {
    height: 45,
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "flex-start",
  },
  headerServerLabel: {
    flexDirection: "row",
    alignItems: "center",
    gap: 8,
    width: "100%",
  },
  headerSplitRow: {
    height: 45,
    flexDirection: "row",
    alignItems: "center",
  },
  headerSplitLeft: {
    flex: 1,
    flexBasis: 0,
    minWidth: 0,
    height: "100%",
    justifyContent: "center",
    alignItems: "flex-start",
    paddingRight: 8,
  },
  headerSplitDivider: {
    width: 4,
    height: 4,
    borderRadius: 2,
    backgroundColor: "#3F4556",
    marginHorizontal: 6,
  },
  headerSplitRight: {
    flex: 1,
    flexBasis: 0,
    minWidth: 0,
    height: "100%",
    justifyContent: "center",
    alignItems: "flex-start",
    paddingLeft: 8,
  },
  workspaceHeaderText: {
    color: "#8F8F8F",
    fontSize: 14,
    fontWeight: "600",
  },
  headerServerText: {
    flex: 1,
    minWidth: 0,
    width: "100%",
  },
  headerSessionText: {
    flexShrink: 1,
    minWidth: 0,
    width: "100%",
    textAlign: "left",
  },
  serverMenuInline: {
    marginTop: 8,
    paddingBottom: 8,
    gap: 4,
  },
  dropdownListViewport: {
    maxHeight: DROPDOWN_VISIBLE_ROWS * 42,
  },
  dropdownListContent: {
    paddingBottom: 2,
  },
  serverEmptyText: {
    color: "#6F7686",
    fontSize: 13,
    textAlign: "center",
    paddingVertical: 10,
  },
  serverRow: {
    flexDirection: "row",
    alignItems: "center",
    gap: 10,
    paddingHorizontal: 4,
    paddingVertical: 8,
    borderBottomWidth: 1,
    borderBottomColor: "#222733",
  },
  serverRowLast: {
    borderBottomWidth: 0,
  },
  serverRowPressed: {
    opacity: 0.85,
  },
  serverStatusDot: {
    width: 9,
    height: 9,
    borderRadius: 5,
  },
  serverStatusActive: {
    backgroundColor: "#4CC26A",
  },
  serverStatusChecking: {
    backgroundColor: "#D2A542",
  },
  serverStatusOffline: {
    backgroundColor: "#D14C55",
  },
  serverNameText: {
    flex: 1,
    color: "#D6DAE4",
    fontSize: 14,
    fontWeight: "500",
  },
  sessionUpdatedText: {
    color: "#8E96A8",
    fontSize: 12,
    fontWeight: "500",
    marginLeft: 8,
  },
  serverDeleteIcon: {
    color: "#8C93A3",
    fontSize: 15,
    fontWeight: "700",
  },
  addServerButton: {
    marginTop: 10,
    alignSelf: "center",
    paddingHorizontal: 8,
    paddingVertical: 6,
  },
  addServerButtonText: {
    color: "#B8BDC9",
    fontSize: 16,
    fontWeight: "600",
  },
  statusLeft: {
    flexDirection: "row",
    alignItems: "center",
    gap: 8,
  },
  readyDot: {
    width: 8,
    height: 8,
    borderRadius: 4,
    backgroundColor: "#4CAF50",
  },
  recordingDot: {
    width: 8,
    height: 8,
    borderRadius: 4,
    backgroundColor: "#FF2E3F",
  },
  statusText: {
    fontSize: 14,
    fontWeight: "500",
    color: "#666",
  },
  statusActions: {
    flexDirection: "row",
    alignItems: "center",
    gap: 6,
  },
  clearButton: {
    width: 32,
    height: 32,
    alignItems: "center",
    justifyContent: "center",
    alignSelf: "center",
  },
  clearButtonPressed: {
    opacity: 0.75,
  },
  clearIcon: {
    color: "#A0A0A0",
    fontSize: 20,
    fontWeight: "700",
    textAlign: "center",
    transform: [{ translateY: -0.5 }],
  },
  transcriptionArea: {
    flex: 1,
    marginHorizontal: 6,
    marginTop: 6,
  },
  splitCardStack: {
    flex: 1,
    gap: 8,
  },
  splitCard: {
    flex: 1,
    backgroundColor: "#151515",
    borderRadius: 20,
    borderWidth: 3,
    borderColor: "#282828",
    overflow: "hidden",
    position: "relative",
  },
  replyCard: {
    paddingTop: 16,
  },
  transcriptionPanel: {
    flex: 1,
    position: "relative",
    overflow: "hidden",
  },
  replyCardLabel: {
    color: "#AAB5CC",
    fontSize: 15,
    fontWeight: "600",
  },
  agentStateHeaderRow: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    marginHorizontal: 20,
    marginBottom: 8,
  },
  agentStateTitleWrap: {
    flexDirection: "row",
    alignItems: "center",
    gap: 8,
  },
  agentStateIconWrap: {
    width: 16,
    height: 16,
    alignItems: "center",
    justifyContent: "center",
  },
  agentStateClose: {
    color: "#8D97AB",
    fontSize: 18,
    fontWeight: "700",
    lineHeight: 18,
  },
  replyScroll: {
    flex: 1,
  },
  replyContent: {
    paddingHorizontal: 20,
    paddingBottom: 18,
    flexGrow: 1,
  },
  replyText: {
    fontSize: 22,
    fontWeight: "500",
    lineHeight: 32,
    color: "#F4F7FF",
  },
  transcriptionScroll: {
    flex: 1,
  },
  transcriptionContent: {
    padding: 20,
    paddingTop: 54,
    paddingBottom: 54,
    flexGrow: 1,
  },
  transcriptionTopActions: {
    position: "absolute",
    top: 10,
    left: 10,
    right: 10,
    zIndex: 4,
    flexDirection: "row",
    justifyContent: "space-between",
  },
  modelErrorBadge: {
    alignSelf: "flex-start",
    marginLeft: 14,
    marginTop: 8,
    marginBottom: 2,
    paddingHorizontal: 10,
    paddingVertical: 5,
    borderRadius: 999,
    backgroundColor: "#3A1A1D",
    borderWidth: 1,
    borderColor: "#5D292F",
  },
  modelErrorText: {
    color: "#FFB9BF",
    fontSize: 12,
    fontWeight: "600",
    letterSpacing: 0.1,
  },
  transcriptionText: {
    fontSize: 28,
    fontWeight: "500",
    lineHeight: 38,
    color: "#FFFFFF",
  },
  placeholderText: {
    fontSize: 28,
    fontWeight: "500",
    color: "#333",
  },
  waveformBoxesRow: {
    position: "absolute",
    left: 20,
    right: 20,
    bottom: 14,
    height: WAVEFORM_ROWS * WAVEFORM_CELL_SIZE + (WAVEFORM_ROWS - 1) * WAVEFORM_CELL_GAP,
    pointerEvents: "none",
  },
  waveformGridRow: {
    flexDirection: "row",
    gap: WAVEFORM_CELL_GAP,
    marginBottom: WAVEFORM_CELL_GAP,
  },
  waveformBox: {
    width: WAVEFORM_CELL_SIZE,
    height: WAVEFORM_CELL_SIZE,
    borderRadius: 1,
    backgroundColor: "#78839A",
    borderWidth: 1,
    borderColor: "#818DA6",
  },
  controlsRow: {
    paddingHorizontal: 6,
    paddingBottom: 6,
    paddingTop: 6,
    flexDirection: "row",
    alignItems: "center",
  },
  recordPressable: {
    flex: 1,
  },
  recordButton: {
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#421B17",
    height: CONTROL_HEIGHT,
    borderRadius: 20,
    width: "100%",
    overflow: "hidden",
  },
  recordBusyCenter: {
    alignItems: "center",
    justifyContent: "center",
    width: "100%",
    height: "100%",
  },
  loadFill: {
    position: "absolute",
    left: 0,
    top: 0,
    bottom: 0,
    backgroundColor: "#FF5B47",
  },
  loadFillPending: {
    backgroundColor: "#66423C",
  },
  loadOverlay: {
    ...StyleSheet.absoluteFillObject,
    alignItems: "center",
    justifyContent: "center",
    paddingHorizontal: 18,
  },
  loadText: {
    color: "#FFF6F4",
    fontSize: 14,
    fontWeight: "700",
    letterSpacing: 0.2,
  },
  settingsRoot: {
    flex: 1,
    backgroundColor: "#121212",
    paddingHorizontal: 16,
    paddingTop: 12,
  },
  settingsTop: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    gap: 12,
    marginBottom: 4,
  },
  settingsTitleBlock: {
    flex: 1,
    gap: 4,
  },
  settingsTitle: {
    color: "#F1F1F1",
    fontSize: 20,
    fontWeight: "700",
  },
  settingsSubtitle: {
    color: "#999999",
    fontSize: 13,
    fontWeight: "500",
  },
  settingsClose: {
    color: "#C5C5C5",
    fontSize: 15,
    fontWeight: "700",
  },
  settingsScroll: {
    flex: 1,
  },
  settingsContent: {
    gap: 24,
    paddingBottom: 24,
  },
  settingsSection: {
    gap: 0,
  },
  settingsSectionLabel: {
    color: "#7D7D7D",
    fontSize: 11,
    fontWeight: "700",
    letterSpacing: 1.05,
    marginBottom: 6,
  },
  settingsTextRow: {
    minHeight: 46,
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
    gap: 12,
    borderBottomWidth: 1,
    borderBottomColor: "#242424",
    paddingVertical: 10,
  },
  settingsMutedText: {
    color: "#868686",
    fontSize: 12,
    fontWeight: "500",
  },
  settingsOptionCopy: {
    flex: 1,
    minWidth: 0,
    gap: 2,
  },
  settingsTextRowTitle: {
    color: "#ECECEC",
    fontSize: 14,
    fontWeight: "600",
  },
  settingsTextRowMeta: {
    color: "#8D8D8D",
    fontSize: 12,
    fontWeight: "500",
  },
  settingsTextRowValue: {
    color: "#BDBDBD",
    fontSize: 13,
    fontWeight: "600",
    maxWidth: "55%",
    textAlign: "right",
  },
  settingsTextRowAction: {
    color: "#B8B8B8",
    fontSize: 12,
    fontWeight: "700",
    letterSpacing: 0.2,
  },
  settingsTextRowActionActive: {
    color: "#FFD8D2",
  },
  settingsInlineRow: {
    flexDirection: "row",
    alignItems: "center",
    minHeight: 52,
    borderBottomWidth: 1,
    borderBottomColor: "#242424",
  },
  settingsInlineLabelPressable: {
    flex: 1,
    minWidth: 0,
    paddingVertical: 10,
    paddingRight: 12,
    gap: 2,
  },
  settingsInlinePressableDisabled: {
    opacity: 0.55,
  },
  settingsInlineName: {
    color: "#E7E7E7",
    fontSize: 14,
    fontWeight: "600",
  },
  settingsInlineMeta: {
    color: "#8F8F8F",
    fontSize: 12,
    fontWeight: "500",
  },
  settingsInlineTextActionPressable: {
    marginLeft: 8,
    paddingVertical: 8,
    paddingHorizontal: 2,
    alignItems: "flex-end",
    justifyContent: "center",
  },
  settingsInlineTextAction: {
    color: "#D0D0D0",
    fontSize: 12,
    fontWeight: "700",
    minWidth: 72,
    textAlign: "right",
  },
  settingsInlineTextActionInstalled: {
    color: "#E2B1A8",
  },
  settingsInlineTextActionDownloading: {
    color: "#FFD7CE",
  },
  scanRoot: {
    flex: 1,
    backgroundColor: "#101014",
    paddingHorizontal: 16,
    paddingTop: 12,
    gap: 12,
  },
  scanTop: {
    flexDirection: "row",
    alignItems: "center",
    justifyContent: "space-between",
  },
  scanTitle: {
    color: "#E8EAF0",
    fontSize: 18,
    fontWeight: "700",
  },
  scanClose: {
    color: "#8FA4CC",
    fontSize: 15,
    fontWeight: "600",
  },
  scanCam: {
    flex: 1,
    borderRadius: 18,
    overflow: "hidden",
  },
  scanEmpty: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    paddingHorizontal: 24,
  },
  scanHint: {
    color: "#A6ABBA",
    fontSize: 14,
    textAlign: "center",
  },
  sendSlot: {
    height: CONTROL_HEIGHT,
    overflow: "hidden",
  },
  sendButton: {
    width: "100%",
    height: "100%",
    borderRadius: 20,
    alignItems: "center",
    justifyContent: "center",
    backgroundColor: "#1D6FF4",
    borderWidth: 2,
    borderColor: "#1557C3",
  },
  sendButtonDisabled: {
    opacity: 0.7,
  },
  sendIcon: {
    color: "#FFFFFF",
    fontSize: 34,
    fontWeight: "700",
    lineHeight: 36,
    transform: [{ translateY: -1 }],
  },
  recordBorder: {
    position: "absolute",
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    borderRadius: 20,
  },
  recordButtonDisabled: {
    opacity: 0.4,
  },
  recordDot: {
    width: 38,
    height: 38,
    backgroundColor: "#FF2E3F",
  },
})
