
'use strict';

import { sampleTrustPackB64u } from './sampleTrustPackB64u.js';
import { sampleTrustPackDsse } from './sampleTrustPackDsse.js';
import { qrSvg, shortCodeFromSha256Id } from '../verify/qr.js';

const TRUST = {"schema_id":"mvg.trust@1","site":"https://www.meridianverity.com","release_id":"meridianverity.com.v31.demo","generated_utc":"2026-02-09T00:00:00Z","contacts":{"legal_email":"legal@meridianverity.com","privacy_email":"privacy@meridianverity.com","security_email":"security@meridianverity.com"},"security_txt":{"canonical_paths":["/security.txt","/.well-known/security.txt","/well-known/security.txt"],"expires":"2027-02-07T00:00:00Z","policy_url":"https://www.meridianverity.com/legal/security-disclosure/","preferred_languages":["en","ko"]},"pgp":{"fingerprint":"94EC8CD8863A2D0CCAF92990B8BF65777FC5A47F","openpgp4fpr":"openpgp4fpr:94EC8CD8863A2D0CCAF92990B8BF65777FC5A47F","public_key_url":"https://www.meridianverity.com/pgp.asc","rotation_note":"Keys may rotate; always verify fingerprint via /trust."},"crypto":{"bootstrap":{"kid":"ta.mvg.bootstrap.2026-02-09.v31.demo","jwk":{"kty":"OKP","crv":"Ed25519","x":"nMgO1r8oSjUy_M3WWMgg8edj-PbPMjy3IZeMwnBRCF8"}},"witness_threshold":2,"witnesses":[{"kid":"ta.mvg.witness.1.2026-02-09.v31.demo","jwk":{"kty":"OKP","crv":"Ed25519","x":"f-3I1itk81eGXCoV5j-PoRd3pdVTbFEAi0hBPyt_sMA"}},{"kid":"ta.mvg.witness.2.2026-02-09.v31.demo","jwk":{"kty":"OKP","crv":"Ed25519","x":"JS2MUSWKRJOb25HSEy_-lts5mCCQcLkrH32GRTRcL7s"}}],"verifier_signing":{"kid":"ta.mvg.verifier.signing.2026-02-09.v31.demo","jwk":{"kty":"OKP","crv":"Ed25519","x":"THSdH7Qo302Ml298hqga5_wJR87wsqhlYYCILlxy4g0"}},"sbom_signing":{"kid":"ta.mvg.sbom.signing.2026-02-09.v31.demo","jwk":{"kty":"OKP","crv":"Ed25519","x":"h871daxc1T4p1OPVN106v7TktghFRXDkU_shFU8lNF8"}}},"trust_artifacts":{"trust_anchor_registry_snapshot_href":"/downloads/MVG_Verifier_Trust_Anchor_Registry_Snapshot_v31.dsse.json","trust_anchor_registry_snapshot_id":"nvrs:sha256:7f28ba4b67ea882c12e3d4781ee62d652b11b5c2a0fefebea993bd7fba6e032b","release_signature_href":"/downloads/MVG_Release_Signature_v31.dsse.json","artifacts_manifest_href":"/downloads/MVG_Artifacts_Manifest_v31.dsse.json","trust_pack_href":"/downloads/MVG_Trust_Pack_v31.zip","trust_pack_signature_href":"/downloads/MVG_Trust_Pack_v31.dsse.json"},"trust_anchor_registry_snapshot_id":"nvrs:sha256:7f28ba4b67ea882c12e3d4781ee62d652b11b5c2a0fefebea993bd7fba6e032b"};

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

// Printable audit summary
const btnPrintSummary = $('btnPrintSummary');
const printArea = $('printArea');
const chkQrIncludeTicket = $('chkQrIncludeTicket');

// Common outputs referenced by the printable report
const verdictEl = $('verdict');
const detailEl = $('detail');
const outSha = $('outSha');
const outBytes = $('outBytes');


let lastReceipt = null;

let lastCountersigned = null;
let lastBaseReceiptSha = null;

let lastOrgApproval = null;

let lastCountersignedVerification = null;

let csPrivCryptoKey = null;
let csPrivJwk = null;
let csPubJwk = null;

// Countersign UI (optional)
const csPrivKeyFile = $('csPrivKeyFile');
const csSignedReceiptFile = $('csSignedReceiptFile');
const csOrg = $('csOrg');
const csName = $('csName');
const csEmail = $('csEmail');
const csStatement = $('csStatement');
const chkEmbedPub = $('chkEmbedPub');

// Ticket binding + org policy (optional)
const chkBindTicket = $('chkBindTicket');
const csTicketSystem = $('csTicketSystem');
const csTicketId = $('csTicketId');
const csTicketUrl = $('csTicketUrl');

const orgKeyringFile = $('orgKeyringFile');
const chkEnforceOrgPolicy = $('chkEnforceOrgPolicy');

const orgVerdict = $('orgVerdict');
const orgDetail = $('orgDetail');
const orgPolicy = $('orgPolicy');
const orgMatched = $('orgMatched');
const orgTicket = $('orgTicket');
const orgApprovalId = $('orgApprovalId');
const orgBundleHash = $('orgBundleHash');

const btnDownloadOrgApproval = $('btnDownloadOrgApproval');
const btnCopyOrgApprovalId = $('btnCopyOrgApprovalId');


const btnCsGenKey = $('btnCsGenKey');
const btnCsDownloadPub = $('btnCsDownloadPub');
const btnCsDownloadPriv = $('btnCsDownloadPriv');
const btnCsClearKey = $('btnCsClearKey');
const btnCsSign = $('btnCsSign');
const btnCsDownloadSigned = $('btnCsDownloadSigned');
const btnCsVerifySigned = $('btnCsVerifySigned');

const csKeyStatus = $('csKeyStatus');
const csKeyHint = $('csKeyHint');
const csKeyFp = $('csKeyFp');

const csVerdict = $('csVerdict');
const csDetail = $('csDetail');
const csBaseReceiptSha = $('csBaseReceiptSha');
const csSignedId = $('csSignedId');
const csCount = $('csCount');

// WebAuthn hardware-backed countersign (optional)
let hwCred = null;            // mvg.webauthn_approval_credential@1
let hwCredIdBytes = null;     // Uint8Array
let hwPublicJwk = null;       // {kty:'EC', crv:'P-256', x, y}
let hwEvidence = null;        // { verdict, fmt, aaguid, evidence_sha256, ... }

const hwCredFile = $('hwCredFile');
const hwAttachment = $('hwAttachment');
const hwUv = $('hwUv');
const hwAttestation = $('hwAttestation');
const chkHwEmbedCred = $('chkHwEmbedCred');

const btnHwRegister = $('btnHwRegister');
const btnHwSign = $('btnHwSign');
const btnHwDownloadCred = $('btnHwDownloadCred');
const btnHwClear = $('btnHwClear');

const hwStatus = $('hwStatus');
const hwDetail = $('hwDetail');
const hwFp = $('hwFp');
const hwAttachmentOut = $('hwAttachmentOut');
const hwFmt = $('hwFmt');
const hwAaguid = $('hwAaguid');
const hwEvidenceSha = $('hwEvidenceSha');
const hwEvidenceVerdict = $('hwEvidenceVerdict');


// ---------------------
// Air‑gap guard (no network)
// ---------------------
function disableNetworkApis(){
  const result = { blocked: [], unable: [], note: '' };

  try {
    if (typeof globalThis.fetch === 'function') {
      globalThis.fetch = () => { throw new Error('NETWORK_DISABLED_FETCH'); };
      result.blocked.push('fetch');
    }
  } catch(e){ result.unable.push('fetch'); }

  try {
    if (typeof globalThis.XMLHttpRequest === 'function') {
      globalThis.XMLHttpRequest = function(){ throw new Error('NETWORK_DISABLED_XHR'); };
      result.blocked.push('xhr');
    }
  } catch(e){ result.unable.push('xhr'); }

  try {
    if (typeof globalThis.WebSocket === 'function') {
      globalThis.WebSocket = function(){ throw new Error('NETWORK_DISABLED_WS'); };
      result.blocked.push('ws');
    }
  } catch(e){ result.unable.push('ws'); }

  try {
    if (typeof globalThis.EventSource === 'function') {
      globalThis.EventSource = function(){ throw new Error('NETWORK_DISABLED_EVENTSOURCE'); };
      result.blocked.push('eventsource');
    }
  } catch(e){ result.unable.push('eventsource'); }

  try {
    if (navigator && typeof navigator.sendBeacon === 'function') {
      navigator.sendBeacon = () => false;
      result.blocked.push('beacon');
    }
  } catch(e){ result.unable.push('beacon'); }

  if(result.unable.length > 0) {
    result.note = "Some APIs could not be patched; rely on CSP (connect-src 'none').";
  } else {
    result.note = 'Network primitives blocked at runtime (defense-in-depth).';
  }

  return result;
}

const AIRGAP = disableNetworkApis();
try{
  const el = $('outAirgap');
  if(el){
    el.textContent = (AIRGAP.unable.length===0) ? ('PASS: ' + AIRGAP.blocked.join(', ')) : ('HOLD: ' + AIRGAP.note);
  }
}catch(e){ /* ignore */ }

// ---------------------
// Receipt helpers
// ---------------------
function canonicalize(obj){
  if(obj === null || obj === undefined) return obj;
  if(Array.isArray(obj)) return obj.map(canonicalize);
  if(typeof obj === 'object') {
    const out = {};
    for(const k of Object.keys(obj).sort()) out[k] = canonicalize(obj[k]);
    return out;
  }
  return obj;
}
function canonicalJson(obj){
  return JSON.stringify(canonicalize(obj), null, 0);
}

async function computeAttachmentBundleManifestHash(primaryApprovalId, ticketTuple){
  const id = String(primaryApprovalId||'').trim();
  const m = id.match(/^sha256:([0-9a-fA-F]{64})$/);
  if(!m) return null;
  const basis = {
    schema_id: 'mvg.attachment_bundle_manifest_hash_basis@1',
    primary_approval_id: 'sha256:' + m[1].toLowerCase(),
    ticket: ticketTuple ? { system: String(ticketTuple.system||''), id: String(ticketTuple.id||'') } : null
  };
  const hex = await sha256Hex(new TextEncoder().encode(canonicalJson(basis)));
  return 'sha256:' + hex;
}

function clearReceipt(){
  lastReceipt = null;
  const a = $('outReceiptId'); if(a) a.textContent='—';
  const b = $('outRunId'); if(b) b.textContent='—';
  const h = $('receiptHint'); if(h) h.textContent='';
  const d = $('btnDownloadReceipt'); if(d) d.disabled=true;
  const c = $('btnCopyReceiptId'); if(c) c.disabled=true;
  clearCountersign();
  refreshHardwareButtons();
}
function enableReceipt(){
  const d = $('btnDownloadReceipt'); if(d) d.disabled=false;
  const c = $('btnCopyReceiptId'); if(c) c.disabled=false;
  refreshHardwareButtons();
}

function downloadJson(filename, obj){
  const blob = new Blob([JSON.stringify(obj, null, 2)], {type:'application/json'});
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  setTimeout(()=>{
    URL.revokeObjectURL(a.href);
    a.remove();
  }, 0);
}

async function makeReceipt(ctx){
  const base = {
    schema_id: 'mvg.verification_receipt@1',
    generated_utc: new Date().toISOString(),
    verifier: { name: 'mvg.verify-trust', version: 'v40.8.0' },
    policy: ctx.policy || {},
    artifacts: ctx.artifacts || {},
    verdicts: ctx.verdicts || {},
    notes: ctx.notes || [],
    environment: {
      user_agent: navigator.userAgent,
      webcrypto: !!(globalThis.crypto && crypto.subtle),
      decompressionstream: (typeof DecompressionStream !== 'undefined')
    },
    airgap_guard: {
      blocked: AIRGAP.blocked,
      unable: AIRGAP.unable,
      note: AIRGAP.note
    }
  };

  const enc = new TextEncoder();

  const fingerprintCore = {
    schema_id: base.schema_id,
    verifier: base.verifier,
    policy: base.policy,
    artifacts: base.artifacts,
    verdicts: base.verdicts
  };
  const fp = await sha256Hex(enc.encode(canonicalJson(fingerprintCore)));
  base.artifact_fingerprint = 'sha256:' + fp;

  const runId = await sha256Hex(enc.encode(canonicalJson(base)));
  base.verification_run_id = 'sha256:' + runId;

  return base;
}

// Receipt buttons
$('btnDownloadReceipt')?.addEventListener('click', ()=>{
  if(!lastReceipt) return;
  const fp = String(lastReceipt.artifact_fingerprint||'').replace(/^sha256:/,'').slice(0,16);
  downloadJson(`MVG_Verification_Receipt_TrustPack_${fp||'receipt'}.json`, lastReceipt);
  const h = $('receiptHint'); if(h){ h.textContent='Downloaded'; setTimeout(()=>h.textContent='', 1200); }
});
$('btnCopyReceiptId')?.addEventListener('click', async ()=>{
  if(!lastReceipt) return;
  try{
    await navigator.clipboard.writeText(String(lastReceipt.artifact_fingerprint||''));
    const h = $('receiptHint'); if(h){ h.textContent='Copied'; setTimeout(()=>h.textContent='', 1200); }
  }catch(e){ /* ignore */ }
});

// init
clearReceipt();


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 bytesToB64u(u8){
  const CHUNK = 0x8000;
  let bin='';
  for(let i=0;i<u8.length;i+=CHUNK){
    bin += String.fromCharCode.apply(null, u8.subarray(i, i+CHUNK));
  }
  return btoa(bin).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
}

function bytesToHex(bytes){
  return [...bytes].map(b=>b.toString(16).padStart(2,'0')).join('');
}
async function sha256Hex(bytes){
  const digest=await crypto.subtle.digest('SHA-256', bytes);
  return bytesToHex(new Uint8Array(digest));
}

async function sha256Bytes(bytes){
  const digest=await crypto.subtle.digest('SHA-256', bytes);
  return new Uint8Array(digest);
}

function concatBytes(a, b){
  const out = new Uint8Array(a.length + b.length);
  out.set(a, 0);
  out.set(b, a.length);
  return out;
}

function bytesEqual(a, b){
  if(!a || !b) return false;
  if(a.length !== b.length) return false;
  for(let i=0;i<a.length;i++) if(a[i] !== b[i]) return false;
  return true;
}
function dssePAE(payloadType, payloadBytes){
  const enc=new TextEncoder();
  const pt=enc.encode(payloadType);
  const head = enc.encode("DSSEv1 ");
  const sp = enc.encode(" ");
  const a=[head, enc.encode(String(pt.length)), sp, pt, sp, enc.encode(String(payloadBytes.length)), sp, payloadBytes];
  const total=a.reduce((s,x)=>s+x.length,0);
  const out=new Uint8Array(total);
  let off=0; for(const x of a){ out.set(x,off); off+=x.length; }
  return out;
}
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);
}
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 };
}

function setVerdict(v, detail){
  const pill=$('verdict');
  pill.className='tag ' + (v==='PASS'?'ok':v==='HOLD'?'hold':'deny');
  pill.textContent=v;
  $('detail').textContent=detail||'';
}

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();
  }
}

function setContentsVerdict(v, detail){
  const pill=$('contentsVerdict');
  if(!pill) return;
  pill.className='tag ' + (v==='PASS'?'ok':v==='HOLD'?'hold':'deny');
  pill.textContent=v;
  const d=$('contentsDetail'); if(d) d.textContent=detail||'';
}

function clearContentsTable(){
  const rows=$('contentsRows'); if(rows) rows.innerHTML='';
  setContentsVerdict('—','');
}

function addContentsRow(path, expected, computed, ok){
  const rows=$('contentsRows'); if(!rows) return;
  const tr=document.createElement('tr');
  const td1=document.createElement('td'); td1.textContent=path; tr.appendChild(td1);
  const td2=document.createElement('td'); td2.className='mono'; td2.textContent=expected; tr.appendChild(td2);
  const td3=document.createElement('td'); td3.className='mono'; td3.textContent=computed; tr.appendChild(td3);
  const td4=document.createElement('td');
  const tag=document.createElement('span'); tag.className='tag ' + (ok?'ok':'deny'); tag.textContent=ok?'OK':'MISMATCH';
  td4.appendChild(tag); tr.appendChild(td4);
  rows.appendChild(tr);
}

function u16(dv,o){ return dv.getUint16(o,true); }
function u32(dv,o){ return dv.getUint32(o,true); }

function findEOCD(bytes){
  // Search backwards for End of Central Directory signature 0x06054b50
  const sig=0x06054b50;
  const dv=new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  const maxBack=Math.min(bytes.byteLength, 22 + 0xFFFF);
  for(let i=bytes.byteLength-22; i>=bytes.byteLength-maxBack; i--){
    if(i<0) break;
    if(u32(dv,i)===sig){
      const cdSize=u32(dv,i+12);
      const cdOffset=u32(dv,i+16);
      const total=u16(dv,i+10);
      return {cdOffset, cdSize, total};
    }
  }
  throw new Error('ZIP_EOCD_NOT_FOUND');
}

function parseCentralDirectory(bytes, eocd){
  const sig=0x02014b50;
  const dv=new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  let off=eocd.cdOffset;
  const end=off+eocd.cdSize;
  const out=[];
  while(off+46<=end){
    if(u32(dv,off)!==sig) break;
    const flags=u16(dv,off+8);
    const method=u16(dv,off+10);
    const compSize=u32(dv,off+20);
    const uncompSize=u32(dv,off+24);
    const fnLen=u16(dv,off+28);
    const exLen=u16(dv,off+30);
    const cmLen=u16(dv,off+32);
    const localOff=u32(dv,off+42);
    const nameBytes=bytes.slice(off+46, off+46+fnLen);
    const name=new TextDecoder('utf-8').decode(nameBytes);
    out.push({name, flags, method, compSize, uncompSize, localOff});
    off = off + 46 + fnLen + exLen + cmLen;
  }
  return out;
}

function getLocalDataSlice(bytes, entry){
  const sig=0x04034b50;
  const dv=new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  const o=entry.localOff;
  if(u32(dv,o)!==sig) throw new Error('ZIP_LOCAL_HEADER');
  const fnLen=u16(dv,o+26);
  const exLen=u16(dv,o+28);
  const dataStart=o + 30 + fnLen + exLen;
  const dataEnd=dataStart + entry.compSize;
  if(dataEnd>bytes.byteLength) throw new Error('ZIP_BOUNDS');
  return bytes.slice(dataStart, dataEnd);
}

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){
  // Reads trust_pack_manifest.json from inside the ZIP and verifies every listed file.
  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
  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');

  // Build sets for extra detection
  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;

  // Render all declared entries (stable order)
  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){
      addContentsRow(path, expSha, '—', false);
      badCount++; continue;
    }
    const fb=await extractFileBytes(zipBytes, ent);
    const gotSha=(await sha256Hex(fb)).toLowerCase();
    const gotBytes=fb.byteLength;
    const ok=(gotSha===expSha && gotBytes===expBytes);
    addContentsRow(path, expSha.slice(0,16)+'…', gotSha.slice(0,16)+'…', ok);
    if(ok) okCount++; else badCount++;
  }

  // Extra files (not declared)
  for(const x of extras){
    addContentsRow(x, '—', 'extra', false);
    badCount++;
  }

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

async function verify(zipBytes, dsseEnv){
  clearContentsTable();
  clearReceipt();

  if(!crypto?.subtle?.importKey || !crypto?.subtle?.digest) {
    setVerdict('HOLD','Crypto unavailable (WebCrypto)');
    setContentsVerdict('HOLD','Crypto unavailable');
    return;
  }

  const keys={};
  keys[TRUST.crypto.bootstrap.kid]=TRUST.crypto.bootstrap.jwk;
  const witnessKeys={};
  for(const w of (TRUST.crypto.witnesses||[])){ keys[w.kid]=w.jwk; witnessKeys[w.kid]=w.jwk; }

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

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

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

  $('outSha').textContent=sha;
  $('outBytes').textContent=String(bytes);
  $('outSig').textContent=`sigs: ${rAll.okCount} • witness: ${rW.okCount}/${wth}`;

  if(payload.trust_pack_sha256 !== sha) throw new Error('ZIP_SHA_MISMATCH');
  if(payload.trust_pack_bytes !== bytes) throw new Error('ZIP_BYTES_MISMATCH');

  // Deep check: unzip + verify file inventory hashes inside trust_pack_manifest.json
  let c={ verdict:'HOLD', detail:'—', okCount:0, listed:0, extras:0 };
  try{
    c = await verifyTrustPackContents(zipBytes);
    setContentsVerdict(c.verdict, c.detail);
  } catch(e){
    const msg = String(e?.message||e);
    setContentsVerdict('HOLD', msg);
    c = { verdict:'HOLD', detail:msg, okCount:0, listed:0, extras:0 };
  }

  const overall = (c.verdict==='PASS') ? 'PASS' : (c.verdict==='FAIL') ? 'FAIL' : 'HOLD';
  const overallDetail =
    overall==='PASS' ? 'Trust Pack verified (DSSE + contents).'
    : overall==='HOLD' ? 'Trust Pack digest verified. Contents verification unavailable (fail-closed).'
    : 'Trust Pack digest verified, but contents manifest mismatched.';

  setVerdict(overall, overallDetail);


// Generate and expose receipt (deterministic; reviewer-generated)
const enc = new TextEncoder();
const dssePayloadSha = await sha256Hex(enc.encode(new TextDecoder().decode(rAll.payloadBytes)));
const receipt = await makeReceipt({
  policy: { witness_threshold: wth },
  artifacts: {
    trust_pack: { sha256: sha, bytes },
    trust_pack_dsse: {
      payloadType: dsseEnv.payloadType,
      payload_sha256: await sha256Hex(rAll.payloadBytes),
      sigs_ok: rAll.okCount,
      witness_ok: rW.okCount
    },
    release: { id: TRUST.release_id, generated_utc: TRUST.generated_utc }
  },
  verdicts: { overall, contents: c.verdict },
  notes: [
    'Offline verifier: no uploads; intended to run without network.',
    'Receipt is reviewer-generated; attach internally for procurement/legal review.'
  ]
});
lastReceipt = receipt;
const ridEl = $('outReceiptId'); if(ridEl) ridEl.textContent = receipt.artifact_fingerprint;
const runEl = $('outRunId'); if(runEl) runEl.textContent = receipt.verification_run_id;
enableReceipt();

  // Base receipt sha256 (for countersigning)
  try{
    lastBaseReceiptSha = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(receipt))));
    if(csBaseReceiptSha) csBaseReceiptSha.textContent = lastBaseReceiptSha;
  }catch(e){ /* ignore */ }

  // Copy summary (security-team friendly)
  const btnCopy = $('btnCopyContents');
  if(btnCopy){
    const lines = [
      `Trust Pack: ${overall} — ${TRUST.release_id||'—'}`,
      `ZIP sha256: ${sha}`,
      `DSSE: sigs ${rAll.okCount} • witness ${rW.okCount}/${wth}`,
      `Contents: ${c.verdict} • ${c.okCount}/${c.listed} matched • extras ${c.extras}`,
      `Receipt: ${lastReceipt?.artifact_fingerprint || '—'}`,
      `Verify: /verify-trust (offline)`
    ].join('\n');
    btnCopy.onclick = ()=>copyText(lines);
  }
}



async function readFileBytes(file){
  const buf=await file.arrayBuffer();
  return new Uint8Array(buf);
}

async function onVerifySelected(){
  const zf=$('zipFile').files?.[0];
  const sf=$('sigFile').files?.[0];
  if(!zf || !sf) return;
  const zb=await readFileBytes(zf);
  const env=JSON.parse(new TextDecoder().decode(await readFileBytes(sf)));
  try{ await verify(zb, env); }catch(e){ setVerdict('FAIL', String(e?.message||e)); }
}

async function onVerifySample(){
  try{
    const zipBytes=b64uToBytes(sampleTrustPackB64u);
    await verify(zipBytes, sampleTrustPackDsse);
  }catch(e){ setVerdict('FAIL', String(e?.message||e)); }
}

$('btnVerifySelected').addEventListener('click', ()=>onVerifySelected());
$('btnVerifySample').addEventListener('click', ()=>onVerifySample());


// ---------------------
// Countersign (local) — produces a signed internal approval record
// ---------------------
function setCsTag(el, v){
  if(!el) return;
  el.textContent = v;
  el.className = 'tag ' + (v==='PASS'?'ok':(v==='HOLD'?'hold':(v==='FAIL'?'deny':'neutral')));
}
function setCsVerdict(v, msg){
  setCsTag(csVerdict, v);
  if(csDetail) csDetail.textContent = msg || '';
}
function setCsKeyStatus(v, msg){
  setCsTag(csKeyStatus, v);
  if(csKeyHint) csKeyHint.textContent = msg || '';
}

// WebAuthn hardware-backed countersign UI helpers
function setHwTag(el, v){
  if(!el) return;
  el.textContent = v;
  el.className = 'tag ' + (v==='PASS'?'ok':(v==='HOLD'?'hold':(v==='FAIL'?'deny':'neutral')));
}
function setHwStatus(v, msg){
  setHwTag(hwStatus, v);
  if(hwDetail) hwDetail.textContent = msg || '';
}
function setText(el, v){
  if(!el) return;
  el.textContent = (v === null || v === undefined || v === '') ? '—' : String(v);
}
function isWebAuthnSupported(){
  return !!(globalThis.PublicKeyCredential && navigator.credentials && typeof navigator.credentials.get === 'function');
}
function refreshHardwareButtons(){
  const supported = isWebAuthnSupported();
  if(!supported){
    if(hwStatus && hwStatus.textContent==='—') setHwStatus('HOLD', 'WebAuthn not supported in this browser/context.');
    if(btnHwSign) btnHwSign.disabled = true;
    if(btnHwDownloadCred) btnHwDownloadCred.disabled = !hwCred;
    return;
  }
  if(btnHwSign) btnHwSign.disabled = !(!!lastReceipt && !!hwCred && !!hwPublicJwk);
  if(btnHwDownloadCred) btnHwDownloadCred.disabled = !hwCred;
}
function clearHardwareKey(){
  hwCred = null;
  hwCredIdBytes = null;
  hwPublicJwk = null;
  hwEvidence = null;
  if(hwCredFile) hwCredFile.value = '';
  setHwStatus('—','');
  setText(hwFp, '—');
  setText(hwAttachmentOut, '—');
  setText(hwFmt, '—');
  setText(hwAaguid, '—');
  setText(hwEvidenceSha, '—');
  setText(hwEvidenceVerdict, '—');
  refreshHardwareButtons();
}

// Org approval policy UI (optional)
function setOrgTag(el, v){
  if(!el) return;
  el.textContent = v;
  el.className = 'tag ' + (v==='PASS'?'ok':(v==='HOLD'?'hold':(v==='FAIL'?'deny':'neutral')));
}
function setOrgVerdict(v, msg){
  setOrgTag(orgVerdict, v);
  if(orgDetail) orgDetail.textContent = msg || '';
}
function clearOrgApprovalUi(){
  lastOrgApproval = null;
  if(orgPolicy) orgPolicy.textContent = '—';
  if(orgMatched) orgMatched.textContent = '—';
  if(orgTicket) orgTicket.textContent = '—';
  if(orgApprovalId) orgApprovalId.textContent = '—';
  if(orgBundleHash) orgBundleHash.textContent = '—';
  setOrgVerdict('—','');
  if(btnDownloadOrgApproval) btnDownloadOrgApproval.disabled = true;
  if(btnCopyOrgApprovalId) btnCopyOrgApprovalId.disabled = true;
}

function clearCountersign(){
  lastCountersigned = null;
  lastBaseReceiptSha = null;
  csPrivCryptoKey = null;
  csPrivJwk = null;
  csPubJwk = null;

  clearOrgApprovalUi();

  if(csKeyFp) csKeyFp.textContent = '—';
  setCsKeyStatus('—','');
  setCsVerdict('—','');

  if(csBaseReceiptSha) csBaseReceiptSha.textContent = '—';
  if(csSignedId) csSignedId.textContent = '—';
  if(csCount) csCount.textContent = '—';

  if(btnCsDownloadPub) btnCsDownloadPub.disabled = true;
  if(btnCsDownloadPriv) btnCsDownloadPriv.disabled = true;
  if(btnCsDownloadSigned) btnCsDownloadSigned.disabled = true;
}

function compact(obj){
  if(obj === null || obj === undefined) return undefined;
  if(Array.isArray(obj)){
    const arr = obj.map(compact).filter(v=>v!==undefined);
    return arr;
  }
  if(typeof obj === 'object'){
    const out = {};
    for(const k of Object.keys(obj)){
      const v = compact(obj[k]);
      if(v === undefined) continue;
      if(typeof v === 'string' && v.trim()==='') continue;
      out[k]=v;
    }
    return out;
  }
  if(typeof obj === 'string' && obj.trim()==='') return undefined;
  return obj;
}

function pickPrivateJwk(obj){
  if(!obj || typeof obj !== 'object') return null;
  if(obj.kty && obj.d) return obj;
  if(obj.private_key_jwk && obj.private_key_jwk.d) return obj.private_key_jwk;
  if(obj.jwk && obj.jwk.d) return obj.jwk;
  return null;
}
function publicFromPrivateJwk(jwk){
  return { kty:jwk.kty, crv:jwk.crv, x:jwk.x, y:jwk.y };
}
function isP256EcJwk(jwk, needPrivate){
  if(!jwk) return false;
  if(String(jwk.kty) !== 'EC') return false;
  if(String(jwk.crv) !== 'P-256') return false;
  if(typeof jwk.x !== 'string' || typeof jwk.y !== 'string') return false;
  if(needPrivate && typeof jwk.d !== 'string') return false;
  return true;
}

async function importEcdsaPrivate(jwk){
  return crypto.subtle.importKey('jwk', jwk, {name:'ECDSA', namedCurve:'P-256'}, false, ['sign']);
}
async function importEcdsaPublic(jwk){
  return crypto.subtle.importKey('jwk', jwk, {name:'ECDSA', namedCurve:'P-256'}, false, ['verify']);
}
async function jwkPublicFingerprintSha256(jwkPub){
  const minimal = { crv:jwkPub.crv, kty:jwkPub.kty, x:jwkPub.x, y:jwkPub.y };
  const hex = await sha256Hex(new TextEncoder().encode(canonicalJson(minimal)));
  return 'sha256:' + hex;
}

async function signPayloadWithLoadedKey(payload){
  if(!csPrivCryptoKey) throw new Error('NO_PRIVATE_KEY');
  const payloadBytes = new TextEncoder().encode(canonicalJson(payload));
  const sigAb = await crypto.subtle.sign({name:'ECDSA', hash:'SHA-256'}, csPrivCryptoKey, payloadBytes);
  const sigB64u = bytesToB64u(new Uint8Array(sigAb));
  const payloadSha = await sha256Hex(payloadBytes);
  return { signature_b64u: sigB64u, payload_sha256: 'sha256:' + payloadSha };
}

async function makeCountersignedReceipt(baseReceipt, countersignatures){
  const baseSha = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(baseReceipt))));
  const env = {
    schema_id: 'mvg.countersigned_verification_receipt@1',
    countersigned_receipt_version: '1.0.0',
    generated_utc: new Date().toISOString(),
    base_receipt: baseReceipt,
    base_receipt_sha256: baseSha,
    countersignatures: countersignatures || [],
    notes: [
      'Countersignatures are reviewer-generated and do not imply MVG endorsement of signer identity.',
      'Verification is offline: signatures cover the countersign payload and bind it to the base receipt sha256.'
    ]
  };

  const idCore = {
    schema_id: env.schema_id,
    base_receipt_sha256: env.base_receipt_sha256,
    countersignatures: (env.countersignatures||[]).map(cs=>({ payload_sha256: cs.payload_sha256, signature_b64u: cs.signature_b64u }))
  };
  const rid = await sha256Hex(new TextEncoder().encode(canonicalJson(idCore)));
  env.countersigned_receipt_id = 'sha256:' + rid;

  return env;
}

// ---------------------
// WebAuthn helpers (hardware-backed countersign)
// ---------------------
const CBOR_BREAK = Symbol('cbor_break');

function cborReadUint(dv, offset, ai){
  if(ai < 24) return [ai, offset];
  if(ai === 24) return [dv.getUint8(offset), offset+1];
  if(ai === 25) return [dv.getUint16(offset, false), offset+2];
  if(ai === 26) return [dv.getUint32(offset, false), offset+4];
  if(ai === 27){
    const hi = dv.getUint32(offset, false);
    const lo = dv.getUint32(offset+4, false);
    const v = (BigInt(hi) << 32n) | BigInt(lo);
    return [v <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(v) : v, offset+8];
  }
  throw new Error('CBOR_UINT_BAD_AI');
}

function cborDecode(u8, offset=0){
  const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
  if(offset >= u8.length) throw new Error('CBOR_EOF');
  const ib = dv.getUint8(offset); offset++;
  const mt = ib >> 5;
  const ai = ib & 31;

  if(mt === 0){
    const [v, o] = cborReadUint(dv, offset, ai);
    return [v, o];
  }
  if(mt === 1){
    const [v, o] = cborReadUint(dv, offset, ai);
    return [-(v + 1), o];
  }
  if(mt === 2){
    if(ai === 31){
      const parts = [];
      while(true){
        const [chunk, o] = cborDecode(u8, offset);
        offset = o;
        if(chunk === CBOR_BREAK) break;
        if(!(chunk instanceof Uint8Array)) throw new Error('CBOR_INDEF_BYTES_EXPECT_BYTES');
        parts.push(chunk);
      }
      const total = parts.reduce((s,p)=>s+p.length,0);
      const out = new Uint8Array(total);
      let off = 0; for(const p of parts){ out.set(p, off); off += p.length; }
      return [out, offset];
    }
    const [len, o] = cborReadUint(dv, offset, ai);
    offset = o;
    const end = offset + Number(len);
    if(end > u8.length) throw new Error('CBOR_BYTES_OOB');
    return [u8.slice(offset, end), end];
  }
  if(mt === 3){
    if(ai === 31){
      let out = '';
      while(true){
        const [chunk, o] = cborDecode(u8, offset);
        offset = o;
        if(chunk === CBOR_BREAK) break;
        if(typeof chunk !== 'string') throw new Error('CBOR_INDEF_TEXT_EXPECT_TEXT');
        out += chunk;
      }
      return [out, offset];
    }
    const [len, o] = cborReadUint(dv, offset, ai);
    offset = o;
    const end = offset + Number(len);
    if(end > u8.length) throw new Error('CBOR_TEXT_OOB');
    const s = new TextDecoder().decode(u8.slice(offset, end));
    return [s, end];
  }
  if(mt === 4){
    const arr = [];
    if(ai === 31){
      while(true){
        const [v, o] = cborDecode(u8, offset);
        offset = o;
        if(v === CBOR_BREAK) break;
        arr.push(v);
      }
      return [arr, offset];
    }
    const [len, o] = cborReadUint(dv, offset, ai);
    offset = o;
    for(let i=0;i<Number(len);i++){
      const [v, o2] = cborDecode(u8, offset);
      offset = o2;
      arr.push(v);
    }
    return [arr, offset];
  }
  if(mt === 5){
    const m = new Map();
    if(ai === 31){
      while(true){
        const [k, o1] = cborDecode(u8, offset);
        offset = o1;
        if(k === CBOR_BREAK) break;
        const [v, o2] = cborDecode(u8, offset);
        offset = o2;
        m.set(k, v);
      }
      return [m, offset];
    }
    const [len, o] = cborReadUint(dv, offset, ai);
    offset = o;
    for(let i=0;i<Number(len);i++){
      const [k, o1] = cborDecode(u8, offset);
      offset = o1;
      const [v, o2] = cborDecode(u8, offset);
      offset = o2;
      m.set(k, v);
    }
    return [m, offset];
  }
  if(mt === 6){
    const [_tag, o] = cborReadUint(dv, offset, ai);
    return cborDecode(u8, o);
  }
  if(mt === 7){
    if(ai === 20) return [false, offset];
    if(ai === 21) return [true, offset];
    if(ai === 22) return [null, offset];
    if(ai === 23) return [undefined, offset];
    if(ai === 24) return [dv.getUint8(offset), offset+1];
    if(ai === 31) return [CBOR_BREAK, offset];
    throw new Error('CBOR_SIMPLE_UNSUPPORTED');
  }

  throw new Error('CBOR_MT_UNSUPPORTED');
}

function cborDecodeAll(u8){
  const [v, o] = cborDecode(u8, 0);
  if(o !== u8.length) throw new Error('CBOR_TRAILING_BYTES');
  return v;
}

function formatUuidFromBytes(bytes16){
  if(!(bytes16 instanceof Uint8Array) || bytes16.length !== 16) return '';
  const hex = bytesToHex(bytes16);
  return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
}

function parseAuthenticatorData(u8){
  if(!(u8 instanceof Uint8Array)) throw new Error('AUTH_DATA_NOT_BYTES');
  if(u8.length < 37) throw new Error('AUTH_DATA_TOO_SHORT');
  const rpIdHash = u8.slice(0,32);
  const flags = u8[32];
  const signCount = new DataView(u8.buffer, u8.byteOffset + 33, 4).getUint32(0, false);
  const userPresent = (flags & 0x01) !== 0;
  const userVerified = (flags & 0x04) !== 0;
  const backupEligible = (flags & 0x08) !== 0;
  const backupState = (flags & 0x10) !== 0;
  const hasAttestedCredentialData = (flags & 0x40) !== 0;
  const hasExtensions = (flags & 0x80) !== 0;

  let offset = 37;
  let aaguid = null;
  let credentialId = null;
  let credentialPublicKey = null;

  if(hasAttestedCredentialData){
    if(u8.length < offset + 16 + 2) throw new Error('AUTH_DATA_ATTESTED_TOO_SHORT');
    const aag = u8.slice(offset, offset+16);
    aaguid = formatUuidFromBytes(aag);
    offset += 16;
    const credIdLen = new DataView(u8.buffer, u8.byteOffset + offset, 2).getUint16(0, false);
    offset += 2;
    if(u8.length < offset + credIdLen) throw new Error('AUTH_DATA_CREDID_OOB');
    credentialId = u8.slice(offset, offset + credIdLen);
    offset += credIdLen;
    const [coseKey, o] = cborDecode(u8, offset);
    credentialPublicKey = coseKey;
    offset = o;
  }

  return {
    rpIdHash,
    flags,
    signCount,
    userPresent,
    userVerified,
    backupEligible,
    backupState,
    hasAttestedCredentialData,
    hasExtensions,
    aaguid,
    credentialId,
    credentialPublicKey
  };
}

function coseEc2P256ToJwk(cose){
  if(!(cose instanceof Map)) throw new Error('COSE_KEY_NOT_MAP');
  const kty = cose.get(1);
  const crv = cose.get(-1);
  const x = cose.get(-2);
  const y = cose.get(-3);
  if(kty !== 2) throw new Error('COSE_KTY_UNSUPPORTED_EXPECT_EC2');
  if(crv !== 1) throw new Error('COSE_CRV_UNSUPPORTED_EXPECT_P256');
  if(!(x instanceof Uint8Array) || !(y instanceof Uint8Array)) throw new Error('COSE_COORDS_MISSING');
  return { kty:'EC', crv:'P-256', x: bytesToB64u(x), y: bytesToB64u(y) };
}

async function summarizeWebAuthnCredentialBundle(bundle){
  if(!bundle || typeof bundle !== 'object') return { verdict:'FAIL', detail:'No bundle.' };
  if(String(bundle.schema_id||'') !== 'mvg.webauthn_approval_credential@1') return { verdict:'FAIL', detail:'schema_id mismatch.' };

  const evidenceSha = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(bundle))));
  const attObjB64u = String(bundle.attestation_object_b64u||'');
  if(!attObjB64u) return { verdict:'FAIL', detail:'Missing attestation_object_b64u.', evidence_sha256: evidenceSha };
  const attObjBytes = b64uToBytes(attObjB64u);
  const attObjSha = 'sha256:' + (await sha256Hex(attObjBytes));

  let fmt = '';
  let aaguid = '';
  let derivedJwk = null;
  let derivedFp = '';
  try{
    const decoded = cborDecodeAll(attObjBytes);
    if(!(decoded instanceof Map)) throw new Error('ATTESTATION_CBOR_NOT_MAP');
    fmt = String(decoded.get('fmt')||'');
    const authData = decoded.get('authData');
    if(!(authData instanceof Uint8Array)) throw new Error('ATTESTATION_AUTHDATA_MISSING');
    const parsed = parseAuthenticatorData(authData);
    aaguid = String(parsed.aaguid||'');
    if(parsed.credentialPublicKey){
      derivedJwk = coseEc2P256ToJwk(parsed.credentialPublicKey);
      derivedFp = await jwkPublicFingerprintSha256(derivedJwk);
    }
    // rpIdHash consistency (best-effort)
    const rpId = String(bundle.rp_id||'');
    if(rpId){
      const expectedRpHash = await sha256Bytes(new TextEncoder().encode(rpId));
      if(!bytesEqual(expectedRpHash, parsed.rpIdHash)){
        return {
          verdict:'FAIL',
          detail:'rpIdHash mismatch vs bundle.rp_id.',
          evidence_sha256: evidenceSha,
          attestation_object_sha256: attObjSha,
          fmt,
          aaguid
        };
      }
    }
    // Credential id match (best-effort)
    if(parsed.credentialId && bundle.credential_id_b64u){
      const derivedCredIdB64u = bytesToB64u(parsed.credentialId);
      if(derivedCredIdB64u !== String(bundle.credential_id_b64u)){
        return {
          verdict:'FAIL',
          detail:'credential_id mismatch vs attestation authData.',
          evidence_sha256: evidenceSha,
          attestation_object_sha256: attObjSha,
          fmt,
          aaguid
        };
      }
    }
  }catch(e){
    return { verdict:'FAIL', detail:String(e.message||e), evidence_sha256: evidenceSha, attestation_object_sha256: attObjSha, fmt, aaguid };
  }

  // Public key consistency: bundle.public_key_jwk should match derived JWK (when available)
  const bundleJwk = bundle.public_key_jwk;
  if(bundleJwk && derivedFp){
    const bundleFp = String(bundle.public_key_fingerprint_sha256||'') || (await jwkPublicFingerprintSha256(bundleJwk));
    if(bundleFp !== derivedFp){
      return {
        verdict:'FAIL',
        detail:'public_key_jwk mismatch vs attestation credentialPublicKey.',
        evidence_sha256: evidenceSha,
        attestation_object_sha256: attObjSha,
        fmt,
        aaguid,
        derived_public_key_fingerprint_sha256: derivedFp,
        bundle_public_key_fingerprint_sha256: bundleFp
      };
    }
  }

  if(fmt === 'none'){
    return {
      verdict:'HOLD',
      detail:'fmt=none (no vendor attestation statement). Evidence is still bound to the key and rpId.',
      evidence_sha256: evidenceSha,
      attestation_object_sha256: attObjSha,
      fmt,
      aaguid,
      derived_public_key_fingerprint_sha256: derivedFp || null
    };
  }

  return {
    verdict:'PASS',
    detail:`Parsed and internally consistent (fmt=${fmt||'unknown'}). Attestation trust evaluation is org-specific.`,
    evidence_sha256: evidenceSha,
    attestation_object_sha256: attObjSha,
    fmt,
    aaguid,
    derived_public_key_fingerprint_sha256: derivedFp || null
  };
}

function safeJsonFromBytes(u8){
  try{ return JSON.parse(new TextDecoder().decode(u8)); }catch(_e){ return null; }
}

async function verifyCountersignedReceipt(obj){
  if(!obj || typeof obj !== 'object') return { overall:'FAIL', detail:'Invalid JSON.' };
  if(String(obj.schema_id||'') !== 'mvg.countersigned_verification_receipt@1') return { overall:'FAIL', detail:'schema_id mismatch.' };
  if(!obj.base_receipt) return { overall:'FAIL', detail:'Missing base_receipt.' };

  const computedBaseSha = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(obj.base_receipt))));
  if(String(obj.base_receipt_sha256||'') !== computedBaseSha){
    return { overall:'FAIL', detail:'base_receipt_sha256 mismatch.' };
  }

  const sigs = Array.isArray(obj.countersignatures) ? obj.countersignatures : [];
  if(sigs.length===0){
    return { overall:'HOLD', detail:'No countersignatures present.' };
  }

  let ok=0, bad=0, hold=0;
  let attestPass=0, attestFail=0, attestHold=0;
  const results = [];
  for(const cs of sigs){
    let fp = '';

    try{
      if(String(cs.schema_id||'') !== 'mvg.receipt.countersignature@1') throw new Error('CS_SCHEMA_MISMATCH');
      if(!cs.payload) throw new Error('CS_PAYLOAD_MISSING');
      const payloadBytes = new TextEncoder().encode(canonicalJson(cs.payload));
      const payloadSha = 'sha256:' + (await sha256Hex(payloadBytes));
      if(String(cs.payload_sha256||'') !== payloadSha) throw new Error('CS_PAYLOAD_SHA_MISMATCH');

      if(String(cs.payload.base_receipt_sha256||'') !== computedBaseSha) throw new Error('CS_BASE_SHA_BINDING_MISMATCH');

      // Optional: ticket binding consistency check (self-contained, signed)
      if(cs.payload.ticket_binding_sha256){
        const sys = String(cs.payload.ticket?.system||'');
        const tid = String(cs.payload.ticket?.id||'');
        if(!sys || !tid) throw new Error('CS_TICKET_BINDING_MISSING');
        const tuple = { system: sys, id: tid };
        const tSha = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(tuple))));
        if(String(cs.payload.ticket_binding_sha256||'') !== tSha) throw new Error('CS_TICKET_BINDING_SHA_MISMATCH');
      }

      const method = String(cs.signing_method || cs.payload.signing_method || (cs.webauthn ? 'webauthn' : '') || '').toLowerCase();
      const isWebAuthn = (method === 'webauthn') || (!!cs.webauthn) || String(cs.payload.algorithm||'').toUpperCase().includes('WEBAUTHN');

      const jwkPub = cs.payload.public_key_jwk;
      fp = String(cs.payload.public_key_fingerprint_sha256 || '');
      if(!jwkPub){
        hold++;
        results.push({ status:'HOLD', fingerprint: fp||null, reason:'PUBLIC_KEY_MISSING', ticket: cs.payload.ticket||null, signing_method: isWebAuthn?'webauthn':'jwk' });
        continue;
      }
      if(!isP256EcJwk(jwkPub, false)) throw new Error('CS_PUBLIC_KEY_INVALID');

      const pubKey = await importEcdsaPublic(jwkPub);
      const sigBytes = b64uToBytes(String(cs.signature_b64u||''));

      if(isWebAuthn){
        const wa = cs.webauthn;
        if(!wa || typeof wa !== 'object'){
          hold++;
          results.push({ status:'HOLD', fingerprint: fp||null, reason:'WEBAUTHN_ASSERTION_MISSING', ticket: cs.payload.ticket||null, signing_method:'webauthn' });
          continue;
        }

        const authData = b64uToBytes(String(wa.authenticator_data_b64u||''));
        const clientData = b64uToBytes(String(wa.client_data_json_b64u||''));
        if(authData.length===0 || clientData.length===0){
          hold++;
          results.push({ status:'HOLD', fingerprint: fp||null, reason:'WEBAUTHN_ASSERTION_FIELDS_MISSING', ticket: cs.payload.ticket||null, signing_method:'webauthn' });
          continue;
        }

        const clientObj = safeJsonFromBytes(clientData);
        if(!clientObj) throw new Error('WEBAUTHN_CLIENTDATA_JSON_INVALID');
        if(clientObj.type && String(clientObj.type) !== 'webauthn.get') throw new Error('WEBAUTHN_CLIENTDATA_TYPE_MISMATCH');

        const expectedChallenge = bytesToB64u(await sha256Bytes(payloadBytes));
        if(String(clientObj.challenge||'') !== expectedChallenge) throw new Error('WEBAUTHN_CHALLENGE_MISMATCH');

        const origin = String(clientObj.origin||'');
        const payloadOrigin = String(cs.payload.webauthn?.origin || cs.payload.signing_context?.origin || '');
        if(payloadOrigin && origin && payloadOrigin !== origin) throw new Error('WEBAUTHN_ORIGIN_MISMATCH');

        const rpId = String(cs.payload.webauthn?.rp_id || cs.payload.signing_context?.rp_id || '');
        if(rpId){
          const expectedRpHash = await sha256Bytes(new TextEncoder().encode(rpId));
          if(!bytesEqual(expectedRpHash, authData.slice(0,32))) throw new Error('WEBAUTHN_RPID_HASH_MISMATCH');
        }

        try{
          const parsedAuth = parseAuthenticatorData(authData);
          const uvReq = String(cs.payload.webauthn?.user_verification || cs.payload.signing_context?.user_verification || '');
          if(uvReq === 'required' && !parsedAuth.userVerified) throw new Error('WEBAUTHN_UV_REQUIRED_BUT_NOT_SET');
        }catch(eUv){
          hold++;
          results.push({ status:'HOLD', fingerprint: fp||null, reason:String(eUv.message||eUv), ticket: cs.payload.ticket||null, signing_method:'webauthn' });
          continue;
        }

        const clientHash = await sha256Bytes(clientData);
        const signedBytes = concatBytes(authData, clientHash);
        const okSig = await crypto.subtle.verify({name:'ECDSA', hash:'SHA-256'}, pubKey, sigBytes, signedBytes);
        if(!okSig) throw new Error('WEBAUTHN_SIGNATURE_INVALID');

        let att = null;
        if(cs.webauthn_credential){
          att = await summarizeWebAuthnCredentialBundle(cs.webauthn_credential);
          const expectedSha = String(cs.payload.webauthn_credential_sha256||'');
          if(expectedSha && att && att.evidence_sha256 && expectedSha !== att.evidence_sha256){
            att = { ...att, verdict:'FAIL', detail:`Evidence sha mismatch (payload expects ${expectedSha}).` };
          }
        }else if(cs.payload.webauthn_credential_sha256){
          att = { verdict:'HOLD', detail:'No attestation evidence embedded; external bundle expected.', evidence_sha256: String(cs.payload.webauthn_credential_sha256) };
        }else{
          att = { verdict:'HOLD', detail:'No attestation evidence provided.' };
        }
        if(att){
          if(att.verdict==='PASS') attestPass++; else if(att.verdict==='FAIL') attestFail++; else attestHold++;
        }

        ok++;
        results.push({ status:'PASS', fingerprint: fp||null, reason:'OK', ticket: cs.payload.ticket||null, signing_method:'webauthn', attestation: att });
      }else{
        const okSig = await crypto.subtle.verify({name:'ECDSA', hash:'SHA-256'}, pubKey, sigBytes, payloadBytes);
        if(!okSig) throw new Error('CS_SIGNATURE_INVALID');
        ok++;
        results.push({ status:'PASS', fingerprint: fp||null, reason:'OK', ticket: cs.payload.ticket||null, signing_method:'jwk' });
      }
    }catch(e){
      const msg = String(e && (e.message||e) || e);
      const maybeUnsupported = msg.toLowerCase().includes('algorithm') || msg.toLowerCase().includes('unsupported');
      if(maybeUnsupported) hold++; else bad++;
      results.push({ status: maybeUnsupported?'HOLD':'FAIL', fingerprint: fp||null, reason: msg, ticket: cs?.payload?.ticket||null });
    }
  }

  const attTotal = attestPass + attestFail + attestHold;
  const attNote = attTotal>0 ? ` • attestation: ${attestPass} pass / ${attestFail} fail / ${attestHold} hold` : '';
  if(bad>0) return { overall:'FAIL', detail:`${ok} ok / ${bad} fail / ${hold} hold${attNote}`, results };
  if(hold>0) return { overall:'HOLD', detail:`${ok} ok / ${hold} hold${attNote}`, results };
  return { overall:'PASS', detail:`${ok} countersignature(s) verified${attNote}`, results };
}

async function loadPrivateKeyFromFile(file){
  const obj = JSON.parse(await file.text());
  const jwk = pickPrivateJwk(obj);
  if(!jwk) throw new Error('NO_JWK_PRIVATE_KEY');
  if(!isP256EcJwk(jwk, true)) throw new Error('UNSUPPORTED_KEY_TYPE_EXPECT_EC_P256');
  csPrivJwk = jwk;
  csPubJwk = publicFromPrivateJwk(jwk);
  csPrivCryptoKey = await importEcdsaPrivate(jwk);
  const fp = await jwkPublicFingerprintSha256(csPubJwk);
  if(csKeyFp) csKeyFp.textContent = fp;
  setCsKeyStatus('PASS', 'Loaded EC P‑256 private key (local only).');
  if(btnCsDownloadPub) btnCsDownloadPub.disabled = false;
  if(btnCsDownloadPriv) btnCsDownloadPriv.disabled = false;
}

async function generateDemoKeypair(){
  const kp = await crypto.subtle.generateKey({name:'ECDSA', namedCurve:'P-256'}, true, ['sign','verify']);
  const privJwk = await crypto.subtle.exportKey('jwk', kp.privateKey);
  const pubJwk = await crypto.subtle.exportKey('jwk', kp.publicKey);
  csPrivJwk = privJwk;
  csPubJwk = pubJwk;
  csPrivCryptoKey = kp.privateKey;
  const fp = await jwkPublicFingerprintSha256(pubJwk);
  if(csKeyFp) csKeyFp.textContent = fp;
  setCsKeyStatus('PASS', 'Demo keypair generated (EC P‑256).');
  if(btnCsDownloadPub) btnCsDownloadPub.disabled = false;
  if(btnCsDownloadPriv) btnCsDownloadPriv.disabled = false;
}

function downloadKey(filename, jwk){
  if(!jwk) return;
  downloadJson(filename, jwk);
}


async function readOrgKeyringFromInput(){
  const f = orgKeyringFile?.files?.[0];
  if(!f) return null;
  const obj = JSON.parse(await f.text());
  if(!obj || typeof obj !== 'object') throw new Error('ORG_KEYRING_INVALID_JSON');
  if(obj.schema_id && String(obj.schema_id) !== 'mvg.org_approval_keyring@1') throw new Error('ORG_KEYRING_SCHEMA_MISMATCH');
  if(!Array.isArray(obj.keys)) throw new Error('ORG_KEYRING_KEYS_MISSING');
  return obj;
}

function normalizeTicketTuple(ticket){
  if(!ticket || typeof ticket !== 'object') return null;
  const system = String(ticket.system||'').trim();
  const id = String(ticket.id||'').trim();
  if(!system || !id) return null;
  return { system, id };
}

function isWithinValidityWindow(now, validFromUtc, validToUtc){
  try{
    if(validFromUtc){
      const vf = new Date(validFromUtc);
      if(!isNaN(vf.getTime()) && vf > now) return false;
    }
    if(validToUtc){
      const vt = new Date(validToUtc);
      if(!isNaN(vt.getTime()) && vt < now) return false;
    }
    return true;
  }catch(_e){
    return true;
  }
}

function formatTicketLabel(t){
  if(!t) return '—';
  return `${t.system} ${t.id}`;
}

async function computeOrgApprovalFor(obj, vr){
  const enforce = !!(chkEnforceOrgPolicy && chkEnforceOrgPolicy.checked);

  let orgKr = null;
  try{
    orgKr = await readOrgKeyringFromInput();
  }catch(e){
    const msg = String(e && (e.message||e) || e);
    return { verdict:'FAIL', detail:`Org keyring error: ${msg}` };
  }

  if(!orgKr){
    return { verdict:'HOLD', detail: enforce ? 'Org keyring required (enforcement enabled).' : 'No org keyring loaded.' };
  }

  const policy = orgKr.policy || {};
  const threshold = Number(policy.threshold || 0) || 1;
  const allowedRoles = Array.isArray(policy.allowed_roles) ? policy.allowed_roles.map(x=>String(x)) : [];
  const requireDistinct = (policy.require_distinct_signers !== false);
  const requireTicket = !!policy.require_ticket_binding;

  const index = new Map();
  for(const k of (Array.isArray(orgKr.keys) ? orgKr.keys : [])){
    const jwk = k.public_key_jwk || k.jwk || k.public_key;
    const fp = String(k.public_key_fingerprint_sha256 || '').trim() || (jwk ? await jwkPublicFingerprintSha256(jwk) : '');
    if(!fp) continue;
    index.set(fp, {
      kid: String(k.kid || fp),
      role: String(k.role || ''),
      status: String(k.status || 'active'),
      valid_from_utc: k.valid_from_utc,
      valid_to_utc: k.valid_to_utc
    });
  }

  const now = new Date();
  const resList = Array.isArray(vr && vr.results) ? vr.results : [];
  const matched = [];
  const rejected = [];

  for(const r of resList){
    if(String(r.status||'') !== 'PASS') continue;
    const fp = String(r.fingerprint||'').trim();
    if(!fp){ rejected.push({ reason:'NO_FINGERPRINT' }); continue; }
    const ent = index.get(fp);
    if(!ent){ rejected.push({ fingerprint: fp, reason:'NOT_IN_KEYRING' }); continue; }
    if(String(ent.status||'').toLowerCase() !== 'active'){ rejected.push({ fingerprint: fp, reason:'NOT_ACTIVE' }); continue; }
    if(allowedRoles.length>0 && !allowedRoles.includes(String(ent.role||''))){ rejected.push({ fingerprint: fp, reason:'ROLE_NOT_ALLOWED' }); continue; }
    if(!isWithinValidityWindow(now, ent.valid_from_utc, ent.valid_to_utc)){ rejected.push({ fingerprint: fp, reason:'OUTSIDE_VALIDITY_WINDOW' }); continue; }
    matched.push({ fingerprint: fp, kid: ent.kid, role: ent.role, ticket: normalizeTicketTuple(r.ticket) });
  }

  const counted = [];
  const seen = new Set();
  for(const m of matched){
    if(requireDistinct){
      if(seen.has(m.fingerprint)) continue;
      seen.add(m.fingerprint);
    }
    counted.push(m);
  }

  const count = counted.length;
  const thresholdOk = (count >= threshold);

  let ticketTuple = null;
  let ticketOk = true;

  const tuples = counted.map(m=>m.ticket).filter(Boolean);
  if(requireTicket){
    if(tuples.length !== counted.length){
      ticketOk = false;
    }else if(tuples.length===0){
      ticketOk = false;
    }else{
      const first = tuples[0];
      const allSame = tuples.every(t=>t.system===first.system && t.id===first.id);
      if(!allSame) ticketOk = false;
      else ticketTuple = first;
    }
  }else{
    if(tuples.length>0){
      const first = tuples[0];
      const allSame = tuples.every(t=>t.system===first.system && t.id===first.id);
      ticketTuple = allSame ? first : null;
    }
  }

  const policyLabel = `${threshold}-of-${Math.max(Array.isArray(orgKr.keys)?orgKr.keys.length:0, threshold)}`;
  const matchedLabel = `${count} matched (need ${threshold})`;
  const ticketLabel = ticketTuple ? formatTicketLabel(ticketTuple) : (requireTicket ? 'MISSING/INCONSISTENT' : (tuples.length>0 ? 'INCONSISTENT' : '—'));

  const orgKeyringSha = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(orgKr))));
  const core = compact({
    schema_id: 'mvg.org_approval_certificate@1',
    certificate_version: '1.0.0',
    org_keyring_id: String(orgKr.keyring_id||''),
    org_keyring_sha256: orgKeyringSha,
    policy_id: String(policy.policy_id||''),
    threshold,
    allowed_roles: allowedRoles,
    require_ticket_binding: requireTicket,
    countersigned_receipt_id: String(obj.countersigned_receipt_id||''),
    base_receipt_sha256: String(obj.base_receipt_sha256||''),
    matched_signers: counted.map(m=>({ kid:m.kid, role:m.role, fingerprint:m.fingerprint })),
    ticket: ticketTuple || undefined
  });

  const approvalId = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(core))));
  const verdict = (thresholdOk && ticketOk) ? 'PASS' : 'FAIL';
  const bundleManifestHash = await computeAttachmentBundleManifestHash(approvalId, ticketTuple);

  const certificate = compact({
    ...core,
    generated_utc: new Date().toISOString(),
    org_approval_id: approvalId,
    decision: verdict,
    attachment_bundle_manifest_sha256: bundleManifestHash || undefined,
    notes: [
      'Reviewer-generated certificate. Recomputable offline from the org keyring + countersigned receipt.',
      'This does not assert real-world identity; your org controls key provenance and reviewer assignment.'
    ]
  });

  const extra = [];
  if(vr && vr.overall && vr.overall !== 'PASS'){
    extra.push(`crypto:${vr.overall}`);
  }
  if(rejected.length>0) extra.push(`${rejected.length} rejected`);
  const detail = (verdict==='PASS')
    ? `Threshold satisfied (${matchedLabel})${extra.length?` • ${extra.join(' • ')}`:''}`
    : `Policy not satisfied (${matchedLabel})${requireTicket?` • ticket:${ticketLabel}`:''}${extra.length?` • ${extra.join(' • ')}`:''}`;

  return {
    verdict,
    detail,
    policy_label: policyLabel,
    matched_label: matchedLabel,
    ticket_label: ticketLabel,
    approval_id: approvalId,
    attachment_bundle_manifest_sha256: bundleManifestHash || undefined,
    certificate
  };
}

async function updateOrgApprovalUiFor(obj, vr){
  if(!orgVerdict) return null;
  try{
    const res = await computeOrgApprovalFor(obj, vr);
    if(!res){
      clearOrgApprovalUi();
      return null;
    }

    setOrgVerdict(res.verdict, res.detail || '');
    if(orgPolicy) orgPolicy.textContent = res.policy_label || '—';
    if(orgMatched) orgMatched.textContent = res.matched_label || '—';
    if(orgTicket) orgTicket.textContent = res.ticket_label || '—';
    if(orgApprovalId) orgApprovalId.textContent = res.approval_id || '—';
    if(orgBundleHash) orgBundleHash.textContent = res.attachment_bundle_manifest_sha256 || '—';

    lastOrgApproval = res;

    const enable = (res.verdict === 'PASS' && res.certificate);
    if(btnDownloadOrgApproval) btnDownloadOrgApproval.disabled = !enable;
    if(btnCopyOrgApprovalId) btnCopyOrgApprovalId.disabled = !enable;

    return res;
  }catch(e){
    const msg = String(e && (e.message||e) || e);
    setOrgVerdict('FAIL', msg);
    if(btnDownloadOrgApproval) btnDownloadOrgApproval.disabled = true;
    if(btnCopyOrgApprovalId) btnCopyOrgApprovalId.disabled = true;
    return { verdict:'FAIL', detail: msg };
  }
}

async function downloadOrgApprovalCert(){
  if(!lastOrgApproval || !lastOrgApproval.certificate) return;
  const approvalId = String(lastOrgApproval.approval_id||'').trim() || '';
  const short = shortCodeFromSha256Id(approvalId) || 'org-approval';
  const safe = String(short).replace(/[^A-Za-z0-9-]/g,'') || 'org-approval';
  downloadJson(`MVG_CountersignedApproval_${safe}.json`, lastOrgApproval.certificate);
}

// --- WebAuthn hardware-backed credential management ---
async function setHardwareCredentialBundle(obj){
  if(!obj || typeof obj !== 'object') throw new Error('HARDWARE_CREDENTIAL_INVALID_JSON');
  if(String(obj.schema_id||'') !== 'mvg.webauthn_approval_credential@1') throw new Error('HARDWARE_CREDENTIAL_SCHEMA_MISMATCH');
  if(!obj.credential_id_b64u) throw new Error('HARDWARE_CREDENTIAL_ID_MISSING');
  if(!obj.public_key_jwk) throw new Error('HARDWARE_CREDENTIAL_PUBLIC_KEY_MISSING');
  if(!obj.attestation_object_b64u) throw new Error('HARDWARE_CREDENTIAL_ATTESTATION_OBJECT_MISSING');

  hwCred = obj;
  hwCredIdBytes = b64uToBytes(String(obj.credential_id_b64u));
  hwPublicJwk = obj.public_key_jwk;
  if(!isP256EcJwk(hwPublicJwk, false)) throw new Error('HARDWARE_CREDENTIAL_PUBLIC_KEY_UNSUPPORTED_EXPECT_EC_P256');

  const computedFp = await jwkPublicFingerprintSha256(hwPublicJwk);
  const fp = String(obj.public_key_fingerprint_sha256||'') || computedFp;

  hwEvidence = await summarizeWebAuthnCredentialBundle(obj);

  setText(hwFp, fp || '—');
  setText(hwAttachmentOut, String(obj.authenticator_attachment||'—'));
  setText(hwFmt, hwEvidence?.fmt || '—');
  setText(hwAaguid, hwEvidence?.aaguid || '—');
  setText(hwEvidenceSha, hwEvidence?.evidence_sha256 || '—');
  setText(hwEvidenceVerdict, hwEvidence ? `${hwEvidence.verdict}${hwEvidence.fmt?` (fmt=${hwEvidence.fmt})`:''}` : '—');
  setHwStatus(hwEvidence?.verdict || 'HOLD', hwEvidence?.detail || 'Hardware approval credential loaded.');

  refreshHardwareButtons();
}

async function loadHardwareCredentialFromFile(file){
  const obj = JSON.parse(await file.text());
  await setHardwareCredentialBundle(obj);
}

async function registerHardwareCredential(){
  if(!isWebAuthnSupported()) throw new Error('WEBAUTHN_NOT_SUPPORTED');

  const attachmentSel = String(hwAttachment?.value || 'platform');
  const authenticatorAttachment = attachmentSel === 'any' ? undefined : attachmentSel;
  const uvPolicy = String(hwUv?.value || 'preferred');
  const attPref = String(hwAttestation?.value || 'none');

  const rpId = location.hostname;
  const challenge = crypto.getRandomValues(new Uint8Array(32));
  const userId = crypto.getRandomValues(new Uint8Array(16));
  const uName = String((csEmail && csEmail.value) || 'reviewer@local').trim() || 'reviewer@local';
  const uDisplay = String((csName && csName.value) || 'Reviewer').trim() || 'Reviewer';

  const createOpts = {
    challenge,
    rp: { name: 'Meridian Verity Group', id: rpId },
    user: { id: userId, name: uName, displayName: uDisplay },
    pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
    timeout: 60000,
    attestation: attPref,
    authenticatorSelection: {
      authenticatorAttachment,
      residentKey: 'preferred',
      userVerification: uvPolicy
    }
  };

  const cred = await navigator.credentials.create({ publicKey: createOpts });
  if(!cred) throw new Error('WEBAUTHN_CREATE_FAILED');
  if(!(cred.response && cred.response.attestationObject)) throw new Error('WEBAUTHN_CREATE_NO_ATTESTATION');

  const rawId = new Uint8Array(cred.rawId);
  const attObjBytes = new Uint8Array(cred.response.attestationObject);
  const clientDataBytes = new Uint8Array(cred.response.clientDataJSON);

  const attObj = cborDecodeAll(attObjBytes);
  if(!(attObj instanceof Map)) throw new Error('ATTESTATION_CBOR_NOT_MAP');
  const fmt = String(attObj.get('fmt')||'');
  const authDataBytes = attObj.get('authData');
  if(!(authDataBytes instanceof Uint8Array)) throw new Error('ATTESTATION_AUTHDATA_MISSING');
  const parsed = parseAuthenticatorData(authDataBytes);
  if(!parsed.credentialPublicKey) throw new Error('ATTESTATION_PUBLIC_KEY_MISSING');
  const pubJwk = coseEc2P256ToJwk(parsed.credentialPublicKey);
  const fp = await jwkPublicFingerprintSha256(pubJwk);

  const bundle = compact({
    schema_id: 'mvg.webauthn_approval_credential@1',
    credential_version: '1.0.0',
    generated_utc: new Date().toISOString(),
    rp_id: rpId,
    origin: location.origin,
    authenticator_attachment: cred.authenticatorAttachment || authenticatorAttachment || null,
    user_verification_policy: uvPolicy,
    attestation_conveyance: attPref,
    credential_id_b64u: bytesToB64u(rawId),
    public_key_jwk: pubJwk,
    public_key_fingerprint_sha256: fp,
    attestation_object_b64u: bytesToB64u(attObjBytes),
    client_data_json_b64u: bytesToB64u(clientDataBytes),
    attestation_summary: compact({
      fmt: fmt || null,
      aaguid: parsed.aaguid || null,
      flags: compact({
        user_present: parsed.userPresent,
        user_verified: parsed.userVerified,
        backup_eligible: parsed.backupEligible,
        backup_state: parsed.backupState
      }),
      sign_count: parsed.signCount
    }),
    notes: [
      'Generated locally in-browser. Contains no private key material.',
      'Attestation is provided by the authenticator. Trust evaluation is organization-specific.'
    ]
  });

  await setHardwareCredentialBundle(bundle);
}

function downloadHardwareCredential(){
  if(!hwCred) return;
  const fp = String(hwCred.public_key_fingerprint_sha256||'').replace(/^sha256:/,'');
  const suffix = fp ? fp.slice(0,16) : String(Date.now());
  downloadJson(`MVG_WebAuthn_Approval_Credential_${suffix}.json`, hwCred);
}

async function countersignLatestReceiptWebAuthn(){
  if(!lastReceipt){
    setCsVerdict('HOLD', 'No receipt loaded. Verify a Trust Pack first.');
    return;
  }
  if(!isWebAuthnSupported()){
    setCsVerdict('HOLD', 'WebAuthn is not available in this browser / context.');
    return;
  }
  if(!hwCred || !hwCredIdBytes || !hwPublicJwk){
    setCsVerdict('HOLD', 'Load or register a WebAuthn approval credential first.');
    return;
  }
  if(chkEmbedPub && !chkEmbedPub.checked){
    setCsVerdict('HOLD', 'For WebAuthn countersign, embedding the public key is required (offline-verifiable).');
    return;
  }

  const baseSha = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(lastReceipt))));
  lastBaseReceiptSha = baseSha;
  if(csBaseReceiptSha) csBaseReceiptSha.textContent = baseSha;

  const signer = compact({
    name: String(csName?.value||'').trim() || null,
    email: String(csEmail?.value||'').trim() || null,
    role: String(csRole?.value||'').trim() || null,
    organization: String(csOrg?.value||'').trim() || null
  });

  const ticket = normalizeTicketTuple({
    system: String(csTicketSystem?.value||'').trim(),
    id: String(csTicketId?.value||'').trim()
  });
  let ticketBindingSha = undefined;
  if(ticket){
    ticketBindingSha = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(ticket))));
  }

  const pubFp = String(hwCred.public_key_fingerprint_sha256||'') || (await jwkPublicFingerprintSha256(hwPublicJwk));
  const credSha = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(hwCred))));

  const payload = compact({
    schema_id: 'mvg.receipt.countersign_payload@1',
    signed_utc: new Date().toISOString(),
    algorithm: 'WEBAUTHN_ES256',
    signing_method: 'webauthn',
    base_receipt_sha256: baseSha,
    base_artifact_fingerprint: String(lastReceipt?.artifact?.fingerprint||null),
    base_verification_run_id: String(lastReceipt?.run?.run_id||null),
    public_key_fingerprint_sha256: pubFp,
    public_key_jwk: hwPublicJwk,
    signer,
    ticket,
    ticket_binding_sha256: ticketBindingSha,
    statement: String(csStatement?.value||'').trim() || null,
    webauthn: compact({
      rp_id: String(hwCred.rp_id || location.hostname),
      origin: String(hwCred.origin || location.origin),
      authenticator_attachment: String(hwCred.authenticator_attachment||hwAttachment?.value||''),
      user_verification: String(hwUv?.value || hwCred.user_verification_policy || 'preferred'),
      attestation_fmt: String(hwCred.attestation_summary?.fmt || ''),
      aaguid: String(hwCred.attestation_summary?.aaguid || '')
    }),
    webauthn_credential_sha256: credSha
  });

  const payloadBytes = new TextEncoder().encode(canonicalJson(payload));
  const challengeBytes = await sha256Bytes(payloadBytes);
  const uvPolicy = String(hwUv?.value || hwCred.user_verification_policy || 'preferred');

  const assertion = await navigator.credentials.get({
    publicKey: {
      challenge: challengeBytes,
      timeout: 60000,
      userVerification: uvPolicy,
      rpId: String(hwCred.rp_id || location.hostname),
      allowCredentials: [{ type: 'public-key', id: hwCredIdBytes }]
    }
  });
  if(!assertion) throw new Error('WEBAUTHN_GET_FAILED');

  const resp = assertion.response;
  const authDataB = new Uint8Array(resp.authenticatorData);
  const clientDataB = new Uint8Array(resp.clientDataJSON);
  const sigB = new Uint8Array(resp.signature);

  const wa = compact({
    schema_id: 'mvg.webauthn_assertion@1',
    credential_id_b64u: bytesToB64u(new Uint8Array(assertion.rawId)),
    authenticator_data_b64u: bytesToB64u(authDataB),
    client_data_json_b64u: bytesToB64u(clientDataB),
    user_handle_b64u: resp.userHandle ? bytesToB64u(new Uint8Array(resp.userHandle)) : undefined
  });

  const cs = compact({
    schema_id: 'mvg.receipt.countersignature@1',
    signing_method: 'webauthn',
    payload,
    payload_sha256: 'sha256:' + (await sha256Hex(payloadBytes)),
    signature_b64u: bytesToB64u(sigB),
    webauthn: wa,
    webauthn_credential: (chkHwEmbedCred && chkHwEmbedCred.checked) ? hwCred : undefined
  });

  const next = await makeCountersignedReceipt(lastReceipt, [cs]);
  lastCountersigned = next;
  lastCounterRunId = next?.countersigned_receipt_id || null;
  if(csCount) csCount.textContent = String((next.countersignatures||[]).length);
  if(csSignedId) csSignedId.textContent = String(next.countersigned_receipt_id||'');
  if(btnCsDownloadSigned) btnCsDownloadSigned.disabled = false;
  if(btnCsCopySignedId) btnCsCopySignedId.disabled = false;

  await verifySelectedOrLastCountersigned();
}

async function countersignLatestReceipt(){
  if(!lastReceipt){
    setCsVerdict('HOLD', 'Run verification first to generate a base receipt.');
    return;
  }
  if(!csPrivCryptoKey || !csPubJwk){
    setCsVerdict('HOLD', 'Load a private key (JWK) or generate a demo keypair.');
    return;
  }

  const baseSha = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(lastReceipt))));
  lastBaseReceiptSha = baseSha;
  if(csBaseReceiptSha) csBaseReceiptSha.textContent = baseSha;

  const signer = compact({
    organization: csOrg ? csOrg.value : undefined,
    name: csName ? csName.value : undefined,
    email: csEmail ? csEmail.value : undefined
  }) || {};

  const embed = !!(chkEmbedPub && chkEmbedPub.checked);
  const pubFp = await jwkPublicFingerprintSha256(csPubJwk);

  // Optional: bind approval to a ticket (JIRA / ServiceNow / etc.)
  const bindTicket = !!(chkBindTicket && chkBindTicket.checked);
  const ticketInput = compact({
    system: csTicketSystem ? csTicketSystem.value : undefined,
    id: csTicketId ? csTicketId.value : undefined,
    url: csTicketUrl ? csTicketUrl.value : undefined
  });

  let ticket = undefined;
  let ticketBindingSha = undefined;
  if(bindTicket){
    if(!ticketInput || !ticketInput.id){
      setCsVerdict('HOLD', 'Ticket binding enabled but Ticket ID is missing.');
      return;
    }
    const sys = String(ticketInput.system || 'Other');
    ticket = compact({ system: sys, id: String(ticketInput.id), url: ticketInput.url });
    const tuple = { system: sys, id: String(ticketInput.id) };
    ticketBindingSha = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(tuple))));
  }

  let payload = compact({
    schema_id: 'mvg.receipt.countersign_payload@1',
    signed_utc: new Date().toISOString(),
    algorithm: 'ECDSA_P256_SHA256',
    base_receipt_sha256: baseSha,
    base_artifact_fingerprint: String(lastReceipt.artifact_fingerprint||''),
    base_verification_run_id: String(lastReceipt.verification_run_id||''),
    public_key_fingerprint_sha256: pubFp,
    public_key_jwk: embed ? csPubJwk : undefined,
    signer,
    ticket,
    ticket_binding_sha256: ticketBindingSha,
    statement: csStatement ? csStatement.value : undefined
  });

  const sig = await signPayloadWithLoadedKey(payload);

  const cs = {
    schema_id: 'mvg.receipt.countersignature@1',
    payload,
    payload_sha256: sig.payload_sha256,
    signature_b64u: sig.signature_b64u
  };

  let next = lastCountersigned;
  if(!next || String(next.base_receipt_sha256||'') !== baseSha){
    next = await makeCountersignedReceipt(lastReceipt, []);
  }
  next.countersignatures = Array.isArray(next.countersignatures) ? next.countersignatures : [];
  next.countersignatures.push(cs);

  const idCore = {
    schema_id: next.schema_id,
    base_receipt_sha256: next.base_receipt_sha256,
    countersignatures: next.countersignatures.map(x=>({payload_sha256:x.payload_sha256, signature_b64u:x.signature_b64u}))
  };
  next.countersigned_receipt_id = 'sha256:' + (await sha256Hex(new TextEncoder().encode(canonicalJson(idCore))));

  lastCountersigned = next;

  if(csSignedId) csSignedId.textContent = String(next.countersigned_receipt_id||'—');
  if(csCount) csCount.textContent = String((next.countersignatures||[]).length);
  if(btnCsDownloadSigned) btnCsDownloadSigned.disabled = false;

  const vr = await verifyCountersignedReceipt(next);
  lastCountersignedVerification = vr;
  const or = await updateOrgApprovalUiFor(next, vr);

  const enforce = !!(chkEnforceOrgPolicy && chkEnforceOrgPolicy.checked);
  if(enforce){
    const orgV = String(or && or.verdict || 'HOLD');
    let overall = 'PASS';
    if(String(vr.overall||'') === 'FAIL' || orgV === 'FAIL') overall = 'FAIL';
    else if(String(vr.overall||'') !== 'PASS' || orgV !== 'PASS') overall = 'HOLD';
    setCsVerdict(overall, `${vr.detail} • org:${orgV}`);
  }else{
    setCsVerdict(vr.overall, vr.detail);
  }
}

async function verifySelectedOrLastCountersigned(){
  let obj = lastCountersigned;
  const f = csSignedReceiptFile?.files?.[0];
  if(f){
    try{
      obj = JSON.parse(await f.text());
    }catch(e){
      setCsVerdict('FAIL', 'Invalid JSON.');
      return;
    }
  }
  if(!obj){
    setCsVerdict('HOLD', 'No countersigned receipt in memory; select a file to verify.');
    return;
  }
  try{
    const vr = await verifyCountersignedReceipt(obj);
    lastCountersignedVerification = vr;
    const or = await updateOrgApprovalUiFor(obj, vr);

    const enforce = !!(chkEnforceOrgPolicy && chkEnforceOrgPolicy.checked);
    if(enforce){
      const orgV = String(or && or.verdict || 'HOLD');
      let overall = 'PASS';
      if(String(vr.overall||'') === 'FAIL' || orgV === 'FAIL') overall = 'FAIL';
      else if(String(vr.overall||'') !== 'PASS' || orgV !== 'PASS') overall = 'HOLD';
      setCsVerdict(overall, `${vr.detail} • org:${orgV}`);
    }else{
      setCsVerdict(vr.overall, vr.detail);
    }

    if(csSignedId) csSignedId.textContent = String(obj.countersigned_receipt_id||'—');
    if(csBaseReceiptSha) csBaseReceiptSha.textContent = String(obj.base_receipt_sha256||'—');
    if(csCount) csCount.textContent = String((obj.countersignatures||[]).length);
  }catch(e){
    setCsVerdict('FAIL', String(e.message||e));
  }
}

// Wire UI
csPrivKeyFile?.addEventListener('change', async ()=>{
  try{
    const f = csPrivKeyFile.files && csPrivKeyFile.files[0];
    if(!f) return;
    await loadPrivateKeyFromFile(f);
  }catch(e){
    setCsKeyStatus('FAIL', String(e.message||e));
  }
});

btnCsGenKey?.addEventListener('click', async ()=>{
  try{
    await generateDemoKeypair();
  }catch(e){
    setCsKeyStatus('FAIL', String(e.message||e));
  }
});

btnCsClearKey?.addEventListener('click', ()=>{
  csPrivCryptoKey = null;
  csPrivJwk = null;
  csPubJwk = null;
  if(csKeyFp) csKeyFp.textContent='—';
  setCsKeyStatus('—','');
  if(btnCsDownloadPub) btnCsDownloadPub.disabled = true;
  if(btnCsDownloadPriv) btnCsDownloadPriv.disabled = true;
});

btnCsDownloadPub?.addEventListener('click', async ()=>{
  try{
    if(!csPubJwk) return;
    const fp = (await jwkPublicFingerprintSha256(csPubJwk)).replace(/^sha256:/,'').slice(0,16);
    downloadKey(`MVG_Receipt_Countersign_PublicKey_${fp}.json`, csPubJwk);
  }catch(e){ /* ignore */ }
});

btnCsDownloadPriv?.addEventListener('click', async ()=>{
  try{
    if(!csPrivJwk) return;
    const pub = publicFromPrivateJwk(csPrivJwk);
    const fp = (await jwkPublicFingerprintSha256(pub)).replace(/^sha256:/,'').slice(0,16);
    downloadKey(`MVG_Receipt_Countersign_PrivateKey_${fp}.json`, csPrivJwk);
  }catch(e){ /* ignore */ }
});

btnCsSign?.addEventListener('click', async ()=>{
  try{
    await countersignLatestReceipt();
  }catch(e){
    setCsVerdict('FAIL', String(e.message||e));
  }
});

btnCsDownloadSigned?.addEventListener('click', ()=>{
  if(!lastCountersigned) return;
  const fp = String(lastCountersigned?.base_receipt?.artifact_fingerprint||'').replace(/^sha256:/,'').slice(0,16);
  const name = `MVG_Countersigned_Verification_Receipt_TrustPack_${fp||'receipt'}.json`;
  downloadJson(name, lastCountersigned);
  if(csDetail){
    csDetail.textContent = 'Downloaded';
    setTimeout(()=>{ if(csDetail) csDetail.textContent=''; }, 1200);
  }
});

btnCsVerifySigned?.addEventListener('click', async ()=>{
  await verifySelectedOrLastCountersigned();
});

// ---------------------
// Printable audit summary (HTML → PDF print)
// ---------------------
function escapeHtml(s){
  return String(s ?? '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}
function normVerdict(v){
  const x = String(v ?? '').trim().toUpperCase();
  if(x === 'PASS' || x === 'FAIL' || x === 'HOLD') return x;
  if(x === '—' || x === '') return '—';
  return x;
}
function combineVerdicts(a, b){
  const A = normVerdict(a);
  const B = normVerdict(b);
  if(A === 'FAIL' || B === 'FAIL') return 'FAIL';
  if(A === 'HOLD' || B === 'HOLD') return 'HOLD';
  if(A === 'PASS' && B === 'PASS') return 'PASS';
  if(A === '—') return B;
  if(B === '—') return A;
  return 'HOLD';
}
function shortSha(s){
  const t = String(s ?? '').trim();
  if(!t) return '—';
  const core = t.replace(/^sha256:/,'');
  return core.length > 20 ? `${core.slice(0,16)}…` : core;
}
function collectReviewers(csObj){
  const out = [];
  if(!csObj || !Array.isArray(csObj.countersignatures)) return out;
  for(const cs of csObj.countersignatures){
    const p = (cs && cs.payload) ? cs.payload : {};
    const signer = (p && p.signer) ? p.signer : {};
    const ticket = p && p.ticket ? p.ticket : null;
    out.push({
      signed_utc: p.signed_utc || '',
      method: p.signing_method || cs.signing_method || 'jwk',
      organization: signer.organization || '',
      name: signer.name || '',
      email: signer.email || '',
      key_fp: p.public_key_fingerprint_sha256 || '',
      ticket: ticket ? `${ticket.system || 'Other'}:${ticket.id || ''}` : ''
    });
  }
  return out;
}
function deriveTicketLabel(reviewers){
  const orgTicketLabel = String(orgTicket?.textContent || lastOrgApproval?.ticket_label || '').trim();
  if(orgTicketLabel && orgTicketLabel !== '—') return orgTicketLabel;
  const set = new Set((reviewers||[]).map(r=>String(r.ticket||'').trim()).filter(Boolean));
  if(set.size === 0) return '—';
  if(set.size === 1) return Array.from(set)[0];
  return Array.from(set).join(', ');
}

async function renderAndPrintAuditSummary(){
  if(!printArea) return;
  if(!lastReceipt){
    setCsVerdict('HOLD', 'Run verification first to generate a receipt.');
    return;
  }

  // Ensure we have the latest countersign verification result (offline).
  let csVr = lastCountersignedVerification;
  if(lastCountersigned){
    const need = !csVr || String(csVr.base_receipt_sha256||'') !== String(lastCountersigned.base_receipt_sha256||'');
    if(need){
      try{
        csVr = await verifyCountersignedReceipt(lastCountersigned);
        lastCountersignedVerification = csVr;
      }catch(e){
        csVr = { overall: 'FAIL', detail: String(e && (e.message||e) || e) };
      }
    }
  }

  const nowUtc = new Date().toISOString();
  const context = (document.querySelector('h1')?.textContent || 'Verification').trim();

  const baseOverall = normVerdict(lastReceipt?.verdicts?.overall || verdictEl?.textContent || '—');
  const baseDetail = String(detailEl?.textContent || '').trim();

  const csOverall = lastCountersigned ? normVerdict(csVr?.overall || csVerdict?.textContent || '—') : '—';
  const csDetailText = lastCountersigned ? String(csVr?.detail || csDetail?.textContent || '').trim() : '';

  const orgOverall = normVerdict(lastOrgApproval?.verdict || orgVerdict?.textContent || '—');
  const orgDetailText = String(lastOrgApproval?.detail || orgDetail?.textContent || '').trim();

  const enforceOrg = !!(chkEnforceOrgPolicy && chkEnforceOrgPolicy.checked);

  let final = baseOverall;
  if(lastCountersigned) final = combineVerdicts(final, csOverall);
  if(enforceOrg) final = combineVerdicts(final, orgOverall === '—' ? 'HOLD' : orgOverall);

  const reviewers = collectReviewers(lastCountersigned);
  const ticketLabel = deriveTicketLabel(reviewers);

  const policyLabel = String(orgPolicy?.textContent || lastOrgApproval?.policy_label || '').trim() || '—';
  const matchedLabel = String(orgMatched?.textContent || lastOrgApproval?.matched_label || '').trim() || '—';
  const approvalId = String(orgApprovalId?.textContent || lastOrgApproval?.approval_id || '').trim() || '—';
const orgKeyringSha = String(lastOrgApproval?.certificate?.org_keyring_sha256 || '').trim() || '—';
const approvalShort = shortCodeFromSha256Id(approvalId);
const origin = (location && location.origin && location.origin !== 'null') ? location.origin : '';
const reverifyUrl = origin ? (origin + location.pathname) : location.pathname;

const includeTicketInQr = !chkQrIncludeTicket || !!chkQrIncludeTicket.checked;
let ticketForQr = '';
if(includeTicketInQr && ticketLabel && ticketLabel !== '—'){
  const raw = String(ticketLabel).trim();
  const first = raw.split(',')[0].trim();
  const base = first.length > 42 ? (first.slice(0,40) + '…') : first;
  ticketForQr = raw.includes(',') ? (base + '…') : base;
}
const ticketLine = ticketForQr ? `\nTicket: ${ticketForQr}` : '';

const qrPayload = (approvalId && approvalId !== '—')
  ? `MVG org approval id: ${approvalId}\nShort: ${approvalShort}${ticketLine}\nRe-verify: ${reverifyUrl}`
  : `MVG audit summary\nRe-verify: ${reverifyUrl}`;

const qrSvgHtml = (approvalId && approvalId !== '—')
  ? qrSvg(qrPayload, { size: 104, margin: 4, ecl: 'M' })
  : '';

  const rowsHtml = reviewers.length ? reviewers.map(r=>{
    const who = [r.organization, r.name].filter(Boolean).join(' / ') || (r.email || '—');
    const whoLine = `${escapeHtml(who)}${r.email ? `<br><span class="muted">${escapeHtml(r.email)}</span>` : ''}`;
    return `
      <tr>
        <td class="mono">${escapeHtml(String(r.signed_utc||'—').replace('T',' ').replace('Z','Z'))}</td>
        <td>${whoLine}</td>
        <td class="mono">${escapeHtml(r.method || '—')}</td>
        <td class="mono">${escapeHtml(shortSha(r.key_fp || '—'))}</td>
        <td class="mono">${escapeHtml(r.ticket || '—')}</td>
      </tr>`;
  }).join('') : `<tr><td colspan="5" class="muted">No countersignatures loaded.</td></tr>`;

  const computedCommitment = document.getElementById('outCommitment')?.textContent || '';

  printArea.innerHTML = `
    <div style="font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;">
<div class="muted" style="border:1px solid #d7d7d7; padding:6px 8px; border-radius:12px; margin-bottom:10px; font-size:10.5pt;">
  <b>Attachment cover:</b> Reviewer-generated audit summary for internal procurement/security/legal workflows.<br/>
  <span class="muted">Suggested filename:</span> <span class="mono">MVG_AuditSummary_${escapeHtml(String(approvalShort || '—'))}.pdf</span> Non‑binding unless signed; no patent license by publication; not legal advice; not a compliance certification.
</div>
      <div style="display:flex; justify-content:space-between; align-items:flex-start; gap:16px;">
        <div>
          <h1>${escapeHtml(context)} — Audit summary</h1>
          <div class="muted">Generated (UTC): <span class="mono">${escapeHtml(nowUtc)}</span> • Offline verifier (no uploads, no network calls)</div>
          ${ticketLabel !== '—' ? `<div class="muted" style="margin-top:4px">Ticket: <span class="mono">${escapeHtml(ticketLabel)}</span></div>` : ''}
        </div>
        <div style="text-align:right">
    <div class="pill"><b>FINAL: ${escapeHtml(final)}</b></div>
    ${qrSvgHtml ? `
      <div style="margin-top:10px; display:inline-block; border:1px solid #d7d7d7; padding:8px; border-radius:12px;">
        <div class="muted" style="font-size:9pt; margin-bottom:4px">Approval short code</div>
        <div class="mono" style="font-size:10pt; margin-bottom:6px">${escapeHtml(String(approvalShort || '—'))}</div>
        <div style="line-height:0">${qrSvgHtml}</div>
        <div class="muted" style="font-size:8.8pt; margin-top:6px">Approval ID is deterministic and recomputable offline; scan/copy to re‑verify.</div>
      </div>` : ``}
  </div>
      </div>

      <h2>Verdicts</h2>
      <table>
        <tbody>
          <tr><th style="width:34%">Verification</th><td><b>${escapeHtml(baseOverall)}</b>${baseDetail ? ` — ${escapeHtml(baseDetail)}` : ''}</td></tr>
          <tr><th>Countersigned receipt</th><td>${lastCountersigned ? `<b>${escapeHtml(csOverall)}</b>${csDetailText ? ` — ${escapeHtml(csDetailText)}` : ''}` : '<span class="muted">— (not provided)</span>'}</td></tr>
          <tr><th>Org threshold policy</th><td><b>${escapeHtml(orgOverall)}</b>${orgDetailText ? ` — ${escapeHtml(orgDetailText)}` : ''}<div class="muted" style="margin-top:6px">Policy: <span class="mono">${escapeHtml(policyLabel)}</span> • Matched: <span class="mono">${escapeHtml(matchedLabel)}</span></div></td></tr>
          <tr><th>Fail‑closed enforcement</th><td>${enforceOrg ? '<b>ENABLED</b> (missing/invalid approvals ⇒ HOLD/FAIL)' : '<span class="muted">Disabled</span> (informational)'}</td></tr>
        </tbody>
      </table>

      <h2>Identifiers</h2>
      <table>
        <tbody>
          <tr><th style="width:34%">Artifact fingerprint</th><td class="mono">${escapeHtml(String(lastReceipt.artifact_fingerprint || '—'))}</td></tr>
          <tr><th>Verification run id</th><td class="mono">${escapeHtml(String(lastReceipt.verification_run_id || '—'))}</td></tr>
          <tr><th>Pack sha256</th><td class="mono">${escapeHtml(String(outSha?.textContent || '—'))}</td></tr>
          <tr><th>Pack bytes</th><td class="mono">${escapeHtml(String(outBytes?.textContent || '—'))}</td></tr>
          ${computedCommitment && computedCommitment.trim() !== '—' ? `<tr><th>Computed commitment</th><td class="mono">${escapeHtml(String(computedCommitment))}</td></tr>` : ''}
          <tr><th>Countersigned receipt id</th><td class="mono">${escapeHtml(String(lastCountersigned?.countersigned_receipt_id || '—'))}</td></tr>
          <tr><th>Org approval id</th><td class="mono">${escapeHtml(String(approvalId))}</td></tr>
          <tr><th>Org keyring snapshot sha256</th><td class="mono">${escapeHtml(String(orgKeyringSha))}</td></tr>
          <tr><th>Attachment bundle manifest sha256</th><td class="mono">${escapeHtml(String(lastOrgApproval?.attachment_bundle_manifest_sha256 || '—'))}</td></tr>
          <tr><th>Approval short code</th><td class="mono">${escapeHtml(String(approvalShort || '—'))}</td></tr>
        </tbody>
      </table>

      <h2>Reviewers</h2>
      <table>
        <thead>
          <tr>
            <th style="width:22%">Signed (UTC)</th>
            <th>Reviewer</th>
            <th style="width:12%">Method</th>
            <th style="width:16%">Key fp</th>
            <th style="width:18%">Ticket</th>
          </tr>
        </thead>
        <tbody>
          ${rowsHtml}
        </tbody>
      </table>

      <h2>Boundary conditions</h2>
      <ul class="muted" style="margin:0; padding-left: 18px;">
        <li>Reviewer‑generated summary. Intended for internal procurement/security/legal workflows.</li>
        <li>Non‑binding unless signed. Not legal advice. Not a compliance certification.</li>
        <li>No patent license by publication.</li>
      </ul>
    </div>
  `;

  // Print once the DOM is updated.
  await new Promise((r)=>setTimeout(r, 0));
  window.addEventListener('afterprint', ()=>{ if(printArea) printArea.innerHTML = ''; }, { once: true });
  window.print();
}

btnPrintSummary?.addEventListener('click', async ()=>{
  try{
    await renderAndPrintAuditSummary();
  }catch(e){
    setCsVerdict('FAIL', `Print summary failed: ${String(e && (e.message||e) || e)}`);
  }
});

// WebAuthn (hardware-backed) countersign wiring
hwCredFile?.addEventListener('change', async ()=>{
  try{
    const f = hwCredFile.files && hwCredFile.files[0];
    if(!f) return;
    await loadHardwareCredentialFromFile(f);
  }catch(e){
    clearHardwareKey();
    setHwStatus('FAIL', String(e.message||e));
  }
});

btnHwRegister?.addEventListener('click', async ()=>{
  try{
    await registerHardwareCredential();
  }catch(e){
    setHwStatus('HOLD', String(e.message||e));
  }
});

btnHwDownloadCred?.addEventListener('click', ()=>{
  downloadHardwareCredential();
});

btnHwClear?.addEventListener('click', ()=>{
  clearHardwareKey();
  refreshHardwareButtons();
});

btnHwSign?.addEventListener('click', async ()=>{
  try{
    await countersignLatestReceiptWebAuthn();
  }catch(e){
    setCsVerdict('HOLD', String(e.message||e));
  }
});

// Org approval policy wiring
orgKeyringFile?.addEventListener('change', async ()=>{
  try{
    if(!lastCountersigned){
      clearOrgApprovalUi();
      return;
    }
    const vr = await verifyCountersignedReceipt(lastCountersigned);
    lastCountersignedVerification = vr;
    await updateOrgApprovalUiFor(lastCountersigned, vr);
  }catch(e){
    setOrgVerdict('FAIL', String(e.message||e));
  }
});

chkEnforceOrgPolicy?.addEventListener('change', async ()=>{
  try{
    if(!lastCountersigned){
      clearOrgApprovalUi();
      return;
    }
    const vr = await verifyCountersignedReceipt(lastCountersigned);
    lastCountersignedVerification = vr;
    await updateOrgApprovalUiFor(lastCountersigned, vr);
  }catch(e){
    setOrgVerdict('FAIL', String(e.message||e));
  }
});

btnDownloadOrgApproval?.addEventListener('click', ()=>{
  downloadOrgApprovalCert();
});

btnCopyOrgApprovalId?.addEventListener('click', async ()=>{
  try{
    const id = String(lastOrgApproval?.approval_id||'');
    if(!id) return;
    await navigator.clipboard.writeText(id);
    if(orgDetail){
      const prev = orgDetail.textContent;
      orgDetail.textContent = 'Copied';
      setTimeout(()=>{ if(orgDetail) orgDetail.textContent = prev; }, 900);
    }
  }catch(_e){ /* ignore */ }
});
