/* MVG Trust Verifier — in-browser verification of site artifacts
 * - Verifies Trust Anchor Snapshot (DSSE) with pinned bootstrap key
 * - Verifies Release Signature (DSSE) and Artifacts Manifest (DSSE)
 * - Recomputes SHA-256 for each artifact (same-origin fetch)
 * - NEW (v26): Downloads Trust Pack and verifies DSSE + contents inventory (zip unzip + per-file hash)
 * - NEW (v27): Verifies Trust Pack embedded DSSE chain (snapshot→release→manifest→anchored report)
 *
 * No third-party calls.
 */

'use strict';
(function(){
  const TRUST_JSON = '/trust.json';

  // Pinned bootstrap public key (public-safe demo key).
  const BOOTSTRAP = {
    kid: 'ta.mvg.bootstrap.2026-02-09.v31.demo',
    jwk: {kty:'OKP', crv:'Ed25519', x:'nMgO1r8oSjUy_M3WWMgg8edj-PbPMjy3IZeMwnBRCF8'}
  };

  // Fallbacks (used only if trust.json is unavailable)
  const FALLBACK = {
    trust_snapshot_dsse: '/downloads/MVG_Verifier_Trust_Anchor_Registry_Snapshot_v31.dsse.json',
    release_sig_dsse: '/downloads/MVG_Release_Signature_v31.dsse.json',
    artifacts_manifest_dsse: '/downloads/MVG_Artifacts_Manifest_v31.dsse.json',
    trust_pack_zip: '/downloads/MVG_Trust_Pack_v31.zip',
    trust_pack_sig_dsse: '/downloads/MVG_Trust_Pack_v31.dsse.json'
  };

  const $ = (id)=>document.getElementById(id);

  // Primary UI
  const btn = $('btnTrustVerify');
  const panel = $('trustVerifyPanel');
  const status = $('trustVerifyStatus');
  const summary = $('trustVerifySummary');
  const rows = $('trustVerifyRows');

  // Trust pack contents UI (optional, collapsed by default)
  const tpWrap = $('trustPackContentsWrap');
  const tpSummary = $('trustPackContentsSummary');
  const tpRows = $('trustPackContentsRows');

  // Copy/paste summary UI
  const intakeWrap = $('trustVerifyIntakeWrap');
  const intake = $('trustVerifyIntake');
  const btnCopyIntake = $('btnCopyTrustIntake');

  // Evidence binder (SOC2/ISO intake)
  let btnPdf = $('btnDownloadEvidencePdf');
  let lastEvidence = null;
  function ensurePdfButton(){
    if(btnPdf) return btnPdf;
    if(!intakeWrap) return null;
    const mini = intakeWrap.querySelector('.mini') || intakeWrap.querySelector('div');
    if(!mini) return null;
    btnPdf = document.createElement('button');
    btnPdf.type='button';
    btnPdf.id='btnDownloadEvidencePdf';
    btnPdf.className='btn';
    btnPdf.textContent='Download evidence binder (PDF)';
    btnPdf.disabled = true;
    mini.appendChild(btnPdf);
    return btnPdf;
  }


  // Verifier signing key display
  const verifierKidEl = $('verifierSigningKid');
  const btnCopyVerifierKid = $('btnCopyVerifierKid');

  async function copyText(t){
    try{ await navigator.clipboard.writeText(t); }
    catch(e){
      const ta=document.createElement('textarea');
      ta.value=t; document.body.appendChild(ta); ta.select();
      document.execCommand('copy'); ta.remove();
    }
  }

  if(btnCopyVerifierKid && verifierKidEl){
    btnCopyVerifierKid.addEventListener('click', ()=>copyText(String(verifierKidEl.textContent||'')));
  }

  function b64uToBytes(s){
    s=String(s||'').replace(/-/g,'+').replace(/_/g,'/');
    const pad='='.repeat((4-(s.length%4))%4);
    const bin=atob(s+pad);
    const out=new Uint8Array(bin.length);
    for(let i=0;i<bin.length;i++) out[i]=bin.charCodeAt(i);
    return out;
  }
  function utf8(s){ return new TextEncoder().encode(String(s)); }

  // DSSE PAE (DSSEv1)
  function dssePAE(payloadType, payloadBytes){
    const pt=utf8(payloadType);
    const parts=[
      utf8('DSSEv1 '),
      utf8(String(pt.length)),
      utf8(' '),
      pt,
      utf8(' '),
      utf8(String(payloadBytes.length)),
      utf8(' '),
      payloadBytes
    ];
    let n=0; for(const p of parts) n+=p.length;
    const out=new Uint8Array(n);
    let o=0; for(const p of parts){ out.set(p,o); o+=p.length; }
    return out;
  }

  async function sha256Hex(bytes){
    const b=(bytes instanceof Uint8Array)?bytes:new Uint8Array(bytes);
    const ab=b.buffer.slice(b.byteOffset, b.byteOffset+b.byteLength);
    const dig=await crypto.subtle.digest('SHA-256', ab);
    const u8=new Uint8Array(dig);
    let out='';
    for(let i=0;i<u8.length;i++) out+=u8[i].toString(16).padStart(2,'0');
    return out;
  }

  
  function normSha256(s){
    s=String(s||'').trim();
    const m=s.match(/^[a-z0-9]+:(.+)$/i);
    return (m?m[1]:s).toLowerCase();
  }

// JCS-like canonical JSON (stable key order)
  function canon(v){
    if(v===null||typeof v!=='object') return v;
    if(Array.isArray(v)) return v.map(canon);
    const o={};
    Object.keys(v).sort().forEach(k=>o[k]=canon(v[k]));
    return o;
  }
  function jcsBytes(obj){
    return utf8(JSON.stringify(canon(obj)));
  }

  async function importEd25519Public(jwk){
    return crypto.subtle.importKey('jwk', jwk, {name:'Ed25519'}, false, ['verify']);
  }
  async function verifyEd25519(jwk, msg, sigB64u){
    const key = await importEd25519Public(jwk);
    const sig = b64uToBytes(sigB64u);
    return crypto.subtle.verify({name:'Ed25519'}, key, sig, msg);
  }

  function tagHtml(verdict){
    const v=String(verdict||'').toUpperCase();
    if(v==='PASS') return '<span class="tag ok">PASS</span>';
    if(v==='HOLD') return '<span class="tag hold">HOLD</span>';
    return '<span class="tag deny">FAIL</span>';
  }

  function setRow(path, okOrVerdict, detail){
    const verdict = (typeof okOrVerdict==='string')
      ? okOrVerdict.toUpperCase()
      : (okOrVerdict ? 'PASS' : 'FAIL');
    const tr=document.createElement('tr');
    tr.innerHTML =
      `<td style="font-family:var(--font-mono); font-size:12px">${path}</td>` +
      `<td>${tagHtml(verdict)}</td>` +
      `<td class="muted">${detail||''}</td>`;
    rows.appendChild(tr);
  }

  function clearTrustPackTable(){
    if(tpRows) tpRows.innerHTML='';
    if(tpSummary) tpSummary.textContent='';
    if(tpWrap) tpWrap.style.display='none';
  }

  function addTrustPackRow(file, verdict, detail){
    if(!tpRows) return;
    const tr=document.createElement('tr');
    tr.innerHTML =
      `<td style="font-family:var(--font-mono); font-size:12px">${file}</td>`+
      `<td>${tagHtml(verdict)}</td>`+
      `<td class="muted">${detail||''}</td>`;
    tpRows.appendChild(tr);
  }

  async function fetchJson(path){
    const r = await fetch(path, {cache:'no-store'});
    if(!r.ok) throw new Error(`FETCH_FAIL:${path}:${r.status}`);
    const txt = await r.text();
    return { obj: JSON.parse(txt), txt };
  }

  async function fetchBytes(path){
    const r = await fetch(path, {cache:'no-store'});
    if(!r.ok) throw new Error(`FETCH_FAIL:${path}:${r.status}`);
    return new Uint8Array(await r.arrayBuffer());
  }

  async function verifyDsse(env, expectedPayloadType, pubKeysByKid){
    if(env.payloadType !== expectedPayloadType) throw new Error('PAYLOAD_TYPE_MISMATCH');
    const payloadBytes = b64uToBytes(env.payload);
    const pae = dssePAE(env.payloadType, payloadBytes);
    let okCount=0;
    for(const sig of (env.signatures||[])){
      const jwk = pubKeysByKid[sig.keyid];
      if(!jwk) continue;
      const ok = await verifyEd25519(jwk, pae, sig.sig);
      if(ok) okCount++;
    }
    return { okCount, payloadBytes };
  }

  // ===== ZIP parsing utilities (no external deps) =====

  function readU16(u8, o){ return u8[o] | (u8[o+1]<<8); }
  function readU32(u8, o){ return (u8[o] | (u8[o+1]<<8) | (u8[o+2]<<16) | (u8[o+3]<<24))>>>0; }

  function findEOCD(u8){
    // EOCD signature 0x06054b50
    for(let i=u8.length-22; i>=0 && i>=u8.length-66000; i--){
      if(u8[i]===0x50 && u8[i+1]===0x4b && u8[i+2]===0x05 && u8[i+3]===0x06) return i;
    }
    throw new Error('ZIP_EOCD_NOT_FOUND');
  }

  function parseCentralDirectory(u8, eocdOff){
    const cdSize = readU32(u8, eocdOff+12);
    const cdOff  = readU32(u8, eocdOff+16);
    let o = cdOff;
    const end = cdOff + cdSize;
    const out=[];
    while(o < end){
      // central dir signature 0x02014b50
      if(!(u8[o]===0x50 && u8[o+1]===0x4b && u8[o+2]===0x01 && u8[o+3]===0x02)) throw new Error('ZIP_CDIR_BAD_SIG');
      const method = readU16(u8, o+10);
      const compSize = readU32(u8, o+20);
      const uncompSize = readU32(u8, o+24);
      const nameLen = readU16(u8, o+28);
      const extraLen = readU16(u8, o+30);
      const commentLen = readU16(u8, o+32);
      const lfhOff = readU32(u8, o+42);
      const nameBytes = u8.slice(o+46, o+46+nameLen);
      const name = new TextDecoder().decode(nameBytes);
      out.push({ name, method, compSize, uncompSize, lfhOff });
      o = o + 46 + nameLen + extraLen + commentLen;
    }
    return out;
  }

  function getLocalDataSlice(u8, entry){
    const o = entry.lfhOff;
    // local file header signature 0x04034b50
    if(!(u8[o]===0x50 && u8[o+1]===0x4b && u8[o+2]===0x03 && u8[o+3]===0x04)) throw new Error('ZIP_LFH_BAD_SIG');
    const nameLen = readU16(u8, o+26);
    const extraLen = readU16(u8, o+28);
    const dataOff = o + 30 + nameLen + extraLen;
    return u8.slice(dataOff, dataOff + entry.compSize);
  }

  async function inflateDeflate(compBytes){
    if(typeof DecompressionStream==='undefined') throw new Error('DECOMPRESSION_UNAVAILABLE');
    const ds = new DecompressionStream('deflate');
    const stream = new Blob([compBytes]).stream().pipeThrough(ds);
    const buf = await new Response(stream).arrayBuffer();
    return new Uint8Array(buf);
  }

  async function extractFileBytes(zipBytes, entry){
    const comp = getLocalDataSlice(zipBytes, entry);
    if(entry.method===0) return comp;
    if(entry.method===8) return await inflateDeflate(comp);
    throw new Error('ZIP_UNSUPPORTED_METHOD_'+String(entry.method));
  }

  async function verifyTrustPackContents(zipBytes){
    // Verify every declared file against trust_pack_manifest.json.
    if(!tpWrap) throw new Error('UI_MISSING');
    clearTrustPackTable();
    const eocd=findEOCD(zipBytes);
    const entries=parseCentralDirectory(zipBytes, eocd);
    const byName={};
    for(const e of entries){ byName[e.name]=e; }
    if(!byName['trust_pack_manifest.json']) throw new Error('TRUST_PACK_MANIFEST_MISSING');

    // Parse manifest JSON
    const manifestBytes = await extractFileBytes(zipBytes, byName['trust_pack_manifest.json']);
    let man;
    try{ man = JSON.parse(new TextDecoder().decode(manifestBytes)); }
    catch(e){ throw new Error('TRUST_PACK_MANIFEST_INVALID_JSON'); }

    const list = Array.isArray(man.entries) ? man.entries : [];
    if(list.length===0) throw new Error('TRUST_PACK_MANIFEST_EMPTY');

    // Detect extras (actual files not declared), ignoring dirs and the manifest itself.
    const declared = new Set(list.map(x=>x.path));
    const actualFiles = entries
      .map(e=>e.name)
      .filter(n=>n && !n.endsWith('/') && n !== 'trust_pack_manifest.json');
    const extras = actualFiles.filter(n=>!declared.has(n));

    let okCount=0;
    let badCount=0;

    // Stable order: as declared.
    for(const item of list){
      const path=item.path;
      const expSha=String(item.sha256||'').toLowerCase();
      const expBytes=Number(item.bytes||0);
      const ent=byName[path];
      if(!ent){
        addTrustPackRow(path, 'FAIL', 'missing');
        badCount++; continue;
      }
      const fb=await extractFileBytes(zipBytes, ent);
      const gotSha=(await sha256Hex(fb)).toLowerCase();
      const gotBytes=fb.byteLength;
      const ok=(gotSha===expSha && gotBytes===expBytes);
      const detail = ok
        ? `sha256:${gotSha.slice(0,16)}… • ${gotBytes} bytes`
        : `expected ${expSha.slice(0,16)}…/${expBytes} • got ${gotSha.slice(0,16)}…/${gotBytes}`;
      addTrustPackRow(path, ok ? 'PASS' : 'FAIL', detail);
      if(ok) okCount++; else badCount++;
    }

    for(const x of extras){
      addTrustPackRow(x, 'FAIL', 'extra (not declared)');
      badCount++;
    }

    const verdict = (badCount===0) ? 'PASS' : 'FAIL';
    const detail = (badCount===0)
      ? `${okCount}/${list.length} matched • extras 0`
      : `${badCount} issue(s) • ${okCount}/${list.length} matched • extras ${extras.length}`;

    if(tpWrap) tpWrap.style.display='';
    if(tpSummary) tpSummary.textContent = `${verdict} • ${detail}`;
    return { verdict, detail, okCount, listed:list.length, extras:extras.length };
  }

  
  async function verifyTrustPackInternalDsseChain(zipBytes){
    // Verify DSSE envelopes that are bundled INSIDE the Trust Pack (signatures/* and reports/*).
    // Self-contained chain: bootstrap -> snapshot -> derived keys -> release+manifest -> anchored report.
    const eocd=findEOCD(zipBytes);
    const entries=parseCentralDirectory(zipBytes, eocd);
    const byName={};
    for(const e of entries){ byName[e.name]=e; }

    function pick(re){
      for(const e of entries){
        if(re.test(e.name)) return e.name;
      }
      return null;
    }

    const snapDsse = pick(/^signatures\/MVG_Verifier_Trust_Anchor_Registry_Snapshot_.*\.dsse\.json$/);
    const snapJson = pick(/^signatures\/MVG_Verifier_Trust_Anchor_Registry_Snapshot_.*\.json$/);
    const relDsse  = pick(/^signatures\/MVG_Release_Signature_.*\.dsse\.json$/);
    const manDsse  = pick(/^signatures\/MVG_Artifacts_Manifest_.*\.dsse\.json$/);
    const anchored = pick(/^reports\/MVG_Anchored_Signed_Report_Demo_.*\.dsse\.json$/);

    const missing=[];
    if(!snapDsse) missing.push('snapshot.dsse');
    if(!relDsse) missing.push('release.dsse');
    if(!manDsse) missing.push('manifest.dsse');
    if(!anchored) missing.push('anchored_report.dsse');
    if(missing.length){
      return { verdict:'FAIL', detail:`missing ${missing.join(', ')}` };
    }

    async function readJsonFile(name){
      const b = await extractFileBytes(zipBytes, byName[name]);
      const obj = JSON.parse(new TextDecoder().decode(b));
      return { bytes:b, obj };
    }

    // 1) Snapshot DSSE (bootstrap + witness)
    const snap = await readJsonFile(snapDsse);
    const pub0 = { [BOOTSTRAP.kid]: BOOTSTRAP.jwk };
    const r0 = await verifyDsse(snap.obj, 'application/vnd.mvg.trust_anchors.snapshot+json', pub0);
    if(r0.okCount < 1) return { verdict:'FAIL', detail:'snapshot bootstrap signature invalid' };
    const snapPayload = JSON.parse(new TextDecoder().decode(r0.payloadBytes));

    // snapshot_id check (JCS over snapshot without id)
    const snapNoId = Object.assign({}, snapPayload);
    delete snapNoId.registry_snapshot_id;
    const dig = await sha256Hex(jcsBytes(snapNoId));
    const expectId = `nvrs:sha256:${dig}`;
    if(snapPayload.registry_snapshot_id !== expectId) return { verdict:'FAIL', detail:'snapshot_id mismatch' };

    // Optional: snapshot JSON file should semantically match DSSE payload
    if(snapJson && byName[snapJson]){
      try{
        const snapJsonBytes = await extractFileBytes(zipBytes, byName[snapJson]);
        const snapJsonObj = JSON.parse(new TextDecoder().decode(snapJsonBytes));
        const h1 = await sha256Hex(jcsBytes(snapJsonObj));
        const h2 = await sha256Hex(jcsBytes(snapPayload));
        if(h1 !== h2) return { verdict:'FAIL', detail:'snapshot JSON != DSSE payload' };
      } catch(e){
        return { verdict:'FAIL', detail:'snapshot JSON parse mismatch' };
      }
    }

    // Build trust anchor key maps
    const keys = {};
    const witnessKeys = {};
    let verifierKid = '';
    for(const e of (snapPayload.entries||[])){
      if(e?.key_material?.kty==='OKP' && e?.key_material?.crv==='Ed25519' && e?.key_material?.x){
        keys[e.trust_anchor_id] = {kty:'OKP', crv:'Ed25519', x:e.key_material.x};
        if((e.purposes||[]).includes('registry.witness_signature')){
          witnessKeys[e.trust_anchor_id] = keys[e.trust_anchor_id];
        }
        if((e.purposes||[]).includes('verifier.report_signing')){
          verifierKid = e.trust_anchor_id;
        }
      }
    }

    const wth = (snap.obj?.ext?.witness_threshold ?? snapPayload.witness_threshold ?? 2);
    const r0w = await verifyDsse(snap.obj, 'application/vnd.mvg.trust_anchors.snapshot+json', witnessKeys);
    if(r0w.okCount < wth) return { verdict:'FAIL', detail:`snapshot witness ${r0w.okCount}/${wth}` };

    // 2) Manifest DSSE
    const man = await readJsonFile(manDsse);
    const r2 = await verifyDsse(man.obj, 'application/vnd.mvg.artifacts_manifest+json', keys);
    if(r2.okCount < 1) return { verdict:'FAIL', detail:'manifest signature invalid' };

    // 3) Release DSSE (binds to manifest DSSE bytes)
    const rel = await readJsonFile(relDsse);
    const r1 = await verifyDsse(rel.obj, 'application/vnd.mvg.release_signature+json', keys);
    if(r1.okCount < 1) return { verdict:'FAIL', detail:'release signature invalid' };
    const relPayload = JSON.parse(new TextDecoder().decode(r1.payloadBytes));
    const manDsseSha = await sha256Hex(man.bytes);
    if(relPayload.artifacts_manifest_dsse_sha256 && relPayload.artifacts_manifest_dsse_sha256 !== manDsseSha){
      return { verdict:'FAIL', detail:'release binding mismatch' };
    }

    // 4) Anchored report DSSE (must verify under verifier.report_signing key)
    const ar = await readJsonFile(anchored);
    const r3 = await verifyDsse(ar.obj, ar.obj.payloadType, keys);
    if(r3.okCount < 1) return { verdict:'FAIL', detail:'anchored report signature invalid' };
    const reportKid = (ar.obj.signatures||[])[0]?.keyid || '';
    if(!reportKid || !keys[reportKid]) return { verdict:'FAIL', detail:'report key not in snapshot' };
    if(verifierKid && reportKid !== verifierKid) return { verdict:'FAIL', detail:'report signed by unexpected key' };

    return { verdict:'PASS', detail:'snapshot → release → manifest → report verified' };
  }

async function verifyTrustPackOnline(trustPackZipUrl, trustPackDsseUrl, keys, witnessKeys){
    // Download + verify DSSE signature + digest binding + unzip contents manifest.
    clearTrustPackTable();

    const tpEnv = (await fetchJson(trustPackDsseUrl)).obj;
    const zipBytes = await fetchBytes(trustPackZipUrl);

    const rAll = await verifyDsse(tpEnv, 'application/vnd.mvg.trust_pack_signature+json', keys);
    if(rAll.okCount < 1) throw new Error('TRUST_PACK_SIG_INVALID');

    const rW = await verifyDsse(tpEnv, 'application/vnd.mvg.trust_pack_signature+json', witnessKeys);
    const wth = (tpEnv?.ext?.witness_threshold ?? 2);
    if(rW.okCount < wth) throw new Error('TRUST_PACK_WITNESS_THRESHOLD');

    const payload = JSON.parse(new TextDecoder().decode(rAll.payloadBytes));
    const sha = await sha256Hex(zipBytes);
    const bytes = zipBytes.byteLength;

    if(payload.trust_pack_sha256 && payload.trust_pack_sha256 !== sha) throw new Error('TRUST_PACK_ZIP_SHA_MISMATCH');
    if(payload.trust_pack_bytes && payload.trust_pack_bytes !== bytes) throw new Error('TRUST_PACK_ZIP_BYTES_MISMATCH');

    // Deep verify (fail-closed if decompression not available)
    let contents = { verdict:'HOLD', detail:'—', okCount:0, listed:0, extras:0 };
    try{
      contents = await verifyTrustPackContents(zipBytes);
    } catch(e){
      const msg = String(e?.message||e);
      // HOLD is the safe state for "could not validate contents"
      contents = { verdict:'HOLD', detail:msg, okCount:0, listed:0, extras:0 };
      if(tpWrap) tpWrap.style.display='';
      if(tpSummary) tpSummary.textContent = `HOLD • ${msg}`;
    }

    // Internal DSSE chain verification (signatures/* + reports/*)
    let chain = { verdict:'HOLD', detail:'—' };
    try{
      chain = await verifyTrustPackInternalDsseChain(zipBytes);
    } catch(e){
      const msg = String(e?.message||e);
      chain = { verdict:'HOLD', detail:msg };
    }

    // Overall (fail-closed): FAIL dominates, otherwise HOLD if any component is HOLD
    let overall = (contents.verdict==='PASS') ? 'PASS' : (contents.verdict==='FAIL') ? 'FAIL' : 'HOLD';
    if(chain.verdict==='FAIL') overall = 'FAIL';
    if(chain.verdict==='HOLD' && overall!=='FAIL') overall = 'HOLD';

    // Update the collapsed Trust Pack header summary (keep it compact)
    try{
      if(tpWrap) tpWrap.style.display='';
      if(tpSummary){
        const c = (contents.verdict==='PASS')
          ? `${contents.okCount}/${contents.listed} matched`
          : `${contents.verdict} (${contents.detail||'—'})`;
        const ch = (chain.verdict==='PASS')
          ? 'PASS'
          : `${chain.verdict} (${chain.detail||'—'})`;
        tpSummary.textContent = `${overall} • contents ${c} • internal DSSE ${ch}`;
      }
    }catch(e){}

    const short = (overall==='PASS')
      ? `DSSE sigs ${rAll.okCount} • witness ${rW.okCount}/${wth} • contents ${contents.okCount}/${contents.listed} • internal DSSE PASS`
      : (overall==='HOLD')
        ? `DSSE sigs ${rAll.okCount} • witness ${rW.okCount}/${wth} • contents ${contents.verdict} • internal DSSE ${chain.verdict}`
        : (chain.verdict==='FAIL')
          ? `DSSE ok • internal DSSE FAIL (${chain.detail})`
          : `DSSE ok • contents FAIL (${contents.detail})`;

    return {
      verdict: overall,
      sha,
      bytes,
      sigOk: rAll.okCount,
      witnessOk: rW.okCount,
      witnessThreshold: wth,
      contents,
      chain,
      short
    };
  }

  async function run(){
    panel.style.display='';
    rows.innerHTML='';
    clearTrustPackTable();

    status.className='tag neutral';
    status.textContent='RUNNING';
    summary.textContent='';

    if(!crypto?.subtle?.importKey){
      status.className='tag hold';
      status.textContent='HOLD';
      summary.textContent='Crypto unavailable (Ed25519 WebCrypto)';
      return;
    }

    // Resolve trust artifact URLs (single source of truth: trust.json)
    let trust = null;
    try{ trust = (await fetchJson(TRUST_JSON)).obj; } catch(e){ trust = null; }
    const taHref = trust?.trust_artifacts?.trust_anchor_registry_snapshot_href || FALLBACK.trust_snapshot_dsse;
    const relHref = trust?.trust_artifacts?.release_signature_href || FALLBACK.release_sig_dsse;
    const manHref = trust?.trust_artifacts?.artifacts_manifest_href || FALLBACK.artifacts_manifest_dsse;
    const tpZipHref = trust?.trust_artifacts?.trust_pack_href || FALLBACK.trust_pack_zip;
    const tpSigHref = trust?.trust_artifacts?.trust_pack_signature_href || FALLBACK.trust_pack_sig_dsse;

    // 1) Trust Anchor Snapshot (DSSE)
    const {obj: taEnv} = await fetchJson(taHref);
    const pub0 = { [BOOTSTRAP.kid]: BOOTSTRAP.jwk };
    const r0 = await verifyDsse(taEnv, 'application/vnd.mvg.trust_anchors.snapshot+json', pub0);
    if(r0.okCount < 1) throw new Error('TRUST_SNAPSHOT_SIG_INVALID');

    const ta = JSON.parse(new TextDecoder().decode(r0.payloadBytes));

    // snapshot_id check
    const taNoId = Object.assign({}, ta); delete taNoId.registry_snapshot_id;
    const digest = await sha256Hex(jcsBytes(taNoId));
    const expectId = `nvrs:sha256:${digest}`;
    if(ta.registry_snapshot_id !== expectId) throw new Error('TRUST_SNAPSHOT_ID_MISMATCH');

    // Build key map from snapshot
    const keys={};
    const witnessKeys={};
    let verifierKid='—';
    let sbomKid='—';
    for(const e of (ta.entries||[])){
      if(e?.key_material?.kty==='OKP' && e?.key_material?.crv==='Ed25519' && e?.key_material?.x){
        keys[e.trust_anchor_id]={kty:'OKP', crv:'Ed25519', x:e.key_material.x};
        if((e.purposes||[]).includes('registry.witness_signature')) witnessKeys[e.trust_anchor_id]=keys[e.trust_anchor_id];
        if((e.purposes||[]).includes('verifier.report_signing')) verifierKid = e.trust_anchor_id;
        if((e.purposes||[]).includes('sbom.signing')) sbomKid = e.trust_anchor_id;
      }
    }

    // Update displayed verifier signing key to match the snapshot (removes contradictions)
    if(verifierKidEl && verifierKid && verifierKid !== '—') verifierKidEl.textContent = verifierKid;

    // Verify witness threshold on snapshot (>=2)
    const r0w = await verifyDsse(taEnv, 'application/vnd.mvg.trust_anchors.snapshot+json', Object.assign({}, witnessKeys));
    const wth = (ta.witness_threshold||2);
    if(r0w.okCount < wth) throw new Error('TRUST_WITNESS_THRESHOLD');
    setRow('Trust Anchor Snapshot (DSSE)', 'PASS', `bootstrap + ${r0w.okCount}/${wth} witness`);

    // 2) Release signature (DSSE)
    const {obj: relEnv, txt: relEnvTxt} = await fetchJson(relHref);
    const r1 = await verifyDsse(relEnv, 'application/vnd.mvg.release_signature+json', keys);
    if(r1.okCount < 1) throw new Error('RELEASE_SIG_INVALID');
    setRow('Release Signature (DSSE)', 'PASS', `signatures: ${r1.okCount}`);
    const relDsseBytes = new TextEncoder().encode(relEnvTxt||'');
    const relDsseSha = relDsseBytes.byteLength ? await sha256Hex(relDsseBytes) : '';
    const rel = JSON.parse(new TextDecoder().decode(r1.payloadBytes));

    // 3) Artifacts manifest (DSSE)
    const {obj: manEnv, txt: manEnvTxt} = await fetchJson(manHref);
    const r2 = await verifyDsse(manEnv, 'application/vnd.mvg.artifacts_manifest+json', keys);
    if(r2.okCount < 1) throw new Error('MANIFEST_SIG_INVALID');
    setRow('Artifacts Manifest (DSSE)', 'PASS', `signatures: ${r2.okCount}`);

    // release binding: compare sha256 of manifest DSSE bytes to rel.artifacts_manifest_dsse_sha256
    const manDsseBytes = new TextEncoder().encode(manEnvTxt);
    const manDsseSha = await sha256Hex(manDsseBytes);
    if(rel.artifacts_manifest_dsse_sha256 && rel.artifacts_manifest_dsse_sha256 !== manDsseSha) throw new Error('RELEASE_BINDING_MISMATCH');

    const man = JSON.parse(new TextDecoder().decode(r2.payloadBytes));
    const artifacts = man.artifacts || [];

    // 4) Recompute hashes for each artifact (same-origin fetch)
    const CORE_VERIFIER = new Set([
      '/assets/verify/verify-page.js',
      '/assets/verify/verify-worker.js'
    ]);
    const coreTotal = CORE_VERIFIER.size;
    let coreSeen = 0;
    let coreOk = 0;

    let bad=0;

    // SBOM (standalone DSSE) capture (from artifacts manifest)
    let sbomBytes=null, sbomSha='', sbomPath='';
    let sbomDsseObj=null, sbomDsseSha='', sbomDssePath='';
    let sbomStandalone = { verdict:'—', kid: (sbomKid && sbomKid!=='—') ? sbomKid : '—', sbom_sha256:'', sbom_dsse_sha256:'', detail:'' };

    for(const a of artifacts){
      const path=a.path;
      try{
        const res = await fetch(path, {cache:'no-store'});
        if(!res.ok) throw new Error('fetch');
        const buf = new Uint8Array(await res.arrayBuffer());
        const h = await sha256Hex(buf);
        const ok = (h === normSha256(a.sha256));

        // Capture SBOM + SBOM DSSE artifacts for standalone verification
        if(/^\/downloads\/MVG_ProofPlayground_SBOM_.*\.cdx\.json$/.test(path)){
          sbomBytes = buf; sbomSha = h; sbomPath = path;
        }
        if(/^\/downloads\/MVG_ProofPlayground_SBOM_.*\.dsse\.json$/.test(path)){
          sbomDsseSha = h; sbomDssePath = path;
          try{ sbomDsseObj = JSON.parse(new TextDecoder().decode(buf)); }catch(e){}
        }


        if(CORE_VERIFIER.has(path)){
          coreSeen++;
          if(ok) coreOk++;
        }

        setRow(path, ok ? 'PASS' : 'FAIL', ok ? `sha256:${h.slice(0,12)}…` : `expected:${normSha256(a.sha256).slice(0,12)}… got:${h.slice(0,12)}…`);
        if(!ok) bad++;
      } catch(e){
        setRow(path, 'FAIL', 'fetch/hash failed');
        if(CORE_VERIFIER.has(path)) coreSeen++;
        bad++;
      }
    }

    // One-line proof: verifier code (JS/worker) is locked by the manifest.
    let verifierLockVerdict = 'PASS';
    let verifierLockDetail = 'verify-page.js + verify-worker.js match artifacts manifest';
    if(coreSeen < coreTotal){
      verifierLockVerdict = 'HOLD';
      verifierLockDetail = `manifest does not assert ${coreTotal-coreSeen} core file(s)`;
    } else if(coreOk < coreTotal){
      verifierLockVerdict = 'FAIL';
      verifierLockDetail = `${coreOk}/${coreTotal} matched`;
      bad++; // fail-closed: core verifier mismatch counts as a hard failure signal
    }
    setRow('Verifier code locked (JS/worker)', verifierLockVerdict, verifierLockDetail);

    // SBOM (DSSE standalone) — verifies signature + payload-byte identity (SBOM file == DSSE payload)
    try{
      sbomStandalone.sbom_sha256 = sbomSha || '';
      sbomStandalone.sbom_dsse_sha256 = sbomDsseSha || '';

      if(!sbomBytes || !sbomDsseObj) throw new Error('SBOM_FILES_MISSING');
      if(!sbomKid || sbomKid==='—' || !keys[sbomKid]) throw new Error('SBOM_SIGNING_KEY_MISSING');

      const rS = await verifyDsse(sbomDsseObj, 'application/vnd.cyclonedx+json', { [sbomKid]: keys[sbomKid] });
      if(rS.okCount < 1) throw new Error('SBOM_DSSE_SIG_INVALID');

      const p = rS.payloadBytes;
      let same = (p.length === sbomBytes.length);
      if(same){
        for(let i=0;i<p.length;i++){
          if(p[i] !== sbomBytes[i]){ same=false; break; }
        }
      }
      if(!same) throw new Error('SBOM_DSSE_PAYLOAD_MISMATCH');

      sbomStandalone.verdict = 'PASS';
      sbomStandalone.detail = `sig ok • kid ${sbomKid} • sha256:${(sbomSha||'').slice(0,12)}…`;
    } catch(e){
      const msg = String(e?.message||e);
      const hold = (e && (e.name==='NotSupportedError' || /not supported/i.test(msg) || /Ed25519/i.test(msg)));
      sbomStandalone.verdict = hold ? 'HOLD' : 'FAIL';
      sbomStandalone.detail = hold ? 'ALG_UNSUPPORTED' : msg;
    }
    setRow('SBOM (DSSE standalone)', sbomStandalone.verdict, sbomStandalone.detail);


    // 5) Trust Pack (download + verify DSSE + verify contents inventory)
    let tpRes = null;
    try{
      tpRes = await verifyTrustPackOnline(tpZipHref, tpSigHref, keys, witnessKeys);
      setRow('Trust Pack (DSSE + contents + internal DSSE)', tpRes.verdict, tpRes.short);
    } catch(e){
      const msg = String(e?.message||e);
      tpRes = { verdict:'FAIL', short: msg, contents:{verdict:'FAIL', okCount:0, listed:0, extras:0} };
      setRow('Trust Pack (DSSE + contents + internal DSSE)', 'FAIL', msg);
    }

    // Overall verdict (fail-closed)
    let overall = (bad===0) ? 'PASS' : 'FAIL';
    if(tpRes?.verdict==='FAIL') overall='FAIL';
    if(tpRes?.verdict==='HOLD' && overall!=='FAIL') overall='HOLD';
    if(typeof verifierLockVerdict==='string' && verifierLockVerdict.toUpperCase()==='HOLD' && overall!=='FAIL') overall='HOLD';
    if(sbomStandalone?.verdict==='FAIL') overall='FAIL';
    if(sbomStandalone?.verdict==='HOLD' && overall!=='FAIL') overall='HOLD';

    if(overall==='PASS'){
      status.className='tag ok';
      status.textContent='PASS';
      summary.textContent='All checks passed (artifacts + Trust Pack + chain).';
    } else if(overall==='HOLD'){
      status.className='tag hold';
      status.textContent='HOLD';
      summary.textContent='Verified anchored artifacts; remaining checks unavailable (fail-closed).';
    } else {
      status.className='tag deny';
      status.textContent='FAIL';
      summary.textContent=(bad>0) ? `${bad} mismatches.` : (tpRes?.short||'Verification failed.');
    }

    // 5-line Security intake summary (copy/paste)

    try{
      const rid = (rel?.release_id) ? rel.release_id : (trust?.release_id||'—');
      const sid = ta?.registry_snapshot_id || '—';
      const sidShort = (sid && sid.startsWith('nvrs:sha256:')) ? ('nvrs:sha256:' + sid.slice(11+8, 11+8+16) + '…') : sid;
      const ms = (rel?.artifacts_manifest_dsse_sha256) ? rel.artifacts_manifest_dsse_sha256 : '';
      const msShort = ms ? (ms.slice(0,16)+'…') : '—';
      const ac = Array.isArray(artifacts) ? artifacts.length : 0;

      const tpV = tpRes?.verdict || '—';
      const tpC = tpRes?.contents || {okCount:0, listed:0, extras:0, verdict:'—'};
      const tpCtxt =
        (tpC.verdict==='PASS') ? `${tpC.okCount}/${tpC.listed} matched`
        : (tpC.verdict==='FAIL') ? `FAIL (${tpC.detail||'mismatch'})`
        : `HOLD (${tpC.detail||'unavailable'})`;

      const tpCh = tpRes?.chain || { verdict:'—' };
      const tpChTxt = (tpCh.verdict==='PASS') ? 'PASS'
        : (tpCh.verdict==='FAIL') ? 'FAIL'
        : (tpCh.verdict==='HOLD') ? 'HOLD'
        : '—';

      const lines = [
        `MVG Trust: ${overall} — ${rid}`,
        `Anchors: ${sidShort} — witness ${r0w.okCount}/${wth} — verifier ${verifierKid||'—'}`,
        `Release: sigs ${r1.okCount} • manifest ${msShort}`,
        `Artifacts: ${ac} checked • mismatches ${bad} • verifier ${verifierLockVerdict} (${coreOk}/${coreTotal}) • SBOM DSSE ${sbomStandalone.verdict}`,
        `Trust Pack: ${tpV} • ${tpCtxt} • internal DSSE ${tpChTxt} • /verify-trust • /verify`
      ].join('\n');

      if(intakeWrap && intake){ intakeWrap.style.display=''; intake.textContent=lines; }
      if(btnCopyIntake){ btnCopyIntake.onclick=()=>copyText(lines); }
      // Evidence binder PDF (generated in-browser; private key 0)
      try{
        const b = ensurePdfButton();
        if(b){
          lastEvidence = {
            generated_utc: (new Date()).toISOString(),
            overall,
            release_id: rid,
            snapshot_id: sid,
            verifier_signing_kid: verifierKid||'—',
            witness: { ok: r0w?.okCount||0, threshold: wth||2 },
            mismatches: bad,
            verifier_lock: { verdict: verifierLockVerdict, ok: coreOk, total: coreTotal },
            hashes: {
              artifacts_manifest_dsse_sha256: rel?.artifacts_manifest_dsse_sha256 || manDsseSha || '',
              release_signature_dsse_sha256: relDsseSha || ''
            },
            trust_pack: {
              verdict: tpRes?.verdict || '—',
              sha256: tpRes?.sha || '',
              bytes: tpRes?.bytes || 0,
              witness: { ok: tpRes?.witnessOk || 0, threshold: tpRes?.witnessThreshold || 2 }
            },
            sbom: {
              verdict: sbomStandalone?.verdict || '—',
              signing_kid: sbomStandalone?.kid || '—',
              sbom_sha256: sbomStandalone?.sbom_sha256 || sbomSha || '',
              sbom_dsse_sha256: sbomStandalone?.sbom_dsse_sha256 || sbomDsseSha || ''
            },
            intake_lines: lines.split('\n')
          };

          b.disabled = false;
          b.onclick = ()=>{
            try{
              const pdf = buildEvidenceBinderPdf(lastEvidence);
              const safeRid = String(rid||'release').replace(/[^a-zA-Z0-9._-]+/g,'_');
              const fname = `MVG_Evidence_Binder_${safeRid}.pdf`;
              downloadBytes(fname, pdf, 'application/pdf');
            } catch(e){
              alert('Evidence PDF generation failed: ' + (e?.message||e));
            }
          };
        }
      } catch(e){}

    } catch(e){}
  }

  

  function downloadBytes(filename, bytes, mime){
    const blob = new Blob([bytes], {type: mime || 'application/octet-stream'});
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    setTimeout(()=>{ URL.revokeObjectURL(url); a.remove(); }, 50);
  }

  function pdfEscape(s){
    return String(s)
      .replace(/\\/g,'\\\\')
      .replace(/\(/g,'\\(')
      .replace(/\)/g,'\\)');
  }

  // Evidence binder PDFs must be ASCII-safe for maximum compatibility.
  function asciiSafe(s){
    return String(s||'')
      .replace(/\u2014|\u2013/g,'-')    // em/en dash
      .replace(/\u2011/g,'-')          // non-breaking hyphen
      .replace(/\u2026/g,'...')        // ellipsis
      .replace(/\u2022/g,'*')          // bullet
      .replace(/\u2264/g,'<=')         // <=
      .replace(/\u2265/g,'>=')         // >=
      .replace(/[^\x09\x0A\x0D\x20-\x7E]/g,'?');
  }

  function wrapText(s, maxLen){
    const src = asciiSafe(s);
    const out=[];
    let cur='';
    const words = src.split(/\s+/);
    for(const word0 of words){
      const word = String(word0||'');
      if(!word) continue;

      // Hard-wrap long tokens.
      const chunks = [];
      if(word.length > maxLen){
        for(let i=0;i<word.length;i+=maxLen) chunks.push(word.slice(i,i+maxLen));
      } else {
        chunks.push(word);
      }

      for(const w of chunks){
        if(!cur){ cur = w; continue; }
        if((cur.length + 1 + w.length) <= maxLen){ cur += ' ' + w; }
        else { out.push(cur); cur = w; }
      }
    }
    if(cur) out.push(cur);
    return out.length ? out : [''];
  }

  function padRight(s, n){
    const x = asciiSafe(s);
    if(x.length > n){
      const m = Math.max(0, n - 3);
      return x.slice(0, m) + '...';
    }
    return x + ' '.repeat(n - x.length);
  }

  function shortHex(hex){
    const h = normSha256(hex);
    if(!h) return '-';
    if(h.length <= 24) return h;
    return h.slice(0,12) + '...' + h.slice(-8);
  }

  function shortSnapshotId(id){
    const s = asciiSafe(id||'');
    const prefix = 'nvrs:sha256:';
    if(s.startsWith(prefix)){
      const h = s.slice(prefix.length);
      if(h.length > 24) return prefix + h.slice(0,12) + '...' + h.slice(-8);
      return s;
    }
    if(s.length > 30) return s.slice(0,14) + '...' + s.slice(-10);
    return s || '-';
  }

  function buildEvidenceBinderPdf(ev){
    const created = asciiSafe(ev?.generated_utc || new Date().toISOString());
    const overall = asciiSafe(String(ev?.overall||'-').toUpperCase());
    const rid = asciiSafe(ev?.release_id || '-');
    const sid = asciiSafe(ev?.snapshot_id || '-');
    const verifierKid = asciiSafe(ev?.verifier_signing_kid || '-');

    const witnessOk = Number(ev?.witness?.ok || 0);
    const witnessTh = Number(ev?.witness?.threshold || 2);

    const manSha = asciiSafe(ev?.hashes?.artifacts_manifest_dsse_sha256 || '');
    const relSha = asciiSafe(ev?.hashes?.release_signature_dsse_sha256 || '');

    const tpVerdict = asciiSafe(String(ev?.trust_pack?.verdict || '-').toUpperCase());
    const tpSha = asciiSafe(ev?.trust_pack?.sha256 || '');
    const tpBytes = Number(ev?.trust_pack?.bytes || 0);

    const mismatches = Number(ev?.mismatches || 0);

    const vlockVerdict = asciiSafe(String(ev?.verifier_lock?.verdict || '-').toUpperCase());
    const vlockOk = Number(ev?.verifier_lock?.ok || 0);
    const vlockTotal = Number(ev?.verifier_lock?.total || 0);

    const sb = ev?.sbom || {};
    const sbVerdict = asciiSafe(String(sb?.verdict || '-').toUpperCase());
    const sbKid = asciiSafe(sb?.signing_kid || '-');
    const sbSha = asciiSafe(sb?.sbom_sha256 || '');
    const sbDsseSha = asciiSafe(sb?.sbom_dsse_sha256 || '');

    // Build a fixed, procurement-friendly template.
    const L = [];
    function pushLine(text, font, size, leading, maxLen){
      const f = font || 'F1';
      const sz = size || 10;
      const ld = leading || 12;
      const ml = maxLen || (f==='F2' ? 110 : 95);
      for(const w of wrapText(text, ml)){
        L.push({ text: w, font: f, size: sz, leading: ld });
      }
    }

    // Header / document control
    pushLine('Meridian Verity Group - SOC 2 / ISO Evidence Binder', 'F1', 16, 18, 80);
    pushLine('Document ID: MVG-EVIDENCE-BINDER   Template: v32.0', 'F1', 10, 12);
    pushLine('Generated (UTC): ' + created, 'F1', 10, 12);
    pushLine('Source: https://meridianverity.com/trust/ (same-origin verification)', 'F1', 10, 12);
    pushLine('Contact: security@meridianverity.com', 'F1', 10, 12);
    pushLine('', 'F1', 10, 12);

    // Attestation / disclaimer (fixed wording)
    pushLine('Attestation (informative; non-legal)', 'F1', 12, 16);
    pushLine('This binder is machine-generated by the MVG Trust verifier running locally in your browser.', 'F1', 10, 12);
    pushLine('It summarizes cryptographic verification results for artifacts fetched from meridianverity.com at the time above.', 'F1', 10, 12);
    pushLine('It is NOT a SOC 2 report, an ISO/IEC 27001 certificate, or legal advice. Control mapping is organization-specific.', 'F1', 10, 12);
    pushLine('', 'F1', 10, 12);

    // Evidence summary (table)
    pushLine('Evidence summary (point-in-time)', 'F1', 12, 16);
    pushLine(padRight('ITEM', 30) + ' ' + padRight('RESULT', 6) + ' IDENTIFIER / HASH', 'F2', 9, 11, 110);
    pushLine('-'.repeat(86), 'F2', 9, 11, 110);

    function row(item, result, detail){
      const line = padRight(item, 30) + ' ' + padRight(result, 6) + ' ' + asciiSafe(detail);
      pushLine(line, 'F2', 9, 11, 110);
    }

    const anchorsVerdict = (witnessOk >= witnessTh) ? 'PASS' : 'HOLD';
    row('Trust verdict', overall, 'release=' + rid);
    row('Trust anchor snapshot (DSSE)', anchorsVerdict, 'id=' + shortSnapshotId(sid) + ' witness=' + witnessOk + '/' + witnessTh);
    row('Release signature (DSSE)', relSha ? 'PASS' : '-', relSha ? shortHex(relSha) : '-');
    row('Artifacts manifest (DSSE)', manSha ? 'PASS' : '-', manSha ? shortHex(manSha) : '-');
    row('Artifacts hash recompute', (mismatches===0) ? 'PASS' : 'FAIL', 'mismatches=' + mismatches);
    row('Verifier code lock (JS/worker)', vlockVerdict || '-', 'core=' + vlockOk + '/' + vlockTotal);
    row('Trust Pack (ZIP + DSSE)', tpVerdict || '-', (tpSha ? shortHex(tpSha) : '-') + (tpBytes ? (' bytes=' + tpBytes) : ''));
    row('SBOM (DSSE standalone)', sbVerdict || '-', 'kid=' + sbKid + ' sbom=' + shortHex(sbSha) + ' dsse=' + shortHex(sbDsseSha));

    pushLine('', 'F1', 10, 12);

    // Control mapping (informative template)
    pushLine('Control mapping (informative; not an attestation)', 'F1', 12, 16);
    pushLine('SOC 2 (Security): release integrity and change control evidence -> Release (DSSE) + Manifest (DSSE) + Verifier code lock.', 'F1', 10, 12);
    pushLine('SOC 2 / ISO (Supply chain): component transparency evidence -> SBOM (CycloneDX) + SBOM (DSSE standalone).', 'F1', 10, 12);
    pushLine('ISO/IEC 27001: integrity of published security artifacts -> Trust Pack (DSSE + contents) + anchored snapshots (witness threshold).', 'F1', 10, 12);
    pushLine('', 'F1', 10, 12);

    // Reproduction
    pushLine('Reproduction (public, deterministic)', 'F1', 12, 16);
    pushLine('1) Open /trust and click Run check. 2) Optionally use /verify-trust and /verify-sbom for offline reproduction. 3) Compare hashes above.', 'F1', 10, 12);
    pushLine('', 'F1', 10, 12);

    // 5-line summary (embed existing summary if available; ASCII-sanitized)
    pushLine('Security intake summary (5 lines)', 'F1', 12, 16);
    const intake = Array.isArray(ev?.intake_lines) ? ev.intake_lines : [];
    if(intake.length){
      for(const l of intake.slice(0,5)){
        pushLine('  ' + asciiSafe(l), 'F2', 9, 11, 110);
      }
    } else {
      pushLine('  MVG Trust: ' + overall + ' - ' + rid, 'F2', 9, 11, 110);
      pushLine('  Anchors: ' + shortSnapshotId(sid) + ' witness ' + witnessOk + '/' + witnessTh + ' - verifier ' + verifierKid, 'F2', 9, 11, 110);
      pushLine('  Release: manifest ' + shortHex(manSha) + ' - sig ' + shortHex(relSha), 'F2', 9, 11, 110);
      pushLine('  Artifacts: mismatches ' + mismatches + ' - verifier lock ' + vlockVerdict + ' - SBOM DSSE ' + sbVerdict, 'F2', 9, 11, 110);
      pushLine('  Trust Pack: ' + tpVerdict + ' - ' + shortHex(tpSha), 'F2', 9, 11, 110);
    }

    pushLine('', 'F1', 10, 12);
    pushLine('Notes: This document is a point-in-time evidence summary for security/procurement intake.', 'F1', 10, 12);
    pushLine('It provides cryptographic verification outputs only; it does not assert operational effectiveness of controls.', 'F1', 10, 12);

    return buildPdfDocument(L, {
      title: 'MVG SOC2/ISO Evidence Binder',
      subject: 'Procurement / security intake evidence summary',
      producer: 'MVG Trust Verifier (in-browser, no third-party calls)',
      createdUtc: created
    });
  }

  function buildPdfDocument(lineObjs, meta){
    // Build pages based on vertical space.
    const pages=[];
    let cur=[];
    let y=760;
    const bottom=72;

    for(let i=0;i<lineObjs.length;i++){
      const ln=lineObjs[i];
      const leading = Number(ln?.leading || 12);
      if(y < bottom && cur.length){
        pages.push(cur);
        cur=[];
        y=760;
      }
      cur.push({ text: asciiSafe(ln.text||''), font: ln.font||'F1', size: Number(ln.size||10), leading });
      y -= leading;
    }
    if(cur.length) pages.push(cur);

    const contents = pages.map(p=>buildPdfContentStream(p));
    return buildPdfFromStreams(contents, meta);
  }

  function buildPdfContentStream(lines){
    const parts=[];
    if(!lines || !lines.length) return '';
    parts.push('BT');
    parts.push('1 0 0 1 72 760 Tm');

    let curFont='';
    let curSize=0;
    function setFont(font, size){
      const f = font || 'F1';
      const s = Number(size || 10);
      if(f!==curFont || s!==curSize){
        parts.push(`/${f} ${s} Tf`);
        curFont=f; curSize=s;
      }
    }

    setFont(lines[0].font, lines[0].size);
    parts.push('(' + pdfEscape(lines[0].text||'') + ') Tj');

    for(let i=1;i<lines.length;i++){
      const prevLead = Number(lines[i-1].leading || 12);
      parts.push(`0 -${prevLead} Td`);
      setFont(lines[i].font, lines[i].size);
      parts.push('(' + pdfEscape(lines[i].text||'') + ') Tj');
    }

    parts.push('ET');
    return parts.join('\n') + '\n';
  }

  function buildPdfFromStreams(streams, meta){
    // Minimal multi-page PDF writer. Fonts: Helvetica (F1) + Courier (F2).
    function obj(n, body){ return `${n} 0 obj\n${body}\nendobj\n`; }
    function pad10(x){ return String(x).padStart(10,'0'); }

    const header = '%PDF-1.4\n%MVG\n';
    const parts=[header];
    const offsets={};
    let off = header.length;

    const pageCount = streams.length || 1;

    let oid = 1;
    const catalogId = oid++;
    const pagesId = oid++;
    const fontHelvId = oid++;
    const fontCourierId = oid++;

    const pageIds=[];
    for(let i=0;i<pageCount;i++) pageIds.push(oid++);
    const contentIds=[];
    for(let i=0;i<pageCount;i++) contentIds.push(oid++);

    const infoId = oid++;

    // Info object
    const created = String(meta?.createdUtc||'').replace(/[-:]/g,'').replace(/\..*/,'');
    const info = `<< /Title (${pdfEscape(asciiSafe(meta?.title||''))}) /Subject (${pdfEscape(asciiSafe(meta?.subject||''))}) /Producer (${pdfEscape(asciiSafe(meta?.producer||''))}) /CreationDate (${pdfEscape('D:' + created + 'Z')}) >>`;

    function pushObj(id, body){
      offsets[id]=off;
      const s = obj(id, body);
      parts.push(s);
      off += s.length;
    }

    // Catalog / Pages
    pushObj(catalogId, `<< /Type /Catalog /Pages ${pagesId} 0 R >>`);
    pushObj(pagesId, `<< /Type /Pages /Kids [${pageIds.map(id=>id+' 0 R').join(' ')}] /Count ${pageCount} >>`);

    // Fonts
    pushObj(fontHelvId, '<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>');
    pushObj(fontCourierId, '<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>');

    // Page objects
    for(let i=0;i<pageCount;i++){
      const pid = pageIds[i];
      const cid = contentIds[i];
      const pageBody =
        `<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 612 792] ` +
        `/Resources << /Font << /F1 ${fontHelvId} 0 R /F2 ${fontCourierId} 0 R >> >> ` +
        `/Contents ${cid} 0 R >>`;
      pushObj(pid, pageBody);
    }

    // Content streams
    for(let i=0;i<pageCount;i++){
      const cid = contentIds[i];
      const contentStr = streams[i] || '';
      const contentLen = contentStr.length;
      offsets[cid]=off;
      const o = `${cid} 0 obj\n<< /Length ${contentLen} >>\nstream\n${contentStr}endstream\nendobj\n`;
      parts.push(o);
      off += o.length;
    }

    // Info
    pushObj(infoId, info);

    // Xref
    const xrefStart = off;
    const size = infoId + 1;
    let xref = 'xref\n0 ' + size + '\n';
    xref += '0000000000 65535 f \n';
    for(let i=1;i<size;i++){
      xref += pad10(offsets[i]||0) + ' 00000 n \n';
    }
    const trailer = `trailer\n<< /Size ${size} /Root ${catalogId} 0 R /Info ${infoId} 0 R >>\nstartxref\n${xrefStart}\n%%EOF\n`;
    parts.push(xref + trailer);

    const pdfStr = parts.join('');
    const out = new Uint8Array(pdfStr.length);
    for(let i=0;i<pdfStr.length;i++){ out[i] = pdfStr.charCodeAt(i) & 0xFF; }
    return out;
  }


if(btn){
    btn.addEventListener('click', ()=>run().catch(err=>{
      status.className='tag deny';
      status.textContent='FAIL';
      const msg = String(err?.message||err);
      summary.textContent=msg;
      try{
        const lines=[
          `MVG Trust: FAIL — error`,
          `Anchors: —`,
          `Release: —`,
          `Artifacts: —`,
          `Error: ${msg}`
        ].join('\n');
        if(intakeWrap && intake){ intakeWrap.style.display=''; intake.textContent=lines; }
        if(btnCopyIntake){ btnCopyIntake.onclick=()=>copyText(lines); }
      }catch(e){}
    }));
  }
})();
