import {
  ArrowLeft,
  Captions,
  Check,
  Clock3,
  Download,
  Film,
  FolderOpen,
  Gauge,
  Languages,
  Layers,
  Link as LinkIcon,
  Loader2,
  Maximize2,
  Moon,
  Move,
  Music2,
  PanelRightOpen,
  Pause,
  Play,
  RefreshCcw,
  Scissors,
  SkipBack,
  SkipForward,
  SlidersHorizontal,
  Sparkles,
  Sun,
  Trash2,
  Type,
  Upload,
  Volume2,
  Wand2,
  Zap,
} from "lucide-react";
import React, { useEffect, useMemo, useRef, useState } from "react";

const API_BASE = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000";

const LANGUAGES = [
  { code: "en", label: "EN", name: "English" },
  { code: "th", label: "TH", name: "ไทย" },
  { code: "ja", label: "JP", name: "日本語" },
  { code: "zh", label: "ZH", name: "中文" },
  { code: "ko", label: "KO", name: "한국어" },
];

const NICHES = [
  "education",
  "gaming",
  "podcast",
  "commentary",
  "cars",
  "beauty",
  "fitness",
  "finance",
  "tech",
  "lifestyle",
  "music",
  "other",
];

const CLIP_STYLES = ["informative", "funny", "dramatic", "educational", "commentary"];
const LANGUAGE_OPTIONS = ["Thai", "English", "Japanese", "Chinese", "Korean", "Auto"];
const PLATFORM_OPTIONS = ["tiktok", "youtube_shorts", "instagram_reels"];
const FONT_OPTIONS = ["Inter", "Noto Sans Thai", "Poppins", "Montserrat", "Arial", "Impact"];
const CUE_DENSITIES = ["word", "short", "medium", "long"];
const CAPTION_ANIMATIONS = ["none", "highlight", "pop", "bounce"];

const defaultCaptionStyle = {
  fontFamily: "Inter",
  fontSize: 38,
  fillColor: "#ffffff",
  strokeColor: "#080b12",
  strokeWidth: 4,
  position: 18,
  x: 50, // horizontal % of preview stage
  y: 82, // vertical % of preview stage (default near bottom)
  cueDensity: "short",
  animation: "highlight",
};

const captionPresets = {
  clean: {
    fontFamily: "Inter",
    fontSize: 34,
    fillColor: "#ffffff",
    strokeColor: "#111827",
    strokeWidth: 3,
    position: 18,
    cueDensity: "medium",
    animation: "none",
  },
  bold: {
    fontFamily: "Impact",
    fontSize: 46,
    fillColor: "#fef08a",
    strokeColor: "#020617",
    strokeWidth: 5,
    position: 20,
    cueDensity: "short",
    animation: "pop",
  },
  karaoke: {
    fontFamily: "Poppins",
    fontSize: 40,
    fillColor: "#ffffff",
    strokeColor: "#020617",
    strokeWidth: 4,
    position: 22,
    cueDensity: "word",
    animation: "highlight",
  },
};

const defaultProfile = {
  niche: "education",
  niche_custom: "",
  channel_description: "",
  clip_style: "informative",
  clip_length_seconds: 60,
  clip_count: 5,
  primary_language: "Thai",
  target_platform: "tiktok",
};

const en = {
  appSubtitle: "Turn long videos into short clips — powered by AMD ROCm",
  idle: "Idle",
  queued: "Queued",
  running: "Processing",
  completed: "Done",
  failed: "Failed",
  demoMode: "Demo mode",
  productionMode: "Live mode",
  theme: "Theme",
  language: "Language",
  startPipeline: "Generate clips",
  channelProfile: "Channel profile",
  channelProfileText: "Tell the AI about your channel so it picks the right moments.",
  videoInput: "Video",
  niche: "Channel niche",
  nicheHelp: "Pick the closest category, or choose Other for something more specific.",
  customNiche: "Your niche",
  customNichePlaceholder: "e.g. Thai AI tutorials for beginners",
  channelDescription: "Channel description",
  channelDescriptionHelp: "Write how you'd describe your channel to a friend. The AI uses this to stay on-brand.",
  channelDescriptionPlaceholder: "e.g. I explain AI tools in simple Thai with a bit of humor.",
  clipStyle: "Clip style",
  clipLength: "Clip length (seconds)",
  clipCount: "Number of clips",
  clipCountHelp: "How many clips the AI should find in this video.",
  primaryLanguage: "Language",
  platform: "Platform",
  youtube: "YouTube",
  upload: "Upload",
  youtubeUrl: "YouTube URL",
  youtubePlaceholder: "https://www.youtube.com/watch?v=...",
  videoFile: "Video file",
  pipeline: "Processing",
  ready: "Ready",
  progressNote: "Updates step by step — clip-by-clip progress shows during rendering.",
  currentStep: "Current step",
  timing: "Timing",
  timing_input: "Download",
  timing_transcription: "Transcription",
  timing_highlight_detection: "Highlights",
  timing_multimodal_analysis: "Visual check",
  timing_clip_generation: "Rendering",
  timing_total: "Total",
  transcript: "Transcript",
  transcriptEmpty: "Transcript will appear once the audio is processed.",
  clips: "Clips",
  noClips: "No clips yet",
  noClipsText: "Hit Generate to let the AI find and cut the best moments.",
  readyClips: "ready",
  score: "Score",
  reason: "Why",
  duration: "Duration",
  start: "Start",
  end: "End",
  subtitles: "Subtitles",
  openEditor: "Edit clip",
  approve: "Approve",
  approved: "Approved",
  regenerate: "Redo",
  delete: "Delete",
  download: "Download",
  backToDashboard: "Back",
  editor: "Clip editor",
  editorText: "Trim timing, edit subtitles, and approve before downloading.",
  preview: "Preview",
  clipRange: "Clip range",
  subtitleCues: "Subtitle cues",
  subtitleCueHelp: "Shorter cues work better on TikTok, Reels, and Shorts.",
  timelineTracks: "Timeline",
  videoTrack: "Video",
  subtitleTrack: "Subtitles",
  audioTrack: "Audio",
  toolSelect: "Select",
  toolTrim: "Trim",
  toolCaptions: "Captions",
  toolStyle: "Style",
  toolExport: "Export",
  captionStyle: "Caption style",
  captionPreset: "Preset",
  presetClean: "Clean",
  presetBold: "Bold",
  presetKaraoke: "Karaoke",
  font: "Font",
  fontSize: "Size",
  fillColor: "Fill color",
  strokeColor: "Outline color",
  strokeWidth: "Outline width",
  captionPosition: "Position",
  captionLength: "Line length",
  animation: "Animation",
  density_word: "Word by word",
  density_short: "Short phrases",
  density_medium: "Medium phrases",
  density_long: "Full lines",
  animation_none: "None",
  animation_highlight: "Highlight",
  animation_pop: "Pop",
  animation_bounce: "Bounce",
  editorTools: "Quick trim",
  rangeStart: "Start",
  rangeEnd: "End",
  trimStartBack: "−0.5s",
  trimStartForward: "+0.5s",
  trimEndBack: "−0.5s",
  trimEndForward: "+0.5s",
  moveClipLeft: "−1s",
  moveClipRight: "+1s",
  setClipLength30: "30s",
  setClipLength60: "60s",
  setClipLength90: "90s",
  inspector: "Details",
  title: "Title",
  status: "Status",
  notApproved: "Not approved",
  model: "Model",
  source: "Source",
  source_upload: "Upload",
  source_youtube: "YouTube",
  source_video: "Video",
  settings: "Settings",
  stepInput: "Download",
  stepTranscription: "Transcribe",
  stepHighlights: "Highlights",
  stepVisual: "Visual AI",
  stepRender: "Render",
  stepFinal: "Done",
  renderProgress: "Rendering {{current}} of {{total}}",
  appCta: "Generate clips",
  niche_education: "Education",
  niche_gaming: "Gaming",
  niche_podcast: "Podcast",
  niche_commentary: "Commentary",
  niche_cars: "Cars",
  niche_beauty: "Beauty",
  niche_fitness: "Fitness",
  niche_finance: "Finance",
  niche_tech: "Tech",
  niche_lifestyle: "Lifestyle",
  niche_music: "Music",
  niche_other: "Other",
  style_informative: "Informative",
  style_funny: "Funny",
  style_dramatic: "Dramatic",
  style_educational: "Educational",
  style_commentary: "Commentary",
  platform_tiktok: "TikTok",
  platform_youtube_shorts: "YouTube Shorts",
  platform_instagram_reels: "Instagram Reels",
  languageOption_Thai: "Thai",
  languageOption_English: "English",
  languageOption_Japanese: "Japanese",
  languageOption_Chinese: "Chinese",
  languageOption_Korean: "Korean",
  languageOption_Auto: "Auto-detect",
  // NLE editor
  mediaBin: "Clips",
  aiAssistant: "AI Assistant",
  aiReason: "AI hasn't explained yet — try regenerating.",
  aiReasonHead: "Why this moment",
  aiVisualHead: "Visual analysis",
  aiTighten: "Tighten to 30s",
  aiEmphasize: "Extend to 60s",
  aiRedoAll: "Regenerate clip",
  aiDeleteClip: "Remove clip",
  aiActionRedoSub: "Let AI pick a new moment",
  aiActionTightenSub: "Best for Reels & Shorts",
  aiActionEmphasizeSub: "Best for TikTok storytelling",
  aiActionDeleteSub: "Drop from this batch",
  dragToTrim: "Drag edges to trim · drag body to move",
  dragCueToRetime: "Drag cue edges or body to retime",
  dragToPosition: "Drag caption to reposition",
  // Subtitle editor
  addCue: "Add subtitle",
  cuePlaceholder: "Type subtitle text...",
  seekToCue: "Jump to this cue",
  aiSubtitleHead: "AI subtitle helpers",
  aiPolish: "Polish all",
  aiTranslate: "Translate",
  aiAutoTime: "Auto-time",
  aiAutoTimeHelp: "Re-time using Whisper word-level timestamps",
  // Clip edit
  clipEdit: "Clip length",
  clipLengthLabel: "Set length",
  clipExtendLabel: "Extend",
  clipSkipLabel: "Cut middle out",
  clipSkipAdd: "Cut",
  clipRebuildBtn: "Rebuild clip",
  from: "from",
  to: "to",
  // GPU status
  gpuActive: "GPU active",
  gpuDemo: "demo",
  gpuPending: "GPU pending",
};

const translations = {
  en,
  th: {
    ...en,
    appSubtitle: "ตัดคลิปสั้นจากวิดีโอยาวอัตโนมัติ บน AMD ROCm",
    idle: "พร้อมใช้",
    queued: "รอคิว",
    running: "กำลังประมวลผล",
    completed: "เสร็จแล้ว",
    failed: "เกิดข้อผิดพลาด",
    demoMode: "โหมดเดโม",
    productionMode: "โหมดจริง",
    theme: "ธีม",
    language: "ภาษา",
    startPipeline: "สร้างคลิป",
    channelProfile: "โปรไฟล์ช่อง",
    channelProfileText: "บอก AI ว่าช่องคุณเป็นแนวไหน เพื่อให้เลือกไฮไลต์ได้ตรงกับสไตล์คุณ",
    videoInput: "วิดีโอ",
    niche: "แนวช่อง",
    nicheHelp: "เลือกหมวดที่ใกล้เคียงที่สุด หากเฉพาะเจาะจงมากให้เลือก อื่น ๆ",
    customNiche: "แนวช่องของคุณ",
    customNichePlaceholder: "เช่น สอน AI ภาษาไทยสำหรับมือใหม่",
    channelDescription: "อธิบายช่อง",
    channelDescriptionHelp: "เขียนแบบเป็นกันเอง AI จะนำไปใช้คัดเลือกช่วงที่เหมาะกับสไตล์ช่อง",
    channelDescriptionPlaceholder: "เช่น ช่องสอนใช้ AI แบบง่าย ๆ เข้าใจได้ทันที มีมุกนิดหน่อย",
    clipStyle: "สไตล์คลิป",
    clipLength: "ความยาวคลิป (วินาที)",
    clipCount: "จำนวนคลิป",
    clipCountHelp: "จำนวนคลิปที่ต้องการให้ AI หาให้จากวิดีโอนี้",
    primaryLanguage: "ภาษา",
    platform: "แพลตฟอร์ม",
    youtube: "YouTube",
    upload: "อัปโหลด",
    youtubeUrl: "ลิงก์ YouTube",
    youtubePlaceholder: "https://www.youtube.com/watch?v=...",
    videoFile: "ไฟล์วิดีโอ",
    pipeline: "การประมวลผล",
    ready: "พร้อม",
    progressNote: "อัปเดตทีละขั้นตอน — ระหว่างเรนเดอร์จะแสดงความคืบหน้าทีละคลิป",
    currentStep: "ขั้นตอนปัจจุบัน",
    timing: "เวลาที่ใช้",
    timing_input: "ดาวน์โหลด",
    timing_transcription: "ถอดเสียง",
    timing_highlight_detection: "ไฮไลต์",
    timing_multimodal_analysis: "วิเคราะห์ภาพ",
    timing_clip_generation: "เรนเดอร์",
    timing_total: "รวม",
    transcript: "คำบรรยาย",
    transcriptEmpty: "คำบรรยายจะแสดงเมื่อถอดเสียงเสร็จ",
    clips: "คลิป",
    noClips: "ยังไม่มีคลิป",
    noClipsText: "กด สร้างคลิป เพื่อให้ AI ค้นหาและตัดช่วงที่ดีที่สุด",
    readyClips: "คลิปพร้อมแล้ว",
    score: "คะแนน",
    reason: "เหตุผล",
    duration: "ความยาว",
    start: "เริ่ม",
    end: "จบ",
    subtitles: "ซับไตเติล",
    openEditor: "แก้ไขคลิป",
    approve: "อนุมัติ",
    approved: "อนุมัติแล้ว",
    regenerate: "ทำใหม่",
    delete: "ลบ",
    download: "ดาวน์โหลด",
    backToDashboard: "กลับ",
    editor: "ตัดต่อคลิป",
    editorText: "ปรับช่วงเวลา แก้ซับไตเติล และอนุมัติก่อนดาวน์โหลด",
    preview: "ตัวอย่าง",
    clipRange: "ช่วงเวลาคลิป",
    subtitleCues: "ซับไตเติล",
    subtitleCueHelp: "ซับสั้น ๆ อ่านง่ายกว่าบน TikTok, Reels และ Shorts",
    timelineTracks: "ไทม์ไลน์",
    videoTrack: "วิดีโอ",
    subtitleTrack: "ซับไตเติล",
    audioTrack: "เสียง",
    toolSelect: "เลือก",
    toolTrim: "ตัด",
    toolCaptions: "ซับ",
    toolStyle: "สไตล์",
    toolExport: "ส่งออก",
    captionStyle: "สไตล์ซับไตเติล",
    captionPreset: "พรีเซ็ต",
    presetClean: "เรียบง่าย",
    presetBold: "โดดเด่น",
    presetKaraoke: "คาราโอเกะ",
    font: "ฟอนต์",
    fontSize: "ขนาดตัวอักษร",
    fillColor: "สีตัวอักษร",
    strokeColor: "สีขอบ",
    strokeWidth: "ความหนาขอบ",
    captionPosition: "ตำแหน่ง",
    captionLength: "ความยาวต่อบรรทัด",
    animation: "อนิเมชัน",
    density_word: "ทีละคำ",
    density_short: "วลีสั้น",
    density_medium: "วลีกลาง",
    density_long: "บรรทัดยาว",
    animation_none: "ไม่มี",
    animation_highlight: "ไฮไลต์",
    animation_pop: "เด้งเข้า",
    animation_bounce: "เด้งตามจังหวะ",
    editorTools: "ปรับเวลาเร็ว",
    rangeStart: "จุดเริ่มต้น",
    rangeEnd: "จุดสิ้นสุด",
    trimStartBack: "−0.5 วิ",
    trimStartForward: "+0.5 วิ",
    trimEndBack: "−0.5 วิ",
    trimEndForward: "+0.5 วิ",
    moveClipLeft: "−1 วิ",
    moveClipRight: "+1 วิ",
    setClipLength30: "30 วิ",
    setClipLength60: "60 วิ",
    setClipLength90: "90 วิ",
    inspector: "รายละเอียด",
    title: "ชื่อคลิป",
    status: "สถานะ",
    notApproved: "ยังไม่อนุมัติ",
    model: "โมเดล AI",
    source: "แหล่งที่มา",
    source_upload: "อัปโหลด",
    source_youtube: "YouTube",
    source_video: "วิดีโอ",
    settings: "ตั้งค่า",
    stepInput: "ดาวน์โหลด",
    stepTranscription: "ถอดเสียง",
    stepHighlights: "ไฮไลต์",
    stepVisual: "AI ภาพ",
    stepRender: "เรนเดอร์",
    stepFinal: "เสร็จ",
    renderProgress: "กำลังเรนเดอร์คลิป {{current}} จาก {{total}}",
    appCta: "สร้างคลิป",
    niche_education: "การศึกษา",
    niche_gaming: "เกม",
    niche_podcast: "พอดแคสต์",
    niche_commentary: "คอมเมนทารี",
    niche_cars: "รถยนต์",
    niche_beauty: "บิวตี้",
    niche_fitness: "ฟิตเนส",
    niche_finance: "การเงิน",
    niche_tech: "เทคโนโลยี",
    niche_lifestyle: "ไลฟ์สไตล์",
    niche_music: "ดนตรี",
    niche_other: "อื่น ๆ",
    style_informative: "ให้ข้อมูล",
    style_funny: "ตลก",
    style_dramatic: "ดราม่า",
    style_educational: "สอน",
    style_commentary: "วิเคราะห์",
    platform_tiktok: "TikTok",
    platform_youtube_shorts: "YouTube Shorts",
    platform_instagram_reels: "Instagram Reels",
    languageOption_Thai: "ไทย",
    languageOption_English: "อังกฤษ",
    languageOption_Japanese: "ญี่ปุ่น",
    languageOption_Chinese: "จีน",
    languageOption_Korean: "เกาหลี",
    languageOption_Auto: "ตรวจจับอัตโนมัติ",
    // NLE editor
    mediaBin: "คลิปทั้งหมด",
    aiAssistant: "ผู้ช่วย AI",
    aiReason: "AI ยังไม่ได้อธิบาย ลองสร้างใหม่ดูสิ",
    aiReasonHead: "เหตุผลที่เลือกช่วงนี้",
    aiVisualHead: "วิเคราะห์ภาพ",
    aiTighten: "ตัดเหลือ 30 วิ",
    aiEmphasize: "ขยายเป็น 60 วิ",
    aiRedoAll: "สร้างคลิปนี้ใหม่",
    aiDeleteClip: "ลบคลิปนี้",
    aiActionRedoSub: "ให้ AI หาช่วงใหม่",
    aiActionTightenSub: "เหมาะกับ Reels และ Shorts",
    aiActionEmphasizeSub: "เหมาะกับ TikTok แบบเล่าเรื่อง",
    aiActionDeleteSub: "เอาออกจากชุดนี้",
    dragToTrim: "ลากขอบเพื่อ trim · ลากกลางเพื่อย้าย",
    dragCueToRetime: "ลากขอบหรือกลางซับเพื่อปรับเวลา",
    dragToPosition: "ลากข้อความเพื่อย้ายตำแหน่ง",
    addCue: "เพิ่มซับ",
    cuePlaceholder: "พิมพ์ข้อความซับ...",
    seekToCue: "ข้ามไปที่ซับนี้",
    aiSubtitleHead: "ผู้ช่วย AI สำหรับซับ",
    aiPolish: "เกลาคำพูด",
    aiTranslate: "แปล",
    aiAutoTime: "ตั้งเวลาอัตโนมัติ",
    aiAutoTimeHelp: "ปรับเวลาซับจาก Whisper รายคำ",
    clipEdit: "ปรับความยาวคลิป",
    clipLengthLabel: "ตั้งความยาว",
    clipExtendLabel: "เพิ่มเวลา",
    clipSkipLabel: "ตัดช่วงตรงกลางออก",
    clipSkipAdd: "ตัดออก",
    clipRebuildBtn: "สร้างคลิปใหม่",
    from: "จาก",
    to: "ถึง",
    gpuActive: "GPU ทำงาน",
    gpuDemo: "demo",
    gpuPending: "รอ GPU",
  },
  ja: {
    ...en,
    appSubtitle: "長尺動画を短編クリップに ― AMD ROCm 搭載",
    idle: "待機中",
    queued: "順番待ち",
    running: "処理中",
    completed: "完了",
    failed: "失敗",
    demoMode: "デモモード",
    productionMode: "本番モード",
    theme: "テーマ",
    language: "言語",
    startPipeline: "クリップを生成",
    channelProfile: "チャンネルプロフィール",
    channelProfileText: "AIが最適な瞬間を選べるよう、チャンネルの情報を入力してください。",
    videoInput: "動画",
    niche: "ジャンル",
    nicheHelp: "近いカテゴリを選んでください。当てはまらない場合は「その他」を選択します。",
    customNiche: "ジャンルを入力",
    customNichePlaceholder: "例: 初心者向けタイ語AIチュートリアル",
    channelDescription: "チャンネル説明",
    channelDescriptionHelp: "友人に紹介するつもりで書いてください。AIがブランドの方向性を保つために使用します。",
    channelDescriptionPlaceholder:
      "例: タイ語でAIツールをわかりやすく、少しユーモアを交えて解説しています。",
    clipStyle: "クリップのスタイル",
    clipLength: "クリップの長さ（秒）",
    clipCount: "クリップ数",
    clipCountHelp: "この動画からAIに見つけてほしいクリップの数です。",
    primaryLanguage: "言語",
    platform: "配信プラットフォーム",
    youtube: "YouTube",
    upload: "アップロード",
    youtubeUrl: "YouTube URL",
    youtubePlaceholder: "https://www.youtube.com/watch?v=...",
    videoFile: "動画ファイル",
    pipeline: "処理状況",
    ready: "準備完了",
    progressNote: "工程ごとに更新され、レンダリング中はクリップ単位で進捗を表示します。",
    currentStep: "現在の工程",
    timing: "処理時間",
    timing_input: "ダウンロード",
    timing_transcription: "文字起こし",
    timing_highlight_detection: "ハイライト検出",
    timing_multimodal_analysis: "映像分析",
    timing_clip_generation: "クリップ生成",
    timing_total: "合計",
    transcript: "文字起こし",
    transcriptEmpty: "文字起こしが完了するとここに表示されます。",
    clips: "クリップ",
    noClips: "クリップがまだありません",
    noClipsText: "処理を開始すると、プロフィールに合わせた候補クリップが生成されます。",
    readyClips: "件のクリップ",
    score: "スコア",
    reason: "理由",
    duration: "長さ",
    start: "開始",
    end: "終了",
    openEditor: "エディターを開く",
    editor: "クリップエディター",
    editorText: "このクリップのタイミング、字幕、最終承認を調整します。",
    preview: "プレビュー",
    clipRange: "クリップ範囲",
    subtitles: "字幕",
    subtitleCues: "字幕ブロック",
    subtitleCueHelp: "短い字幕ブロックのほうがTikTok、Reels、Shortsで読みやすくなります。",
    timelineTracks: "タイムライン",
    videoTrack: "映像",
    subtitleTrack: "字幕",
    audioTrack: "音声",
    toolSelect: "選択",
    toolTrim: "トリム",
    toolCaptions: "字幕",
    toolStyle: "スタイル",
    toolExport: "書き出し",
    captionStyle: "字幕スタイル",
    captionPreset: "プリセット",
    presetClean: "シンプル",
    presetBold: "太字",
    presetKaraoke: "カラオケ",
    font: "フォント",
    fontSize: "サイズ",
    fillColor: "文字色",
    strokeColor: "縁の色",
    strokeWidth: "縁の太さ",
    captionPosition: "字幕の位置",
    captionLength: "字幕の長さ",
    animation: "アニメーション",
    density_word: "単語単位",
    density_short: "短めの行",
    density_medium: "標準",
    density_long: "長めの行",
    animation_none: "なし",
    animation_highlight: "ハイライト",
    animation_pop: "ポップ",
    animation_bounce: "バウンス",
    editorTools: "編集ツール",
    rangeStart: "開始位置",
    rangeEnd: "終了位置",
    trimStartBack: "開始 −0.5秒",
    trimStartForward: "開始 +0.5秒",
    trimEndBack: "終了 −0.5秒",
    trimEndForward: "終了 +0.5秒",
    moveClipLeft: "クリップ −1秒",
    moveClipRight: "クリップ +1秒",
    setClipLength30: "30秒に調整",
    setClipLength60: "60秒に調整",
    setClipLength90: "90秒に調整",
    inspector: "クリップ情報",
    title: "タイトル",
    status: "ステータス",
    notApproved: "未承認",
    model: "モデル",
    source: "ソース",
    source_upload: "アップロード",
    source_youtube: "YouTube",
    source_video: "動画",
    settings: "設定",
    stepInput: "入力",
    stepTranscription: "文字起こし",
    stepHighlights: "ハイライト",
    stepVisual: "映像分析",
    stepRender: "レンダリング",
    stepFinal: "仕上げ",
    renderProgress: "クリップ {{current}}/{{total}} をレンダリング中",
    appCta: "クリップを生成",
    approve: "承認",
    approved: "承認済み",
    regenerate: "再生成",
    delete: "削除",
    download: "ダウンロード",
    backToDashboard: "ホームに戻る",
    niche_education: "教育",
    niche_gaming: "ゲーム",
    niche_podcast: "ポッドキャスト",
    niche_commentary: "解説",
    niche_cars: "車",
    niche_beauty: "美容",
    niche_fitness: "フィットネス",
    niche_finance: "ファイナンス",
    niche_tech: "テクノロジー",
    niche_lifestyle: "ライフスタイル",
    niche_music: "音楽",
    niche_other: "その他",
    style_informative: "情報系",
    style_funny: "コメディ",
    style_dramatic: "ドラマチック",
    style_educational: "教育系",
    style_commentary: "解説",
    platform_tiktok: "TikTok",
    platform_youtube_shorts: "YouTube Shorts",
    platform_instagram_reels: "Instagram Reels",
    languageOption_Thai: "タイ語",
    languageOption_English: "英語",
    languageOption_Japanese: "日本語",
    languageOption_Chinese: "中国語",
    languageOption_Korean: "韓国語",
    languageOption_Auto: "自動検出",
    // NLE editor
    mediaBin: "クリップ一覧",
    aiAssistant: "AIアシスタント",
    aiReason: "AIの説明はまだありません。再生成してみてください。",
    aiReasonHead: "この場面を選んだ理由",
    aiVisualHead: "映像分析",
    aiTighten: "30秒に短縮",
    aiEmphasize: "60秒に延長",
    aiRedoAll: "このクリップを再生成",
    aiDeleteClip: "クリップを削除",
    aiActionRedoSub: "AIに別の場面を選ばせる",
    aiActionTightenSub: "Reels・Shortsに最適",
    aiActionEmphasizeSub: "TikTokのストーリーテリングに最適",
    aiActionDeleteSub: "このバッチから外す",
    dragToTrim: "端をドラッグでトリム · 中央をドラッグで移動",
    dragCueToRetime: "字幕の端や本体をドラッグしてタイミング調整",
    dragToPosition: "字幕をドラッグして移動",
    addCue: "字幕を追加",
    cuePlaceholder: "字幕テキストを入力...",
    seekToCue: "この字幕にジャンプ",
    aiSubtitleHead: "AI字幕アシスタント",
    aiPolish: "字幕を整える",
    aiTranslate: "翻訳",
    aiAutoTime: "自動タイミング",
    aiAutoTimeHelp: "Whisperの単語タイムスタンプで字幕を再調整",
    clipEdit: "クリップ長さ",
    clipLengthLabel: "長さを設定",
    clipExtendLabel: "延長",
    clipSkipLabel: "中央を切り取る",
    clipSkipAdd: "切り取り",
    clipRebuildBtn: "クリップを再生成",
    from: "から",
    to: "まで",
    gpuActive: "GPU動作中",
    gpuDemo: "デモ",
    gpuPending: "GPU待機中",
  },
  zh: {
    ...en,
    appSubtitle: "把长视频变成短视频 — 由 AMD ROCm 驱动",
    idle: "待机",
    queued: "排队中",
    running: "处理中",
    completed: "完成",
    failed: "失败",
    demoMode: "演示模式",
    productionMode: "正式模式",
    theme: "主题",
    language: "语言",
    startPipeline: "生成短片",
    channelProfile: "频道资料",
    channelProfileText: "把你的频道情况告诉 AI，它才会挑出最合适的精彩瞬间。",
    videoInput: "视频",
    niche: "频道类型",
    nicheHelp: "选最接近的分类。如果不在列表中，请选择「其他」。",
    customNiche: "自定义类型",
    customNichePlaceholder: "例如：面向初学者的泰语 AI 教程",
    channelDescription: "频道介绍",
    channelDescriptionHelp: "像介绍给朋友一样写就好。AI 会用它判断内容是否符合频道风格。",
    channelDescriptionPlaceholder: "例如：我用泰语讲解 AI 工具，例子简单，也带一点幽默。",
    clipStyle: "短片风格",
    clipLength: "短片时长（秒）",
    clipCount: "短片数量",
    clipCountHelp: "希望 AI 从这段视频中生成多少条候选短片。",
    primaryLanguage: "语言",
    platform: "发布平台",
    youtube: "YouTube",
    upload: "上传",
    youtubeUrl: "YouTube 链接",
    youtubePlaceholder: "https://www.youtube.com/watch?v=...",
    videoFile: "视频文件",
    pipeline: "处理进度",
    ready: "准备就绪",
    progressNote: "每个步骤都会更新进度，渲染阶段会显示每条短片的状态。",
    currentStep: "当前步骤",
    timing: "耗时",
    timing_input: "下载",
    timing_transcription: "语音转写",
    timing_highlight_detection: "亮点识别",
    timing_multimodal_analysis: "画面分析",
    timing_clip_generation: "短片生成",
    timing_total: "总计",
    transcript: "字幕原文",
    transcriptEmpty: "转写完成后会显示在这里。",
    clips: "短片",
    noClips: "还没有短片",
    noClipsText: "启动流程后，AI 会根据频道资料生成候选短片。",
    readyClips: "条短片",
    score: "评分",
    reason: "原因",
    duration: "时长",
    start: "起点",
    end: "终点",
    openEditor: "打开编辑器",
    editor: "短片编辑器",
    editorText: "调整这条短片的时间、字幕，并完成最终确认。",
    preview: "预览",
    clipRange: "片段范围",
    subtitles: "字幕",
    subtitleCues: "字幕段落",
    subtitleCueHelp: "短字幕段落更适合 TikTok、Reels 和 Shorts。",
    timelineTracks: "时间线",
    videoTrack: "画面",
    subtitleTrack: "字幕",
    audioTrack: "音频",
    toolSelect: "选择",
    toolTrim: "裁剪",
    toolCaptions: "字幕",
    toolStyle: "样式",
    toolExport: "导出",
    captionStyle: "字幕样式",
    captionPreset: "预设",
    presetClean: "简洁",
    presetBold: "粗体",
    presetKaraoke: "卡拉 OK",
    font: "字体",
    fontSize: "字号",
    fillColor: "填充色",
    strokeColor: "描边色",
    strokeWidth: "描边宽度",
    captionPosition: "字幕位置",
    captionLength: "字幕长度",
    animation: "动效",
    density_word: "逐词",
    density_short: "短句",
    density_medium: "中等长度",
    density_long: "长句",
    animation_none: "无",
    animation_highlight: "高亮",
    animation_pop: "弹出",
    animation_bounce: "弹跳",
    editorTools: "编辑工具",
    rangeStart: "起点位置",
    rangeEnd: "终点位置",
    trimStartBack: "起点 −0.5 秒",
    trimStartForward: "起点 +0.5 秒",
    trimEndBack: "终点 −0.5 秒",
    trimEndForward: "终点 +0.5 秒",
    moveClipLeft: "整段 −1 秒",
    moveClipRight: "整段 +1 秒",
    setClipLength30: "调整为 30 秒",
    setClipLength60: "调整为 60 秒",
    setClipLength90: "调整为 90 秒",
    inspector: "属性",
    title: "标题",
    status: "状态",
    notApproved: "未确认",
    model: "模型",
    source: "来源",
    source_upload: "上传",
    source_youtube: "YouTube",
    source_video: "视频",
    settings: "设置",
    stepInput: "导入",
    stepTranscription: "语音转写",
    stepHighlights: "亮点识别",
    stepVisual: "画面分析",
    stepRender: "渲染",
    stepFinal: "收尾",
    renderProgress: "正在渲染第 {{current}}/{{total}} 条短片",
    appCta: "生成短片",
    approve: "确认",
    approved: "已确认",
    regenerate: "重新生成",
    delete: "删除",
    download: "下载",
    backToDashboard: "返回主界面",
    niche_education: "教育",
    niche_gaming: "游戏",
    niche_podcast: "播客",
    niche_commentary: "解说",
    niche_cars: "汽车",
    niche_beauty: "美妆",
    niche_fitness: "健身",
    niche_finance: "财经",
    niche_tech: "科技",
    niche_lifestyle: "生活",
    niche_music: "音乐",
    niche_other: "其他",
    style_informative: "知识型",
    style_funny: "幽默",
    style_dramatic: "戏剧化",
    style_educational: "教学型",
    style_commentary: "评论型",
    platform_tiktok: "TikTok",
    platform_youtube_shorts: "YouTube Shorts",
    platform_instagram_reels: "Instagram Reels",
    languageOption_Thai: "泰语",
    languageOption_English: "英语",
    languageOption_Japanese: "日语",
    languageOption_Chinese: "中文",
    languageOption_Korean: "韩语",
    languageOption_Auto: "自动检测",
    // NLE editor
    mediaBin: "片段列表",
    aiAssistant: "AI 助手",
    aiReason: "AI 还没解释，试试重新生成。",
    aiReasonHead: "为什么选这一段",
    aiVisualHead: "画面分析",
    aiTighten: "压缩到 30 秒",
    aiEmphasize: "延长到 60 秒",
    aiRedoAll: "重新生成此片段",
    aiDeleteClip: "删除此片段",
    aiActionRedoSub: "让 AI 找新的精彩瞬间",
    aiActionTightenSub: "适合 Reels 和 Shorts",
    aiActionEmphasizeSub: "适合 TikTok 故事化内容",
    aiActionDeleteSub: "从本批次移除",
    dragToTrim: "拖动边缘修剪 · 拖动中央移动",
    dragCueToRetime: "拖动字幕边缘或中央调整时间",
    dragToPosition: "拖动字幕移动位置",
    addCue: "添加字幕",
    cuePlaceholder: "输入字幕文字...",
    seekToCue: "跳到该字幕",
    aiSubtitleHead: "AI 字幕助手",
    aiPolish: "润色字幕",
    aiTranslate: "翻译",
    aiAutoTime: "自动对时",
    aiAutoTimeHelp: "用 Whisper 单词时间戳重新对齐",
    clipEdit: "片段长度",
    clipLengthLabel: "设置长度",
    clipExtendLabel: "延长",
    clipSkipLabel: "切掉中段",
    clipSkipAdd: "切掉",
    clipRebuildBtn: "重建片段",
    from: "从",
    to: "到",
    gpuActive: "GPU 活动",
    gpuDemo: "演示",
    gpuPending: "等待 GPU",
  },
  ko: {
    ...en,
    appSubtitle: "긴 영상을 짧은 클립으로 ― AMD ROCm 기반",
    idle: "대기 중",
    queued: "대기열",
    running: "처리 중",
    completed: "완료",
    failed: "실패",
    demoMode: "데모 모드",
    productionMode: "정식 모드",
    theme: "테마",
    language: "언어",
    startPipeline: "클립 만들기",
    channelProfile: "채널 프로필",
    channelProfileText: "AI가 적절한 순간을 고를 수 있도록 채널 정보를 입력해 주세요.",
    videoInput: "영상",
    niche: "채널 분야",
    nicheHelp: "가장 가까운 카테고리를 선택하세요. 해당 항목이 없다면 기타를 선택합니다.",
    customNiche: "분야 직접 입력",
    customNichePlaceholder: "예: 초보자를 위한 태국어 AI 튜토리얼",
    channelDescription: "채널 소개",
    channelDescriptionHelp: "친구에게 소개하듯 자연스럽게 작성하세요. AI가 채널 톤을 유지하는 데 사용합니다.",
    channelDescriptionPlaceholder:
      "예: AI 도구를 태국어로 쉽게 설명하고, 약간의 유머도 곁들입니다.",
    clipStyle: "클립 스타일",
    clipLength: "클립 길이(초)",
    clipCount: "클립 개수",
    clipCountHelp: "이 영상에서 AI가 찾아낼 클립 개수입니다.",
    primaryLanguage: "언어",
    platform: "플랫폼",
    youtube: "YouTube",
    upload: "업로드",
    youtubeUrl: "YouTube URL",
    youtubePlaceholder: "https://www.youtube.com/watch?v=...",
    videoFile: "영상 파일",
    pipeline: "처리 진행",
    ready: "준비 완료",
    progressNote: "단계별로 진행률이 갱신되며, 렌더링 중에는 클립별 상태가 표시됩니다.",
    currentStep: "현재 단계",
    timing: "소요 시간",
    timing_input: "다운로드",
    timing_transcription: "음성 인식",
    timing_highlight_detection: "하이라이트 감지",
    timing_multimodal_analysis: "영상 분석",
    timing_clip_generation: "클립 생성",
    timing_total: "총 시간",
    transcript: "스크립트",
    transcriptEmpty: "음성 인식이 끝나면 여기에 표시됩니다.",
    clips: "클립",
    noClips: "아직 클립이 없습니다",
    noClipsText: "처리를 시작하면 채널 프로필에 맞춰 후보 클립이 생성됩니다.",
    readyClips: "개의 클립",
    score: "점수",
    reason: "선정 이유",
    duration: "길이",
    start: "시작",
    end: "끝",
    openEditor: "에디터 열기",
    editor: "클립 에디터",
    editorText: "이 클립의 타이밍, 자막, 최종 확인 상태를 조정합니다.",
    preview: "미리보기",
    clipRange: "클립 범위",
    subtitles: "자막",
    subtitleCues: "자막 단락",
    subtitleCueHelp: "짧은 자막 단락이 TikTok, Reels, Shorts에서 더 읽기 좋습니다.",
    timelineTracks: "타임라인",
    videoTrack: "영상",
    subtitleTrack: "자막",
    audioTrack: "오디오",
    toolSelect: "선택",
    toolTrim: "자르기",
    toolCaptions: "자막",
    toolStyle: "스타일",
    toolExport: "내보내기",
    captionStyle: "자막 스타일",
    captionPreset: "프리셋",
    presetClean: "심플",
    presetBold: "굵은 글씨",
    presetKaraoke: "노래방",
    font: "글꼴",
    fontSize: "글자 크기",
    fillColor: "글자색",
    strokeColor: "외곽선 색",
    strokeWidth: "외곽선 두께",
    captionPosition: "자막 위치",
    captionLength: "자막 길이",
    animation: "애니메이션",
    density_word: "단어 단위",
    density_short: "짧은 줄",
    density_medium: "보통",
    density_long: "긴 줄",
    animation_none: "없음",
    animation_highlight: "하이라이트",
    animation_pop: "팝",
    animation_bounce: "바운스",
    editorTools: "편집 도구",
    rangeStart: "시작 위치",
    rangeEnd: "끝 위치",
    trimStartBack: "시작 −0.5초",
    trimStartForward: "시작 +0.5초",
    trimEndBack: "끝 −0.5초",
    trimEndForward: "끝 +0.5초",
    moveClipLeft: "클립 −1초",
    moveClipRight: "클립 +1초",
    setClipLength30: "30초로 맞추기",
    setClipLength60: "60초로 맞추기",
    setClipLength90: "90초로 맞추기",
    inspector: "상세 정보",
    title: "제목",
    status: "상태",
    notApproved: "미확정",
    model: "모델",
    source: "소스",
    source_upload: "업로드",
    source_youtube: "YouTube",
    source_video: "영상",
    settings: "설정",
    stepInput: "불러오기",
    stepTranscription: "음성 인식",
    stepHighlights: "하이라이트",
    stepVisual: "영상 분석",
    stepRender: "렌더링",
    stepFinal: "마무리",
    renderProgress: "클립 {{current}}/{{total}} 렌더링 중",
    appCta: "클립 만들기",
    approve: "확정",
    approved: "확정됨",
    regenerate: "다시 만들기",
    delete: "삭제",
    download: "다운로드",
    backToDashboard: "홈으로",
    niche_education: "교육",
    niche_gaming: "게임",
    niche_podcast: "팟캐스트",
    niche_commentary: "해설",
    niche_cars: "자동차",
    niche_beauty: "뷰티",
    niche_fitness: "피트니스",
    niche_finance: "금융",
    niche_tech: "테크",
    niche_lifestyle: "라이프스타일",
    niche_music: "음악",
    niche_other: "기타",
    style_informative: "정보형",
    style_funny: "유머",
    style_dramatic: "감동적",
    style_educational: "교육형",
    style_commentary: "해설형",
    platform_tiktok: "TikTok",
    platform_youtube_shorts: "YouTube Shorts",
    platform_instagram_reels: "Instagram Reels",
    languageOption_Thai: "태국어",
    languageOption_English: "영어",
    languageOption_Japanese: "일본어",
    languageOption_Chinese: "중국어",
    languageOption_Korean: "한국어",
    languageOption_Auto: "자동 감지",
    // NLE editor
    mediaBin: "클립 목록",
    aiAssistant: "AI 어시스턴트",
    aiReason: "AI가 아직 설명하지 않았습니다. 다시 만들어 보세요.",
    aiReasonHead: "이 장면을 고른 이유",
    aiVisualHead: "영상 분석",
    aiTighten: "30초로 줄이기",
    aiEmphasize: "60초로 늘리기",
    aiRedoAll: "이 클립 다시 만들기",
    aiDeleteClip: "클립 삭제",
    aiActionRedoSub: "AI가 다른 장면을 찾도록",
    aiActionTightenSub: "Reels와 Shorts에 적합",
    aiActionEmphasizeSub: "TikTok 스토리텔링에 적합",
    aiActionDeleteSub: "이번 배치에서 제외",
    dragToTrim: "끝을 드래그해 트림 · 가운데를 드래그해 이동",
    dragCueToRetime: "자막 끝이나 중앙을 드래그해 타이밍 조정",
    dragToPosition: "자막을 드래그해 이동",
    addCue: "자막 추가",
    cuePlaceholder: "자막 텍스트 입력...",
    seekToCue: "이 자막으로 이동",
    aiSubtitleHead: "AI 자막 도우미",
    aiPolish: "자막 다듬기",
    aiTranslate: "번역",
    aiAutoTime: "자동 타이밍",
    aiAutoTimeHelp: "Whisper 단어 타임스탬프로 재조정",
    clipEdit: "클립 길이",
    clipLengthLabel: "길이 설정",
    clipExtendLabel: "연장",
    clipSkipLabel: "중간 잘라내기",
    clipSkipAdd: "잘라내기",
    clipRebuildBtn: "클립 다시 만들기",
    from: "부터",
    to: "까지",
    gpuActive: "GPU 활성",
    gpuDemo: "데모",
    gpuPending: "GPU 대기",
  },
};

// ============================================================
// App root
// ============================================================
function App() {
  const [profile, setProfile] = useState(defaultProfile);
  const [sourceMode, setSourceMode] = useState("youtube");
  const [youtubeUrl, setYoutubeUrl] = useState("");
  const [file, setFile] = useState(null);
  const [job, setJob] = useState(null);
  const [health, setHealth] = useState(null);
  const [error, setError] = useState("");
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Explicit view state — fixes the editor navigation bug
  const [view, setView] = useState("dashboard"); // 'dashboard' | 'editor'
  const [editorClipId, setEditorClipId] = useState(null);

  const [captionStyles, setCaptionStyles] = useState(() => {
    try {
      return JSON.parse(localStorage.getItem("elevenclip.captionStyles") || "{}");
    } catch {
      return {};
    }
  });
  const [language, setLanguage] = useState(
    () => localStorage.getItem("elevenclip.language") || "en"
  );
  const [theme, setTheme] = useState(() => {
    const saved = localStorage.getItem("elevenclip.theme");
    if (saved) return saved;
    return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
  });

  const t = (key, params) => {
    let value = translations[language]?.[key] ?? translations.en[key] ?? key;
    if (params) {
      Object.entries(params).forEach(([name, replacement]) => {
        value = value.replaceAll(`{{${name}}}`, String(replacement));
      });
    }
    return value;
  };

  const activeClips = useMemo(
    () => (job?.clips || []).filter((clip) => !clip.deleted),
    [job?.clips]
  );

  // Derive the editor clip from view state — no useEffect reset needed
  const editorClip =
    view === "editor" && editorClipId
      ? activeClips.find((clip) => clip.id === editorClipId) || null
      : null;

  const editorCaptionStyle = editorClip
    ? { ...defaultCaptionStyle, ...(captionStyles[editorClip.id] || {}) }
    : defaultCaptionStyle;

  // Navigation helpers
  function openEditor(clip) {
    setEditorClipId(clip.id);
    setView("editor");
  }
  function closeEditor() {
    setView("dashboard");
  }

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    localStorage.setItem("elevenclip.theme", theme);
  }, [theme]);

  useEffect(() => {
    localStorage.setItem("elevenclip.language", language);
  }, [language]);

  useEffect(() => {
    localStorage.setItem("elevenclip.captionStyles", JSON.stringify(captionStyles));
  }, [captionStyles]);

  useEffect(() => {
    fetchJson("/health").then(setHealth).catch(() => setHealth(null));
  }, []);

  useEffect(() => {
    if (!job || !["queued", "running"].includes(job.status)) return;
    const timer = window.setInterval(async () => {
      const next = await fetchJson(`/api/jobs/${job.id}`);
      setJob(next);
    }, 1200);
    return () => window.clearInterval(timer);
  }, [job]);

  async function submitJob(event) {
    event.preventDefault();
    setError("");
    setIsSubmitting(true);
    setView("dashboard");
    setEditorClipId(null);
    try {
      if (sourceMode === "youtube") {
        const next = await fetchJson("/api/jobs/youtube", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ youtube_url: youtubeUrl, profile }),
        });
        setJob(next);
      } else {
        const data = new FormData();
        data.append("profile_json", JSON.stringify(profile));
        data.append("file", file);
        const next = await fetchJson("/api/jobs/upload", { method: "POST", body: data });
        setJob(next);
      }
    } catch (exc) {
      setError(exc.message);
    } finally {
      setIsSubmitting(false);
    }
  }

  async function patchClip(clipId, patch) {
    setJob((current) => ({
      ...current,
      clips: current.clips.map((clip) => (clip.id === clipId ? { ...clip, ...patch } : clip)),
    }));
    try {
      const nextClip = await fetchJson(`/api/jobs/${job.id}/clips/${clipId}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(patch),
      });
      setJob((current) => ({
        ...current,
        clips: current.clips.map((clip) => (clip.id === clipId ? nextClip : clip)),
      }));
    } catch (exc) {
      setError(exc.message);
    }
  }

  async function regenerateClip(clip) {
    const nextClip = await fetchJson(`/api/jobs/${job.id}/clips/${clip.id}/regenerate`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        clip_style: profile.clip_style,
        clip_length_seconds: Number(profile.clip_length_seconds),
        subtitle_text: clip.subtitle_text,
      }),
    });
    setJob((current) => ({
      ...current,
      clips: current.clips.map((item) => (item.id === clip.id ? nextClip : item)),
    }));
  }

  // ─── AI subtitle actions ────────────────────────────────────
  async function callAiSubtitle(endpoint, clip, body) {
    try {
      const nextClip = await fetchJson(
        `/api/jobs/${job.id}/clips/${clip.id}/subtitle/${endpoint}`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body || {}),
        }
      );
      setJob((current) => ({
        ...current,
        clips: current.clips.map((item) => (item.id === clip.id ? nextClip : item)),
      }));
    } catch (exc) {
      setError(exc.message);
    }
  }
  function polishSubtitles(clip) {
    return callAiSubtitle("polish", clip, { style: profile.clip_style });
  }
  function translateSubtitles(clip, targetLanguage) {
    return callAiSubtitle("translate", clip, { target_language: targetLanguage });
  }
  function autoTimeSubtitles(clip) {
    return callAiSubtitle("auto-time", clip, {});
  }

  function setProfileValue(key) {
    return (value) => setProfile((current) => ({ ...current, [key]: value }));
  }

  function updateCaptionStyle(clipId, patch) {
    setCaptionStyles((current) => ({
      ...current,
      [clipId]: { ...defaultCaptionStyle, ...(current[clipId] || {}), ...patch },
    }));
  }

  return (
    <main className="app-shell">
      <AppHeader
        job={job}
        health={health}
        language={language}
        setLanguage={setLanguage}
        theme={theme}
        setTheme={setTheme}
        t={t}
        compact={view === "editor"}
      />

      {view === "editor" && editorClipId && editorClip ? (
        <ClipEditorPage
          clip={editorClip}
          clips={activeClips}
          job={job}
          health={health}
          t={t}
          onBack={closeEditor}
          onSelectClip={openEditor}
          onPatch={patchClip}
          onDelete={(clip) => {
            patchClip(clip.id, { deleted: true });
            closeEditor();
          }}
          onApprove={(clip) => patchClip(clip.id, { approved: !clip.approved })}
          onRegenerate={regenerateClip}
          onPolishSubtitles={polishSubtitles}
          onTranslateSubtitles={translateSubtitles}
          onAutoTimeSubtitles={autoTimeSubtitles}
          captionStyle={editorCaptionStyle}
          onCaptionStyleChange={(patch) => updateCaptionStyle(editorClip.id, patch)}
        />
      ) : (
        <Dashboard
          profile={profile}
          setProfile={setProfile}
          setProfileValue={setProfileValue}
          sourceMode={sourceMode}
          setSourceMode={setSourceMode}
          youtubeUrl={youtubeUrl}
          setYoutubeUrl={setYoutubeUrl}
          file={file}
          setFile={setFile}
          error={error}
          isSubmitting={isSubmitting}
          submitJob={submitJob}
          job={job}
          activeClips={activeClips}
          t={t}
          onOpenEditor={openEditor}
          onPatch={patchClip}
          onDelete={(clip) => patchClip(clip.id, { deleted: true })}
          onApprove={(clip) => patchClip(clip.id, { approved: !clip.approved })}
          onRegenerate={regenerateClip}
        />
      )}
    </main>
  );
}

// ============================================================
// Dashboard layout
// ============================================================
function Dashboard({
  profile,
  setProfile,
  setProfileValue,
  sourceMode,
  setSourceMode,
  youtubeUrl,
  setYoutubeUrl,
  file,
  setFile,
  error,
  isSubmitting,
  submitJob,
  job,
  activeClips,
  t,
  onOpenEditor,
  onPatch,
  onDelete,
  onApprove,
  onRegenerate,
}) {
  return (
    <div className="workspace-grid">
      {/* Sidebar — profile form */}
      <div className="sidebar-column">
        <section className="panel input-panel">
          <ProfileForm
            t={t}
            profile={profile}
            setProfile={setProfile}
            setProfileValue={setProfileValue}
            sourceMode={sourceMode}
            setSourceMode={setSourceMode}
            youtubeUrl={youtubeUrl}
            setYoutubeUrl={setYoutubeUrl}
            file={file}
            setFile={setFile}
            error={error}
            isSubmitting={isSubmitting}
            submitJob={submitJob}
          />
        </section>
      </div>

      {/* Main content column */}
      <div className="main-column">
        <ProgressPanel job={job} t={t} />
        <TranscriptPanel job={job} t={t} />
        <ClipsPanel
          clips={activeClips}
          t={t}
          onOpenEditor={onOpenEditor}
          onPatch={onPatch}
          onDelete={onDelete}
          onApprove={onApprove}
          onRegenerate={onRegenerate}
        />
      </div>
    </div>
  );
}

// ============================================================
// App Header
// ============================================================
function AppHeader({ job, health, language, setLanguage, theme, setTheme, t, compact }) {
  const status = job?.status || "idle";
  const modeLabel = health ? (health.demo_mode ? t("demoMode") : t("productionMode")) : "API";
  const modeClass = health ? (health.demo_mode ? "demo" : "prod") : "";

  return (
    <header className={`app-header ${compact ? "compact" : ""}`}>
      <div className="brand-block">
        <div className="brand-mark">
          <Scissors size={20} />
        </div>
        <div>
          <h1>ElevenClip.AI</h1>
          {!compact && <p>{t("appSubtitle")}</p>}
        </div>
      </div>

      <div className="header-actions">
        <span className={`mode-pill ${modeClass}`}>{modeLabel}</span>
        <StatusPill status={status} t={t} />
        <label className="toolbar-select" title={t("language")}>
          <Languages size={14} />
          <select value={language} onChange={(event) => setLanguage(event.target.value)}>
            {LANGUAGES.map((item) => (
              <option key={item.code} value={item.code}>
                {item.label}
              </option>
            ))}
          </select>
        </label>
        <button
          className="icon-button"
          type="button"
          title={t("theme")}
          onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
        >
          {theme === "dark" ? <Sun size={16} /> : <Moon size={16} />}
        </button>
      </div>
    </header>
  );
}

// ============================================================
// Status pill
// ============================================================
function StatusPill({ status, t }) {
  return <span className={`status-pill ${status}`}>{t(status)}</span>;
}

// ============================================================
// Profile form (sidebar)
// ============================================================
function ProfileForm({
  t,
  profile,
  setProfile,
  setProfileValue,
  sourceMode,
  setSourceMode,
  youtubeUrl,
  setYoutubeUrl,
  file,
  setFile,
  error,
  isSubmitting,
  submitJob,
}) {
  return (
    <form className="form-stack" onSubmit={submitJob}>
      <div className="panel-heading">
        <div>
          <h2>{t("channelProfile")}</h2>
          <p>{t("channelProfileText")}</p>
        </div>
        <div className="panel-heading-icon">
          <SlidersHorizontal size={16} />
        </div>
      </div>

      <SelectField
        label={t("niche")}
        helper={t("nicheHelp")}
        value={profile.niche}
        onChange={setProfileValue("niche")}
        options={NICHES.map((value) => ({ value, label: t(`niche_${value}`) }))}
      />
      {profile.niche === "other" && (
        <TextField
          label={t("customNiche")}
          value={profile.niche_custom}
          onChange={setProfileValue("niche_custom")}
          placeholder={t("customNichePlaceholder")}
        />
      )}
      <TextAreaField
        label={t("channelDescription")}
        helper={t("channelDescriptionHelp")}
        value={profile.channel_description}
        onChange={setProfileValue("channel_description")}
        placeholder={t("channelDescriptionPlaceholder")}
        rows={3}
      />

      <div className="form-grid-two">
        <SelectField
          label={t("clipStyle")}
          value={profile.clip_style}
          onChange={setProfileValue("clip_style")}
          options={CLIP_STYLES.map((value) => ({ value, label: t(`style_${value}`) }))}
        />
        <SelectField
          label={t("clipLength")}
          value={profile.clip_length_seconds}
          onChange={(value) =>
            setProfile((current) => ({ ...current, clip_length_seconds: Number(value) }))
          }
          options={[30, 45, 60, 90, 120].map((value) => ({ value, label: String(value) }))}
        />
      </div>

      <SelectField
        label={t("clipCount")}
        helper={t("clipCountHelp")}
        value={profile.clip_count}
        onChange={(value) => setProfile((current) => ({ ...current, clip_count: Number(value) }))}
        options={[3, 5, 10].map((value) => ({ value, label: String(value) }))}
      />

      <div className="form-grid-two">
        <SelectField
          label={t("primaryLanguage")}
          value={profile.primary_language}
          onChange={setProfileValue("primary_language")}
          options={LANGUAGE_OPTIONS.map((value) => ({
            value,
            label: t(`languageOption_${value}`),
          }))}
        />
        <SelectField
          label={t("platform")}
          value={profile.target_platform}
          onChange={setProfileValue("target_platform")}
          options={PLATFORM_OPTIONS.map((value) => ({ value, label: t(`platform_${value}`) }))}
        />
      </div>

      <div className="divider" />

      <div className="panel-heading compact">
        <div>
          <h2>{t("videoInput")}</h2>
        </div>
        <div className="panel-heading-icon">
          <Film size={16} />
        </div>
      </div>

      <div className="segmented">
        <button
          type="button"
          className={sourceMode === "youtube" ? "active" : ""}
          onClick={() => setSourceMode("youtube")}
        >
          <LinkIcon size={14} />
          {t("youtube")}
        </button>
        <button
          type="button"
          className={sourceMode === "upload" ? "active" : ""}
          onClick={() => setSourceMode("upload")}
        >
          <Upload size={14} />
          {t("upload")}
        </button>
      </div>

      {sourceMode === "youtube" ? (
        <TextField
          label={t("youtubeUrl")}
          value={youtubeUrl}
          onChange={setYoutubeUrl}
          placeholder={t("youtubePlaceholder")}
        />
      ) : (
        <label className="field-block">
          <span className="field-label">{t("videoFile")}</span>
          <input
            className="file-input"
            type="file"
            accept="video/mp4,video/quicktime,video/*"
            onChange={(event) => setFile(event.target.files?.[0] || null)}
          />
          {file && <span className="file-name">{file.name}</span>}
        </label>
      )}

      {error && <div className="error-box">{error}</div>}

      <button
        className="primary-button"
        style={{ width: "100%" }}
        disabled={isSubmitting || (sourceMode === "youtube" ? !youtubeUrl : !file)}
        type="submit"
      >
        {isSubmitting ? <Loader2 className="spin" size={16} /> : <Wand2 size={16} />}
        {t("startPipeline")}
      </button>
    </form>
  );
}

// ============================================================
// Progress panel
// ============================================================
function ProgressPanel({ job, t }) {
  const progress = Math.round((job?.progress || 0) * 100);
  const steps = [
    ["input", t("stepInput")],
    ["transcription", t("stepTranscription")],
    ["highlight_detection", t("stepHighlights")],
    ["multimodal_analysis", t("stepVisual")],
    ["clip_generation", t("stepRender")],
    ["finalizing", t("stepFinal")],
  ];
  const stepIndex = Math.max(0, (job?.step_index || 0) - 1);
  const message =
    job?.current_step === "clip_generation" && job.active_clip_total
      ? t("renderProgress", {
          current: job.active_clip_index || 1,
          total: job.active_clip_total,
        })
      : job?.message || t("ready");

  return (
    <section className="panel progress-panel">
      <div className="panel-heading">
        <div>
          <h2>{t("pipeline")}</h2>
          <p>{message}</p>
        </div>
        <strong className="progress-percent">{progress}%</strong>
      </div>

      <div className="progress-track">
        <div className="progress-bar" style={{ width: `${progress}%` }} />
      </div>

      <div className="step-list" aria-label={t("currentStep")}>
        {steps.map(([id, label], index) => (
          <div
            key={id}
            className={`step-item${index < stepIndex ? " done" : ""}${
              index === stepIndex ? " active" : ""
            }`}
          >
            <span>{index + 1}</span>
            <p>{label}</p>
          </div>
        ))}
      </div>

      <p className="helper-text" style={{ marginTop: 12 }}>
        {t("progressNote")}
      </p>

      {job?.timings && Object.keys(job.timings).length > 0 && (
        <div className="timing-grid">
          {Object.entries(job.timings).map(([name, value]) => (
            <div key={name}>
              <span>{t(`timing_${name}`)}</span>
              <strong>{value}s</strong>
            </div>
          ))}
        </div>
      )}

      {job?.error && <div className="error-box" style={{ marginTop: 12 }}>{job.error}</div>}
    </section>
  );
}

// ============================================================
// Transcript panel
// ============================================================
function TranscriptPanel({ job, t }) {
  return (
    <section className="panel transcript-panel">
      <div className="panel-heading compact">
        <div>
          <h2>{t("transcript")}</h2>
          {!job?.transcript?.length && <p>{t("transcriptEmpty")}</p>}
        </div>
        <div className="panel-heading-icon">
          <Captions size={16} />
        </div>
      </div>
      {job?.transcript?.length > 0 && (
        <div className="transcript-list">
          {job.transcript.map((segment) => (
            <div key={segment.id} className="transcript-row">
              <span>
                {formatTime(segment.start_seconds)} – {formatTime(segment.end_seconds)}
              </span>
              <p>{segment.text}</p>
            </div>
          ))}
        </div>
      )}
    </section>
  );
}

// ============================================================
// Clips panel
// ============================================================
function ClipsPanel({ clips, t, onOpenEditor, onPatch, onDelete, onApprove, onRegenerate }) {
  return (
    <section className="panel clips-panel">
      <div className="panel-heading">
        <div>
          <h2>{t("clips")}</h2>
          <p>
            {clips.length} {t("readyClips")}
          </p>
        </div>
        <div className="panel-heading-icon">
          <Film size={16} />
        </div>
      </div>

      {clips.length === 0 ? (
        <div className="empty-state">
          <Film size={32} />
          <h3>{t("noClips")}</h3>
          <p>{t("noClipsText")}</p>
        </div>
      ) : (
        <div className="clip-grid">
          {clips.map((clip) => (
            <ClipCard
              key={clip.id}
              clip={clip}
              t={t}
              onOpenEditor={onOpenEditor}
              onPatch={onPatch}
              onDelete={onDelete}
              onApprove={onApprove}
              onRegenerate={onRegenerate}
            />
          ))}
        </div>
      )}
    </section>
  );
}

// ============================================================
// Clip card
// ============================================================
function ClipCard({ clip, t, onOpenEditor, onPatch, onDelete, onApprove, onRegenerate }) {
  const duration = Math.max(1, clip.end_seconds - clip.start_seconds);

  return (
    <article className="clip-card">
      <div className="clip-video">
        {clip.video_url ? (
          <video controls src={`${API_BASE}${clip.video_url}`} />
        ) : (
          <Film size={28} />
        )}
      </div>

      <div className="clip-body">
        {/* Title + score */}
        <div className="clip-meta">
          <div style={{ minWidth: 0 }}>
            <h3 className="clip-title">{clip.title}</h3>
            <p className="clip-reason">{clip.reason}</p>
          </div>
          <span className="score-badge">
            <Gauge size={12} />
            {Math.round(clip.score)}
          </span>
        </div>

        {/* Duration row */}
        <div className="clip-duration-row">
          <span>
            <Clock3 size={12} />
            {duration.toFixed(1)}s
          </span>
          <span>
            {formatTime(clip.start_seconds)} – {formatTime(clip.end_seconds)}
          </span>
        </div>

        {/* Subtitle snippet */}
        {clip.subtitle_text && (
          <p className="subtitle-snippet">{clip.subtitle_text}</p>
        )}

        {/* Action row — primary CTA on top, icon group below */}
        <div className="clip-actions">
          <button
            className="btn btn-primary"
            type="button"
            title={t("openEditor")}
            onClick={() => onOpenEditor(clip)}
          >
            <PanelRightOpen size={14} />
            {t("openEditor")}
          </button>

          <div className="clip-actions-icons">
            <button
              className={`btn btn-icon ${clip.approved ? "btn-success" : ""}`}
              type="button"
              title={clip.approved ? t("approved") : t("approve")}
              onClick={() => onApprove(clip)}
            >
              <Check size={14} />
            </button>

            <button
              className="btn btn-icon"
              type="button"
              title={t("regenerate")}
              onClick={() => onRegenerate(clip)}
            >
              <RefreshCcw size={14} />
            </button>

            <button
              className="btn btn-icon btn-danger"
              type="button"
              title={t("delete")}
              onClick={() => onDelete(clip)}
            >
              <Trash2 size={14} />
            </button>

            {clip.download_url && (
              <a
                className="btn btn-icon"
                href={`${API_BASE}${clip.download_url}`}
                title={t("download")}
                style={{ borderColor: "var(--primary-dim)", background: "var(--primary-glow)", color: "var(--primary)" }}
              >
                <Download size={14} />
              </a>
            )}
          </div>
        </div>
      </div>
    </article>
  );
}

// ============================================================
// Clip editor page — NLE 4-panel layout (Premiere-style)
// ============================================================
function ClipEditorPage({
  clip,
  clips,
  job,
  health,
  t,
  onBack,
  onSelectClip,
  onPatch,
  onDelete,
  onApprove,
  onRegenerate,
  onPolishSubtitles,
  onTranslateSubtitles,
  onAutoTimeSubtitles,
  captionStyle,
  onCaptionStyleChange,
}) {
  const videoRef = useRef(null);
  const [playhead, setPlayhead] = useState(clip.start_seconds);
  const [isPlaying, setIsPlaying] = useState(false);
  const [selectedCueIndex, setSelectedCueIndex] = useState(0);

  // DRAFT state for in-flight drag (no API calls during mousemove)
  const [cueDraft, setCueDraft] = useState(null); // { index, cue: {start_seconds, end_seconds} } | null
  const [captionDraft, setCaptionDraft] = useState(null); // null | { x, y }
  const [aiBusy, setAiBusy] = useState({ polish: false, translate: false, autoTime: false });

  const effStart = clip.start_seconds;
  const effEnd = clip.end_seconds;
  const duration = Math.max(0.5, effEnd - effStart);
  const effCaptionStyle = captionDraft
    ? { ...captionStyle, ...captionDraft }
    : captionStyle;

  // Cue source: explicit subtitle_cues from backend if present, else auto-distribute
  const baseCues = useMemo(() => {
    if (Array.isArray(clip.subtitle_cues) && clip.subtitle_cues.length) {
      return clip.subtitle_cues.map((cue) => ({
        start_seconds: Number(cue.start_seconds || 0),
        end_seconds: Number(cue.end_seconds || 0),
        text: String(cue.text || ""),
      }));
    }
    return getSubtitleCues(clip, duration, captionStyle);
  }, [clip, duration, captionStyle]);

  // Apply draft (one cue's timing) on top of base cues
  const cues = useMemo(() => {
    if (!cueDraft) return baseCues;
    return baseCues.map((cue, index) =>
      index === cueDraft.index
        ? { ...cue, start_seconds: cueDraft.cue.start_seconds, end_seconds: cueDraft.cue.end_seconds }
        : cue
    );
  }, [baseCues, cueDraft]);

  const metadataModel = clip.metadata?.model || "unknown";
  const sourceKind = job?.source?.kind || "video";

  const timelineDuration = Math.max(
    effEnd + 5,
    ...(clips || []).map((c) => Number(c.end_seconds || 0)),
    ...(job?.transcript || []).map((segment) => Number(segment.end_seconds || 0)),
    1
  );

  // Range-locked playback: video plays only within [effStart, effEnd]
  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;
    function onTimeUpdate() {
      const ct = video.currentTime;
      if (ct >= effEnd - 0.05) {
        video.pause();
        try {
          video.currentTime = effStart;
        } catch {
          /* ignore */
        }
      } else if (ct < effStart - 0.5) {
        try {
          video.currentTime = effStart;
        } catch {
          /* ignore */
        }
      }
      setPlayhead(video.currentTime);
    }
    function onPlay() {
      setIsPlaying(true);
    }
    function onPause() {
      setIsPlaying(false);
    }
    video.addEventListener("timeupdate", onTimeUpdate);
    video.addEventListener("play", onPlay);
    video.addEventListener("pause", onPause);
    return () => {
      video.removeEventListener("timeupdate", onTimeUpdate);
      video.removeEventListener("play", onPlay);
      video.removeEventListener("pause", onPause);
    };
  }, [effStart, effEnd]);

  // When the clip itself changes (selected from bin), seek to its start
  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;
    function onLoaded() {
      try {
        video.currentTime = clip.start_seconds;
      } catch {
        /* ignore */
      }
      setPlayhead(clip.start_seconds);
    }
    if (video.readyState >= 1) onLoaded();
    else video.addEventListener("loadedmetadata", onLoaded, { once: true });
    video.load();
    return () => video.removeEventListener("loadedmetadata", onLoaded);
  }, [clip.id, clip.video_url, clip.start_seconds]);

  // Active cue at current playhead
  const playheadInClip = clamp(playhead - effStart, 0, duration);
  const autoActive = cues.findIndex(
    (cue) => playheadInClip >= cue.start_seconds && playheadInClip < cue.end_seconds
  );
  const activeIndex =
    selectedCueIndex >= 0 && selectedCueIndex < cues.length
      ? selectedCueIndex
      : Math.max(0, autoActive);
  const activeCueText = cues[activeIndex]?.text || clip.subtitle_text || clip.title || "";

  // ─── Mutations ──────────────────────────────────────────────
  function commitCaption(patch) {
    setCaptionDraft(null);
    onCaptionStyleChange(patch);
  }
  function persistCues(nextCues) {
    onPatch(clip.id, {
      subtitle_cues: nextCues.map((cue) => ({
        start_seconds: roundTime(Number(cue.start_seconds || 0)),
        end_seconds: roundTime(Number(cue.end_seconds || 0)),
        text: String(cue.text || ""),
      })),
      subtitle_text: nextCues.map((cue) => cue.text).join(" "),
    });
  }
  function commitCueTiming(index, partial) {
    setCueDraft(null);
    const next = baseCues.map((cue, i) =>
      i === index
        ? { ...cue, start_seconds: partial.start_seconds, end_seconds: partial.end_seconds }
        : cue
    );
    persistCues(next);
  }
  function patchCueText(index, text) {
    const next = baseCues.map((cue, i) => (i === index ? { ...cue, text } : cue));
    persistCues(next);
  }
  function addCue() {
    const last = baseCues[baseCues.length - 1];
    const startNew = last ? Math.min(last.end_seconds + 0.5, duration - 1) : 0;
    const endNew = clamp(startNew + 2, startNew + 0.5, duration);
    const next = [
      ...baseCues,
      { start_seconds: startNew, end_seconds: endNew, text: "" },
    ];
    persistCues(next);
    setSelectedCueIndex(next.length - 1);
  }
  function removeCue(index) {
    const next = baseCues.filter((_, i) => i !== index);
    persistCues(next);
    setSelectedCueIndex(Math.max(0, Math.min(index, next.length - 1)));
  }
  function setClipLength(seconds) {
    onPatch(clip.id, {
      end_seconds: roundTime(
        clamp(clip.start_seconds + seconds, clip.start_seconds + 1, timelineDuration)
      ),
    });
  }
  function extendClip(deltaSeconds) {
    onPatch(clip.id, {
      end_seconds: roundTime(
        clamp(clip.end_seconds + deltaSeconds, clip.start_seconds + 1, timelineDuration)
      ),
    });
  }
  function addSkipRange(rangeStart, rangeEnd) {
    const start = clamp(Number(rangeStart), 0, duration);
    const end = clamp(Number(rangeEnd), start + 0.2, duration);
    const existing = Array.isArray(clip.skip_ranges) ? clip.skip_ranges : [];
    onPatch(clip.id, {
      skip_ranges: [...existing, { start_seconds: roundTime(start), end_seconds: roundTime(end) }],
    });
  }
  function removeSkipRange(index) {
    const existing = Array.isArray(clip.skip_ranges) ? clip.skip_ranges : [];
    onPatch(clip.id, {
      skip_ranges: existing.filter((_, i) => i !== index),
    });
  }
  async function runAiAction(kind, fn) {
    setAiBusy((b) => ({ ...b, [kind]: true }));
    try {
      await fn();
    } finally {
      setAiBusy((b) => ({ ...b, [kind]: false }));
    }
  }
  function seekTo(seconds) {
    const video = videoRef.current;
    const target = clamp(seconds, effStart, effEnd);
    if (video) {
      try {
        video.currentTime = target;
      } catch {
        /* ignore */
      }
    }
    setPlayhead(target);
  }
  function togglePlay() {
    const video = videoRef.current;
    if (!video) return;
    if (video.paused) {
      if (video.currentTime < effStart || video.currentTime >= effEnd - 0.05) {
        try {
          video.currentTime = effStart;
        } catch {
          /* ignore */
        }
      }
      video.play();
    } else {
      video.pause();
    }
  }

  return (
    <div className="editor-shell nle">
      <div className="editor-topbar">
        <button className="ghost-button" type="button" onClick={onBack}>
          <ArrowLeft size={16} />
          {t("backToDashboard")}
        </button>

        <div className="editor-topbar-info">
          <h2>{clip.title}</h2>
          <p>
            {formatTime(effStart)} – {formatTime(effEnd)} &nbsp;·&nbsp;{" "}
            {duration.toFixed(1)}s &nbsp;·&nbsp; {Math.round(clip.score)} {t("score")}
          </p>
        </div>

        <div className="editor-topbar-actions">
          <button
            className={`btn ${clip.approved ? "btn-success" : ""}`}
            type="button"
            onClick={() => onApprove(clip)}
          >
            <Check size={14} />
            {clip.approved ? t("approved") : t("approve")}
          </button>
          {clip.download_url && (
            <a
              className="btn btn-primary"
              href={`${API_BASE}${clip.download_url}`}
              title={t("download")}
            >
              <Download size={14} />
              {t("download")}
            </a>
          )}
        </div>
      </div>

      <div className="editor-grid-nle">
        <MediaBinPanel
          clips={clips || []}
          activeId={clip.id}
          onSelect={onSelectClip}
          t={t}
        />

        <PreviewStage
          clip={clip}
          videoRef={videoRef}
          captionStyle={effCaptionStyle}
          activeCueText={activeCueText}
          isPlaying={isPlaying}
          playhead={playhead}
          effStart={effStart}
          effEnd={effEnd}
          onTogglePlay={togglePlay}
          onSeekDelta={(delta) => seekTo(playhead + delta)}
          onCaptionDraftChange={setCaptionDraft}
          onCaptionCommit={commitCaption}
          t={t}
        />

        <AIAssistantPanel
          clip={clip}
          health={health}
          t={t}
          onRegenerate={onRegenerate}
          onDelete={onDelete}
        />

        <TimelineEditor
          clip={clip}
          cues={cues}
          timelineDuration={timelineDuration}
          playhead={playhead}
          effStart={effStart}
          effEnd={effEnd}
          selectedCueIndex={activeIndex}
          onSelectCue={setSelectedCueIndex}
          onSeek={seekTo}
          onCueDraftChange={(index, cuePartial) =>
            setCueDraft({ index, cue: cuePartial })
          }
          onCueCommit={commitCueTiming}
          t={t}
        />

        <EditorInspector
          clip={clip}
          metadataModel={metadataModel}
          sourceKind={sourceKind}
          captionStyle={captionStyle}
          onCaptionStyleChange={onCaptionStyleChange}
          cues={baseCues}
          activeIndex={activeIndex}
          onSelectCue={setSelectedCueIndex}
          onPatchCueText={patchCueText}
          onPatchCueTiming={(index, partial) =>
            commitCueTiming(index, partial)
          }
          onAddCue={addCue}
          onRemoveCue={removeCue}
          onSeek={seekTo}
          aiBusy={aiBusy}
          onPolish={() =>
            runAiAction("polish", () => onPolishSubtitles(clip))
          }
          onTranslate={(targetLang) =>
            runAiAction("translate", () => onTranslateSubtitles(clip, targetLang))
          }
          onAutoTime={() =>
            runAiAction("autoTime", () => onAutoTimeSubtitles(clip))
          }
          onSetClipLength={setClipLength}
          onExtendClip={extendClip}
          onAddSkipRange={addSkipRange}
          onRemoveSkipRange={removeSkipRange}
          onRegenerate={onRegenerate}
          t={t}
        />
      </div>
    </div>
  );
}

// ============================================================
// Media Bin (left column)
// ============================================================
function MediaBinPanel({ clips, activeId, onSelect, t }) {
  return (
    <aside className="nle-panel nle-bin">
      <div className="nle-panel-head">
        <h3>
          {t("mediaBin")} <span style={{ color: "var(--text-soft)", fontWeight: 500, marginLeft: 6 }}>· {clips.length}</span>
        </h3>
        <span className="nle-panel-icon">
          <FolderOpen size={12} />
        </span>
      </div>
      <div className="nle-panel-body">
        <div className="nle-bin-list">
          {clips.map((c) => (
            <button
              type="button"
              key={c.id}
              className={`nle-bin-item ${c.id === activeId ? "active" : ""}`}
              onClick={() => onSelect && onSelect(c)}
            >
              <div className="nle-bin-thumb">
                {c.video_url ? (
                  <video src={`${API_BASE}${c.video_url}`} muted preload="metadata" />
                ) : (
                  <Film size={18} />
                )}
              </div>
              <div className="nle-bin-meta">
                <span className="nle-bin-title">{c.title}</span>
                <span className="nle-bin-sub">
                  {formatTime(c.start_seconds)}–{formatTime(c.end_seconds)}
                  <span className="nle-bin-score">
                    <Gauge size={10} />
                    {Math.round(c.score)}
                  </span>
                  {c.approved && (
                    <Check size={10} style={{ color: "var(--success)" }} />
                  )}
                </span>
              </div>
            </button>
          ))}
        </div>
      </div>
    </aside>
  );
}

// ============================================================
// Preview Stage (center top) — video + draggable caption
// ============================================================
function PreviewStage({
  clip,
  videoRef,
  captionStyle,
  activeCueText,
  isPlaying,
  playhead,
  effStart,
  effEnd,
  onTogglePlay,
  onSeekDelta,
  onCaptionDraftChange,
  onCaptionCommit,
  t,
}) {
  const stageRef = useRef(null);

  function handleCaptionDragStart(event) {
    event.preventDefault();
    const stage = stageRef.current;
    if (!stage) return;
    const rect = stage.getBoundingClientRect();
    function compute(ev) {
      const x = clamp(((ev.clientX - rect.left) / rect.width) * 100, 4, 96);
      const y = clamp(((ev.clientY - rect.top) / rect.height) * 100, 6, 94);
      return { x: Math.round(x), y: Math.round(y) };
    }
    function onMove(ev) {
      onCaptionDraftChange(compute(ev));
    }
    function onUp(ev) {
      onCaptionCommit(compute(ev));
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
    }
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
  }

  const playheadInClip = clamp(playhead - effStart, 0, Math.max(0.5, effEnd - effStart));
  const clipDuration = Math.max(0.5, effEnd - effStart);

  return (
    <section className="nle-panel nle-preview">
      <div className="nle-panel-head">
        <h3>{t("preview")}</h3>
        <span className="nle-panel-icon">
          <Maximize2 size={12} />
        </span>
      </div>
      <div className="preview-stage" ref={stageRef}>
        <div className="preview-stage-canvas">
          {clip.video_url ? (
            <video
              ref={videoRef}
              src={`${API_BASE}${clip.video_url}`}
              playsInline
              preload="metadata"
              muted
            />
          ) : (
            <Film size={56} style={{ color: "var(--text-soft)" }} />
          )}
          <CaptionOverlay
            text={activeCueText}
            settings={captionStyle}
            onMouseDown={handleCaptionDragStart}
          />
        </div>
      </div>
      <div className="preview-toolbar">
        <div className="preview-toolbar-left">
          <button
            className="btn btn-icon"
            type="button"
            onClick={() => onSeekDelta(-1)}
            title={t("trimStartBack")}
          >
            <SkipBack size={14} />
          </button>
          <button
            className="btn btn-icon btn-primary"
            type="button"
            onClick={onTogglePlay}
          >
            {isPlaying ? <Pause size={14} /> : <Play size={14} />}
          </button>
          <button
            className="btn btn-icon"
            type="button"
            onClick={() => onSeekDelta(1)}
            title={t("trimStartForward")}
          >
            <SkipForward size={14} />
          </button>
        </div>
        <div className="preview-time">
          <strong>{formatTime(playheadInClip)}</strong> / {formatTime(clipDuration)}
        </div>
        <div className="preview-toolbar-right">
          <span style={{ fontSize: "0.7rem", color: "var(--text-muted)" }}>
            <Move size={11} style={{ verticalAlign: "-2px", marginRight: 4 }} />
            {t("dragToPosition")}
          </span>
        </div>
      </div>
    </section>
  );
}

// ============================================================
// Caption Overlay — draggable, positioned by captionStyle.x/y
// ============================================================
function CaptionOverlay({ text, settings, onMouseDown }) {
  const words = (text || "").split(/\s+/).filter(Boolean);
  const litCount = Math.max(1, Math.ceil(words.length * 0.45));
  const x = typeof settings.x === "number" ? settings.x : 50;
  const y =
    typeof settings.y === "number"
      ? settings.y
      : 100 - (typeof settings.position === "number" ? settings.position : 18);
  const style = {
    left: `${x}%`,
    top: `${y}%`,
    color: settings.fillColor,
    fontFamily: `"${settings.fontFamily}", Inter, ui-sans-serif, system-ui, sans-serif`,
    fontSize: `${settings.fontSize}px`,
    WebkitTextStroke: `${settings.strokeWidth}px ${settings.strokeColor}`,
    textShadow: `0 3px 12px ${settings.strokeColor}`,
  };
  return (
    <div
      className={`caption-overlay ${settings.animation}`}
      style={style}
      onMouseDown={onMouseDown}
    >
      {settings.animation === "highlight" && words.length
        ? words.map((word, index) => (
            <span key={`${word}-${index}`} className={index < litCount ? "lit" : ""}>
              {word}
              {index < words.length - 1 ? " " : ""}
            </span>
          ))
        : text || "—"}
    </div>
  );
}

// ============================================================
// Timeline Editor (center bottom) — read-only V1 + draggable T1 cues
// ============================================================
function TimelineEditor({
  clip,
  cues,
  timelineDuration,
  playhead,
  effStart,
  effEnd,
  selectedCueIndex,
  onSelectCue,
  onSeek,
  onCueDraftChange,
  onCueCommit,
  t,
}) {
  const laneRef = useRef(null);
  const cueLaneRef = useRef(null);

  const ticks = useMemo(() => {
    const result = [];
    const step = Math.max(5, Math.ceil(timelineDuration / 12 / 5) * 5);
    for (let s = 0; s <= timelineDuration; s += step) {
      result.push(s);
    }
    return result;
  }, [timelineDuration]);

  const clipLeftPct = (effStart / timelineDuration) * 100;
  const clipWidthPct = ((effEnd - effStart) / timelineDuration) * 100;
  const playheadPct = clamp((playhead / timelineDuration) * 100, 0, 100);

  function rectOf(ref) {
    return ref.current ? ref.current.getBoundingClientRect() : null;
  }

  // Drag a T1 cue edge or body. `edge` is "left" | "right" | "body".
  function startCueDrag(index, edge) {
    return (event) => {
      event.preventDefault();
      event.stopPropagation();
      const rect = rectOf(cueLaneRef);
      if (!rect) return;
      const cue = cues[index];
      if (!cue) return;
      const initialStart = cue.start_seconds;
      const initialEnd = cue.end_seconds;
      const length = initialEnd - initialStart;
      const startX = event.clientX;
      const clipDur = Math.max(0.1, effEnd - effStart);

      function compute(ev) {
        if (edge === "body") {
          const dx = ev.clientX - startX;
          const deltaSeconds = (dx / rect.width) * timelineDuration;
          const newStart = clamp(initialStart + deltaSeconds, 0, clipDur - length);
          return {
            start_seconds: roundTime(newStart),
            end_seconds: roundTime(newStart + length),
          };
        }
        // Edge-relative: convert mouse → absolute seconds in clip
        const ratio = clamp((ev.clientX - rect.left) / rect.width, 0, 1);
        const absoluteSeconds = ratio * timelineDuration;
        const cueLocal = clamp(absoluteSeconds - effStart, 0, clipDur);
        if (edge === "left") {
          return {
            start_seconds: roundTime(clamp(cueLocal, 0, initialEnd - 0.3)),
            end_seconds: initialEnd,
          };
        }
        return {
          start_seconds: initialStart,
          end_seconds: roundTime(clamp(cueLocal, initialStart + 0.3, clipDur)),
        };
      }

      function onMove(ev) {
        onCueDraftChange(index, compute(ev));
      }
      function onUp(ev) {
        const final = compute(ev);
        onCueCommit(index, final);
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mouseup", onUp);
      }
      window.addEventListener("mousemove", onMove);
      window.addEventListener("mouseup", onUp);
    };
  }

  function handleRulerClick(event) {
    const rect = rectOf(laneRef);
    if (!rect) return;
    const ratio = clamp((event.clientX - rect.left) / rect.width, 0, 1);
    onSeek(ratio * timelineDuration);
  }

  return (
    <section className="nle-panel nle-timeline">
      <div className="nle-panel-head">
        <h3>{t("timelineTracks")}</h3>
        <span className="nle-panel-icon">
          <Layers size={12} />
        </span>
      </div>
      <div className="timeline-toolbar">
        <span>
          <Captions size={11} style={{ verticalAlign: "-2px", marginRight: 4 }} />
          {t("dragCueToRetime")}
        </span>
      </div>
      <div className="timeline-area">
        <div
          className="timeline-ruler"
          onClick={handleRulerClick}
          ref={laneRef}
          style={{ cursor: "pointer" }}
        >
          {ticks.map((tick) => {
            const left = (tick / timelineDuration) * 100;
            const isMajor = tick % 30 === 0;
            return (
              <React.Fragment key={tick}>
                <span
                  className={`timeline-tick ${isMajor ? "major" : ""}`}
                  style={{ left: `${left}%` }}
                />
                {isMajor && (
                  <span
                    className="timeline-tick-label"
                    style={{ left: `${left}%` }}
                  >
                    {formatTime(tick)}
                  </span>
                )}
              </React.Fragment>
            );
          })}
          <div
            className="timeline-playhead"
            style={{ left: `${playheadPct}%` }}
          />
        </div>
        <div className="timeline-stack">
          <div className="timeline-track">
            <div className="timeline-track-label">V1</div>
            <div className="timeline-track-lane video">
              <div
                className="timeline-clip readonly"
                style={{
                  left: `${clipLeftPct}%`,
                  width: `${clipWidthPct}%`,
                }}
                title={clip.title}
              >
                <span className="timeline-clip-label">{clip.title}</span>
              </div>
              <div
                className="timeline-playhead"
                style={{ left: `${playheadPct}%` }}
              />
            </div>
          </div>
          <div className="timeline-track">
            <div className="timeline-track-label">T1</div>
            <div className="timeline-track-lane" ref={cueLaneRef}>
              {cues.map((cue, index) => {
                const cueLeft =
                  ((effStart + cue.start_seconds) / timelineDuration) * 100;
                const cueWidth =
                  ((cue.end_seconds - cue.start_seconds) / timelineDuration) * 100;
                return (
                  <div
                    key={`cue-${index}`}
                    className={`timeline-caption-block ${
                      index === selectedCueIndex ? "selected" : ""
                    }`}
                    style={{
                      left: `${clamp(cueLeft, 0, 100)}%`,
                      width: `${clamp(cueWidth, 1.4, 100 - cueLeft)}%`,
                    }}
                    onMouseDown={startCueDrag(index, "body")}
                    onClick={(e) => {
                      // Suppress click if a drag occurred (mouse moved)
                      if (e.defaultPrevented) return;
                      onSelectCue(index);
                    }}
                    title={cue.text}
                  >
                    <span
                      className="cue-handle left"
                      onMouseDown={startCueDrag(index, "left")}
                    />
                    <span className="cue-text">{cue.text || "—"}</span>
                    <span
                      className="cue-handle right"
                      onMouseDown={startCueDrag(index, "right")}
                    />
                  </div>
                );
              })}
              <div
                className="timeline-playhead"
                style={{ left: `${playheadPct}%` }}
              />
            </div>
          </div>
          <div className="timeline-track">
            <div className="timeline-track-label">A1</div>
            <div className="timeline-track-lane audio">
              <div className="timeline-waveform">
                {Array.from({ length: 80 }).map((_, index) => (
                  <span
                    key={index}
                    style={{
                      height: `${24 + ((index * 17 + clip.id.length * 11) % 70)}%`,
                    }}
                  />
                ))}
              </div>
              <div
                className="timeline-playhead"
                style={{ left: `${playheadPct}%` }}
              />
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

// ============================================================
// AI Assistant Panel (right top)
// ============================================================
function AIAssistantPanel({ clip, health, t, onRegenerate, onDelete }) {
  const visualNote = clip.metadata?.visual_note;
  const visualScore = clip.metadata?.visual_score;
  const visualModel = clip.metadata?.visual_model;
  const textModel = clip.metadata?.model;
  const gpuActive = health && health.demo_mode === false;
  const acceleratorName =
    health?.accelerator?.device_name ||
    (gpuActive ? "MI300X" : t("gpuPending"));

  return (
    <aside className="nle-panel nle-ai">
      <div className="nle-panel-head">
        <h3>
          {t("aiAssistant")}{" "}
          <span
            className={`gpu-tag ${gpuActive ? "active" : "pending"}`}
            title={acceleratorName}
          >
            <span className="gpu-dot" />
            {gpuActive ? t("gpuActive") : t("gpuDemo")}
          </span>
        </h3>
        <span className="nle-panel-icon">
          <Sparkles size={12} />
        </span>
      </div>
      <div className="nle-panel-body">
        {/* Why AI picked this clip (Qwen text) */}
        <div className="ai-card">
          <div className="ai-card-head">
            <span className="ai-card-tag">
              <Wand2 size={10} /> Qwen2.5
            </span>
            <span className="ai-card-sub">{t("aiReasonHead")}</span>
          </div>
          <p className="ai-card-body">{clip.reason || t("aiReason")}</p>
          {textModel && (
            <p className="ai-card-foot">
              {t("model")}: {textModel}
            </p>
          )}
        </div>

        {/* Visual analysis (Qwen-VL) */}
        {visualNote && (
          <div className="ai-card vision">
            <div className="ai-card-head">
              <span className="ai-card-tag vision">
                <Sparkles size={10} /> Qwen2-VL
              </span>
              <span className="ai-card-sub">{t("aiVisualHead")}</span>
              {typeof visualScore === "number" && (
                <span className="ai-card-score">
                  {Math.round(visualScore)}
                </span>
              )}
            </div>
            <p className="ai-card-body">{visualNote}</p>
            {visualModel && (
              <p className="ai-card-foot">
                {t("model")}: {visualModel}
              </p>
            )}
          </div>
        )}

        <div className="ai-actions compact">
          <button
            type="button"
            className="ai-action"
            onClick={() => onRegenerate(clip)}
          >
            <span className="ai-action-icon">
              <RefreshCcw size={14} />
            </span>
            <span className="ai-action-text">
              <strong>{t("aiRedoAll")}</strong>
              <small>{t("aiActionRedoSub")}</small>
            </span>
          </button>
          <button
            type="button"
            className="ai-action danger"
            onClick={() => onDelete(clip)}
          >
            <span
              className="ai-action-icon"
              style={{ background: "var(--danger-soft)", color: "var(--danger)" }}
            >
              <Trash2 size={14} />
            </span>
            <span className="ai-action-text">
              <strong>{t("aiDeleteClip")}</strong>
              <small>{t("aiActionDeleteSub")}</small>
            </span>
          </button>
        </div>
      </div>
    </aside>
  );
}

// ============================================================
// Editor Inspector (right bottom) — metadata + caption + active cue
// ============================================================
function EditorInspector({
  clip,
  metadataModel,
  sourceKind,
  captionStyle,
  onCaptionStyleChange,
  cues,
  activeIndex,
  onSelectCue,
  onPatchCueText,
  onPatchCueTiming,
  onAddCue,
  onRemoveCue,
  onSeek,
  aiBusy,
  onPolish,
  onTranslate,
  onAutoTime,
  onSetClipLength,
  onExtendClip,
  onAddSkipRange,
  onRemoveSkipRange,
  onRegenerate,
  t,
}) {
  const clipDuration = Math.max(0.5, clip.end_seconds - clip.start_seconds);
  const skipRanges = Array.isArray(clip.skip_ranges) ? clip.skip_ranges : [];

  return (
    <aside className="nle-panel nle-inspector">
      <div className="nle-panel-head">
        <h3>{t("inspector")}</h3>
        <span className="nle-panel-icon">
          <SlidersHorizontal size={12} />
        </span>
      </div>
      <div className="nle-panel-body">
        <div className="inspector-stack">
          <section>
            <dl className="inspector-meta">
              <div>
                <dt>{t("score")}</dt>
                <dd className="score-value">{Math.round(clip.score)}</dd>
              </div>
              <div>
                <dt>{t("status")}</dt>
                <dd>{clip.approved ? t("approved") : t("notApproved")}</dd>
              </div>
              <div>
                <dt>{t("source")}</dt>
                <dd>{t(`source_${sourceKind}`)}</dd>
              </div>
              <div>
                <dt>{t("model")}</dt>
                <dd style={{ fontSize: "0.74rem" }}>{metadataModel}</dd>
              </div>
            </dl>
          </section>

          <SubtitleEditor
            clip={clip}
            cues={cues}
            activeIndex={activeIndex}
            clipDuration={clipDuration}
            onSelectCue={onSelectCue}
            onPatchCueText={onPatchCueText}
            onPatchCueTiming={onPatchCueTiming}
            onAddCue={onAddCue}
            onRemoveCue={onRemoveCue}
            onSeek={onSeek}
            aiBusy={aiBusy}
            onPolish={onPolish}
            onTranslate={onTranslate}
            onAutoTime={onAutoTime}
            t={t}
          />

          <ClipEditPanel
            clip={clip}
            clipDuration={clipDuration}
            skipRanges={skipRanges}
            onSetClipLength={onSetClipLength}
            onExtendClip={onExtendClip}
            onAddSkipRange={onAddSkipRange}
            onRemoveSkipRange={onRemoveSkipRange}
            onRegenerate={onRegenerate}
            t={t}
          />

          <section>
            <h4>
              <Captions size={11} style={{ verticalAlign: "-2px", marginRight: 5 }} />
              {t("captionStyle")}
            </h4>
            <CaptionStylePanel
              t={t}
              settings={captionStyle}
              onChange={onCaptionStyleChange}
            />
          </section>
        </div>
      </div>
    </aside>
  );
}

// ============================================================
// Subtitle Editor — full per-cue control + AI subtitle actions
// ============================================================
function SubtitleEditor({
  clip,
  cues,
  activeIndex,
  clipDuration,
  onSelectCue,
  onPatchCueText,
  onPatchCueTiming,
  onAddCue,
  onRemoveCue,
  onSeek,
  aiBusy,
  onPolish,
  onTranslate,
  onAutoTime,
  t,
}) {
  const [translateLang, setTranslateLang] = useState("English");

  return (
    <section className="subtitle-editor">
      <div className="subtitle-editor-head">
        <h4>
          <Type size={11} style={{ verticalAlign: "-2px", marginRight: 5 }} />
          {t("subtitleCues")}
        </h4>
        <span className="subtitle-count">{cues.length}</span>
      </div>

      <div className="cue-rows">
        {cues.map((cue, index) => (
          <div
            key={`${clip.id}-cue-${index}`}
            className={`cue-row ${index === activeIndex ? "active" : ""}`}
            onClick={() => onSelectCue(index)}
          >
            <div className="cue-row-times">
              <NumberStepper
                value={cue.start_seconds}
                min={0}
                max={Math.max(0, cue.end_seconds - 0.2)}
                step={0.1}
                onChange={(v) =>
                  onPatchCueTiming(index, {
                    start_seconds: v,
                    end_seconds: cue.end_seconds,
                  })
                }
              />
              <span className="cue-row-sep">–</span>
              <NumberStepper
                value={cue.end_seconds}
                min={cue.start_seconds + 0.2}
                max={clipDuration}
                step={0.1}
                onChange={(v) =>
                  onPatchCueTiming(index, {
                    start_seconds: cue.start_seconds,
                    end_seconds: v,
                  })
                }
              />
              <button
                type="button"
                className="cue-row-jump"
                title={t("seekToCue")}
                onClick={(e) => {
                  e.stopPropagation();
                  onSeek(clip.start_seconds + cue.start_seconds);
                }}
              >
                <Play size={11} />
              </button>
              <button
                type="button"
                className="cue-row-delete"
                title={t("delete")}
                onClick={(e) => {
                  e.stopPropagation();
                  onRemoveCue(index);
                }}
              >
                <Trash2 size={11} />
              </button>
            </div>
            <textarea
              className="cue-row-text"
              rows={2}
              value={cue.text}
              onChange={(e) => onPatchCueText(index, e.target.value)}
              onClick={(e) => e.stopPropagation()}
              placeholder={t("cuePlaceholder")}
            />
          </div>
        ))}
      </div>

      <button type="button" className="btn cue-add" onClick={onAddCue}>
        <span style={{ fontSize: "1rem", lineHeight: 1 }}>+</span> {t("addCue")}
      </button>

      <div className="ai-subtitle-actions">
        <p className="ai-subtitle-head">
          <Sparkles size={11} style={{ verticalAlign: "-2px", marginRight: 5 }} />
          {t("aiSubtitleHead")}
        </p>
        <div className="ai-subtitle-row">
          <button
            type="button"
            className="btn btn-primary"
            disabled={aiBusy?.polish}
            onClick={onPolish}
          >
            {aiBusy?.polish ? <Loader2 size={12} className="spin" /> : <Wand2 size={12} />}
            {t("aiPolish")}
          </button>
          <button
            type="button"
            className="btn"
            disabled={aiBusy?.autoTime}
            onClick={onAutoTime}
            title={t("aiAutoTimeHelp")}
          >
            {aiBusy?.autoTime ? (
              <Loader2 size={12} className="spin" />
            ) : (
              <Clock3 size={12} />
            )}
            {t("aiAutoTime")}
          </button>
        </div>
        <div className="ai-subtitle-row translate">
          <select
            value={translateLang}
            onChange={(e) => setTranslateLang(e.target.value)}
          >
            {LANGUAGE_OPTIONS.filter((l) => l !== "Auto").map((lang) => (
              <option key={lang} value={lang}>
                {t(`languageOption_${lang}`)}
              </option>
            ))}
          </select>
          <button
            type="button"
            className="btn"
            disabled={aiBusy?.translate}
            onClick={() => onTranslate(translateLang)}
          >
            {aiBusy?.translate ? (
              <Loader2 size={12} className="spin" />
            ) : (
              <Languages size={12} />
            )}
            {t("aiTranslate")}
          </button>
        </div>
      </div>
    </section>
  );
}

// ============================================================
// Clip Edit Panel — length presets, extend, cut middle, regenerate
// ============================================================
function ClipEditPanel({
  clip,
  clipDuration,
  skipRanges,
  onSetClipLength,
  onExtendClip,
  onAddSkipRange,
  onRemoveSkipRange,
  onRegenerate,
  t,
}) {
  const [skipStart, setSkipStart] = useState(0);
  const [skipEnd, setSkipEnd] = useState(0);

  function handleAddSkip() {
    const start = Math.max(0, Number(skipStart) || 0);
    const end = Math.max(start + 0.2, Number(skipEnd) || start + 1);
    if (end <= start) return;
    onAddSkipRange(start, end);
    setSkipStart(0);
    setSkipEnd(0);
  }

  return (
    <section className="clip-edit-panel">
      <h4>
        <Scissors size={11} style={{ verticalAlign: "-2px", marginRight: 5 }} />
        {t("clipEdit")}
      </h4>

      <div className="clip-edit-row">
        <span className="clip-edit-label">{t("clipLengthLabel")}</span>
        <div className="clip-edit-buttons">
          {[30, 45, 60, 90].map((sec) => (
            <button
              key={sec}
              type="button"
              className="btn btn-icon"
              onClick={() => onSetClipLength(sec)}
              title={`${sec}s`}
            >
              {sec}s
            </button>
          ))}
        </div>
      </div>

      <div className="clip-edit-row">
        <span className="clip-edit-label">{t("clipExtendLabel")}</span>
        <div className="clip-edit-buttons">
          {[5, 10, 30].map((sec) => (
            <button
              key={sec}
              type="button"
              className="btn btn-icon"
              onClick={() => onExtendClip(sec)}
              title={`+${sec}s`}
            >
              +{sec}s
            </button>
          ))}
        </div>
      </div>

      <div className="clip-edit-row vertical">
        <span className="clip-edit-label">{t("clipSkipLabel")}</span>
        <div className="clip-skip-input">
          <input
            type="number"
            min="0"
            max={clipDuration}
            step="0.1"
            value={skipStart}
            placeholder={t("from")}
            onChange={(e) => setSkipStart(e.target.value)}
          />
          <span>–</span>
          <input
            type="number"
            min="0"
            max={clipDuration}
            step="0.1"
            value={skipEnd}
            placeholder={t("to")}
            onChange={(e) => setSkipEnd(e.target.value)}
          />
          <button
            type="button"
            className="btn"
            onClick={handleAddSkip}
            title={t("clipSkipAdd")}
          >
            <Scissors size={11} />
            {t("clipSkipAdd")}
          </button>
        </div>
        {skipRanges.length > 0 && (
          <ul className="skip-list">
            {skipRanges.map((range, index) => (
              <li key={`skip-${index}`}>
                <span>
                  {range.start_seconds.toFixed(1)}s – {range.end_seconds.toFixed(1)}s
                </span>
                <button
                  type="button"
                  className="btn btn-icon btn-danger"
                  onClick={() => onRemoveSkipRange(index)}
                  title={t("delete")}
                >
                  <Trash2 size={10} />
                </button>
              </li>
            ))}
          </ul>
        )}
      </div>

      <button
        type="button"
        className="btn btn-primary clip-edit-rerender"
        onClick={() => onRegenerate(clip)}
      >
        <RefreshCcw size={12} />
        {t("clipRebuildBtn")}
      </button>
    </section>
  );
}

// ============================================================
// Number Stepper — compact numeric input with +/− buttons
// ============================================================
function NumberStepper({ value, min, max, step, onChange }) {
  const safe = Number(value) || 0;
  function clampVal(v) {
    return Math.min(max, Math.max(min, Math.round(v * 10) / 10));
  }
  return (
    <div className="num-stepper">
      <input
        type="number"
        value={safe.toFixed(1)}
        min={min}
        max={max}
        step={step}
        onChange={(e) => onChange(clampVal(Number(e.target.value)))}
        onClick={(e) => e.stopPropagation()}
      />
    </div>
  );
}

// ============================================================
// Caption style panel
// ============================================================
function CaptionStylePanel({ t, settings, onChange }) {
  return (
    <div className="caption-style-panel">
      <div className="preset-row">
        {Object.entries(captionPresets).map(([key, preset]) => (
          <button type="button" key={key} onClick={() => onChange(preset)}>
            {t(`preset${capitalize(key)}`)}
          </button>
        ))}
      </div>

      <div className="style-grid">
        <SelectField
          label={t("font")}
          value={settings.fontFamily}
          onChange={(value) => onChange({ fontFamily: value })}
          options={FONT_OPTIONS.map((value) => ({ value, label: value }))}
        />
        <SelectField
          label={t("captionLength")}
          value={settings.cueDensity}
          onChange={(value) => onChange({ cueDensity: value })}
          options={CUE_DENSITIES.map((value) => ({ value, label: t(`density_${value}`) }))}
        />
        <SelectField
          label={t("animation")}
          value={settings.animation}
          onChange={(value) => onChange({ animation: value })}
          options={CAPTION_ANIMATIONS.map((value) => ({ value, label: t(`animation_${value}`) }))}
        />
      </div>

      <div className="color-grid">
        <ColorField
          label={t("fillColor")}
          value={settings.fillColor}
          onChange={(fillColor) => onChange({ fillColor })}
        />
        <ColorField
          label={t("strokeColor")}
          value={settings.strokeColor}
          onChange={(strokeColor) => onChange({ strokeColor })}
        />
      </div>

      <RangeControl
        label={t("fontSize")}
        value={settings.fontSize}
        min={24}
        max={64}
        onChange={(fontSize) => onChange({ fontSize })}
      />
      <RangeControl
        label={t("strokeWidth")}
        value={settings.strokeWidth}
        min={0}
        max={8}
        onChange={(strokeWidth) => onChange({ strokeWidth })}
      />
      <RangeControl
        label={t("captionPosition")}
        value={settings.position}
        min={8}
        max={38}
        onChange={(position) => onChange({ position })}
      />
    </div>
  );
}

// ============================================================
// Mini transcript (editor right column)
// ============================================================
function TranscriptMini({ transcript, clip, t }) {
  const rows = transcript.filter(
    (segment) =>
      segment.end_seconds >= clip.start_seconds && segment.start_seconds <= clip.end_seconds
  );

  return (
    <div className="mini-transcript">
      <h3>{t("transcript")}</h3>
      {rows.length === 0 && (
        <p style={{ margin: 0, fontSize: "0.8rem", color: "var(--text-soft)" }}>
          {t("transcriptEmpty")}
        </p>
      )}
      {rows.map((segment) => (
        <div key={segment.id}>
          <span>
            {formatTime(segment.start_seconds)} – {formatTime(segment.end_seconds)}
          </span>
          <p>{segment.text}</p>
        </div>
      ))}
    </div>
  );
}

// ============================================================
// Reusable form fields
// ============================================================
function TextField({ label, value, onChange, placeholder, helper }) {
  return (
    <label className="field-block">
      <span className="field-label">{label}</span>
      <input
        className="text-input"
        value={value}
        placeholder={placeholder}
        onChange={(event) => onChange(event.target.value)}
      />
      {helper && <span className="helper-text">{helper}</span>}
    </label>
  );
}

function TextAreaField({ label, value, onChange, placeholder, helper, rows = 3 }) {
  return (
    <label className="field-block">
      <span className="field-label">{label}</span>
      <textarea
        value={value}
        placeholder={placeholder}
        onChange={(event) => onChange(event.target.value)}
        rows={rows}
      />
      {helper && <span className="helper-text">{helper}</span>}
    </label>
  );
}

function SelectField({ label, value, onChange, options, helper }) {
  return (
    <label className="field-block">
      <span className="field-label">{label}</span>
      <select
        className="text-input"
        value={value}
        onChange={(event) => onChange(event.target.value)}
      >
        {options.map((option) => (
          <option key={option.value} value={option.value}>
            {option.label}
          </option>
        ))}
      </select>
      {helper && <span className="helper-text">{helper}</span>}
    </label>
  );
}

function NumberField({ label, value, onChange }) {
  return (
    <label>
      <span style={{ display: "block", fontSize: "0.74rem", fontWeight: 700, color: "var(--text-muted)", marginBottom: 5 }}>
        {label}
      </span>
      <input
        type="number"
        min="0"
        step="0.5"
        value={value}
        onChange={(event) => onChange(event.target.value)}
        style={{
          width: "100%",
          minHeight: 36,
          padding: "7px 10px",
          border: "1px solid var(--border)",
          borderRadius: "var(--radius-sm)",
          background: "var(--surface)",
          color: "var(--text)",
          outline: "none",
          fontVariantNumeric: "tabular-nums",
        }}
      />
    </label>
  );
}

function ColorField({ label, value, onChange }) {
  return (
    <label className="color-field">
      <span>{label}</span>
      <input type="color" value={value} onChange={(event) => onChange(event.target.value)} />
    </label>
  );
}

function RangeControl({ label, value, min, max, onChange }) {
  return (
    <label className="range-control">
      <span>
        {label}
        <strong>{value}</strong>
      </span>
      <input
        type="range"
        min={min}
        max={max}
        step="1"
        value={value}
        onChange={(event) => onChange(Number(event.target.value))}
      />
    </label>
  );
}

// ============================================================
// Utility functions — unchanged
// ============================================================
async function fetchJson(path, options) {
  const response = await fetch(`${API_BASE}${path}`, options);
  if (!response.ok) {
    let detail = response.statusText;
    try {
      const payload = await response.json();
      detail = payload.detail || detail;
    } catch {
      detail = response.statusText;
    }
    throw new Error(detail);
  }
  return response.json();
}

function getSubtitleCues(clip, duration, settings = defaultCaptionStyle) {
  return splitSubtitleText(clip.subtitle_text || "", settings.cueDensity).map(
    (text, index, all) => {
      const cueDuration = duration / Math.max(all.length, 1);
      return {
        start_seconds: roundTime(index * cueDuration),
        end_seconds: roundTime((index + 1) * cueDuration),
        text,
      };
    }
  );
}

function splitSubtitleText(text, density = "short") {
  const clean = text.trim().replace(/\s+/g, " ");
  if (!clean) return [""];
  const limits = {
    word: { words: 1, chars: 18 },
    short: { words: 4, chars: 30 },
    medium: { words: 7, chars: 46 },
    long: { words: 12, chars: 78 },
  };
  const limit = limits[density] || limits.short;
  const words = clean.split(" ");
  if (words.length <= 1) {
    const chunks = [];
    for (let index = 0; index < clean.length; index += limit.chars) {
      chunks.push(clean.slice(index, index + limit.chars));
    }
    return chunks;
  }
  const chunks = [];
  let current = [];
  words.forEach((word) => {
    const candidate = [...current, word].join(" ");
    const punctuationBreak =
      current.length > 0 && /[,.!?;:]$/.test(current[current.length - 1]);
    if (
      current.length > 0 &&
      (candidate.length > limit.chars ||
        current.length >= limit.words ||
        punctuationBreak)
    ) {
      chunks.push(current.join(" "));
      current = [word];
    } else {
      current.push(word);
    }
  });
  if (current.length) chunks.push(current.join(" "));
  return chunks;
}

function roundTime(value) {
  return Math.round(value * 10) / 10;
}

function clamp(value, min, max) {
  return Math.min(Math.max(value, min), max);
}

function capitalize(value) {
  return value.charAt(0).toUpperCase() + value.slice(1);
}

function formatTime(value) {
  const safeValue = Number.isFinite(Number(value)) ? Number(value) : 0;
  const minutes = Math.floor(safeValue / 60);
  const seconds = Math.floor(safeValue % 60);
  return `${minutes}:${String(seconds).padStart(2, "0")}`;
}

export default App;
