// Human review — listen, then slice. Marking a span on the main wave splits it out
// as its own audit track that carries a tag + comment and can be auditioned alone.
// Geometry follows the kit: tracks 56, label col 223, r12 cards, Open Sans 18/24 · 12/16.

function HumanReviewView({ art, onBack, onRecord, arts, onSelect, onAddComment }) {
  const A = art;
  const DUR = A ? A.dur : 60;   // real takes carry the measured file duration
  const [mode, setMode] = React.useState('listen');          // listen | review
  const [t, setT] = React.useState(0);
  const [playing, setPlaying] = React.useState(false);
  const [scope, setScope] = React.useState(null);            // [t0,t1] currently auditioned
  const [sel, setSel] = React.useState(null);                // selected mark id
  const [pending, setPending] = React.useState(null);        // fresh selection awaiting commit
  const [marks, setMarks] = React.useState([]);
  const [lang, setLang] = React.useState('Original');
  const [notes, setNotes] = React.useState(false);
  const [zoom, setZoom] = React.useState(0.62);
  const [packed, setPacked] = React.useState(false);   // share lanes when slices don't overlap

  // Slices move only — horizontally along the take and vertically between audit
  // lines. They are never resized: a slice's bounds are part of a recorded review.
  const lanesRef = React.useRef(null);
  const startDrag = (e, m) => {
    e.stopPropagation(); e.preventDefault();
    const host = lanesRef.current; if (!host) return;
    const perPx = DUR / host.getBoundingClientRect().width;
    const x0 = e.clientX, y0 = e.clientY, a0 = m.t0, len = m.t1 - m.t0;
    const rowH = 64, baseLane = laneOf[m.id] || 0;
    const node = e.currentTarget;
    try { node.setPointerCapture(e.pointerId); } catch (err) {}
    setSel(m.id);
    let moved = false;
    const mv = ev => {
      if (Math.abs(ev.clientX - x0) > 2 || Math.abs(ev.clientY - y0) > 2) moved = true;
      const t0 = Math.max(0, Math.min(DUR - len, a0 + (ev.clientX - x0) * perPx));
      const lane = Math.max(0, baseLane + Math.round((ev.clientY - y0) / rowH));
      setMarks(ms => ms.map(x => x.id === m.id ? { ...x, t0, t1: t0 + len, lane } : x));
    };
    const up = () => {
      try { node.releasePointerCapture(e.pointerId); } catch (err) {}
      node.removeEventListener('pointermove', mv);
      node.removeEventListener('pointerup', up);
      node.removeEventListener('pointercancel', up);
      // a press without travel is an audition, not a move
      if (!moved) playFrom(m.t0, m.t1);
    };
    node.addEventListener('pointermove', mv);
    node.addEventListener('pointerup', up);
    node.addEventListener('pointercancel', up);
  };

  // send a slice to a row of its own, below every row currently in use
  const detach = (e, m) => {
    e.stopPropagation();
    const free = Math.max(-1, ...Object.values(laneOf)) + 1;
    setMarks(ms => ms.map(x => x.id === m.id ? { ...x, lane: free } : x));
    setSel(m.id);
  };

  // pack marks onto as few lanes as possible without overlapping
  const laneOf = React.useMemo(() => {
    if (!packed) {
      // an explicit lane (set by dragging a slice up or down) wins over source order
      const out = {};
      marks.forEach((m, i) => { out[m.id] = m.lane == null ? i : m.lane; });
      return out;
    }
    const ends = []; const out = {};
    [...marks].sort((a, b) => a.t0 - b.t0).forEach(m => {
      let r = ends.findIndex(e => e <= m.t0 - 0.05);
      if (r < 0) r = ends.length;
      ends[r] = m.t1; out[m.id] = r;
    });
    return out;
  }, [marks, packed]);
  const laneCount = Math.max(-1, ...Object.values(laneOf)) + 1;
  const seed = A ? A.seed : 3.2;
  const clip = A && A.clip;
  const peaks = React.useMemo(() => wavePeaks(seed, 240, clip), [seed, clip]);
  const ovPeaks = React.useMemo(() => wavePeaks(seed + 0.4, 200, clip), [seed, clip]);

  // ---- transport ----
  const stop = () => { Player.stop(); setPlaying(false); setScope(null); };
  const playFrom = (from, until) => {
    if (!A) return;
    Player.start({ dur: DUR, seed, voice: A.voice, g: (LAB.voices[A.voice] || {}).g, src: A.src, clip: A.clip },
      from, seed,
      tt => { setT(tt); setPlaying(true); },
      () => { setPlaying(false); setScope(null); },
      until);
    setT(from); setPlaying(true); setScope(until ? [from, until] : null);
  };
  const toggle = () => playing ? stop() : playFrom(t >= DUR ? 0 : t);
  React.useEffect(() => () => Player.stop(), []);

  // A queue selection swaps the artifact without unmounting this view. Reset
  // all take-specific transport and review state so the new voice never
  // inherits the previous take's playhead, playback flag or marked regions.
  React.useEffect(() => {
    Player.stop();
    setMode('listen');
    setT(0);
    setPlaying(false);
    setScope(null);
    setSel(null);
    setPending(null);
    setMarks([]);
    setLang('Original');
    setNotes(false);
    setZoom(0.62);
    setPacked(false);
    setDraft('');
  }, [A && A.id]);

  // ---- marking ----
  const commit = (t0, t1, tag) => {
    const id = 'm' + (marks.length + 1);
    setMarks(m => [...m, { id, t0, t1, tag: tag || null, comment: '', n: m.length + 1 }]);
    setSel(id); setPending(null);
  };
  const onRegion = ([f0, f1]) => {
    if (mode !== 'review') return;
    const t0 = f0 * DUR, t1 = f1 * DUR;
    if (t1 - t0 < 0.4) return;
    setPending([t0, t1]);
  };
  const patch = (id, k, v) => setMarks(m => m.map(x => x.id === id ? { ...x, [k]: v } : x));
  const drop = id => { setMarks(m => m.filter(x => x.id !== id)); if (sel === id) setSel(null); };

  const cur = marks.find(m => m.id === sel) || null;
  const clock = s => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(Math.floor(s % 60)).padStart(2, '0')}`;
  const tc = s => `${clock(s)}:${String(Math.floor((s % 1) * 100)).padStart(2, '0')}`;
  const pct = s => (s / DUR) * 100;
  // one row per lane: a single slice keeps its own name, a shared lane becomes an audit line
  const rows = [{ id: 'main', name: 'Main', sub: lang, tone: 'blue', items: [{ t0: 0, t1: DUR, seed }] }];
  for (let r = 0; r < laneCount; r++) {
    const items = marks.filter(m => laneOf[m.id] === r);
    if (!items.length) continue;
    rows.push({
      id: 'lane' + r,
      name: items.length === 1 ? `Marked tag ${items[0].n}` : `Audit line ${r + 1}`,
      sub: items.length === 1 ? (items[0].tag || 'untagged') : `${items.length} slices`,
      tone: items.some(m => m.tag) ? 'pink' : 'pink-l',
      items: items.map(m => ({ ...m, seed: seed + m.n }))
    });
  }
  const [draft, setDraft] = React.useState('');
  const comments = (A && A.comments) || [];
  const VOICE = (A && LAB.voices[A.voice]) || { name: '—', g: '—', lang: '—' };
  const cmtAt = cur ? cur.t0 : t;
  const cmtTo = cur ? cur.t1 : t;
  const addCmt = () => {
    if (!draft.trim() || !A || !onAddComment) return;
    onAddComment(A.id, cmtAt, cmtTo, draft.trim());
    setDraft('');
  };

  const ruleMarks = Array.from({ length: 6 }, (_, i) => Math.round((i / 5) * DUR));

  // ---- review-state submenu ----
  const STATES = [
    { id: 'unreviewed', label: 'Unreviewed', icon: 'clock' },
    { id: 'reviewed',   label: 'Reviewed',   icon: 'eq' },
    { id: 'approved',   label: 'Approved',   icon: 'check' },
    { id: 'rejected',   label: 'Rejected',   icon: 'close' },
    { id: 'hold',       label: 'Hold',       icon: 'pause' }
  ];
  const all = arts || [];
  // 'reviewed' is now a real state in the inbox, so match on it directly —
  // a derived "everything not unreviewed" view would double-count the other buckets.
  const inState = id => all.filter(x => x.state === id);
  const [bucket, setBucket] = React.useState('reviewed');
  React.useEffect(() => { if (A && A.state !== bucket) setBucket(A.state); }, [A && A.id]);
  const inBucket = inState(bucket);
  const [railOpen, setRailOpen] = React.useState(() => localStorage.getItem('vlc_hrrail') !== '0');
  const [infoOpen, setInfoOpen] = React.useState(() => localStorage.getItem('vlc_hrinfo') !== '0');
  React.useEffect(() => { localStorage.setItem('vlc_hrinfo', infoOpen ? '1' : '0'); }, [infoOpen]);
  React.useEffect(() => { localStorage.setItem('vlc_hrrail', railOpen ? '1' : '0'); }, [railOpen]);

  return <div className="view">
    <TopBarStd greet/>
    <div className={'split hr' + (railOpen ? '' : ' shut') + (infoOpen ? '' : ' noinfo')}>
      <div className="submenu">
        <div className="sm-top">
          {railOpen && <span className="sm-lbl">Review Queue</span>}
          <button className="sm-tog" title={railOpen ? 'Collapse queue' : 'Expand queue'} onClick={() => setRailOpen(!railOpen)}>
            <Icon n={railOpen ? 'collapse' : 'chevR'} s={16}/>
          </button>
        </div>
        <div className="sm-nav">
          {STATES.map(s => {
            const n = inState(s.id).length;
            return <button key={s.id} className={bucket === s.id ? 'on' : ''} title={s.label + ' · ' + n} onClick={() => setBucket(s.id)}>
              <span className="sm-i"><Icon n={s.icon} s={13}/></span>
              {railOpen && <span style={{ flex: 1 }}>{s.label}</span>}
              {railOpen && <span className="sm-n">{n}</span>}
            </button>;
          })}
        </div>
        {railOpen && <span className="sm-lbl" style={{ marginTop: 8 }}>{inBucket.length} take{inBucket.length === 1 ? '' : 's'}</span>}
        <div className="sm-takes">
          {inBucket.map(x => <button key={x.id} className={A && x.id === A.id ? 'on' : ''}
            title={x.file.replace(/\.wav$/, '') + ' · ' + (LAB.voices[x.voice] || {}).name}
            onClick={() => onSelect && onSelect(x.id)}>
            <span className="sm-wv"><Icon n="wave" s={14}/></span>
            {railOpen && <span className="st"><b>{x.file.replace(/\.wav$/, '')}</b><i>{(LAB.voices[x.voice] || {}).name} · {clock(x.dur)}</i></span>}
          </button>)}
          {!inBucket.length && railOpen && <span className="sm-empty">Nothing in this state.</span>}
        </div>
        <button className="btn-line sm-back" title="Back to Review" onClick={onBack}>
          <Icon n="chevL" s={15}/>{railOpen && 'Back to Review'}
        </button>
      </div>
      <div className="pane hr-pane" key={A ? A.id : 'none'}>

      {/* ---------- player card ---------- */}
      <div className="kcard" style={{ gap: 20 }}>
        <div className="kcard-h">
          <div className="kh-t">
            <b style={{ fontSize: 18, lineHeight: '24px', letterSpacing: '-.02em', fontWeight: 600 }}>
              {A ? `${A.voice}-${A.file.replace(/\.wav$/, '')}` : 'No take selected'}
            </b>
            <span className="sub">12:54 &nbsp; {clock(DUR)} &nbsp; {A ? `ckpt_${A.ckpt}` : 'Original'}</span>
          </div>
          <div className="ha">
            <button className="ibtn sm plain" title="Back to inbox" onClick={onBack}><Icon n="back" s={17}/></button>
            <button className="ibtn sm plain" style={{ color: 'var(--bad)' }} title="Discard"><Icon n="trash" s={16}/></button>
            <button className={'ibtn sm plain' + (infoOpen ? ' on' : '')} title={infoOpen ? 'Hide signal panel' : 'Show signal panel'} onClick={() => setInfoOpen(!infoOpen)}><Icon n="more" s={16}/></button>
          </div>
        </div>

        <div className={'bigwave' + (mode === 'review' ? ' selecting' : '')}>
          <WaveCanvas peaks={peaks} progress={t / DUR} height={160} barW={4} gap={2}
            region={pending ? [pending[0] / DUR, pending[1] / DUR] : (cur ? [cur.t0 / DUR, cur.t1 / DUR] : null)}
            markers={marks.map(m => m.t0 / DUR)}
            onSeek={f => { setT(f * DUR); if (playing) playFrom(f * DUR); }}
            onRegion={onRegion}/>
          <div className="ruler">{ruleMarks.map((m, i) => <span key={i}>{clock(m)}</span>)}</div>
          {mode === 'review' && !pending && !marks.length &&
            <div className="wave-hint"><Icon n="cursor" s={14}/>Drag across the wave to mark a section</div>}
        </div>

        {/* fresh selection → commit bar */}
        {pending && <div className="markbar">
          <span className="mb-r">{tc(pending[0])} → {tc(pending[1])} <em>({(pending[1] - pending[0]).toFixed(1)}s)</em></span>
          <button className="btn-line sm" onClick={() => playFrom(pending[0], pending[1])}><Icon n="play" s={13}/>Hear it</button>
          <span className="grow"></span>
          <div className="mb-tags">
            {LAB.rejectTags.slice(0, 5).map(tg => <button key={tg} className="tagchip" onClick={() => commit(pending[0], pending[1], tg)}>{tg}</button>)}
          </div>
          <button className="btn-green sm" onClick={() => commit(pending[0], pending[1])}><Icon n="plus" s={14}/>Split &amp; mark</button>
          <button className="ibtn sm plain" title="Cancel" onClick={() => setPending(null)}><Icon n="close" s={15}/></button>
        </div>}

        <div>
          <div className="ovstrip" onClick={e => { const r = e.currentTarget.getBoundingClientRect(); const nt = ((e.clientX - r.left) / r.width) * DUR; setT(nt); if (playing) playFrom(nt); }}>
            <WaveCanvas peaks={ovPeaks} progress={t / DUR} height={44} barW={3} gap={2} minH={2} caps={false}/>
            {marks.map(m => <span key={m.id} className="ov-mark" style={{ left: pct(m.t0) + '%', width: (pct(m.t1) - pct(m.t0)) + '%' }}></span>)}
            <span className="ovhead" style={{ left: pct(t) + '%' }}></span>
          </div>
          <div className="ovends"><span>{clock(t)}</span><span>{clock(DUR)}</span></div>
        </div>

        <div className="bigtime">{tc(t)}</div>

        <div className="row">
          {/* Edit flips listen → review, which is what turns the wave into a slicing surface */}
          <button className={'btn-line' + (mode === 'review' ? ' on' : '')} onClick={() => { setMode(mode === 'review' ? 'listen' : 'review'); setPending(null); }}>
            <Icon n={mode === 'review' ? 'check' : 'pencil'} s={16}/>{mode === 'review' ? 'Done editing' : 'Edit'}
          </button>
          <div style={{ flex: 1, display: 'flex', justifyContent: 'center', gap: 10 }}>
            <button className="tbtn" title="Back 5s" onClick={() => setT(Math.max(0, t - 5))}><Icon n="prev" s={17}/></button>
            <button className="play" onClick={toggle}><Icon n={playing ? 'pause' : 'play'} s={14}/></button>
            <button className="tbtn" title="Forward 5s" onClick={() => setT(Math.min(DUR, t + 5))}><Icon n="next" s={17}/></button>
          </div>
          <button className="btn-green" onClick={onRecord}><Icon n="mic" s={16}/>Re-record</button>
        </div>
      </div>

      {/* ---------- audit timeline ---------- */}
      <div className="tl">
        <div className="tl-h">
          <button className="ibtn sm plain" title="Fit"><Icon n="sort" s={17}/></button>
          <span className="tl-tc">{clock(t)}<span>/{clock(DUR)}</span></span>
          <span className="grow"></span>
          <div className="tl-r">
            <button className={'ibtn sm plain' + (packed ? ' on' : '')} title={packed ? 'Split onto separate lines' : 'Collapse slices onto shared lines'} onClick={() => setPacked(!packed)}><Icon n="sort" s={17}/></button>
            <button className={'ibtn sm plain' + (mode === 'review' ? ' on' : '')} title="Slice mode" onClick={() => setMode(mode === 'review' ? 'listen' : 'review')}><Icon n="eq" s={17}/></button>
            <button className="ibtn sm plain" title="Comment on selection" onClick={() => cur && setSel(cur.id)}><Icon n="pencil" s={17}/></button>
            <button className="ibtn sm plain" title="Mark current position" onClick={() => setPending([t, Math.min(DUR, t + 2)])}><Icon n="plus" s={17}/></button>
          </div>
        </div>

        <div className="tl-body">
          <div className="tl-labels">
            <div className="tl-lang">
              <span className={'tl-l' + (lang === 'Original' ? ' on' : '')} onClick={() => setLang('Original')}><span className="ci"><Icon n="mute" s={16}/></span>Original</span>
              <span className={'tl-l' + (lang === 'English' ? ' on' : '')} onClick={() => setLang('English')}><span className="ci"><Icon n="speaker" s={16}/></span>English</span>
            </div>
            {rows.map(tr => <div key={tr.id} className={'tl-trk' + (tr.items.some(i => i.id === sel) ? ' on' : '')} onClick={() => tr.id !== 'main' && setSel(tr.items[0].id)}>
              <div className="tt"><b>{tr.name}</b><span>{tr.sub}</span></div>
              <div className="ta">
                {tr.id === 'main'
                  ? <button title="Solo"><Icon n="headphone" s={16}/></button>
                  : <>
                      <button title="Comment / tag" onClick={e => { e.stopPropagation(); setSel(tr.items[0].id); }}><Icon n="pencil" s={16}/></button>
                      <button title="Remove" onClick={e => { e.stopPropagation(); tr.items.forEach(i => drop(i.id)); }}><Icon n="close" s={16}/></button>
                    </>}
              </div>
            </div>)}
            {!marks.length && <div className="tl-empty">Nothing marked yet. Hit <b>Edit</b>, then drag across the wave.</div>}
          </div>

          <div className="tl-lanes" ref={lanesRef} style={{ '--tlz': 1 + zoom * 3 }}>
            <div className="tl-rule">
              {ruleMarks.map((m, i) => <React.Fragment key={i}>
                <i style={{ left: `calc(${(i / 5) * 92 + 2}%)` }}></i>
                <span style={{ left: `calc(${(i / 5) * 92 + 2}%)` }}>{clock(m)}</span>
              </React.Fragment>)}
            </div>
            {rows.map(tr => <div key={tr.id} className="tl-lane">
              {tr.items.map((it, ix) => {
                const isMain = tr.id === 'main';
                const trimming = false;
                // two slices sitting flush against each other come from different
                // parts of the take — the join badge pulls them apart onto their own lines
                const next = tr.items[ix + 1];
                const joined = !isMain && !!next;
                return <React.Fragment key={it.id || 'main'}>
                  <span
                    className={'tl-clip ' + tr.tone + (sel === it.id ? ' sel' : '') + (isMain ? '' : ' cut')}
                    style={{ left: pct(it.t0) + '%', width: Math.max(1.5, pct(it.t1) - pct(it.t0)) + '%' }}
                    title={isMain ? 'Full take' : 'Drag to move · up or down to change line'}
                    onPointerDown={e => { if (!isMain) startDrag(e, it); }}
                    onClick={e => { e.stopPropagation(); if (isMain) playFrom(0); }}>
                    <i className="cap l"></i>
                    <WaveCanvas peaks={wavePeaks(it.seed, 200, it.clip)} progress={0} height={52} barW={1.5} gap={1.5} minH={2} caps={false}
                      dim={tr.tone === 'blue' ? '#8699AF' : '#C5B8C1'}/>
                    {!isMain && <i className="cap r"></i>}
                  </span>
                  {joined && <button className="tl-join" style={{ left: pct((it.t1 + next.t0) / 2) + '%' }}
                    title="These are different parts of the take — send each back to its own line"
                    onPointerDown={e => e.stopPropagation()}
                    onClick={e => detach(e, next)}>
                    <svg width="18" height="14" viewBox="0 0 18 14" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round">
                      <path d="M2 3.5 5.5 7 2 10.5"/><path d="M9 2v10"/><path d="M16 3.5 12.5 7 16 10.5"/>
                    </svg>
                  </button>}
                </React.Fragment>;
              })}
            </div>)}
            <span className="tl-head" style={{ left: `calc(${(t / DUR) * 92 + 2}%)` }}></span>
            {scope && <span className="tl-scope" style={{ left: `calc(${(scope[0] / DUR) * 92 + 2}%)`, width: `calc(${((scope[1] - scope[0]) / DUR) * 92}%)` }}></span>}
          </div>

          {/* inspector: the marked slice's tag + comment */}
          <div className="tl-insp">
            <div className="tl-zoom">
              <button className="ibtn sm plain" title="Zoom out" onClick={() => setZoom(Math.max(0, zoom - .12))}><Icon n="zoomOut" s={16}/></button>
              <span className="zt" onClick={e => { const r = e.currentTarget.getBoundingClientRect(); setZoom(Math.max(0, Math.min(1, (e.clientX - r.left) / r.width))); }}><span className="zf" style={{ width: (zoom * 100) + '%' }}></span></span>
              <button className="ibtn sm plain" title="Zoom in" onClick={() => setZoom(Math.min(1, zoom + .12))}><Icon n="zoomIn" s={16}/></button>
            </div>
            {cur ? <>
              <div className="ti-h">
                <b>Marked tag {cur.n}</b>
                <span>{tc(cur.t0)} → {tc(cur.t1)}</span>
              </div>
              <button className="btn-line sm" onClick={() => playFrom(cur.t0, cur.t1)}><Icon n="play" s={13}/>Play slice</button>
              <div className="ti-sec">
                <label>Tag</label>
                <div className="mb-tags wrap">
                  {LAB.rejectTags.slice(0, 8).map(tg =>
                    <button key={tg} className={'tagchip' + (cur.tag === tg ? ' on' : '')}
                      onClick={() => patch(cur.id, 'tag', cur.tag === tg ? null : tg)}>{tg}</button>)}
                </div>
              </div>
              <div className="ti-sec">
                <label>Comment</label>
                <textarea placeholder="What's wrong with this stretch?" value={cur.comment}
                  onChange={e => patch(cur.id, 'comment', e.target.value)}/>
              </div>
            </> : <div className="ti-e">No slice selected.</div>}
          </div>
        </div>

        <div className="tl-foot">
          <span className="tf-s">{marks.length ? `${marks.length} marked ${marks.length === 1 ? 'section' : 'sections'} · review in progress` : 'on going review awaiting'}</span>
          <div className="seg">
            {['Original', 'English'].map(l => <button key={l} className={lang === l ? 'on' : ''} onClick={() => setLang(l)}>{l}</button>)}
          </div>
          <button className="ibtn sm" title="Add language"><Icon n="plus" s={15}/></button>
          <button className={'btn-line' + (notes ? ' on' : '')} onClick={() => setNotes(!notes)}>Review notes</button>
          <button className="btn-green">Save</button>
        </div>

        {notes && <div className="tl-notes">
          {marks.length ? marks.map(m => <div key={m.id} className="note-row">
            <span className="nr-t">{tc(m.t0)} → {tc(m.t1)}</span>
            {m.tag ? <span className="badge amber">{m.tag}</span> : <span className="badge gray">untagged</span>}
            <span className="nr-c">{m.comment || <em>no comment</em>}</span>
          </div>) : <div className="ti-e">No notes yet.</div>}
        </div>}
      </div>

      {/* ---------- comment composer: you review, you comment ---------- */}
      <div className="kcard hr-cmt">
        <div className="kcard-h">
          <div className="kh-t"><b>Comment {cur ? `on ${tc(cur.t0)}–${tc(cur.t1)}` : `at ${tc(t)}`}</b>
            <span>{cur ? 'Attached to the selected slice' : 'Select a slice on the timeline, or comment at the playhead'}</span></div>
        </div>
        <div className="hr-cmt-row">
          <input placeholder={cur ? 'Describe what you hear in this slice…' : 'Mark a region on the wave to comment on it, or type a note at the playhead…'}
            value={draft} onChange={e => setDraft(e.target.value)}
            onKeyDown={e => { if (e.key === 'Enter') addCmt(); }}/>
          <button className="btn accent" disabled={!draft.trim()} onClick={addCmt}>Add</button>
        </div>
      </div>
      </div>

      {/* ---------- right rail: signal, identity, comments ---------- */}
      {infoOpen && <aside className="hr-side">
        <div className="wb-panel"><h4>Signal</h4>
          <div className="kv">
            <div><div className="k">Duration</div><div className="v">{clock(DUR)}</div></div>
            <div><div className="k">Sample rate</div><div className="v">{A ? (A.sr / 1000).toFixed(0) : 48} kHz</div></div>
            <div><div className="k">Peak</div><div className="v">{A ? A.peak : '—'}</div></div>
            <div><div className="k">RMS</div><div className="v">{A ? A.rms : '—'}</div></div>
            <div><div className="k">Checkpoint</div><div className="v">ckpt_{A ? A.ckpt : '—'}</div></div>
            <div><div className="k">Slices</div><div className="v">{marks.length}</div></div>
          </div>
        </div>
        <div className="wb-panel"><h4>Identity — expected vs perceived</h4>
          <div className="idrow">
            <label>Expected</label>
            <div className="kv">
              <div><div className="k">Language</div><div className="v">{VOICE.lang}</div></div>
              <div><div className="k">Speaker</div><div className="v">{VOICE.name} ({VOICE.g})</div></div>
            </div>
            <label style={{ marginTop: 6 }}>Perceived</label>
            <select defaultValue={VOICE.lang}><option>sv-SE</option><option>nb-NO</option><option>other / unclear</option></select>
            <select defaultValue={VOICE.name + ' (' + VOICE.g + ')'}>{Object.values(LAB.voices).map(x => <option key={x.name}>{x.name} ({x.g})</option>)}<option>unknown / drift</option></select>
          </div>
        </div>
        <div className="wb-panel"><h4>Timestamp comments ({comments.length})</h4>
          <div className="clist">
            {comments.map((c, i) => <div key={i} className="centry">
              <span className="at" onClick={() => playFrom(c.t0, c.t1 > c.t0 ? c.t1 : undefined)}>{tc(c.t0)}{c.t1 > c.t0 ? '–' + tc(c.t1) : ''}</span> {c.text}
              <div className="who">{c.who} · appended to review_events.jsonl</div>
            </div>)}
            {!comments.length && <div className="empty">No comments yet.</div>}
          </div>
        </div>
      </aside>}
    </div>
  </div>;
}

Object.assign(window, { HumanReviewView });
