/* research.jsx — flagship Ask / Research flow.
   Ask -> live research pipeline -> streamed IRAC answer -> verified case cards -> case drawer. */
const { useState, useEffect, useRef, useCallback } = React;

/* ---------- actions (copy / share / export) ---------- */
function copyText(text, ok){
  try{
    if(navigator.clipboard && navigator.clipboard.writeText){
      navigator.clipboard.writeText(text).then(()=>toast(ok||"Copied"),()=>toast("Copy failed"));
    } else {
      const ta=document.createElement("textarea"); ta.value=text; document.body.appendChild(ta);
      ta.select(); document.execCommand("copy"); document.body.removeChild(ta); toast(ok||"Copied");
    }
  }catch(e){ toast("Copy failed"); }
}
function shareResult(result){
  const url=location.origin+PAGES.ask+"?q="+result.id;
  if(navigator.share){ navigator.share({ title:"Advokacy — "+result.concept, url }).catch(()=>{}); return; }
  copyText(url, "Share link copied");
}
// ── case / citation helpers (View Doc · Insights) ──
function ikDoc(tid){ return "https://indiankanoon.org/doc/"+tid+"/"; }
function citeText(c){ return c.title + (c.cite ? ", "+c.cite : (c.year?", "+c.year:"")); }
function pinpoint(c, text, para){ return "“"+plainText(text)+"” ("+citeText(c)+(para?", ¶ "+para:"")+")"; }
const PROP_LABEL={ test:"Test", factor:"Factor", exception:"Exception", definition:"Definition", procedural_rule:"Procedure", holding:"Holding" };
function propLabel(t){ return PROP_LABEL[t]||"Proposition"; }
// Download the whole authority set as a plain citation list (pasteable).
function exportCitations(result){
  const cs=result.cases||[];
  const head="AUTHORITIES — "+(result.query||"")+"\n("+cs.length+" verified judgments · Indian Kanoon)\n\n";
  const body=cs.map((c,i)=> (i+1)+". "+c.title+(c.cite?", "+c.cite:"")+(c.court?" — "+c.court:"")+"\n   "+c.ik).join("\n\n");
  const blob=new Blob([head+body+"\n"], { type:"text/plain;charset=utf-8" });
  const url=URL.createObjectURL(blob);
  const a=document.createElement("a"); a.href=url; a.download="advokacy-authorities.txt";
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(()=>URL.revokeObjectURL(url), 1500);
  toast("Authority list downloaded");
}
/* Build a plain-text research memo and download it. Strips the rich-text
   markers (**bold**, <cite>) so the memo reads cleanly. */
function plainText(s){ return (s||"").replace(/<cite case="[^"]+">([\s\S]*?)<\/cite>/g,"$1").replace(/\*\*([\s\S]*?)\*\*/g,"$1"); }
function exportMemo(result, lang){
  const a=result.analysis[lang]||result.analysis.en;
  const L=["ADVOKACY — RESEARCH MEMO","="+"=".repeat(40),"",
    "Question: "+result.query,
    "Area: "+result.area+"   ·   "+result.section.id,
    "Confidence: "+result.confidence+"%   ·   "+result.cases.length+" verified authorities","",
    "ISSUE","-----",plainText(a.issue),"",
    "RULE","----",plainText(a.rule),"",
    "APPLICATION","-----------",plainText(a.application),"",
    "CONCLUSION","----------",plainText(a.conclusion),""];
  if(a.test && a.test.length){ L.push("THE TEST TO SATISFY","-".repeat(18));
    a.test.forEach((t,i)=>L.push("  "+(i+1)+". "+plainText(t))); L.push(""); }
  if(a.authorityWeight){ L.push("WEIGHT OF AUTHORITY","-".repeat(19),plainText(a.authorityWeight),""); }
  if(a.counterpoint){ L.push("THE OTHER SIDE","-".repeat(14),plainText(a.counterpoint),""); }
  L.push("AUTHORITIES (verified on Indian Kanoon)","-".repeat(40));
  result.cases.forEach((c,i)=>{
    const st=(window.LAW_STATUS[c.status]||{}).label||"";
    L.push((i+1)+". "+c.title, "   "+c.cite+"   ·   "+c.bench+"   ·   "+st, "   "+c.ik, "");
  });
  L.push("","Advokacy is a research assistant, not legal advice. Confirm every authority before relying on it.");
  const blob=new Blob([L.join("\n")],{type:"text/plain"});
  const url=URL.createObjectURL(blob); const link=document.createElement("a");
  link.href=url; link.download="advokacy-memo-"+result.id+".txt"; document.body.appendChild(link);
  link.click(); document.body.removeChild(link); URL.revokeObjectURL(url);
  toast("Memo exported");
}

/* ---------- rich text: **bold** + <cite case="id">label</cite> ---------- */
function boldSplit(s, base){
  return s.split(/\*\*([\s\S]*?)\*\*/).map((seg,i)=>
    i%2 ? <strong key={base+"b"+i}>{seg}</strong>
        : <React.Fragment key={base+"f"+i}>{seg}</React.Fragment>);
}
function RichText({ text, onCite, activeCite }){
  const out=[]; const re=/<cite case="([^"]+)">([\s\S]*?)<\/cite>/g;
  let last=0,m,k=0;
  while((m=re.exec(text))){
    if(m.index>last) out.push(...boldSplit(text.slice(last,m.index),"t"+k++));
    const id=m[1];
    out.push(
      <button key={"c"+k++} className={"citelink"+(activeCite===id?" hot":"")}
              onClick={()=>onCite(id)}>
        {m[2]}<Icon name="ext" style={{width:11,height:11,marginLeft:3,verticalAlign:"-1px"}}/>
      </button>
    );
    last=re.lastIndex;
  }
  if(last<text.length) out.push(...boldSplit(text.slice(last),"t"+k++));
  return <>{out}</>;
}

/* ---------- empty / ask state ---------- */
// Centered welcome (the reference's empty-state canvas): emblem + serif title +
// lede + suggestion chips. Asking happens through the persistent bottom composer
// (SearchBar), so the hero carries no input of its own.
function AskHero({ onAsk }){
  return (
    <div className="ask-wrap">
      <div className="ask-hero ask-hero-center">
        <div className="hero-emblem"><Icon name="scale"/></div>
        <h1 className="ask-h1">Legal Research Assistant</h1>
        <p className="ask-lede">Ask any question about Indian law — statutes, case law,
          structured analysis, procedural steps. Every answer is backed by real, verified
          Indian Kanoon judgments.</p>

        <div className="sugg-grid hero-chips">
          {(window.SUGGESTIONS||[]).map(s=>(
            <button key={s.key} className="hero-chip" onClick={()=>onAsk({ query:s.q, key:s.key })}>
              {s.q}
            </button>
          ))}
        </div>

        <div className="trust-row hero-trust">
          <div className="trust"><Icon name="shield"/> Never invents a case</div>
          <div className="trust"><Icon name="check"/> Every citation verified on IK</div>
          <div className="trust"><Icon name="scale"/> Good-law status flagged</div>
        </div>
      </div>
    </div>
  );
}

/* ---------- compact search bar (present on every non-empty tab) ---------- */
function SearchBar({ onAsk, placeholder, lang, setLang, showLang }){
  const [q,setQ]=useState("");
  function submit(){ const v=q.trim(); if(v){ onAsk(v); setQ(""); } }
  return (
    <div className="askbar">
      <div className="askbar-box">
        {showLang ? <LangPick lang={lang} setLang={setLang} compact/> : <Icon name="search" style={{width:17,height:17}}/>}
        <input className="askbar-in" value={q} placeholder={placeholder||"Ask another question…"}
          onChange={e=>setQ(e.target.value)}
          onKeyDown={e=>{ if(e.key==="Enter"&&!e.shiftKey){ e.preventDefault(); submit(); } }}/>
        <button className="btn btn-primary" onClick={submit} disabled={!q.trim()}>
          Research <Icon name="arrow"/>
        </button>
      </div>
    </div>
  );
}

/* ---------- language picker ---------- */
function LangPick({ lang, setLang, compact }){
  const [open,setOpen]=useState(false);
  const [q,setQ]=useState("");
  const wrap=useRef(null);
  const all=window.LANGUAGES||[];
  const cur=all.find(l=>l.code===lang)||all[0];
  const needle=q.trim().toLowerCase();
  const list=needle
    ? all.filter(l=>l.label.toLowerCase().includes(needle)||l.native.toLowerCase().includes(needle)||l.code.includes(needle))
    : all;
  function close(){ setOpen(false); setQ(""); }
  // Close on a click/tap OUTSIDE the picker or on Escape — not on mouse-leave,
  // so dragging onto the scrollbar or out of the menu keeps it open.
  useEffect(()=>{
    if(!open) return;
    function onDown(e){ if(wrap.current && !wrap.current.contains(e.target)) close(); }
    function onKey(e){ if(e.key==="Escape") close(); }
    document.addEventListener("mousedown",onDown);
    document.addEventListener("touchstart",onDown);
    document.addEventListener("keydown",onKey);
    return ()=>{
      document.removeEventListener("mousedown",onDown);
      document.removeEventListener("touchstart",onDown);
      document.removeEventListener("keydown",onKey);
    };
  },[open]);
  return (
    <div className="langpick" ref={wrap}>
      <button className={"lang-btn"+(compact?" sm":"")} onClick={()=>setOpen(o=>!o)}>
        <Icon name="globe" style={{width:14,height:14}}/> <span dir="auto">{cur.native}</span>
        <Icon name="chev" style={{width:12,height:12,transform:"rotate(90deg)",opacity:.6}}/>
      </button>
      {open&&(
        <div className="lang-menu">
          <input className="lang-search mono" autoFocus placeholder="Search 23 languages…"
            value={q} onChange={e=>setQ(e.target.value)}/>
          <div className="lang-list">
            {list.map(l=>(
              <button key={l.code} className={"lang-opt"+(l.code===lang?" on":"")}
                onClick={()=>{setLang(l.code);close();}}>
                <span dir="auto">{l.native}</span><span className="mono">{l.label}</span>
                {l.code===lang&&<Icon name="check" style={{width:13,height:13,marginLeft:"auto"}}/>}
              </button>
            ))}
            {list.length===0&&<div className="lang-empty mono">No match</div>}
          </div>
        </div>
      )}
    </div>
  );
}

/* ---------- research pipeline (loading) ---------- */
function Pipeline({ onDone }){
  const steps=window.PIPELINE||[];
  const [i,setI]=useState(0);
  const [pct,setPct]=useState(5);
  useEffect(()=>{
    if(i>=steps.length){ const t=setTimeout(onDone,420); return ()=>clearTimeout(t); }
    const t=setTimeout(()=>setI(i+1), i===0?420:620);
    return ()=>clearTimeout(t);
  },[i]);
  // Creep the percentage toward ~97% and NEVER freeze — it only "completes" when
  // the real result arrives and this component unmounts. Gives a live
  // "working in the background" cue for the whole 1–2 min research run, so the
  // app never looks stuck while the model is still working.
  useEffect(()=>{
    const id=setInterval(()=>{ setPct(p=> p>=97 ? 97 : p + Math.max(0.35,(97-p)*0.04)); }, 500);
    return ()=>clearInterval(id);
  },[]);
  return (
    <div className="pipe-wrap">
      <div className="pipe-card">
        <div className="pipe-title"><span className="spin"/> Researching your question <span className="pipe-pct mono">{Math.round(pct)}%</span></div>
        <div className="pipe-bar"><span style={{width:pct+"%"}}/></div>
        <div className="pipe-steps">
          {steps.map((s,n)=>{
            const st=n<i?"done":n===i?"now":"wait";
            return (
              <div className={"pstep "+st} key={s.k}>
                <span className="pstep-ico">
                  {st==="done"?<Icon name="check" style={{width:13,height:13}}/>:st==="now"?<span className="mini-spin"/>:<span className="pdot"/>}
                </span>
                <div className="pstep-tx">
                  <div className="pstep-l">{s.label}</div>
                  <div className="pstep-d mono">{s.detail}</div>
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

/* ---------- answer (focused prose: legal position · requirements · the other side) ---------- */
// Break a section's text into paragraphs (blank-line OR single-newline separated).
function toParas(text){ return String(text||"").split(/\n+/).map(s=>s.trim()).filter(Boolean); }
function Answer({ result, lang, onCite, activeCite, onNew, translating }){
  const a = result.analysis[lang] || result.analysis[result.baseLang] || result.analysis.en
            || result.analysis[Object.keys(result.analysis)[0]];
  const translated = !!result.analysis[lang];
  const langObj=(window.LANGUAGES||[]).find(l=>l.code===lang);
  // The legal position: one short paragraph (the governing rule). Falls back to
  // the application text only if the rule is empty.
  const positionParas = toParas(a.rule).length ? toParas(a.rule) : toParas(a.application);
  const [reqOpen,setReqOpen] = useState(false);   // "What must be established" starts collapsed
  // "The legal position" stays compact (8em clamp) but never truncates: a
  // "Show more" toggle reveals the rest. The button only appears when the text
  // actually overflows the clamp. Resets to collapsed for each new result.
  const [posOpen,setPosOpen] = useState(false);
  const posRef = useRef(null);
  const [posOverflow,setPosOverflow] = useState(false);
  const posKey = positionParas.join("\n");
  useEffect(()=>{ setPosOpen(false); },[result && result.id]);
  useEffect(()=>{
    const el=posRef.current; if(!el) return;
    const check=()=>setPosOverflow(el.scrollHeight - el.clientHeight > 4);
    check(); const t=setTimeout(check,60); return ()=>clearTimeout(t);  // re-measure after fonts/reflow
  },[posKey]);
  return (
    <div className="answer">
      <div className="ans-qhead">
        <h2 className="ans-q">{result.query}</h2>
        <div className="ans-qfoot">
          <span className="conf"><Icon name="shield" style={{width:14,height:14}}/> {result.confidence}% confidence</span>
          <span className="conf-sep">·</span>
          <span>{result.cases.length} verified authorities</span>
          {translating &&
            <span className="mt-note mono">· <span className="tx-spin"/> translating to {langObj?langObj.label:lang}…</span>}
          {!translating && !translated && lang!==result.baseLang &&
            <span className="mt-note mono">· {langObj?langObj.label:""} unavailable — showing original</span>}
        </div>
      </div>

      {(positionParas.length || (a.test && a.test.length)) ? (
        <section className="ans-sec">
          <h3 className="ans-h">The legal position</h3>
          {positionParas.length ? (
            <div className="ans-position-box">
              <div ref={posRef} className={"ans-position"+(posOpen?" open":"")+(posOverflow?" has-more":"")}>
                {positionParas.map((p,i)=>(
                  <p className="ans-p" dir="auto" key={i}><RichText text={p} onCite={onCite} activeCite={activeCite}/></p>
                ))}
              </div>
              {posOverflow ? (
                <button className="ans-more" onClick={()=>setPosOpen(o=>!o)} aria-expanded={posOpen}>
                  {posOpen?"Show less":"Show more"}
                  <Icon name="chev" style={{width:12,height:12,flexShrink:0,transition:"transform .18s",transform:posOpen?"rotate(-90deg)":"rotate(90deg)"}}/>
                </button>
              ) : null}
            </div>
          ) : null}
          {a.test && a.test.length ? (
            <div className="lp-req">
              <button className="lp-req-label mono lp-req-btn" onClick={()=>setReqOpen(o=>!o)} aria-expanded={reqOpen}>
                <Icon name="chev" style={{width:12,height:12,flexShrink:0,transition:"transform .18s",transform:reqOpen?"rotate(90deg)":"none"}}/>
                What must be established
              </button>
              {reqOpen ? (
                <ul className="req-list">
                  {a.test.map((t,i)=>(
                    <li key={i}>
                      <span className="req-ic"><Icon name="check" style={{width:13,height:13}}/></span>
                      <span dir="auto"><RichText text={t} onCite={onCite} activeCite={activeCite}/></span>
                    </li>
                  ))}
                </ul>
              ) : null}
            </div>
          ) : null}
        </section>
      ) : null}
    </div>
  );
}

/* ---------- authorities (case cards) ---------- */
function statusTone(s){ return (window.LAW_STATUS[s]||{}).tone || "ok"; }
const REL_LABEL={ "on-point":"On point", "analogous":"Analogous", "distinguishable":"Distinguishable", "contradictory":"Contradictory", "background":"Background" };
function relLabel(t){ return REL_LABEL[t]||""; }

function CaseCard({ c, n, onOpen, hot }){
  const st=window.LAW_STATUS[c.status]||window.LAW_STATUS.good;
  const sections=(c.sections||[]).slice(0,3);
  const hasTags=!!c.disposition || sections.length>0 || !!c.authority || !!c.relType;
  const open=()=>onOpen(c.id);
  return (
    <div className={"case"+(hot?" hot":"")} role="button" tabIndex={0} onClick={open}
         onKeyDown={e=>{ if(e.key==="Enter"||e.key===" "){ e.preventDefault(); open(); } }}>
      <div className="case-top">
        {n ? <span className="case-num mono">{n}</span> : null}
        <span className={"tier-badge tier-"+c.tier}>{c.tier}</span>
        {c.benchStrength ? <span className="case-bench mono">{c.benchStrength} Bench</span> : null}
        {c.foundational ? <span className="case-found" title="Frequently-cited / foundational authority">★</span> : null}
        {(!c.status || c.status==="good")
          ? <span className="vbadge"><Icon name="check" style={{width:11,height:11}}/> Verified</span>
          : <span className={"pill "+st.tone}><span className="dot"/>{st.label}</span>}
      </div>
      {/* title links straight to the full judgment on Indian Kanoon */}
      <a className="case-title case-title-link" href={c.ik} target="_blank" rel="noreferrer"
         onClick={e=>e.stopPropagation()} title="Open the full judgment on Indian Kanoon">
        {c.title}<Icon name="ext" style={{width:13,height:13}}/>
      </a>
      <div className="case-cite mono">{c.cite}</div>
      {hasTags ? (
        <div className="case-tags">
          {c.authority ? <span className={"case-auth "+c.authority}>{c.authority==="binding"?"Binding":"Persuasive"}</span> : null}
          {c.relType ? <span className={"case-reltype rt-"+c.relType}>{relLabel(c.relType)}</span> : null}
          {c.disposition ? <span className="case-disp">{c.disposition}</span> : null}
          {sections.map(s=><span className="case-sec mono" key={s}>{s}</span>)}
        </div>
      ) : null}
      {/* "Why this case" only — no headnote, no ratio (kept to the popup / View Doc) */}
      {c.relevance ? <div className="case-rel"><Icon name="spark" style={{width:12,height:12}}/><span dir="auto">{c.relevance}</span></div> : null}
      <div className="case-foot">
        {c.cites ? <span className="case-cb"><Icon name="book" style={{width:12,height:12}}/>{c.cites.toLocaleString()} citations</span>
                 : <span className="mono" style={{color:"var(--ink-3)"}}>{c.bench}</span>}
        <span className="case-cb"><Icon name="up" style={{width:12,height:12}}/>{c.citedby.toLocaleString()} cited by</span>
      </div>
    </div>
  );
}

const TIER_LABEL={ SC:"Supreme Court of India", HC:"High Courts", DC:"District & Sessions Courts", TRIB:"Tribunals", CT:"Other Courts" };
const TIER_ORDER=["SC","HC","DC","TRIB","CT"];

// Authorities grouped by court, each group collapsible (as in the earlier app).
function Authorities({ result, onOpen, activeCite }){
  // group cases by tier, preserving rank order within each group
  const groups={};
  result.cases.forEach(c=>{ (groups[c.tier]=groups[c.tier]||[]).push(c); });
  const tiers=TIER_ORDER.filter(t=>groups[t]&&groups[t].length);
  Object.keys(groups).forEach(t=>{ if(!tiers.includes(t)) tiers.push(t); });
  // Number in DISPLAY order — Supreme Court group first, then the rest — so the
  // labels read 1,2,3… top-to-bottom instead of by the underlying rank order.
  const numById={}; let _seq=0; tiers.forEach(t=>groups[t].forEach(c=>{ numById[c.id]=++_seq; }));

  // Only the Supreme Court group is expanded initially (or the top tier if there
  // are no SC cases); the rest open on click, or when a clicked citation lands
  // in them (effect below).
  const initOpen=(ts)=>{ const o={}; ts.forEach(t=>o[t]=false); if(ts.includes("SC")) o.SC=true; else if(ts[0]) o[ts[0]]=true; return o; };
  const [open,setOpen]=useState(()=>initOpen(tiers));
  useEffect(()=>{ setOpen(initOpen(tiers)); },[result.id]);
  // make sure the group holding a clicked citation is expanded
  useEffect(()=>{
    if(!activeCite) return;
    const c=result.cases.find(x=>x.id===activeCite);
    if(c) setOpen(prev=> prev[c.tier] ? prev : { ...prev, [c.tier]:true });
  },[activeCite]);
  const toggle=(t)=>setOpen(prev=>({ ...prev, [t]:!prev[t] }));

  return (
    <div className="auth">
      <div className="auth-groups">
        {tiers.map(t=>(
          <div className={"auth-group"+(open[t]?" open":"")} key={t}>
            <button className="auth-group-head" onClick={()=>toggle(t)} aria-expanded={!!open[t]}>
              <Icon name="chev" style={{width:14,height:14,flexShrink:0,transition:"transform .18s",transform:open[t]?"rotate(90deg)":"none"}}/>
              <span className={"tier-badge tier-"+t}>{t}</span>
              <span className="agh-label">{TIER_LABEL[t]||t}</span>
              <span className="agh-count mono">{groups[t].length}</span>
            </button>
            {open[t] ? (
              <div className="case-grid">
                {groups[t].map(c=>(
                  <CaseCard key={c.id} c={c} n={numById[c.id]}
                            hot={activeCite===c.id} onOpen={onOpen}/>
                ))}
              </div>
            ) : null}
          </div>
        ))}
      </div>
    </div>
  );
}

/* ---------- middle-pane case-law browser ---------- */
function escRe(s){ return String(s).replace(/[.*+?^${}()|[\]\\]/g,"\\$&"); }
const HL_STOP=new Set("the a an of to in on for and or is are was were be been by as at it its this that with within without under over upon whether what which who when where how case court held judgment section under any all may can shall".split(" "));
// Distinctive terms from the user's query, for highlighting + the "x/y terms" chip.
function queryTerms(q){ return [...new Set(String(q||"").toLowerCase().split(/[^a-z0-9]+/).filter(w=>w.length>3 && !HL_STOP.has(w)))].slice(0,8); }
// Render text with the query terms <mark>-highlighted (returns string when no terms).
function Highlight({ text, terms }){
  const t=String(text||"");
  if(!t || !terms || !terms.length) return t;
  const re=new RegExp("\\b("+terms.map(escRe).join("|")+")\\b","gi");
  return t.split(re).map((p,i)=> i%2 ? <mark className="hl" key={i}>{p}</mark> : p);
}

// Grouped by court (collapsed). A collapsed case previews its ratio ("what it's
// about"); expanding shows headnote / facts / issue / outcome / ratio with the
// query terms highlighted. The title opens the full judgment in the popup.
function CaseLawList({ result, onOpenPopup }){
  const groups={};
  result.cases.forEach(c=>{ (groups[c.tier]=groups[c.tier]||[]).push(c); });
  const tiers=TIER_ORDER.filter(t=>groups[t]&&groups[t].length);
  Object.keys(groups).forEach(t=>{ if(!tiers.includes(t)) tiers.push(t); });
  // Number in DISPLAY order (Supreme Court first, then the rest) so SC-1, SC-2,
  // HC-3… read sequentially top-to-bottom rather than by underlying rank.
  const numById={}; let _seq=0; tiers.forEach(t=>groups[t].forEach(c=>{ numById[c.id]=++_seq; }));
  const terms=queryTerms(result.query);
  const [openG,setOpenG]=useState(()=>{ const o={}; tiers.forEach(t=>o[t]=true); return o; });   // court groups expanded by default
  const [openC,setOpenC]=useState({});
  const toggleG=(t)=>setOpenG(p=>({ ...p, [t]:!p[t] }));
  const toggleC=(id)=>setOpenC(p=>({ ...p, [id]:!p[id] }));
  return (
    <section className="ans-sec cl-sec">
      <h3 className="ans-h">Case law <span className="cl-total mono">{result.cases.length}</span></h3>
      <div className="cl-groups">
        {tiers.map(t=>(
          <div className={"cl-group"+(openG[t]?" open":"")} key={t}>
            <button className="cl-ghead" onClick={()=>toggleG(t)} aria-expanded={!!openG[t]}>
              <Icon name="chev" style={{width:14,height:14,flexShrink:0,transition:"transform .18s",transform:openG[t]?"rotate(90deg)":"none"}}/>
              <span className={"tier-badge tier-"+t}>{t}</span>
              <span className="cl-glabel">{TIER_LABEL[t]||t}</span>
              <span className="cl-gcount mono">{groups[t].length}</span>
            </button>
            {openG[t] ? (
              <div className="cl-list">
                {groups[t].map(c=>{
                  const ex=!!openC[c.id];
                  const st=window.LAW_STATUS[c.status]||window.LAW_STATUS.good;
                  const summary=c.ratio || c.headnote || c.relevance || "";
                  const hay=((c.headnote||"")+" "+(c.facts||"")+" "+(c.issue||"")+" "+(c.outcome||"")+" "+(c.ratio||"")+" "+(c.title||"")).toLowerCase();
                  const hits=terms.filter(x=>hay.indexOf(x)>=0).length;
                  return (
                    <div className={"cl-case"+(ex?" open":"")} key={c.id}>
                      <div className="cl-row" onClick={()=>toggleC(c.id)}>
                        <Icon name="chev" style={{width:13,height:13,flexShrink:0,color:"var(--ink-3)",transition:"transform .18s",transform:ex?"rotate(90deg)":"none"}}/>
                        <span className={"tier-badge cl-badge tier-"+c.tier}>{c.tier}-{numById[c.id]}</span>
                        <a className="cl-title" href={c.ik} target="_blank" rel="noreferrer"
                           onClick={(e)=>{ e.stopPropagation(); }}
                           title="Open the full judgment in a new tab">{c.title}<Icon name="ext" style={{width:12,height:12}}/></a>
                        {(!c.status||c.status==="good")
                          ? <span className="vbadge"><Icon name="check" style={{width:10,height:10}}/> Verified</span>
                          : <span className={"pill "+st.tone}><span className="dot"/>{st.label}</span>}
                      </div>
                      {!ex ? (
                        <div className="cl-preview" onClick={()=>toggleC(c.id)}>
                          {summary ? (
                            <div className="cl-psum">
                              {c.ratio ? <span className="cl-plabel">Ratio decidendi</span> : null}
                              <span className="cl-ptext"><Highlight text={summary} terms={terms}/></span>
                            </div>
                          ) : null}
                          <div className="cl-hint">Tap to expand full analysis</div>
                        </div>
                      ) : (
                        <div className="cl-detail">
                          <div className="cl-chips">
                            {c.citedby ? <a className="cl-chip" href={c.ik} target="_blank" rel="noreferrer"><Icon name="up" style={{width:11,height:11}}/> {c.citedby.toLocaleString()} citations <Icon name="ext" style={{width:10,height:10}}/></a> : null}
                            {terms.length ? <span className="cl-chip ghost">{hits}/{terms.length} terms</span> : null}
                          </div>
                          {c.headnote ? <div className="cl-d"><div className="cl-dl">Headnote</div><p dir="auto"><Highlight text={c.headnote} terms={terms}/></p></div> : null}
                          {c.facts ? <div className="cl-d"><div className="cl-dl">Facts</div><p dir="auto"><Highlight text={c.facts} terms={terms}/></p></div> : null}
                          {c.issue ? <div className="cl-d"><div className="cl-dl">Issue</div><p dir="auto"><Highlight text={c.issue} terms={terms}/></p></div> : null}
                          {c.outcome ? <div className="cl-d"><div className="cl-dl">Outcome</div><p dir="auto"><Highlight text={c.outcome} terms={terms}/></p></div> : null}
                          {c.ratio ? <div className="cl-d"><div className="cl-dl">Ratio decidendi</div><blockquote className="dr-quote" dir="auto"><Highlight text={c.ratio} terms={terms}/></blockquote></div> : null}
                          <button className="dv-cbtn" onClick={()=>onOpenPopup(c.id)}><Icon name="ext" style={{width:12,height:12}}/> Full judgment</button>
                        </div>
                      )}
                    </div>
                  );
                })}
              </div>
            ) : null}
          </div>
        ))}
      </div>
    </section>
  );
}

/* ---------- case drawer ---------- */
function Drawer({ c, onClose }){
  const [max,setMax]=useState(false);
  useEffect(()=>{
    function esc(e){ if(e.key==="Escape") onClose(); }
    window.addEventListener("keydown",esc); return ()=>window.removeEventListener("keydown",esc);
  },[]);
  useEffect(()=>{ setMax(false); },[c&&c.id]);   // reset size when a new case opens
  if(!c) return null;
  const st=window.LAW_STATUS[c.status]||window.LAW_STATUS.good;
  return (
    <div className="drawer-scrim" onClick={onClose}>
      <aside className={"drawer"+(max?" drawer-max":"")} onClick={e=>e.stopPropagation()}>
        <div className="dr-top">
          <span className={"tier-badge tier-"+c.tier}>{c.tier}</span>
          <span className="dr-court">{c.court}</span>
          <button className="dr-x dr-max" onClick={()=>setMax(m=>!m)} title={max?"Restore":"Expand"} aria-label={max?"Restore":"Expand"}><Icon name="panel" style={{width:16,height:16}}/></button>
          <button className="dr-x" onClick={onClose} aria-label="Close"><Icon name="plus" style={{transform:"rotate(45deg)",width:18,height:18}}/></button>
        </div>
        <h2 className="dr-title">{c.title}</h2>
        <div className="dr-cite mono">{c.cite}</div>

        {(!c.status || c.status==="good") ? (
          <div className="dr-verified-row">
            <span className="vbadge"><Icon name="check" style={{width:12,height:12}}/> Verified on Indian Kanoon</span>
          </div>
        ) : (
          <div className={"goodlaw "+st.tone}>
            <div className="gl-ico">
              <Icon name={st.tone==="bad"?"plus":"shield"}
                    style={{width:16,height:16,transform:st.tone==="bad"?"rotate(45deg)":"none"}}/>
            </div>
            <div className="gl-tx"><b>{st.label}</b><span>{st.note}</span></div>
          </div>
        )}

        {c.authorityReason ? (
          <div className={"authrow "+(c.authority||"persuasive")}>
            <span className="authrow-lvl">{c.authority==="binding"?"Binding":"Persuasive"}</span>
            <span className="authrow-why">{c.authorityReason}</span>
          </div>
        ) : null}

        {c.headnote ? <div className="dr-sec"><div className="dr-sl mono">Headnote</div><p dir="auto">{c.headnote}</p></div> : null}
        {c.facts ? <div className="dr-sec"><div className="dr-sl mono">Facts</div><p dir="auto">{c.facts}</p></div> : null}
        {c.outcome ? <div className="dr-sec"><div className="dr-sl mono">Outcome</div><p dir="auto">{c.outcome}</p></div> : null}
        {c.keyPassage ? <div className="dr-sec"><div className="dr-sl mono">Key passage{c.keyPassagePara?" · ¶ "+c.keyPassagePara:""}</div><blockquote className="dr-quote" dir="auto">“{c.keyPassage}”</blockquote></div> : null}

        {/* Section order: Headnote · Facts · Outcome · Key passage (above) then
            Holding · Ratio · Key excerpt · Acts referenced. The bench/year/
            authored-by/cited-by metadata grid was removed. */}
        {c.holding ? <div className="dr-sec"><div className="dr-sl mono">Holding{c.paraNum?" · ¶ "+c.paraNum:""}</div><p dir="auto">{c.holding}</p></div> : null}
        {c.ratio ? <div className="dr-sec"><div className="dr-sl mono">{c.ratioBasis==="obiter"?"Obiter dicta · persuasive":c.ratioBasis==="ratio"?"Ratio decidendi · binding":"Ratio decidendi"}</div><p dir="auto">{c.ratio}</p></div> : null}
        {c.excerpt ? <div className="dr-sec"><div className="dr-sl mono">Key excerpt</div><blockquote>{c.excerpt}</blockquote></div> : null}
        {c.acts && c.acts.length ? (
          <div className="dr-sec"><div className="dr-sl mono">Acts referenced</div>
            <div className="dr-chips">{c.acts.map((x,i)=><span className="dr-chip mono" key={i}>{x}</span>)}</div></div>
        ) : null}
        {(c.relevance || c.relType) ? (
          <div className="dr-sec">
            <div className="dr-sl mono">Why this case{c.relType?" · "+relLabel(c.relType):""}{c.caseConfidence?" · "+c.caseConfidence+"% match":""}</div>
            {c.relevance ? <p dir="auto">{c.relevance}</p> : null}
          </div>
        ) : null}
        {c.relies && c.relies.length ? (
          <div className="dr-sec"><div className="dr-sl mono">Relies on</div>
            <div className="dr-chips">{c.relies.map((x,i)=><span className="dr-chip" key={i}>{x}</span>)}</div></div>
        ) : null}

        <div className="dr-actions">
          <a className="btn btn-primary" href={c.ik} target="_blank" rel="noreferrer">Open on Indian Kanoon <Icon name="ext"/></a>
          <button className="btn btn-ghost" onClick={()=>copyText(c.title+", "+c.cite, "Citation copied")}><Icon name="copy"/> Copy citation</button>
        </div>
        <div className="dr-verified mono"><Icon name="check" style={{width:13,height:13}}/> Verified on Indian Kanoon · source confirmed</div>
      </aside>
    </div>
  );
}

/* ---------- follow-up questions ---------- */
function FollowUps({ issues, onFollow }){
  if(!issues || !issues.length || typeof onFollow!=="function") return null;
  return (
    <div className="followups">
      <div className="sugg-head">Continue your research</div>
      <div className="fu-list">
        {issues.map((q,i)=>(
          <button key={i} className="fu-item" onClick={()=>onFollow(q)}>
            <span className="fu-q">{q}</span>
            <Icon name="arrow" style={{width:15,height:15}}/>
          </button>
        ))}
      </div>
    </div>
  );
}

/* ---------- result view (answer + authorities + sticky sub-bar) ---------- */
// The answer canvas (center column). Authorities now live in the persistent
// right panel, so citation state (onCite/activeCite) and the case Drawer are
// owned by the page App and threaded in — this keeps the answer's inline
// citations in sync with the right-panel case cards.
function ResultView({ result, lang, setLang, onNew, onFollow, translating, onCite, activeCite }){
  return (
    <div className="scroll">
      <div className="result-grid result-grid-single">
        <Answer result={result} lang={lang} onCite={onCite} activeCite={activeCite} onNew={onNew} translating={translating}/>
        {result.cases && result.cases.length ? <CaseLawList result={result} onOpenPopup={onCite}/> : null}
        <FollowUps issues={result.issues} onFollow={onFollow}/>
      </div>
    </div>
  );
}

// Result actions (language · share · export), rendered just above the bottom
// composer instead of in a top ribbon, so the actions sit next to where the
// user types their next question.
function ResultActions({ result, lang, setLang }){
  return (
    <div className="composer-actions">
      <LangPick lang={lang} setLang={setLang}/>
      <div className="ca-spacer"/>
      <button className="btn btn-ghost btn-sm" onClick={()=>shareResult(result)}><Icon name="share"/> Share</button>
      <button className="btn btn-ghost btn-sm" onClick={()=>exportMemo(result, lang)}><Icon name="docs"/> Export memo</button>
    </div>
  );
}

// View Doc — reading view of the selected judgment: holding + pinpoint, the
// propositions it lays down, its citation graph, and copy-ready citations.
function DocView({ c, onOpen }){
  const props=(Array.isArray(c.excerpts)?c.excerpts:[]).filter(e=>e&&e.text);
  const citesL=Array.isArray(c.citesList)?c.citesList:[];
  const citedbyL=Array.isArray(c.citedbyList)?c.citedbyList:[];
  return (
    <div className="docview">
      <div className="dv-top"><span className={"tier-badge tier-"+c.tier}>{c.tier}</span><span className="dv-court">{c.court}</span></div>
      <a className="dv-title dv-title-link" href={c.ik} target="_blank" rel="noreferrer" title="Open full judgment on Indian Kanoon">{c.title}<Icon name="ext" style={{width:13,height:13}}/></a>
      <div className="dv-cite mono">{c.cite}</div>

      <div className="dv-copy">
        <button className="dv-cbtn" onClick={()=>copyText(citeText(c),"Citation copied")}><Icon name="copy" style={{width:12,height:12}}/> Copy citation</button>
      </div>

      {/* Holding removed here — a shortcut opens the full judgment instead. */}
      <a className="dv-fulllink" href={c.ik} target="_blank" rel="noreferrer">
        <Icon name="ext" style={{width:14,height:14}}/> Open the full judgment on Indian Kanoon
      </a>

      {props.length ? (
        <div className="dv-sec">
          <div className="dv-sl">Propositions it lays down</div>
          <ul className="dv-props">
            {props.map((e,i)=>(
              <li key={i}>
                <div className="dv-prop-h">
                  <span className="dv-ptype">{propLabel(e.type)}{e.para?" · ¶ "+e.para:""}</span>
                  <button className="dv-pin-copy" title="Copy with pinpoint" onClick={()=>copyText(pinpoint(c,e.text,e.para),"Passage + pinpoint copied")}><Icon name="copy" style={{width:11,height:11}}/></button>
                </div>
                <blockquote className="dr-quote" dir="auto">“{e.text}”</blockquote>
              </li>
            ))}
          </ul>
        </div>
      ) : null}

      {citesL.length ? (
        <div className="dv-sec">
          <div className="dv-sl">Cites{c.cites?" · "+c.cites.toLocaleString():""}</div>
          <ul className="dv-cgraph">{citesL.map((x,i)=>(<li key={i}><a href={ikDoc(x.tid)} target="_blank" rel="noreferrer">{x.title||("Judgment "+x.tid)}<Icon name="ext" style={{width:11,height:11}}/></a></li>))}</ul>
        </div>
      ) : null}
      {citedbyL.length ? (
        <div className="dv-sec">
          <div className="dv-sl">Cited by{c.citedby?" · "+c.citedby.toLocaleString():""}</div>
          <ul className="dv-cgraph">{citedbyL.map((x,i)=>(<li key={i}><a href={ikDoc(x.tid)} target="_blank" rel="noreferrer">{x.title||("Judgment "+x.tid)}<Icon name="ext" style={{width:11,height:11}}/></a></li>))}</ul>
        </div>
      ) : null}

      <div className="dv-grid">
        <div className="dr-f"><span className="dr-fl mono">Bench</span><span>{c.bench||"—"}</span></div>
        <div className="dr-f"><span className="dr-fl mono">Year</span><span>{c.year||"—"}</span></div>
      </div>

      <div className="dv-actions">
        <button className="btn btn-ghost btn-sm" onClick={()=>onOpen(c.id)}>Full detail</button>
      </div>
    </div>
  );
}

// Insights — strategy view over the whole authority set: mix, weight of
// authority, a doctrine timeline, a split/conflict detector, and export.
function Insights({ result, a, onOpen }){
  const cs=result.cases||[];
  const binding=cs.filter(c=>c.authority==="binding").length;
  const courts=new Set(cs.map(c=>c.tier)).size;
  const timeline=cs.filter(c=>/^\d{4}$/.test(String(c.year))).map(c=>({c,y:Number(c.year)})).sort((a,b)=>a.y-b.y);
  const conflicts=cs.filter(c=> c.relType==="contradictory" || c.relType==="distinguishable" || (c.status && c.status!=="good"));
  const stLabel=(s)=>((window.LAW_STATUS||{})[s]||{}).label||s;
  return (
    <div className="insights">
      <div className="ins-stats">
        <div className="ins-stat"><b>{cs.length}</b><span>authorities</span></div>
        <div className="ins-stat"><b>{binding}</b><span>binding</span></div>
        <div className="ins-stat"><b>{courts}</b><span>courts</span></div>
      </div>

      {a && a.authorityWeight ? (
        <div className="ins-sec"><div className="ins-h">Weight of authority</div>
          <p dir="auto"><RichText text={a.authorityWeight} onCite={()=>{}} activeCite={null}/></p></div>
      ) : null}

      {timeline.length>=2 ? (
        <div className="ins-sec"><div className="ins-h">Doctrine timeline</div>
          <ol className="ins-tl">
            {timeline.map(({c,y})=>(
              <li key={c.id} className="ins-tli" role="button" tabIndex={0} onClick={()=>onOpen(c.id)}
                  onKeyDown={e=>{ if(e.key==="Enter"){ onOpen(c.id); } }}>
                <span className="ins-tl-yr mono">{y}</span>
                <span className="ins-tl-body">
                  <span className="ins-tl-title">{c.title}</span>
                  <span className="ins-tl-meta mono">{c.tier}{c.status&&c.status!=="good"?" · "+stLabel(c.status):""}</span>
                </span>
              </li>
            ))}
          </ol>
        </div>
      ) : null}

      {conflicts.length ? (
        <div className="ins-sec"><div className="ins-h">Splits &amp; cautions</div>
          <ul className="ins-conf">
            {conflicts.map(c=>{
              const bad=c.status&&c.status!=="good";
              return (
                <li key={c.id} className="ins-confli" role="button" tabIndex={0} onClick={()=>onOpen(c.id)}
                    onKeyDown={e=>{ if(e.key==="Enter"){ onOpen(c.id); } }}>
                  <span className={"ins-conf-tag "+(bad?"bad":"warn")}>{bad?stLabel(c.status):relLabel(c.relType)}</span>
                  <span className="ins-conf-title">{c.title}</span>
                </li>
              );
            })}
          </ul>
        </div>
      ) : null}

      <div className="ins-sec">
        <button className="btn btn-ghost btn-sm btn-block" onClick={()=>exportCitations(result)}>
          <Icon name="docs" style={{width:14,height:14}}/> Export authority list
        </button>
      </div>
    </div>
  );
}

// Persistent right panel ("Case Laws / View Doc / Insights"). The tabs are
// linked: selecting a case in Case Laws populates View Doc; Insights summarises
// the analysis. Shows a placeholder before a search.
function RightPanel({ result, activeCite, onSelect, onOpenPopup }){
  const [tab,setTab]=useState("cases");
  const cases = result && result.cases ? result.cases : null;
  const hasCases = !!(cases && cases.length);
  const activeCase = hasCases && activeCite ? cases.find(c=>c.id===activeCite) : null;
  const a = result && result.analysis
    ? (result.analysis[result.baseLang] || result.analysis.en || result.analysis[Object.keys(result.analysis)[0]])
    : null;
  // selecting a case links through to View Doc; a fresh result returns to the list
  useEffect(()=>{ if(activeCite) setTab("doc"); },[activeCite]);
  useEffect(()=>{ setTab("cases"); },[result && result.id]);
  return (
    <aside className="rpanel">
      <div className="rpanel-tabs" role="tablist">
        <button className={"rpt"+(tab==="cases"?" on":"")} onClick={()=>setTab("cases")}>
          Case Laws{hasCases ? <span className="rpt-count">{cases.length}</span> : null}
        </button>
        <button className={"rpt"+(tab==="doc"?" on":"")} onClick={()=>setTab("doc")}>View Doc</button>
        <button className={"rpt"+(tab==="insights"?" on":"")} onClick={()=>setTab("insights")}>Insights</button>
      </div>
      <div className="rpanel-body">
        {tab==="cases" && (hasCases
          ? <Authorities result={result} onOpen={onSelect} activeCite={activeCite}/>
          : <div className="rpanel-empty"><Icon name="scale"/><p>Case citations will appear here after your search.</p></div>)}
        {tab==="doc" && (activeCase
          ? <DocView c={activeCase} onOpen={onOpenPopup}/>
          : <div className="rpanel-empty"><Icon name="docs"/><p>Select a case from Case Laws to read it here.</p></div>)}
        {tab==="insights" && (hasCases
          ? <Insights result={result} a={a} onOpen={onSelect}/>
          : <div className="rpanel-empty"><Icon name="spark"/><p>Insights about your question will appear here.</p></div>)}
      </div>
      <div className="rpanel-foot">
        <div className="vf-h mono"><Icon name="shield" style={{width:13,height:13}}/> Verify citations before court use</div>
        <div className="vf-srcs"><span className="vf-src">SCC Online</span><span className="vf-src">Manupatra</span><span className="vf-src">Indian Kanoon</span></div>
      </div>
    </aside>
  );
}

/* ---------- real backend call: POST /api/research ----------
   Returns the v2-shaped { ok, result } (or a typed { ok:false, … } that the
   caller turns into an honest message). Replaces the canned demo routing. */
async function runResearch(query, lang, reuse){
  let res;
  try{
    const payload={ query, lang: lang||"en" };
    // Re-asking the same question in a new language: send the authorities we
    // already have so the server skips retrieval and only re-runs the analysis.
    if(Array.isArray(reuse) && reuse.length) payload.reuse=reuse;
    res = await fetch("/api/research", {
      method:"POST",
      headers:{ "Content-Type":"application/json" },
      body: JSON.stringify(payload),
    });
  }catch(e){ throw new Error("Couldn't reach the server. Check that it's running."); }
  let data;
  try{ data = await res.json(); }catch(e){ throw new Error("The server returned an unexpected response."); }
  if(res.status===429) throw new Error((data && data.error) || "Too many requests — wait a moment and retry.");
  if(!res.ok && !data) throw new Error("Research failed (HTTP "+res.status+").");
  return data;
}

/* ---------- multilingual result helpers ----------
   A result keeps every language it has been generated/translated in: the IRAC
   block lives in result.analysis[lang], and the other language-specific text
   (concept, area, section.maps, follow-ups, and each case's headnote / ratio /
   relevance) lives in result.i18n[lang]. normalizeResult snapshots the language
   a fresh result arrived in; localizeResult projects the chosen language onto
   the flat shape the UI components read. */
function normalizeResult(r){
  if(!r || r.__i18n) return r;
  const base = (r.analysis && Object.keys(r.analysis)[0]) || "en";
  const cases = {};
  (r.cases||[]).forEach(c=>{ cases[c.id]={ headnote:c.headnote, ratio:c.ratio, relevance:c.relevance }; });
  r.baseLang = base;
  r.i18n = { [base]: { concept:r.concept, area:r.area, sectionMaps:(r.section&&r.section.maps)||"", issues:r.issues||[], cases } };
  r.__i18n = true;
  return r;
}
function localizeResult(r, lang){
  if(!r || !r.i18n) return r;
  const b = r.i18n[lang] || r.i18n[r.baseLang];
  if(!b) return r;
  return {
    ...r,
    concept: b.concept, area: b.area,
    section: { ...r.section, maps: b.sectionMaps },
    issues: b.issues,
    cases: (r.cases||[]).map(c=>({ ...c, ...((b.cases&&b.cases[c.id])||{}) })),
  };
}
// Translate an existing result into `toLang` via the server (no re-analysis).
// Returns the raw { ok, analysis, i18n } payload for the caller to merge.
async function translate(result, toLang){
  const have = result.analysis||{};
  const from = have.en ? "en" : (result.baseLang || Object.keys(have)[0] || "en");
  const a = have[from] || {};
  const bundle = (result.i18n && result.i18n[from]) || {};
  const payload = {
    lang: toLang,
    translate: {
      from, query: result.query||"",
      concept: bundle.concept, area: bundle.area, sectionMaps: bundle.sectionMaps,
      analysis: a, issues: bundle.issues||[],
      cases: (result.cases||[]).map(c=>{ const cc=(bundle.cases&&bundle.cases[c.id])||{};
        return { id:c.id, headnote:cc.headnote, ratio:cc.ratio, relevance:cc.relevance }; }),
    },
  };
  const res = await fetch("/api/research", { method:"POST", headers:{ "Content-Type":"application/json" }, body: JSON.stringify(payload) });
  return await res.json();
}

/* ---------- research error / unconfigured state ---------- */
function ResearchError({ info, onNew }){
  const note=(info && (info.note || info.message)) || "Something went wrong while researching your question.";
  const unconfigured = info && info.configured===false;
  return (
    <div className="pipe-wrap">
      <div className="pipe-card" style={{textAlign:"center"}}>
        <div className="vb-ico" style={{margin:"0 auto 14px",background:"var(--warn)"}}>
          <Icon name="shield" style={{width:16,height:16}}/>
        </div>
        <div style={{fontSize:16,fontWeight:700,color:"var(--ink)",marginBottom:8}}>
          {unconfigured ? "Research isn't configured yet" : "Couldn't complete the research"}
        </div>
        <p style={{color:"var(--ink-2)",fontSize:14,lineHeight:1.6,margin:"0 auto 20px",maxWidth:420}}>{note}</p>
        <button className="btn btn-primary" onClick={onNew} style={{margin:"0 auto"}}>
          <Icon name="plus" style={{width:15,height:15}}/> New question
        </button>
      </div>
    </div>
  );
}

/* ---------- search workspace tabs ----------
   Browser-style tabs for the Ask page. Each tab is an independent search
   workspace (empty, searching, or showing a result) and is supplied by the
   page; "+" opens a new one. A small spinner marks tabs whose search is
   in flight. */
function TabStrip({ tabs, activeId, onSelect, onClose, onNew }){
  if(!tabs || !tabs.length) return null;
  return (
    <div className="qtabs" role="tablist">
      {tabs.map(t=>(
        <div key={t.id} role="tab" aria-selected={t.id===activeId}
             className={"qtab"+(t.id===activeId?" on":"")} title={t.title||"New search"}
             onClick={()=>onSelect(t.id)}>
          {t.phase==="thinking" ? <span className="qtab-spin"/> : null}
          <span className="qtab-t">{t.title||"New search"}</span>
          <button className="qtab-x" aria-label="Close tab"
                  onClick={(e)=>{e.stopPropagation();onClose(t.id);}}>
            <Icon name="plus" style={{width:13,height:13,transform:"rotate(45deg)"}}/>
          </button>
        </div>
      ))}
      <button className="qtab-new" title="New search" aria-label="New search" onClick={onNew}>
        <Icon name="plus" style={{width:15,height:15}}/>
      </button>
    </div>
  );
}

/* ---------- per-query processing timer (resets on every query) ----------
   Live-ticks while a search is in flight, then freezes at the total time the
   user waited for results. `startedAt`/`elapsedMs` are tracked per tab so each
   query gets its own timer. */
function fmtDuration(ms){
  const s=Math.max(0,ms)/1000;
  if(s<60) return s.toFixed(1)+"s";
  const m=Math.floor(s/60), r=Math.round(s%60);
  return m+"m "+(r<10?"0":"")+r+"s";
}
function QueryTimer({ startedAt, elapsedMs, running }){
  const [now,setNow]=useState(()=>Date.now());
  useEffect(()=>{
    if(!running) return;
    setNow(Date.now());
    const t=setInterval(()=>setNow(Date.now()),100);
    return ()=>clearInterval(t);
  },[running,startedAt]);
  const ms = running ? Math.max(0, now-(startedAt||now)) : (elapsedMs||0);
  return (
    <div className={"qtimer"+(running?" run":"")} aria-live="polite" title="Time to process this query">
      <svg className="qtimer-ico" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7.5V12l3 1.8"/></svg>
      <span>{running?"Researching…":"Processed in"}</span>
      <span className="qtimer-v mono">{fmtDuration(ms)}</span>
    </div>
  );
}

window.Research = { AskHero, Pipeline, ResultView, ResultActions, RightPanel, Drawer, RichText, LangPick, runResearch, ResearchError, TabStrip, SearchBar, translate, normalizeResult, localizeResult, QueryTimer };
