// frontend\src\components\ui\ModelDetails.tsx

'use client'

import * as React from 'react'

import type { RecordJob } from '../../types'
import Modal from './Modal'
import Button from './Button'
import TagBadge from './TagBadge'
import RecordJobActions from './RecordJobActions'
import Tabs, { type TabItem } from './Tabs'
import {
  ArrowTopRightOnSquareIcon,
  CalendarDaysIcon,
  ArrowPathIcon,
  HeartIcon as HeartOutlineIcon,
  StarIcon as StarOutlineIcon,
  IdentificationIcon,
  LanguageIcon,
  LinkIcon,
  MapPinIcon,
  PhotoIcon,
  SparklesIcon,
  UsersIcon,
  ClockIcon,
  EyeIcon as EyeOutlineIcon,
} from '@heroicons/react/24/outline'
import {
  HeartIcon as HeartSolidIcon,
  StarIcon as StarSolidIcon,
  EyeIcon as EyeSolidIcon,
} from '@heroicons/react/24/solid'
import FinishedVideoPreview from './FinishedVideoPreview'
import TagOverflowRow from './TagOverflowRow'
import PreviewScrubber from './PreviewScrubber'
import { formatResolution } from './formatters'
import Pagination from './Pagination'
import LiveVideo from './LiveVideo'

function cn(...parts: Array<string | false | null | undefined>) {
  return parts.filter(Boolean).join(' ')
}

function isRunningJob(job: RecordJob): boolean {
  const s = String((job as any)?.status ?? '').toLowerCase()
  const ended = Boolean((job as any)?.endedAt ?? (job as any)?.completedAt)
  return !ended && (s === 'running' || s === 'postwork')
}

const MD_CACHE_TTL_MS = 10 * 60 * 1000 // 10 min

type OnlineCacheEntry = {
  at: number
  room: ChaturbateRoom | null
  meta: Pick<OnlineResp, 'enabled' | 'fetchedAt' | 'lastError'> | null
}

type BioCacheEntry = {
  at: number
  bio: BioContext | null
  meta: Pick<BioResp, 'enabled' | 'fetchedAt' | 'lastError'> | null
}

// In-Memory (über Modal-Open/Close hinweg, solange Tab offen)
const mdOnlineMem = new Map<string, OnlineCacheEntry>()
const mdBioMem = new Map<string, BioCacheEntry>()

function isFresh(at: number) {
  return Number.isFinite(at) && Date.now() - at <= MD_CACHE_TTL_MS
}

function ssGet<T>(key: string): T | null {
  try {
    if (typeof window === 'undefined') return null
    const raw = window.sessionStorage.getItem(key)
    if (!raw) return null
    return JSON.parse(raw) as T
  } catch {
    return null
  }
}

function ssSet(key: string, value: any) {
  try {
    if (typeof window === 'undefined') return
    window.sessionStorage.setItem(key, JSON.stringify(value))
  } catch {
    // ignore
  }
}

function ssKeyOnline(modelKey: string) {
  return `md:cb:online:${modelKey}`
}
function ssKeyBio(modelKey: string) {
  return `md:cb:bio:${modelKey}`
}

const nf = new Intl.NumberFormat('de-DE')

function fmtInt(n: number | undefined | null) {
  if (n == null || !Number.isFinite(n)) return '—'
  return nf.format(n)
}

function fmtBytes(n: number | undefined | null) {
  if (n == null || !Number.isFinite(n)) return '—'
  const units = ['B', 'KB', 'MB', 'GB', 'TB']
  let v = n
  let i = 0
  while (v >= 1024 && i < units.length - 1) {
    v /= 1024
    i++
  }
  const digits = i === 0 ? 0 : i === 1 ? 0 : 1
  return `${v.toFixed(digits)} ${units[i]}`
}

function fmtHms(totalSeconds: number | undefined | null) {
  if (totalSeconds == null || !Number.isFinite(totalSeconds) || totalSeconds < 0) return '—'
  const s = Math.floor(totalSeconds)
  const hh = Math.floor(s / 3600)
  const mm = Math.floor((s % 3600) / 60)
  const ss = s % 60
  if (hh > 0) return `${hh}:${String(mm).padStart(2, '0')}:${String(ss).padStart(2, '0')}`
  return `${mm}:${String(ss).padStart(2, '0')}`
}

function fmtDateTime(v: string | Date | null | undefined) {
  if (!v) return '—'
  const d = typeof v === 'string' ? new Date(v) : v
  if (Number.isNaN(d.getTime())) return String(v)
  return d.toLocaleString('de-DE', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
  })
}

function splitTags(v?: string | null) {
  if (!v) return []
  return v
    .split(',')
    .map((t) => t.trim())
    .filter(Boolean)
}

function baseName(path: string) {
  return (path || '').split(/[\\/]/).pop() || ''
}

function shortDate(v: string | Date | null | undefined) {
  if (!v) return '—'
  const d = typeof v === 'string' ? new Date(v) : v
  if (Number.isNaN(d.getTime())) return String(v)
  // kompakter als fmtDateTime (ohne Sekunden)
  return d.toLocaleDateString('de-DE', { year: '2-digit', month: '2-digit', day: '2-digit' })
}

function stripHotPrefix(name: string) {
  return name.startsWith('HOT ') ? name.slice(4) : name
}

function isHotName(name: string) {
  return String(name || '').startsWith('HOT ')
}

function replaceBaseName(path: string, newBase: string) {
  const p = String(path || '')
  if (!p) return p
  // ersetzt nur das letzte Segment (funktioniert für / und \)
  return p.replace(/([\\/])[^\\/]*$/, `$1${newBase}`)
}

function toggleHotFileName(file: string) {
  return isHotName(file) ? stripHotPrefix(file) : `HOT ${file}`
}

function modelNameFromOutput(output?: string) {
  const fileRaw = baseName(output || '')
  const file = stripHotPrefix(fileRaw)
  if (!file) return '—'

  const stem = file.replace(/\.[^.]+$/, '')

  // match: <model>_DD_MM_YYYY__HH-MM-SS
  const m = stem.match(/^(.*?)_\d{1,2}_\d{1,2}_\d{4}__\d{1,2}-\d{2}-\d{2}$/)
  if (m?.[1]) return m[1]

  const i = stem.lastIndexOf('_')
  return i > 0 ? stem.slice(0, i) : stem
}

function stripHtmlToText(input?: string | null) {
  if (!input) return ''
  // simpel & sicher: tags weg (kein dangerouslySetInnerHTML)
  return String(input)
    .replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, '')
    .replace(/<style[\s\S]*?>[\s\S]*?<\/style>/gi, '')
    .replace(/<\/?[^>]+>/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
}

function errorSummary(input?: string | null) {
  const s = String(input ?? '').trim()
  if (!s) return ''

  // bevorzugt: HTTP-Code rausziehen (kommt bei dir oft vor)
  const m = s.match(/HTTP\s+(\d{3})/i)
  if (m?.[1]) return `HTTP ${m[1]}`

  // wenn HTML/doctype drin: nur "HTML response" anzeigen
  const lower = s.toLowerCase()
  if (lower.includes('<!doctype') || lower.includes('<html') || lower.includes('text/html')) {
    return 'HTTP Error (HTML response)'
  }

  // fallback: erste Zeile / kurzer Ausschnitt
  const oneLine = s.replace(/\s+/g, ' ')
  return oneLine.length > 80 ? oneLine.slice(0, 77) + '…' : oneLine
}

function errorDetails(input?: string | null) {
  const s = String(input ?? '').trim()
  if (!s) return ''
  // HTML zu Text strippen, damit es lesbar/kopierbar wird
  const t = stripHtmlToText(s)
  // hart begrenzen, damit es nicht eskaliert
  return t.length > 2000 ? t.slice(0, 2000) + '…' : t
}


function absCbUrl(u?: string | null) {
  if (!u) return ''
  const s = String(u).trim()
  if (!s) return ''
  if (s.startsWith('http://') || s.startsWith('https://')) return s
  if (s.startsWith('/')) return `https://chaturbate.com${s}`
  return `https://chaturbate.com/${s}`
}

function pill(cls: string) {
  return cn(
    'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset',
    cls
  )
}

const previewBlurCls = (blur?: boolean) =>
  blur ? 'blur-md scale-[1.03] brightness-90' : ''

function firstNonEmptyString(...values: unknown[]): string | undefined {
  for (const v of values) {
    if (typeof v === 'string') {
      const s = v.trim()
      if (s) return s
    }
  }
  return undefined
}

function parseJobMeta(metaRaw: unknown): any | null {
  if (!metaRaw) return null
  if (typeof metaRaw === 'string') {
    try {
      return JSON.parse(metaRaw)
    } catch {
      return null
    }
  }
  if (typeof metaRaw === 'object') return metaRaw
  return null
}

function normalizeDurationSeconds(value: unknown): number | undefined {
  if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) return undefined
  // ms -> s Heuristik wie in FinishedVideoPreview
  return value > 24 * 60 * 60 ? value / 1000 : value
}

function clamp(n: number, min: number, max: number) {
  return Math.max(min, Math.min(max, n))
}

const DEFAULT_SPRITE_STEP_SECONDS = 5

function chooseSpriteGrid(count: number): [number, number] {
  if (count <= 1) return [1, 1]

  const targetRatio = 16 / 9
  let bestCols = 1
  let bestRows = count
  let bestWaste = Number.POSITIVE_INFINITY
  let bestRatioScore = Number.POSITIVE_INFINITY

  for (let c = 1; c <= count; c++) {
    const r = Math.max(1, Math.ceil(count / c))
    const waste = c * r - count
    const ratio = c / r
    const ratioScore = Math.abs(ratio - targetRatio)

    if (
      waste < bestWaste ||
      (waste === bestWaste && ratioScore < bestRatioScore) ||
      (waste === bestWaste && ratioScore === bestRatioScore && r < bestRows)
    ) {
      bestWaste = waste
      bestRatioScore = ratioScore
      bestCols = c
      bestRows = r
    }
  }

  return [bestCols, bestRows]
}

// ------ API types (Chaturbate online) ------

type ChaturbateRoom = {
  gender?: string
  location?: string
  country?: string
  current_show?: string
  username?: string
  room_subject?: string
  tags?: string[]
  is_new?: boolean
  num_users?: number
  num_followers?: number
  spoken_languages?: string
  display_name?: string
  birthday?: string
  is_hd?: boolean
  age?: number
  seconds_online?: number
  image_url?: string
  image_url_360x270?: string
  chat_room_url?: string
  chat_room_url_revshare?: string
  iframe_embed?: string
  iframe_embed_revshare?: string
  slug?: string
}

type OnlineResp = {
  enabled?: boolean
  fetchedAt?: string
  count?: number
  lastError?: string
  rooms?: ChaturbateRoom[]
}

// ------ API types (Chaturbate biocontext proxy) ------

type BioPhotoSet = {
  id: number
  name: string
  cover_url?: string
  tokens?: number
  is_video?: boolean
  user_can_access?: boolean
  user_has_purchased?: boolean
  fan_club_only?: boolean
  label_text?: string
  label_color?: string
  video_has_sound?: boolean
  video_ready?: boolean
}

type BioSocial = {
  id: number
  title_name: string
  image_url?: string
  link?: string
  popup_link?: boolean
  tokens?: number
  purchased?: boolean
  label_text?: string
  label_color?: string
}

type BioContext = {
  follower_count?: number
  location?: string
  real_name?: string
  body_decorations?: string
  last_broadcast?: string
  smoke_drink?: string
  body_type?: string
  display_birthday?: string
  about_me?: string
  wish_list?: string
  time_since_last_broadcast?: string
  fan_club_cost?: number
  performer_has_fanclub?: boolean
  fan_club_is_member?: boolean
  fan_club_join_url?: string
  needs_supporter_to_pm?: boolean
  interested_in?: string[]
  display_age?: number
  sex?: string
  subgender?: string
  room_status?: string // offline/online
  photo_sets?: BioPhotoSet[]
  social_medias?: BioSocial[]
  is_broadcaster_or_staff?: boolean
}

type BioResp = {
  enabled?: boolean
  fetchedAt?: string
  lastError?: string
  model?: string
  bio?: BioContext | null
}

// ------ props ------

// ------ API types (local model store) ------
// /api/models liefert StoredModel aus dem models_store
type StoredModel = {
  id: string
  modelKey: string
  tags?: string | null
  lastSeenOnline?: boolean | null
  lastSeenOnlineAt?: string
  favorite?: boolean
  watching?: boolean
  liked?: boolean | null
  hot?: boolean
  keep?: boolean
  createdAt?: string
  updatedAt?: string
  cbOnlineJson?: string | null
  cbOnlineFetchedAt?: string | null
  cbOnlineLastError?: string | null
}

type Props = {
  open: boolean
  modelKey: string | null
  onClose: () => void
  onOpenPlayer?: (job: RecordJob, startAtSec?: number) => void
  cookies?: Record<string, string>
  runningJobs?: RecordJob[]
  blurPreviews?: boolean
  teaserPlayback?: 'still' | 'hover' | 'all'
  teaserAudio?: boolean  
  onToggleWatch?: (job: RecordJob) => void | Promise<void>
  onToggleFavorite?: (job: RecordJob) => void | Promise<void>
  onToggleLike?: (job: RecordJob) => void | Promise<void>
  onToggleHot?: (
    job: RecordJob
    ) =>
      | void
      | { ok?: boolean; oldFile?: string; newFile?: string }
      | Promise<void | { ok?: boolean; oldFile?: string; newFile?: string }>
  onDelete?: (job: RecordJob) => void | Promise<void>
  onKeep?: (job: RecordJob) => void | Promise<void>
  onStopJob?: (id: string) => void | Promise<void>
}

function normalizeModelKey(raw: string | null | undefined): string {
  let s = String(raw ?? '').trim()
  if (!s) return ''

  // falls jemand eine URL reinreicht
  s = s.replace(/^https?:\/\//i, '')

  // falls host/path drin ist -> letzten Segment nehmen
  if (s.includes('/')) {
    const parts = s.split('/').filter(Boolean)
    s = parts[parts.length - 1] || s
  }

  // falls host:model drin ist -> nach ":" nehmen
  if (s.includes(':')) {
    s = s.split(':').pop() || s
  }

  return s.trim().toLowerCase()
}

function buildChaturbateCookieHeader(cookies?: Record<string, string>): string {
  const c = cookies ?? {}

  const cf =
    c['cf_clearance'] ||
    c['cf-clearance'] ||
    c['cfclearance'] ||
    ''

  const sess =
    // ✅ wichtig: deine App nutzt "sessionId"
    c['sessionId'] ||
    c['sessionid'] ||
    c['session_id'] ||
    c['session-id'] ||
    ''

  const parts: string[] = []
  if (cf) parts.push(`cf_clearance=${cf}`)
  if (sess) parts.push(`sessionid=${sess}`) // upstream erwartet sessionid=...
  return parts.join('; ')
}


export default function ModelDetails({
  open,
  modelKey,
  onClose,
  onOpenPlayer,
  cookies,
  runningJobs,
  blurPreviews,
  teaserPlayback = 'hover',
  teaserAudio = false,
  onToggleWatch,
  onToggleFavorite,
  onToggleLike,
  onToggleHot,
  onDelete,
  onKeep,
  onStopJob
}: Props) {
  
  //const isDesktop = useMediaQuery('(min-width: 640px)')

  const [models, setModels] = React.useState<StoredModel[]>([])

  const [room, setRoom] = React.useState<ChaturbateRoom | null>(null)
  const [roomMeta, setRoomMeta] = React.useState<Pick<OnlineResp, 'enabled' | 'fetchedAt' | 'lastError'> | null>(null)

  const [bio, setBio] = React.useState<BioContext | null>(null)
  const [bioMeta, setBioMeta] = React.useState<Pick<BioResp, 'enabled' | 'fetchedAt' | 'lastError'> | null>(null)
  const [bioLoading, setBioLoading] = React.useState(false)

  const [done, setDone] = React.useState<RecordJob[]>([])
  const [doneLoading, setDoneLoading] = React.useState(false)

  const [running, setRunning] = React.useState<RecordJob[]>([])
  const [runningLoading, setRunningLoading] = React.useState(false)

  const runningReqSeqRef = React.useRef(0)

  const [, setBioRefreshSeq] = React.useState(0)

  const [imgViewer, setImgViewer] = React.useState<{ src: string; alt?: string } | null>(null)

  const [, setRunningHover] = React.useState(false)

  const [stopPending, setStopPending] = React.useState(false)

  const [doneTotalCount, setDoneTotalCount] = React.useState(0)
  
  const [donePage, setDonePage] = React.useState(1)
  const DONE_PAGE_SIZE = 4

  const key = normalizeModelKey(modelKey)

  type TabKey = 'info' | 'downloads' | 'running'
  const [tab, setTab] = React.useState<TabKey>('info')

  const bioReqRef = React.useRef<AbortController | null>(null)
  

    // ===== Gallery UI State (wie FinishedDownloadsGalleryView) =====
  const [durations, setDurations] = React.useState<Record<string, number>>({})
  const [hoverTeaserKey, setHoverTeaserKey] = React.useState<string | null>(null)
  const [teaserKey, setTeaserKey] = React.useState<string | null>(null)

  const [hoveredModelPreviewKey, setHoveredModelPreviewKey] = React.useState<string | null>(null)
  const [scrubIndexByKey, setScrubIndexByKey] = React.useState<Record<string, number | undefined>>({})

  const [hoveredThumbKey, setHoveredThumbKey] = React.useState<string | null>(null)

  const lower = React.useCallback((s: string) => String(s ?? '').toLowerCase(), [])

  const deletingKeys = React.useMemo(() => new Set<string>(), [])
  const keepingKeys = React.useMemo(() => new Set<string>(), [])
  const removingKeys = React.useMemo(() => new Set<string>(), [])
  const deletedKeys = React.useMemo(() => new Set<string>(), [])

  // ✅ 1) Beim Öffnen sofort aus Cache rendern
  React.useEffect(() => {
    if (!open || !key) return

    // Online
    const memOnline = mdOnlineMem.get(key)
    const ssOnline = ssGet<OnlineCacheEntry>(ssKeyOnline(key))

    const onlineHit =
      (memOnline && isFresh(memOnline.at) ? memOnline : null) ||
      (ssOnline && isFresh(ssOnline.at) ? ssOnline : null)

    if (onlineHit) {
      setRoom(onlineHit.room ?? null)
      setRoomMeta(onlineHit.meta ?? null)
    }

    // Bio
    const memBio = mdBioMem.get(key)
    const ssBio = ssGet<BioCacheEntry>(ssKeyBio(key))

    const bioHit =
      (memBio && isFresh(memBio.at) ? memBio : null) ||
      (ssBio && isFresh(ssBio.at) ? ssBio : null)

    if (bioHit) {
      setBio(bioHit.bio ?? null)
      setBioMeta(bioHit.meta ?? null)
      // bioLoading NICHT anfassen – Fetch kann trotzdem laufen
    }
  }, [open, key])

  React.useEffect(() => {
    if (!open) return
    setStopPending(false)
  }, [open, key])

  const refreshBio = React.useCallback(async () => {
    if (!key) return

    // vorherigen abbrechen
    bioReqRef.current?.abort()
    const ac = new AbortController()
    bioReqRef.current = ac

    setBioLoading(true)
    try {
      const cookieHeader = buildChaturbateCookieHeader(cookies)
      const url = `/api/chaturbate/biocontext?model=${encodeURIComponent(key)}&refresh=1`

      const r = await fetch(url, {
        cache: 'no-store',
        signal: ac.signal,
        headers: cookieHeader ? { 'X-Chaturbate-Cookie': cookieHeader } : undefined,
      })

      if (!r.ok) {
        const text = await r.text().catch(() => '')
        throw new Error(text || `HTTP ${r.status}`)
      }

      const data = (await r.json().catch(() => null)) as BioResp
      const meta = { enabled: data?.enabled, fetchedAt: data?.fetchedAt, lastError: data?.lastError }
      const nextBio = (data?.bio as BioContext) ?? null

      setBioMeta(meta)
      setBio(nextBio)

      const entry: BioCacheEntry = { at: Date.now(), bio: nextBio, meta }
      mdBioMem.set(key, entry)
      ssSet(ssKeyBio(key), entry)
    } catch (e: any) {
      if (e?.name === 'AbortError') return
      setBioMeta({ enabled: undefined, fetchedAt: undefined, lastError: e?.message || 'Fetch fehlgeschlagen' })
    } finally {
      setBioLoading(false)
    }
  }, [key, cookies])

  React.useEffect(() => {
    if (open) return
    bioReqRef.current?.abort()
    bioReqRef.current = null
  }, [open])

  const refetchModels = React.useCallback(async () => {
    try {
      const r = await fetch('/api/models', { cache: 'no-store' })
      const data = (await r.json().catch(() => null)) as any
      setModels(Array.isArray(data) ? data : [])
    } catch {
      // ignore
    }
  }, [])

  const refetchDone = React.useCallback(async () => {
    if (!key) return
    setDoneLoading(true)
    try {
      const url =
        `/api/record/done?model=${encodeURIComponent(key)}` +
        `&page=${donePage}&pageSize=${DONE_PAGE_SIZE}` +
        `&sort=completed_desc&includeKeep=1&withCount=1`

      const r = await fetch(url, { cache: 'no-store' })
      const data = await r.json().catch(() => null)

      const items = Array.isArray(data?.items) ? (data.items as RecordJob[]) : []
      const count = Number(data?.count ?? items.length)

      setDone(items)
      setDoneTotalCount(Number.isFinite(count) ? count : items.length)
    } catch {
      // ignore
    } finally {
      setDoneLoading(false)
    }
  }, [key, donePage])

  const refetchDoneRef = React.useRef(refetchDone)
  
  React.useEffect(() => {
    refetchDoneRef.current = refetchDone
  }, [refetchDone])

  React.useEffect(() => {
    if (!open) return

    const es = new EventSource('/api/stream')

    const onJobs = () => {
      // optional
    }

    const onDone = () => {
      void refetchDoneRef.current()
    }

    es.addEventListener('jobs', onJobs)
    es.addEventListener('doneChanged', onDone)

    return () => {
      es.removeEventListener('jobs', onJobs)
      es.removeEventListener('doneChanged', onDone)
      es.close()
    }
  }, [open])

  // erzeugt ein "Job"-Objekt, das für deine Toggle-Handler reicht
  function jobFromModelKey(key: string): RecordJob {
    // muss zum Regex in App.tsx passen: <model>_MM_DD_YYYY__HH-MM-SS.ext
    return {
      id: `model:${key}`,
      output: `${key}_01_01_2000__00-00-00.mp4`,
      status: 'finished',
    } as any
  }

  const openImage = React.useCallback((src?: string | null, alt?: string) => {
    const s = String(src ?? '').trim()
    if (!s) return
    setImgViewer({ src: s, alt })
  }, [])

  // wenn Modal zu geht, wieder "normal" (kein force refresh)
  React.useEffect(() => {
    if (!open) setBioRefreshSeq(0)
  }, [open])

  const runningList = React.useMemo(() => {
    return Array.isArray(runningJobs) ? runningJobs : running
  }, [runningJobs, running])

  React.useEffect(() => {
    if (!open) return
    setDonePage(1)
  }, [open, modelKey])

  React.useEffect(() => {
    if (!open) return
    void refetchModels()
  }, [open, refetchModels])

  // Done downloads (inkl. keep/<model>/) -> serverseitig paginiert laden
  React.useEffect(() => {
    if (!open || !key) return
    void refetchDone()
  }, [open, key, refetchDone])

  // Running jobs
  React.useEffect(() => {
    if (!open) return
    if (Array.isArray(runningJobs)) return

    const ac = new AbortController()
    const seq = ++runningReqSeqRef.current

    setRunningLoading(true)

    fetch('/api/record/jobs', { cache: 'no-store', signal: ac.signal })
      .then((r) => r.json())
      .then((data: RecordJob[]) => {
        if (ac.signal.aborted) return
        if (runningReqSeqRef.current !== seq) return
        setRunning(Array.isArray(data) ? data : [])
      })
      .catch(() => {
        if (ac.signal.aborted) return
        if (runningReqSeqRef.current !== seq) return
        setRunning([])
      })
      .finally(() => {
        if (ac.signal.aborted) return
        if (runningReqSeqRef.current !== seq) return
        setRunningLoading(false)
      })

    return () => {
      ac.abort()
    }
  }, [open, runningJobs])

  const model = React.useMemo(() => {
    if (!key) return null
    return models.find((m) => (m.modelKey || '').toLowerCase() === key) ?? null
  }, [models, key])

  const storedRoomFromSnap = React.useMemo<ChaturbateRoom | null>(() => {
    const raw = (model as any)?.cbOnlineJson
    if (!raw || typeof raw !== 'string') return null
    try {
      return JSON.parse(raw) as ChaturbateRoom
    } catch {
      return null
    }
  }, [model])

  const storedRoomMeta = React.useMemo(() => {
    const fetchedAt = (model as any)?.cbOnlineFetchedAt
    const lastError = (model as any)?.cbOnlineLastError
    if (!fetchedAt && !lastError) return null
    return { enabled: true, fetchedAt, lastError } as Pick<OnlineResp, 'enabled' | 'fetchedAt' | 'lastError'>
  }, [model])

  const effectiveRoom = room ?? storedRoomFromSnap
  const effectiveRoomMeta = roomMeta ?? storedRoomMeta

  const doneMatches = done

  const runningMatches = React.useMemo(() => {
    if (!key) return []
    return runningList.filter((j) => {
      const m = modelNameFromOutput(j.output)
      return m !== '—' && m.trim().toLowerCase() === key
    })
  }, [runningList, key])

  const titleName = effectiveRoom?.display_name || model?.modelKey || key || 'Model'
  const heroImg = effectiveRoom?.image_url_360x270 || effectiveRoom?.image_url || ''
  const heroImgFull = effectiveRoom?.image_url || heroImg
  const roomUrl = effectiveRoom?.chat_room_url_revshare || effectiveRoom?.chat_room_url || ''

  const showLabel = (effectiveRoom?.current_show || '').trim().toLowerCase()
  const showPill = showLabel
    ? showLabel === 'public'
      ? 'Public'
      : showLabel === 'private'
        ? 'Private'
        : showLabel
    : ''

  const bioLocation = (bio?.location || '').trim()
  const bioFollowers = bio?.follower_count
  const bioAge = bio?.display_age
  const bioStatus = (bio?.room_status || '').trim()
  const bioLast = bio?.last_broadcast ? fmtDateTime(bio.last_broadcast) : '—'
  const about = stripHtmlToText(bio?.about_me)
  const wish = stripHtmlToText(bio?.wish_list)

  const storedPresenceLabel =
    model?.lastSeenOnline == null ? '' : model.lastSeenOnline ? 'online' : 'offline'

  const effectivePresenceLabel = (bioStatus || showLabel || storedPresenceLabel || '').trim()

  const socials = Array.isArray(bio?.social_medias) ? bio!.social_medias! : []
  const photos = Array.isArray(bio?.photo_sets) ? bio!.photo_sets! : []
  const interested = Array.isArray(bio?.interested_in) ? bio!.interested_in! : []

  const allTags = React.useMemo(() => {
    const a = splitTags(model?.tags)
    const b = Array.isArray(effectiveRoom?.tags) ? (effectiveRoom!.tags as string[]) : []
    const map = new Map<string, string>()
    for (const t of [...a, ...b]) {
      const k = String(t).trim().toLowerCase()
      if (!k) continue
      if (!map.has(k)) map.set(k, String(t).trim())
    }
    return Array.from(map.values()).sort((x, y) => x.localeCompare(y, 'de'))
  }, [model?.tags, effectiveRoom?.tags])

  const Stat = ({
    icon,
    label,
    value,
  }: {
    icon: React.ReactNode
    label: string
    value: React.ReactNode
  }) => (
    <div className="rounded-lg border border-gray-200/70 bg-white/60 px-2.5 py-2 sm:px-3 dark:border-white/10 dark:bg-white/5">
      <div className="flex items-center gap-1.5 text-[11px] sm:text-xs text-gray-600 dark:text-gray-300">
        {icon}
        {label}
      </div>
      <div className="mt-0.5 text-sm font-semibold text-gray-900 dark:text-white">{value}</div>
    </div>
  )

  const keyFor = React.useCallback((j: RecordJob) => {
    // stabil, auch wenn id fehlt
    const id = String((j as any)?.id ?? '')
    const out = String(j.output ?? '')
    return id ? `${id}::${out}` : out
  }, [])

  const handleToggleHot = React.useCallback(
    async (job: RecordJob) => {
      const out = job.output || ''
      const oldFile = baseName(out)
      if (!oldFile) {
        await onToggleHot?.(job)
        return
      }

      const newFile = toggleHotFileName(oldFile)

      // ✅ 1) UI sofort updaten (optimistisch)
      setDone((prev) =>
        prev.map((j) => {
          const same =
            (j.id && job.id && j.id === job.id) ||
            (j.output && job.output && j.output === job.output)

          if (!same) return j
          return { ...j, output: replaceBaseName(j.output || '', newFile) }
        })
      )

      // ✅ 2) Backend/App Handler
      await onToggleHot?.(job)

      // ✅ 3) Server-Truth nachziehen (falls Backend anders renamed)
      refetchDone()
    },
    [onToggleHot, refetchDone]
  )

  const handleScrubberClickIndex = React.useCallback(
    (job: RecordJob, segmentIndex: number, _segmentCount: number) => {
      const metaRaw = (job as any)?.meta
      const meta = typeof metaRaw === 'string' ? (() => { try { return JSON.parse(metaRaw) } catch { return null } })() : metaRaw

      const step =
        typeof meta?.previewSprite?.stepSeconds === 'number' && Number.isFinite(meta.previewSprite.stepSeconds) && meta.previewSprite.stepSeconds > 0
          ? meta.previewSprite.stepSeconds
          : 5

      const startAtSec = Math.max(0, Math.floor(segmentIndex) * step)
      onOpenPlayer?.(job, startAtSec)
    },
    [onOpenPlayer]
  )

  const handleToggleFavoriteModel = React.useCallback(async () => {
    if (!key) return

    // ✅ UI sofort (optimistisch)
    setModels((prev) =>
      prev.map((m) =>
        (m.modelKey || '').toLowerCase() === key
          ? { ...m, favorite: !Boolean(m.favorite) }
          : m
      )
    )

    // ✅ Backend/App Handler
    await onToggleFavorite?.(jobFromModelKey(key))

    // ✅ Server-Truth nachziehen
    refetchModels()
  }, [key, onToggleFavorite, refetchModels])

  const handleToggleLikeModel = React.useCallback(async () => {
    if (!key) return

    // liked ist bei dir boolean | null -> wir togglen "true <-> false"
    setModels((prev) =>
      prev.map((m) =>
        (m.modelKey || '').toLowerCase() === key
          ? { ...m, liked: m.liked === true ? false : true }
          : m
      )
    )

    await onToggleLike?.(jobFromModelKey(key))
    refetchModels()
  }, [key, onToggleLike, refetchModels])

  const handleToggleWatchModel = React.useCallback(async () => {
    if (!key) return

    // ✅ UI sofort (optimistisch)
    setModels((prev) =>
      prev.map((m) =>
        (m.modelKey || '').toLowerCase() === key
          ? { ...m, watching: !Boolean(m.watching) }
          : m
      )
    )

    // ✅ Backend/App Handler
    await onToggleWatch?.(jobFromModelKey(key))

    // ✅ Server-Truth nachziehen
    refetchModels()
  }, [key, onToggleWatch, refetchModels])

  React.useEffect(() => {
    if (!open) {
      setHoverTeaserKey(null)
      setTeaserKey(null)
      setHoveredModelPreviewKey(null)
      setHoveredThumbKey(null) 
      setScrubIndexByKey({})
      setDurations({})
    }
  }, [open, key])

  const handleDuration = React.useCallback(
    (job: RecordJob, seconds: number) => {
      const k = keyFor(job)
      setDurations((prev) => (prev[k] === seconds ? prev : { ...prev, [k]: seconds }))
    },
    [keyFor]
  )

  const handleHoverPreviewKeyChange = React.useCallback(
    (k: string | null) => {
      setHoverTeaserKey(k)
      if (teaserPlayback === 'hover') setTeaserKey(k)
    },
    [teaserPlayback]
  )

  const runtimeOf = React.useCallback(
    (job: RecordJob) => {
      const k = keyFor(job)
      const raw =
        (job as any)?.durationSeconds ??
        (job as any)?.meta?.durationSeconds ??
        durations[k]

      const n =
        typeof raw === 'number' && Number.isFinite(raw)
          ? raw > 24 * 60 * 60
            ? raw / 1000
            : raw
          : null

      return fmtHms(n)
    },
    [durations, keyFor]
  )

  const sizeBytesOf = React.useCallback((job: RecordJob) => {
    const v =
      (job as any)?.sizeBytes ??
      (job as any)?.size ??
      (job as any)?.meta?.sizeBytes ??
      (job as any)?.meta?.size
    return typeof v === 'number' && Number.isFinite(v) ? v : null
  }, [])

  const formatBytes = React.useCallback((bytes?: number | null) => fmtBytes(bytes ?? null), [])

  const setScrubIndexForKey = React.useCallback((key: string, index: number | undefined) => {
    setScrubIndexByKey((prev) => {
      if (index === undefined) {
        if (!(key in prev)) return prev
        const next = { ...prev }
        delete next[key]
        return next
      }
      if (prev[key] === index) return prev
      return { ...prev, [key]: index }
    })
  }, [])

  const clearScrubIndex = React.useCallback(
    (key: string) => setScrubIndexForKey(key, undefined),
    [setScrubIndexForKey]
  )

  React.useEffect(() => {
    if (!open) return
    setTab('info')
  }, [open, key])

  const tabs: TabItem[] = [
    { id: 'info', label: 'Info' },
    { id: 'downloads', label: 'Downloads', count: doneTotalCount ? fmtInt(doneTotalCount) : undefined, disabled: doneLoading },
    { id: 'running', label: 'Running', count: runningMatches.length ? fmtInt(runningMatches.length) : undefined, disabled: runningLoading },
  ]

  return (
    <Modal
      open={open}
      onClose={onClose}
      title={titleName}
      width="max-w-6xl"
      layout="split"
      scroll="right"
      leftWidthClass="lg:w-[320px]"
      mobileCollapsedImageSrc={heroImg || undefined}
      mobileCollapsedImageAlt={titleName}
      rightBodyClassName="pt-0 sm:pt-2"
      titleRight={
        tab === 'info' ? (
          <Button
            variant="secondary"
            className={cn('h-8 px-2 sm:h-9 sm:px-3', 'whitespace-nowrap')}
            disabled={bioLoading || !modelKey}
            onClick={() => void refreshBio()}
            title="BioContext neu abrufen"
          >
            <span className="inline-flex items-center gap-2">
              <ArrowPathIcon className={cn('size-4', bioLoading ? 'animate-spin' : '')} />
              <span className="hidden sm:inline">Bio aktualisieren</span>
            </span>
          </Button>
        ) : null
      }
      left={
        <div className="space-y-3 sm:space-y-4">

          {/* ===================== */}
          {/* DESKTOP (sm:block)    */}
          {/* ===================== */}
          <div className="hidden sm:block space-y-2">
            <div className="overflow-hidden rounded-lg border border-gray-200/70 bg-white/70 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5">
              {/* Image */}
              <div className="relative">
                {heroImg ? (
                  <button
                    type="button"
                    className="block w-full cursor-zoom-in"
                    onClick={() => openImage(heroImgFull, titleName)}
                    aria-label="Bild vergrößern"
                  >
                    <img
                      src={heroImg}
                      alt={titleName}
                      className={cn('h-24 sm:h-52 w-full object-cover transition', previewBlurCls(blurPreviews))}
                    />
                  </button>
                ) : (
                  <div className="h-24 sm:h-52 w-full bg-gradient-to-br from-indigo-500/10 via-transparent to-sky-500/10" />
                )}

                <div
                  aria-hidden
                  className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/40 via-black/0 to-black/0"
                />

                {/* Pills */}
                <div className="absolute left-3 top-3 flex flex-wrap items-center gap-2">
                  {showPill ? (
                    <span
                      className={pill(
                        'bg-emerald-500/10 text-emerald-900 ring-emerald-200 backdrop-blur dark:text-emerald-200 dark:ring-emerald-400/20'
                      )}
                    >
                      {showPill}
                    </span>
                  ) : null}
                  
                  {effectivePresenceLabel ? (
                    <span
                      className={pill(
                        effectivePresenceLabel.toLowerCase() === 'online'
                          ? 'bg-emerald-500/10 text-emerald-900 ring-emerald-200 backdrop-blur dark:text-emerald-200 dark:ring-emerald-400/20'
                          : 'bg-gray-500/10 text-gray-900 ring-gray-200 backdrop-blur dark:text-gray-200 dark:ring-white/15'
                      )}
                    >
                      {effectivePresenceLabel}
                    </span>
                  ) : null}

                  {effectiveRoom?.is_hd ? (
                    <span
                      className={pill(
                        'bg-indigo-500/10 text-indigo-900 ring-indigo-200 backdrop-blur dark:text-indigo-200 dark:ring-indigo-400/20'
                      )}
                    >
                      HD
                    </span>
                  ) : null}

                  {effectiveRoom?.is_new ? (
                    <span
                      className={pill(
                        'bg-amber-500/10 text-amber-900 ring-amber-200 backdrop-blur dark:text-amber-200 dark:ring-amber-400/20'
                      )}
                    >
                      NEW
                    </span>
                  ) : null}
                </div>

                {/* Title */}
                <div className="absolute bottom-3 left-3 right-3">
                  <div className="truncate text-sm font-semibold text-white drop-shadow">
                    {effectiveRoom?.display_name || effectiveRoom?.username || model?.modelKey || key || '—'}
                  </div>
                  <div className="truncate text-xs text-white/85 drop-shadow">
                    {effectiveRoom?.username ? `@${effectiveRoom.username}` : model?.modelKey ? `@${model.modelKey}` : ''}
                  </div>
                </div>

                {/* Local flags icons */}
                <div className="absolute bottom-3 right-3 flex items-center gap-2">
                  {/* Watched */}
                  <button
                    type="button"
                    onClick={(e) => {
                      e.preventDefault()
                      e.stopPropagation()
                      handleToggleWatchModel()
                    }}
                    className={cn(
                      'inline-flex items-center justify-center rounded-full p-1.5 ring-1 ring-inset backdrop-blur',
                      'cursor-pointer hover:scale-[1.03] active:scale-[0.98] transition',
                      model?.watching ? 'bg-sky-500/25 ring-sky-200/30' : 'bg-black/20 ring-white/15'
                    )}
                    title={model?.watching ? 'Watch entfernen' : 'Auf Watch setzen'}
                    aria-pressed={Boolean(model?.watching)}
                    aria-label={model?.watching ? 'Watch entfernen' : 'Auf Watch setzen'}
                  >
                    <span className="relative inline-block size-4">
                      <EyeOutlineIcon
                        className={cn(
                          'absolute inset-0 size-4 transition-all duration-200 ease-out motion-reduce:transition-none',
                          model?.watching ? 'opacity-0 scale-75 rotate-12' : 'opacity-100 scale-100 rotate-0',
                          'text-white/70'
                        )}
                      />
                      <EyeSolidIcon
                        className={cn(
                          'absolute inset-0 size-4 transition-all duration-200 ease-out motion-reduce:transition-none',
                          model?.watching ? 'opacity-100 scale-110 rotate-0' : 'opacity-0 scale-75 -rotate-12',
                          'text-sky-200'
                        )}
                      />
                    </span>
                  </button>

                  {/* Favorite */}
                  <button
                    type="button"
                    onClick={(e) => {
                      e.preventDefault()
                      e.stopPropagation()
                      handleToggleFavoriteModel()
                    }}
                    className={cn(
                      'inline-flex items-center justify-center rounded-full p-1.5 ring-1 ring-inset backdrop-blur',
                      'cursor-pointer hover:scale-[1.03] active:scale-[0.98] transition',
                      model?.favorite ? 'bg-amber-500/25 ring-amber-200/30' : 'bg-black/20 ring-white/15'
                    )}
                    title={model?.favorite ? 'Favorit entfernen' : 'Als Favorit markieren'}
                    aria-pressed={Boolean(model?.favorite)}
                    aria-label={model?.favorite ? 'Favorit entfernen' : 'Als Favorit markieren'}
                  >
                    <span className="relative inline-block size-4">
                      <StarOutlineIcon
                        className={cn(
                          'absolute inset-0 size-4 transition-all duration-200 ease-out motion-reduce:transition-none',
                          model?.favorite ? 'opacity-0 scale-75 rotate-12' : 'opacity-100 scale-100 rotate-0',
                          'text-white/70'
                        )}
                      />
                      <StarSolidIcon
                        className={cn(
                          'absolute inset-0 size-4 transition-all duration-200 ease-out motion-reduce:transition-none',
                          model?.favorite ? 'opacity-100 scale-110 rotate-0' : 'opacity-0 scale-75 -rotate-12',
                          'text-amber-200'
                        )}
                      />
                    </span>
                  </button>

                  {/* Like */}
                  <button
                    type="button"
                    onClick={(e) => {
                      e.preventDefault()
                      e.stopPropagation()
                      handleToggleLikeModel()
                    }}
                    className={cn(
                      'inline-flex items-center justify-center rounded-full p-1.5 ring-1 ring-inset backdrop-blur',
                      'cursor-pointer hover:scale-[1.03] active:scale-[0.98] transition',
                      model?.liked ? 'bg-rose-500/25 ring-rose-200/30' : 'bg-black/20 ring-white/15'
                    )}
                    title={model?.liked ? 'Like entfernen' : 'Liken'}
                    aria-pressed={model?.liked === true}
                    aria-label={model?.liked ? 'Like entfernen' : 'Liken'}
                  >
                    <span className="relative inline-block size-4">
                      <HeartOutlineIcon
                        className={cn(
                          'absolute inset-0 size-4 transition-all duration-200 ease-out motion-reduce:transition-none',
                          model?.liked ? 'opacity-0 scale-75 rotate-12' : 'opacity-100 scale-100 rotate-0',
                          'text-white/70'
                        )}
                      />
                      <HeartSolidIcon
                        className={cn(
                          'absolute inset-0 size-4 transition-all duration-200 ease-out motion-reduce:transition-none',
                          model?.liked ? 'opacity-100 scale-110 rotate-0' : 'opacity-0 scale-75 -rotate-12',
                          'text-rose-200'
                        )}
                      />
                    </span>
                  </button>
                </div>
              </div>

              {/* Summary */}
              <div className="p-3 sm:p-4">
                <div className="grid grid-cols-2 gap-2 sm:gap-3">
                  <Stat icon={<UsersIcon className="size-4" />} label="Viewer" value={fmtInt(effectiveRoom?.num_users)} />
                  <Stat
                    icon={<SparklesIcon className="size-4" />}
                    label="Follower"
                    value={fmtInt(effectiveRoom?.num_followers ?? bioFollowers)}
                  />
                </div>

                <dl className="mt-2 grid grid-cols-2 gap-2 text-xs">
                  <div className="rounded-lg border border-gray-200/60 bg-white/50 px-2.5 py-2 dark:border-white/10 dark:bg-white/5">
                    <dt className="flex items-center gap-1 text-gray-600 dark:text-gray-300">
                      <MapPinIcon className="size-4" />
                      Location
                    </dt>
                    <dd className="mt-1 text-[13px] font-semibold leading-snug text-gray-900 dark:text-white break-words">
                      {effectiveRoom?.location || bioLocation || '—'}
                    </dd>
                  </div>

                  <div className="rounded-lg border border-gray-200/60 bg-white/50 px-2.5 py-2 dark:border-white/10 dark:bg-white/5">
                    <dt className="flex items-center gap-1 text-gray-600 dark:text-gray-300">
                      <LanguageIcon className="size-4" />
                      Sprache
                    </dt>
                    <dd className="mt-1 text-[13px] font-semibold leading-snug text-gray-900 dark:text-white break-words">
                      {effectiveRoom?.spoken_languages || '—'}
                    </dd>
                  </div>

                  <div className="rounded-lg border border-gray-200/60 bg-white/50 px-2.5 py-2 dark:border-white/10 dark:bg-white/5">
                    <dt className="flex items-center gap-1 text-gray-600 dark:text-gray-300">
                      <CalendarDaysIcon className="size-4" />
                      Online
                    </dt>
                    <dd className="mt-1 text-[13px] font-semibold leading-snug text-gray-900 dark:text-white break-words">
                      {fmtHms(effectiveRoom?.seconds_online)}
                    </dd>
                  </div>

                  <div className="rounded-lg border border-gray-200/60 bg-white/50 px-2.5 py-2 dark:border-white/10 dark:bg-white/5">
                    <dt className="flex items-center gap-1 text-gray-600 dark:text-gray-300">
                      <HeartOutlineIcon className="size-4" />
                      Alter
                    </dt>
                    <dd className="mt-1 text-[13px] font-semibold leading-snug text-gray-900 dark:text-white break-words">
                      {bioAge != null ? String(bioAge) : effectiveRoom?.age != null ? String(effectiveRoom.age) : '—'}
                    </dd>
                  </div>

                  <div className="rounded-lg border border-gray-200/60 bg-white/50 px-2.5 py-2 dark:border-white/10 dark:bg-white/5">
                    <dt className="flex items-center gap-1 text-gray-600 dark:text-gray-300">
                      <CalendarDaysIcon className="size-4" />
                      Last broadcast
                    </dt>
                    <dd className="mt-1 text-[13px] font-semibold leading-snug text-gray-900 dark:text-white break-words">
                      {bioLast}
                    </dd>
                  </div>
                </dl>

                {/* Meta warnings */}
                {effectiveRoomMeta?.enabled === false ? (
                  <div className="mt-2 rounded-lg border border-amber-200/60 bg-amber-50/70 px-3 py-2 text-xs text-amber-900 dark:border-amber-400/20 dark:bg-amber-400/10 dark:text-amber-200">
                    Chaturbate-Online ist aktuell deaktiviert.
                  </div>
                ) : effectiveRoomMeta?.lastError ? (
                  <div className="mt-2 rounded-lg border border-rose-200/60 bg-rose-50/70 px-3 py-2 text-xs text-rose-900 dark:border-rose-400/20 dark:bg-rose-400/10 dark:text-rose-200">
                    <div className="font-medium">Online-Info: {errorSummary(effectiveRoomMeta.lastError)}</div>

                    <details className="mt-1">
                      <summary className="cursor-pointer select-none text-[11px] text-rose-900/80 underline underline-offset-2 dark:text-rose-200/80">
                        Details
                      </summary>
                      <pre className="mt-1 max-h-28 overflow-auto whitespace-pre-wrap break-words rounded-md bg-black/5 p-2 text-[11px] leading-snug dark:bg-white/10">
                        {errorDetails(effectiveRoomMeta.lastError)}
                      </pre>
                    </details>
                  </div>
                ) : null}

                {bioMeta?.enabled === false ? (
                  <div className="mt-2 rounded-lg border border-amber-200/60 bg-amber-50/70 px-3 py-2 text-xs text-amber-900 dark:border-amber-400/20 dark:bg-amber-400/10 dark:text-amber-200">
                    BioContext ist aktuell deaktiviert.
                  </div>
                ) : bioMeta?.lastError ? (
                  <div className="mt-2 rounded-lg border border-rose-200/60 bg-rose-50/70 px-3 py-2 text-xs text-rose-900 dark:border-rose-400/20 dark:bg-rose-400/10 dark:text-rose-200">
                    BioContext: {bioMeta.lastError}
                  </div>
                ) : null}
              </div>
            </div>

            {/* Tags (desktop) */}
            <div className="rounded-lg border border-gray-200/70 bg-white/70 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5">
              <div className="text-sm font-semibold text-gray-900 dark:text-white">Tags</div>
              {allTags.length ? (
                <div className="mt-2 flex flex-wrap gap-2">
                  {allTags.map((t) => (
                    <TagBadge key={t} tag={t} />
                  ))}
                </div>
              ) : (
                <div className="mt-2 text-sm text-gray-600 dark:text-gray-300">Keine Tags vorhanden.</div>
              )}
            </div>
          </div>
        </div>
      }
      rightHeader={
        <div
          className={cn(
            'border-b border-gray-200/70 dark:border-white/10',
            'bg-white/95 backdrop-blur dark:bg-gray-800/95'
          )}
        >
          {/* ===================== */}
          {/* ✅ MOBILE: Header + Tags immer sichtbar (über Tabs) */}
          {/* ===================== */}
          <div className="sm:hidden px-2 pb-2 space-y-1.5">
            {/* HERO Header (mobile) */}
            <div className="rounded-lg border border-gray-200/70 bg-white/70 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5 overflow-hidden">
              {/* Hero Background */}
              <div className="relative h-40">
                {heroImg ? (
                  <button
                    type="button"
                    className="absolute inset-0 block w-full"
                    onClick={() => (heroImgFull ? openImage(heroImgFull, titleName) : undefined)}
                    aria-label="Bild vergrößern"
                  >
                    <img
                      src={heroImgFull || heroImg}
                      alt={titleName}
                      className={cn('h-full w-full object-cover', previewBlurCls(blurPreviews))}
                    />
                  </button>
                ) : (
                  <div className="absolute inset-0 bg-gradient-to-br from-indigo-500/10 via-transparent to-sky-500/10" />
                )}

                {/* Gradient overlay */}
                <div
                  aria-hidden
                  className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-black/0"
                />

                {/* Top row: name + action icons */}
                <div className="absolute left-3 right-3 top-3 flex items-start justify-between gap-2">
                  <div className="min-w-0">
                    <div className="truncate text-base font-semibold text-white drop-shadow">
                      {effectiveRoom?.display_name || effectiveRoom?.username || model?.modelKey || key || '—'}
                    </div>
                    <div className="truncate text-xs text-white/85 drop-shadow">
                      {effectiveRoom?.username ? `@${effectiveRoom.username}` : model?.modelKey ? `@${model.modelKey}` : ''}
                    </div>
                  </div>

                  <div className="shrink-0 flex items-center gap-1.5">
                    {/* Watched */}
                    <button
                      type="button"
                      onClick={(e) => {
                        e.preventDefault()
                        e.stopPropagation()
                        handleToggleWatchModel()
                      }}
                      className={cn(
                        'inline-flex items-center justify-center rounded-full p-1.5 ring-1 ring-inset backdrop-blur',
                        'transition active:scale-[0.98]',
                        model?.watching ? 'bg-sky-500/25 ring-sky-200/30' : 'bg-black/30 ring-white/20'
                      )}
                      title={model?.watching ? 'Watch entfernen' : 'Auf Watch setzen'}
                      aria-pressed={Boolean(model?.watching)}
                      aria-label={model?.watching ? 'Watch entfernen' : 'Auf Watch setzen'}
                    >
                      <span className="relative inline-block size-4">
                        <EyeOutlineIcon className={cn('absolute inset-0 size-4', 'text-white/70')} />
                        <EyeSolidIcon className={cn('absolute inset-0 size-4', model?.watching ? 'opacity-100' : 'opacity-0', 'text-sky-200')} />
                      </span>
                    </button>

                    {/* Favorite */}
                    <button
                      type="button"
                      onClick={(e) => {
                        e.preventDefault()
                        e.stopPropagation()
                        handleToggleFavoriteModel()
                      }}
                      className={cn(
                        'inline-flex items-center justify-center rounded-full p-1.5 ring-1 ring-inset backdrop-blur',
                        'transition active:scale-[0.98]',
                        model?.favorite ? 'bg-amber-500/25 ring-amber-200/30' : 'bg-black/30 ring-white/20'
                      )}
                      title={model?.favorite ? 'Favorit entfernen' : 'Als Favorit markieren'}
                      aria-pressed={Boolean(model?.favorite)}
                      aria-label={model?.favorite ? 'Favorit entfernen' : 'Als Favorit markieren'}
                    >
                      <span className="relative inline-block size-4">
                        <StarOutlineIcon className={cn('absolute inset-0 size-4', 'text-white/70')} />
                        <StarSolidIcon className={cn('absolute inset-0 size-4', model?.favorite ? 'opacity-100' : 'opacity-0', 'text-amber-200')} />
                      </span>
                    </button>

                    {/* Like */}
                    <button
                      type="button"
                      onClick={(e) => {
                        e.preventDefault()
                        e.stopPropagation()
                        handleToggleLikeModel()
                      }}
                      className={cn(
                        'inline-flex items-center justify-center rounded-full p-1.5 ring-1 ring-inset backdrop-blur',
                        'transition active:scale-[0.98]',
                        model?.liked ? 'bg-rose-500/25 ring-rose-200/30' : 'bg-black/30 ring-white/20'
                      )}
                      title={model?.liked ? 'Like entfernen' : 'Liken'}
                      aria-pressed={model?.liked === true}
                      aria-label={model?.liked ? 'Like entfernen' : 'Liken'}
                    >
                      <span className="relative inline-block size-4">
                        <HeartOutlineIcon className={cn('absolute inset-0 size-4', 'text-white/70')} />
                        <HeartSolidIcon className={cn('absolute inset-0 size-4', model?.liked ? 'opacity-100' : 'opacity-0', 'text-rose-200')} />
                      </span>
                    </button>
                  </div>
                </div>

                {/* Pills bottom-left */}
                <div className="absolute left-3 bottom-3 flex flex-wrap items-center gap-1.5">
                  {showPill ? (
                    <span className={pill('bg-white/15 text-white ring-white/20')}>
                      {showPill}
                    </span>
                  ) : null}

                  {effectivePresenceLabel ? (
                    <span
                      className={pill(
                        (effectivePresenceLabel || '').toLowerCase() === 'online'
                          ? 'bg-emerald-500/25 text-white ring-emerald-200/30'
                          : 'bg-white/15 text-white ring-white/20'
                      )}
                    >
                      {effectivePresenceLabel}
                    </span>
                  ) : null}

                  {effectiveRoom?.is_hd ? <span className={pill('bg-white/15 text-white ring-white/20')}>HD</span> : null}
                  {effectiveRoom?.is_new ? <span className={pill('bg-white/15 text-white ring-white/20')}>NEW</span> : null}
                </div>

                {/* Room link bottom-right */}
                {roomUrl ? (
                  <div className="absolute right-3 bottom-3">
                    <a
                      href={roomUrl}
                      target="_blank"
                      rel="noreferrer"
                      className="inline-flex h-8 items-center justify-center gap-1 rounded-lg px-3 text-sm font-medium bg-white/15 text-white ring-1 ring-white/20 backdrop-blur hover:bg-white/20"
                      title="Room öffnen"
                    >
                      <ArrowTopRightOnSquareIcon className="size-4" />
                      <span>Room</span>
                    </a>
                  </div>
                ) : null}
              </div>

              {/* Quick stats row (unter dem Hero) */}
              <div className="px-3 py-2 text-[12px] text-gray-700 dark:text-gray-200">
                <div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1">
                  <span className="inline-flex items-center gap-1">
                    <UsersIcon className="size-3.5 text-gray-400" />
                    <span className="font-semibold text-gray-900 dark:text-white">{fmtInt(effectiveRoom?.num_users)}</span>
                  </span>
                  <span className="inline-flex items-center gap-1">
                    <SparklesIcon className="size-3.5 text-gray-400" />
                    <span className="font-semibold text-gray-900 dark:text-white">{fmtInt(effectiveRoom?.num_followers ?? bioFollowers)}</span>
                  </span>
                  <span className="inline-flex items-center gap-1">
                    <ClockIcon className="size-3.5 text-gray-400" />
                    <span className="font-semibold text-gray-900 dark:text-white">{fmtHms(effectiveRoom?.seconds_online)}</span>
                  </span>
                  <span className="inline-flex items-center gap-1">
                    <CalendarDaysIcon className="size-3.5 text-gray-400" />
                    <span className="font-semibold text-gray-900 dark:text-white">
                      {bio?.last_broadcast ? shortDate(bio.last_broadcast) : '—'}
                    </span>
                  </span>
                </div>

                {/* Meta warnings (mobile) */}
                {effectiveRoomMeta?.enabled === false ? (
                  <div className="mt-2 rounded-lg border border-amber-200/60 bg-amber-50/70 px-3 py-2 text-xs text-amber-900 dark:border-amber-400/20 dark:bg-amber-400/10 dark:text-amber-200">
                    Chaturbate-Online ist aktuell deaktiviert.
                  </div>
                ) : effectiveRoomMeta?.lastError ? (
                  <div className="mt-2 rounded-lg border border-rose-200/60 bg-rose-50/70 px-3 py-2 text-xs text-rose-900 dark:border-rose-400/20 dark:bg-rose-400/10 dark:text-rose-200">
                    Online-Info: {effectiveRoomMeta.lastError}
                  </div>
                ) : null}

                {bioMeta?.enabled === false ? (
                  <div className="mt-2 rounded-lg border border-amber-200/60 bg-amber-50/70 px-3 py-2 text-xs text-amber-900 dark:border-amber-400/20 dark:bg-amber-400/10 dark:text-amber-200">
                    BioContext ist aktuell deaktiviert.
                  </div>
                ) : bioMeta?.lastError ? (
                  <div className="mt-2 rounded-lg border border-rose-200/60 bg-rose-50/70 px-3 py-2 text-xs text-rose-900 dark:border-rose-400/20 dark:bg-rose-400/10 dark:text-rose-200">
                    <div className="flex items-start justify-between gap-2">
                      <div className="font-medium">BioContext: {errorSummary(bioMeta.lastError)}</div>
                    </div>
                    <details className="mt-1">
                      <summary className="cursor-pointer select-none text-[11px] text-rose-900/80 underline underline-offset-2 dark:text-rose-200/80">
                        Details
                      </summary>
                      <pre className="mt-1 max-h-28 overflow-auto whitespace-pre-wrap break-words rounded-md bg-black/5 p-2 text-[11px] leading-snug dark:bg-white/10">
                        {errorDetails(bioMeta.lastError)}
                      </pre>
                    </details>
                  </div>
                ) : null}
              </div>
            </div>

            {/* Tags (mobile, compact row) */}
            {allTags.length ? (
              <div className="rounded-lg border border-gray-200/70 bg-white/70 px-2 py-1.5 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5">
                <div className="flex items-center gap-2">
                  <div className="shrink-0 text-xs font-semibold text-gray-900 dark:text-white">
                    Tags <span className="text-gray-500 dark:text-gray-400">({allTags.length})</span>
                  </div>

                  <div className="-mx-1 flex-1 overflow-x-auto px-1 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
                    <div className="flex min-w-max items-center gap-1.5">
                      {allTags.slice(0, 20).map((t) => (
                        <TagBadge key={t} tag={t} />
                      ))}
                      {allTags.length > 20 ? (
                        <span className={pill('bg-gray-500/10 text-gray-900 ring-gray-200 dark:text-gray-200 dark:ring-white/15')}>
                          +{allTags.length - 20}
                        </span>
                      ) : null}
                    </div>
                  </div>
                </div>
              </div>
            ) : null}
          </div>

          {/* ===================== */}
          {/* (dein bisheriger Header) Row 1: Meta + Actions */}
          {/* ===================== */}
          <div className="hidden sm:flex items-start justify-between gap-2 px-2 py-2 sm:px-4">
            {/* Meta */}
            <div className="min-w-0">
              <div className="text-[11px] leading-snug text-gray-600 dark:text-gray-300">
                {key ? (
                  <div className="flex flex-wrap gap-x-2 gap-y-1">
                    {effectiveRoomMeta?.fetchedAt ? (
                      <span className="text-gray-500 dark:text-gray-400">
                        Online-Stand: {fmtDateTime(effectiveRoomMeta.fetchedAt)}
                      </span>
                    ) : null}
                    {bioMeta?.fetchedAt ? (
                      <span className="text-gray-500 dark:text-gray-400">
                        · Bio-Stand: {fmtDateTime(bioMeta.fetchedAt)}
                      </span>
                    ) : null}
                    {model?.lastSeenOnlineAt ? (
                      <span className="text-gray-500 dark:text-gray-400">
                        · Zuletzt gesehen:{' '}
                        <span className="font-medium">
                          {model.lastSeenOnline == null
                            ? '—'
                            : model.lastSeenOnline
                              ? 'online'
                              : 'offline'}
                        </span>{' '}
                        ({fmtDateTime(model.lastSeenOnlineAt)})
                      </span>
                    ) : null}
                  </div>
                ) : (
                  '—'
                )}
              </div>
            </div>

            {/* Actions */}
            <div className="flex shrink-0 items-center gap-2">
              {roomUrl ? (
                <a
                  href={roomUrl}
                  target="_blank"
                  rel="noreferrer"
                  className={cn(
                    'inline-flex h-8 items-center justify-center gap-1 rounded-lg',
                    'border border-gray-200/70 bg-white/70 px-3 text-sm font-medium text-gray-900 shadow-sm backdrop-blur hover:bg-white',
                    'dark:border-white/10 dark:bg-white/5 dark:text-white',
                    'whitespace-nowrap'
                  )}
                  title="Room öffnen"
                >
                  <ArrowTopRightOnSquareIcon className="size-4" />
                  <span className="hidden sm:inline">Room öffnen</span>
                </a>
              ) : null}
            </div>
          </div>

          {/* Row 2: Tabs */}
          <div className="px-2 pb-2">
            <div className="-mx-1 overflow-x-auto px-1 [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
              <div className="min-w-max">
                <Tabs
                  tabs={tabs}
                  value={tab}
                  onChange={(id) => setTab(id as TabKey)}
                  variant="pillsBrand"
                  ariaLabel="Bereich auswählen"
                />
              </div>
            </div>
          </div>
        </div>
      }
      footer={
        <div className="w-full">
          <Button variant="secondary" className="w-full sm:w-auto" onClick={onClose}>
            Schließen
          </Button>
        </div>
      }
    >
      {/* RIGHT content (Tabs) */}
      <div className="min-h-0">
        {/* INFO */}
        {tab === 'info' ? (
          <div className="space-y-2">
            {/* Subject */}
            <div className="rounded-lg border border-gray-200/70 bg-white/70 p-3 sm:p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5">
              <div className="flex items-center justify-between gap-3">
                <div className="text-sm font-semibold text-gray-900 dark:text-white">Room Subject</div>
              </div>
              <div className="mt-2 text-sm text-gray-800 dark:text-gray-100">
                {effectiveRoom?.room_subject ? (
                  <p className="line-clamp-4 whitespace-pre-wrap break-words">{effectiveRoom.room_subject}</p>
                ) : (
                  <p className="text-gray-600 dark:text-gray-300">Keine Subject-Info vorhanden.</p>
                )}
              </div>
            </div>

            {/* Bio */}
            <div className="rounded-lg border border-gray-200/70 bg-white/70 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5">
              <div className="flex items-center justify-between gap-3">
                <div className="text-sm font-semibold text-gray-900 dark:text-white">Bio</div>
                {bioLoading ? <span className="text-xs text-gray-500 dark:text-gray-400">Lade…</span> : null}
              </div>

              <div className="mt-2 grid gap-3 sm:grid-cols-2">
                <div className="rounded-lg border border-gray-200/70 bg-white/60 p-3 dark:border-white/10 dark:bg-white/5">
                  <div className="flex items-center gap-2 text-xs font-medium text-gray-700 dark:text-gray-200">
                    <IdentificationIcon className="size-4" />
                    Über mich
                  </div>
                  <div className="mt-2 text-sm text-gray-800 dark:text-gray-100">
                    {about ? (
                      <p className="line-clamp-6 whitespace-pre-wrap">{about}</p>
                    ) : (
                      <span className="text-gray-600 dark:text-gray-300">—</span>
                    )}
                  </div>
                </div>

                <div className="rounded-lg border border-gray-200/70 bg-white/60 p-3 dark:border-white/10 dark:bg-white/5">
                  <div className="flex items-center gap-2 text-xs font-medium text-gray-700 dark:text-gray-200">
                    <SparklesIcon className="size-4" />
                    Wishlist
                  </div>
                  <div className="mt-2 text-sm text-gray-800 dark:text-gray-100">
                    {wish ? (
                      <p className="line-clamp-6 whitespace-pre-wrap">{wish}</p>
                    ) : (
                      <span className="text-gray-600 dark:text-gray-300">—</span>
                    )}
                  </div>
                </div>
              </div>

              <div className="mt-2 grid gap-2 sm:grid-cols-2">
                <div className="rounded-lg border border-gray-200/70 bg-white/60 px-3 py-2 text-xs dark:border-white/10 dark:bg-white/5">
                  <span className="text-gray-600 dark:text-gray-300">Real name:</span>{' '}
                  <span className="font-medium text-gray-900 dark:text-white">
                    {bio?.real_name ? stripHtmlToText(bio.real_name) : '—'}
                  </span>
                </div>
                <div className="rounded-lg border border-gray-200/70 bg-white/60 px-3 py-2 text-xs dark:border-white/10 dark:bg-white/5">
                  <span className="text-gray-600 dark:text-gray-300">Body type:</span>{' '}
                  <span className="font-medium text-gray-900 dark:text-white">{bio?.body_type || '—'}</span>
                </div>
                <div className="rounded-lg border border-gray-200/70 bg-white/60 px-3 py-2 text-xs dark:border-white/10 dark:bg-white/5">
                  <span className="text-gray-600 dark:text-gray-300">Smoke/Drink:</span>{' '}
                  <span className="font-medium text-gray-900 dark:text-white">{bio?.smoke_drink || '—'}</span>
                </div>
                <div className="rounded-lg border border-gray-200/70 bg-white/60 px-3 py-2 text-xs dark:border-white/10 dark:bg-white/5">
                  <span className="text-gray-600 dark:text-gray-300">Sex:</span>{' '}
                  <span className="font-medium text-gray-900 dark:text-white">{bio?.sex || '—'}</span>
                </div>
              </div>

              {interested.length ? (
                <div className="mt-2">
                  <div className="text-xs font-medium text-gray-700 dark:text-gray-200">Interested in</div>
                  <div className="mt-2 flex flex-wrap gap-2">
                    {interested.map((x) => (
                      <span
                        key={x}
                        className={pill(
                          'bg-sky-500/10 text-sky-900 ring-sky-200 dark:text-sky-200 dark:ring-sky-400/20'
                        )}
                      >
                        {x}
                      </span>
                    ))}
                  </div>
                </div>
              ) : null}
            </div>

            {/* Socials */}
            <div className="rounded-lg border border-gray-200/70 bg-white/70 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5">
              <div className="flex items-center justify-between gap-3">
                <div className="text-sm font-semibold text-gray-900 dark:text-white">Socials</div>
                {socials.length ? (
                  <span className="text-xs text-gray-600 dark:text-gray-300">{socials.length} Links</span>
                ) : null}
              </div>

              {socials.length ? (
                <div className="mt-2 grid gap-2 sm:grid-cols-2">
                  {socials.map((s) => {
                    const href = absCbUrl(s.link)
                    return (
                      <a
                        key={s.id}
                        href={href}
                        target="_blank"
                        rel="noreferrer"
                        className={cn(
                          'group flex min-w-0 w-full items-center gap-3 overflow-hidden rounded-lg border border-gray-200/70 bg-white/60 px-3 py-2 text-gray-900',
                          'transition hover:border-indigo-200 hover:bg-gray-50/80 hover:text-gray-900 dark:border-white/10 dark:bg-white/5 dark:text-white dark:hover:border-indigo-400/30 dark:hover:bg-white/10 dark:hover:text-white'
                        )}
                        title={s.title_name}
                      >
                        <div className="flex size-9 items-center justify-center rounded-lg bg-black/5 dark:bg-white/5">
                          {s.image_url ? <img src={s.image_url} alt="" className="size-5" /> : <LinkIcon className="size-5" />}
                        </div>

                        <div className="min-w-0">
                          <div className="truncate text-sm font-medium text-gray-900 dark:text-white">{s.title_name}</div>
                          <div className="text-xs text-gray-600 dark:text-gray-300">
                            {s.label_text ? s.label_text : s.tokens != null ? `${s.tokens} token(s)` : 'Link'}
                          </div>
                        </div>

                        <ArrowTopRightOnSquareIcon className="ml-auto size-4 text-gray-400 transition group-hover:text-indigo-500 dark:text-gray-500 dark:group-hover:text-indigo-400" />
                      </a>
                    )
                  })}
                </div>
              ) : (
                <div className="mt-2 text-sm text-gray-600 dark:text-gray-300">Keine Social-Medias vorhanden.</div>
              )}
            </div>

            {/* Photo sets */}
            <div className="rounded-lg border border-gray-200/70 bg-white/70 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5">
              <div className="flex items-center justify-between gap-3">
                <div className="text-sm font-semibold text-gray-900 dark:text-white">Photo sets</div>
                {photos.length ? (
                  <span className="text-xs text-gray-600 dark:text-gray-300">{photos.length} Sets</span>
                ) : null}
              </div>

              {photos.length ? (
                <div className="mt-2 grid gap-3 sm:grid-cols-2">
                  {photos.slice(0, 6).map((p) => (
                    <div
                      key={p.id}
                      className="overflow-hidden rounded-lg border border-gray-200/70 bg-white/60 dark:border-white/10 dark:bg-white/5"
                    >
                      <div className="relative">
                        {p.cover_url ? (
                          <button
                            type="button"
                            className="block w-full cursor-zoom-in"
                            onClick={() => openImage(p.cover_url, p.name)}
                            aria-label="Bild vergrößern"
                          >
                            <img
                              src={p.cover_url}
                              alt={p.name}
                              className={cn('h-28 w-full object-cover transition', previewBlurCls(blurPreviews))}
                            />
                          </button>
                        ) : (
                          <div className="flex h-28 w-full items-center justify-center bg-black/5 dark:bg-white/5">
                            <PhotoIcon className="size-6 text-gray-500" />
                          </div>
                        )}
                        <div className="absolute left-2 top-2 flex flex-wrap gap-2">
                          {p.is_video ? (
                            <span
                              className={pill(
                                'bg-indigo-500/10 text-indigo-900 ring-indigo-200 dark:text-indigo-200 dark:ring-indigo-400/20'
                              )}
                            >
                              Video
                            </span>
                          ) : (
                            <span
                              className={pill(
                                'bg-gray-500/10 text-gray-900 ring-gray-200 dark:text-gray-200 dark:ring-white/15'
                              )}
                            >
                              Photos
                            </span>
                          )}
                          {p.fan_club_only ? (
                            <span
                              className={pill(
                                'bg-amber-500/10 text-amber-900 ring-amber-200 dark:text-amber-200 dark:ring-amber-400/20'
                              )}
                            >
                              FanClub
                            </span>
                          ) : null}
                        </div>
                      </div>

                      <div className="p-3">
                        <div className="truncate text-sm font-medium text-gray-900 dark:text-white">{p.name}</div>
                        <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
                          Tokens: <span className="font-medium text-gray-900 dark:text-white">{fmtInt(p.tokens)}</span>
                          {p.user_can_access === true ? <span className="ml-2">· Zugriff: ✅</span> : null}
                        </div>
                      </div>
                    </div>
                  ))}
                </div>
              ) : (
                <div className="mt-2 text-sm text-gray-600 dark:text-gray-300">Keine Photo-Sets vorhanden.</div>
              )}
            </div>
          </div>
        ) : null}

        {/* DOWNLOADS */}
        {tab === 'downloads' ? (
          <div className="min-h-0">
            <div className="rounded-lg border border-gray-200/70 bg-white/70 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5">
              <div className="shrink-0 flex flex-wrap items-center justify-between gap-3">
                <div>
                  <div className="text-sm font-semibold text-gray-900 dark:text-white">Abgeschlossene Downloads</div>
                  <div className="text-xs text-gray-600 dark:text-gray-300">
                    Inkl. Dateien aus <span className="font-medium">/done/keep/</span>
                  </div>
                </div>

                <div className="w-full sm:w-auto">
                  <Pagination
                    page={donePage}
                    pageSize={DONE_PAGE_SIZE}
                    totalItems={doneTotalCount}
                    onPageChange={(p) => setDonePage(p)}
                    siblingCount={1}
                    boundaryCount={1}
                    showSummary={true}
                    prevLabel="Zurück"
                    nextLabel="Weiter"
                    ariaLabel="Downloads Seiten"
                    className="border-0 bg-transparent px-0 py-0 dark:bg-transparent"
                  />
                </div>
              </div>

              <div className="mt-2">
                {doneLoading ? (
                  <div className="text-sm text-gray-600 dark:text-gray-300">Lade Downloads…</div>
                ) : doneMatches.length === 0 ? (
                  <div className="text-sm text-gray-600 dark:text-gray-300">
                    Keine abgeschlossenen Downloads für dieses Model gefunden.
                  </div>
                ) : (
                  <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
                    {doneMatches.map((j) => {
                      const k = keyFor(j)

                      const allowSound = Boolean(teaserAudio) && hoverTeaserKey === k
                      const previewMuted = !allowSound

                      const fileRaw = baseName(j.output || '')
                      const isHot = isHotName(fileRaw)
                      const file = stripHotPrefix(fileRaw)

                      const dur = runtimeOf(j)
                      const size = formatBytes(sizeBytesOf(j))

                      // Auflösung bevorzugt aus meta/videoWidth/videoHeight
                      const meta = parseJobMeta((j as any)?.meta)
                      const resObj =
                        typeof meta?.videoWidth === 'number' &&
                        typeof meta?.videoHeight === 'number' &&
                        meta.videoWidth > 0 &&
                        meta.videoHeight > 0
                          ? { w: meta.videoWidth, h: meta.videoHeight }
                          : typeof (j as any)?.videoWidth === 'number' &&
                            typeof (j as any)?.videoHeight === 'number' &&
                            (j as any).videoWidth > 0 &&
                            (j as any).videoHeight > 0
                            ? { w: (j as any).videoWidth, h: (j as any).videoHeight }
                            : null

                      const resLabel = formatResolution(resObj)

                      const busy = deletingKeys.has(k) || keepingKeys.has(k) || removingKeys.has(k)
                      const deleted = deletedKeys.has(k)

                      // Model “Flags” (hier: aktuelles Model)
                      const isFav = Boolean(model?.favorite)
                      const isLiked = model?.liked === true
                      const isWatching = Boolean(model?.watching)

                      // Tags: nimm stored + room-tags (wie links), deduped
                      const cardTags = allTags

                      // Model Preview Bild: nimm Hero
                      const modelImageSrc = firstNonEmptyString(heroImgFull, heroImg)

                      // Preview-ID (für sprite fallback)
                      const fileForPreviewId = stripHotPrefix(baseName(j.output || ''))
                      const previewId = fileForPreviewId.replace(/\.[^.]+$/, '').trim()

                      // -------- Sprite/Scrubber Setup (wie Gallery) --------
                      const spritePathRaw = firstNonEmptyString(
                        meta?.previewSprite?.path,
                        (meta as any)?.previewSpritePath,
                        previewId ? `/api/preview-sprite/${encodeURIComponent(previewId)}` : undefined
                      )
                      const spritePath = spritePathRaw ? spritePathRaw.replace(/\/+$/, '') : undefined

                      const spriteStepSecondsRaw = meta?.previewSprite?.stepSeconds ?? (meta as any)?.previewSpriteStepSeconds
                      const spriteStepSeconds =
                        typeof spriteStepSecondsRaw === 'number' && Number.isFinite(spriteStepSecondsRaw) && spriteStepSecondsRaw > 0
                          ? spriteStepSecondsRaw
                          : DEFAULT_SPRITE_STEP_SECONDS

                      const durationForSprite =
                        normalizeDurationSeconds(meta?.durationSeconds) ??
                        normalizeDurationSeconds((j as any)?.durationSeconds) ??
                        normalizeDurationSeconds(durations[k])

                      const inferredSpriteCountFromDuration =
                        typeof durationForSprite === 'number' && durationForSprite > 0
                          ? Math.max(1, Math.min(200, Math.floor(durationForSprite / spriteStepSeconds) + 1))
                          : undefined

                      const spriteCountRaw =
                        meta?.previewSprite?.count ??
                        (meta as any)?.previewSpriteCount ??
                        inferredSpriteCountFromDuration

                      const spriteColsRaw = meta?.previewSprite?.cols ?? (meta as any)?.previewSpriteCols
                      const spriteRowsRaw = meta?.previewSprite?.rows ?? (meta as any)?.previewSpriteRows

                      const spriteCount =
                        typeof spriteCountRaw === 'number' && Number.isFinite(spriteCountRaw) ? Math.max(0, Math.floor(spriteCountRaw)) : 0

                      const [inferredCols, inferredRows] = spriteCount > 1 ? chooseSpriteGrid(spriteCount) : [0, 0]

                      const spriteCols =
                        typeof spriteColsRaw === 'number' && Number.isFinite(spriteColsRaw) ? Math.max(0, Math.floor(spriteColsRaw)) : inferredCols

                      const spriteRows =
                        typeof spriteRowsRaw === 'number' && Number.isFinite(spriteRowsRaw) ? Math.max(0, Math.floor(spriteRowsRaw)) : inferredRows

                      const spriteVersion =
                        (typeof meta?.updatedAtUnix === 'number' && Number.isFinite(meta.updatedAtUnix) ? meta.updatedAtUnix : undefined) ??
                        (typeof (meta as any)?.fileModUnix === 'number' && Number.isFinite((meta as any).fileModUnix) ? (meta as any).fileModUnix : undefined) ??
                        0

                      const spriteUrl = spritePath && spriteVersion ? `${spritePath}?v=${encodeURIComponent(String(spriteVersion))}` : spritePath || undefined

                      const hasScrubberUi = Boolean(spriteUrl) && spriteCount > 1
                      const hasSpriteScrubber = hasScrubberUi && spriteCols > 0 && spriteRows > 0

                      const scrubberCount = hasScrubberUi ? spriteCount : 0
                      const scrubberStepSeconds = hasScrubberUi ? spriteStepSeconds : 0
                      const hasScrubber = hasScrubberUi

                      const activeScrubIndex = scrubIndexByKey[k]
                      const scrubProgressRatio =
                        typeof activeScrubIndex === 'number' && scrubberCount > 1 ? clamp(activeScrubIndex / (scrubberCount - 1), 0, 1) : undefined

                      const spriteFrameStyle: React.CSSProperties | undefined =
                        hasSpriteScrubber && typeof activeScrubIndex === 'number'
                          ? (() => {
                              const idx = clamp(activeScrubIndex, 0, Math.max(0, spriteCount - 1))
                              const col = idx % spriteCols
                              const row = Math.floor(idx / spriteCols)

                              const posX = spriteCols <= 1 ? 0 : (col / (spriteCols - 1)) * 100
                              const posY = spriteRows <= 1 ? 0 : (row / (spriteRows - 1)) * 100

                              return {
                                backgroundImage: `url("${spriteUrl}")`,
                                backgroundRepeat: 'no-repeat',
                                backgroundSize: `${spriteCols * 100}% ${spriteRows * 100}%`,
                                backgroundPosition: `${posX}% ${posY}%`,
                              }
                            })()
                          : undefined

                      const showModelPreviewInThumb = hoveredModelPreviewKey === k && Boolean(modelImageSrc)
                      const showScrubberSpriteInThumb = !showModelPreviewInThumb && Boolean(spriteFrameStyle)
                      const hideTeaserUnderOverlay = showModelPreviewInThumb || showScrubberSpriteInThumb

                      if (deleted) return null

                      return (
                        <div
                          key={k}
                          role="button"
                          tabIndex={0}
                          className={[
                            'group relative rounded-lg overflow-hidden outline-1 outline-black/5 dark:-outline-offset-1 dark:outline-white/10',
                            'bg-white dark:bg-gray-900/40',
                            'transition-all duration-200',
                            'hover:-translate-y-0.5 hover:shadow-md dark:hover:shadow-none',
                            'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:focus-visible:outline-indigo-500',
                            busy && 'pointer-events-none opacity-70',
                            removingKeys.has(k) && 'opacity-0 translate-y-2 scale-[0.98]',
                          ]
                            .filter(Boolean)
                            .join(' ')}
                          onClick={() => onOpenPlayer?.(j)}
                          onKeyDown={(e) => {
                            if (e.key === 'Enter' || e.key === ' ') onOpenPlayer?.(j)
                          }}
                        >
                          {/* Thumb */}
                          <div
                            className="group/thumb relative aspect-video rounded-t-lg bg-black/5 dark:bg-white/5"
                            onMouseEnter={() => {
                              setHoveredThumbKey(k)
                              handleHoverPreviewKeyChange(k)
                            }}
                            onMouseLeave={() => {
                              setHoveredThumbKey(null)
                              handleHoverPreviewKeyChange(null)
                              clearScrubIndex(k)
                              setHoveredModelPreviewKey((prev) => (prev === k ? null : prev))
                            }}
                          >
                            <div className="absolute inset-0 overflow-hidden rounded-t-lg">
                              <div className="absolute inset-0">
                                <FinishedVideoPreview
                                  job={j}
                                  getFileName={(p) => stripHotPrefix(baseName(p))}
                                  durationSeconds={durations[k] ?? (j as any)?.durationSeconds}
                                  onDuration={handleDuration}
                                  variant="fill"
                                  showPopover={false}
                                  blur={blurPreviews}
                                  animated={
                                    hideTeaserUnderOverlay
                                      ? false
                                      : teaserPlayback === 'all'
                                      ? true
                                      : teaserPlayback === 'hover'
                                      ? teaserKey === k
                                      : false
                                  }
                                  animatedMode="teaser"
                                  animatedTrigger="always"
                                  muted={previewMuted}
                                  popoverMuted={previewMuted}
                                  scrubProgressRatio={scrubProgressRatio}
                                  preferScrubProgress={typeof activeScrubIndex === 'number'}
                                />
                              </div>

                              {/* Sprite preload */}
                              {hasSpriteScrubber && spriteUrl ? (
                                <img src={spriteUrl} alt="" className="hidden" loading="lazy" decoding="async" aria-hidden="true" />
                              ) : null}

                              {/* Sprite overlay frame */}
                              {showScrubberSpriteInThumb && spriteFrameStyle ? (
                                <div className="absolute inset-x-0 top-0 bottom-[6px] z-[5]" aria-hidden="true">
                                  <div className="h-full w-full" style={spriteFrameStyle} />
                                </div>
                              ) : null}

                              {/* Model image preview overlay */}
                              {showModelPreviewInThumb && modelImageSrc ? (
                                <div className="absolute inset-0 z-[6]">
                                  <img src={modelImageSrc} alt="Model preview" className="h-full w-full object-cover" draggable={false} />
                                  <div className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/65 to-transparent px-2 py-1.5">
                                    <div className="text-[10px] font-semibold tracking-wide text-white/95">MODEL PREVIEW</div>
                                  </div>
                                </div>
                              ) : null}

                              {/* Hover Scrubber */}
                              {hasScrubber && hoveredThumbKey === k ? (
                                <div
                                  className="absolute inset-x-0 bottom-0 z-30 pointer-events-none opacity-100 transition-opacity duration-150"
                                  onClick={(e) => e.stopPropagation()}
                                  onMouseDown={(e) => e.stopPropagation()}
                                >
                                  <PreviewScrubber
                                    className="pointer-events-auto px-1"
                                    imageCount={scrubberCount}
                                    activeIndex={activeScrubIndex}
                                    onActiveIndexChange={(idx) => setScrubIndexForKey(k, idx)}
                                    onIndexClick={(index) => {
                                      setScrubIndexForKey(k, index)
                                      handleScrubberClickIndex(j, index, scrubberCount)
                                    }}
                                    stepSeconds={scrubberStepSeconds}
                                  />
                                </div>
                              ) : null}

                              {/* Meta overlay bottom right */}
                              <div className="pointer-events-none absolute right-2 bottom-2 z-10 transition-opacity duration-150 group-hover/thumb:opacity-0 group-focus-within/thumb:opacity-0">
                                <div
                                  className="flex items-center gap-1.5 text-right text-[11px] font-semibold leading-none text-white [text-shadow:_0_1px_0_rgba(0,0,0,0.95),_1px_0_0_rgba(0,0,0,0.95),_-1px_0_0_rgba(0,0,0,0.95),_0_-1px_0_rgba(0,0,0,0.95),_1px_1px_0_rgba(0,0,0,0.8),_-1px_1px_0_rgba(0,0,0,0.8),_1px_-1px_0_rgba(0,0,0,0.8),_-1px_-1px_0_rgba(0,0,0,0.8)]"
                                  title={[dur, resObj ? `${resObj.w}×${resObj.h}` : resLabel || '', size].filter(Boolean).join(' • ')}
                                >
                                  <span>{dur}</span>
                                  {resLabel ? <span aria-hidden="true">•</span> : null}
                                  {resLabel ? <span>{resLabel}</span> : null}
                                  <span aria-hidden="true">•</span>
                                  <span>{size}</span>
                                </div>
                              </div>
                            </div>
                          </div>

                          {/* Footer */}
                          <div className="relative min-h-[118px] px-4 py-3 rounded-b-lg border-t border-black/5 dark:border-white/10 bg-white dark:bg-gray-900">
                            {/* filename */}
                            <div className="min-w-0">
                              <div className="mt-0.5 flex items-start gap-2 min-w-0">
                                {isHot ? (
                                  <span className="shrink-0 self-start rounded bg-amber-500/15 px-1.5 py-0.5 text-[11px] leading-none font-semibold text-amber-800 dark:text-amber-300">
                                    HOT
                                  </span>
                                ) : null}
                                <span className="min-w-0 truncate text-sm font-semibold text-gray-900 dark:text-white" title={file || '—'}>
                                  {file || '—'}
                                </span>
                              </div>
                            </div>

                            {/* actions */}
                            <div className="mt-2" onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}>
                              <div className="w-full rounded-md bg-gray-50/70 p-1 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
                                <RecordJobActions
                                  job={j}
                                  variant="table"
                                  busy={busy}
                                  collapseToMenu
                                  compact={true}
                                  isHot={isHot}
                                  isFavorite={isFav}
                                  isLiked={isLiked}
                                  isWatching={isWatching}
                                  onToggleWatch={onToggleWatch}
                                  onToggleFavorite={onToggleFavorite}
                                  onToggleLike={onToggleLike}
                                  onToggleHot={async (job) => {
                                    await handleToggleHot(job)
                                    return true
                                  }}
                                  onKeep={async (job) => {
                                    try {
                                      await onKeep?.(job)
                                      return true
                                    } catch {
                                      return false
                                    }
                                  }}
                                  onDelete={async (job) => {
                                    try {
                                      await onDelete?.(job)
                                      return true
                                    } catch {
                                      return false
                                    }
                                  }}
                                  order={[ 'hot', 'keep', 'delete']}
                                  className="w-full gap-1.5"
                                />
                              </div>
                            </div>

                            {/* tags */}
                            <div className="mt-2" onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}>
                              <TagOverflowRow
                                rowKey={k}
                                tags={cardTags}
                                activeTagSet={new Set()} // in ModelDetails kein Tag-Filter -> leer
                                lower={lower}
                                onToggleTagFilter={() => {}}
                              />
                            </div>
                          </div>
                        </div>
                      )
                    })}
                  </div>
                )}
              </div>
            </div>
          </div>
        ) : null}

        {/* RUNNING */}
        {tab === 'running' ? (
          <div className="min-h-0">
            <div className="rounded-lg border border-gray-200/70 bg-white/70 p-4 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5">
              <div className="flex items-center justify-between gap-3">
                <div className="text-sm font-semibold text-gray-900 dark:text-white">Laufender Download</div>
                {runningLoading ? <span className="text-xs text-gray-500 dark:text-gray-400">Lade…</span> : null}
              </div>

              <div className="mt-3">
                {runningLoading ? (
                  <div className="text-sm text-gray-600 dark:text-gray-300">Lade…</div>
                ) : runningMatches.length === 0 ? (
                  <div className="text-sm text-gray-600 dark:text-gray-300">Kein laufender Job.</div>
                ) : (() => {
                  const j = runningMatches[0]
                  const k = keyFor(j)

                  const allowSound = Boolean(teaserAudio) && hoverTeaserKey === k
                  const previewMuted = !allowSound

                  const fileRaw = baseName(j.output || '')
                  const isHot = isHotName(fileRaw)
                  const file = stripHotPrefix(fileRaw) || '—'

                  const meta = parseJobMeta((j as any)?.meta)

                  // Auflösung bevorzugt aus meta/videoWidth/videoHeight
                  const resObj =
                    typeof meta?.videoWidth === 'number' &&
                    typeof meta?.videoHeight === 'number' &&
                    meta.videoWidth > 0 &&
                    meta.videoHeight > 0
                      ? { w: meta.videoWidth, h: meta.videoHeight }
                      : typeof (j as any)?.videoWidth === 'number' &&
                        typeof (j as any)?.videoHeight === 'number' &&
                        (j as any).videoWidth > 0 &&
                        (j as any).videoHeight > 0
                        ? { w: (j as any).videoWidth, h: (j as any).videoHeight }
                        : null

                  const resLabel = formatResolution(resObj)

                  // Dauer + Size im gleichen Stil wie Gallery
                  const dur = runtimeOf(j)
                  const size = formatBytes(sizeBytesOf(j))

                  const cardTags = allTags
                  const modelImageSrc = firstNonEmptyString(heroImgFull, heroImg)

                  // Preview-ID (für sprite fallback)
                  const fileForPreviewId = stripHotPrefix(baseName(j.output || ''))
                  const previewId = fileForPreviewId.replace(/\.[^.]+$/, '').trim()

                  // -------- Sprite/Scrubber Setup (wie Gallery) --------
                  const spritePathRaw = firstNonEmptyString(
                    meta?.previewSprite?.path,
                    (meta as any)?.previewSpritePath,
                    previewId ? `/api/preview-sprite/${encodeURIComponent(previewId)}` : undefined
                  )
                  const spritePath = spritePathRaw ? spritePathRaw.replace(/\/+$/, '') : undefined

                  const spriteStepSecondsRaw =
                    meta?.previewSprite?.stepSeconds ?? (meta as any)?.previewSpriteStepSeconds
                  const spriteStepSeconds =
                    typeof spriteStepSecondsRaw === 'number' &&
                    Number.isFinite(spriteStepSecondsRaw) &&
                    spriteStepSecondsRaw > 0
                      ? spriteStepSecondsRaw
                      : DEFAULT_SPRITE_STEP_SECONDS

                  const durationForSprite =
                    normalizeDurationSeconds(meta?.durationSeconds) ??
                    normalizeDurationSeconds((j as any)?.durationSeconds) ??
                    normalizeDurationSeconds(durations[k])

                  const inferredSpriteCountFromDuration =
                    typeof durationForSprite === 'number' && durationForSprite > 0
                      ? Math.max(1, Math.min(200, Math.floor(durationForSprite / spriteStepSeconds) + 1))
                      : undefined

                  const spriteCountRaw =
                    meta?.previewSprite?.count ??
                    (meta as any)?.previewSpriteCount ??
                    inferredSpriteCountFromDuration

                  const spriteColsRaw = meta?.previewSprite?.cols ?? (meta as any)?.previewSpriteCols
                  const spriteRowsRaw = meta?.previewSprite?.rows ?? (meta as any)?.previewSpriteRows

                  const spriteCount =
                    typeof spriteCountRaw === 'number' && Number.isFinite(spriteCountRaw)
                      ? Math.max(0, Math.floor(spriteCountRaw))
                      : 0

                  const [inferredCols, inferredRows] =
                    spriteCount > 1 ? chooseSpriteGrid(spriteCount) : [0, 0]

                  const spriteCols =
                    typeof spriteColsRaw === 'number' && Number.isFinite(spriteColsRaw)
                      ? Math.max(0, Math.floor(spriteColsRaw))
                      : inferredCols

                  const spriteRows =
                    typeof spriteRowsRaw === 'number' && Number.isFinite(spriteRowsRaw)
                      ? Math.max(0, Math.floor(spriteRowsRaw))
                      : inferredRows

                  const spriteVersion =
                    (typeof meta?.updatedAtUnix === 'number' && Number.isFinite(meta.updatedAtUnix)
                      ? meta.updatedAtUnix
                      : undefined) ??
                    (typeof (meta as any)?.fileModUnix === 'number' && Number.isFinite((meta as any).fileModUnix)
                      ? (meta as any).fileModUnix
                      : undefined) ??
                    0

                  const spriteUrl =
                    spritePath && spriteVersion
                      ? `${spritePath}?v=${encodeURIComponent(String(spriteVersion))}`
                      : spritePath || undefined

                  const hasScrubberUi = Boolean(spriteUrl) && spriteCount > 1
                  const hasSpriteScrubber = hasScrubberUi && spriteCols > 0 && spriteRows > 0

                  const scrubberCount = hasScrubberUi ? spriteCount : 0

                  const activeScrubIndex = scrubIndexByKey[k]
                  const scrubProgressRatio =
                    typeof activeScrubIndex === 'number' && scrubberCount > 1
                      ? clamp(activeScrubIndex / (scrubberCount - 1), 0, 1)
                      : undefined

                  const spriteFrameStyle: React.CSSProperties | undefined =
                    hasSpriteScrubber && typeof activeScrubIndex === 'number'
                      ? (() => {
                          const idx = clamp(activeScrubIndex, 0, Math.max(0, spriteCount - 1))
                          const col = idx % spriteCols
                          const row = Math.floor(idx / spriteCols)

                          const posX = spriteCols <= 1 ? 0 : (col / (spriteCols - 1)) * 100
                          const posY = spriteRows <= 1 ? 0 : (row / (spriteRows - 1)) * 100

                          return {
                            backgroundImage: `url("${spriteUrl}")`,
                            backgroundRepeat: 'no-repeat',
                            backgroundSize: `${spriteCols * 100}% ${spriteRows * 100}%`,
                            backgroundPosition: `${posX}% ${posY}%`,
                          }
                        })()
                      : undefined

                  const showModelPreviewInThumb = hoveredModelPreviewKey === k && Boolean(modelImageSrc)
                  const showScrubberSpriteInThumb = !showModelPreviewInThumb && Boolean(spriteFrameStyle)
                  const hideTeaserUnderOverlay = showModelPreviewInThumb || showScrubberSpriteInThumb

                  const showLive =  isRunningJob(j)

                  return (
                    <div className="w-full">
                      <div
                        className={cn(
                          'group relative overflow-hidden rounded-lg outline-1 outline-black/5',
                          'bg-white shadow-sm backdrop-blur',
                          'dark:-outline-offset-1 dark:outline-white/10 dark:bg-gray-900/40'
                        )}
                      >
                        {/* Thumb (GROSS) */}
                        <div
                          role={onOpenPlayer ? 'button' : undefined}
                          tabIndex={onOpenPlayer ? 0 : undefined}
                          className={cn(
                            'group/thumb relative w-full bg-black/5 dark:bg-white/5',
                            'aspect-video sm:aspect-[21/9]',
                            onOpenPlayer ? 'cursor-pointer' : ''
                          )}
                          onClick={() => onOpenPlayer?.(j)}
                          onKeyDown={(e) => {
                            if (!onOpenPlayer) return
                            if (e.key === 'Enter' || e.key === ' ') onOpenPlayer(j)
                          }}
                          onMouseEnter={() => {
                            setRunningHover(true)
                            setHoveredThumbKey(k)
                            handleHoverPreviewKeyChange(k)
                          }}
                          onMouseLeave={() => {
                            setRunningHover(false)
                            setHoveredThumbKey(null)
                            handleHoverPreviewKeyChange(null)
                            clearScrubIndex(k)
                            setHoveredModelPreviewKey((prev) => (prev === k ? null : prev))
                          }}
                        >
                          {/* Clip area */}
                          <div className="absolute inset-0 overflow-hidden">
                            {/* Base Preview (nur wenn Live NICHT aktiv) */}
                            {!showLive ? (
                              <div className="absolute inset-0">
                                <FinishedVideoPreview
                                  job={j}
                                  getFileName={(p) => stripHotPrefix(baseName(p))}
                                  durationSeconds={durations[k] ?? (j as any)?.durationSeconds}
                                  onDuration={handleDuration}
                                  variant="fill"
                                  showPopover={false}
                                  blur={blurPreviews}
                                  animated={
                                    hideTeaserUnderOverlay
                                      ? false
                                      : teaserPlayback === 'all'
                                        ? true
                                        : teaserPlayback === 'hover'
                                          ? teaserKey === k
                                          : false
                                  }
                                  animatedMode="teaser"
                                  animatedTrigger="always"
                                  muted={previewMuted}
                                  popoverMuted={previewMuted}
                                  scrubProgressRatio={scrubProgressRatio}
                                  preferScrubProgress={typeof activeScrubIndex === 'number'}
                                />
                              </div>
                            ) : null}

                            {/* Live HLS inline (statt Teaser) */}
                            {showLive ? (
                              <div className="absolute inset-0 z-10">
                                <LiveVideo
                                  src={`/api/preview/live?id=${encodeURIComponent(String((j as any)?.id ?? ''))}&hover=1`}
                                  className="absolute inset-0 w-full h-full"
                                  muted
                                />

                                {/* ✅ LIVE badge oben links */}
                                <div className="absolute left-2 top-2 z-20 pointer-events-none inline-flex items-center gap-1.5 rounded-full bg-red-600/90 px-2 py-1 text-[11px] font-semibold text-white shadow-sm">
                                  <span className="inline-block size-1.5 rounded-full bg-white animate-pulse" />
                                  Live
                                </div>
                              </div>
                            ) : null}

                            {/* Meta overlay bottom right */}
                            <div className="pointer-events-none absolute right-2 bottom-2 z-20 transition-opacity duration-150 group-hover/thumb:opacity-0 group-focus-within/thumb:opacity-0">
                              <div
                                className="flex items-center gap-1.5 text-right text-[11px] font-semibold leading-none text-white [text-shadow:_0_1px_0_rgba(0,0,0,0.95),_1px_0_0_rgba(0,0,0,0.95),_-1px_0_0_rgba(0,0,0,0.95),_0_-1px_0_rgba(0,0,0,0.95),_1px_1px_0_rgba(0,0,0,0.8),_-1px_1px_0_rgba(0,0,0,0.8),_1px_-1px_0_rgba(0,0,0,0.8),_-1px_-1px_0_rgba(0,0,0,0.8)]"
                                title={[dur, resObj ? `${resObj.w}×${resObj.h}` : resLabel || '', size].filter(Boolean).join(' • ')}
                              >
                                <span>{size}</span>
                              </div>
                            </div>
                          </div>
                        </div>

                        {/* Footer (wie Gallery) */}
                        <div className="relative px-4 py-3 border-t border-black/5 dark:border-white/10 bg-white dark:bg-gray-900">
                          {/* filename */}
                          <div className="min-w-0">
                            <div className="mt-0.5 flex items-start gap-2 min-w-0">
                              {isHot ? (
                                <span className="shrink-0 self-start rounded bg-amber-500/15 px-1.5 py-0.5 text-[11px] leading-none font-semibold text-amber-800 dark:text-amber-300">
                                  HOT
                                </span>
                              ) : null}
                              <span className="min-w-0 truncate text-sm font-semibold text-gray-900 dark:text-white" title={file}>
                                {file}
                              </span>
                            </div>

                            {/* kleine Meta-Zeile (optional, aber nice) */}
                            <div className="mt-1 text-xs text-gray-600 dark:text-gray-300">
                              Start:{' '}
                              <span className="font-medium text-gray-900 dark:text-white">
                                {fmtDateTime((j as any)?.startedAt as any)}
                              </span>
                              {' · '}
                              Status:{' '}
                              <span className="font-medium text-gray-900 dark:text-white">
                                {String((j as any)?.status ?? 'running')}
                              </span>
                            </div>
                          </div>

                          {/* ✅ actions: Stop Button wie im Player */}
                          <div
                            className="mt-2"
                            onClick={(e) => e.stopPropagation()}
                            onMouseDown={(e) => e.stopPropagation()}
                          >
                            <div className="w-full rounded-md bg-gray-50/70 p-1 ring-1 ring-black/5 dark:bg-white/5 dark:ring-white/10">
                              <Button
                                variant="primary"
                                color="red"
                                size="sm"
                                rounded="md"
                                className="w-full shadow-none"
                                disabled={!onStopJob || !isRunningJob(j) || stopPending}
                                title={stopPending ? 'Stoppe…' : 'Stop'}
                                aria-label={stopPending ? 'Stoppe…' : 'Stop'}
                                onClick={async (e) => {
                                  e.preventDefault()
                                  e.stopPropagation()
                                  if (!onStopJob) return
                                  if (!isRunningJob(j)) return
                                  if (stopPending) return

                                  try {
                                    setStopPending(true)
                                    await onStopJob(String((j as any)?.id ?? ''))
                                  } finally {
                                    setStopPending(false)
                                  }
                                }}
                              >
                                {stopPending ? 'Stoppe…' : 'Stoppen'}
                              </Button>
                            </div>
                          </div>

                          {/* tags */}
                          <div className="mt-2" onClick={(e) => e.stopPropagation()} onMouseDown={(e) => e.stopPropagation()}>
                            <TagOverflowRow
                              rowKey={k}
                              tags={cardTags}
                              activeTagSet={new Set()}
                              lower={lower}
                              onToggleTagFilter={() => {}}
                            />
                          </div>
                        </div>
                      </div>
                    </div>
                  )
                })()}
              </div>
            </div>
          </div>
        ) : null}
      </div>

      {/* Image viewer modal (unverändert) */}
      <Modal
        open={Boolean(imgViewer)}
        onClose={() => setImgViewer(null)}
        title={imgViewer?.alt || 'Bild'}
        width="max-w-4xl"
        layout="single"
        scroll="body"
        footer={
          <div className="flex items-center justify-end gap-2">
            {imgViewer?.src ? (
              <a
                href={imgViewer.src}
                target="_blank"
                rel="noreferrer"
                className="inline-flex items-center gap-1 rounded-lg border border-gray-200/70 bg-white/70 px-3 py-1.5 text-xs font-medium text-gray-900 shadow-sm backdrop-blur hover:bg-white dark:border-white/10 dark:bg-white/5 dark:text-white"
              >
                <ArrowTopRightOnSquareIcon className="size-4" />
                In neuem Tab
              </a>
            ) : null}
            <Button variant="secondary" onClick={() => setImgViewer(null)}>
              Schließen
            </Button>
          </div>
        }
      >
        <div className="grid place-items-center">
          {imgViewer?.src ? (
            <img
              src={imgViewer.src}
              alt={imgViewer.alt || ''}
              className={cn('max-h-[80vh] w-auto max-w-full rounded-lg object-contain', previewBlurCls(blurPreviews))}
            />
          ) : null}
        </div>
      </Modal>
    </Modal>
  )
}
