// screens-asset.jsx - asset detail (Frame.io-style player, filmstrip, comments)

// Simple gradient palette - replaces the missing thumbGrad function
const _FRAME_GRADIENTS = [
  'linear-gradient(135deg,#1a1f2e 0%,#2a3045 100%)',
  'linear-gradient(135deg,#1e2030 0%,#2d2040 100%)',
  'linear-gradient(135deg,#0d1520 0%,#1a2535 100%)',
  'linear-gradient(135deg,#1f1a2e 0%,#2a2040 100%)',
  'linear-gradient(135deg,#0f1e18 0%,#1a3028 100%)',
  'linear-gradient(135deg,#1e1510 0%,#302018 100%)',
  'linear-gradient(135deg,#1a1020 0%,#281830 100%)',
  'linear-gradient(135deg,#101828 0%,#182438 100%)',
  'linear-gradient(135deg,#1e2820 0%,#283830 100%)',
  'linear-gradient(135deg,#201820 0%,#302030 100%)',
  'linear-gradient(135deg,#181e28 0%,#202838 100%)',
];

function AssetDetail({ asset, onClose }) {
  const [playing, setPlaying] = React.useState(false);
  const [currentMs, setCurrentMs] = React.useState(0);
  const [tab, setTab] = React.useState("comments");
  const [showResolved, setShowResolved] = React.useState(false);
  const [comments, setComments] = React.useState([]);
  const [newComment, setNewComment] = React.useState("");
  const [commentsLoading, setCommentsLoading] = React.useState(false);
  const [confirm, confirmModal] = window.useConfirm();

  // Stream / video state
  const [streamUrl, setStreamUrl] = React.useState(null);
  const [streamType, setStreamType] = React.useState(null);
  // Why the stream is unavailable: 'no_proxy' (has hi-res source, browser
  // can't play it directly) or null (still loading / live / playable).
  const [streamReason, setStreamReason] = React.useState(null);
  const [streamHasSource, setStreamHasSource] = React.useState(false);
  const [streamLoading, setStreamLoading] = React.useState(false);
  const [videoDuration, setVideoDuration] = React.useState(0);
  const [retrying, setRetrying] = React.useState(false);
  const [reprocessing, setReprocessing] = React.useState(null); // 'proxy' | 'thumbnail' | null
  const [filmFrames, setFilmFrames] = React.useState([]);
  const [filmstripLoading, setFilmstripLoading] = React.useState(false);
  const [filmstripKey, setFilmstripKey] = React.useState(0);
  // Player health: 'idle' | 'loading' | 'playing' | 'paused' | 'seeking' | 'waiting' | 'stalled' | 'error'
  const [playerState, setPlayerState] = React.useState('idle');
  const [playerError, setPlayerError] = React.useState(null);
  // Array of {start, end} in milliseconds - populated from HTMLMediaElement.buffered
  const [buffered, setBuffered] = React.useState([]);
  // Wall-clock when waiting/stalled began (so we can show how long it's been hung)
  const [stallStart, setStallStart] = React.useState(null);
  const [stallElapsedMs, setStallElapsedMs] = React.useState(0);
  const videoRef = React.useRef(null);

  const assetId = asset && asset.id;
  const totalMs = videoDuration > 0 ? videoDuration : parseDuration(asset.duration);

  // Fetch stream URL when asset changes
  React.useEffect(() => {
    if (!assetId) return;
    setStreamUrl(null);
    setStreamType(null);
    setStreamReason(null);
    setStreamHasSource(false);
    setVideoDuration(0);
    setCurrentMs(0);
    setPlaying(false);
    setFilmFrames([]);
    setStreamLoading(true);
    window.ZAMPP_API.fetch('/assets/' + assetId + '/stream')
      .then(function(r) {
        if (r && r.hls_url) {
          // Prefer HLS for in-browser playback; `url` stays the MP4 proxy
          // (used by the Premiere plugin importer + as a fallback).
          setStreamUrl(r.hls_url);
          setStreamType('hls');
        } else if (r && r.url) {
          setStreamUrl(r.url);
          setStreamType(r.type || 'mp4');
        } else if (r) {
          // {url: null, reason: 'no_proxy', has_source: true|false}
          setStreamReason(r.reason || null);
          setStreamHasSource(!!r.has_source);
        }
      })
      .catch(function() {})
      .finally(function() { setStreamLoading(false); });
  }, [assetId]);

  // Wire hls.js for live HLS streams
  React.useEffect(() => {
    if (!streamUrl || streamType !== 'hls' || !videoRef.current) return;
    if (!window.Hls) return;
    const hls = new window.Hls();
    hls.loadSource(streamUrl);
    hls.attachMedia(videoRef.current);
    return function() { hls.destroy(); };
  }, [streamUrl, streamType]);

  // Fetch server-side filmstrip (pre-built by filmstrip worker via FFmpeg).
  // Falls back to nothing if not ready yet - user can right-click → Re-generate.
  React.useEffect(() => {
    if (!assetId) return;
    let cancelled = false;
    setFilmFrames([]);
    setFilmstripLoading(true);

    window.ZAMPP_API.fetch('/assets/' + assetId + '/filmstrip')
      .then(function(r) {
        if (cancelled) return;
        if (!r || !r.url) { setFilmstripLoading(false); return; }
        // Fetch the JSON array of base64 frames from the signed S3 URL
        return fetch(r.url)
          .then(function(res) { return res.json(); })
          .then(function(frames) {
            if (!cancelled && Array.isArray(frames) && frames.length) {
              setFilmFrames(frames);
            }
          });
      })
      .catch(function() {})
      .finally(function() { if (!cancelled) setFilmstripLoading(false); });

    return function() { cancelled = true; };
  }, [assetId, filmstripKey]);

  // Fake playback timer - only used when no real video stream
  React.useEffect(() => {
    if (!playing || totalMs <= 0 || streamUrl) return;
    const i = setInterval(function() {
      setCurrentMs(function(t) {
        const next = t + 100;
        if (next >= totalMs) { setPlaying(false); return totalMs; }
        return next;
      });
    }, 100);
    return function() { clearInterval(i); };
  }, [playing, totalMs, streamUrl]);

  const togglePlay = function() {
    if (videoRef.current) {
      if (videoRef.current.paused) { videoRef.current.play(); }
      else { videoRef.current.pause(); }
    } else {
      setPlaying(function(p) { return !p; });
    }
  };

  // Read HTMLMediaElement.buffered into a JSON-friendly array of {start, end} ms.
  // Called from onProgress and onTimeUpdate so the UI tracks buffer growth in real time.
  const updateBuffered = function() {
    const v = videoRef.current;
    if (!v || !v.buffered || v.buffered.length === 0) { setBuffered([]); return; }
    const ranges = [];
    for (let i = 0; i < v.buffered.length; i++) {
      ranges.push({
        start: v.buffered.start(i) * 1000,
        end:   v.buffered.end(i)   * 1000,
      });
    }
    setBuffered(ranges);
  };

  // Tick the stall elapsed counter once per second when we're hung
  React.useEffect(() => {
    if (!stallStart) { setStallElapsedMs(0); return; }
    const i = setInterval(() => setStallElapsedMs(Date.now() - stallStart), 250);
    return () => clearInterval(i);
  }, [stallStart]);

  // #143 - if the player is stalled within 250 ms of EOF for more than 1.2 s,
  // treat it as a clean end. Avoids the silent-freeze users hit when seeking
  // to the last instant of a clip.
  React.useEffect(() => {
    if (!stallStart) return;
    if (!videoRef.current || !totalMs) return;
    const id = setTimeout(() => {
      const v = videoRef.current;
      if (!v) return;
      const posMs = (v.currentTime || 0) * 1000;
      if (totalMs - posMs <= 250 && (playerState === 'waiting' || playerState === 'stalled')) {
        try { v.pause(); } catch (_) {}
        setPlaying(false);
        setPlayerState('paused');
        setStallStart(null);
      }
    }, 1200);
    return () => clearTimeout(id);
  }, [stallStart, totalMs, playerState]);

  const seek = function(ms) {
    // #143 - seeking exactly to `totalMs` parked the playhead one micro-sample
    // past the last decoded frame; the player then asked S3 for a range past
    // EOF and stalled silently. Pull the clamp back 50 ms so the final frames
    // are reachable but the player never asks for bytes past the file size.
    const upperBoundMs = Math.max(0, (totalMs || 0) - 50);
    const clamped = Math.max(0, Math.min(upperBoundMs, ms));
    setCurrentMs(clamped);
    if (videoRef.current) videoRef.current.currentTime = clamped / 1000;
  };

  // Pull a presigned hi-res URL and trigger a browser download with the
  // asset's display name as the filename. Falls back to opening in a new tab.
  const [downloading, setDownloading] = React.useState(false);

  // Gate the download behind a one-time "large file / connection speed"
  // warning, shared with the library via the df.lib.download.warnDismissed
  // localStorage flag. Once dismissed, downloads start without the prompt.
  const dismissForeverRef = React.useRef(false);
  const requestDownload = async function() {
    if (downloading) return;
    let dismissed = false;
    try { dismissed = localStorage.getItem('df.lib.download.warnDismissed') === '1'; } catch (_) {}
    if (!dismissed) {
      dismissForeverRef.current = false;
      const ok = await confirm({
        title: 'Download original',
        message: <div>
          <div style={{ fontSize: 13, color: 'var(--text-2)', lineHeight: 1.5, marginBottom: 10 }}>
            You're about to download the full-length original master for <b>{asset.name}</b>.
            These files can be very large and download speed depends on your connection.
          </div>
          <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12.5, color: 'var(--text-3)', cursor: 'pointer' }}>
            <input
              type="checkbox"
              onChange={function(e) { dismissForeverRef.current = e.target.checked; }}
            />
            Don't show this warning again
          </label>
        </div>,
        confirmLabel: 'Download',
        cancelLabel: 'Cancel',
        danger: false,
      });
      if (!ok) return;
      // Persist the dismissal only after the user confirms the download.
      if (dismissForeverRef.current) {
        try { localStorage.setItem('df.lib.download.warnDismissed', '1'); } catch (_) {}
      }
    }
    downloadHires();
  };

  const downloadHires = function() {
    if (downloading) return;
    setDownloading(true);
    window.ZAMPP_API.fetch('/assets/' + assetId + '/hires')
      .then(function(r) {
        if (!r || !r.url) { window.alert('No hi-res source available for this asset.'); return; }
        const a = document.createElement('a');
        a.href = r.url;
        a.download = r.filename || (asset.name + '.' + (r.ext || 'mov'));
        a.target  = '_blank';
        a.rel     = 'noopener';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
      })
      .catch(function(e) { window.alert('Download failed: ' + (e.message || 'unknown error')); })
      .finally(function() { setDownloading(false); });
  };

  // Right-click style menu on the kebab icon - delete, copy ID.
  const [menuOpen, setMenuOpen] = React.useState(false);
  const moreBtnRef = React.useRef(null);
  React.useEffect(function() {
    if (!menuOpen) return;
    const close = function() { setMenuOpen(false); };
    window.addEventListener('click', close);
    return function() { window.removeEventListener('click', close); };
  }, [menuOpen]);

  const copyId = function() {
    if (navigator.clipboard) navigator.clipboard.writeText(assetId).catch(function() {});
    setMenuOpen(false);
  };
  const deleteAsset = async function() {
    setMenuOpen(false);
    if (!(await confirm({
      title: 'Delete asset?',
      message: 'Delete "' + (asset.name || assetId) + '" permanently?\nRemoves DB row + S3 objects. Cannot be undone.',
    }))) return;
    window.ZAMPP_API.fetch('/assets/' + assetId + '?hard=true', { method: 'DELETE' })
      .then(function() { onClose && onClose(); })
      .catch(function(e) { window.alert('Delete failed: ' + e.message); });
  };

  const retryProcessing = function() {
    if (retrying) return;
    setRetrying(true);
    window.ZAMPP_API.fetch('/assets/' + assetId + '/retry', { method: 'POST' })
      .then(function() {
        window.alert('Re-queued for processing. The proxy worker will pick it up shortly; refresh in a minute to see the player.');
      })
      .catch(function(e) { window.alert('Retry failed: ' + (e.message || 'unknown error')); })
      .finally(function() { setRetrying(false); });
  };

  const reprocessJob = function(type) {
    if (reprocessing) return;
    setReprocessing(type);
    window.ZAMPP_API.fetch('/assets/' + assetId + '/reprocess?type=' + type, { method: 'POST' })
      .then(function() {
        window.alert((type === 'proxy' ? 'Proxy' : 'Thumbnail') + ' job queued. Refresh in a moment to see the result.');
      })
      .catch(function(e) { window.alert('Reprocess failed: ' + (e.message || 'unknown error')); })
      .finally(function() { setReprocessing(null); });
  };

  const regenFilmstrip = function() {
    window.ZAMPP_API.fetch('/assets/' + assetId + '/reprocess?type=filmstrip', { method: 'POST' })
      .then(function() { window.alert('Filmstrip job queued: it will appear automatically when ready.'); })
      .catch(function(e) { window.alert('Failed to queue filmstrip: ' + (e.message || 'unknown error')); });
  };

  // Map a /assets/:id/comments row into the legacy shape the consumer
  // components (PlaybackBar pins, FilmStrip pins, comment list) already expect.
  function _normalizeComment(row) {
    const frameMs = row.frame_ms != null ? row.frame_ms : 0;
    const real = row.created_at
      ? window.ZAMPP_API.fmtRelative(row.created_at)
      : 'just now';
    return {
      id:       row.id,
      who:      row.author_name || window.ZAMPP_DATA?.ME?.name || 'You',
      avatar:   row.author_initials || (row.author_name ? row.author_name.slice(0, 2).toUpperCase() : (window.ZAMPP_DATA?.ME?.initials || 'ZG')),
      time:     msToTimecode(frameMs),
      frame_ms: frameMs,
      real,
      text:     row.body,
      resolved: !!row.resolved,
      frame:    Math.floor(frameMs / 1000 * 30),
    };
  }

  // Load persisted comments whenever the open asset changes.
  React.useEffect(() => {
    if (!assetId) return;
    setCommentsLoading(true);
    window.ZAMPP_API.fetch('/assets/' + assetId + '/comments')
      .then(function(r) { setComments(((r && r.comments) || []).map(_normalizeComment)); })
      .catch(function() { setComments([]); })
      .finally(function() { setCommentsLoading(false); });
  }, [assetId]);

  const addComment = function() {
    const text = newComment.trim();
    if (!text) return;
    setNewComment('');
    window.ZAMPP_API.fetch('/assets/' + assetId + '/comments', {
      method: 'POST',
      body: JSON.stringify({ body: text, frame_ms: Math.round(currentMs) }),
    })
      .then(function(row) {
        setComments(function(c) { return [...c, _normalizeComment(row)]; });
      })
      .catch(function(e) {
        window.alert('Could not post comment: ' + (e.message || 'unknown error'));
        setNewComment(text);
      });
  };

  const toggleResolved = function(c) {
    window.ZAMPP_API.fetch('/assets/' + assetId + '/comments/' + c.id, {
      method: 'PATCH',
      body: JSON.stringify({ resolved: !c.resolved }),
    })
      .then(function(row) {
        setComments(function(prev) { return prev.map(x => x.id === c.id ? _normalizeComment(row) : x); });
      })
      .catch(function() {});
  };

  const deleteComment = async function(c) {
    if (!(await confirm({ title: 'Delete comment?', message: 'Delete this comment?' }))) return;
    window.ZAMPP_API.fetch('/assets/' + assetId + '/comments/' + c.id, { method: 'DELETE' })
      .then(function() {
        setComments(function(prev) { return prev.filter(x => x.id !== c.id); });
      })
      .catch(function(e) { window.alert('Delete failed: ' + e.message); });
  };

  const visibleComments = comments.filter(function(c) { return showResolved || !c.resolved; });

  // The player overlay text reflects three states: live processing,
  // explicit error, "ingested but never proxied" (the SRT-archive case),
  // or generic not-ready.
  const isMissingProxy = !streamUrl && streamReason === 'no_proxy' && streamHasSource;
  const statusMessage =
    asset.status === 'processing' ? 'Processing…' :
    asset.status === 'live'       ? 'Live recording in progress' :
    asset.status === 'error'      ? 'Processing failed' :
    isMissingProxy                ? 'This clip was ingested but never got a browser-playable proxy.' :
                                    'Preview not yet available';

  // "Retry processing" (for status=error) and "Generate proxy" (for
  // status=ready/archived but missing proxy) both call the same /retry
  // endpoint. Label depends on what the user is staring at.
  const canRetry = (asset.status === 'error') || isMissingProxy;
  const retryLabel = isMissingProxy ? 'Generate proxy' : 'Retry processing';

  return (
    <div className="asset-detail fade-in">
      {confirmModal}
      <div className="asset-detail-header">
        <button className="icon-btn" aria-label="Back" onClick={onClose}><Icon name="arrowLeft" /></button>
        <div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span style={{ fontWeight: 600, fontSize: 14 }}>{asset.name}</span>
            <StatusDot status={asset.status} />
          </div>
          <div style={{ fontSize: 11.5, color: "var(--text-3)", display: "flex", gap: 6 }}>
            <span>{asset.project}</span><span>·</span><span>updated {asset.updated}</span>
          </div>
        </div>
        <div style={{ flex: 1 }} />
        {asset.original_s3_key && (
          <button className="btn ghost sm" onClick={requestDownload} disabled={downloading} title="Download the hi-res master file">
            <Icon name="download" />{downloading ? 'Preparing…' : 'Download'}
          </button>
        )}
        <div style={{ position: 'relative' }}>
          <button ref={moreBtnRef} className="icon-btn" aria-label="More actions" onClick={function(e) { e.stopPropagation(); setMenuOpen(function(v) { return !v; }); }}>
            <Icon name="more" />
          </button>
          {menuOpen && (
            <div className="row-menu" style={{ right: 0, left: 'auto' }} onClick={function(e) { e.stopPropagation(); }}>
              <button onClick={copyId}><Icon name="library" size={11} />Copy asset ID</button>
              <button onClick={function() { setMenuOpen(false); reprocessJob('proxy'); }} disabled={!!reprocessing}>
                <Icon name="jobs" size={11} />{reprocessing === 'proxy' ? 'Queuing…' : 'Re-generate proxy'}
              </button>
              <button onClick={function() { setMenuOpen(false); reprocessJob('thumbnail'); }} disabled={!!reprocessing}>
                <Icon name="jobs" size={11} />{reprocessing === 'thumbnail' ? 'Queuing…' : 'Re-generate thumbnail'}
              </button>
              <button className="danger" onClick={deleteAsset}><Icon name="trash" size={11} />Delete permanently</button>
            </div>
          )}
        </div>
      </div>

      <div className="asset-detail-body">
        <div className="player-col">
          <div className="player">
            <div className="player-canvas">
              {streamUrl ? (
                <video
                  key={streamUrl}
                  ref={videoRef}
                  src={streamType !== 'hls' ? streamUrl : undefined}
                  style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block', background: '#000' }}
                  onTimeUpdate={function() {
                    if (videoRef.current) setCurrentMs(videoRef.current.currentTime * 1000);
                    updateBuffered();
                  }}
                  onLoadedMetadata={function() {
                    if (videoRef.current) setVideoDuration(videoRef.current.duration * 1000);
                    setPlayerState('paused');
                  }}
                  onProgress={updateBuffered}
                  onPlay={function() { setPlaying(true); setPlayerState('playing'); setStallStart(null); }}
                  onPlaying={function() { setPlayerState('playing'); setStallStart(null); setPlayerError(null); }}
                  onPause={function() { setPlaying(false); setPlayerState('paused'); setStallStart(null); }}
                  onSeeking={function() { setPlayerState('seeking'); setStallStart(Date.now()); }}
                  onSeeked={function() { setStallStart(null); if (videoRef.current?.paused) setPlayerState('paused'); }}
                  onWaiting={function() { setPlayerState('waiting'); if (!stallStart) setStallStart(Date.now()); }}
                  onStalled={function() { setPlayerState('stalled'); if (!stallStart) setStallStart(Date.now()); }}
                  onCanPlay={function() { setStallStart(null); }}
                  onCanPlayThrough={function() { setStallStart(null); }}
                  onError={function(e) {
                    const err = videoRef.current?.error;
                    const msg = err ? `MediaError code=${err.code} message=${err.message || '(none)'}` : 'unknown error';
                    setPlayerState('error');
                    setPlayerError(msg);
                    console.error('[player]', msg, e);
                  }}
                  onEnded={function() { setPlaying(false); setPlayerState('paused'); }}
                />
              ) : (
                <React.Fragment>
                  <FauxFrame seed={asset.seed || 1} />
                  <div className="scanlines" />
                  <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                    <div style={{ textAlign: 'center', color: 'var(--text-3)', maxWidth: 360, padding: '0 16px' }}>
                      {streamLoading ? (
                        <div style={{ fontSize: 13 }}>Loading…</div>
                      ) : (
                        <React.Fragment>
                          <div style={{ fontSize: 13, marginBottom: 8 }}>{statusMessage}</div>
                          <StatusDot status={asset.status} />
                          {canRetry && (
                            <button
                              className="btn primary sm"
                              style={{ marginTop: 14, display: 'inline-flex' }}
                              onClick={retryProcessing}
                              disabled={retrying}
                            >
                              {retrying ? 'Queueing…' : retryLabel}
                            </button>
                          )}
                          {isMissingProxy && (
                            <div style={{ fontSize: 11, color: 'var(--text-4)', marginTop: 10 }}>
                              The hi-res master is in storage. We just need to transcode a browser-playable copy.
                            </div>
                          )}
                        </React.Fragment>
                      )}
                    </div>
                  </div>
                  {!streamLoading && !playing && totalMs > 0 && (
                    <button className="player-play-overlay" onClick={togglePlay}>
                      <Icon name="play" size={28} />
                    </button>
                  )}
                </React.Fragment>
              )}

              <div className="player-overlay-markers">
                {visibleComments
                  .filter(function(c) { return Math.abs(parseDuration(c.time) - currentMs) < 200; })
                  .map(function(c) {
                    return (
                      <div key={c.id} className="player-pin">
                        <div className="player-pin-avatar">{c.avatar}</div>
                        <div className="player-pin-bubble">{c.text}</div>
                      </div>
                    );
                  })}
              </div>
              {asset.status === "live" && (
                <div style={{ position: "absolute", top: 12, left: 12 }}>
                  <span className="badge live">LIVE · REC</span>
                </div>
              )}
              {/* Player health badge: shows when waiting/stalled so the freeze is visible */}
              {streamUrl && (playerState === 'waiting' || playerState === 'stalled' || playerState === 'seeking' || playerState === 'error') && (
                <div style={{ position: "absolute", top: 12, right: 12, display: "flex", gap: 6, alignItems: "center" }}>
                  <span className={'badge ' + (playerState === 'error' ? 'danger' : playerState === 'stalled' ? 'warning' : 'neutral')}>
                    {playerState === 'seeking'  && 'SEEKING'}
                    {playerState === 'waiting'  && 'BUFFERING'}
                    {playerState === 'stalled'  && 'STALLED'}
                    {playerState === 'error'    && 'ERROR'}
                    {stallElapsedMs > 500 && playerState !== 'error' && ` · ${(stallElapsedMs/1000).toFixed(1)}s`}
                  </span>
                  {playerError && (
                    <span style={{ fontSize: 10.5, color: 'var(--text-3)', background: 'rgba(0,0,0,0.6)', padding: '2px 6px', borderRadius: 3, fontFamily: 'var(--font-mono)' }}>
                      {playerError}
                    </span>
                  )}
                </div>
              )}
              {totalMs > 0 && (
                <div className="player-tc">
                  <span className="mono">{msToTimecode(currentMs)}</span>
                  <span style={{ opacity: 0.5 }}> / {asset.duration}</span>
                </div>
              )}
            </div>

            {totalMs > 0 && (
              <div className="player-controls">
                <button className="icon-btn" aria-label={playing ? 'Pause' : 'Play'} onClick={togglePlay}>
                  <Icon name={playing ? "pause" : "play"} size={14} />
                </button>
                <span className="mono" style={{ fontSize: 11.5, color: "var(--text-2)", minWidth: 70 }}>{msToTimecode(currentMs)}</span>
                <PlaybackBar current={currentMs} total={totalMs} onSeek={seek} comments={visibleComments} buffered={buffered} />
                <span className="mono" style={{ fontSize: 11.5, color: "var(--text-3)", minWidth: 70, textAlign: "right" }}>{asset.duration}</span>
                {streamUrl && (
                  <React.Fragment>
                    <div style={{ width: 1, height: 18, background: "var(--border-strong)" }} />
                    <button
                      className="icon-btn"
                      title="Toggle mute"
                      aria-label="Toggle mute"
                      onClick={function() { if (videoRef.current) { videoRef.current.muted = !videoRef.current.muted; } }}>
                      <Icon name="audio" size={14} />
                    </button>
                    <button
                      className="icon-btn"
                      title="Toggle fullscreen"
                      aria-label="Toggle fullscreen"
                      onClick={function() { if (videoRef.current && videoRef.current.requestFullscreen) videoRef.current.requestFullscreen().catch(function() {}); }}>
                      <Icon name="layout" size={14} />
                    </button>
                  </React.Fragment>
                )}
              </div>
            )}

            {totalMs > 0 && (
              <FilmStrip
                seed={asset.seed || 1}
                current={currentMs}
                total={totalMs}
                onSeek={seek}
                comments={visibleComments}
                frames={filmFrames}
                loading={filmstripLoading}
                 onRegenFilmstrip={regenFilmstrip}
                 onRegenProxy={function() { reprocessJob('proxy'); }}
                 reprocessing={reprocessing}
               />
             )}
           </div>

           <div className="asset-detail-tabs">
            <button className={tab === "comments" ? "active" : ""} onClick={function() { setTab("comments"); }}>
              Comments <span className="count">{comments.length}</span>
            </button>
            <button className={tab === "files" ? "active" : ""} onClick={function() { setTab("files"); }}>
              Files
            </button>
            <button className={tab === "metadata" ? "active" : ""} onClick={function() { setTab("metadata"); }}>
              Metadata
            </button>
            <div style={{ flex: 1 }} />
            {tab === "comments" && (
              <label className="tiny-toggle">
                <input type="checkbox" checked={showResolved} onChange={function(e) { setShowResolved(e.target.checked); }} /> Show resolved
              </label>
            )}
          </div>

          <div className="asset-detail-content">
            {tab === "comments" && (
              commentsLoading ? (
                <div style={{ padding: '24px', textAlign: 'center', color: 'var(--text-3)', fontSize: 12.5 }}>Loading comments…</div>
              ) : (
                <CommentsList
                  comments={visibleComments}
                  onSeek={function(c) { seek(c.frame_ms != null ? c.frame_ms : parseDuration(c.time)); }}
                  onResolve={function(id) {
                    const c = comments.find(x => x.id === id);
                    if (c) toggleResolved(c);
                  }}
                  onDelete={function(id) {
                    const c = comments.find(x => x.id === id);
                    if (c) deleteComment(c);
                  }}
                />
              )
            )}
            {tab === "files" && (
              <FilesTab
                asset={asset}
                filmFrames={filmFrames}
                filmstripLoading={filmstripLoading}
                streamUrl={streamUrl}
                reprocessing={reprocessing}
                 onRegenProxy={function() { reprocessJob('proxy'); }}
                 onRegenThumbnail={function() { reprocessJob('thumbnail'); }}
                 onRegenFilmstrip={regenFilmstrip}
              />
            )}
            {tab === "metadata" && <MetadataTab asset={asset} />}
          </div>
        </div>

        <div className="comment-composer-col">
          <CommentComposer
            asset={asset}
            currentMs={currentMs}
            value={newComment}
            onChange={setNewComment}
            onSubmit={addComment}
          />
        </div>
      </div>
    </div>
  );
}

function PlaybackBar({ current, total, onSeek, comments, buffered }) {
  const ref = React.useRef(null);
  const handle = function(e) {
    const r = ref.current.getBoundingClientRect();
    const p = (e.clientX - r.left) / r.width;
    onSeek(Math.max(0, Math.min(1, p)) * total);
  };
  const pct = total > 0 ? (current / total) * 100 : 0;
  const bufferedRanges = Array.isArray(buffered) ? buffered : [];
  return (
    <div className="playback-bar" ref={ref} onClick={handle}>
      {/* Buffered byte ranges: translucent grey segments showing what the browser has loaded */}
      {total > 0 && bufferedRanges.map((br, i) => {
        const left  = Math.max(0, (br.start / total) * 100);
        const right = Math.min(100, (br.end   / total) * 100);
        if (right <= left) return null;
        return (
          <div key={'buf-' + i} style={{
            position: 'absolute', top: 0, bottom: 0,
            left: left + '%', width: (right - left) + '%',
            background: 'rgba(255,255,255,0.18)',
            borderRadius: 'inherit',
            pointerEvents: 'none',
          }} />
        );
      })}
      <div className="playback-fill" style={{ width: pct + '%' }} />
      <div className="playback-handle" style={{ left: pct + '%' }} />
      {comments.map(function(c) {
        const ct = parseDuration(c.time);
        if (!ct || total <= 0) return null;
        const x = (ct / total) * 100;
        return (
          <div
            key={c.id}
            className={'playback-marker ' + (c.resolved ? 'resolved' : '')}
            style={{ left: x + '%' }}
            onClick={function(e) { e.stopPropagation(); onSeek(ct); }}
          >
            <span className="marker-avatar">{c.avatar}</span>
          </div>
        );
      })}
    </div>
  );
}

function FilmStrip({ seed, current, total, onSeek, comments, frames, loading, onRegenFilmstrip, onRegenProxy, reprocessing }) {
  const ref = React.useRef(null);
  const [ctx, setCtx] = React.useState(null);
  const fallbackFrames = 28;

  const handle = function(e) {
    if (!ref.current || total <= 0) return;
    const r = ref.current.getBoundingClientRect();
    onSeek(((e.clientX - r.left) / r.width) * total);
  };

  const handleContextMenu = function(e) {
    e.preventDefault();
    setCtx({ x: e.clientX, y: e.clientY });
  };

  React.useEffect(function() {
    if (!ctx) return;
    var close = function() { setCtx(null); };
    window.addEventListener('click', close);
    window.addEventListener('contextmenu', close);
    window.addEventListener('scroll', close, true);
    return function() {
      window.removeEventListener('click', close);
      window.removeEventListener('contextmenu', close);
      window.removeEventListener('scroll', close, true);
    };
  }, [ctx]);

  const pct = total > 0 ? (current / total) * 100 : 0;
  const items = Array.isArray(frames) && frames.length ? frames : Array.from({ length: fallbackFrames }).map(function(_, i) { return null; });
  return (
    <div className="filmstrip-wrap">
      <div className="filmstrip" ref={ref} onClick={handle} onContextMenu={handleContextMenu}>
        {items.map(function(src, i) {
          return (
            <div key={i} className="film-frame" style={src ? undefined : { background: _FRAME_GRADIENTS[(seed + i) % _FRAME_GRADIENTS.length] }}>
              {src ? (
                <img src={src} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
              ) : (
                <FauxFrame seed={(seed + i) % 6} />
              )}
            </div>
          );
        })}
        <div className="filmstrip-playhead" style={{ left: pct + '%' }} />
        {comments.map(function(c) {
          const ct = parseDuration(c.time);
          if (!ct || total <= 0) return null;
          const x = (ct / total) * 100;
          return (
            <div key={c.id} className={'filmstrip-pin ' + (c.resolved ? 'resolved' : '')} style={{ left: x + '%' }}>
              <span className="filmstrip-pin-avatar">{c.avatar}</span>
            </div>
          );
        })}
      </div>
      <div className="filmstrip-tc">
        {[0, 0.25, 0.5, 0.75, 1].map(function(p) {
          return <span key={p} className="mono">{msToTimecode(p * total)}</span>;
        })}
      </div>
      {loading && <div style={{ fontSize: 11, color: 'var(--text-3)', marginTop: 6 }}>Building filmstrip…</div>}

      {ctx && (
        <div className="row-menu" style={{ position: 'fixed', top: ctx.y, left: ctx.x, zIndex: 9999 }} onClick={function(e) { e.stopPropagation(); }}>
          <button onClick={function() { setCtx(null); onRegenFilmstrip && onRegenFilmstrip(); }}>
            <Icon name="jobs" size={11} />Re-generate filmstrip
          </button>
          <button onClick={function() { setCtx(null); onRegenProxy && onRegenProxy(); }} disabled={!!reprocessing}>
            <Icon name="jobs" size={11} />{reprocessing === 'proxy' ? 'Queuing…' : 'Re-generate proxy'}
          </button>
        </div>
      )}
    </div>
  );
}

function CommentsList({ comments, onSeek, onResolve, onDelete }) {
  if (comments.length === 0) {
    return <div style={{ padding: 40, textAlign: "center", color: "var(--text-3)" }}>No comments yet. Add one below to mark a frame.</div>;
  }
  return (
    <div className="comments-list">
      {comments.map(function(c) {
        return (
          <div key={c.id} className={'comment ' + (c.resolved ? 'resolved' : '')}>
            <div className="comment-avatar avatar" style={{ background: avatarColor(c.avatar) }}>{c.avatar}</div>
            <div className="comment-body">
              <div className="comment-head">
                <span className="comment-who">{c.who}</span>
                <button className="comment-time" onClick={function() { onSeek(c); }} title="Jump to this frame"><Icon name="clock" size={10} />{c.time}</button>
                <span className="comment-when">{c.real}</span>
                <div style={{ flex: 1 }} />
                <button className="comment-action" onClick={function() { onResolve(c.id); }}
                  title={c.resolved ? 'Reopen' : 'Mark resolved'}>
                  <Icon name={c.resolved ? "refresh" : "check"} size={12} />
                </button>
                {onDelete && (
                  <button className="comment-action" onClick={function() { onDelete(c.id); }} title="Delete comment">
                    <Icon name="trash" size={12} />
                  </button>
                )}
              </div>
              <div className="comment-text">{c.text}</div>
              {c.resolved && <div className="comment-resolved">✓ Resolved</div>}
            </div>
          </div>
        );
      })}
    </div>
  );
}

function CommentComposer({ asset, currentMs, value, onChange, onSubmit }) {
  const me = window.ZAMPP_DATA?.ME;
  const myInitials = me?.initials || 'ZG';
  return (
    <div className="comment-composer">
      <div className="composer-head">
        <div className="avatar" style={{ background: avatarColor(myInitials), width: 22, height: 22, fontSize: 10 }}>{myInitials}</div>
        <span style={{ fontSize: 12, color: 'var(--text-2)', fontWeight: 500 }}>{me?.name || 'You'}</span>
      </div>
      <div className="composer-tag">
        <span className="badge accent">@ {msToTimecode(currentMs)}</span>
      </div>
      <textarea
        className="composer-input"
        placeholder="Leave a comment at this timecode…"
        value={value}
        onChange={function(e) { onChange(e.target.value); }}
        onKeyDown={function(e) { if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) onSubmit(); }}
      />
      <div className="composer-bar">
        <span style={{ flex: 1 }} />
        <span style={{ fontSize: 10.5, color: "var(--text-3)" }}>⌘↵ to send</span>
        <button className="btn primary sm" onClick={onSubmit} disabled={!value.trim()}>Comment</button>
      </div>
    </div>
  );
}

function FilesTab({ asset, filmFrames, filmstripLoading, streamUrl, reprocessing, onRegenProxy, onRegenThumbnail, onRegenFilmstrip }) {
  const hasProxy     = !!asset.proxy_s3_key;
  const hasHires     = !!asset.original_s3_key;
  const hasThumb     = !!asset.thumbnail_s3_key;
  const hasFilmstrip = !!asset.filmstrip_s3_key;
  const filmstripReady = Array.isArray(filmFrames) && filmFrames.length > 0;

  // Thumbnail endpoint returns a signed URL, not raw image bytes
  const [thumbUrl, setThumbUrl] = React.useState(null);
  React.useEffect(function() {
    if (!hasThumb) return;
    window.ZAMPP_API.fetch('/assets/' + asset.id + '/thumbnail')
      .then(function(r) { if (r && r.url) setThumbUrl(r.url); })
      .catch(function() {});
  }, [asset.id, hasThumb]);

  // Rows: label | status badge | path | action button
  const FileRow = function({ label, present, path, icon, actionLabel, onAction, disabled, children }) {
    return (
      <div style={{ border: '1px solid var(--border)', borderRadius: 6, background: 'var(--bg-2)', overflow: 'hidden', marginBottom: 8 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px', borderBottom: children ? '1px solid var(--border)' : 'none' }}>
          <Icon name={icon || 'jobs'} size={13} />
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 2 }}>
              <span style={{ fontWeight: 600, fontSize: 13 }}>{label}</span>
              {present
                ? <span className="badge success">ready</span>
                : <span className="badge neutral">missing</span>}
            </div>
            {path && (
              <div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={path}>
                {path}
              </div>
            )}
          </div>
          {actionLabel && onAction && (
            <button className="btn ghost sm" onClick={onAction} disabled={!!disabled}>
              {disabled ? 'Queuing…' : actionLabel}
            </button>
          )}
        </div>
        {children && <div style={{ padding: '10px 12px' }}>{children}</div>}
      </div>
    );
  };

  return (
    <div style={{ padding: '12px 0' }}>

      {/* Proxy */}
      <FileRow
        label="Proxy (browser playback)"
        present={hasProxy}
        path={asset.proxy_s3_key || null}
        icon="play"
        actionLabel={reprocessing === 'proxy' ? 'Queuing…' : 'Re-generate'}
        onAction={onRegenProxy}
        disabled={reprocessing === 'proxy'}
      />

      {/* Hi-res */}
      <FileRow
        label="Hi-res master"
        present={hasHires}
        path={asset.original_s3_key || null}
        icon="download"
        actionLabel={null}
        onAction={null}
      />

      {/* Thumbnail */}
      <FileRow
        label="Thumbnail"
        present={hasThumb}
        path={asset.thumbnail_s3_key || null}
        icon="library"
        actionLabel={reprocessing === 'thumbnail' ? 'Queuing…' : 'Re-generate'}
        onAction={onRegenThumbnail}
        disabled={reprocessing === 'thumbnail'}
      >
        {thumbUrl && (
          <img
            src={thumbUrl}
            alt="Thumbnail"
            style={{ maxHeight: 100, borderRadius: 4, display: 'block' }}
            onError={function(e) { e.target.style.display = 'none'; }}
          />
        )}
      </FileRow>

      {/* Filmstrip */}
      <FileRow
        label="Filmstrip"
        present={hasFilmstrip}
        path={hasFilmstrip ? (asset.filmstrip_s3_key || null) : filmstripLoading ? 'Fetching…' : 'Not generated yet: right-click filmstrip or click Re-generate'}
        icon="editor"
        actionLabel="Re-generate"
        onAction={onRegenFilmstrip}
        disabled={filmstripLoading}
      >
        {filmstripReady && (
          <div style={{ display: 'flex', gap: 2, overflowX: 'auto', paddingBottom: 2 }}>
            {filmFrames.filter(Boolean).slice(0, 14).map(function(src, i) {
              return (
                <img key={i} src={src} alt="" style={{ width: 80, height: 45, objectFit: 'cover', borderRadius: 3, flexShrink: 0 }} />
              );
            })}
          </div>
        )}
        {!filmstripReady && filmstripLoading && (
          <div style={{ fontSize: 11.5, color: 'var(--text-3)' }}>Fetching filmstrip from server…</div>
        )}
      </FileRow>

    </div>
  );
}

function MetadataTab({ asset }) {
  var rows = [
    { k: "Filename", v: asset.name },
    { k: "Duration", v: asset.duration || '·' },
    { k: "Resolution", v: asset.res || '·' },
    { k: "Codec", v: asset.codec || '·' },
    { k: "File size", v: asset.size || '·' },
    { k: "Status", v: asset.status || '·' },
    { k: "Updated", v: asset.updated || '·' },
    { k: "Project", v: asset.project || '·' },
  ];
  var audioMeta = asset.audio_metadata;
  if (audioMeta && Array.isArray(audioMeta) && audioMeta.length > 0) {
    rows.push({ k: "Audio tracks", v: audioMeta.length });
    audioMeta.forEach(function(tr, i) {
      var label = tr.title || ('Track ' + (tr.index != null ? tr.index : i + 1));
      var ch = tr.channel_layout || (tr.channels ? (tr.channels === 1 ? 'mono' : tr.channels === 2 ? 'stereo' : tr.channels + 'ch') : '·');
      var parts = [tr.codec || '·', ch, tr.sample_rate ? (tr.sample_rate / 1000).toFixed(1) + ' kHz' : '·', tr.bit_depth ? tr.bit_depth + '-bit' : '·', tr.bit_rate ? Math.round(tr.bit_rate / 1000) + ' kbps' : '·'];
      if (tr.language) parts.push(tr.language);
      rows.push({ k: "  " + label, v: parts.join(' · ') });
    });
  }
  return (
    <div className="meta-table">
      {rows.map(function(r) {
        return (
          <div key={r.k} className="meta-row">
            <div className="meta-k">{r.k}</div>
            <div className="meta-v">{r.v}</div>
          </div>
        );
      })}
    </div>
  );
}

var _AUDIO_TRACK_COLORS = [
  'var(--accent)',
  'var(--purple)',
  'var(--success)',
  'var(--warning)',
  'var(--danger)',
  '#6B7280',
  '#F472B6',
  '#34D399',
];

function AudioTab({ asset }) {
  var assetId = asset && asset.id;
  var [tracks, setTracks] = React.useState([]);
  var [loading, setLoading] = React.useState(true);
  var [masterVol, setMasterVol] = React.useState(100);
  var [trackState, setTrackState] = React.useState({});
  var [levels, setLevels] = React.useState({});
  var analyserRef = React.useRef(null);
  var rafRef = React.useRef(null);
  var audioCtxRef = React.useRef(null);
  var sourceRef = React.useRef(null);

  React.useEffect(function() {
    if (!assetId) return;
    var cancelled = false;
    setLoading(true);
    window.ZAMPP_API.fetch('/assets/' + assetId + '/audio')
      .then(function(r) {
        if (cancelled) return;
        var t = (r && r.tracks) || [];
        setTracks(t);
        var initial = {};
        t.forEach(function(tr, i) {
          initial[i] = { muted: false, solo: false, volume: 100 };
        });
        setTrackState(initial);
      })
      .catch(function() { setTracks([]); })
      .finally(function() { if (!cancelled) setLoading(false); });
    return function() { cancelled = true; };
  }, [assetId]);

  var toggleMute = function(i) {
    setTrackState(function(prev) {
      var s = Object.assign({}, prev);
      s[i] = Object.assign({}, s[i] || { muted: false, solo: false, volume: 100 });
      s[i].muted = !s[i].muted;
      return s;
    });
  };

  var toggleSolo = function(i) {
    setTrackState(function(prev) {
      var s = Object.assign({}, prev);
      s[i] = Object.assign({}, s[i] || { muted: false, solo: false, volume: 100 });
      s[i].solo = !s[i].solo;
      return s;
    });
  };

  var setTrackVol = function(i, v) {
    setTrackState(function(prev) {
      var s = Object.assign({}, prev);
      s[i] = Object.assign({}, s[i] || { muted: false, solo: false, volume: 100 });
      s[i].volume = v;
      return s;
    });
  };

  var anySolo = tracks.some(function(_, i) { return trackState[i] && trackState[i].solo; });

  React.useEffect(function() {
    var video = document.querySelector('.player-canvas video');
    if (!video || !window.AudioContext) return;
    var ctx, source, analyser, leftData, rightData;
    try {
      ctx = new (window.AudioContext || window.webkitAudioContext)();
      source = ctx.createMediaElementSource(video);
      analyser = ctx.createAnalyser();
      analyser.fftSize = 256;
      analyser.smoothingTimeConstant = 0.8;
      var splitter = ctx.createChannelSplitter(2);
      source.connect(analyser);
      source.connect(ctx.destination);
      analyser.connect(splitter);
      var leftAnalyser = ctx.createAnalyser();
      leftAnalyser.fftSize = 256;
      leftAnalyser.smoothingTimeConstant = 0.8;
      var rightAnalyser = ctx.createAnalyser();
      rightAnalyser.fftSize = 256;
      rightAnalyser.smoothingTimeConstant = 0.8;
      splitter.connect(leftAnalyser, 0);
      splitter.connect(rightAnalyser, 1);
      audioCtxRef.current = ctx;
      sourceRef.current = source;
      analyserRef.current = { left: leftAnalyser, right: rightAnalyser };
      leftData = new Uint8Array(leftAnalyser.frequencyBinCount);
      rightData = new Uint8Array(rightAnalyser.frequencyBinCount);
    } catch (e) {
      return;
    }
    var running = true;
    var tick = function() {
      if (!running) return;
      if (analyserRef.current) {
        analyserRef.current.left.getByteFrequencyData(leftData);
        analyserRef.current.right.getByteFrequencyData(rightData);
        var lAvg = 0, rAvg = 0;
        for (var j = 0; j < leftData.length; j++) { lAvg += leftData[j]; rAvg += rightData[j]; }
        lAvg = lAvg / leftData.length / 255;
        rAvg = rAvg / rightData.length / 255;
        setLevels({ L: lAvg, R: rAvg });
      }
      rafRef.current = requestAnimationFrame(tick);
    };
    tick();
    return function() {
      running = false;
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      try { ctx.close(); } catch (_) {}
    };
  }, [assetId, tracks.length]);

  if (loading) {
    return <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-3)', fontSize: 12.5 }}>Loading audio…</div>;
  }

  if (!tracks.length) {
    return (
      <div style={{ padding: 24, textAlign: 'center', color: 'var(--text-3)', fontSize: 12.5 }}>
        <Icon name="audio" size={20} style={{ opacity: 0.4, display: 'block', margin: '0 auto 8px' }} />
        No audio tracks found. Either this asset has no audio, or it has not been probed yet.
      </div>
    );
  }

  return (
    <div className="audio-tab">
      <div className="audio-tracks">
        {tracks.map(function(tr, i) {
          var st = trackState[i] || { muted: false, solo: false, volume: 100 };
          var isAudible = st.muted ? false : (anySolo ? st.solo : true);
          var color = _AUDIO_TRACK_COLORS[i % _AUDIO_TRACK_COLORS.length];
          var chLabel = tr.channel_layout || (tr.channels ? (tr.channels === 1 ? 'mono' : tr.channels === 2 ? 'stereo' : tr.channels + 'ch') : '·');
          var trackName = tr.title || ('Track ' + (tr.index != null ? tr.index : i + 1));
          var langTag = tr.language ? <span className="badge neutral" style={{ marginLeft: 6 }}>{tr.language}</span> : null;
          var codecLabel = tr.codec || '·';
          var srLabel = tr.sample_rate ? (tr.sample_rate / 1000).toFixed(1) + ' kHz' : '·';
          var bdLabel = tr.bit_depth ? tr.bit_depth + '-bit' : '·';
          var brLabel = tr.bit_rate ? Math.round(tr.bit_rate / 1000) + ' kbps' : '·';

          return (
            <div key={i} className={'audio-track' + (isAudible ? '' : ' muted')}>
              <div className="audio-track-label" style={{ color: color }}>
                <Icon name="audio" size={13} />
                <span style={{ fontWeight: 600, fontSize: 12.5 }}>{trackName}</span>
                {langTag}
              </div>
              <div className="audio-track-waveform">
                <Waveform seed={tr.index || i} color={color} />
              </div>
              <div className="audio-track-meters">
                <AudioLevelMeter level={isAudible ? (levels.L || 0) : 0} label="L" />
                <AudioLevelMeter level={isAudible ? (levels.R || 0) : 0} label="R" />
              </div>
              <div className="audio-track-meta mono">
                <span>{codecLabel}</span>
                <span>{chLabel}</span>
                <span>{srLabel}</span>
                <span>{bdLabel}</span>
                <span>{brLabel}</span>
              </div>
              <div className="audio-track-controls">
                <button className={'audio-btn mute' + (st.muted ? ' active' : '')} onClick={function() { toggleMute(i); }} title="Mute">M</button>
                <button className={'audio-btn solo' + (st.solo ? ' active' : '')} onClick={function() { toggleSolo(i); }} title="Solo">S</button>
                <div className="audio-fader">
                  <input type="range" min="0" max="100" value={st.volume}
                    onChange={function(e) { setTrackVol(i, parseInt(e.target.value, 10)); }}
                    title={st.volume + '%'}
                  />
                  <span className="mono">{st.volume}</span>
                </div>
              </div>
            </div>
          );
        })}
      </div>
      <div className="audio-master">
        <div className="audio-master-label">Master</div>
        <div className="audio-master-meters">
          <AudioLevelMeter level={levels.L || 0} label="L" tall />
          <AudioLevelMeter level={levels.R || 0} label="R" tall />
        </div>
        <div className="audio-fader master-fader">
          <input type="range" min="0" max="100" value={masterVol}
            onChange={function(e) { setMasterVol(parseInt(e.target.value, 10)); }}
            title={masterVol + '%'}
          />
          <span className="mono">{masterVol}</span>
        </div>
      </div>
    </div>
  );
}

function AudioLevelMeter({ level, label, tall }) {
  var segs = tall ? 28 : 16;
  var pct = Math.max(0, Math.min(1, level));
  return (
    <div className="audio-level-meter">
      <div className="audio-level-bar">
        {Array.from({ length: segs }).map(function(_, i) {
          var v = i / segs;
          var on = v < pct;
          var color = v < 0.6 ? 'var(--success)' : v < 0.85 ? 'var(--warning)' : 'var(--danger)';
          return <div key={i} className="audio-level-seg" style={{ background: on ? color : 'var(--bg-3)', opacity: on ? 1 : 0.3 }} />;
        })}
      </div>
      <span className="audio-level-label mono">{label}</span>
    </div>
  );
}

function parseDuration(d) {
  if (!d || d === '·' || typeof d !== 'string') return 0;
  const parts = d.split(':');
  if (parts.length < 2) return 0;
  const nums = parts.map(Number);
  if (nums.some(isNaN)) return 0;
  if (nums.length === 3) return ((nums[0] * 60 + nums[1]) * 60 + nums[2]) * 1000;
  return (nums[0] * 60 + nums[1]) * 1000;
}

function msToTimecode(ms) {
  const total = Math.floor((ms || 0) / 1000);
  const h = Math.floor(total / 3600);
  const m = Math.floor((total % 3600) / 60);
  const s = total % 60;
  return String(h).padStart(2, "0") + ':' + String(m).padStart(2, "0") + ':' + String(s).padStart(2, "0");
}

function avatarColor(initials) {
  let h = 0;
  for (const c of (initials || '?')) h = (h * 31 + c.charCodeAt(0)) & 0xff;
  return 'linear-gradient(135deg, hsl(' + (h % 360) + ' 60% 50%), hsl(' + ((h + 60) % 360) + ' 60% 45%))';
}

Object.assign(window, { AssetDetail, msToTimecode, parseDuration, avatarColor });
