/**
 * HiteCrypto — HITE Encryption Library for Browser
 * Hilbert Information Theory Encryption — AES-256-GCM + KO42 Entropy + Landauer Certification
 *
 * Used by ZeqAuth v3 and ZeqVault for all cryptographic operations.
 * Replaces raw WebCrypto with HITE-standard encryption that binds every
 * operation to thermodynamic law via Landauer bound proofs.
 *
 * Zeq OS 1.287 Hz Limited — /
 * License: CC BY 4.0
 */
(function (root) {
  "use strict";

  // ═══════════════════════════════════════════════════════════════════════
  // Secure-context guard (Phase 6.1.AUTH-FIX-2)
  // ═══════════════════════════════════════════════════════════════════════
  // Web Crypto's `crypto.subtle` is only present in *secure contexts*:
  //   https://…   or   http://localhost   or   http://127.0.0.1
  // Plain http://*.local — what the dev /etc/hosts setup uses — is NOT a
  // secure context, so crypto.subtle === undefined there. Without this
  // guard, importKey() throws with a confusing "Cannot read properties of
  // undefined" stack that bubbles into a browser alert().
  //
  // We surface a clean diagnostic up front. Callers can read
  //   HiteCrypto.SECURE_CONTEXT_OK
  //   HiteCrypto.SECURE_CONTEXT_REASON
  // and render a friendly inline message instead of letting the throw
  // reach an alert().
  var _hasSubtle =
    typeof crypto !== "undefined" &&
    crypto &&
    typeof crypto.subtle !== "undefined" &&
    typeof crypto.subtle.importKey === "function";
  var _isSecureCtx =
    typeof window !== "undefined" && typeof window.isSecureContext === "boolean"
      ? window.isSecureContext
      : false;
  var SECURE_CONTEXT_OK = _hasSubtle && _isSecureCtx;
  var SECURE_CONTEXT_REASON = SECURE_CONTEXT_OK
    ? ""
    : !_hasSubtle
      ? "Web Crypto unavailable in this browser context. " +
        "HITE encryption requires window.crypto.subtle. " +
        "If you're testing locally, open the page via http://localhost (or https://) — " +
        "browsers do not expose crypto.subtle on http://<anything>.local."
      : "Browser reports this page is not a secure context. " +
        "HITE encryption requires HTTPS or http://localhost.";

  function _ensureSecureContext() {
    if (!SECURE_CONTEXT_OK) {
      var err = new Error(SECURE_CONTEXT_REASON);
      err.code = "INSECURE_CONTEXT";
      throw err;
    }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Constants
  // ═══════════════════════════════════════════════════════════════════════

  var VERSION = "1.0.0";
  // A1 (2026-07-11) — de-identified credential format. On disk a credential is
  // now just [SALT:16][IV:12][CIPHERTEXT] with NO ZEQ2 magic and NO plaintext
  // metadata: every identifying field (label, timestamp, "HITE", "KO42",
  // zeqond, Landauer verdict) moves INSIDE the AES-GCM envelope, so the bytes
  // are indistinguishable from random. Recognised by successful decryption,
  // not by a marker. FORMAT_V2 is written inside the encrypted envelope only.
  var FORMAT_V2 = 2;
  var HITE_PBKDF2_ITERS = 210000;
  // HITE_SALT_PREFIX is part of the frozen wire protocol — changing the
  // literal values here would invalidate every HITE ciphertext ever produced.
  // Keep as-is; the DISPLAY values coincidentally match.
  var HITE_SALT_PREFIX = "HULYAS.HITE.f=1.287Hz.tau=0.777s";
  // Sourced from /js/zeq-kernel-constants.js when preloaded; literal fallbacks
  // preserve behavior on pages that don't include it.
  var _ZKC = (typeof window !== 'undefined' && window.ZeqKernelConstants) || null;
  var ZEQOND_SEC = _ZKC ? _ZKC.TAU_ZQ_S : 0.777;
  var PULSE_HZ   = _ZKC ? _ZKC.F_H_HZ   : 1.287;
  var KO42_MIN_ENTROPY_BITS = 128;

  // ZEQ2 binary magic: 0x5A 0x45 0x51 0x32 0x02 0x00 0x00 0x00
  var ZEQ_MAGIC = new Uint8Array([0x5A, 0x45, 0x51, 0x32, 0x02, 0x00, 0x00, 0x00]);

  // Landauer constants
  var K_B = 1.380649e-23;        // Boltzmann constant (J/K)
  var T_ROOM = 300;              // Room temperature (K)
  var SOLAR_POWER = 3.828e26;    // Sun's luminosity (W)

  // ═══════════════════════════════════════════════════════════════════════
  // Byte helpers
  // ═══════════════════════════════════════════════════════════════════════

  function bytesToHex(buf) {
    var arr = new Uint8Array(buf), s = "";
    for (var i = 0; i < arr.length; i++) s += arr[i].toString(16).padStart(2, "0");
    return s;
  }

  function hexToBytes(hex) {
    var len = hex.length / 2;
    var out = new Uint8Array(len);
    for (var i = 0; i < len; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16);
    return out;
  }

  function utf8Bytes(str) { return new TextEncoder().encode(str); }
  function utf8String(buf) { return new TextDecoder().decode(buf); }

  function concatBytes() {
    var total = 0;
    for (var i = 0; i < arguments.length; i++) total += arguments[i].length;
    var out = new Uint8Array(total), off = 0;
    for (var j = 0; j < arguments.length; j++) {
      out.set(new Uint8Array(arguments[j]), off);
      off += arguments[j].length;
    }
    return out;
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Zeqond Timing
  // ═══════════════════════════════════════════════════════════════════════

  function currentUnixSec() { return Date.now() / 1000; }
  function currentZeqond() { return Math.floor(currentUnixSec() / ZEQOND_SEC); }
  function currentPhase() { return (currentUnixSec() % ZEQOND_SEC) / ZEQOND_SEC; }

  // ═══════════════════════════════════════════════════════════════════════
  // KO42 Entropy Harvesting
  // ═══════════════════════════════════════════════════════════════════════
  //
  // Collects environmental entropy from mouse, touch, timing, and
  // device events. Mixed with crypto.getRandomValues for defense in depth.
  // The HITE standard requires >= 128 bits before any crypto operation.

  var _entropyPool = new Uint8Array(32);
  var _entropyBits = 0;
  var _entropyIdx = 0;
  var _lastTime = 0;
  var _collecting = false;
  var _entropyCallbacks = [];

  function _mixByte(b) {
    // XOR into pool with rotation
    _entropyPool[_entropyIdx] ^= b;
    _entropyIdx = (_entropyIdx + 1) % _entropyPool.length;
    _entropyBits = Math.min(_entropyBits + 1, 256);
    // Notify listeners
    if (_entropyBits >= KO42_MIN_ENTROPY_BITS && _entropyCallbacks.length > 0) {
      var cbs = _entropyCallbacks.splice(0);
      for (var i = 0; i < cbs.length; i++) cbs[i](_entropyBits);
    }
  }

  function _onMouseMove(e) {
    var now = performance.now();
    _mixByte((e.clientX ^ e.clientY) & 0xFF);
    _mixByte((now * 1000) & 0xFF);
    _mixByte(((e.clientX * 7 + e.clientY * 13) >>> 4) & 0xFF);
    _lastTime = now;
  }

  function _onTouchMove(e) {
    if (!e.touches || !e.touches[0]) return;
    var t = e.touches[0];
    _mixByte((t.clientX ^ t.clientY) & 0xFF);
    _mixByte((performance.now() * 1000) & 0xFF);
  }

  function _onKeyDown(e) {
    _mixByte(e.keyCode & 0xFF);
    _mixByte((performance.now() * 100) & 0xFF);
  }

  function _onDeviceMotion(e) {
    if (!e.accelerationIncludingGravity) return;
    var a = e.accelerationIncludingGravity;
    _mixByte(((a.x || 0) * 100) & 0xFF);
    _mixByte(((a.y || 0) * 100) & 0xFF);
    _mixByte(((a.z || 0) * 100) & 0xFF);
  }

  // Timer-based entropy: collect jitter from setTimeout variance
  var _timerInterval = null;

  function _timerEntropy() {
    var now = performance.now();
    var delta = now - _lastTime;
    _mixByte((delta * 10000) & 0xFF);
    _mixByte(((now * 37) >>> 3) & 0xFF);
    _lastTime = now;
  }

  function startEntropyCollection() {
    if (_collecting) return;
    _collecting = true;

    // Seed pool with system randomness first
    var seed = new Uint8Array(32);
    crypto.getRandomValues(seed);
    for (var i = 0; i < 32; i++) _entropyPool[i] ^= seed[i];

    // Attach listeners
    if (typeof document !== "undefined") {
      document.addEventListener("mousemove", _onMouseMove, { passive: true });
      document.addEventListener("touchmove", _onTouchMove, { passive: true });
      document.addEventListener("keydown", _onKeyDown, { passive: true });
    }
    if (typeof window !== "undefined" && window.DeviceMotionEvent) {
      window.addEventListener("devicemotion", _onDeviceMotion, { passive: true });
    }

    // Timer jitter collection every 50ms
    _lastTime = performance.now();
    _timerInterval = setInterval(_timerEntropy, 50);
  }

  function stopEntropyCollection() {
    if (!_collecting) return;
    _collecting = false;
    if (typeof document !== "undefined") {
      document.removeEventListener("mousemove", _onMouseMove);
      document.removeEventListener("touchmove", _onTouchMove);
      document.removeEventListener("keydown", _onKeyDown);
    }
    if (typeof window !== "undefined") {
      window.removeEventListener("devicemotion", _onDeviceMotion);
    }
    if (_timerInterval) { clearInterval(_timerInterval); _timerInterval = null; }
  }

  function getEntropyBits() { return _entropyBits; }

  /** Mix system random with KO42 entropy pool — returns mixed salt */
  function mixEntropy(systemRandom) {
    var mixed = new Uint8Array(systemRandom.length);
    for (var i = 0; i < systemRandom.length; i++) {
      mixed[i] = systemRandom[i] ^ _entropyPool[i % _entropyPool.length];
    }
    return mixed;
  }

  /**
   * Wait until at least `targetBits` of KO42 entropy have been gathered,
   * or until `timeoutMs` elapses (whichever comes first). Resolves with
   * the current entropy bit count. Never rejects — callers always get
   * to proceed; entropy below the threshold is logged but not fatal.
   *
   * Both arguments are optional. Defaults: targetBits = KO42_MIN_ENTROPY_BITS,
   * timeoutMs = no timeout (waits indefinitely — only safe when the caller
   * is sure entropy will arrive, e.g. from an active user typing).
   *
   * Bug fix (2026-05-09): previous implementation ignored both arguments
   * and hung forever when entropy never crossed the threshold (e.g.
   * automated tests, idle pages). Now honours the documented signature
   * used at every call site (`waitForEntropy(64, 3000)` in auth-v3.js
   * and wizard-home.js).
   */
  function waitForEntropy(targetBits, timeoutMs) {
    var target = (typeof targetBits === "number" && targetBits > 0) ? targetBits : KO42_MIN_ENTROPY_BITS;
    if (_entropyBits >= target) return Promise.resolve(_entropyBits);
    return new Promise(function (resolve) {
      var timer = null;
      var resolver = function (bits) {
        if (timer) { clearTimeout(timer); timer = null; }
        resolve(bits);
      };
      _entropyCallbacks.push(resolver);
      if (typeof timeoutMs === "number" && timeoutMs > 0) {
        timer = setTimeout(function () {
          var idx = _entropyCallbacks.indexOf(resolver);
          if (idx >= 0) _entropyCallbacks.splice(idx, 1);
          resolve(_entropyBits);
        }, timeoutMs);
      }
    });
  }

  // ═══════════════════════════════════════════════════════════════════════
  // HITE Key Derivation — PBKDF2-SHA-256, 210,000 iterations
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Derive an AES-256-GCM CryptoKey from a passphrase + salt.
   * @param {string} passphrase - User's PIN or passphrase
   * @param {Uint8Array} salt - 16 or 32 byte salt (should be KO42-mixed)
   * @param {number} [iters=210000] - PBKDF2 iterations
   * @returns {Promise<CryptoKey>}
   */
  async function deriveKey(passphrase, salt, iters) {
    if (typeof iters !== "number") iters = HITE_PBKDF2_ITERS;
    // Prepend HITE salt prefix to the salt for domain separation
    var hiteSalt = concatBytes(utf8Bytes(HITE_SALT_PREFIX), salt);
    var baseKey = await crypto.subtle.importKey(
      "raw", utf8Bytes(passphrase), { name: "PBKDF2" }, false, ["deriveKey"]
    );
    return crypto.subtle.deriveKey(
      { name: "PBKDF2", salt: hiteSalt, iterations: iters, hash: "SHA-256" },
      baseKey,
      { name: "AES-GCM", length: 256 },
      false,
      ["encrypt", "decrypt"]
    );
  }

  // ── A2 (2026-07-11) — Argon2id, memory-hard KDF for PIN-based secrets ──────
  // PINs are low-entropy; PBKDF2 is fast to brute-force on GPUs. Argon2id makes
  // each guess cost real memory + time. Self-hosted WASM (hash-wasm), no runtime
  // external fetch. Params are FIXED so decrypt reproduces the key; if ever
  // retuned, add the old param-set to the decrypt try-list.
  var ARGON2_MEM_KIB = 65536;   // 64 MiB
  var ARGON2_TIME    = 3;       // passes
  var ARGON2_PARALLEL = 1;
  var ARGON2_HASHLEN = 32;      // → AES-256 key

  function _argon2lib() {
    return (root && root.hashwasm) || (typeof globalThis !== "undefined" && globalThis.hashwasm) || null;
  }
  /** True if the Argon2id engine is loaded (script present). */
  function argon2Available() { var l = _argon2lib(); return !!(l && typeof l.argon2id === "function"); }

  /** Derive a 256-bit AES-GCM key from (pin, salt) via Argon2id. */
  async function deriveKeyArgon2(passphrase, salt) {
    var lib = _argon2lib();
    if (!lib || typeof lib.argon2id !== "function") throw new Error("argon2 unavailable");
    var raw = await lib.argon2id({
      password: passphrase,
      salt: salt,                       // Uint8Array (16 bytes ≥ 8 min)
      parallelism: ARGON2_PARALLEL,
      iterations: ARGON2_TIME,
      memorySize: ARGON2_MEM_KIB,
      hashLength: ARGON2_HASHLEN,
      outputType: "binary"
    });
    var keyBytes = raw instanceof Uint8Array ? raw : new Uint8Array(raw);
    return await crypto.subtle.importKey(
      "raw", keyBytes, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]
    );
  }

  // ═══════════════════════════════════════════════════════════════════════
  // HITE HMAC — For ZeqAuth equation hashing
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * HMAC-SHA-256 with HITE-derived key.
   * The salt is concatenated with the HITE prefix before key import,
   * domain-separating this HMAC from all other uses.
   *
   * @param {string} message - The equation string
   * @param {string} saltHex - 64-char hex salt
   * @returns {Promise<string>} 64-char hex HMAC digest
   */
  async function hmacHite(message, saltHex) {
    var saltBytes = hexToBytes(saltHex);
    // Domain-separated key: HITE prefix + raw salt
    var keyMaterial = concatBytes(utf8Bytes(HITE_SALT_PREFIX), saltBytes);
    var key = await crypto.subtle.importKey(
      "raw", keyMaterial, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
    );
    var sig = await crypto.subtle.sign("HMAC", key, utf8Bytes(message));
    return bytesToHex(sig);
  }

  /**
   * Generate a KO42-mixed random salt (32 bytes → 64 hex chars).
   * @returns {string} hex-encoded salt
   */
  function randomSaltHex() {
    var raw = new Uint8Array(32);
    crypto.getRandomValues(raw);
    var mixed = mixEntropy(raw);
    return bytesToHex(mixed);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Landauer Bound Calculation
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Calculate the thermodynamic Landauer bound for brute-forcing.
   * @param {number} [bits=256] - Key space in bits
   * @param {number} [temperature=300] - Temperature in Kelvin
   * @returns {{ energyJ: number, energyStr: string, sunsRequired: number, sunsStr: string, energyPerBit: number, verdict: string }}
   */
  function landauer(bits, temperature) {
    if (typeof bits !== "number") bits = 256;
    if (typeof temperature !== "number") temperature = T_ROOM;

    var energyPerBit = K_B * temperature * Math.log(2);
    // Total energy to brute-force = 2^bits * energyPerBit
    // Use log to avoid Infinity: log10(2^256 * E) = 256*log10(2) + log10(E)
    var log10Total = bits * Math.log10(2) + Math.log10(energyPerBit);
    var log10Suns = log10Total - Math.log10(SOLAR_POWER);

    // For display: use scientific notation
    var exponentE = Math.floor(log10Total);
    var mantissaE = Math.pow(10, log10Total - exponentE);
    var exponentS = Math.floor(log10Suns);
    var mantissaS = Math.pow(10, log10Suns - exponentS);

    var energyStr = mantissaE.toFixed(2) + "×10^" + exponentE;
    var sunsStr = mantissaS.toFixed(2) + "×10^" + exponentS;

    return {
      bits: bits,
      energyJ: log10Total,  // log10 of total energy (actual number is too large)
      energyStr: energyStr + " J",
      sunsRequired: log10Suns,
      sunsStr: sunsStr + " suns",
      energyPerBit: energyPerBit,
      verdict: exponentS > 6 ? "IMPOSSIBLE" : "FEASIBLE"
    };
  }

  // ═══════════════════════════════════════════════════════════════════════
  // .zeq Binary Format — Encrypt
  // ═══════════════════════════════════════════════════════════════════════
  //
  // Format: [MAGIC:8] [META_LEN:4] [META_JSON] [SALT:16] [IV:12] [CIPHERTEXT]
  //
  // Metadata JSON:
  //   { v, f, n, s, t, l: { e, s }, o: ["KO42"], entropy, z: { phase, zeqond } }

  /**
   * Build .zeq metadata object.
   * @param {string} label - Entry label / filename
   * @param {number} plainSize - Original plaintext size in bytes
   * @returns {object} Metadata for .zeq header
   */
  function buildMeta(label, plainSize) {
    var land = landauer(256);
    return {
      v: VERSION,
      f: "HITE",
      n: label || "equation",
      s: plainSize,
      t: new Date().toISOString(),
      l: { e: land.energyStr, s: land.sunsStr, verdict: land.verdict },
      o: ["KO42"],
      entropy: _entropyBits,
      z: { phase: parseFloat(currentPhase().toFixed(6)), zeqond: currentZeqond() }
    };
  }

  /**
   * Encrypt plaintext into a .zeq binary blob.
   *
   * @param {string} plaintext - The equation or data to encrypt
   * @param {string} passphrase - User's PIN
   * @param {string} [label] - Label for metadata
   * @returns {Promise<{ blob: Uint8Array, meta: object }>}
   */
  async function encrypt(plaintext, passphrase, label) {
    // Generate KO42-mixed salt and IV
    var sysSalt = new Uint8Array(16);
    crypto.getRandomValues(sysSalt);
    var salt = mixEntropy(sysSalt);

    var iv = new Uint8Array(12);
    crypto.getRandomValues(iv);

    // Derive key — Argon2id (memory-hard) when the engine is loaded, else fall
    // back to PBKDF2 so a download never fails. decrypt tries both, so either
    // choice round-trips.
    var key;
    if (argon2Available()) {
      try { key = await deriveKeyArgon2(passphrase, salt); }
      catch (_) { key = await deriveKey(passphrase, salt, HITE_PBKDF2_ITERS); }
    } else {
      key = await deriveKey(passphrase, salt, HITE_PBKDF2_ITERS);
    }

    // A1 — seal ALL metadata inside the ciphertext. The AES-GCM plaintext is a
    // state-contract envelope { v, k:"zeqsc", d:<payload>, m:<inner meta> }.
    // Nothing identifying is written in the clear.
    var innerMeta = { t: new Date().toISOString(), s: utf8Bytes(plaintext).length };
    if (label) innerMeta.l = String(label);
    var envelope = JSON.stringify({ v: FORMAT_V2, k: "zeqsc", d: plaintext, m: innerMeta });

    var encrypted = await crypto.subtle.encrypt(
      { name: "AES-GCM", iv: iv, tagLength: 128 },
      key,
      utf8Bytes(envelope)
    );
    var ciphertext = new Uint8Array(encrypted); // includes auth tag

    // On-disk layout: [SALT:16][IV:12][CIPHERTEXT] — headerless, marker-free.
    var blob = concatBytes(salt, iv, ciphertext);

    // `meta` is returned for the UI (Landauer bound, etc.) but is NEVER written
    // to disk anymore — purely in-memory at download time.
    var meta = buildMeta(label, utf8Bytes(plaintext).length);

    return { blob: blob, meta: meta, format: FORMAT_V2 };
  }

  /**
   * Decrypt a .zeq binary blob back to plaintext.
   * Supports ZEQ2 format. Returns null on wrong passphrase.
   *
   * @param {Uint8Array} zeqBlob - The .zeq binary
   * @param {string} passphrase - User's PIN
   * @returns {Promise<{ plaintext: string, meta: object } | null>}
   */
  async function decrypt(zeqBlob, passphrase) {
    if (!zeqBlob || zeqBlob.length < 24) return null;

    // Legacy ZEQ2 files (magic + plaintext metadata header) — kept working
    // forever so existing recovery files still open.
    var isLegacy = true;
    for (var m = 0; m < 4; m++) { if (zeqBlob[m] !== ZEQ_MAGIC[m]) { isLegacy = false; break; } }
    if (isLegacy) return await decryptZeq2(zeqBlob, passphrase);

    // A1 headerless format: [SALT:16][IV:12][CIPHERTEXT]. No marker — it is
    // recognised only by a successful AEAD decrypt.
    if (zeqBlob.length < 16 + 12 + 16) return null;
    var salt = zeqBlob.slice(0, 16);
    var iv = zeqBlob.slice(16, 28);
    var ciphertext = zeqBlob.slice(28);

    var plainStr = await tryDecryptKDFs(salt, iv, ciphertext, passphrase);
    if (plainStr === null) return null; // wrong PIN, corrupted, or not ours

    // Parse the sealed state-contract envelope; tolerate a bare plaintext too.
    try {
      var env = JSON.parse(plainStr);
      if (env && typeof env === "object" && typeof env.d === "string") {
        return { plaintext: env.d, meta: env.m || {}, format: env.v || FORMAT_V2 };
      }
    } catch (_) { /* decrypted but not our envelope — return raw below */ }
    return { plaintext: plainStr, meta: {}, format: 0 };
  }

  // Try the known key-derivation functions in order; the AES-GCM tag tells us
  // which one is right. A1 uses PBKDF2 only; A2 will PREPEND Argon2id here so
  // new files derive with Argon2id while every older file still opens.
  async function tryDecryptKDFs(salt, iv, ciphertext, passphrase) {
    var kdfs = [];
    if (argon2Available()) {
      kdfs.push(async function () { return await deriveKeyArgon2(passphrase, salt); });
    }
    // PBKDF2 covers A1 files, and legacy new-format files, always.
    kdfs.push(async function () { return await deriveKey(passphrase, salt, HITE_PBKDF2_ITERS); });
    for (var i = 0; i < kdfs.length; i++) {
      try {
        var key = await kdfs[i]();
        var plain = await crypto.subtle.decrypt(
          { name: "AES-GCM", iv: iv, tagLength: 128 }, key, ciphertext
        );
        return utf8String(plain);
      } catch (_) { /* wrong KDF or wrong PIN — try next */ }
    }
    return null;
  }

  // Legacy ZEQ2 reader: [MAGIC:8][META_LEN:4][META_JSON][SALT:16][IV:12][CT].
  async function decryptZeq2(zeqBlob, passphrase) {
    var off = 8; // skip magic
    var metaLen = (zeqBlob[off] << 24) | (zeqBlob[off + 1] << 16) | (zeqBlob[off + 2] << 8) | zeqBlob[off + 3];
    off += 4;
    var meta;
    try { meta = JSON.parse(utf8String(zeqBlob.slice(off, off + metaLen))); }
    catch (_) { return null; }
    off += metaLen;
    var salt = zeqBlob.slice(off, off + 16); off += 16;
    var iv = zeqBlob.slice(off, off + 12); off += 12;
    var ciphertext = zeqBlob.slice(off);
    try {
      var key = await deriveKey(passphrase, salt, HITE_PBKDF2_ITERS);
      var plain = await crypto.subtle.decrypt(
        { name: "AES-GCM", iv: iv, tagLength: 128 }, key, ciphertext
      );
      return { plaintext: utf8String(plain), meta: meta, format: 1, legacy: true };
    } catch (_) { return null; }
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Legacy format support — for backward-compatible vault decryption
  // ═══════════════════════════════════════════════════════════════════════

  /**
   * Decrypt a legacy vault entry (5 separate hex fields, 200k iters, no HITE prefix).
   * Used for entries created before HITE integration.
   *
   * @param {{ equation_ciphertext, equation_iv, equation_tag, pbkdf2_salt, pbkdf2_iters }} entry
   * @param {string} pin
   * @returns {Promise<string|null>} plaintext or null on failure
   */
  async function decryptLegacy(entry, pin) {
    try {
      // Legacy path: no HITE salt prefix, uses raw salt
      var baseKey = await crypto.subtle.importKey(
        "raw", utf8Bytes(pin), { name: "PBKDF2" }, false, ["deriveKey"]
      );
      var key = await crypto.subtle.deriveKey(
        { name: "PBKDF2", salt: hexToBytes(entry.pbkdf2_salt), iterations: entry.pbkdf2_iters, hash: "SHA-256" },
        baseKey,
        { name: "AES-GCM", length: 256 },
        false,
        ["decrypt"]
      );
      var ct = hexToBytes(entry.equation_ciphertext);
      var tag = hexToBytes(entry.equation_tag);
      var combined = concatBytes(ct, tag);
      var plain = await crypto.subtle.decrypt(
        { name: "AES-GCM", iv: hexToBytes(entry.equation_iv), tagLength: 128 },
        key,
        combined
      );
      return utf8String(plain);
    } catch (_) {
      return null;
    }
  }

  /**
   * Decrypt a legacy auth HMAC (plain HMAC-SHA256, no HITE prefix).
   * Used for login against identities created before HITE integration.
   *
   * @param {string} equation
   * @param {string} saltHex
   * @returns {Promise<string>} 64-char hex hash
   */
  async function hmacLegacy(equation, saltHex) {
    var key = await crypto.subtle.importKey(
      "raw", hexToBytes(saltHex), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]
    );
    var sig = await crypto.subtle.sign("HMAC", key, utf8Bytes(equation));
    return bytesToHex(sig);
  }

  // ═══════════════════════════════════════════════════════════════════════
  // Public API
  // ═══════════════════════════════════════════════════════════════════════

  var HiteCrypto = {
    // Version
    VERSION: VERSION,

    // 6.1.AUTH-FIX-2 — surface secure-context status to callers so the UI
    // can show a clean inline message instead of letting an unhandled
    // "Cannot read properties of undefined (reading 'importKey')" reach
    // a browser alert().
    SECURE_CONTEXT_OK: SECURE_CONTEXT_OK,
    SECURE_CONTEXT_REASON: SECURE_CONTEXT_REASON,
    requireSecureContext: _ensureSecureContext,

    // Constants
    HITE_PBKDF2_ITERS: HITE_PBKDF2_ITERS,
    HITE_SALT_PREFIX: HITE_SALT_PREFIX,
    ZEQ_MAGIC: ZEQ_MAGIC,

    // Byte helpers
    bytesToHex: bytesToHex,
    hexToBytes: hexToBytes,

    // Zeqond timing
    currentZeqond: currentZeqond,
    currentPhase: currentPhase,
    ZEQOND_SEC: ZEQOND_SEC,
    PULSE_HZ: PULSE_HZ,

    // KO42 Entropy
    startEntropyCollection: startEntropyCollection,
    stopEntropyCollection: stopEntropyCollection,
    getEntropyBits: getEntropyBits,
    waitForEntropy: waitForEntropy,
    mixEntropy: mixEntropy,

    // Key derivation
    deriveKey: deriveKey,

    // HMAC (HITE-standard)
    hmacHite: hmacHite,
    randomSaltHex: randomSaltHex,

    // Landauer bound
    landauer: landauer,

    // .zeq encrypt/decrypt
    encrypt: encrypt,
    decrypt: decrypt,
    buildMeta: buildMeta,
    FORMAT_V2: FORMAT_V2,
    tryDecryptKDFs: tryDecryptKDFs,
    deriveKeyArgon2: deriveKeyArgon2,
    argon2Available: argon2Available,

    // Legacy backward compatibility
    decryptLegacy: decryptLegacy,
    hmacLegacy: hmacLegacy
  };

  // Export
  if (typeof module !== "undefined" && module.exports) {
    module.exports = HiteCrypto;
  }
  root.HiteCrypto = HiteCrypto;

})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);
