'use strict';

import { sampleAssurancePackB64u } from './sampleAssurancePackB64u.js';
import { sampleAssurancePackDsse } from './sampleAssurancePackDsse.js';
import { sampleAssuranceKeyringDsse } from './sampleAssuranceKeyringDsse.js';
import { sampleAssuranceKeyTransparencyLogDsse } from './sampleAssuranceKeyTransparencyLogDsse.js';
import { qrSvg, shortCodeFromSha256Id } from '../verify/qr.js';

const ASSURANCE = {
  schema_id: 'mvg.assurance@1',
  pack_version: '1.0.0',
  pack_href: '/downloads/MVG_Assurance_Pack_PUBLIC_v1.0.0.zip',
  pack_sig_href: '/downloads/MVG_Assurance_Pack_PUBLIC_v1.0.0.dsse.json',
  keyring_dsse_href: '/downloads/MVG_Assurance_Keyring_PUBLIC_v1.0.0.dsse.json',
  keyring_trust_anchors_href: '/downloads/MVG_Assurance_Keyring_TrustAnchors_PUBLIC_v1.0.0.json',
  key_transparency_log_dsse_href: '/downloads/MVG_Assurance_KeyTransparency_Log_PUBLIC_v1.0.0.dsse.json'
};

// Pinned trust anchors for verifying the *keyring snapshot* DSSE (and public demo log).
// (Public-safe demo keys; replace with production trust anchors.)
const KEYRING_TRUST_ANCHORS = {
  schema_id: 'mvg.assurance.keyring_trust_anchors@1',
  generated_utc: '2026-02-11T00:00:00Z',
  bootstrap: {
    kid: 'ta.mvg.assurance.bootstrap.2026-02-11.v1.0.0.demo',
    jwk: {"kty": "OKP", "crv": "Ed25519", "x": "LYOCM33bJ8kGawDSM38Ekh8Av52HaTbTQQ9Qgckk-hM"}
  },
  witness_threshold: 2,
  witnesses: [
    {
      kid: 'ta.mvg.assurance.keyring.witness.1.2026-02-11.v1.0.0.demo',
      jwk: {"kty": "OKP", "crv": "Ed25519", "x": "WdE-fCrtDBQ4zaMmDZ1l5KAItxRR2l8qdzlo8j_uzBM"}
    },
    {
      kid: 'ta.mvg.assurance.keyring.witness.2.2026-02-11.v1.0.0.demo',
      jwk: {"kty": "OKP", "crv": "Ed25519", "x": "VasohPelMZNUPZmCI34CA_P0f9e6Rsyldt--bEGP-4Q"}
    }
  ]
};

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

// Buttons
const btnSample = $('btnVerifySample');
const btnSelected = $('btnVerifySelected');
const btnCopyContents = $('btnCopyContents');
const btnDownloadReceipt = $('btnDownloadReceipt');
const btnCopyReceiptId = $('btnCopyReceiptId');
const btnPrintSummary = $('btnPrintSummary');
const printArea = $('printArea');
const chkQrIncludeTicket = $('chkQrIncludeTicket');

// Inputs
const zipInput = $('zipFile');
const sigInput = $('sigFile');
const keyringInput = $('keyringFile');
const logInput = $('logFile');
const chkTransparency = $('chkTransparency');

// Outputs
const verdictEl = $('verdict');
const detailEl = $('detail');

const outSha = $('outSha');
const outBytes = $('outBytes');
const outAirgap = $('outAirgap');

const outKeyring = $('outKeyring');
const outKeyringVer = $('outKeyringVer');
const outSig = $('outSig');

const outLog = $('outLog');
const outLogHead = $('outLogHead');
const outLogConsistency = $('outLogConsistency');

const outReceiptId = $('outReceiptId');
const outRunId = $('outRunId');
const receiptHint = $('receiptHint');

const contentsVerdict = $('contentsVerdict');
const contentsDetail = $('contentsDetail');
const contentsRows = $('contentsRows');

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;        // parsed evidence summary

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: '' };

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

  // XHR
  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'); }

  // WebSocket
  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'); }

  // EventSource
  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'); }

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

  // Hard recommendation: serve this page with CSP connect-src 'none'.
  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();
if(outAirgap){
  if(AIRGAP.unable.length===0){
    outAirgap.textContent = 'PASS: ' + AIRGAP.blocked.join(', ');
  } else {
    outAirgap.textContent = 'HOLD: ' + AIRGAP.note;
  }
}

// ---------------------
// UI helpers
// ---------------------
function setVerdict(v, msg){
  verdictEl.textContent=v;
  verdictEl.className='tag '+(v==='PASS'?'ok':(v==='HOLD'?'hold':(v==='FAIL'?'deny':'neutral')));
  detailEl.textContent=msg||'';
}
function setContentsVerdict(v, msg){
  contentsVerdict.textContent=v;
  contentsVerdict.className='tag '+(v==='PASS'?'ok':(v==='HOLD'?'hold':(v==='FAIL'?'deny':'neutral')));
  contentsDetail.textContent=msg||'';
}

function clearReceipt(){
  lastReceipt = null;
  if(outReceiptId) outReceiptId.textContent='—';
  if(outRunId) outRunId.textContent='—';
  if(receiptHint) receiptHint.textContent='';
  if(btnDownloadReceipt) btnDownloadReceipt.disabled = true;
  if(btnCopyReceiptId) btnCopyReceiptId.disabled = true;
  clearCountersign();
  refreshHardwareButtons();
}

function enableReceipt(){
  if(btnDownloadReceipt) btnDownloadReceipt.disabled = false;
  if(btnCopyReceiptId) btnCopyReceiptId.disabled = false;
  refreshHardwareButtons();
}

function clearContents(){
  contentsRows.innerHTML='';
}
function addContentsRow(file, exp, got, ok){
  const tr=document.createElement('tr');

  const td1=document.createElement('td'); td1.className='mono'; td1.textContent=String(file ?? ''); tr.appendChild(td1);
  const td2=document.createElement('td'); td2.className='mono'; td2.textContent=String(exp ?? ''); tr.appendChild(td2);
  const td3=document.createElement('td'); td3.className='mono'; td3.textContent=String(got ?? ''); tr.appendChild(td3);

  const td4=document.createElement('td');
  const tag=document.createElement('span');
  tag.className='tag ' + (ok ? 'ok' : 'deny');
  tag.textContent = ok ? 'PASS' : 'FAIL';
  td4.appendChild(tag);
  tr.appendChild(td4);

  contentsRows.appendChild(tr);
}

// ---------------------
// Bytes / crypto helpers
// ---------------------
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(u8){
  let out='';
  for(let i=0;i<u8.length;i++) out+=u8[i].toString(16).padStart(2,'0');
  return out;
}
function utf8(s){ return new TextEncoder().encode(String(s)); }
async function sha256Hex(u8){
  const ab=u8.buffer.slice(u8.byteOffset, u8.byteOffset+u8.byteLength);
  const dig=await crypto.subtle.digest('SHA-256', ab);
  return bytesToHex(new Uint8Array(dig));
}

async function sha256Bytes(u8){
  const ab=u8.buffer.slice(u8.byteOffset, u8.byteOffset+u8.byteLength);
  const dig=await crypto.subtle.digest('SHA-256', ab);
  return new Uint8Array(dig);
}

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

// Canonical JSON (recursive key sort) for deterministic hashes.
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(utf8(canonicalJson(basis)));
  return 'sha256:' + hex;
}

// DSSE PAE
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 importEd25519Public(jwk){
  return crypto.subtle.importKey('jwk', jwk, {name:'Ed25519'}, false, ['verify']);
}
async function verifyEd25519(jwk, msgBytes, sigB64u){
  const key = await importEd25519Public(jwk);
  const sig = b64uToBytes(sigB64u);
  return crypto.subtle.verify({name:'Ed25519'}, key, sig, msgBytes);
}

function parseUtcMs(s){
  const t = Date.parse(String(s||''));
  return Number.isFinite(t) ? t : NaN;
}
function isKeyValidAt(key, tMs){
  if(!key) return false;
  const status = String(key.status||'').toLowerCase();
  if(status==='revoked') return false;
  const vf = parseUtcMs(key.valid_from_utc);
  if(Number.isFinite(vf) && tMs < vf) return false;
  const vt = key.valid_to_utc ? parseUtcMs(key.valid_to_utc) : NaN;
  if(Number.isFinite(vt) && tMs > vt) return false;
  return true;
}

async function verifyDsse(dsseEnv, expectedPayloadType, pubKeysByKid, witnessKids, witnessThreshold){
  if(!dsseEnv || dsseEnv.payloadType!==expectedPayloadType) throw new Error('DSSE_PAYLOAD_TYPE_MISMATCH');
  const payloadBytes = b64uToBytes(dsseEnv.payload);
  const pae = dssePAE(dsseEnv.payloadType, payloadBytes);
  const sigs = Array.isArray(dsseEnv.signatures)? dsseEnv.signatures : [];
  if(sigs.length===0) throw new Error('DSSE_NO_SIGNATURES');

  let okCount=0;
  let witnessOk=0;
  const okKids = new Set();

  for(const s of sigs){
    const kid = s.keyid;
    const jwk = pubKeysByKid[kid];
    if(!jwk) continue;
    const ok = await verifyEd25519(jwk, pae, s.sig);
    if(ok) {
      okCount++;
      okKids.add(kid);
      if(witnessKids.includes(kid)) witnessOk++;
    }
  }

  if(okCount===0) throw new Error('DSSE_SIG_INVALID');

  const wth = Number(dsseEnv?.ext?.witness_threshold ?? witnessThreshold ?? 0);
  if(wth>0 && witnessKids.length>0 && witnessOk < wth) {
    return { ok:false, hold:true, reason:('WITNESS_THRESHOLD_'+wth+'_NOT_MET'), payloadBytes, okKids, witnessOk };
  }

  return { ok:true, hold:false, reason:'OK', payloadBytes, okKids, witnessOk };
}

// ---------------------
// Keyring verification
// ---------------------
async function verifyKeyringSnapshot(keyringDsseEnv){
  const pubKeys={};
  pubKeys[KEYRING_TRUST_ANCHORS.bootstrap.kid]=KEYRING_TRUST_ANCHORS.bootstrap.jwk;
  for(const w of KEYRING_TRUST_ANCHORS.witnesses) pubKeys[w.kid]=w.jwk;
  const witnessKids = KEYRING_TRUST_ANCHORS.witnesses.map(w=>w.kid);

  const res = await verifyDsse(
    keyringDsseEnv,
    'application/vnd.mvg.assurance_keyring+json',
    pubKeys,
    witnessKids,
    KEYRING_TRUST_ANCHORS.witness_threshold
  );

  if(res.hold) return { ok:false, hold:true, reason:res.reason };

  // Require bootstrap signature (root of trust)
  if(!res.okKids.has(KEYRING_TRUST_ANCHORS.bootstrap.kid)) {
    return { ok:false, hold:false, reason:'BOOTSTRAP_SIGNATURE_MISSING' };
  }

  let keyring;
  try {
    keyring = JSON.parse(new TextDecoder().decode(res.payloadBytes));
  } catch(e) {
    return { ok:false, hold:false, reason:'KEYRING_PAYLOAD_INVALID_JSON' };
  }

  // Build lookups for pack verification
  const metaByKid={};
  const pubByKid={};
  const keys = Array.isArray(keyring.keys) ? keyring.keys : [];
  for(const k of keys) {
    if(k && k.kid && k.jwk) {
      metaByKid[k.kid]=k;
      pubByKid[k.kid]=k.jwk;
    }
  }

  const packWitnessKids = keys.filter(k=>String(k.role||'')==='pack.witness').map(k=>k.kid);
  const packWitnessThreshold = Number(keyring?.pack_signature_policy?.witness_threshold ?? 0);

  return {
    ok:true,
    hold:false,
    reason:'OK',
    keyring,
    pubByKid,
    metaByKid,
    packWitnessKids,
    packWitnessThreshold,
    keyring_payload_sha256: await sha256Hex(res.payloadBytes),
    keyring_payload_bytes: res.payloadBytes.byteLength,
    verified_signing_kids: Array.from(res.okKids)
  };
}

// ---------------------
// Transparency log verification (optional)
// ---------------------
async function computeEntrySha(entry){
  const e = {};
  for(const k of Object.keys(entry||{}).sort()){
    if(k==='entry_sha256') continue;
    e[k]=entry[k];
  }
  const s = JSON.stringify(e, null, 0);
  return await sha256Hex(utf8(s));
}

async function verifyTransparencyLog(logDsseEnv){
  const pubKeys={};
  pubKeys[KEYRING_TRUST_ANCHORS.bootstrap.kid]=KEYRING_TRUST_ANCHORS.bootstrap.jwk;
  for(const w of KEYRING_TRUST_ANCHORS.witnesses) pubKeys[w.kid]=w.jwk;
  const witnessKids = KEYRING_TRUST_ANCHORS.witnesses.map(w=>w.kid);

  const res = await verifyDsse(
    logDsseEnv,
    'application/vnd.mvg.assurance_key_transparency_log+json',
    pubKeys,
    witnessKids,
    KEYRING_TRUST_ANCHORS.witness_threshold
  );

  if(res.hold) return { ok:false, hold:true, reason:res.reason };

  // Require bootstrap signature
  if(!res.okKids.has(KEYRING_TRUST_ANCHORS.bootstrap.kid)) {
    return { ok:false, hold:false, reason:'BOOTSTRAP_SIGNATURE_MISSING' };
  }

  let log;
  try {
    log = JSON.parse(new TextDecoder().decode(res.payloadBytes));
  } catch(e) {
    return { ok:false, hold:false, reason:'LOG_PAYLOAD_INVALID_JSON' };
  }

  const logPayloadSha = await sha256Hex(res.payloadBytes);
  const logPayloadBytes = res.payloadBytes.byteLength;

  // Basic structure checks
  if(String(log.schema_id||'') !== 'mvg.assurance.key_transparency_log@1') {
    return { ok:false, hold:false, reason:'LOG_SCHEMA_ID_MISMATCH' };
  }

  const head = log.head || {};
  const chainStart = String(head.chain_start_sha256||'').toLowerCase();
  const headSeq = Number(head.head_seq||0);
  const headEntry = String(head.head_entry_sha256||'').toLowerCase();

  const entriesRaw = Array.isArray(log.entries) ? log.entries : [];
  const entries = entriesRaw.slice().sort((a,b)=>Number(a.seq||0)-Number(b.seq||0));

  if(headSeq<=0 || entries.length===0) return { ok:false, hold:false, reason:'LOG_EMPTY' };
  if(entries[0].seq !== 1) return { ok:false, hold:false, reason:'LOG_SEQ_NOT_START_1' };
  if(entries[entries.length-1].seq !== headSeq) return { ok:false, hold:false, reason:'LOG_HEAD_SEQ_MISMATCH' };

  // Verify chain + entry hashes
  let prev = chainStart;
  for(const entry of entries){
    const seq = Number(entry.seq||0);
    if(seq<=0) return { ok:false, hold:false, reason:'LOG_BAD_SEQ' };
    if(String(entry.prev_entry_sha256||'').toLowerCase() !== prev) {
      return { ok:false, hold:false, reason:'LOG_CHAIN_BROKEN_AT_'+String(seq) };
    }
    const computed = (await computeEntrySha(entry)).toLowerCase();
    const claimed = String(entry.entry_sha256||'').toLowerCase();
    if(computed !== claimed) {
      return { ok:false, hold:false, reason:'LOG_ENTRY_HASH_MISMATCH_AT_'+String(seq) };
    }
    prev = claimed;
  }

  if(prev !== headEntry) {
    return { ok:false, hold:false, reason:'LOG_HEAD_ENTRY_HASH_MISMATCH' };
  }

  return {
    ok:true,
    hold:false,
    reason:'OK',
    log,
    head_entry_sha256: headEntry,
    head_seq: headSeq,
    log_payload_sha256: logPayloadSha,
    log_payload_bytes: logPayloadBytes,
    verified_signing_kids: Array.from(res.okKids)
  };
}

async function verifyKeyringAgainstLog(keyring, log){
  const issues = [];

  // Build latest state per kid
  const entries = Array.isArray(log.entries) ? log.entries.slice().sort((a,b)=>Number(a.seq||0)-Number(b.seq||0)) : [];
  const state = {};
  for(const e of entries){
    if(!e || !e.kid) continue;
    state[e.kid] = {
      kid: e.kid,
      role: e.role,
      status: e.status,
      jwk_sha256: String(e.jwk_sha256||'').toLowerCase(),
      seq: Number(e.seq||0),
      utc: e.utc
    };
  }

  const keys = Array.isArray(keyring.keys) ? keyring.keys : [];
  const keyKids = new Set(keys.map(k=>k.kid));

  for(const k of keys){
    const kid = k.kid;
    const s = state[kid];
    if(!s) {
      issues.push('KEY_NOT_IN_LOG:'+kid);
      continue;
    }
    if(String(k.role||'') !== String(s.role||'')) issues.push('ROLE_MISMATCH:'+kid);
    if(String(k.status||'') !== String(s.status||'')) issues.push('STATUS_MISMATCH:'+kid);

    const jwkSha = (await sha256Hex(utf8(canonicalJson(k.jwk)))).toLowerCase();
    if(jwkSha !== s.jwk_sha256) issues.push('JWK_SHA_MISMATCH:'+kid);
  }

  // Ensure all active keys in the log exist in the keyring snapshot
  for(const kid of Object.keys(state)){
    const st = state[kid];
    if(String(st.status||'').toLowerCase() === 'active' && !keyKids.has(kid)){
      issues.push('ACTIVE_KEY_MISSING_FROM_KEYRING:'+kid);
    }
  }

  if(issues.length===0) return { ok:true, reason:'OK' };
  return { ok:false, reason:'KEYRING_LOG_INCONSISTENT', issues };
}

// ---------------------
// Minimal ZIP reader (central directory only). Supports STORE and DEFLATE.
// ---------------------
function u16(d,o){ return d.getUint16(o,true); }
function u32(d,o){ return d.getUint32(o,true); }

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

function parseCentralDirectory(bytes, eocdOff){
  const dv=new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  const cdSize=u32(dv, eocdOff+12);
  const cdOff=u32(dv, eocdOff+16);
  let p=cdOff;
  const out=[];
  while(p < cdOff+cdSize){
    if(u32(dv,p)!==0x02014b50) throw new Error('ZIP_CD_BAD_SIG');
    const method=u16(dv,p+10);
    const compSize=u32(dv,p+20);
    const uncompSize=u32(dv,p+24);
    const fnLen=u16(dv,p+28);
    const exLen=u16(dv,p+30);
    const cmLen=u16(dv,p+32);
    const lho=u32(dv,p+42);
    const fnBytes = bytes.slice(p+46, p+46+fnLen);
    const name=new TextDecoder().decode(fnBytes);
    out.push({name, method, compSize, uncompSize, lho});
    p += 46 + fnLen + exLen + cmLen;
  }
  return out;
}

function getLocalDataSlice(bytes, entry){
  const dv=new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  const p=entry.lho;
  if(u32(dv,p)!==0x04034b50) throw new Error('ZIP_LH_BAD_SIG');
  const fnLen=u16(dv,p+26);
  const exLen=u16(dv,p+28);
  const dataStart = p + 30 + fnLen + exLen;
  return bytes.slice(dataStart, dataStart + entry.compSize);
}

async function inflateDeflate(compBytes){
  if(typeof DecompressionStream==='undefined') throw new Error('NO_DECOMPRESSIONSTREAM');
  const ds = new DecompressionStream('deflate-raw');
  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 verifyAssurancePackContents(zipBytes){
  // Reads assurance_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['assurance_pack_manifest.json']) throw new Error('ASSURANCE_PACK_MANIFEST_MISSING');

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

  const list = Array.isArray(man.entries) ? man.entries : [];
  if(list.length===0) throw new Error('ASSURANCE_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 !== 'assurance_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++;
  }

  if(badCount===0) return {ok:true, detail:`${okCount} files verified`};
  return {ok:false, detail:`${okCount} ok / ${badCount} issues`};
}

// ---------------------
// Receipt generation
// ---------------------
async function makeReceipt(ctx){
  // ctx contains: artifacts + verdicts + policy + notes (no secrets)
  const base = {
    schema_id: 'mvg.verification_receipt@1',
    generated_utc: new Date().toISOString(),
    verifier: {
      name: 'mvg.verify-assurance',
      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
    }
  };

  // Artifact fingerprint: deterministic across environments (no generated_utc / environment)
  const fingerprintCore = {
    schema_id: base.schema_id,
    verifier: base.verifier,
    policy: base.policy,
    artifacts: base.artifacts,
    verdicts: base.verdicts
  };
  const fp = await sha256Hex(utf8(canonicalJson(fingerprintCore)));
  base.artifact_fingerprint = 'sha256:' + fp;

  // Run id: includes generated_utc + environment (unique per run)
  const runId = await sha256Hex(utf8(canonicalJson(base)));
  base.verification_run_id = 'sha256:' + runId;

  return base;
}

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

// ---------------------
// Main verification flow
// ---------------------
async function runVerification(zipBytes, packDsseEnv, keyringDsseEnv, opts){
  opts = opts || {};
  clearContents();
  clearReceipt();

  // Reset outputs
  outSha.textContent='—';
  outBytes.textContent='—';
  outKeyring.textContent='—';
  outKeyringVer.textContent='—';
  outSig.textContent='—';
  if(outLog) outLog.textContent='—';
  if(outLogHead) outLogHead.textContent='—';
  if(outLogConsistency) outLogConsistency.textContent='—';
  setContentsVerdict('—','');

  // Crypto availability
  if(!globalThis.crypto || !crypto.subtle){
    outKeyring.textContent = 'HOLD: WebCrypto unavailable';
    outSig.textContent = 'HOLD: WebCrypto unavailable';
    setVerdict('HOLD', 'Crypto unavailable (WebCrypto).');
    return;
  }

  // Hash ZIP
  const gotSha = await sha256Hex(zipBytes);
  outSha.textContent = gotSha;
  outBytes.textContent = String(zipBytes.byteLength);

  // Verify keyring snapshot DSSE (root-of-trust)
  let kr;
  try {
    const r = await verifyKeyringSnapshot(keyringDsseEnv);
    if(!r.ok){
      outKeyring.textContent = (r.hold ? 'HOLD: ' : 'FAIL: ') + r.reason;
      setVerdict(r.hold?'HOLD':'FAIL', 'Keyring snapshot could not be verified.');
      return;
    }
    kr = r;
    outKeyring.textContent = 'PASS (signed keyring)';
    outKeyringVer.textContent = String(r.keyring?.keyring_version || r.keyring?.keyring_id || '—');
  } catch(e) {
    const msg = String(e && (e.message||e) || e);
    const maybeUnsupported = msg.toLowerCase().includes('ed25519') || msg.toLowerCase().includes('algorithm') || msg.toLowerCase().includes('unsupported');
    outKeyring.textContent = (maybeUnsupported?'HOLD: ':'FAIL: ') + msg;
    setVerdict(maybeUnsupported?'HOLD':'FAIL', maybeUnsupported ? 'Crypto unavailable / unsupported (fail-closed).' : 'Keyring verification failed.');
    return;
  }

  // Verify Assurance Pack DSSE envelope using keys from keyring
  let payload;
  let packOkKids = null;
  let packDssePayloadBytes = null;

  try{
    const witnessKids = kr.packWitnessKids;
    const res = await verifyDsse(
      packDsseEnv,
      'application/vnd.mvg.assurance_pack_signature+json',
      kr.pubByKid,
      witnessKids,
      kr.packWitnessThreshold
    );
    if(res.hold) {
      outSig.textContent = 'HOLD: '+res.reason;
      setVerdict('HOLD', 'Witness threshold not met (fail-closed).');
      return;
    }
    packOkKids = res.okKids;
    packDssePayloadBytes = res.payloadBytes;
    payload = JSON.parse(new TextDecoder().decode(res.payloadBytes));
  } catch(e){
    const msg = String(e && (e.message||e) || e);
    const maybeUnsupported = msg.toLowerCase().includes('ed25519') || msg.toLowerCase().includes('algorithm') || msg.toLowerCase().includes('unsupported');
    if(maybeUnsupported){
      outSig.textContent = 'HOLD: '+msg;
      setVerdict('HOLD', 'Crypto unavailable / unsupported (fail-closed).');
      return;
    }
    outSig.textContent = 'FAIL: '+msg;
    setVerdict('FAIL', 'Signature verification failed.');
    return;
  }

  // Enforce assessor signature present
  const signingKid = String(payload?.signing_kid || '');
  if(signingKid && packOkKids && !packOkKids.has(signingKid)){
    outSig.textContent = 'FAIL: ASSESSOR_SIG_MISSING';
    setVerdict('FAIL', 'Assessor signature missing (fail-closed).');
    return;
  }

  // Enforce key validity windows at generated_utc (rollover safety)
  const genUtc = String(payload?.generated_utc || '');
  const genMs = parseUtcMs(genUtc);
  if(!Number.isFinite(genMs)){
    outSig.textContent = 'FAIL: GENERATED_UTC_INVALID';
    setVerdict('FAIL', 'generated_utc missing/invalid in DSSE payload.');
    return;
  }
  if(signingKid && !isKeyValidAt(kr.metaByKid[signingKid], genMs)){
    outSig.textContent = 'FAIL: SIGNING_KEY_NOT_VALID_AT_TIME';
    setVerdict('FAIL', 'Signing key is not valid at generated_utc per keyring.');
    return;
  }
  const witnessKidsPayload = Array.isArray(payload?.witness_kids) ? payload.witness_kids : [];
  for(const wk of witnessKidsPayload){
    if(!isKeyValidAt(kr.metaByKid[wk], genMs)){
      outSig.textContent = 'FAIL: WITNESS_KEY_NOT_VALID_AT_TIME';
      setVerdict('FAIL', 'Witness key is not valid at generated_utc per keyring.');
      return;
    }
  }

  outSig.textContent = 'PASS (assessor + witnesses)';

  // Validate payload fields (sha + bytes)
  const expSha = String(payload.assurance_pack_sha256||'').toLowerCase();
  const expBytes = Number(payload.assurance_pack_bytes||0);
  const okDigest = (expSha===gotSha.toLowerCase() && expBytes===zipBytes.byteLength);

  if(!okDigest){
    setVerdict('FAIL', 'Digest mismatch: payload does not match ZIP.');
    return;
  }

  // Contents verification (deep)
  let contentsOk = false;
  try{
    const r = await verifyAssurancePackContents(zipBytes);
    if(r.ok) {
      contentsOk = true;
      setContentsVerdict('PASS', r.detail);
    } else {
      setContentsVerdict('FAIL', r.detail);
    }
  } catch(e){
    if(String(e.message||e).includes('NO_DECOMPRESSIONSTREAM')) {
      setContentsVerdict('HOLD', 'DecompressionStream unavailable (cannot verify contents).');
    } else {
      setContentsVerdict('FAIL', String(e.message||e));
    }
  }

  // Optional: Transparency log verification
  const requireTransparency = !!(opts.requireTransparency);
  let transparencyVerdict = '—';
  let transparencyReason = 'NOT_REQUESTED';
  let logRes = null;
  let logConsistency = null;

  if(requireTransparency){
    const providedLogEnv = opts.logDsseEnv || null;

    // If not provided, use embedded demo log only if it matches the demo keyring expectation.
    let logEnvToUse = providedLogEnv;
    if(!logEnvToUse){
      const expectedSha = String(kr.keyring?.transparency_log?.log_sha256||'').toLowerCase();
      const demoSha = await sha256Hex(b64uToBytes(sampleAssuranceKeyTransparencyLogDsse.payload));
      if(expectedSha && expectedSha === demoSha.toLowerCase()){
        logEnvToUse = sampleAssuranceKeyTransparencyLogDsse;
      }
    }

    if(!logEnvToUse){
      transparencyVerdict = 'HOLD';
      transparencyReason = 'LOG_REQUIRED';
      if(outLog) outLog.textContent = 'HOLD: provide log DSSE file';
      if(outLogHead) outLogHead.textContent = '—';
      if(outLogConsistency) outLogConsistency.textContent = '—';
    } else {
      try{
        logRes = await verifyTransparencyLog(logEnvToUse);
        if(!logRes.ok){
          transparencyVerdict = logRes.hold ? 'HOLD' : 'FAIL';
          transparencyReason = logRes.reason;
          if(outLog) outLog.textContent = (logRes.hold?'HOLD: ':'FAIL: ') + logRes.reason;
          if(outLogHead) outLogHead.textContent = '—';
          if(outLogConsistency) outLogConsistency.textContent = '—';
        } else {
          // Compare with keyring's declared transparency log fields (fail-closed)
          const krTL = kr.keyring?.transparency_log || {};
          const declaredSha = String(krTL.log_sha256||'').toLowerCase();
          const declaredBytes = Number(krTL.log_bytes||0);
          const declaredHead = String(krTL.head_entry_sha256||'').toLowerCase();
          const idOk = (!declaredHead || declaredHead === logRes.head_entry_sha256);
          const shaOk = (!declaredSha || declaredSha === logRes.log_payload_sha256.toLowerCase());
          const bytesOk = (!declaredBytes || declaredBytes === logRes.log_payload_bytes);

          if(outLog) outLog.textContent = 'PASS (DSSE + chain)';
          if(outLogHead) outLogHead.textContent = logRes.head_entry_sha256;

          if(!idOk || !shaOk || !bytesOk){
            transparencyVerdict = 'FAIL';
            transparencyReason = 'LOG_MISMATCH_WITH_KEYRING';
            if(outLogConsistency) outLogConsistency.textContent = 'FAIL: keyring ↔ log mismatch';
          } else {
            logConsistency = await verifyKeyringAgainstLog(kr.keyring, logRes.log);
            if(!logConsistency.ok){
              transparencyVerdict = 'FAIL';
              transparencyReason = logConsistency.reason;
              if(outLogConsistency) outLogConsistency.textContent = 'FAIL: ' + (logConsistency.issues ? logConsistency.issues[0] : logConsistency.reason);
            } else {
              transparencyVerdict = 'PASS';
              transparencyReason = 'OK';
              if(outLogConsistency) outLogConsistency.textContent = 'PASS (keyring matches log)';
            }
          }
        }
      } catch(e){
        const msg = String(e && (e.message||e) || e);
        const maybeUnsupported = msg.toLowerCase().includes('ed25519') || msg.toLowerCase().includes('algorithm') || msg.toLowerCase().includes('unsupported');
        transparencyVerdict = maybeUnsupported ? 'HOLD' : 'FAIL';
        transparencyReason = msg;
        if(outLog) outLog.textContent = (maybeUnsupported?'HOLD: ':'FAIL: ') + msg;
      }
    }
  }

  // Determine final verdict (fail-closed)
  let overall = 'PASS';
  let overallMsg = 'Keyring + signature + digest verified.';
  if(contentsVerdict.textContent === 'FAIL'){
    overall = 'FAIL';
    overallMsg = 'Contents verification failed.';
  } else if(contentsVerdict.textContent === 'HOLD'){
    overall = 'HOLD';
    overallMsg = 'Contents verification unavailable (fail-closed).';
  }

  if(overall === 'PASS' && requireTransparency){
    if(transparencyVerdict === 'PASS'){
      overall = 'PASS';
      overallMsg = 'Integrity + contents + transparency verified.';
    } else if(transparencyVerdict === 'HOLD'){
      overall = 'HOLD';
      overallMsg = 'Integrity verified, but transparency check is HOLD (fail-closed).';
    } else if(transparencyVerdict === 'FAIL'){
      overall = 'FAIL';
      overallMsg = 'Integrity verified, but transparency check failed.';
    }
  }

  setVerdict(overall, overallMsg);

  // Generate and expose receipt
  const artifacts = {
    assurance_pack: {
      sha256: gotSha,
      bytes: zipBytes.byteLength
    },
    pack_dsse: {
      payloadType: packDsseEnv.payloadType,
      payload_sha256: packDssePayloadBytes ? await sha256Hex(packDssePayloadBytes) : '—',
      verified_kids: packOkKids ? Array.from(packOkKids) : []
    },
    keyring: {
      keyring_version: String(kr.keyring?.keyring_version || ''),
      payload_sha256: kr.keyring_payload_sha256,
      payload_bytes: kr.keyring_payload_bytes,
      verified_kids: kr.verified_signing_kids
    }
  };

  if(requireTransparency){
    artifacts.transparency_log = logRes && logRes.ok ? {
      log_id: String(logRes.log?.log_id || ''),
      head_entry_sha256: logRes.head_entry_sha256,
      payload_sha256: logRes.log_payload_sha256,
      payload_bytes: logRes.log_payload_bytes,
      verified_kids: logRes.verified_signing_kids
    } : {
      status: transparencyVerdict,
      reason: transparencyReason
    };
  }

  const verdicts = {
    overall,
    contents: contentsVerdict.textContent,
    transparency: requireTransparency ? transparencyVerdict : '—'
  };

  const receipt = await makeReceipt({
    policy: {
      require_transparency_log: requireTransparency
    },
    artifacts,
    verdicts,
    notes: [
      'Offline verifier: no uploads; intended to run without network.',
      'Receipt is reviewer-generated; attach internally for procurement/legal review.'
    ]
  });

  lastReceipt = receipt;
  if(outReceiptId) outReceiptId.textContent = receipt.artifact_fingerprint;
  if(outRunId) outRunId.textContent = receipt.verification_run_id;
  enableReceipt();

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

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

// ---------------------
// Event handlers
// ---------------------
btnSample?.addEventListener('click', async ()=>{
  try{
    const zipBytes = b64uToBytes(sampleAssurancePackB64u);
    const dsseEnv = sampleAssurancePackDsse;
    const keyringEnv = sampleAssuranceKeyringDsse;

    await runVerification(zipBytes, dsseEnv, keyringEnv, {
      requireTransparency: !!chkTransparency?.checked,
      logDsseEnv: sampleAssuranceKeyTransparencyLogDsse
    });
  } catch(e){
    setVerdict('FAIL', String(e.message||e));
  }
});

btnSelected?.addEventListener('click', async ()=>{
  try{
    const zf = zipInput?.files?.[0];
    const sf = sigInput?.files?.[0];
    const kf = keyringInput?.files?.[0];
    const lf = logInput?.files?.[0];

    if(!zf || !sf) {
      setVerdict('HOLD', 'Select both ZIP and DSSE JSON.');
      return;
    }

    const zipBytes = await readFileBytes(zf);
    const dsseEnv = JSON.parse(await sf.text());

    let keyringEnv = sampleAssuranceKeyringDsse;
    if(kf) keyringEnv = JSON.parse(await kf.text());

    let logEnv = null;
    if(lf) logEnv = JSON.parse(await lf.text());

    await runVerification(zipBytes, dsseEnv, keyringEnv, {
      requireTransparency: !!chkTransparency?.checked,
      logDsseEnv: logEnv
    });

  } catch(e){
    setVerdict('FAIL', String(e.message||e));
  }
});

btnCopyContents?.addEventListener('click', async ()=>{
  const rid = lastReceipt?.artifact_fingerprint ? String(lastReceipt.artifact_fingerprint) : '';
  const summary =
`Assurance Pack verification: ${verdictEl.textContent}
Detail: ${detailEl.textContent}
ZIP sha256: ${outSha.textContent}
Keyring: ${outKeyringVer.textContent}
Transparency: ${outLogConsistency?.textContent || '—'}
Receipt: ${rid || '—'}`;
  try {
    await navigator.clipboard.writeText(summary);
    btnCopyContents.textContent='Copied';
    setTimeout(()=>btnCopyContents.textContent='Copy summary', 1200);
  } catch(e) {
    // ignore
  }
});

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

btnCopyReceiptId?.addEventListener('click', async ()=>{
  if(!lastReceipt) return;
  const txt = String(lastReceipt.artifact_fingerprint||'');
  try{
    await navigator.clipboard.writeText(txt);
    if(receiptHint) {
      receiptHint.textContent = 'Copied';
      setTimeout(()=>receiptHint.textContent='', 1200);
    }
  } catch(e){
    // ignore
  }
});


// ---------------------
// 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 || String(v).trim()==='') ? '—' : String(v);
}

function isWebAuthnSupported(){
  return typeof window !== 'undefined' && !!(window.PublicKeyCredential && navigator.credentials);
}

function clearHardwareKey(){
  hwCred = null;
  hwCredIdBytes = null;
  hwPublicJwk = null;
  hwEvidence = null;

  setHwStatus('—','');
  setText(hwFp, '—');
  setText(hwAttachmentOut, '—');
  setText(hwFmt, '—');
  setText(hwAaguid, '—');
  setText(hwEvidenceSha, '—');
  setText(hwEvidenceVerdict, '—');

  if(btnHwDownloadCred) btnHwDownloadCred.disabled = true;
  if(btnHwSign) btnHwSign.disabled = true;
}

function refreshHardwareButtons(){
  const supported = isWebAuthnSupported();
  if(!supported){
    if(btnHwRegister) btnHwRegister.disabled = true;
    if(btnHwSign) btnHwSign.disabled = true;
    if(btnHwDownloadCred) btnHwDownloadCred.disabled = true;
    setHwStatus('HOLD','WebAuthn not supported in this browser.');
    return;
  }

  if(btnHwRegister) btnHwRegister.disabled = false;
  if(btnHwDownloadCred) btnHwDownloadCred.disabled = !hwCred;
  if(btnHwSign) btnHwSign.disabled = !(hwCred && lastReceipt);
}

// 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(utf8(canonicalJson(minimal)));
  return 'sha256:' + hex;
}

async function signPayloadWithLoadedKey(payload){
  if(!csPrivCryptoKey) throw new Error('NO_PRIVATE_KEY');
  const payloadBytes = utf8(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(utf8(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(utf8(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 bi = (BigInt(hi) << 32n) | BigInt(lo);
    const asNum = Number(bi);
    return [Number.isSafeInteger(asNum) ? asNum : bi, offset+8];
  }
  if(ai === 31) return [null, offset]; // indefinite length marker handled by caller
  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;

  // unsigned / negative ints
  if(mt === 0){
    const [v, o] = cborReadUint(dv, offset, ai);
    return [v, o];
  }
  if(mt === 1){
    const [v, o] = cborReadUint(dv, offset, ai);
    const n = (typeof v === 'bigint') ? (-1n - v) : (-1 - v);
    return [n, o];
  }

  // byte string
  if(mt === 2){
    if(ai === 31){
      const chunks = [];
      while(true){
        const [c, o] = cborDecode(u8, offset);
        offset = o;
        if(c === CBOR_BREAK) break;
        if(!(c instanceof Uint8Array)) throw new Error('CBOR_INDEF_BYTES_CHUNK_NOT_BYTES');
        chunks.push(c);
      }
      let n = 0; for(const c of chunks) n += c.length;
      const out = new Uint8Array(n);
      let p = 0; for(const c of chunks){ out.set(c, p); p += c.length; }
      return [out, offset];
    }
    const [len, o] = cborReadUint(dv, offset, ai);
    offset = o;
    if(typeof len !== 'number') throw new Error('CBOR_BYTES_LEN_BIGINT');
    const end = offset + len;
    if(end > u8.length) throw new Error('CBOR_BYTES_OOB');
    return [u8.slice(offset, end), end];
  }

  // text string
  if(mt === 3){
    if(ai === 31){
      let out = '';
      while(true){
        const [c, o] = cborDecode(u8, offset);
        offset = o;
        if(c === CBOR_BREAK) break;
        if(typeof c !== 'string') throw new Error('CBOR_INDEF_TEXT_CHUNK_NOT_STRING');
        out += c;
      }
      return [out, offset];
    }
    const [len, o] = cborReadUint(dv, offset, ai);
    offset = o;
    if(typeof len !== 'number') throw new Error('CBOR_TEXT_LEN_BIGINT');
    const end = offset + len;
    if(end > u8.length) throw new Error('CBOR_TEXT_OOB');
    const str = new TextDecoder().decode(u8.slice(offset, end));
    return [str, end];
  }

  // array
  if(mt === 4){
    if(ai === 31){
      const arr = [];
      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;
    if(typeof len !== 'number') throw new Error('CBOR_ARRAY_LEN_BIGINT');
    const arr = [];
    for(let i=0;i<len;i++){
      const [v, o2] = cborDecode(u8, offset);
      offset = o2;
      arr.push(v);
    }
    return [arr, offset];
  }

  // map
  if(mt === 5){
    const map = new Map();
    if(ai === 31){
      while(true){
        const [k, o] = cborDecode(u8, offset);
        offset = o;
        if(k === CBOR_BREAK) break;
        const [v, o2] = cborDecode(u8, offset);
        offset = o2;
        map.set(k, v);
      }
      return [map, offset];
    }
    const [len, o] = cborReadUint(dv, offset, ai);
    offset = o;
    if(typeof len !== 'number') throw new Error('CBOR_MAP_LEN_BIGINT');
    for(let i=0;i<len;i++){
      const [k, o1] = cborDecode(u8, offset);
      offset = o1;
      const [v, o2] = cborDecode(u8, offset);
      offset = o2;
      map.set(k, v);
    }
    return [map, offset];
  }

  // tag
  if(mt === 6){
    const [_tag, o] = cborReadUint(dv, offset, ai);
    return cborDecode(u8, o);
  }

  // floats / simple
  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 === 31) return [CBOR_BREAK, offset];
    if(ai === 24) return [dv.getUint8(offset), offset+1];
    if(ai === 25){
      const v = dv.getUint16(offset, false);
      return [v, offset+2];
    }
    if(ai === 26){
      const v = dv.getUint32(offset, false);
      return [v, offset+4];
    }
    if(ai === 27){
      const v = dv.getFloat64(offset, false);
      return [v, offset+8];
    }
    return [null, offset];
  }

  throw new Error('CBOR_UNSUPPORTED_MT');
}

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

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

function parseAuthenticatorData(u8){
  if(!(u8 instanceof Uint8Array)) throw new Error('AUTHDATA_NOT_BYTES');
  if(u8.length < 37) throw new Error('AUTHDATA_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);
  let offset = 37;

  const userPresent = !!(flags & 0x01);
  const userVerified = !!(flags & 0x04);
  const backupEligible = !!(flags & 0x08);
  const backupState = !!(flags & 0x10);
  const hasAttested = !!(flags & 0x40);
  const hasExtensions = !!(flags & 0x80);

  let aaguidBytes = null;
  let credentialIdBytes = null;
  let credentialPublicKey = null;

  if(hasAttested){
    if(u8.length < offset + 16 + 2) throw new Error('AUTHDATA_ATTESTED_TRUNC');
    aaguidBytes = u8.slice(offset, offset+16); 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('AUTHDATA_CREDID_TRUNC');
    credentialIdBytes = u8.slice(offset, offset+credIdLen); offset += credIdLen;
    const [coseKey, o2] = cborDecode(u8, offset);
    credentialPublicKey = coseKey;
    offset = o2;
  }

  return {
    rpIdHash,
    flags,
    signCount,
    userPresent,
    userVerified,
    backupEligible,
    backupState,
    hasAttestedCredentialData: hasAttested,
    hasExtensions,
    aaguid: aaguidBytes ? uuidFrom16(aaguidBytes) : null,
    credentialIdBytes,
    credentialPublicKey
  };
}

function coseEc2P256ToJwk(cose){
  if(!(cose instanceof Map)) throw new Error('COSE_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_UNSUPPORTED_KTY');
  if(crv !== 1) throw new Error('COSE_UNSUPPORTED_CRV');
  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){
  // Returns: { verdict, detail, fmt, aaguid, evidence_sha256, attestation_object_sha256, derived_fingerprint }
  if(!bundle || typeof bundle !== 'object') return { verdict:'FAIL', detail:'Bundle is not JSON object.' };
  if(String(bundle.schema_id||'') !== 'mvg.webauthn_approval_credential@1') return { verdict:'FAIL', detail:'schema_id mismatch.' };

  try{
    const evidenceSha = 'sha256:' + (await sha256Hex(utf8(canonicalJson(bundle))));
    const attObjBytes = b64uToBytes(String(bundle.attestation_object_b64u||''));
    if(attObjBytes.length === 0) return { verdict:'FAIL', detail:'attestation_object_b64u missing/empty.', evidence_sha256: evidenceSha };

    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);
    const aaguid = parsed.aaguid || null;
    const credIdB64u = parsed.credentialIdBytes ? bytesToB64u(parsed.credentialIdBytes) : null;
    const bundleCredId = String(bundle.credential_id_b64u||'');
    if(bundleCredId && credIdB64u && bundleCredId !== credIdB64u) throw new Error('CREDENTIAL_ID_MISMATCH');

    let derivedJwk = null;
    try{
      if(parsed.credentialPublicKey) derivedJwk = coseEc2P256ToJwk(parsed.credentialPublicKey);
    }catch(_e){ derivedJwk = null; }

    // Check that the public key in the bundle matches the public key extracted from attestationObject (when extractable)
    const bundleJwk = bundle.public_key_jwk || null;
    let derivedFp = null;
    let keyMatch = true;
    if(bundleJwk && derivedJwk){
      derivedFp = await jwkPublicFingerprintSha256(derivedJwk);
      const bundleFp = await jwkPublicFingerprintSha256(bundleJwk);
      keyMatch = (derivedFp === bundleFp);
      if(!keyMatch) throw new Error('ATTESTATION_PUBLIC_KEY_MISMATCH');
    }

    // rpIdHash check (if rp_id present)
    const rpId = String(bundle.rp_id||'').trim();
    if(rpId){
      const rpHash = await sha256Bytes(utf8(rpId));
      if(!bytesEqual(parsed.rpIdHash, rpHash)) throw new Error('ATTESTATION_RPID_HASH_MISMATCH');
    }

    const attObjSha = 'sha256:' + (await sha256Hex(attObjBytes));

    const noVendorAttest = (fmt === 'none');
    const verdict = noVendorAttest ? 'HOLD' : 'PASS';
    const detail = noVendorAttest
      ? 'Parsed and internally consistent, but attestation fmt="none" (no vendor attestation).' 
      : 'Parsed and internally consistent attestation evidence (trust evaluation is org-defined).';

    return {
      verdict,
      detail,
      fmt: fmt || null,
      aaguid,
      evidence_sha256: evidenceSha,
      attestation_object_sha256: attObjSha,
      derived_fingerprint: derivedFp || (bundle.public_key_fingerprint_sha256 || null)
    };
  }catch(e){
    const msg = String(e && (e.message||e) || e);
    return { verdict:'FAIL', detail: msg };
  }
}

function safeJsonFromBytes(u8){
  try{
    const txt = new TextDecoder().decode(u8);
    return JSON.parse(txt);
  }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.' };

  // Verify base sha
  const computedBaseSha = 'sha256:' + (await sha256Hex(utf8(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 = utf8(canonicalJson(cs.payload));
      const payloadSha = 'sha256:' + (await sha256Hex(payloadBytes));
      if(String(cs.payload_sha256||'') !== payloadSha) throw new Error('CS_PAYLOAD_SHA_MISMATCH');

      // Bind to base receipt
      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(utf8(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 algo = String(cs.payload.algorithm||'').toUpperCase();
      const isWebAuthn = (method === 'webauthn') || !!cs.webauthn || algo.includes('WEBAUTHN');

      // Public key availability
      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');

        // Challenge binds the WebAuthn signature to the countersign payload digest
        const expectedChallenge = bytesToB64u(await sha256Bytes(payloadBytes));
        if(String(clientObj.challenge||'') !== expectedChallenge) throw new Error('WEBAUTHN_CHALLENGE_MISMATCH');

        // Optional origin self-consistency check (payload is self-asserted; clientData is authenticator-signed)
        const payloadOrigin = String(cs.payload.webauthn?.origin || cs.payload.signing_context?.origin || '');
        if(payloadOrigin && clientObj.origin && String(clientObj.origin) !== payloadOrigin) throw new Error('WEBAUTHN_ORIGIN_MISMATCH');

        // rpIdHash check (uses rp_id declared inside the signed payload when present)
        const rpId = String(cs.payload.webauthn?.rp_id || cs.payload.signing_context?.rp_id || '');
        if(rpId){
          const rpHash = await sha256Bytes(utf8(rpId));
          if(!bytesEqual(authData.slice(0,32), rpHash)) throw new Error('WEBAUTHN_RPID_HASH_MISMATCH');
        }

        // User verification requirement (if declared)
        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){
          const msgUv = String(eUv && (eUv.message||eUv) || eUv);
          // Do not treat as crypto failure; surface as HOLD.
          hold++;
          results.push({ status:'HOLD', fingerprint: fp||null, reason: msgUv, ticket: cs.payload.ticket||null, signing_method:'webauthn' });
          continue;
        }

        // Verify WebAuthn signature: sig over (authenticatorData || sha256(clientDataJSON))
        const clientHash = await sha256Bytes(clientData);
        const signed = concatBytes(authData, clientHash);
        const okSig = await crypto.subtle.verify({name:'ECDSA', hash:'SHA-256'}, pubKey, sigBytes, signed);
        if(!okSig) throw new Error('WEBAUTHN_SIGNATURE_INVALID');

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

        ok++;
        results.push({ status:'PASS', fingerprint: fp||null, reason:'OK', ticket: cs.payload.ticket||null, signing_method:'webauthn', attestation: attRes });
      } 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){
  // Returns: { verdict, detail, policy_label, matched_label, ticket_label, approval_id, certificate }
  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;

  // Build fingerprint -> key entry map
  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) });
  }

  // Distinct counting
  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);

  // Ticket evaluation
  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' : '—'));

  // Build deterministic approval id (recomputable)
  const orgKeyringSha = 'sha256:' + (await sha256Hex(utf8(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(utf8(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; // UI not present
  try{
    const res = await computeOrgApprovalFor(obj, vr);
    if(!res){
      clearOrgApprovalUi();
      return null;
    }

    // Update UI fields
    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');

  const jwk = obj.public_key_jwk;
  if(!jwk || !isP256EcJwk(jwk, false)) throw new Error('HARDWARE_CREDENTIAL_PUBLIC_KEY_INVALID_EXPECT_EC_P256');

  hwCred = obj;
  hwPublicJwk = jwk;
  hwCredIdBytes = b64uToBytes(String(obj.credential_id_b64u||''));
  if(hwCredIdBytes.length === 0) throw new Error('HARDWARE_CREDENTIAL_ID_MISSING');

  // Evidence parsing + internal consistency checks
  hwEvidence = await summarizeWebAuthnCredentialBundle(obj);

  const fp = String(obj.public_key_fingerprint_sha256||'').trim() || (await jwkPublicFingerprintSha256(jwk));
  setText(hwFp, fp);
  setText(hwAttachmentOut, obj.authenticator_attachment || '—');
  setText(hwFmt, hwEvidence?.fmt || obj.attestation_summary?.fmt || '—');
  setText(hwAaguid, hwEvidence?.aaguid || obj.attestation_summary?.aaguid || '—');
  setText(hwEvidenceSha, hwEvidence?.evidence_sha256 || '—');
  setText(hwEvidenceVerdict, hwEvidence ? `${hwEvidence.verdict}${hwEvidence.fmt?` (fmt:${hwEvidence.fmt})`:''}` : '—');

  setHwStatus(hwEvidence?.verdict || 'PASS', 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 attachment = (attachmentSel === 'any') ? undefined : attachmentSel;
  const uv = String(hwUv?.value || 'preferred');
  const attPref = String(hwAttestation?.value || 'none');

  // Use signer metadata if present (purely UI; no secrets)
  const userEmail = String(csEmail?.value || 'reviewer@local').trim() || 'reviewer@local';
  const userName = String(csName?.value || 'Reviewer').trim() || 'Reviewer';

  const rpId = String(location.hostname || '').trim();
  if(!rpId) throw new Error('WEBAUTHN_RP_ID_UNAVAILABLE');

  const userId = crypto.getRandomValues(new Uint8Array(16));
  const challenge = crypto.getRandomValues(new Uint8Array(32));

  const publicKey = compact({
    challenge,
    rp: { name: 'Meridian Verity Group', id: rpId },
    user: { id: userId, name: userEmail, displayName: userName },
    pubKeyCredParams: [ { type: 'public-key', alg: -7 } ], // ES256
    timeout: 60000,
    attestation: attPref,
    authenticatorSelection: compact({
      authenticatorAttachment: attachment,
      residentKey: 'preferred',
      userVerification: uv
    })
  });

  let cred = null;
  try{
    cred = await navigator.credentials.create({ publicKey });
  }catch(e){
    const msg = String(e && (e.message||e) || e);
    throw new Error(`WEBAUTHN_CREATE_FAILED: ${msg}`);
  }
  if(!cred || !cred.response) throw new Error('WEBAUTHN_CREATE_FAILED_NO_CREDENTIAL');

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

  // Extract public key + AAGUID from attestationObject (CBOR)
  const attObj = cborDecodeAll(attObjBytes);
  if(!(attObj instanceof Map)) throw new Error('WEBAUTHN_ATTESTATION_CBOR_NOT_MAP');
  const fmt = String(attObj.get('fmt')||'');
  const authDataBytes = attObj.get('authData');
  if(!(authDataBytes instanceof Uint8Array)) throw new Error('WEBAUTHN_ATTESTATION_AUTHDATA_MISSING');

  const parsed = parseAuthenticatorData(authDataBytes);
  if(!parsed.credentialPublicKey) throw new Error('WEBAUTHN_ATTESTATION_CREDENTIAL_PUBLIC_KEY_MISSING');
  const pubJwk = coseEc2P256ToJwk(parsed.credentialPublicKey);
  const pubFp = 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: String(location.origin||''),
    authenticator_attachment: String(cred.authenticatorAttachment || attachment || 'unknown'),
    user_verification_policy: uv,
    attestation_conveyance: attPref,
    credential_id_b64u: bytesToB64u(rawId),
    public_key_jwk: pubJwk,
    public_key_fingerprint_sha256: pubFp,
    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 your browser. Contains no private key material.',
      'Attestation is authenticator-provided evidence. Trust evaluation requires your org policy and trust anchors.'
    ]
  });

  await setHardwareCredentialBundle(bundle);
}

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

async function countersignLatestReceiptWebAuthn(){
  if(!lastReceipt){
    setCsVerdict('HOLD', 'Run verification first to generate a base receipt.');
    return;
  }
  if(!isWebAuthnSupported()){
    setCsVerdict('HOLD', 'WebAuthn is not supported in this browser/environment.');
    return;
  }
  if(!hwCred || !hwCredIdBytes || !hwPublicJwk){
    setCsVerdict('HOLD', 'Load or register a WebAuthn approval credential first.');
    return;
  }

  // For WebAuthn, embedding the public key is required for offline verification.
  const embedPub = !!(chkEmbedPub && chkEmbedPub.checked);
  if(!embedPub){
    setCsVerdict('HOLD', 'For WebAuthn countersign, enable “Embed public key in payload”.');
    return;
  }

  // Base receipt sha
  const baseSha = 'sha256:' + (await sha256Hex(utf8(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 pubFp = String(hwCred.public_key_fingerprint_sha256||'').trim() || (await jwkPublicFingerprintSha256(hwPublicJwk));
  const credSha = 'sha256:' + (await sha256Hex(utf8(canonicalJson(hwCred))));

  // 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(utf8(canonicalJson(tuple))));
  }

  const uv = String(hwUv?.value || hwCred.user_verification_policy || 'preferred');
  const rpId = String(hwCred.rp_id || location.hostname);

  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||''),
    base_verification_run_id: String(lastReceipt.verification_run_id||''),
    public_key_fingerprint_sha256: pubFp,
    public_key_jwk: hwPublicJwk,
    signer,
    ticket,
    ticket_binding_sha256: ticketBindingSha,
    statement: csStatement ? csStatement.value : undefined,
    webauthn: compact({
      rp_id: rpId,
      origin: String(hwCred.origin || location.origin),
      authenticator_attachment: String(hwCred.authenticator_attachment||'unknown'),
      user_verification: uv,
      attestation_fmt: String(hwCred.attestation_summary?.fmt || hwEvidence?.fmt || ''),
      aaguid: String(hwCred.attestation_summary?.aaguid || hwEvidence?.aaguid || '')
    }),
    webauthn_credential_sha256: credSha
  });

  const payloadBytes = utf8(canonicalJson(payload));
  const payloadSha = 'sha256:' + (await sha256Hex(payloadBytes));
  const challenge = await sha256Bytes(payloadBytes); // 32 bytes

  let assertion = null;
  try{
    assertion = await navigator.credentials.get({
      publicKey: compact({
        challenge,
        timeout: 60000,
        userVerification: uv,
        rpId,
        allowCredentials: [ { type: 'public-key', id: hwCredIdBytes } ]
      })
    });
  }catch(e){
    const msg = String(e && (e.message||e) || e);
    setCsVerdict('HOLD', `WebAuthn signing cancelled or unavailable: ${msg}`);
    return;
  }
  if(!assertion || !assertion.response) {
    setCsVerdict('HOLD', 'WebAuthn signing failed (no credential returned).');
    return;
  }

  const resp = assertion.response;
  const authDataBytes = new Uint8Array(resp.authenticatorData);
  const clientDataBytes = new Uint8Array(resp.clientDataJSON);
  const sigBytes = 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(authDataBytes),
    client_data_json_b64u: bytesToB64u(clientDataBytes),
    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: payloadSha,
    signature_b64u: bytesToB64u(sigBytes),
    webauthn: wa,
    webauthn_credential: (chkHwEmbedCred && chkHwEmbedCred.checked) ? hwCred : undefined
  });

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

  // Recompute receipt id after adding signature
  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(utf8(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 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;
  }

  // Base receipt sha
  const baseSha = 'sha256:' + (await sha256Hex(utf8(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(utf8(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);

  // Recompute receipt id after adding signature
  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(utf8(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', ()=>{
  // Clear only key material (keep last countersigned receipt)
  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_AssurancePack_${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('FAIL', String(e.message||e));
  }
});

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

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

btnHwSign?.addEventListener('click', async ()=>{
  try{
    await countersignLatestReceiptWebAuthn();
  }catch(e){
    setCsVerdict('FAIL', 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
  }
});


// Initialize disabled until first run
clearReceipt();