/* docs.jsx — Document Intelligence: grounded Q&A over an uploaded legal document.
   Real upload -> /api/docintel extracts text (PDF / DOCX / TXT) and the LLM
   produces the summary, key facts and clauses; questions are answered grounded
   ONLY in that document. The file is never stored server-side — the extracted
   text is held in the browser and sent back with each question. */
const { useState, useRef, useCallback, useEffect } = React;
const MODE_KEY = "adv_v2_docmode";
const STREAM_SENTINEL = "§§§META§§§";   // must match docintel.js
// Split a streamed buffer into the prose (the answer) and parsed metadata
// (cites/law). Robust to the model NOT emitting the exact sentinel: it also
// peels a trailing {…"cites"/"law"…} JSON blob (complete or still arriving) so
// the raw metadata JSON never leaks into the visible answer.
function stripPartialSentinel(s){
  for(let k=Math.min(STREAM_SENTINEL.length-1, s.length); k>0; k--){
    if(s.endsWith(STREAM_SENTINEL.slice(0,k))) return s.slice(0, s.length-k);
  }
  return s;
}
function splitMeta(buf){
  const i=buf.indexOf(STREAM_SENTINEL);
  if(i>=0){
    let meta=null; try{ meta=JSON.parse(buf.slice(i+STREAM_SENTINEL.length).trim()); }catch(e){ meta=null; }
    return { prose: buf.slice(0,i).replace(/\s+$/,""), meta };
  }
  // No sentinel — the model appended the metadata JSON on its own. Peel the
  // trailing object that starts with "cites" or "law" (works mid-stream too).
  const m=buf.match(/\{\s*"(?:cites|law)"[\s\S]*$/);
  if(m){
    let meta=null; try{ meta=JSON.parse(m[0]); }catch(e){ meta=null; }
    return { prose: stripPartialSentinel(buf.slice(0,m.index).replace(/\s+$/,"")).replace(/\s+$/,""), meta };
  }
  return { prose: stripPartialSentinel(buf), meta:null };
}
function loadMode(){ try{ return localStorage.getItem(MODE_KEY)==="analysis"?"analysis":"document"; }catch(e){ return "document"; } }

const ACCEPT = ".pdf,.docx,.doc,.txt,.md,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain";
const MAX_MB = 4;
// Auth: Document Intelligence requires a session (Ask stays free). Send the
// Bearer token and, on a 401, clear it so the page re-gates to the login prompt.
function authHeaders(){ return { "Content-Type":"application/json", ...(window.AdvAuth?window.AdvAuth.authHeader():{}) }; }
function on401(res){ if(res && res.status===401 && window.AdvAuth){ window.AdvAuth._clear(); return true; } return false; }
// "Legal analysis" lenses. Keys match the server. `prompt` is the editable line
// dropped into the ask box when the lens is picked (the user can tweak it, then
// commit with Ask) — Chat is the default and stays free-text.
const LENSES = [
  { k:"strategy", label:"Legal Strategy",  prompt:"Lay out the legal strategy for this matter." },
  { k:"risk",     label:"Risk Radar",      prompt:"Identify the key legal risks across these documents." },
  { k:"opposing", label:"Opponent's View", prompt:"How would the opposing side argue against this — and how do we counter it?" },
  { k:"clause",   label:"Clause Review",   prompt:"Review the clauses and flag anything problematic, ambiguous or one-sided." },
];
const LENS_LABEL = LENSES.reduce((m,l)=>{ m[l.k]=l.label; return m; },{});
const OK_EXT = ["pdf", "docx", "doc", "txt", "md", "text"];

function extOf(name){ const m=String(name||"").toLowerCase().match(/\.([a-z0-9]+)$/); return m?m[1]:""; }
function fileLooksOk(file){
  if(!file) return false;
  if(OK_EXT.includes(extOf(file.name))) return true;
  return /pdf|wordprocessingml|msword|^text\//i.test(file.type||"");
}
// FileReader -> base64 (strip the "data:...;base64," prefix).
function readBase64(file){
  return new Promise((res,rej)=>{
    const r=new FileReader();
    r.onerror=()=>rej(new Error("Could not read the file."));
    r.onload=()=>{ const s=String(r.result||""); const i=s.indexOf(","); res(i>=0?s.slice(i+1):s); };
    r.readAsDataURL(file);
  });
}

// Case-citation detection: "Name v. Name" + a recognised reporter citation
// (SCC / SCR / AIR / INSC / SCALE). Turns citations in an answer into clickable
// links that resolve to the real Indian Kanoon judgment.
const _NAME = "[A-Z][A-Za-z0-9.&'()\\-\\u2019 ]{1,70}?\\s+v(?:s)?\\.?\\s+[A-Z][A-Za-z0-9.&'()\\-\\u2019 ]{1,70}?";
const _CITE = "(?:\\(\\d{4}\\)\\s*\\d+\\s*(?:SCC OnLine|SCC|SCR)\\s*\\d+|AIR\\s*\\d{4}\\s*[A-Za-z]{1,5}\\s*\\d+|\\d{4}\\s*INSC\\s*\\d+|\\(\\d{4}\\)\\s*\\d+\\s*SCALE\\s*\\d+)";
function citeRe(){ return new RegExp("(" + _NAME + ")\\s*[,:;]?\\s*(" + _CITE + ")", "g"); }
// Sentence connectors that can get swallowed before a case name ("In X v. Y…").
const CITE_CONN = new Set(["in","see","per","relying","following","cf","vide","as","although","whereas","here","thus","hence","also","and","the","of","by","under","applying","citing","on","upon","like","viz","namely","e.g","ie"]);
function trimLeadWords(s){
  const w=s.split(/\s+/);
  while(w.length>2 && (/^[a-z]/.test(w[0]) || CITE_CONN.has(w[0].toLowerCase().replace(/[.,]/g,"")))) w.shift();
  return w.join(" ");
}
// Bare "Party v(s) Party" case names with NO reporter citation (e.g.
// "Vedantam Srinivasa Chary vs Assistant Commissioner") — common in drafted
// analysis. Linkified as a second pass so they resolve via name search. Unlike
// _NAME (whose 2nd party is bounded by a trailing citation), here the 2nd party
// is greedy and we trim it back to the real name afterwards.
const _NAME_BARE = "[A-Z][A-Za-z0-9.&'()\\-\\u2019 ]{1,70}?\\s+v(?:s)?\\.?\\s+[A-Z][A-Za-z0-9.&'()\\-\\u2019 ]{1,80}";
function bareNameRe(){ return new RegExp("(" + _NAME_BARE + ")", "g"); }
// Strip a leading court phrase a greedy match can swallow ("the Supreme Court
// in X v Y" → "X v Y"; "Bombay High Court in …" → "…").
function stripCourtLead(s){
  return String(s).replace(/^(?:the\s+)?(?:hon'?ble\s+)?(?:[A-Z][A-Za-z]+\s+)?(?:Supreme Court|High Court)\s+(?:in|of)\s+/i, "");
}
// Abbreviations that legitimately appear INSIDE case names followed by a capital
// (so "… Lrs. And Others", "… Ors. …", "… Ltd. …" must NOT be read as a
// sentence break). Lower-case, no trailing period.
const NAME_ABBR = new Set(["lrs","ors","anr","ltd","pvt","co","corp","nos","no","smt","sri","shri","kum","dr","mr","mrs","ms","govt","assn","inc","llp","rep","retd","ph","etc","ux","i.e","e.g","st","mohd","md","mohammad","mohammed","prof","col","capt","gen","maj","messrs","hon"]);
// Drop trailing prose the greedy 2nd party swallowed: a genuine sentence
// continuation ("…Commissioner. The deed…" → "…Commissioner") then trailing
// lowercase words ("…Maharashtra ruled" → "…Maharashtra"). The sentence cut
// ignores single-letter initials ("K.G.") and the name abbreviations above, so
// "…Per Lrs. And Others vs The Joint Collector And Ors." stays intact.
function trimNameTail(s){
  let t=String(s);
  // Only look for a sentence break AFTER the "v"/"vs" separator — everything
  // before it is the FIRST party's name, which may legitimately contain a period
  // ("Mohd. Riazuddin", "K.G. Rao", "M/s. …") that must never be cut.
  const vm=/\sv(?:s)?\.?\s/i.exec(t);
  const from=vm ? vm.index + vm[0].length : 0;
  const re=/([A-Za-z][A-Za-z'&\-]*)\.\s+[A-Z]/g; re.lastIndex=from; let m, cut=-1;
  while((m=re.exec(t))){
    const w=m[1].toLowerCase();
    if(w.length===1 || NAME_ABBR.has(w)) continue;   // initial or known abbreviation → not a sentence end
    cut=m.index+m[1].length; break;                  // cut right after the word (before its period)
  }
  if(cut>=0) t=t.slice(0,cut);
  const w=t.split(/\s+/); while(w.length>2 && /^[a-z]/.test(w[w.length-1])) w.pop();
  return w.join(" ");
}
function linkifyBareNames(str, kb, onCite){
  if(!onCite || !str) return [str];
  const re=bareNameRe(); const out=[]; let last=0,m,n=0;
  while((m=re.exec(str))){
    let name=trimNameTail(stripCourtLead(trimLeadWords((m[1]||"").replace(/\s+/g," ").trim()))).replace(/[.,;:]+$/,"").trim();
    if(name.length<6 || !/\sv(?:s)?\.?\s/i.test(name)) continue;   // must still be "X v(s) Y"
    // Skip generic role pairs ("Plaintiff vs Defendant") — not real case names.
    const sides=name.split(/\sv(?:s)?\.?\s/i);
    const GENERIC=/^(plaintiff|defendant|petitioner|respondent|appellant|applicant|complainant|accused|claimant|opposite party|parties)s?$/i;
    if(sides.length===2 && GENERIC.test(sides[0].trim()) && GENERIC.test(sides[1].trim())) continue;
    if(m.index>last) out.push(str.slice(last,m.index));
    const ym=name.match(/\b(1[89]\d\d|20\d\d)\b/); const year=ym?ym[1]:"";
    out.push(<button key={kb+"_bn"+(n++)} className="doc-citelink" title="Open this case"
      onClick={()=>onCite({ raw:name, name, citation:"", year })}>{name}<Icon name="ext" style={{width:11,height:11,marginLeft:3,verticalAlign:"-1px"}}/></button>);
    last=m.index+m[0].length;
  }
  if(last<str.length) out.push(str.slice(last));
  return out;
}
function linkifyCites(str, kb, onCite){
  if(!onCite || !str) return [str];
  const re=citeRe(); const out=[]; let last=0,m,n=0;
  while((m=re.exec(str))){
    if(m.index>last) out.push(str.slice(last,m.index));
    const rawName=(m[1]||"").replace(/\s+/g," ").trim();
    const kept=trimLeadWords(rawName);
    const dropped=rawName.slice(0, rawName.length-kept.length);   // leading connectors → keep as plain text
    if(dropped) out.push(dropped);
    const name=kept.replace(/[.,;:]+$/,"");
    const citation=(m[2]||"").replace(/\s+/g," ").trim();
    const raw=(name+" "+citation).trim();
    const ym=raw.match(/\b(1[89]\d\d|20\d\d)\b/); const year=ym?ym[1]:"";
    out.push(<button key={kb+"_cl"+(n++)} className="doc-citelink" title="Open this case"
      onClick={()=>onCite({ raw, name, citation, year })}>{raw}<Icon name="ext" style={{width:11,height:11,marginLeft:3,verticalAlign:"-1px"}}/></button>);
    last=m.index+m[0].length;
  }
  if(last<str.length) out.push(str.slice(last));
  return out;
}

// Turn [n] reference markers in a grounded-analysis answer into clickable chips
// that open the matching verified authority (refs[n-1]) in the case drawer.
function linkifyRefs(str, kb, onCite, refs){
  if(!refs || !refs.length || !str) return [str];
  const re=/\[(\d{1,2})\]/g; const out=[]; let last=0,m,n=0;
  while((m=re.exec(str))){
    const idx=Number(m[1]); const ref=refs[idx-1];
    if(m.index>last) out.push(str.slice(last,m.index));
    if(ref) out.push(<button key={kb+"_rf"+(n++)} className="doc-reflink" title={ref.title+" — "+ref.court}
      onClick={()=>onCite({ raw:ref.title, name:ref.title, citation:ref.cite, data:{ case:ref } })}>[{idx}]</button>);
    else out.push(m[0]);
    last=m.index+m[0].length;
  }
  if(last<str.length) out.push(str.slice(last));
  return out;
}
// Map only the string pieces of a mixed (string | element) array through fn.
function expandStrings(parts, fn){
  const out=[];
  parts.forEach((p,i)=>{ if(typeof p==="string") out.push(...fn(p,i)); else out.push(p); });
  return out;
}
// Linkify case citations (name + reporter), then bare "X v(s) Y" case names,
// then [n] reference markers — each pass only touches leftover plain text.
function linkifyAll(str, kb, onCite, refs){
  let parts=linkifyCites(str, kb, onCite);
  parts=expandStrings(parts,(p,i)=>linkifyBareNames(p, kb+"n"+i, onCite));
  if(refs && refs.length) parts=expandStrings(parts,(p,i)=>linkifyRefs(p, kb+"r"+i, onCite, refs));
  return parts;
}

// Inline markdown: **bold** then *italic*, `code`, and clickable citations.
function mdInline(text, onCite, kb, refs){
  kb=kb||"x";
  const out=[];
  String(text==null?"":text).split(/\*\*([\s\S]+?)\*\*/).forEach((seg,i)=>{
    if(i%2){ out.push(<strong key={kb+"b"+i}>{linkifyAll(seg,kb+"b"+i,onCite,refs)}</strong>); return; }
    seg.split(/`([^`]+)`/).forEach((cs,k)=>{
      if(k%2){ out.push(<code key={kb+"c"+i+"_"+k}>{cs}</code>); return; }
      cs.split(/\*([^*\n]+?)\*/).forEach((s,j)=>{
        if(j%2) out.push(<em key={kb+"i"+i+"_"+k+"_"+j}>{linkifyAll(s,kb+"i"+i+"_"+k+"_"+j,onCite,refs)}</em>);
        else if(s!=="") out.push(...linkifyAll(s,kb+"t"+i+"_"+k+"_"+j,onCite,refs));
      });
    });
  });
  return out;
}

// Block markdown: headings, bullet/numbered lists, and paragraphs. Tolerant of
// partial input so it renders cleanly while an answer is still streaming.
function DocMarkdown({ text, onCite, refs }){
  const lines=String(text==null?"":text).replace(/\r/g,"").split("\n");
  const blocks=[]; let list=null, para=[], quote=null;
  const flushPara=()=>{ if(para.length){ blocks.push({t:"p",lines:para}); para=[]; } };
  const flushList=()=>{ if(list){ blocks.push(list); list=null; } };
  const flushQuote=()=>{ if(quote){ blocks.push(quote); quote=null; } };
  const flushAll=()=>{ flushPara(); flushList(); flushQuote(); };
  for(const raw of lines){
    const t=raw.trim(); let m;
    if(!t){ flushAll(); continue; }
    if(/^([-*_])\1{2,}$/.test(t)){ flushAll(); blocks.push({t:"hr"}); continue; }   // --- rule
    if(m=t.match(/^(#{1,4})\s+(.*)$/)){ flushAll(); blocks.push({t:"h",level:m[1].length,text:m[2]}); continue; }
    if(m=t.match(/^>\s?(.*)$/)){ flushPara(); flushList(); if(!quote){ quote={t:"q",lines:[]}; } quote.lines.push(m[1]); continue; }
    if(m=t.match(/^[-*•]\s+(.*)$/)){ flushPara(); flushQuote(); if(!list||list.t!=="ul"){ flushList(); list={t:"ul",items:[]}; } list.items.push(m[1]); continue; }
    if(m=t.match(/^\d+[.)]\s+(.*)$/)){ flushPara(); flushQuote(); if(!list||list.t!=="ol"){ flushList(); list={t:"ol",items:[]}; } list.items.push(m[1]); continue; }
    flushList(); flushQuote(); para.push(t);
  }
  flushAll();
  return <>{blocks.map((b,i)=>{
    if(b.t==="hr") return <hr className="qa-md-hr" key={i}/>;
    if(b.t==="h") return <div className={"qa-md-h qa-md-h"+b.level} key={i}>{mdInline(b.text,onCite,"h"+i,refs)}</div>;
    if(b.t==="q") return <blockquote className="qa-md-q" key={i}>{b.lines.map((ln,j)=><React.Fragment key={j}>{j>0?<br/>:null}{mdInline(ln,onCite,"q"+i+"_"+j,refs)}</React.Fragment>)}</blockquote>;
    if(b.t==="ul") return <ul className="qa-md-ul" key={i}>{b.items.map((it,j)=><li key={j}>{mdInline(it,onCite,"u"+i+"_"+j,refs)}</li>)}</ul>;
    if(b.t==="ol") return <ol className="qa-md-ol" key={i}>{b.items.map((it,j)=><li key={j}>{mdInline(it,onCite,"o"+i+"_"+j,refs)}</li>)}</ol>;
    return <p className="qa-md-p" key={i}>{b.lines.map((ln,j)=><React.Fragment key={j}>{j>0?<br/>:null}{mdInline(ln,onCite,"p"+i+"_"+j,refs)}</React.Fragment>)}</p>;
  })}</>;
}
// Copy a draft's plain text to the clipboard with a toast.
function copyAnswer(text){
  if(navigator.clipboard){ navigator.clipboard.writeText(String(text||"")).then(()=>{ if(window.toast) toast("Draft copied"); },()=>{}); }
}

// Deep-links to other legal databases for a cited authority / statute. Most
// (SCC Online, Manupatra, Casemine) are subscription-gated with no open API, so
// we link to their search rather than pulling content; primary verified data
// still comes from Indian Kanoon.
function caseSources({ name, citation }){
  const q=encodeURIComponent([name,citation].filter(Boolean).join(" ").trim());
  const nq=encodeURIComponent((name||citation||"").trim());
  return [
    { label:"Google Scholar", url:"https://scholar.google.com/scholar?hl=en&as_sdt=2006&q="+q },
    { label:"SCC Online", url:"https://www.scconline.com/Members/SearchEntry.aspx?searchtype=NL&querystring="+nq },
    { label:"Casemine", url:"https://www.casemine.com/search/in?q="+nq },
    { label:"Manupatra", url:"https://www.google.com/search?q="+encodeURIComponent(((name||citation||"")+" site:manupatra.com").trim()) },
    { label:"SC portal", url:"https://main.sci.gov.in/judgments" },
    { label:"eCourts", url:"https://judgments.ecourts.gov.in/" },
  ];
}
function statuteSources({ act, section, label }){
  const q=encodeURIComponent([act, section?("section "+section):""].filter(Boolean).join(" ").trim());
  const aq=encodeURIComponent((act||"").trim());
  return [
    { label:"IndiaCode", url:"https://www.indiacode.nic.in/simple-search?query="+aq },
    { label:"Indian Kanoon", url:"https://indiankanoon.org/search/?formInput="+encodeURIComponent((label||act||"").trim()) },
    { label:"Web search", url:"https://www.google.com/search?q="+q },
  ];
}

// Drawer for a cited authority — the SAME layout and details as the Ask page's
// authority drawer (good-law status, court/tier, bench, cited-by, holding, key
// passage, Open-on-Indian-Kanoon), resolved from a free-text citation.
function CiteDrawer({ cite, onClose }){
  useEffect(()=>{
    if(!cite) return;
    function esc(e){ if(e.key==="Escape") onClose(); }
    window.addEventListener("keydown",esc); return ()=>window.removeEventListener("keydown",esc);
  },[cite]);
  if(!cite) return null;
  const data=cite.data||{};
  const c=data.case||null;
  const fallbackIk=(data.ik)||("https://indiankanoon.org/search/?formInput="+encodeURIComponent(cite.citation||cite.name||cite.raw));
  const LS=window.LAW_STATUS||{};
  const st=(c && (LS[c.status]||LS.good)) || { tone:"ok", label:"", note:"" };
  function copyCite(){ const t=(c?c.title+", "+(c.cite||""):cite.raw); if(navigator.clipboard){ navigator.clipboard.writeText(t).then(()=>{ if(window.toast) toast("Citation copied"); },()=>{}); } }
  return (
    <div className="drawer-scrim" onClick={onClose}>
      <aside className="drawer" onClick={e=>e.stopPropagation()}>
        {c ? (
          <>
            <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" onClick={onClose}><Icon name="plus" style={{transform:"rotate(45deg)",width:18,height:18}}/></button>
            </div>
            <h2 className="dr-title" dir="auto">{c.title}</h2>
            {c.cite ? <div className="dr-cite mono">{c.cite}</div> : null}

            <div className={"goodlaw "+st.tone}>
              <div className="gl-ico">
                <Icon name={st.tone==="bad"?"plus":st.tone==="warn"?"shield":"check"} 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.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}

            <div className="dr-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">Authored by</span><span>{c.judge||"—"}</span></div>
              <div className="dr-f"><span className="dr-fl mono">Year</span><span>{c.year||"—"}</span></div>
              <div className="dr-f"><span className="dr-fl mono">Cited by</span><span>{(c.citedby||0).toLocaleString()} judgments</span></div>
            </div>

            {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.disposition ? <div className="dr-sec"><div className="dr-sl mono">Disposition</div><p dir="auto">{c.disposition}</p></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={copyCite}><Icon name="copy"/> Copy citation</button>
            </div>
            <div className="dr-sec"><div className="dr-sl mono">Also available on</div>
              <div className="dr-chips">{caseSources({ name:c.title, citation:c.cite }).map((s,i)=>(
                <a className="dr-chip src-chip" key={i} href={s.url} target="_blank" rel="noreferrer">{s.label}<Icon name="ext" style={{width:10,height:10,opacity:.55}}/></a>
              ))}</div>
            </div>
            <div className="dr-verified mono"><Icon name="check" style={{width:13,height:13}}/> Verified on Indian Kanoon · other sources are external lookups</div>
          </>
        ) : (
          <>
            <div className="dr-top">
              <span className="tier-badge">CASE</span>
              <span className="dr-court">Authority</span>
              <button className="dr-x" onClick={onClose}><Icon name="plus" style={{transform:"rotate(45deg)",width:18,height:18}}/></button>
            </div>
            <h2 className="dr-title" dir="auto">{cite.name||cite.raw}</h2>
            {cite.citation ? <div className="dr-cite mono">{cite.citation}</div> : null}
            {cite.loading
              ? <div className="dr-sec"><p className="qa-pending mono"><span className="qtab-spin"/> Looking it up on Indian Kanoon…</p></div>
              : <div className="dr-sec"><p>{data.configured===false
                  ? "Case-law lookup isn't configured on this server."
                  : "Couldn't confirm this citation automatically — open Indian Kanoon to find the judgment."}</p></div>}
            {!cite.loading &&
              <div className="dr-actions"><a className="btn btn-primary" href={fallbackIk} target="_blank" rel="noreferrer">Search on Indian Kanoon <Icon name="ext"/></a></div>}
            {!cite.loading &&
              <div className="dr-sec"><div className="dr-sl mono">Also search on</div>
                <div className="dr-chips">{caseSources({ name:cite.name||cite.raw, citation:cite.citation }).map((s,i)=>(
                  <a className="dr-chip src-chip" key={i} href={s.url} target="_blank" rel="noreferrer">{s.label}<Icon name="ext" style={{width:10,height:10,opacity:.55}}/></a>
                ))}</div>
              </div>}
          </>
        )}
      </aside>
    </div>
  );
}

// Parse a free-text "law applied" chip (e.g. "Contract Act, 1872, Section 17"
// or "A.P.CTA 1955, Section 4(1)") into the act name + section.
function parseLaw(label){
  const s=String(label||"").trim();
  const m=s.match(/^(.*?)[,;]?\s*(?:section|sec\.?|§|article|art\.?)\s*([0-9]+[A-Za-z]*(?:\([0-9a-z]+\))?(?:\s*\/\s*[0-9]+[A-Za-z]*)?)\s*$/i);
  let act=s, section="", sectionNum="";
  if(m){ act=m[1].replace(/[,;]\s*$/,"").trim(); section=m[2].trim(); const n=section.match(/\d+[A-Za-z]?/); sectionNum=n?n[0]:""; }
  if(!act) act=s;   // e.g. a bare "Article 21" — keep the label as the title
  return { act, section, sectionNum };
}

// Drawer for a "law applied" chip — looks up the bare-act section text on
// IndiaCode (central acts) and offers links; mirrors the case drawer's layout.
function LawDrawer({ law, onClose }){
  useEffect(()=>{
    if(!law) return;
    function esc(e){ if(e.key==="Escape") onClose(); }
    window.addEventListener("keydown",esc); return ()=>window.removeEventListener("keydown",esc);
  },[law]);
  if(!law) return null;
  const results=(law.data && Array.isArray(law.data.results)) ? law.data.results : [];
  const hit=results.find(r=>r && r.found && r.text) || null;
  const ikUrl="https://indiankanoon.org/search/?formInput="+encodeURIComponent(law.label||law.act);
  const icUrl=(hit && hit.url) || ("https://www.indiacode.nic.in/simple-search?query="+encodeURIComponent(law.act));
  return (
    <div className="drawer-scrim" onClick={onClose}>
      <aside className="drawer" onClick={e=>e.stopPropagation()}>
        <div className="dr-top">
          <span className="tier-badge">STATUTE</span>
          <span className="dr-court">{law.section?("Section "+law.section):"Provision"}</span>
          <button className="dr-x" onClick={onClose}><Icon name="plus" style={{transform:"rotate(45deg)",width:18,height:18}}/></button>
        </div>
        <h2 className="dr-title" dir="auto">{law.act}</h2>
        {law.section ? <div className="dr-cite mono">Section {law.section}</div> : null}
        {law.loading
          ? <div className="dr-sec"><p className="qa-pending mono"><span className="qtab-spin"/> Looking up the bare act…</p></div>
          : hit
            ? <div className="dr-sec"><div className="dr-sl mono">Bare act text · {hit.act}{hit.section?" · S. "+hit.section:""}</div><blockquote className="dr-quote" dir="auto">{hit.text}</blockquote></div>
            : <div className="dr-sec"><div className="dr-sl mono">Official text</div><p>The verbatim section text isn't available here — common for State Acts, which aren't on IndiaCode. Use the links below to read it on the official record.</p></div>}
        <div className="dr-actions">
          <a className="btn btn-primary" href={hit&&hit.url?hit.url:icUrl} target="_blank" rel="noreferrer">IndiaCode <Icon name="ext"/></a>
          <a className="btn btn-ghost" href={ikUrl} target="_blank" rel="noreferrer">Indian Kanoon <Icon name="ext"/></a>
        </div>
        <div className="dr-sec"><div className="dr-sl mono">Also search on</div>
          <div className="dr-chips">{statuteSources({ act:law.act, section:law.section, label:law.label }).map((s,i)=>(
            <a className="dr-chip src-chip" key={i} href={s.url} target="_blank" rel="noreferrer">{s.label}<Icon name="ext" style={{width:10,height:10,opacity:.55}}/></a>
          ))}</div>
        </div>
        <div className="dr-verified mono"><Icon name="shield" style={{width:13,height:13}}/> Verify the provision against the official gazette before relying on it.</div>
      </aside>
    </div>
  );
}

/* ---------- tabbed workspace ----------
   Document Intelligence is a multi-tab workspace, mirroring the Ask page: each
   tab is an independent document (empty / loading / loaded / error) and the set
   of tabs persists in sessionStorage, so navigating away and back keeps them. */
const TABS_KEY = "adv_v2_doctabs";
function shorten(s){ s=String(s||"").trim(); return s.length>26 ? s.slice(0,26)+"…" : (s||"New document"); }
// A tab holds a SET of documents (each = the server's `doc` object + its
// extracted `text`). Q&A spans them all. `addNote` carries a non-fatal message
// when only some of a multi-file drop could be added.
function newDocTab(){ return { id:"d"+Date.now().toString(36)+Math.random().toString(36).slice(2,6), title:"New document", phase:"empty", docs:[], thread:[], mode:loadMode(), side:"", errNote:"", addNote:"", busy:false, precedents:null, precBusy:false, precNote:"" }; }
// A title for the tab strip: first document, "+N" when there are more.
function tabTitle(docs){
  if(!docs || !docs.length) return "New document";
  const first=shorten(docs[0].name||"Document");
  return docs.length>1 ? first+" +"+(docs.length-1) : first;
}
function loadDocTabs(){
  try{ const o=JSON.parse(sessionStorage.getItem(TABS_KEY)||"null");
    if(o && Array.isArray(o.tabs) && o.tabs.length){
      // Migrate the old single-document shape ({doc,text}) to {docs:[…]}.
      o.tabs=o.tabs.map(t=>{
        if(Array.isArray(t.docs)) return t;
        const docs = t.doc ? [{ ...t.doc, text:t.text||"" }] : [];
        const { doc, text, ...rest }=t;
        return { ...rest, docs };
      });
      return o;
    }
  }catch(e){}
  return null;
}

function DocTabStrip({ 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 document"}
             onClick={()=>onSelect(t.id)}>
          {t.busy ? <span className="qtab-spin"/> : null}
          <span className="qtab-t">{t.title||"New document"}</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 document" aria-label="New document" onClick={onNew}>
        <Icon name="plus" style={{width:15,height:15}}/>
      </button>
    </div>
  );
}

// One document tab's view. Presentational: all state lives in `tab`; uploads and
// questions are dispatched up to DocsApp (which owns the async so switching tabs
// mid-request never aborts it). A tab holds a SET of documents (tab.docs) and
// every question is answered across all of them. Local-only: draft + drag state.
// On-point precedents for the uploaded document(s): verified, good-law-graded
// Indian Kanoon authorities found via the shared Ask retrieval. Each card opens
// in the same case drawer as a cited authority (fully resolved, no extra fetch).
// `precedents` is null until the user runs the search, then an array (possibly
// empty). The whole panel is a no-op on servers where DOCINTEL_PRECEDENTS is
// off — the server returns a note explaining how to enable it.
function DocPrecedents({ precedents, issue, busy, note, onFind, onOpen }){
  const LS=window.LAW_STATUS||{};
  const has=Array.isArray(precedents)&&precedents.length>0;
  return (
    <div className="doc-precedents">
      <div className="dp-head">
        <div className="dp-title"><Icon name="scale" style={{width:15,height:15}}/> On-point precedents <span className="dp-sub mono">from Indian Kanoon</span></div>
        <button className="btn btn-ghost dp-find" disabled={busy} onClick={onFind}>
          {busy ? <><span className="qtab-spin"/> Searching…</> : (has?<><Icon name="spark" style={{width:13,height:13}}/> Refresh</>:<><Icon name="spark" style={{width:13,height:13}}/> Find precedents</>)}
        </button>
      </div>
      {precedents===null && !busy && <p className="dp-hint mono">Surface verified case-law authorities on point with this document — each confirmed on Indian Kanoon with its good-law status.</p>}
      {has && issue ? <p className="dp-issue">On the issue: <b>{issue}</b></p> : null}
      {note && <div className="doc-err mono" style={{margin:"4px 0 0"}}><Icon name="shield" style={{width:13,height:13}}/> {note}</div>}
      {has && <div className="dp-list">
        {precedents.map((c,i)=>{
          const st=(LS[c.status]||LS.good)||{ tone:"ok" };
          // The holding paragraph is this system's ratio; show the headnote and a
          // distinct key passage when they add something beyond it.
          const holding=(c.holding||"").trim();
          const keyPassage=(c.keyPassage||"").trim();
          const headnote=(c.headnote||"").trim();
          const showHeadnote=headnote && headnote!==holding;
          const showKey=keyPassage && keyPassage!==holding;
          return (
            <button className="dp-card" key={c.id||i} onClick={()=>onOpen({ raw:c.title, name:c.title, citation:c.cite, data:{ case:c } })}>
              <div className="dp-card-top">
                <span className={"tier-badge tier-"+c.tier}>{c.tier}</span>
                {c.benchStrength?<span className="dp-bench mono">{c.benchStrength}</span>:null}
                <span className={"dp-dot "+(st.tone||"ok")}/>
                <span className={"dp-status "+(st.tone||"ok")}>{st.label||"Good law"}</span>
                <span className="dp-court mono">{c.court}</span>
              </div>
              <div className="dp-card-title" dir="auto">{c.title}</div>
              {c.cite?<div className="dp-card-cite mono">{c.cite}</div>:null}

              {showHeadnote?<div className="dp-block">
                <div className="dp-lbl mono">Headnote</div>
                <div className="dp-text dp-clamp2" dir="auto">{headnote}</div>
              </div>:null}

              {holding?<div className="dp-block">
                <div className="dp-lbl mono">Holding · ratio{c.paraNum?` · ¶ ${c.paraNum}`:""}</div>
                <div className="dp-text dp-clamp4" dir="auto">{holding}</div>
              </div>:null}

              {showKey?<div className="dp-block">
                <div className="dp-lbl mono">Key passage</div>
                <blockquote className="dp-quote dp-clamp3" dir="auto">“{keyPassage}”</blockquote>
              </div>:null}

              {c.disposition?<div className="dp-block"><span className="dp-disp">{c.disposition}</span></div>:null}

              {c.sections&&c.sections.length?<div className="dp-chips">
                {c.sections.map((s,j)=><span className="dp-chip mono" key={j}>{s}</span>)}
              </div>:null}

              <div className="dp-card-meta mono">{[c.year,c.bench].filter(Boolean).join(" · ")}{c.citedby?` · cited by ${c.citedby.toLocaleString()}`:""}<span className="dp-open">Open <Icon name="arrow" style={{width:11,height:11}}/></span></div>
            </button>
          );
        })}
      </div>}
    </div>
  );
}

// Verified case-law references that back a legal-analysis answer. Each carries
// the model's explanation of HOW it applies (with its court), plus the verified
// holding; clicking opens the full case drawer (already fully resolved).
function AnswerRefs({ refs, onOpen }){
  const LS=window.LAW_STATUS||{};
  return (
    <div className="qa-refs">
      <div className="qa-refs-h mono"><Icon name="scale" style={{width:12,height:12}}/> References · {refs.length} verified on Indian Kanoon</div>
      <div className="qa-reflist">
        {refs.map((c,i)=>{
          const st=(LS[c.status]||LS.good)||{ tone:"ok" };
          const holding=(c.holding||c.headnote||"").trim();
          return (
            <button className="qa-ref" key={c.id||i} onClick={()=>onOpen({ raw:c.title, name:c.title, citation:c.cite, data:{ case:c } })}>
              <div className="qa-ref-top">
                <span className="qa-ref-n mono">{c.n||i+1}</span>
                <span className={"tier-badge tier-"+c.tier}>{c.tier}</span>
                <span className={"dp-dot "+(st.tone||"ok")}/>
                <span className={"dp-status "+(st.tone||"ok")}>{st.label||"Good law"}</span>
                <span className="dp-court mono">{c.court}</span>
              </div>
              <div className="qa-ref-title" dir="auto">{c.title}{c.cite?<span className="qa-ref-cite mono"> · {c.cite}</span>:null}</div>
              {c.why?<div className="qa-ref-why" dir="auto"><b>Why it applies:</b> {c.why}</div>:null}
              {holding?<div className="qa-ref-hold dp-clamp3" dir="auto"><span className="dp-lbl mono">Holding{c.paraNum?` · ¶ ${c.paraNum}`:""}</span> {holding}</div>:null}
              <div className="qa-ref-meta mono">{[c.year,c.bench].filter(Boolean).join(" · ")}{c.citedby?` · cited by ${c.citedby.toLocaleString()}`:""}<span className="dp-open">Open <Icon name="arrow" style={{width:11,height:11}}/></span></div>
            </button>
          );
        })}
      </div>
    </div>
  );
}

// Determinate-feeling progress for document reading. A single extract call is
// one network round-trip with no sub-progress, so the bar would otherwise look
// frozen; we show the real "X of N files" floor and creep the percentage up
// within the current file so it always reads as moving, snapping to 100 at the end.
function DocProgress({ up }){
  const total=(up&&up.total)||1, done=(up&&up.done)||0;
  const allDone = done>=total;
  const [pct,setPct]=useState(0);
  useEffect(()=>{
    if(allDone){ setPct(100); return; }
    const floor=Math.round((done/total)*100);
    const ceil=Math.min(96, floor + Math.round(90/total));   // headroom for the in-flight file
    const t=setInterval(()=>setPct(p=> p<floor ? floor : (p<ceil ? p+Math.max(1,Math.round((ceil-p)/14)) : p)), 140);
    return ()=>clearInterval(t);
  },[done,total,allDone]);
  const shown=allDone?100:pct;
  return (
    <div className="docprog">
      <div className="docprog-track"><span className="docprog-fill" style={{width:shown+"%"}}/></div>
      <div className="docprog-meta mono">
        {allDone ? "Finishing up…" : (total>1 ? "Reading document "+Math.min(done+1,total)+" of "+total : "Reading the document")}
        <b>{shown}%</b>
      </div>
    </div>
  );
}

function DocWorkspace({ tab, onFiles, onRemoveDoc, onSend, onReAsk, onCite, onLaw, onPrecedents, onSetMode, onSetSide, onNewDoc }){
  const [draft,setDraft]=useState("");
  const [opt,setOpt]=useState("chat");        // one of the 5 options; Chat is the default
  const [clOpen,setClOpen]=useState({});      // per-document "Clauses detected" expand state (collapsed by default)
  const [dragging,setDragging]=useState(false);
  const [expOpen,setExpOpen]=useState(false);
  const fileRef=useRef(null);
  const expRef=useRef(null);
  const { phase, docs, thread, mode, side, errNote, addNote, busy, precedents, precBusy, precNote } = tab;
  useEffect(()=>{ function od(e){ if(expRef.current && !expRef.current.contains(e.target)) setExpOpen(false); } document.addEventListener("mousedown",od); return ()=>document.removeEventListener("mousedown",od); },[]);
  function onPick(e){ const fs=e.target.files; if(fs&&fs.length) onFiles(fs); e.target.value=""; }
  function onDrop(e){ e.preventDefault(); setDragging(false); const fs=e.dataTransfer&&e.dataTransfer.files; if(fs&&fs.length) onFiles(fs); }
  // Pick one of the 5 options. Chat = free-text Q&A; a lens drops its editable
  // prompt into the box (NOT sent yet) so the user can tweak it and commit with Ask.
  function pickOpt(k){ setOpt(k); const l=LENSES.find(x=>x.k===k); setDraft(l?l.prompt:""); }
  function ask(){
    const q=draft.trim();
    if(busy || (!q && opt==="chat")) return;     // Chat needs text; a lens can fire on its own
    const useMode = opt==="chat" ? "document" : "analysis";
    setDraft(""); setOpt("chat");
    onSend(q, opt==="chat"?null:opt, useMode);
  }
  const docMeta=(d)=>[ d.type, d.pages?d.pages+" pages":null, d.words?d.words.toLocaleString()+" words":null ].filter(Boolean).join(" · ");

  if(phase==="empty" || phase==="error"){
    return (
      <div className="doc-stage">
        <div className="doc-empty">
          <div className="pill brand" style={{marginBottom:18}}><span className="dot"/> Document Intelligence</div>
          <h1 className="ask-h1">Read any document.<br/><span>Ask it anything.</span></h1>
          <p className="ask-lede">Upload one or more judgments, contracts, FIRs or notices. Advokacy extracts the key facts and answers your questions across <b>all</b> of them — grounded only in those documents, with a pointer to the exact clause and file.</p>
          <input ref={fileRef} type="file" accept={ACCEPT} multiple style={{display:"none"}} onChange={onPick}/>
          <button className={"dropzone"+(dragging?" drag":"")}
            onClick={()=>fileRef.current&&fileRef.current.click()}
            onDragOver={e=>{e.preventDefault();setDragging(true);}}
            onDragLeave={()=>setDragging(false)}
            onDrop={onDrop}>
            <div className="dz-ico"><Icon name="upload" style={{width:26,height:26}}/></div>
            <b>{dragging?"Drop to upload":"Drop files or click to upload"}</b>
            <span className="mono">PDF · DOCX · TXT · up to {MAX_MB} MB each · multiple allowed</span>
          </button>
          {phase==="error" && <div className="doc-err mono"><Icon name="shield" style={{width:13,height:13}}/> {errNote}</div>}
          <div className="trust-row">
            <div className="trust"><Icon name="shield"/> Answers only from your documents</div>
            <div className="trust"><Icon name="check"/> Every answer points to a clause</div>
          </div>
        </div>
      </div>
    );
  }

  if(phase==="loading"){
    return (
      <div className="doc-stage">
        <div className="doc-empty">
          <h1 className="ask-h1" style={{fontSize:24}}>Reading your documents…</h1>
          <p className="ask-lede">Extracting the text and pulling out the key facts and clauses.</p>
          <div className="docprog-wrap"><DocProgress up={tab.up}/></div>
        </div>
      </div>
    );
  }

  // loaded — one or more documents. Union the parties across the set (de-duped,
  // order preserved) for the summary banner and the "acting for" selector.
  const multi=docs.length>1;
  const parties=[]; const seen=new Set();
  docs.forEach(d=>(d.parties||[]).forEach(p=>{ if(p && !seen.has(p)){ seen.add(p); parties.push(p); } }));
  return (
    <>
      <div className="result-bar">
        <button className="rb-back" onClick={onNewDoc}><Icon name="plus" style={{width:15,height:15}}/> New document</button>
        <div className="rb-spacer"/>
        <div className="doc-export" ref={expRef}>
          <button className="btn btn-ghost" onClick={()=>setExpOpen(o=>!o)}>
            <Icon name="upload" style={{width:14,height:14,transform:"rotate(180deg)"}}/> Export
            <Icon name="chev" style={{width:12,height:12,transform:"rotate(90deg)",opacity:.6}}/>
          </button>
          {expOpen && (
            <div className="exp-menu">
              <button className="exp-item" onClick={()=>{ setExpOpen(false); exportPdf(tab); }}><Icon name="docs" style={{width:14,height:14}}/> PDF <span className="exp-hint mono">print / save as PDF</span></button>
              <button className="exp-item" onClick={()=>{ setExpOpen(false); exportWord(tab); }}><Icon name="docs" style={{width:14,height:14}}/> Word <span className="exp-hint mono">.doc download</span></button>
            </div>
          )}
        </div>
      </div>
      <div className="scroll">
        <div className="doc-merged">
          {/* one merged overview card per document — file · summary · key facts · clauses */}
          {multi && <div className="doc-setcount mono">{docs.length} documents</div>}
          {docs.map((doc,di)=>(
            <div className="doccard" key={di}>
              <div className="doc-file">
                <div className="doc-file-ic"><Icon name="docs"/></div>
                <div style={{minWidth:0,flex:1}}>
                  <div className="doc-file-n" title={doc.name}>{doc.name}</div>
                  <div className="doc-file-m mono">{[doc.type, doc.date, docMeta(doc)].filter(Boolean).join(" · ")}</div>
                </div>
                <button className="doccard-x" title="Remove this document" aria-label="Remove document" disabled={busy} onClick={()=>onRemoveDoc(di)}>
                  <Icon name="plus" style={{width:14,height:14,transform:"rotate(45deg)"}}/>
                </button>
              </div>
              {doc.ocr && <div className="doc-err mono" style={{margin:"0 0 12px",color:"var(--ink-3)"}}><Icon name="spark" style={{width:13,height:13}}/> Scanned PDF — text recovered via OCR; verify accuracy against the original.</div>}
              {doc.truncated && <div className="doc-err mono" style={{margin:"0 0 12px"}}><Icon name="shield" style={{width:13,height:13}}/> Long document — analysis used the first part of the text.</div>}
              {doc.summary ? <p className="ds-text doc-cardsum">{doc.summary}</p> : null}
              {doc.facts.length>0 && <>
                <div className="doc-sec-l mono">Key facts</div>
                <div className="doc-facts">
                  {doc.facts.map((f,i)=>(
                    <div className="doc-fact" key={i}><span className="df-k">{f.k}</span><span className="df-v">{f.v}</span></div>
                  ))}
                </div>
              </>}
              {doc.clauses.length>0 && <div className="doc-clausesec">
                <button className="doc-sec-l mono doc-cltoggle" onClick={()=>setClOpen(o=>({ ...o, [di]:!o[di] }))} aria-expanded={!!clOpen[di]}>
                  <Icon name="chev" style={{width:11,height:11,transition:"transform .18s",transform:clOpen[di]?"rotate(90deg)":"none"}}/>
                  Clauses detected <span className="doc-clcount">{doc.clauses.length}</span>
                </button>
                {clOpen[di] && <div className="doc-clauses">
                  {doc.clauses.map((c,i)=>(
                    <div className="doc-clause" key={i}>
                      {c.id?<span className="dc-id mono">{c.id}</span>:null}
                      <span className="dc-t">{c.t}</span>
                      {c.p?<span className="dc-p mono">p.{c.p}</span>:null}
                    </div>
                  ))}
                </div>}
              </div>}
            </div>
          ))}
          {parties.length>0 && <div className="ds-parties">
            {parties.map((p,i)=><span className="party" key={i}><Icon name="scale" style={{width:13,height:13}}/>{p}</span>)}
          </div>}
          <input ref={fileRef} type="file" accept={ACCEPT} multiple style={{display:"none"}} onChange={onPick}/>
          {busy && tab.up
            ? <div className="docprog-wrap"><DocProgress up={tab.up}/></div>
            : <button className="docadd" disabled={busy} onClick={()=>fileRef.current&&fileRef.current.click()}>
                <Icon name="plus" style={{width:14,height:14}}/> Add document
              </button>}
          {addNote && <div className="doc-err mono" style={{margin:"10px 0 0"}}><Icon name="shield" style={{width:13,height:13}}/> {addNote}</div>}

            <DocPrecedents precedents={precedents} issue={tab.precIssue} busy={precBusy} note={precNote} onFind={onPrecedents} onOpen={onCite}/>

            <div className="qa-thread">
              {thread.length===0 && <div className="qa-hint mono">{mode==="analysis"
                ? "Legal analysis mode — answers apply Indian law (statutes, strategy, risks) across "+(multi?"all "+docs.length+" documents":"this document")+". Verify before relying on them."
                : "Ask anything about "+(multi?"these "+docs.length+" documents":"this document")+" — answers are grounded only in "+(multi?"their":"its")+" text."}</div>}
              {thread.map((m,i)=>(
                <div className="qa" key={i}>
                  <div className="qa-q"><span className="qa-av">Q</span><p>{m.q}</p></div>
                  <div className={"qa-a"+(m.mode==="analysis"?" ana":"")+(m.error?" err":"")}>
                    {m.pending
                      ? <div className="qa-pending">
                          <span className="qtab-spin"/>
                          <div className="qa-pending-tx">
                            <b>{m.mode==="analysis"?"Drafting the legal analysis…":"Reading the document…"}</b>
                            {m.mode==="analysis"?<span className="mono">Finding verified authorities on Indian Kanoon and grounding the answer in them.</span>:null}
                          </div>
                        </div>
                      : <>
                          {!m.streaming && !m.error && <div className="qa-a-hd">
                            <span className={"qa-a-tag "+(m.mode==="analysis"?"ana":"doc")}>
                              <Icon name={m.mode==="analysis"?"scale":"docs"} style={{width:12,height:12}}/>
                              {m.mode==="analysis"?"Legal analysis":"Document answer"}
                            </span>
                            <button className="qa-copy" title="Copy draft" onClick={()=>copyAnswer(m.a)}><Icon name="copy" style={{width:13,height:13}}/></button>
                          </div>}
                          <div className="qa-md"><DocMarkdown text={m.a} onCite={onCite} refs={m.references}/>{m.streaming?<span className="tx-caret"/>:null}</div>
                          {!m.streaming && m.law && m.law.length>0 && <div className="qa-law">
                            {m.law.map((l,j)=>(
                              <button className="qa-lawchip mono" key={j} title={"Look up “"+l+"”"} onClick={()=>onLaw(l)}>
                                <Icon name="scale" style={{width:11,height:11}}/>{l}<Icon name="chev" style={{width:11,height:11,opacity:.5}}/>
                              </button>
                            ))}
                          </div>}
                          {!m.streaming && m.note && (!m.references || !m.references.length) &&
                            <div className="qa-note mono"><Icon name="shield" style={{width:12,height:12}}/> {m.note}</div>}
                          {!m.streaming && m.docFlags && m.docFlags.length>0 && <div className="qa-flags">
                            <div className="qa-flags-h mono"><Icon name="shield" style={{width:12,height:12}}/> Check the document — these stated points look wrong or unverifiable</div>
                            {m.docFlags.map((f,j)=>(<div className="qa-flag" key={j}>{f}</div>))}
                          </div>}
                          {!m.streaming && m.references && m.references.length>0 &&
                            <AnswerRefs refs={m.references} onOpen={onCite}/>}
                          {!m.streaming && <div className="qa-cites">
                            {m.cites.map((c,j)=>(
                              <span className="qa-cite mono" key={j} title={c.d||""}><Icon name="docs" style={{width:11,height:11}}/>{c.c}{c.p?" · p."+c.p:""}{c.d?" · "+shorten(c.d):""}</span>
                            ))}
                            {!m.error && (m.mode==="analysis"
                              ? (m.references && m.references.length>0
                                  ? <span className="qa-grounded mono"><Icon name="check" style={{width:11,height:11}}/> {m.references.length} verified {m.references.length===1?"authority":"authorities"} — verify before relying</span>
                                  : <span className="qa-applied mono"><Icon name="spark" style={{width:11,height:11}}/> no verified authority found — verify independently</span>)
                              : <span className="qa-grounded mono"><Icon name="check" style={{width:11,height:11}}/> grounded in source</span>)}
                          </div>}
                          {!m.streaming && !m.error && m.mode==="document" && m.q &&
                            <button className="qa-reask mono" disabled={busy} onClick={()=>onReAsk(m.q)}>
                              <Icon name="spark" style={{width:11,height:11}}/> Need legal analysis? Re-ask applying Indian law →
                            </button>}
                        </>}
                  </div>
                </div>
              ))}
            </div>

            {parties.length>0 &&
              <div className="doc-actingfor">
                <span className="af-l mono">Acting for</span>
                <select className="doc-side mono" value={side} onChange={e=>onSetSide(e.target.value)} title="Frame the analysis for a party (optional)">
                  <option value="">— optional —</option>
                  {parties.map((p,i)=><option key={i} value={p}>{p}</option>)}
                </select>
              </div>}
            {/* 5 options — Chat (default) + four legal-analysis lenses. Picking a
                lens loads its prompt into the box; nothing runs until Ask. */}
            <div className="doc-opts">
              <button className={"doc-opt"+(opt==="chat"?" on":"")} disabled={busy} onClick={()=>pickOpt("chat")}><Icon name="docs" style={{width:13,height:13}}/> Chat</button>
              {LENSES.map(l=>(
                <button className={"doc-opt"+(opt===l.k?" on":"")} key={l.k} disabled={busy} onClick={()=>pickOpt(l.k)}><Icon name="scale" style={{width:13,height:13}}/> {l.label}</button>
              ))}
            </div>

            <div className="doc-ask">
              <input className="doc-ask-in" placeholder={opt==="chat"?(multi?"Ask across all "+docs.length+" documents…":"Ask about this document…"):"Edit the request if you like, then press Ask…"} value={draft}
                onChange={e=>setDraft(e.target.value)} onKeyDown={e=>{if(e.key==="Enter")ask();}} disabled={busy}/>
              <button className="btn btn-primary" onClick={ask} disabled={busy||(!draft.trim()&&opt==="chat")}>Ask <Icon name="arrow"/></button>
            </div>
            <div className="result-foot mono">{opt==="chat"
              ? "Chat answers strictly from the uploaded "+(multi?"files":"file")+" and never fills gaps from outside law."
              : "Legal-analysis lenses apply general Indian law and the model's knowledge — verify statutes and citations before relying on them."}</div>
        </div>
      </div>
    </>
  );
}

function DocsApp(){
  const init=loadDocTabs();
  const [tabs,setTabs]=useState(()=> init ? init.tabs : [newDocTab()]);
  const [activeId,setActiveId]=useState(()=> init ? init.activeId : null);
  const [cite,setCite]=useState(null);          // case citation drawer (overlay)
  const [law,setLaw]=useState(null);            // statute/section drawer (overlay)
  const tabsRef=useRef(tabs); tabsRef.current=tabs;
  const stateRef=useRef({ tabs, activeId }); stateRef.current={ tabs, activeId };

  // Persist to sessionStorage, stripping transient in-flight state so a reload is
  // consistent: an interrupted upload reverts to empty and an unfinished (pending
  // / still-streaming) answer is dropped. Debounced so token-by-token streaming
  // doesn't re-serialise on every chunk; flushed on unmount so navigating away
  // (the whole point of this) still saves.
  function persistNow(){
    try{
      const { tabs, activeId }=stateRef.current;
      const persist=tabs.map(t=>{
        let nt=t;
        if(t.phase==="loading") nt={ ...nt, phase:"empty", title:"New document", docs:[] };
        if(nt.busy) nt={ ...nt, busy:false };
        if(nt.precBusy) nt={ ...nt, precBusy:false };
        const th=nt.thread||[];
        if(th.length && (th[th.length-1].pending || th[th.length-1].streaming)) nt={ ...nt, thread:th.slice(0,-1) };
        return nt;
      });
      sessionStorage.setItem(TABS_KEY, JSON.stringify({ tabs:persist, activeId }));
    }catch(e){}
  }
  useEffect(()=>{ const id=setTimeout(persistNow,350); return ()=>clearTimeout(id); },[tabs,activeId]);
  useEffect(()=>{ return ()=>persistNow(); },[]);   // flush on unmount

  // The v2 desktop UI is multi-page: clicking a nav link is a FULL page reload,
  // which aborts any in-flight request (analysis/upload) — it can't survive the
  // navigation. Warn before leaving while something is still running so the user
  // doesn't lose it by accident; if they stay, it completes normally.
  useEffect(()=>{
    const busy = tabs.some(t=> t.busy || t.precBusy || (t.thread||[]).some(m=> m.pending || m.streaming));
    if(!busy) return;
    const h=(e)=>{ e.preventDefault(); e.returnValue=""; return ""; };
    window.addEventListener("beforeunload",h);
    return ()=>window.removeEventListener("beforeunload",h);
  },[tabs]);

  // keep a valid active tab
  useEffect(()=>{
    if(!tabs.length){ const t=newDocTab(); setTabs([t]); setActiveId(t.id); return; }
    if(!tabs.find(t=>t.id===activeId)) setActiveId(tabs[0].id);
  },[tabs,activeId]);

  function patch(id,p){ setTabs(ts=>ts.map(t=> t.id===id ? { ...t, ...(typeof p==="function"?p(t):p) } : t)); }
  function patchThreadLast(id,p){ setTabs(ts=>ts.map(t=>{ if(t.id!==id) return t; const th=t.thread.slice(); if(th.length) th[th.length-1]={ ...th[th.length-1], ...p }; return { ...t, thread:th }; })); }

  function addTab(){ const t=newDocTab(); setTabs(ts=>[...ts,t]); setActiveId(t.id); return t.id; }
  function selectTab(id){ setActiveId(id); }
  function openNewTab(){
    setTabs(ts=>{
      const empty=ts.find(t=>t.phase==="empty" && !(t.docs&&t.docs.length));
      if(empty){ setTimeout(()=>setActiveId(empty.id),0); return ts; }
      const t=newDocTab(); setTimeout(()=>setActiveId(t.id),0); return [...ts,t];
    });
  }
  function closeTab(id){
    setTabs(ts=>{
      const idx=ts.findIndex(t=>t.id===id);
      const next=ts.filter(t=>t.id!==id);
      if(!next.length){ const t=newDocTab(); setTimeout(()=>setActiveId(t.id),0); return [t]; }
      if(id===activeId){ const pick=next[Math.max(0,idx-1)]; setTimeout(()=>setActiveId(pick.id),0); }
      return next;
    });
  }

  // Extract ONE file -> { doc } (server metadata + text) or { error } / { auth }.
  async function extractOne(file){
    let data; try{ data=await readBase64(file); }catch(e){ return { error:"Could not read “"+file.name+"”." }; }
    let res;
    try{ res=await fetch("/api/docintel",{ method:"POST", headers:authHeaders(),
      body: JSON.stringify({ action:"extract", name:file.name, mime:file.type, data }) }); }
    catch(e){ return { error:"Couldn't reach the server." }; }
    if(on401(res)) return { auth:true };
    let d=null; try{ d=await res.json(); }catch(e){ d=null; }
    if(!d) return { error: res.status===404
      ? "Upload endpoint not found (404) — the server needs a restart/redeploy."
      : "Unexpected response (HTTP "+res.status+")." };
    if(res.status===429) return { error:d.error||"Too many requests — wait a moment and retry." };
    if(d.ok && d.doc) return { doc:{ ...d.doc, text:d.text||"" } };
    return { error: d.note||d.error||"Could not read “"+file.name+"”." };
  }

  // Upload + extract one or more files into a tab (parent-owned so switching
  // tabs mid-upload never aborts it). Works for the first drop AND for adding
  // more documents to an already-loaded set; partial failures never wipe what's
  // already there — they surface as a non-fatal note.
  async function handleFiles(id, files){
    const incoming=Array.from(files||[]);
    if(!incoming.length) return;
    const ok=[], rejected=[];
    for(const f of incoming){
      if(!fileLooksOk(f)) rejected.push(f.name+" (unsupported type)");
      else if(f.size > MAX_MB*1024*1024) rejected.push(f.name+" (over "+MAX_MB+" MB)");
      else ok.push(f);
    }
    const tab=tabsRef.current.find(t=>t.id===id);
    const hadDocs=!!(tab && tab.docs && tab.docs.length);
    if(!ok.length){
      const note="Couldn't add: "+rejected.join(", ");
      if(hadDocs) patch(id,{ addNote:note }); else patch(id,{ phase:"error", errNote:note });
      return;
    }
    if(!hadDocs) patch(id,{ phase:"loading", errNote:"", addNote:"", title:shorten(ok[0].name) });
    patch(id,{ busy:true, addNote:"", up:{ done:0, total:ok.length } });   // progress for the % readout

    const errs=rejected.slice(); let authExpired=false;
    // Read all the files TOGETHER (in parallel) but surface each as it finishes —
    // every successful extract patches the doc in immediately, so results appear
    // progressively rather than waiting for the whole batch.
    await Promise.all(ok.map(async (f)=>{
      const r=await extractOne(f);
      patch(id, t=>({ up:{ done:((t.up&&t.up.done)||0)+1, total:(t.up&&t.up.total)||ok.length } }));
      if(r.auth){ authExpired=true; return; }
      if(r.doc) patch(id, t=>({ docs:[...(t.docs||[]), r.doc] }));
      else errs.push(r.error||("Could not read “"+f.name+"”."));
    }));
    patch(id, t=>{
      const docs=t.docs||[];
      if(!docs.length){
        return { phase:"error", busy:false, up:null,
          errNote: authExpired ? "Your session expired — please log in again." : (errs.join(" · ")||"Could not read the document(s).") };
      }
      return { phase:"loaded", busy:false, up:null, title:tabTitle(docs),
        addNote: authExpired ? "Your session expired — sign in again to add more." : (errs.length ? "Some files were skipped — "+errs.join(" · ") : "") };
    });
  }

  // Remove one document from a tab's set (reverts to the empty state if it was
  // the last one).
  function removeDoc(id, idx){
    patch(id, t=>{
      const docs=(t.docs||[]).filter((_,i)=>i!==idx);
      if(!docs.length) return { docs:[], phase:"empty", thread:[], title:"New document", addNote:"" };
      return { docs, title:tabTitle(docs), addNote:"" };
    });
  }

  // Ask / lens, streaming-first with JSON fallback (parent-owned).
  async function send(id, question, lens, modeArg){
    const tab=tabsRef.current.find(t=>t.id===id);
    if(!tab || tab.busy || !tab.docs || !tab.docs.length) return;
    const q=(question||"").trim();
    if(!q && !lens) return;
    const useMode=modeArg||tab.mode;
    const label=q||(LENS_LABEL[lens]||"Analysis");
    const payload={ docs: tab.docs.map(d=>({ name:d.name, text:d.text })), question:q,
      history: tab.thread.filter(m=>m.a).slice(-4).map(m=>({ q:m.q, a:m.a })),
      mode:useMode, lens:lens||null, side: tab.side||"",
      summary: useMode==="analysis" ? tab.docs.map(d=>d.summary).filter(Boolean).join(" ") : "" };
    patch(id, t=>({ busy:true, thread:[...t.thread, { q:label, a:"", cites:[], law:[], references:[], docFlags:[], mode:useMode, pending:true, streaming:true }] }));
    try{
      // Legal analysis answers are grounded in VERIFIED case-law retrieved first,
      // so they come back as one structured JSON (references + explanations) —
      // not a token stream. Document-only mode still streams.
      if(useMode==="analysis"){ await jsonAsk(id,payload); }
      else { const streamed=await tryStream(id,payload); if(!streamed) await jsonAsk(id,payload); }
    }catch(err){ patchThreadLast(id,{ a:"Couldn't reach the server. Check that it's running.", cites:[], law:[], pending:false, streaming:false, error:true }); }
    finally{ patch(id,{ busy:false }); }
  }
  async function tryStream(id,payload){
    let res;
    try{ res=await fetch("/api/docintel-stream",{ method:"POST", headers:authHeaders(), body: JSON.stringify(payload) }); }
    catch(e){ return false; }
    if(on401(res)) return false;             // expired -> let jsonAsk surface it
    if(!res.ok || !res.body) return false;
    const reader=res.body.getReader(); const dec=new TextDecoder(); let buf="";
    while(true){ const { value, done }=await reader.read(); if(done) break;
      buf+=dec.decode(value,{ stream:true }); patchThreadLast(id,{ a:splitMeta(buf).prose, pending:false, streaming:true }); }
    const { prose, meta }=splitMeta(buf);
    if(!prose && !meta) return false;
    patchThreadLast(id,{ a:prose||"(no answer)", cites:(meta&&Array.isArray(meta.cites))?meta.cites:[], law:(meta&&Array.isArray(meta.law))?meta.law:[], pending:false, streaming:false, error:false });
    return true;
  }
  async function jsonAsk(id,payload){
    const res=await fetch("/api/docintel",{ method:"POST", headers:authHeaders(), body: JSON.stringify({ action:"ask", ...payload }) });
    if(on401(res)){ patchThreadLast(id,{ a:"Your session expired — please log in again.", cites:[], law:[], pending:false, streaming:false, error:true }); return; }
    let d; try{ d=await res.json(); }catch(e){ d=null; }
    const a=(d&&d.ok&&d.answer) ? d.answer
      : ((d&&(d.note||d.error)) || (res.status===404 ? "Endpoint not found (404) — the server needs a restart/redeploy." : "Couldn't answer that — please try again."));
    patchThreadLast(id,{ a,
      cites:(d&&d.ok&&Array.isArray(d.cites))?d.cites:[],
      law:(d&&d.ok&&Array.isArray(d.law))?d.law:[],
      references:(d&&d.ok&&Array.isArray(d.references))?d.references:[],
      docFlags:(d&&d.ok&&Array.isArray(d.docFlags))?d.docFlags:[],
      note:(d&&d.ok&&d.note)?d.note:"",
      pending:false, streaming:false, error:!(d&&d.ok) });
  }
  // Find on-point Indian Kanoon precedents for the uploaded document(s). Sends
  // the combined summaries + a slice of text to /api/docintel (action:"precedents")
  // which reuses Ask's verified retrieval. Results are verified cases (good-law
  // graded) the user can open in the same drawer as cited authorities.
  async function findPrecedents(id){
    const tab=tabsRef.current.find(t=>t.id===id);
    if(!tab || tab.precBusy || !tab.docs || !tab.docs.length) return;
    const summary=tab.docs.map(d=>d.summary).filter(Boolean).join(" ");
    const text=tab.docs.map(d=>d.text).filter(Boolean).join("\n").slice(0,12000);
    patch(id,{ precBusy:true, precNote:"" });
    let res;
    try{ res=await fetch("/api/docintel",{ method:"POST", headers:authHeaders(),
      body: JSON.stringify({ action:"precedents", summary, text }) }); }
    catch(e){ patch(id,{ precBusy:false, precNote:"Couldn't reach the server." }); return; }
    if(on401(res)){ patch(id,{ precBusy:false, precNote:"Your session expired — please log in again." }); return; }
    let d=null; try{ d=await res.json(); }catch(e){ d=null; }
    if(d && d.ok){ patch(id,{ precBusy:false, precedents:(d.cases||[]), precIssue:(d.issue||""), precNote:(d.cases&&d.cases.length)?"":(d.note||"No on-point precedents found.") }); }
    else { patch(id,{ precBusy:false, precNote:(d&&(d.note||d.error)) || (res.status===404?"Endpoint not found (404) — the server needs a restart/redeploy.":"Couldn't find precedents — please try again.") }); }
  }
  function reAsk(id,q){ patch(id,{ mode:"analysis" }); try{ localStorage.setItem(MODE_KEY,"analysis"); }catch(e){} send(id,q,null,"analysis"); }
  function setTabMode(id,m){ patch(id,{ mode:m }); try{ localStorage.setItem(MODE_KEY,m); }catch(e){} }
  function setTabSide(id,s){ patch(id,{ side:s }); }

  // Open a cited authority in the same drawer the Ask page uses: resolve the
  // free-text citation into the full verified case object (/api/citation-case)
  // and show its details — good-law status, court, bench, cited-by, holding,
  // key passage, Open-on-Indian-Kanoon.
  const openCite=useCallback((c)=>{
    // A precedent card already carries the fully-resolved, verified case object
    // (data.case) — open it straight away, no citation-case round-trip.
    if(c && c.data && c.data.case){ setCite({ ...c, loading:false, error:"" }); return; }
    setCite({ ...c, loading:true, data:null, error:"" });
    fetch("/api/citation-case",{ method:"POST", headers:{ "Content-Type":"application/json" },
      body: JSON.stringify({ caseName:c.name||c.raw, citation:c.citation, year:c.year }) })
      .then(r=>r.json())
      .then(d=> setCite(prev=> prev&&prev.raw===c.raw ? { ...prev, loading:false, data:d } : prev))
      .catch(()=> setCite(prev=> prev&&prev.raw===c.raw ? { ...prev, loading:false, data:{ ok:false, error:"network" } } : prev));
  },[]);

  // Open a "law applied" chip in the side drawer: resolve the bare-act section
  // text from IndiaCode (central acts) plus lookup links.
  const openLaw=useCallback((label)=>{
    const { act, section, sectionNum } = parseLaw(label);
    setLaw({ label, act, section, loading:true, data:null });
    const payload = sectionNum ? { sections:[{ section:sectionNum, act }], acts:[] } : { sections:[], acts:[act] };
    fetch("/api/indiacode",{ method:"POST", headers:{ "Content-Type":"application/json" }, body: JSON.stringify(payload) })
      .then(r=>r.json())
      .then(d=> setLaw(prev=> prev&&prev.label===label ? { ...prev, loading:false, data:d } : prev))
      .catch(()=> setLaw(prev=> prev&&prev.label===label ? { ...prev, loading:false, data:null } : prev));
  },[]);

  const active=tabs.find(t=>t.id===activeId)||tabs[0]||null;
  return (
    <>
      <DocTabStrip tabs={tabs} activeId={active&&active.id} onSelect={selectTab} onClose={closeTab} onNew={addTab}/>
      {active && <DocWorkspace key={active.id} tab={active}
        onFiles={(fs)=>handleFiles(active.id,fs)}
        onRemoveDoc={(i)=>removeDoc(active.id,i)}
        onSend={(q,lens,mode)=>send(active.id,q,lens,mode)}
        onReAsk={(q)=>reAsk(active.id,q)}
        onCite={openCite}
        onLaw={openLaw}
        onPrecedents={()=>findPrecedents(active.id)}
        onSetMode={(m)=>setTabMode(active.id,m)}
        onSetSide={(s)=>setTabSide(active.id,s)}
        onNewDoc={openNewTab}/>}
      <CiteDrawer cite={cite} onClose={()=>setCite(null)}/>
      <LawDrawer law={law} onClose={()=>setLaw(null)}/>
    </>
  );
}


/* ---------- export: capture a document's full conversation ----------
   Builds a self-contained HTML document (metadata + summary + key facts +
   clauses + the entire Q&A thread, markdown rendered) and downloads it as a
   Word-compatible .doc, or opens a print view for "Save as PDF". No build step
   / external libraries needed. */
function escHtml(s){ return String(s==null?"":s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;"); }
function mdInlineHtml(text){
  let s=escHtml(text);
  s=s.replace(/\*\*([\s\S]+?)\*\*/g,"<strong>$1</strong>");
  s=s.replace(/`([^`]+)`/g,"<code>$1</code>");
  s=s.replace(/\*([^*\n]+?)\*/g,"<em>$1</em>");
  return s;
}
function mdToHtml(text){
  const lines=String(text==null?"":text).replace(/\r/g,"").split("\n");
  let html="", list=null, para=[];
  const flushP=()=>{ if(para.length){ html+="<p>"+para.map(mdInlineHtml).join("<br>")+"</p>"; para=[]; } };
  const flushL=()=>{ if(list){ html+="<"+list.t+">"+list.items.map(it=>"<li>"+mdInlineHtml(it)+"</li>").join("")+"</"+list.t+">"; list=null; } };
  for(const raw of lines){
    const t=raw.trim(); let m;
    if(!t){ flushP(); flushL(); continue; }
    if(m=t.match(/^(#{1,4})\s+(.*)$/)){ flushP(); flushL(); html+="<h4>"+mdInlineHtml(m[2])+"</h4>"; continue; }
    if(m=t.match(/^[-*•]\s+(.*)$/)){ flushP(); if(!list||list.t!=="ul"){ flushL(); list={t:"ul",items:[]}; } list.items.push(m[1]); continue; }
    if(m=t.match(/^\d+[.)]\s+(.*)$/)){ flushP(); if(!list||list.t!=="ol"){ flushL(); list={t:"ol",items:[]}; } list.items.push(m[1]); continue; }
    flushL(); para.push(t);
  }
  flushP(); flushL();
  return html;
}
const EXPORT_CSS = `
  *{box-sizing:border-box}
  body{font-family:'Hanken Grotesk',Calibri,Arial,sans-serif;color:#1a1a22;line-height:1.55;max-width:760px;margin:0 auto;padding:36px 30px;font-size:13px}
  .hd{display:flex;justify-content:space-between;align-items:baseline;border-bottom:2px solid #4a3aff;padding-bottom:10px;margin-bottom:18px}
  .brand{font-weight:700;color:#4a3aff;font-size:13px}
  .muted{color:#6b6b78;font-size:11px}
  h1{font-size:21px;margin:0 0 4px}
  h2{font-size:15px;margin:24px 0 8px;padding-bottom:4px;border-bottom:1px solid #e6e6ee}
  h4{font-size:13px;margin:12px 0 4px}
  .meta-line{margin-bottom:10px}
  .parties{margin:6px 0 12px}
  .party{display:inline-block;background:#f0f0f7;border:1px solid #e0e0ee;border-radius:20px;padding:2px 10px;font-size:11px;margin:0 5px 5px 0}
  .summary{background:#f7f7fc;border-left:3px solid #4a3aff;padding:10px 14px;border-radius:4px}
  table.facts{border-collapse:collapse;width:100%;margin-bottom:8px}
  table.facts td{border:1px solid #e6e6ee;padding:6px 10px;vertical-align:top}
  table.facts .fk{font-weight:600;width:32%;background:#fafaff}
  ul.clauses{padding-left:18px}
  .qa{margin:0 0 16px;padding:14px 16px;border:1px solid #e6e6ee;border-left:3px solid #4a3aff;border-radius:6px;page-break-inside:avoid}
  .lbl{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#4a3aff;margin:0 0 4px}
  .a-lbl{margin-top:13px}
  .qa .q{font-weight:600;font-size:14px;margin:0 0 2px}
  .badge{display:inline-block;font-size:9.5px;font-weight:600;color:#4a3aff;background:#eeebff;border-radius:20px;padding:1px 8px;margin-left:6px;letter-spacing:0;text-transform:none}
  .a{font-size:13px}
  .a p{margin:0 0 8px}.a h4{margin:10px 0 4px}.a ul,.a ol{margin:0 0 8px;padding-left:20px}
  .a code{background:#f0f0f7;padding:1px 4px;border-radius:3px;font-size:12px}
  .meta{font-size:11px;color:#555;margin-top:8px;padding-top:6px;border-top:1px dashed #e0e0ee}
  .foot{margin-top:26px;padding-top:12px;border-top:1px solid #e6e6ee;font-size:10.5px;color:#6b6b78}
`;
function buildExportHtml(tab){
  const docs=(tab.docs&&tab.docs.length)?tab.docs:[], thread=tab.thread||[];
  const multi=docs.length>1;
  // Each document: header (meta + parties), summary, key facts, clauses.
  const docSection=(doc,idx)=>{
    const meta=[ doc.type, doc.pages?doc.pages+" pages":null, doc.words?doc.words.toLocaleString()+" words":null, doc.date ].filter(Boolean).map(escHtml).join(" · ");
    const parties=(doc.parties||[]).map(p=>`<span class="party">${escHtml(p)}</span>`).join(" ");
    const facts=(doc.facts||[]).map(f=>`<tr><td class="fk">${escHtml(f.k)}</td><td>${escHtml(f.v)}</td></tr>`).join("");
    const clauses=(doc.clauses||[]).map(c=>`<li><b>${escHtml(c.id)}</b> ${escHtml(c.t)}${c.p?` <span class="muted">(p.${c.p})</span>`:""}</li>`).join("");
    return (multi?`<h2>Document ${idx+1} — ${escHtml(doc.name||"Document")}</h2>`:"")
      + (meta?`<div class="muted meta-line">${meta}</div>`:"")
      + (parties?`<div class="parties">${parties}</div>`:"")
      + (doc.summary?`<p class="summary">${escHtml(doc.summary)}</p>`:"")
      + (facts?`<h${multi?3:2}>Key facts</h${multi?3:2}><table class="facts"><tbody>${facts}</tbody></table>`:"")
      + (clauses?`<h${multi?3:2}>Clauses detected</h${multi?3:2}><ul class="clauses">${clauses}</ul>`:"");
  };
  const answered=thread.filter(m=>m.a);
  const qa=answered.map((m,i)=>{
    const modeLabel = m.mode==="analysis" ? "Legal analysis" : "Document-only";
    const law=(m.law&&m.law.length)?`<div class="meta"><b>Law applied:</b> ${m.law.map(escHtml).join("; ")}</div>`:"";
    const cites=(m.cites&&m.cites.length)?`<div class="meta"><b>Cited in document:</b> ${m.cites.map(c=>escHtml(c.c)+(c.p?` (p.${c.p})`:"")+(c.d?` — ${escHtml(c.d)}`:"")).join("; ")}</div>`:"";
    return `<div class="qa">`
      + `<div class="lbl">User Question ${i+1}</div>`
      + `<div class="q">${escHtml(m.q)}</div>`
      + `<div class="lbl a-lbl">Answer<span class="badge">${modeLabel}</span></div>`
      + `<div class="a">${mdToHtml(m.a)}</div>${law}${cites}`
      + `</div>`;
  }).join("");
  const now=new Date().toLocaleString();
  const title = multi ? docs.length+" documents" : ((docs[0]&&docs[0].name)||"Document");
  return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${escHtml(title)}</title><style>${EXPORT_CSS}</style></head><body>`
    + `<div class="hd"><div class="brand">Advokacy · Document Intelligence</div><div class="muted">Exported ${escHtml(now)}</div></div>`
    + `<h1>${escHtml(title)}</h1>`
    + (multi?`<div class="muted meta-line">${docs.map(d=>escHtml(d.name||"Document")).join(" · ")}</div>`:"")
    + docs.map(docSection).join("")
    + `<h2>Full conversation${answered.length?` — ${answered.length} ${answered.length===1?"exchange":"exchanges"}`:""}</h2>`
    + (qa||`<p class="muted">No questions have been asked about ${multi?"these documents":"this document"} yet.</p>`)
    + `<div class="foot">Advokacy is a research assistant, not legal advice. Legal-analysis answers apply general Indian law and may be wrong — verify every statute and citation against the official record before relying on them.</div>`
    + `</body></html>`;
}
function exportSafeName(tab){
  const first=(tab.docs&&tab.docs[0]&&tab.docs[0].name)||"document";
  const more=(tab.docs&&tab.docs.length>1)?` +${tab.docs.length-1}`:"";
  const n=first.replace(/\.[a-z0-9]+$/i,"").replace(/[^\w.\- ]+/g,"").trim().slice(0,60) || "document";
  return "Advokacy — " + n + more;
}
function exportWord(tab){
  const blob=new Blob(["﻿"+buildExportHtml(tab)], { type:"application/msword" });
  const url=URL.createObjectURL(blob);
  const a=document.createElement("a"); a.href=url; a.download=exportSafeName(tab)+".doc";
  document.body.appendChild(a); a.click();
  setTimeout(()=>{ document.body.removeChild(a); URL.revokeObjectURL(url); }, 0);
}
function exportPdf(tab){
  const w=window.open("", "_blank");
  if(!w){ if(window.toast) toast("Allow pop-ups to export as PDF"); return; }
  w.document.open(); w.document.write(buildExportHtml(tab)); w.document.close(); w.focus();
  setTimeout(()=>{ try{ w.print(); }catch(e){} }, 400);
}

window.DocsApp = DocsApp;
