{"format":"sanctum-bundle/1","html":"<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; base-uri 'none'; form-action 'none'\">\n<meta name=\"referrer\" content=\"no-referrer\">\n<meta name=\"theme-color\" content=\"#0a0a0a\">\n<title>Enigma Shamir for Sanctum</title>\n<meta name=\"description\" content=\"Split a secret into shares and combine them back. Runs offline and asks for no access.\">\n<link rel=\"icon\" href=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' width='32' height='32'%3E%3Crect width='32' height='32' rx='6' fill='%230A0A0A'/%3E%3Crect x='3' y='3' width='26' height='26' rx='5' fill='none' stroke='%23FBFF00' stroke-width='1.5'/%3E%3Ctext x='16' y='20.5' text-anchor='middle' font-family='Poppins,Helvetica,Arial,sans-serif' font-size='10' font-weight='700' fill='%23FFFFFF' letter-spacing='-0.3'%3Esss%3C/text%3E%3C/svg%3E\">\n<style>\n  :root{\n    --ink:#000000; --ink-soft:#0a0a0a; --panel:#111111; --paper:#FFFFFF; --volt:#FBFF00;\n    --ash:#8A8A8A; --ash-soft:#5E5E5E; --line:rgba(255,255,255,.12); --line-strong:rgba(255,255,255,.24); --field:rgba(255,255,255,.5);\n    --danger:#FF5A4E; --ok:#7CFF8A;\n    --display:'Poppins',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n    --body:'Inter',system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;\n    --mono:'Space Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace;\n    --gap:clamp(16px,4vw,48px);\n    color-scheme:dark;\n  }\n  *{box-sizing:border-box}\n  html,body{margin:0;background:#0A0A0A;color:var(--paper)}\n  body{font-family:var(--body);font-size:15px;line-height:1.6;-webkit-font-smoothing:antialiased;min-height:100vh;\n    padding:env(safe-area-inset-top,0) 0 env(safe-area-inset-bottom,0)}\n  [hidden]{display:none!important}\n  a{color:inherit}\n  code{font-family:var(--mono);font-size:.92em}\n  .wrap{max-width:980px;margin:0 auto;padding:0 var(--gap)}\n  .i{width:16px;height:16px;flex:none;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}\n\n  /* header */\n  header.bar{position:sticky;top:0;z-index:40;background:rgba(0,0,0,.88);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}\n  .bar-in{display:flex;align-items:center;gap:16px;min-height:60px;padding-block:8px}\n  .mark{display:flex;align-items:center;gap:12px}\n  .ets-badge{display:inline-flex;align-items:center;justify-content:center;width:42px;height:28px;background:var(--paper);border-radius:0}\n  .ets{font-family:var(--body);font-weight:800;font-size:13px;color:#000;letter-spacing:.01em;line-height:1}\n  .wordmark{font-family:var(--mono);font-size:12px;letter-spacing:.16em;text-transform:uppercase}\n  .wordmark b{color:var(--ash);font-weight:400}\n\n  /* hero */\n  .hero{padding:clamp(36px,7vw,80px) 0 clamp(24px,4vw,40px)}\n  .eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.28em;text-transform:uppercase;color:var(--ash);margin:0 0 18px}\n  h1{font-family:var(--display);font-weight:800;font-size:clamp(38px,8.5vw,88px);line-height:.95;letter-spacing:-.02em;margin:0}\n  .dash{display:inline-block;width:.46em;height:.13em;background:var(--volt);vertical-align:.3em;margin:0 .04em;border-radius:0}\n  .lede{max-width:640px;margin:24px 0 0;font-size:16px}\n  .lede span{color:var(--ash)}\n\n  .banner{border:1px solid var(--line-strong);color:var(--ash);padding:12px 16px;margin:0 0 16px;font-size:13px;display:flex;gap:10px;align-items:flex-start}\n  .banner .i{margin-top:3px}\n  .banner.bad{border-color:var(--danger);color:var(--danger)}\n  .banner.warn{border-color:rgba(251,255,0,.5);color:var(--paper)}\n  .banner.warn .i{color:var(--paper)}\n\n  section.block{padding:clamp(24px,4vw,40px) 0;border-top:1px solid var(--line)}\n  .kicker{font-family:var(--mono);font-size:11px;letter-spacing:.26em;text-transform:uppercase;color:var(--ash);margin:0 0 12px;display:flex;gap:12px;align-items:center}\n  .kicker::before{content:\"\";width:28px;height:2px;background:var(--volt);border-radius:0}\n  h2{font-family:var(--display);font-weight:700;font-size:clamp(22px,3.4vw,32px);line-height:1.1;letter-spacing:-.015em;margin:0 0 6px}\n  h2:focus{outline:none}\n  h2:focus-visible{outline:2px solid var(--volt);outline-offset:4px}\n  .muted{color:var(--ash)}\n  .small{font-size:13px}\n\n  /* tabs */\n  .tabs{display:flex;border:1px solid var(--field);width:max-content;max-width:100%;margin:0 0 24px}\n  .tabs button{background:transparent;color:var(--ash);border:0;border-radius:0;padding:11px 20px;font-family:var(--mono);font-size:12px;letter-spacing:.14em;text-transform:uppercase;cursor:pointer;display:inline-flex;gap:8px;align-items:center}\n  .tabs button:hover{color:var(--paper)}\n  .tabs button[aria-selected=\"true\"]{background:var(--volt);color:#000}\n  .tabs button + button{border-left:1px solid var(--field)}\n\n  /* cards and forms */\n  .cols{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(0,1fr);gap:24px;align-items:start}\n  @media (max-width:860px){.cols{grid-template-columns:minmax(0,1fr)}}\n  .card{border:1px solid var(--line);background:var(--panel);padding:clamp(16px,3vw,28px);min-width:0;margin:0 0 24px}\n  .cols .card{margin:0}\n  .card h3{font-family:var(--display);font-weight:600;font-size:18px;letter-spacing:-.01em;margin:0 0 14px}\n  label.f{display:block;margin:0 0 14px}\n  .lab{display:block;font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ash);margin-bottom:6px}\n  input[type=text],input[type=number],select,textarea{\n    width:100%;background:#000;color:var(--paper);border:1px solid var(--field);border-radius:0;\n    padding:10px 12px;font:inherit;font-size:14px;outline:none}\n  textarea{font-family:var(--mono);font-size:13px;resize:vertical;line-height:1.5;min-height:92px}\n  #secret{min-height:150px}\n  @media (max-width:860px){.seg button{min-height:40px}}\n  input:focus,select:focus,textarea:focus{border-color:var(--volt);box-shadow:0 0 0 1px var(--volt)}\n  input[aria-invalid=\"true\"],textarea[aria-invalid=\"true\"]{border-color:var(--danger)}\n  .masked{-webkit-text-security:disc;text-security:disc}\n  .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}\n  .row.end{justify-content:space-between}\n  .pair{display:grid;grid-template-columns:1fr 1fr;gap:14px}\n  @media (max-width:420px){.pair{grid-template-columns:1fr}}\n  button{font-family:var(--body)}\n  button.btn{background:var(--volt);color:#000;border:1px solid var(--volt);border-radius:0;padding:10px 16px;font-weight:700;\n    font-size:12px;letter-spacing:.1em;text-transform:uppercase;cursor:pointer;display:inline-flex;gap:8px;align-items:center;justify-content:center;line-height:1.2}\n  button.btn:hover:not(:disabled){background:#fff;border-color:#fff}\n  button.btn:disabled{opacity:.4;cursor:not-allowed}\n  button.ghost{background:transparent;color:var(--paper);border:1px solid var(--field)}\n  button.btn:disabled,button.ghost:disabled{background:transparent;color:var(--ash);border-color:var(--line-strong);opacity:1}\n  button.ghost:hover:not(:disabled){border-color:var(--volt);color:var(--paper);background:rgba(251,255,0,.08)}\n  button.sm{padding:6px 10px;font-size:11px}\n  button:focus-visible,summary:focus-visible,.tabs button:focus-visible{outline:2px solid var(--volt);outline-offset:2px}\n  .seg{display:inline-flex;border:1px solid var(--field)}\n  .seg button{background:transparent;color:var(--ash);border:0;border-radius:0;padding:0 14px;min-height:34px;font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;cursor:pointer}\n  .seg button + button{border-left:1px solid var(--field)}\n  .seg button[aria-pressed=\"true\"]{background:var(--paper);color:#000}\n  details{border:1px solid var(--line);padding:10px 12px;margin:0 0 14px}\n  details summary{padding:6px 0;cursor:pointer;font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ash)}\n  details[open] > summary{margin-bottom:12px;color:var(--paper)}\n  details.how{padding:14px 16px}\n  details.how ul{margin:0;padding-left:20px;color:var(--ash);font-size:14px}\n  details.how li{margin:0 0 10px}\n  details.how li b{color:var(--paper);font-weight:600}\n  .hint{font-size:12.5px;color:var(--ash);margin:6px 0 14px}\n  .hint.err,.msg.err{color:var(--danger)}\n  .msg{min-height:1.5em;margin:10px 0 0;font-size:13px}\n  .msg.ok{color:var(--ok)}\n  .count{font-family:var(--mono);font-size:12px;color:var(--ash)}\n  .count.err{color:var(--danger)}\n  .radio{display:flex;gap:10px;align-items:flex-start;margin:0 0 10px;font-size:14px}\n  .radio input{margin-top:5px;accent-color:var(--volt)}\n  .radio small{display:block;color:var(--ash);font-size:12.5px}\n  .sentence{font-size:14px;margin:2px 0 14px}\n  .sentence b{font-family:var(--mono);color:var(--paper);font-weight:700}\n  .softwarn{font-size:12.5px;color:var(--paper);display:flex;gap:8px;align-items:flex-start;margin:0 0 10px}\n  .softwarn .i{color:var(--paper);margin-top:3px}\n\n  /* results */\n  .result.stale .sharelist,.result.stale .sumline{opacity:.4}\n  .sumline{font-size:15px;margin:0 0 16px}\n  .sumline code{color:var(--paper);font-weight:700}\n  ol.sharelist{list-style:none;margin:0 0 16px;padding:0;display:grid;gap:10px}\n  .share{border:1px solid var(--line-strong);background:#000;padding:10px 12px}\n  .share-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:6px}\n  .share-n{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ash)}\n  .share-text,.out{font-family:var(--mono);font-size:12.5px;line-height:1.5;overflow-wrap:anywhere;word-break:break-all;user-select:all;-webkit-user-select:all;margin:0;white-space:pre-wrap}\n  .caution{border:1px solid rgba(251,255,0,.5);padding:12px 14px;font-size:13px;margin:16px 0 0;display:flex;gap:10px;align-items:flex-start}\n  .caution .i{color:var(--paper);margin-top:3px}\n  .fallback{margin-top:14px}\n  .okline{display:flex;gap:10px;align-items:flex-start;margin:0 0 6px;font-weight:600}\n  .okline .i{color:var(--ok);width:20px;height:20px;margin-top:2px}\n  .okline.neutral .i{color:var(--ash)}\n  .okline.warn .i{color:var(--paper)}\n  .secretbox{border:1px solid var(--line-strong);background:#000;padding:12px 14px;margin:14px 0 10px;min-height:48px}\n  .secretbox .out{user-select:none;-webkit-user-select:none}\n  .secretbox.shown .out{user-select:all;-webkit-user-select:all}\n  .hash{font-family:var(--mono);font-size:12px;color:var(--paper);overflow-wrap:anywhere;word-break:break-all;margin:0 0 8px}\n\n  /* share list on combine */\n  ul.parsed{list-style:none;margin:12px 0 0;padding:0;display:grid;gap:6px}\n  ul.parsed li{display:flex;gap:10px;align-items:flex-start;font-size:13px;border-left:2px solid var(--line-strong);padding:4px 0 4px 10px}\n  ul.parsed li .i{margin-top:3px}\n  ul.parsed li.ok{border-left-color:var(--ok)}\n  ul.parsed li.ok .i{color:var(--ok)}\n  ul.parsed li.bad{border-left-color:var(--danger)}\n  ul.parsed li.bad .i{color:var(--danger)}\n  ul.parsed li.note{border-left-color:var(--ash-soft);color:var(--ash)}\n  ul.parsed li.note .i{color:var(--ash)}\n  ul.parsed .who{font-family:var(--mono);font-size:12px;letter-spacing:.06em;white-space:nowrap}\n  ul.parsed .sid{font-family:var(--mono);font-size:12px;color:var(--ash)}\n  .status{display:flex;gap:10px;align-items:flex-start;font-size:14px;margin:14px 0}\n  .status .i{margin-top:3px}\n  .status.bad{color:var(--danger)}\n  .status.ready .i{color:var(--ok)}\n  .status.idle{color:var(--ash)}\n\n  .toast{position:fixed;left:50%;bottom:calc(24px + env(safe-area-inset-bottom,0px));transform:translateX(-50%);background:var(--volt);color:#000;\n    padding:10px 16px;font-family:var(--mono);font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;z-index:60;max-width:calc(100vw - 32px)}\n  footer{padding:32px 0 56px;color:var(--ash);font-size:12px;border-top:1px solid var(--line)}\n  footer p{margin:0 0 8px;max-width:760px}\n  .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}\n  @media (prefers-reduced-motion:no-preference){ .working .i{animation:spin 1s linear infinite} }\n  @keyframes spin{to{transform:rotate(360deg)}}\n</style>\n</head>\n<body>\n\n<!-- Icons: Lucide (ISC licence), inlined. -->\n<svg width=\"0\" height=\"0\" style=\"position:absolute\" aria-hidden=\"true\" focusable=\"false\">\n  <defs>\n    <symbol id=\"i-scissors\" viewBox=\"0 0 24 24\"><circle cx=\"6\" cy=\"6\" r=\"3\"/><path d=\"M8.12 8.12 12 12\"/><path d=\"M20 4 8.12 15.88\"/><circle cx=\"6\" cy=\"18\" r=\"3\"/><path d=\"M14.8 14.8 20 20\"/></symbol>\n    <symbol id=\"i-combine\" viewBox=\"0 0 24 24\"><path d=\"M10 18H5a3 3 0 0 1-3-3v-1\"/><path d=\"M14 2a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2\"/><path d=\"M20 2a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2\"/><path d=\"m7 21 3-3-3-3\"/><rect x=\"14\" y=\"14\" width=\"8\" height=\"8\" rx=\"2\"/><rect x=\"2\" y=\"2\" width=\"8\" height=\"8\" rx=\"2\"/></symbol>\n    <symbol id=\"i-copy\" viewBox=\"0 0 24 24\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></symbol>\n    <symbol id=\"i-eye\" viewBox=\"0 0 24 24\"><path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></symbol>\n    <symbol id=\"i-eye-off\" viewBox=\"0 0 24 24\"><path d=\"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49\"/><path d=\"M14.084 14.158a3 3 0 0 1-4.242-4.242\"/><path d=\"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143\"/><path d=\"m2 2 20 20\"/></symbol>\n    <symbol id=\"i-dices\" viewBox=\"0 0 24 24\"><rect width=\"12\" height=\"12\" x=\"2\" y=\"10\" rx=\"2\" ry=\"2\"/><path d=\"m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6\"/><path d=\"M6 18h.01\"/><path d=\"M10 14h.01\"/><path d=\"M15 6h.01\"/><path d=\"M18 9h.01\"/></symbol>\n    <symbol id=\"i-alert\" viewBox=\"0 0 24 24\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></symbol>\n    <symbol id=\"i-x\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m15 9-6 6\"/><path d=\"m9 9 6 6\"/></symbol>\n    <symbol id=\"i-ok\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"m9 12 2 2 4-4\"/></symbol>\n    <symbol id=\"i-trash\" viewBox=\"0 0 24 24\"><path d=\"M3 6h18\"/><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"/><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"/><line x1=\"10\" x2=\"10\" y1=\"11\" y2=\"17\"/><line x1=\"14\" x2=\"14\" y1=\"11\" y2=\"17\"/></symbol>\n    <symbol id=\"i-shield\" viewBox=\"0 0 24 24\"><path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\"/><path d=\"m9 12 2 2 4-4\"/></symbol>\n    <symbol id=\"i-info\" viewBox=\"0 0 24 24\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 16v-4\"/><path d=\"M12 8h.01\"/></symbol>\n    <symbol id=\"i-refresh\" viewBox=\"0 0 24 24\"><path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></symbol>\n    <symbol id=\"i-clip\" viewBox=\"0 0 24 24\"><rect width=\"8\" height=\"4\" x=\"8\" y=\"2\" rx=\"1\" ry=\"1\"/><path d=\"M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2\"/><path d=\"m9 14 2 2 4-4\"/></symbol>\n  </defs>\n</svg>\n\n<header class=\"bar\">\n  <div class=\"wrap bar-in\">\n    <span class=\"mark\" aria-label=\"Enigma Technologies Solutions\">\n      <span class=\"ets-badge\"><span class=\"ets\">ETS</span></span>\n      <span class=\"wordmark\">Enigma <b>/ Shamir · Sanctum</b></span>\n    </span>\n  </div>\n</header>\n\n<main>\n  <section class=\"hero\">\n    <div class=\"wrap\">\n      <p class=\"eyebrow\">Enigma Technologies Solutions · Secret sharing for Sanctum</p>\n      <h1>split<span class=\"dash\"></span>combine</h1>\n      <p class=\"lede\">Split a secret into shares. Any <em>k</em> of them rebuild it. <span>Fewer than the needed number reveal nothing about the secret's content; the size bucket and the needed number are visible in every share. Everything runs in this window. Nothing is stored or sent.</span></p>\n    </div>\n  </section>\n\n  <div class=\"wrap\">\n    <div class=\"banner bad\" id=\"noCrypto\" role=\"alert\" hidden>\n      <svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-alert\"/></svg>\n      <span>This window has no secure random source, so nothing can be split. Update Sanctum and try again.</span>\n    </div>\n    <div class=\"banner\" id=\"noCore\" role=\"alert\" hidden>\n      <svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-alert\"/></svg>\n      <span>The secret sharing code did not load. This file is incomplete.</span>\n    </div>\n    <div class=\"banner\">\n      <svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-info\"/></svg>\n      <span>Closing this window discards the secret, the shares and any result.</span>\n    </div>\n  </div>\n\n  <section class=\"block\" id=\"app\">\n    <div class=\"wrap\">\n      <div class=\"tabs\" role=\"tablist\" aria-label=\"Mode\">\n        <button type=\"button\" role=\"tab\" id=\"tabSplit\" aria-selected=\"true\" aria-controls=\"panelSplit\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-scissors\"/></svg>Split</button>\n        <button type=\"button\" role=\"tab\" id=\"tabCombine\" aria-selected=\"false\" aria-controls=\"panelCombine\" tabindex=\"-1\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-combine\"/></svg>Combine</button>\n      </div>\n\n      <!-- ================= SPLIT ================= -->\n      <div id=\"panelSplit\" role=\"tabpanel\" aria-labelledby=\"tabSplit\">\n        <p class=\"kicker\">01 · Split</p>\n        <h2>Turn one secret into shares</h2>\n        <p class=\"muted small\" style=\"margin:0 0 24px;max-width:640px\">Enter a secret, choose how many shares to make and how many are needed to rebuild it, then split.</p>\n\n        <div class=\"cols\">\n          <div class=\"card\">\n            <h3>Secret</h3>\n            <div class=\"row end\" style=\"margin-bottom:10px\">\n              <div class=\"seg\" role=\"group\" aria-label=\"Secret type\">\n                <button type=\"button\" id=\"modeText\" aria-pressed=\"true\">Text</button>\n                <button type=\"button\" id=\"modeHex\" aria-pressed=\"false\">Hex</button>\n              </div>\n              <span class=\"row\" style=\"gap:12px\"><span class=\"count\" id=\"sCount\">0 of 1024 bytes</span>\n              <button type=\"button\" class=\"btn ghost sm\" id=\"sClear\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-trash\"/></svg><span>Clear</span></button></span>\n            </div>\n            <label class=\"f\" for=\"secret\"><span class=\"lab\" id=\"secretLab\">Secret (text, UTF-8)</span></label>\n            <textarea id=\"secret\" class=\"masked\" rows=\"4\" autocomplete=\"off\" autocapitalize=\"off\" autocorrect=\"off\" spellcheck=\"false\" maxlength=\"8192\" aria-describedby=\"sCount sHint sErr\" style=\"margin-top:-8px\"></textarea>\n            <p class=\"hint\" id=\"sHint\">Hidden while you type. Line breaks are kept.</p>\n            <p class=\"hint err\" id=\"sErr\" hidden></p>\n            <div class=\"row\">\n              <button type=\"button\" class=\"btn ghost sm\" id=\"sToggle\" aria-pressed=\"false\" aria-label=\"Show secret\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-eye\"/></svg><span aria-hidden=\"true\">Show</span></button>\n              <button type=\"button\" class=\"btn ghost sm\" id=\"sGen\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-dices\"/></svg><span>Generate</span></button>\n              <label class=\"row\" style=\"gap:6px;flex-wrap:nowrap\"><span class=\"lab\" style=\"margin:0\">Length</span>\n                <input type=\"number\" id=\"genLen\" value=\"32\" min=\"8\" max=\"128\" step=\"1\" style=\"width:76px;padding:6px 8px\" aria-describedby=\"genInfo\"></label>\n            </div>\n            <p class=\"hint\" id=\"genInfo\">Generate makes a random secret from 28 easy to read letters and digits. At 32 characters that is 153.8 bits.</p>\n            <details id=\"sHashBox\">\n              <summary>Check value (SHA-256)</summary>\n              <p class=\"hash\" id=\"sHash\" aria-live=\"polite\">Show the secret to see this value.</p>\n              <p class=\"hint\" style=\"margin:0\">Anyone who sees this value can test guesses against it. Do not store it next to the shares.</p>\n            </details>\n          </div>\n\n          <div class=\"card\">\n            <h3>Shares</h3>\n            <div class=\"pair\">\n              <label class=\"f\"><span class=\"lab\">Shares (n)</span>\n                <input type=\"number\" id=\"nShares\" value=\"5\" min=\"2\" max=\"255\" step=\"1\" inputmode=\"numeric\" autocomplete=\"off\" aria-describedby=\"kSentence\"></label>\n              <label class=\"f\"><span class=\"lab\">Needed (k)</span>\n                <input type=\"number\" id=\"kNeeded\" value=\"3\" min=\"2\" max=\"255\" step=\"1\" inputmode=\"numeric\" autocomplete=\"off\" aria-describedby=\"kSentence\"></label>\n            </div>\n            <p class=\"sentence\" id=\"kSentence\" aria-live=\"polite\"></p>\n            <p class=\"softwarn\" id=\"kWarn\" hidden><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-alert\"/></svg><span></span></p>\n            <label class=\"f\"><span class=\"lab\">Label (optional)</span>\n              <input type=\"text\" id=\"label\" maxlength=\"32\" autocomplete=\"off\" autocapitalize=\"off\" spellcheck=\"false\" aria-describedby=\"labelHint\"></label>\n            <p class=\"hint\" id=\"labelHint\" style=\"margin-top:-8px\">Letters, digits, dot, underscore, hyphen; up to 32. Helps you tell sets apart. It is not secret and not checked.</p>\n          </div>\n          <div class=\"card\" style=\"grid-column:1/-1\">\n            <details style=\"margin:0\" id=\"optBox\">\n              <summary>Options</summary>\n              <fieldset style=\"border:0;padding:0;margin:0 0 12px\">\n                <legend class=\"lab\" style=\"padding:0\">Share format</legend>\n                <label class=\"radio\"><input type=\"radio\" name=\"fmt\" value=\"ss1\" checked><span>Sanctum (recommended)<small>Shares record how many are needed and which set they belong to. A check value catches wrong or missing shares.</small></span></label>\n                <label class=\"radio\"><input type=\"radio\" name=\"fmt\" value=\"plain\"><span>Plain, compatible with ssss-made-easy and shamir-secret-sharing<small>No checks. Wrong or too few shares give a wrong answer without a warning. Use it to exchange shares with those tools.</small></span></label>\n              </fieldset>\n              <label class=\"f\"><span class=\"lab\">Hide secret length</span>\n                <select id=\"pad\">\n                  <option value=\"0\">Off</option>\n                  <option value=\"16\">16 bytes</option>\n                  <option value=\"32\" selected>32 bytes</option>\n                  <option value=\"64\">64 bytes</option>\n                  <option value=\"128\">128 bytes</option>\n                  <option value=\"256\">256 bytes</option>\n                </select></label>\n              <p class=\"hint\" id=\"padHint\" style=\"margin:0\">Rounds the hidden data up to a multiple of this size, so a share does not show the exact length. It hides the length inside that range, not the fact that a secret exists.</p>\n            </details>\n          </div>\n        </div>\n\n        <div class=\"row\" style=\"margin:24px 0 0\">\n          <button type=\"button\" class=\"btn\" id=\"btnSplit\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-scissors\"/></svg><span>Split</span></button>\n        </div>\n        <p class=\"msg\" id=\"splitMsg\" role=\"status\" aria-live=\"polite\"></p>\n\n        <div class=\"card result\" id=\"splitResult\" hidden style=\"margin-top:16px\">\n          <h3 id=\"splitResultTitle\" tabindex=\"-1\">Shares</h3>\n          <p class=\"sumline\" id=\"splitSummary\" aria-live=\"polite\"></p>\n          <p class=\"msg err\" id=\"splitStale\" role=\"status\" hidden>Inputs changed. Split again.</p>\n          <ol class=\"sharelist\" id=\"shareList\"></ol>\n          <div class=\"row\">\n            <button type=\"button\" class=\"btn ghost\" id=\"btnCopyAll\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-copy\"/></svg><span>Copy all (one per line)</span></button>\n            <button type=\"button\" class=\"btn ghost\" id=\"btnCheck\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-refresh\"/></svg><span>Check these shares</span></button>\n            <button type=\"button\" class=\"btn ghost\" id=\"btnClearClip\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-clip\"/></svg><span>Empty clipboard</span></button>\n            <button type=\"button\" class=\"btn ghost\" id=\"btnWipeSplit\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-trash\"/></svg><span>Clear everything</span></button>\n          </div>\n          <p class=\"msg\" id=\"checkMsg\" role=\"status\" aria-live=\"polite\"></p>\n          <div class=\"caution\">\n            <svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-alert\"/></svg>\n            <span id=\"cautionText\"></span>\n          </div>\n          <div class=\"fallback\" id=\"splitFallback\" hidden>\n            <label class=\"lab\" for=\"splitFallbackText\">Copy was blocked. The text is selected; press Ctrl+C or Cmd+C.</label>\n            <textarea id=\"splitFallbackText\" readonly rows=\"4\" spellcheck=\"false\"></textarea>\n          </div>\n        </div>\n      </div>\n\n      <!-- ================= COMBINE ================= -->\n      <div id=\"panelCombine\" role=\"tabpanel\" aria-labelledby=\"tabCombine\" hidden>\n        <p class=\"kicker\">02 · Combine</p>\n        <h2>Rebuild a secret from shares</h2>\n        <p class=\"muted small\" style=\"margin:0 0 24px;max-width:640px\">Paste at least as many shares as were needed. Extra shares are fine.</p>\n\n        <div class=\"card\">\n          <label class=\"f\" for=\"shares\"><span class=\"lab\">Paste shares, one per line</span></label>\n          <textarea id=\"shares\" rows=\"6\" maxlength=\"1000000\" aria-describedby=\"cStatus\" autocomplete=\"off\" autocapitalize=\"off\" autocorrect=\"off\" spellcheck=\"false\" placeholder=\"ops-ss1-3-0b704569-80f380b5a1c4e7d20b9f3a4d6e18c2f5-c852&#10;ops-ss1-3-0b704569-3be1d09c77f2a4185ce0b6d3912f07a8-51e0\" style=\"margin-top:-8px\"></textarea>\n          <ul class=\"parsed\" id=\"parsedList\" aria-label=\"Pasted shares\"></ul>\n          <div class=\"status idle\" id=\"cStatus\" role=\"status\" aria-live=\"polite\">\n            <svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-info\"/></svg><span>Paste at least two shares.</span>\n          </div>\n          <div id=\"plainThr\" hidden>\n            <label class=\"f\" style=\"max-width:280px\"><span class=\"lab\">How many shares were needed (optional)</span>\n              <input type=\"number\" id=\"cThr\" min=\"2\" max=\"255\" step=\"1\" inputmode=\"numeric\" autocomplete=\"off\" aria-describedby=\"cThrHint cStatus\"></label>\n            <p class=\"hint\" id=\"cThrHint\" style=\"margin-top:-8px\">Plain shares do not record this. If you fill it in, extra shares are checked against each other.</p>\n          </div>\n          <div class=\"row\">\n            <button type=\"button\" class=\"btn\" id=\"btnCombine\" disabled><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-combine\"/></svg><span>Combine</span></button>\n            <button type=\"button\" class=\"btn ghost\" id=\"btnCClear\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-trash\"/></svg><span>Clear</span></button>\n          </div>\n        </div>\n\n        <div class=\"banner bad\" id=\"combineErr\" role=\"alert\" hidden>\n          <svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-x\"/></svg>\n          <span id=\"combineErrText\"></span>\n        </div>\n\n        <div class=\"card result\" id=\"combineResult\" hidden>\n          <h3 id=\"combineResultTitle\" tabindex=\"-1\">Secret</h3>\n          <div id=\"cVerdict\"></div>\n          <div id=\"cWarnings\"></div>\n          <div class=\"secretbox\" id=\"cBox\"><pre class=\"out\" id=\"cOut\" role=\"group\" tabindex=\"0\" aria-label=\"Rebuilt secret, hidden\"></pre></div>\n          <p class=\"count\" id=\"cMeta\" style=\"margin:0 0 12px\"></p>\n          <p class=\"hint\" id=\"cViewNote\" hidden></p>\n          <div class=\"row\">\n            <button type=\"button\" class=\"btn ghost sm\" id=\"cToggle\" aria-pressed=\"false\" aria-label=\"Show secret\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-eye\"/></svg><span aria-hidden=\"true\">Show</span></button>\n            <div class=\"seg\" role=\"group\" aria-label=\"View as\">\n              <button type=\"button\" id=\"viewText\" aria-pressed=\"true\">Text</button>\n              <button type=\"button\" id=\"viewHex\" aria-pressed=\"false\">Hex</button>\n            </div>\n            <button type=\"button\" class=\"btn ghost sm\" id=\"cCopy\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-copy\"/></svg><span>Copy</span></button>\n            <button type=\"button\" class=\"btn ghost sm\" id=\"cWipe\"><svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-trash\"/></svg><span>Clear</span></button>\n          </div>\n          <details id=\"cHashBox\" style=\"margin-top:16px\">\n            <summary>Check value (SHA-256)</summary>\n            <p class=\"hash\" id=\"cHash\" aria-live=\"polite\">Show the secret to see this value.</p>\n            <p class=\"hint\" style=\"margin:0\">Anyone who sees this value can test guesses against it. Do not store it next to the shares.</p>\n          </details>\n          <div class=\"fallback\" id=\"combineFallback\" hidden>\n            <label class=\"lab\" for=\"combineFallbackText\">Copy was blocked. The text is selected; press Ctrl+C or Cmd+C.</label>\n            <textarea id=\"combineFallbackText\" readonly rows=\"3\" spellcheck=\"false\"></textarea>\n          </div>\n        </div>\n      </div>\n    </div>\n  </section>\n\n  <section class=\"block\">\n    <div class=\"wrap\">\n      <details class=\"how\">\n        <summary>How this works and what it does not protect</summary>\n        <ul>\n          <li><b>The scheme.</b> Shamir's secret sharing works on each byte of the secret in a field of 256 values. Any <em>k</em> shares rebuild the secret. Fewer than <em>k</em> reveal nothing about it, however much computing power they have, as long as the random numbers were good.</li>\n          <li><b>Randomness.</b> Random numbers come from this window's secure random source. If it is missing, the tool stops instead of using something weaker.</li>\n          <li><b>Two formats.</b> A Sanctum share adds the number of shares needed, a set id, a checksum on the share and a check value hidden inside the secret. The tool can then tell you when a share is mistyped, missing or from another set. These checks are not secret, so they do not prove the secret is the original. A plain share has none of this and matches ssss-made-easy and the shamir-secret-sharing package.</li>\n          <li><b>Not the ssss command line.</b> These shares do not work with the ssss-split and ssss-combine programs.</li>\n          <li><b>What it covers.</b> It protects against lost and mistyped shares. It does not protect against a dishonest share holder, malware, screen capture or another program reading the clipboard. Someone holding enough other shares can invent a share that rebuilds a secret of their choice, so the checks catch accidents, not cheating.</li>\n          <li><b>Length.</b> Every share is about as long as the secret. The length option pads the hidden data so the exact size is not visible.</li>\n          <li><b>Cleanup.</b> Nothing is stored. Clear empties the fields and the bytes this page holds, where it can. The idle timer only hides the secret again after 60 seconds without typing. Browsers may keep copies of text in memory, so treat this as best effort.</li>\n        </ul>\n      </details>\n    </div>\n  </section>\n</main>\n\n<footer>\n  <div class=\"wrap\">\n    <p>Sanctum example tool, released under AGPL-3.0 with the rest of Sanctum. It asks Sanctum for no permissions.</p>\n    <p>The feature set follows ssss-made-easy by Martin Carpella and 0xjjpa (GPL-3.0). The plain format and the field arithmetic match the shamir-secret-sharing package (Apache-2.0, Privy) and HashiCorp Vault's shamir package. This file was written from a specification and contains none of their code. Icons are from Lucide (ISC).</p>\n  </div>\n</footer>\n\n<div class=\"toast\" id=\"toast\" role=\"status\" aria-live=\"polite\" hidden></div>\n\n<script>\n/*\n * Shamir secret sharing core for the Sanctum example tool.\n *\n * Plain script: no DOM, no imports, no network, no dynamic code. Synchronous.\n * It sets globalThis.ShamirCore and, under Node, module.exports.\n * The only platform dependencies are globalThis.crypto.getRandomValues,\n * TextEncoder and TextDecoder.\n *\n * Maths: Shamir's scheme over GF(256) with modulus 0x11b. The exp and log\n * tables are built at load time from the generator 3.\n * Formats and rules follow the project spec: \"ss1\" (self-describing, with a\n * check value inside the shared payload) and \"plain\" (bare shares in the\n * token-NN-hex layout used by ssss-made-easy and shamir-secret-sharing).\n *\n * Credit: the feature set follows ssss-made-easy (Martin Carpella, 0xjjpa;\n * GPL-3.0). The share layout matches shamir-secret-sharing (Apache-2.0) and\n * the HashiCorp Vault shamir package. This file was written from the spec,\n * without copying their code or tables.\n */\n(function () {\n  \"use strict\";\n\n  // ---------------------------------------------------------------- constants\n\n  var PAD_CHOICES = Object.freeze([0, 16, 32, 64, 128, 256]);\n\n  var LIMITS = Object.freeze({\n    MIN_SHARES: 2,\n    MAX_SHARES: 255,\n    MIN_THRESHOLD: 2,\n    MAX_SECRET_BYTES: 1024,\n    MAX_LABEL_CHARS: 32,\n    PAD_CHOICES: PAD_CHOICES,\n    GENERATOR_ALPHABET: \"ABCDEFGHJKMNPQRTUVWXYZ234679\",\n  });\n\n  // 1 to 32 label characters, and no dash-delimited segment equal to ss1 in any case.\n  var LABEL_PATTERN = /^(?!(?:[A-Za-z0-9._-]*-)?[Ss][Ss]1(?:-|$))[A-Za-z0-9._-]{1,32}$/;\n\n  var TAG_DOMAIN = asciiBytes(\"sanctum-shamir/v1/tag\");\n  var CHK_DOMAIN = asciiBytes(\"sanctum-shamir/v1/share\");\n\n  var MESSAGES = {\n    SECRET_EMPTY: \"The secret is empty.\",\n    SECRET_TOO_LONG: \"The secret is too long.\",\n    BAD_SHARE_COUNT: \"The number of shares must be a whole number from 2 to 255.\",\n    BAD_THRESHOLD: \"The threshold is not valid.\",\n    BAD_OPTION: \"An option is not valid.\",\n    RANDOM_UNAVAILABLE: \"No secure random source is available.\",\n    EMPTY_INPUT: \"Nothing to read.\",\n    BAD_SYNTAX: \"This does not look like a share.\",\n    BAD_HEX: \"The share has a non-hex character or an odd number of digits.\",\n    SHARE_TOO_SHORT: \"The share is too short.\",\n    X_OUT_OF_RANGE: \"The share position is out of range.\",\n    BAD_CHECKSUM: \"The share checksum does not match.\",\n    NO_SHARES: \"No shares were given.\",\n    MIXED_FORMATS: \"Shares of different formats cannot be combined.\",\n    MIXED_SETS: \"The shares come from different splits.\",\n    LENGTH_MISMATCH: \"The shares have different lengths.\",\n    DUPLICATE_X: \"Two different shares claim the same position.\",\n    NOT_ENOUGH_SHARES: \"Not enough shares.\",\n    INCONSISTENT_SHARES: \"A share does not agree with the others.\",\n    INTEGRITY_FAILED: \"The shares did not rebuild a valid secret.\",\n  };\n\n  // ------------------------------------------------------------------- errors\n\n  class ShamirError extends Error {\n    constructor(code, details, message) {\n      super(message || MESSAGES[code] || code);\n      this.name = \"ShamirError\";\n      this.code = code;\n      this.details = Object.freeze(details ? Object.assign({}, details) : {});\n    }\n  }\n\n  function fail(code, details, message) {\n    throw new ShamirError(code, details, message);\n  }\n\n  // ------------------------------------------------------------------ helpers\n\n  function asciiBytes(s) {\n    var out = new Uint8Array(s.length);\n    for (var i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 0xff;\n    return out;\n  }\n\n  function concat(parts) {\n    var total = 0;\n    var i;\n    for (i = 0; i < parts.length; i++) total += parts[i].length;\n    var out = new Uint8Array(total);\n    var pos = 0;\n    for (i = 0; i < parts.length; i++) {\n      out.set(parts[i], pos);\n      pos += parts[i].length;\n    }\n    return out;\n  }\n\n  var HEX_CHARS = \"0123456789abcdef\";\n  var HEX_PAIRS = (function () {\n    var t = new Array(256);\n    for (var i = 0; i < 256; i++) t[i] = HEX_CHARS[i >> 4] + HEX_CHARS[i & 15];\n    return t;\n  })();\n\n  function toHex(bytes) {\n    var parts = new Array(bytes.length);\n    for (var i = 0; i < bytes.length; i++) parts[i] = HEX_PAIRS[bytes[i]];\n    return parts.join(\"\");\n  }\n\n  function hexValue(c) {\n    if (c >= 48 && c <= 57) return c - 48;\n    if (c >= 97 && c <= 102) return c - 87;\n    if (c >= 65 && c <= 70) return c - 55;\n    return -1;\n  }\n\n  function fromHex(hex) {\n    if (typeof hex !== \"string\") fail(\"BAD_HEX\", { reason: \"not a string\" });\n    if (hex.length % 2 !== 0) fail(\"BAD_HEX\", { reason: \"odd length\" });\n    var out = new Uint8Array(hex.length / 2);\n    for (var i = 0; i < out.length; i++) {\n      var hi = hexValue(hex.charCodeAt(2 * i));\n      var lo = hexValue(hex.charCodeAt(2 * i + 1));\n      if (hi < 0 || lo < 0) fail(\"BAD_HEX\", { reason: \"non-hex character\", position: 2 * i });\n      out[i] = (hi << 4) | lo;\n    }\n    return out;\n  }\n\n  function utf8Encode(text) {\n    return new TextEncoder().encode(text);\n  }\n\n  function utf8Decode(bytes) {\n    try {\n      return new TextDecoder(\"utf-8\", { fatal: true, ignoreBOM: true }).decode(bytes);\n    } catch (e) {\n      return null;\n    }\n  }\n\n  function isBytes(x) {\n    return x instanceof Uint8Array || Object.prototype.toString.call(x) === \"[object Uint8Array]\";\n  }\n\n  function wipe(bytes) {\n    if (bytes && typeof bytes.fill === \"function\") bytes.fill(0);\n  }\n\n  function isValidLabel(label) {\n    return typeof label === \"string\" && LABEL_PATTERN.test(label);\n  }\n\n  function entropyBits(length) {\n    return length * Math.log2(LIMITS.GENERATOR_ALPHABET.length);\n  }\n\n  // ------------------------------------------------------------------- SHA-256\n\n  var SHA_K = new Uint32Array(64);\n  var SHA_H0 = new Uint32Array(8);\n  (function () {\n    var primes = [];\n    for (var c = 2; primes.length < 64; c++) {\n      var isPrime = true;\n      for (var d = 2; d * d <= c; d++) {\n        if (c % d === 0) {\n          isPrime = false;\n          break;\n        }\n      }\n      if (isPrime) primes.push(c);\n    }\n    function frac32(v) {\n      return Math.floor((v - Math.floor(v)) * 4294967296) >>> 0;\n    }\n    var i;\n    for (i = 0; i < 64; i++) SHA_K[i] = frac32(Math.cbrt(primes[i]));\n    for (i = 0; i < 8; i++) SHA_H0[i] = frac32(Math.sqrt(primes[i]));\n  })();\n\n  function sha256(bytes) {\n    var len = bytes.length;\n    var padded = ((len + 9 + 63) >> 6) << 6;\n    var buf = new Uint8Array(padded);\n    buf.set(bytes, 0);\n    buf[len] = 0x80;\n    var view = new DataView(buf.buffer);\n    view.setUint32(padded - 8, Math.floor(len / 536870912), false);\n    view.setUint32(padded - 4, (len << 3) >>> 0, false);\n\n    var h = new Uint32Array(SHA_H0);\n    var w = new Uint32Array(64);\n    for (var off = 0; off < padded; off += 64) {\n      var t;\n      for (t = 0; t < 16; t++) w[t] = view.getUint32(off + 4 * t, false);\n      for (t = 16; t < 64; t++) {\n        var x15 = w[t - 15];\n        var x2 = w[t - 2];\n        var s0 = ((x15 >>> 7) | (x15 << 25)) ^ ((x15 >>> 18) | (x15 << 14)) ^ (x15 >>> 3);\n        var s1 = ((x2 >>> 17) | (x2 << 15)) ^ ((x2 >>> 19) | (x2 << 13)) ^ (x2 >>> 10);\n        w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;\n      }\n      var a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];\n      for (t = 0; t < 64; t++) {\n        var S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));\n        var ch = (e & f) ^ (~e & g);\n        var t1 = (hh + S1 + ch + SHA_K[t] + w[t]) >>> 0;\n        var S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));\n        var maj = (a & b) ^ (a & c) ^ (b & c);\n        var t2 = (S0 + maj) >>> 0;\n        hh = g;\n        g = f;\n        f = e;\n        e = (d + t1) >>> 0;\n        d = c;\n        c = b;\n        b = a;\n        a = (t1 + t2) >>> 0;\n      }\n      h[0] = (h[0] + a) >>> 0;\n      h[1] = (h[1] + b) >>> 0;\n      h[2] = (h[2] + c) >>> 0;\n      h[3] = (h[3] + d) >>> 0;\n      h[4] = (h[4] + e) >>> 0;\n      h[5] = (h[5] + f) >>> 0;\n      h[6] = (h[6] + g) >>> 0;\n      h[7] = (h[7] + hh) >>> 0;\n    }\n    var out = new Uint8Array(32);\n    var ov = new DataView(out.buffer);\n    for (var i = 0; i < 8; i++) ov.setUint32(4 * i, h[i], false);\n    wipe(buf);\n    wipe(w);\n    return out;\n  }\n\n  // -------------------------------------------------------------------- GF(256)\n\n  var EXP = new Uint8Array(510);\n  var LOG = new Uint8Array(256);\n  (function () {\n    var e = 1;\n    for (var i = 0; i < 255; i++) {\n      EXP[i] = e;\n      LOG[e] = i;\n      var twice = ((e << 1) ^ (e & 0x80 ? 0x1b : 0)) & 0xff;\n      e = e ^ twice; // multiply by the generator 3\n    }\n    for (var j = 255; j < 510; j++) EXP[j] = EXP[j - 255];\n  })();\n\n  function gfMul(a, b) {\n    if (a === 0 || b === 0) return 0;\n    return EXP[LOG[a] + LOG[b]];\n  }\n\n  function gfInv(a) {\n    if (a === 0) throw new RangeError(\"zero has no inverse\");\n    return EXP[255 - LOG[a]];\n  }\n\n  // Lagrange weights for evaluating at `target`, given distinct nonzero xs.\n  // Every factor (target xor x_j) and (x_i xor x_j) is nonzero in our uses:\n  // target is 0 or an x that differs from all the xs.\n  function lagrangeLogWeights(xs, target) {\n    var k = xs.length;\n    var out = new Uint16Array(k);\n    for (var i = 0; i < k; i++) {\n      var acc = 0;\n      for (var j = 0; j < k; j++) {\n        if (j === i) continue;\n        acc += LOG[target ^ xs[j]] + 255 - LOG[xs[i] ^ xs[j]];\n      }\n      out[i] = acc % 255;\n    }\n    return out;\n  }\n\n  // Value at `target` of the polynomial through the given raw shares (y bytes then x).\n  function interpolate(raws, target) {\n    var k = raws.length;\n    var L = raws[0].length - 1;\n    var xs = new Uint8Array(k);\n    for (var i = 0; i < k; i++) xs[i] = raws[i][L];\n    var lw = lagrangeLogWeights(xs, target);\n    var out = new Uint8Array(L);\n    for (var b = 0; b < L; b++) {\n      var acc = 0;\n      for (var s = 0; s < k; s++) {\n        var y = raws[s][b];\n        if (y !== 0) acc ^= EXP[LOG[y] + lw[s]];\n      }\n      out[b] = acc;\n    }\n    return out;\n  }\n\n  // ------------------------------------------------------------------- random\n\n  function defaultRandomBytes(n) {\n    var c = globalThis.crypto;\n    if (!c || typeof c.getRandomValues !== \"function\") fail(\"RANDOM_UNAVAILABLE\");\n    var out = new Uint8Array(n);\n    for (var off = 0; off < n; off += 65536) {\n      c.getRandomValues(out.subarray(off, Math.min(n, off + 65536)));\n    }\n    return out;\n  }\n\n  function checkedDraw(source, n) {\n    var bytes = source(n);\n    if (!isBytes(bytes) || bytes.length !== n) {\n      fail(\"BAD_OPTION\", { option: \"randomBytes\", reason: \"must return exactly \" + n + \" bytes\" });\n    }\n    return bytes;\n  }\n\n  function randomSecret(opts) {\n    var o = opts || {};\n    var length = o.length === undefined ? 32 : o.length;\n    if (!Number.isInteger(length) || length < 8 || length > 128) {\n      fail(\"BAD_OPTION\", { option: \"length\", min: 8, max: 128, have: length });\n    }\n    var source;\n    if (o.randomBytes !== undefined) {\n      if (typeof o.randomBytes !== \"function\") fail(\"BAD_OPTION\", { option: \"randomBytes\" });\n      source = o.randomBytes;\n    } else {\n      var c = globalThis.crypto;\n      if (!c || typeof c.getRandomValues !== \"function\") fail(\"RANDOM_UNAVAILABLE\");\n      source = defaultRandomBytes;\n    }\n    var alphabet = LIMITS.GENERATOR_ALPHABET;\n    var m = alphabet.length;\n    var limit = m * Math.floor(256 / m); // 252\n    var out = \"\";\n    while (out.length < length) {\n      var batch = checkedDraw(source, length - out.length);\n      for (var i = 0; i < batch.length && out.length < length; i++) {\n        if (batch[i] < limit) out += alphabet[batch[i] % m];\n      }\n      if (source === defaultRandomBytes) wipe(batch);\n    }\n    return out;\n  }\n\n  // ------------------------------------------------------------ share objects\n\n  function makeShare(format, label, threshold, setId, body) {\n    var x = body[body.length - 1];\n    var text;\n    if (format === \"ss1\") {\n      var chk = checksum(threshold, fromHex(setId), body);\n      text = \"ss1-\" + threshold + \"-\" + setId + \"-\" + toHex(body) + \"-\" + chk;\n    } else {\n      text = (x < 10 ? \"0\" : \"\") + x + \"-\" + toHex(body);\n    }\n    if (label) text = label + \"-\" + text;\n    return Object.freeze({\n      format: format,\n      label: label || null,\n      x: x,\n      threshold: format === \"ss1\" ? threshold : null,\n      setId: format === \"ss1\" ? setId : null,\n      body: body,\n      text: text,\n    });\n  }\n\n  function checksum(k, setIdBytes, body) {\n    var d = sha256(concat([CHK_DOMAIN, Uint8Array.of(k), setIdBytes, body]));\n    return toHex(d.subarray(0, 2));\n  }\n\n  function makeTag(lenBytes, secret) {\n    return sha256(concat([TAG_DOMAIN, lenBytes, secret])).subarray(0, 4);\n  }\n\n  // -------------------------------------------------------------------- split\n\n  function split(secret, n, k, options) {\n    if (!isBytes(secret)) fail(\"BAD_OPTION\", { option: \"secret\" }, \"The secret must be a Uint8Array.\");\n    if (secret.length === 0) fail(\"SECRET_EMPTY\");\n    if (secret.length > LIMITS.MAX_SECRET_BYTES) {\n      fail(\"SECRET_TOO_LONG\", { max: LIMITS.MAX_SECRET_BYTES, have: secret.length });\n    }\n    if (!Number.isInteger(n) || n < LIMITS.MIN_SHARES || n > LIMITS.MAX_SHARES) {\n      fail(\"BAD_SHARE_COUNT\", { min: LIMITS.MIN_SHARES, max: LIMITS.MAX_SHARES, have: n });\n    }\n    if (!Number.isInteger(k) || k < LIMITS.MIN_THRESHOLD || k > n) {\n      fail(\"BAD_THRESHOLD\", { min: LIMITS.MIN_THRESHOLD, max: n, have: k });\n    }\n\n    var opt = options || {};\n    var format = opt.format === undefined ? \"ss1\" : opt.format;\n    if (format !== \"ss1\" && format !== \"plain\") fail(\"BAD_OPTION\", { option: \"format\" });\n    var label = opt.label === undefined || opt.label === null ? null : opt.label;\n    if (label !== null && !isValidLabel(label)) fail(\"BAD_OPTION\", { option: \"label\" });\n    var padTo = opt.padTo === undefined ? 0 : opt.padTo;\n    if (PAD_CHOICES.indexOf(padTo) < 0) fail(\"BAD_OPTION\", { option: \"padTo\" });\n    if (format === \"plain\" && padTo !== 0) fail(\"BAD_OPTION\", { option: \"padTo\" });\n    var source;\n    var ownRandom = false;\n    if (opt.randomBytes !== undefined && opt.randomBytes !== null) {\n      if (typeof opt.randomBytes !== \"function\") fail(\"BAD_OPTION\", { option: \"randomBytes\" });\n      source = opt.randomBytes;\n    } else {\n      var c = globalThis.crypto;\n      if (!c || typeof c.getRandomValues !== \"function\") fail(\"RANDOM_UNAVAILABLE\");\n      source = defaultRandomBytes;\n      ownRandom = true;\n    }\n\n    // Payload: the secret itself (plain) or LEN || secret || zero padding || TAG (ss1).\n    var setId = null;\n    var payload;\n    if (format === \"ss1\") {\n      var idBytes = checkedDraw(source, 4);\n      setId = toHex(idBytes);\n      if (ownRandom) wipe(idBytes);\n      var L = secret.length;\n      var base = 2 + L + 4;\n      var total = padTo === 0 ? base : Math.ceil(base / padTo) * padTo;\n      payload = new Uint8Array(total);\n      payload[0] = L >> 8;\n      payload[1] = L & 0xff;\n      payload.set(secret, 2);\n      payload.set(makeTag(payload.subarray(0, 2), secret), total - 4);\n    } else {\n      payload = new Uint8Array(secret);\n    }\n\n    var P = payload.length;\n    var km1 = k - 1;\n    var rand = checkedDraw(source, P * km1);\n\n    var bodies = new Array(n);\n    var tbl = new Uint8Array(256);\n    for (var s = 0; s < n; s++) {\n      var x = s + 1;\n      var lx = LOG[x];\n      tbl[0] = 0;\n      for (var v = 1; v < 256; v++) tbl[v] = EXP[LOG[v] + lx];\n      var body = new Uint8Array(P + 1);\n      for (var i = 0; i < P; i++) {\n        var off = i * km1;\n        var r = rand[off + km1 - 1];\n        for (var j = km1 - 2; j >= 0; j--) r = tbl[r] ^ rand[off + j];\n        body[i] = tbl[r] ^ payload[i];\n      }\n      body[P] = x;\n      bodies[s] = body;\n    }\n\n    var shares = new Array(n);\n    for (var q = 0; q < n; q++) shares[q] = makeShare(format, label, k, setId, bodies[q]);\n\n    wipe(payload);\n    wipe(tbl);\n    if (ownRandom) wipe(rand);\n\n    return Object.freeze({\n      shares: Object.freeze(shares),\n      format: format,\n      threshold: k,\n      setId: setId,\n      payloadBytes: P,\n    });\n  }\n\n  // -------------------------------------------------------------------- parse\n\n  var SS1_RE = /^(?:([A-Za-z0-9._-]{1,32})-)?ss1-(\\d{1,3})-([0-9a-f]{8})-([0-9a-f]+)-([0-9a-f]{4})$/i;\n  var HEX_ONLY_RE = /^[0-9a-f]+$/i;\n  // A plain prefix is `[label-]NN` (the label may be empty, as the original site produces),\n  // or a bare label with no dashes. Anything else is refused: a looser rule let a damaged\n  // ss1 share (for example one with a mistyped \"ss1\" tag) parse as a tiny plain share.\n  var PLAIN_HEAD_RE = /^(?:([A-Za-z0-9._-]{0,32})-)?(\\d{1,3})$/;\n  var PLAIN_BARE_LABEL_RE = /^[A-Za-z0-9._]{1,32}$/;\n\n  function parseShare(text) {\n    if (typeof text !== \"string\") fail(\"EMPTY_INPUT\", { reason: \"not a string\" });\n    var t = text.trim();\n    if (t === \"\") fail(\"EMPTY_INPUT\");\n\n    var m = SS1_RE.exec(t);\n    if (m) {\n      var k = parseInt(m[2], 10);\n      if (k < 2 || k > 255 || String(k) !== m[2]) fail(\"BAD_THRESHOLD\", { have: m[2] });\n      var bodyHex = m[4];\n      if (bodyHex.length % 2 !== 0) fail(\"BAD_HEX\", { reason: \"odd length\" });\n      var body = fromHex(bodyHex);\n      if (body.length < 8) fail(\"SHARE_TOO_SHORT\", { have: body.length, min: 8 });\n      if (body[body.length - 1] === 0) fail(\"X_OUT_OF_RANGE\");\n      var setId = m[3].toLowerCase();\n      if (checksum(k, fromHex(setId), body) !== m[5].toLowerCase()) fail(\"BAD_CHECKSUM\");\n      return makeShare(\"ss1\", m[1] || null, k, setId, body);\n    }\n\n    var segs = t.split(\"-\");\n    for (var i = 0; i < segs.length; i++) {\n      if (segs[i].toLowerCase() === \"ss1\") fail(\"BAD_SYNTAX\", { reason: \"damaged ss1 share\" });\n    }\n\n    var head = \"\";\n    var tail = t;\n    var cut = t.lastIndexOf(\"-\");\n    if (cut >= 0) {\n      head = t.slice(0, cut);\n      tail = t.slice(cut + 1);\n    }\n    if (tail === \"\") fail(\"BAD_SYNTAX\", { reason: \"empty body\" });\n    if (!HEX_ONLY_RE.test(tail) || tail.length % 2 !== 0) fail(\"BAD_HEX\");\n    var pbody = fromHex(tail);\n    if (pbody.length < 2) fail(\"SHARE_TOO_SHORT\", { have: pbody.length, min: 2 });\n    if (pbody[pbody.length - 1] === 0) fail(\"X_OUT_OF_RANGE\");\n\n    var label = null;\n    if (head !== \"\") {\n      var hm = PLAIN_HEAD_RE.exec(head);\n      if (hm) {\n        label = hm[1] || null;\n      } else if (PLAIN_BARE_LABEL_RE.test(head)) {\n        label = head;\n      } else {\n        fail(\"BAD_SYNTAX\", { reason: \"unrecognised share prefix\" });\n      }\n    }\n    return makeShare(\"plain\", label, null, null, pbody);\n  }\n\n  function parseShares(text) {\n    var out = [];\n    if (typeof text !== \"string\") return out;\n    var pieces = text.split(/\\s+/);\n    for (var i = 0; i < pieces.length; i++) {\n      if (pieces[i] === \"\") continue;\n      var index = out.length;\n      try {\n        out.push(Object.freeze({ index: index, raw: pieces[i], share: parseShare(pieces[i]), error: null }));\n      } catch (e) {\n        if (!(e instanceof ShamirError)) throw e;\n        out.push(\n          Object.freeze({\n            index: index,\n            raw: pieces[i],\n            share: null,\n            error: Object.freeze({ code: e.code, message: e.message }),\n          })\n        );\n      }\n    }\n    return out;\n  }\n\n  // Turn an input list into [{ share, idx }]. With `strict`, a parse failure\n  // throws with details.index. Otherwise failures are skipped.\n  function prepare(list, strict) {\n    var out = [];\n    for (var i = 0; i < list.length; i++) {\n      var item = list[i];\n      var share = null;\n      if (typeof item === \"string\") {\n        try {\n          share = parseShare(item);\n        } catch (e) {\n          if (!(e instanceof ShamirError)) throw e;\n          if (strict) fail(e.code, Object.assign({}, e.details, { index: i }), e.message);\n        }\n      } else if (item && isBytes(item.body) && (item.format === \"ss1\" || item.format === \"plain\")) {\n        share = item;\n      } else if (strict) {\n        fail(\"BAD_SYNTAX\", { index: i, reason: \"not a share\" });\n      }\n      if (share) out.push({ share: share, idx: i });\n    }\n    return out;\n  }\n\n  function dedupe(entries) {\n    var seen = Object.create(null);\n    var uniq = [];\n    var dups = 0;\n    for (var i = 0; i < entries.length; i++) {\n      var s = entries[i].share;\n      var key = s.format + \":\" + (s.setId || \"\") + \":\" + (s.threshold || 0) + \":\" + s.x + \":\" + toHex(s.body);\n      if (seen[key]) {\n        dups++;\n        continue;\n      }\n      seen[key] = true;\n      uniq.push(entries[i]);\n    }\n    return { uniq: uniq, dups: dups };\n  }\n\n  function validThreshold(t) {\n    return Number.isInteger(t) && t >= 2 && t <= 255;\n  }\n\n  // ----------------------------------------------------------------- validate\n\n  function validateShares(shares, options) {\n    var list = prepare(shares || [], false);\n    var problems = [];\n    var optT = options && validThreshold(options.threshold) ? options.threshold : null;\n\n    if (list.length === 0) {\n      return Object.freeze({\n        format: null,\n        setId: null,\n        threshold: null,\n        count: 0,\n        ignoredDuplicates: 0,\n        needed: null,\n        missing: 0,\n        canCombine: false,\n        problems: Object.freeze([]),\n      });\n    }\n\n    var first = list[0].share;\n    var formats = Object.create(null);\n    var i;\n    for (i = 0; i < list.length; i++) formats[list[i].share.format] = true;\n    var mixedFormats = Object.keys(formats).length > 1;\n    var format = mixedFormats ? \"mixed\" : first.format;\n\n    var dd = dedupe(list);\n    var uniq = dd.uniq;\n    var bad;\n\n    if (mixedFormats) {\n      bad = [];\n      for (i = 0; i < uniq.length; i++) if (uniq[i].share.format !== first.format) bad.push(uniq[i].idx);\n      problems.push({ code: \"MIXED_FORMATS\", indices: bad });\n    }\n\n    var setId = null;\n    var threshold = null;\n    var ss1s = uniq.filter(function (e) {\n      return e.share.format === \"ss1\";\n    });\n    if (ss1s.length > 0) {\n      var ref = ss1s[0].share;\n      bad = [];\n      for (i = 0; i < ss1s.length; i++) {\n        if (ss1s[i].share.setId !== ref.setId || ss1s[i].share.threshold !== ref.threshold) bad.push(ss1s[i].idx);\n      }\n      if (bad.length > 0) problems.push({ code: \"MIXED_SETS\", indices: bad });\n      else if (!mixedFormats) {\n        setId = ref.setId;\n        threshold = ref.threshold;\n      }\n    }\n\n    bad = [];\n    for (i = 0; i < uniq.length; i++) if (uniq[i].share.body.length !== uniq[0].share.body.length) bad.push(uniq[i].idx);\n    if (bad.length > 0) problems.push({ code: \"LENGTH_MISMATCH\", indices: bad });\n\n    var byX = Object.create(null);\n    for (i = 0; i < uniq.length; i++) {\n      var xk = uniq[i].share.x;\n      (byX[xk] || (byX[xk] = [])).push(uniq[i].idx);\n    }\n    bad = [];\n    Object.keys(byX).forEach(function (k) {\n      if (byX[k].length > 1) bad = bad.concat(byX[k]);\n    });\n    if (bad.length > 0) {\n      bad.sort(function (a, b) {\n        return a - b;\n      });\n      problems.push({ code: \"DUPLICATE_X\", indices: bad });\n    }\n\n    var needed = first.format === \"ss1\" ? first.threshold : optT || 2;\n    var count = uniq.length;\n    var missing = Math.max(0, needed - count);\n    if (missing > 0) problems.push({ code: \"NOT_ENOUGH_SHARES\", indices: [] });\n\n    return Object.freeze({\n      format: format,\n      setId: setId,\n      threshold: threshold,\n      count: count,\n      ignoredDuplicates: dd.dups,\n      needed: needed,\n      missing: missing,\n      canCombine: problems.length === 0,\n      problems: Object.freeze(\n        problems.map(function (p) {\n          return Object.freeze({ code: p.code, indices: p.indices });\n        })\n      ),\n    });\n  }\n\n  // ------------------------------------------------------------------ combine\n\n  function combine(shares, options) {\n    var inputs = Array.isArray(shares) ? shares : [];\n    var entries = prepare(inputs, true);\n    if (entries.length === 0) fail(\"NO_SHARES\");\n\n    var first = entries[0].share;\n    var i;\n    for (i = 1; i < entries.length; i++) {\n      if (entries[i].share.format !== first.format) fail(\"MIXED_FORMATS\", { index: entries[i].idx });\n    }\n    var format = first.format;\n\n    var optT = null;\n    if (format === \"plain\" && options && options.threshold !== undefined && options.threshold !== null) {\n      if (!validThreshold(options.threshold)) {\n        fail(\"BAD_OPTION\", { option: \"threshold\", min: 2, max: 255, have: options.threshold });\n      }\n      optT = options.threshold;\n    }\n\n    var dd = dedupe(entries);\n    var uniq = dd.uniq;\n\n    if (format === \"ss1\") {\n      for (i = 1; i < uniq.length; i++) {\n        if (uniq[i].share.setId !== uniq[0].share.setId || uniq[i].share.threshold !== uniq[0].share.threshold) {\n          fail(\"MIXED_SETS\", { index: uniq[i].idx });\n        }\n      }\n    }\n    for (i = 1; i < uniq.length; i++) {\n      if (uniq[i].share.body.length !== uniq[0].share.body.length) fail(\"LENGTH_MISMATCH\", { index: uniq[i].idx });\n    }\n    var seenX = Object.create(null);\n    for (i = 0; i < uniq.length; i++) {\n      var x = uniq[i].share.x;\n      if (seenX[x]) fail(\"DUPLICATE_X\", { x: x });\n      seenX[x] = true;\n    }\n\n    var need = format === \"ss1\" ? first.threshold : optT || 2;\n    if (uniq.length < need) fail(\"NOT_ENOUGH_SHARES\", { have: uniq.length, need: need });\n\n    var t = format === \"ss1\" ? need : optT || uniq.length;\n    var use = uniq.slice(0, t);\n    var extra = uniq.slice(t);\n    var raws = use.map(function (e) {\n      return e.share.body;\n    });\n    var payload = interpolate(raws, 0);\n\n    var mismatched = [];\n    for (i = 0; i < extra.length; i++) {\n      var want = interpolate(raws, extra[i].share.x);\n      var have = extra[i].share.body;\n      var same = true;\n      for (var b = 0; b < want.length; b++) {\n        if (want[b] !== have[b]) {\n          same = false;\n          break;\n        }\n      }\n      if (!same) mismatched.push(extra[i].idx);\n    }\n\n    if (format === \"plain\") {\n      if (mismatched.length > 0) {\n        wipe(payload);\n        fail(\"INCONSISTENT_SHARES\", { indices: mismatched });\n      }\n      return Object.freeze({\n        secret: payload,\n        format: \"plain\",\n        threshold: optT,\n        setId: null,\n        verified: false,\n        sharesUsed: t,\n        ignoredDuplicates: dd.dups,\n        warnings: Object.freeze([]),\n      });\n    }\n\n    // ss1: LEN(2) || secret || zero padding || TAG(4)\n    var ok = payload.length >= 7;\n    var ln = 0;\n    if (ok) {\n      ln = (payload[0] << 8) | payload[1];\n      ok = ln >= 1 && 2 + ln + 4 <= payload.length;\n    }\n    if (ok) {\n      var padEnd = payload.length - 4;\n      var pad = 0;\n      for (i = 2 + ln; i < padEnd; i++) pad |= payload[i];\n      var tag = makeTag(payload.subarray(0, 2), payload.subarray(2, 2 + ln));\n      var diff = pad;\n      for (i = 0; i < 4; i++) diff |= tag[i] ^ payload[padEnd + i];\n      ok = diff === 0;\n    }\n    if (!ok) {\n      wipe(payload);\n      fail(\"INTEGRITY_FAILED\", { extraMismatch: mismatched });\n    }\n    var secret = payload.slice(2, 2 + ln);\n    wipe(payload);\n    return Object.freeze({\n      secret: secret,\n      format: \"ss1\",\n      threshold: first.threshold,\n      setId: first.setId,\n      verified: true,\n      sharesUsed: t,\n      ignoredDuplicates: dd.dups,\n      warnings: Object.freeze(\n        mismatched.length > 0 ? [Object.freeze({ code: \"EXTRA_SHARE_MISMATCH\", inputIndices: mismatched })] : []\n      ),\n    });\n  }\n\n  // ------------------------------------------------------------------- export\n\n  var api = Object.freeze({\n    LIMITS: LIMITS,\n    LABEL_PATTERN: LABEL_PATTERN,\n    ShamirError: ShamirError,\n    split: split,\n    parseShare: parseShare,\n    parseShares: parseShares,\n    validateShares: validateShares,\n    combine: combine,\n    toHex: toHex,\n    fromHex: fromHex,\n    utf8Encode: utf8Encode,\n    utf8Decode: utf8Decode,\n    sha256: sha256,\n    randomSecret: randomSecret,\n    entropyBits: entropyBits,\n    isValidLabel: isValidLabel,\n    wipe: wipe,\n    gf256: Object.freeze({ mul: gfMul, inv: gfInv }),\n  });\n\n  globalThis.ShamirCore = api;\n  if (typeof module !== \"undefined\" && module.exports) module.exports = api;\n})();\n</script>\n<script>\n(function(){\n\"use strict\";\nvar C = globalThis.ShamirCore;\nfunction $(id){ return document.getElementById(id); }\n\nif(!C){ $(\"noCore\").hidden = false; return; }\nif(!(globalThis.crypto && typeof crypto.getRandomValues === \"function\")){ $(\"noCrypto\").hidden = false; }\n\nvar MAXB = C.LIMITS.MAX_SECRET_BYTES;\n\n/* ---------- small helpers ---------- */\nvar toastTimer = null;\nfunction toast(msg){\n  var t = $(\"toast\"); t.textContent = msg; t.hidden = false;\n  clearTimeout(toastTimer); toastTimer = setTimeout(function(){ t.hidden = true; }, 1800);\n}\nfunction icon(name){ return '<svg class=\"i\" aria-hidden=\"true\"><use href=\"#i-' + name + '\"></use></svg>'; }\nfunction setIcon(btn, name, label){\n  btn.querySelector(\"use\").setAttribute(\"href\", \"#i-\" + name);\n  if(label != null) btn.querySelector(\"span\").textContent = label;\n}\nfunction el(tag, cls, text){\n  var e = document.createElement(tag);\n  if(cls) e.className = cls;\n  if(text != null) e.textContent = text;\n  return e;\n}\nfunction plural(n, one, many){ return n + \" \" + (n === 1 ? one : many); }\nfunction eqBytes(a, b){\n  if(a.length !== b.length) return false;\n  var d = 0; for(var i = 0; i < a.length; i++) d |= a[i] ^ b[i];\n  return d === 0;\n}\n/* Let the browser paint the busy state first: rAF, then a timeout. The timer is a fallback for hidden windows. */\nfunction frame(fn){\n  var done = false;\n  function go(){ if(done) return; done = true; fn(); }\n  if(typeof requestAnimationFrame === \"function\") requestAnimationFrame(function(){ setTimeout(go, 0); });\n  setTimeout(go, 150);\n}\nfunction selectAll(t){ t.focus(); t.select(); }\n\n/* Copy with a selectable fallback. host: {box, area, msg?} */\nfunction copyText(text, okMsg, host){\n  function fallback(){\n    host.box.hidden = false; host.area.value = text; selectAll(host.area);\n    var done = false;\n    try { done = document.execCommand(\"copy\"); } catch(e){ done = false; }\n    if(done){ toast(okMsg); host.box.hidden = true; host.area.value = \"\"; }\n  }\n  if(navigator.clipboard && navigator.clipboard.writeText){\n    navigator.clipboard.writeText(text).then(function(){\n      host.box.hidden = true; host.area.value = \"\"; toast(okMsg);\n    }, fallback);\n  } else { fallback(); }\n}\n\n/* ---------- typed error copy ---------- */\nfunction errText(code, d, ctx){\n  d = d || {}; ctx = ctx || {};\n  switch(code){\n    case \"SECRET_EMPTY\": return \"Enter a secret first.\";\n    case \"SECRET_TOO_LONG\": return \"The secret is \" + d.have + \" bytes. The limit is \" + d.max + \".\";\n    case \"BAD_SHARE_COUNT\": return \"Shares must be a whole number from 2 to 255.\";\n    case \"BAD_THRESHOLD\":\n      if(ctx.onShare) return \"This share's needed number is not valid.\";\n      return \"Needed shares must be a whole number from 2 up to the number of shares\" + (d.max ? \" (\" + d.max + \").\" : \".\");\n    case \"BAD_OPTION\":\n      if(d.option === \"padTo\") return \"Hiding the secret length is only available for Sanctum shares.\";\n      if(d.option === \"threshold\") return \"How many shares were needed must be a whole number from 2 to 255.\";\n      return \"Labels use letters, digits, dot, underscore and hyphen, up to 32 characters, and cannot contain the part \\\"ss1\\\".\";\n    case \"RANDOM_UNAVAILABLE\": return \"This window has no secure random source, so nothing was split.\";\n    case \"EMPTY_INPUT\": return \"Nothing to read.\";\n    case \"BAD_SYNTAX\": return \"This does not look like a share. A Sanctum share looks like ss1-3-0b704569-...-c852.\";\n    case \"BAD_HEX\": return \"The share has a character that is not 0 to 9 or a to f, or an odd number of digits. Check for a missing or extra character.\";\n    case \"SHARE_TOO_SHORT\": return \"This share is too short to be complete.\";\n    case \"X_OUT_OF_RANGE\": return \"This share has an invalid position (0). It is damaged.\";\n    case \"BAD_CHECKSUM\": return \"The end of this share does not match its contents. A character was probably mistyped or lost. Copy it again.\";\n    case \"NO_SHARES\": return \"Paste the shares first.\";\n    case \"MIXED_FORMATS\": return \"Sanctum shares and plain shares cannot be combined with each other.\";\n    case \"MIXED_SETS\": return \"These shares come from different sets.\" + (ctx.setIds && ctx.setIds.length > 1 ? \" Set ids found: \" + ctx.setIds.join(\", \") + \".\" : \"\") + \" Use shares that have the same set id.\";\n    case \"LENGTH_MISMATCH\": return \"These shares are not the same length, so they are not from one set.\";\n    case \"DUPLICATE_X\": return \"Two different shares claim the same position\" + (d.x != null ? \" (\" + d.x + \")\" : \"\") + \". One of them is wrong.\";\n    case \"TYPO_MINOR_SANCTUM\": return \"This looks like a Sanctum share but the others are plain shares. Remove it, or remove the others.\";\n    case \"TYPO_MINOR_PLAIN\": return \"This looks like a plain share but the others are Sanctum shares. Remove it, or remove the others.\";\n    case \"NOT_ENOUGH_SHARES\":\n      if(d.have != null && d.need != null) return d.have + \" of \" + d.need + \" needed. Add \" + (d.need - d.have) + \" more.\";\n      return \"Not enough shares.\";\n    case \"INCONSISTENT_SHARES\":\n      return (d.indices && d.indices.length > 1 ? \"Shares \" : \"Share \") + posList(d.indices, ctx) + (d.indices && d.indices.length > 1 ? \" do not agree\" : \" does not agree\") + \" with the others. Replace \" + (d.indices && d.indices.length > 1 ? \"them.\" : \"it.\");\n    case \"INTEGRITY_FAILED\":\n      return \"The shares did not rebuild a valid secret. At least one share is wrong, or they belong to different sets.\" + (ctx.need ? \" If you have more than \" + ctx.need + \" shares, try leaving one out.\" : \"\");\n  }\n  return \"Something went wrong (\" + code + \").\";\n}\nfunction posList(indices, ctx){\n  var toks = ctx.tokens || [];\n  return (indices || []).map(function(i){\n    var t = toks[i];\n    return t && t.share ? t.share.x : \"at entry \" + (i + 1);\n  }).join(\", \");\n}\nfunction errOf(e, ctx){\n  if(e && e.code) return errText(e.code, e.details, ctx);\n  return \"Something went wrong. Nothing was changed.\";\n}\n\n/* ---------- masking and idle timer ---------- */\nvar splitShown = false, combineShown = false, idleTimer = null;\nvar mode = \"text\";           // split secret entry mode\nvar viewMode = \"text\";       // combine result view\nfunction applySplitMask(){\n  $(\"secret\").classList.toggle(\"masked\", !splitShown);\n  $(\"sToggle\").setAttribute(\"aria-pressed\", String(splitShown));\n  setIcon($(\"sToggle\"), splitShown ? \"eye-off\" : \"eye\", splitShown ? \"Hide\" : \"Show\");\n  refreshSplitHash();\n}\nfunction hideFallbacks(){\n  [\"splitFallback\", \"combineFallback\"].forEach(function(id){ $(id).hidden = true; });\n  $(\"splitFallbackText\").value = \"\"; $(\"combineFallbackText\").value = \"\";\n}\nfunction remaskAll(){\n  hideFallbacks();\n  splitShown = false; combineShown = false;\n  applySplitMask(); applyCombineView();\n  $(\"sHashBox\").open = false; $(\"cHashBox\").open = false;\n}\nfunction bumpIdle(){\n  clearTimeout(idleTimer);\n  idleTimer = setTimeout(function(){ remaskAll(); }, 60000);\n}\n\n/* ---------- tabs ---------- */\nfunction selectTab(which){\n  var split = which === \"split\";\n  $(\"tabSplit\").setAttribute(\"aria-selected\", String(split));\n  $(\"tabCombine\").setAttribute(\"aria-selected\", String(!split));\n  $(\"tabSplit\").tabIndex = split ? 0 : -1;\n  $(\"tabCombine\").tabIndex = split ? -1 : 0;\n  $(\"panelSplit\").hidden = !split;\n  $(\"panelCombine\").hidden = split;\n  remaskAll();\n}\n$(\"tabSplit\").addEventListener(\"click\", function(){ selectTab(\"split\"); });\n$(\"tabCombine\").addEventListener(\"click\", function(){ selectTab(\"combine\"); });\n[$(\"tabSplit\"), $(\"tabCombine\")].forEach(function(b){\n  b.addEventListener(\"keydown\", function(ev){\n    var next = null;\n    if(ev.key === \"ArrowRight\" || ev.key === \"ArrowLeft\") next = b === $(\"tabSplit\") ? \"combine\" : \"split\";\n    else if(ev.key === \"Home\") next = \"split\";\n    else if(ev.key === \"End\") next = \"combine\";\n    if(next){\n      selectTab(next); (next === \"split\" ? $(\"tabSplit\") : $(\"tabCombine\")).focus(); ev.preventDefault();\n    }\n  });\n});\n\n/* ================= SPLIT ================= */\nvar splitState = \"idle\";   // idle | working | done | stale\nvar clearVer = 0;          // bumped by Clear; deferred results are then dropped\nvar inputVer = 0;          // bumped on every Split input; lets deferred work notice edits\nvar shareTexts = [];\nvar splitK = 0;\n\nfunction readSecret(){\n  var v = $(\"secret\").value;\n  if(mode === \"text\") return { bytes: C.utf8Encode(v) };\n  try { return { bytes: C.fromHex(v.replace(/\\s+/g, \"\")) }; }\n  catch(e){ return { err: \"Hex needs an even number of digits, using only 0 to 9 and a to f.\" }; }\n}\nfunction updateSecretCount(){\n  var s = readSecret(), c = $(\"sCount\"), over = false;\n  var se = $(\"sErr\");\n  if(s.err){\n    c.textContent = \"Not valid hex\"; c.classList.add(\"err\");\n    se.textContent = s.err; se.hidden = false; $(\"secret\").setAttribute(\"aria-invalid\", \"true\");\n    return { over: false, bad: true };\n  }\n  var n = s.bytes.length; over = n > MAXB;\n  c.textContent = n + \" of \" + MAXB + \" bytes\";\n  c.classList.toggle(\"err\", over);\n  se.hidden = !over; se.textContent = over ? errText(\"SECRET_TOO_LONG\", { have: n, max: MAXB }) : \"\";\n  $(\"secret\").setAttribute(\"aria-invalid\", String(over));\n  C.wipe(s.bytes);\n  return { over: over, bad: false };\n}\nfunction refreshSplitHash(){\n  var out = $(\"sHash\");\n  if(!$(\"sHashBox\").open) { return; }\n  if(!splitShown){ out.textContent = \"Show the secret to see this value.\"; return; }\n  var s = readSecret();\n  if(s.err){ out.textContent = \"Enter valid hex to see this value.\"; return; }\n  if(!s.bytes.length){ out.textContent = \"Enter a secret to see this value.\"; return; }\n  out.textContent = C.toHex(C.sha256(s.bytes));\n  C.wipe(s.bytes);\n}\nfunction setMode(m){\n  if(m === mode) return;\n  mode = m;\n  $(\"modeText\").setAttribute(\"aria-pressed\", String(m === \"text\"));\n  $(\"modeHex\").setAttribute(\"aria-pressed\", String(m === \"hex\"));\n  $(\"secretLab\").textContent = m === \"text\" ? \"Secret (text, UTF-8)\" : \"Secret (hex digits)\";\n  $(\"sHint\").textContent = m === \"text\" ? \"Hidden while you type. Line breaks are kept.\" : \"Hidden while you type. Spaces and line breaks between digits are ignored.\";\n  $(\"secret\").value = \"\";\n  onSplitInput();\n}\n$(\"modeText\").addEventListener(\"click\", function(){ setMode(\"text\"); });\n$(\"modeHex\").addEventListener(\"click\", function(){ setMode(\"hex\"); });\n$(\"sToggle\").addEventListener(\"click\", function(){ splitShown = !splitShown; applySplitMask(); bumpIdle(); });\n$(\"sHashBox\").addEventListener(\"toggle\", refreshSplitHash);\n\nfunction markStale(){\n  if(splitState !== \"done\") return;\n  splitState = \"stale\";\n  $(\"splitResult\").classList.add(\"stale\");\n  $(\"splitStale\").hidden = false;\n  [\"btnCopyAll\", \"btnCheck\"].forEach(function(id){ $(id).disabled = true; });\n  Array.prototype.forEach.call($(\"shareList\").querySelectorAll(\"button\"), function(b){ b.disabled = true; });\n}\nfunction fmtValue(){ return document.querySelector('input[name=\"fmt\"]:checked').value; }\nfunction numVal(id){ var v = $(id).value.trim(); return v === \"\" ? NaN : Number(v); }\n\nfunction updateSentence(){\n  var n = numVal(\"nShares\"), k = numVal(\"kNeeded\");\n  var s = $(\"kSentence\"), w = $(\"kWarn\");\n  w.hidden = true;\n  var okN = Number.isInteger(n) && n >= 2 && n <= 255;\n  var okK = Number.isInteger(k) && k >= 2 && okN && k <= n;\n  $(\"nShares\").setAttribute(\"aria-invalid\", String(!okN));\n  $(\"kNeeded\").setAttribute(\"aria-invalid\", String(!okK));\n  if(!okN){ s.textContent = errText(\"BAD_SHARE_COUNT\"); s.className = \"sentence hint err\"; return; }\n  if(!okK){ s.textContent = errText(\"BAD_THRESHOLD\", { max: n }); s.className = \"sentence hint err\"; return; }\n  s.className = \"sentence\";\n  s.innerHTML = \"\";\n  s.appendChild(document.createTextNode(\"Any \"));\n  s.appendChild(el(\"b\", null, String(k)));\n  s.appendChild(document.createTextNode(\" of the \"));\n  s.appendChild(el(\"b\", null, String(n)));\n  s.appendChild(document.createTextNode(\" shares rebuild the secret. \" + (k - 1 === 1 ? \"1 share or fewer reveals\" : (k - 1) + \" or fewer reveal\") + \" nothing about it.\"));\n  var warn = null;\n  if(k === n) warn = \"Losing one share loses the secret.\";\n  else if(n > 50) warn = \"Many shares are hard to store safely.\";\n  if(warn){ w.hidden = false; w.querySelector(\"span\").textContent = warn; }\n}\nfunction updateLabelState(){\n  var v = $(\"label\").value, bad = v !== \"\" && !C.isValidLabel(v);\n  $(\"label\").setAttribute(\"aria-invalid\", String(bad));\n  $(\"labelHint\").classList.toggle(\"err\", bad);\n}\nfunction updatePadState(){\n  var plain = fmtValue() === \"plain\";\n  $(\"pad\").disabled = plain;\n  $(\"padHint\").textContent = plain\n    ? \"Not available with plain shares, which are exactly as long as the secret plus one byte.\"\n    : \"Rounds the hidden data up to a multiple of this size, so a share does not show the exact length. It hides the length inside that range, not the fact that a secret exists.\";\n}\nfunction updateSplitButton(){\n  var st = updateSecretCount();\n  $(\"btnSplit\").disabled = st.over || splitState === \"working\";\n}\nfunction onSplitInput(){\n  inputVer++;\n  updateSplitButton(); updateSentence(); updateLabelState(); updatePadState();\n  refreshSplitHash(); markStale(); bumpIdle();\n  $(\"splitMsg\").textContent = \"\"; $(\"splitMsg\").className = \"msg\"; $(\"splitMsg\").setAttribute(\"role\", \"status\");\n}\n[\"secret\", \"nShares\", \"kNeeded\", \"label\", \"pad\"].forEach(function(id){ $(id).addEventListener(\"input\", onSplitInput); });\nArray.prototype.forEach.call(document.querySelectorAll('input[name=\"fmt\"]'), function(r){ r.addEventListener(\"change\", onSplitInput); });\n\n$(\"sGen\").addEventListener(\"click\", function(){\n  var len = numVal(\"genLen\");\n  try {\n    var s = C.randomSecret({ length: len });\n    if(mode !== \"text\") setMode(\"text\");\n    $(\"secret\").value = s;\n    splitShown = true; applySplitMask();\n    $(\"genInfo\").textContent = \"Generated \" + s.length + \" characters, \" + C.entropyBits(s.length).toFixed(1) + \" bits of randomness.\";\n    $(\"genInfo\").classList.remove(\"err\");\n    onSplitInput();\n  } catch(e){\n    $(\"genInfo\").textContent = e && e.code === \"BAD_OPTION\" ? \"Length must be a whole number from 8 to 128.\"\n      : e && e.code === \"RANDOM_UNAVAILABLE\" ? \"This window has no secure random source, so no secret was generated.\" : errOf(e);\n    $(\"genInfo\").classList.add(\"err\");\n  }\n});\n$(\"genLen\").addEventListener(\"input\", function(){\n  var n = numVal(\"genLen\");\n  if(Number.isInteger(n) && n >= 8 && n <= 128){\n    $(\"genInfo\").classList.remove(\"err\");\n    $(\"genInfo\").textContent = \"Generate makes a random secret from 28 easy to read letters and digits. At \" + n + \" characters that is \" + C.entropyBits(n).toFixed(1) + \" bits.\";\n  }\n});\n\nfunction wipeSplitResult(){\n  $(\"btnCopyAll\").disabled = false; $(\"btnCheck\").disabled = false;\n  shareTexts = [];\n  $(\"shareList\").textContent = \"\";\n  $(\"splitResult\").hidden = true;\n  $(\"splitResult\").classList.remove(\"stale\");\n  $(\"splitStale\").hidden = true;\n  $(\"splitFallback\").hidden = true; $(\"splitFallbackText\").value = \"\";\n  $(\"checkMsg\").textContent = \"\";\n  splitState = \"idle\";\n}\nfunction clearSplit(){\n  clearVer++; inputVer++;\n  hideFallbacks();\n  $(\"secret\").value = \"\";\n  $(\"sHash\").textContent = \"Show the secret to see this value.\";\n  wipeSplitResult();\n  $(\"splitMsg\").textContent = \"\"; $(\"splitMsg\").className = \"msg\";\n  splitShown = false; applySplitMask();\n  $(\"sHashBox\").open = false;\n  updateSplitButton();\n}\n$(\"sClear\").addEventListener(\"click\", function(){ clearSplit(); toast(\"Cleared\"); });\n$(\"btnWipeSplit\").addEventListener(\"click\", function(){ clearSplit(); $(\"secret\").focus(); toast(\"Cleared\"); });\n\nfunction splitOptions(){\n  var o = { format: fmtValue() };\n  var lab = $(\"label\").value.trim();\n  if(lab) o.label = lab;\n  if(o.format === \"ss1\") o.padTo = Number($(\"pad\").value);\n  return o;\n}\nfunction showSplitError(text){\n  var m = $(\"splitMsg\"); m.setAttribute(\"role\", \"alert\"); m.textContent = text; m.className = \"msg err\";\n}\nfunction renderShares(res, k){\n  var list = $(\"shareList\"); list.textContent = \"\";\n  shareTexts = res.shares.map(function(s){ return s.text; });\n  splitK = k;\n  res.shares.forEach(function(s, i){\n    var li = el(\"li\", \"share\");\n    var head = el(\"div\", \"share-head\");\n    head.appendChild(el(\"span\", \"share-n\", \"Share \" + s.x));\n    var b = el(\"button\", \"btn ghost sm\");\n    b.type = \"button\";\n    b.innerHTML = icon(\"copy\") + \"<span>Copy</span>\";\n    b.setAttribute(\"aria-label\", \"Copy share \" + s.x);\n    b.addEventListener(\"click\", function(){ copyText(s.text, \"Copied share \" + s.x, splitHost); });\n    head.appendChild(b);\n    li.appendChild(head);\n    li.appendChild(el(\"p\", \"share-text\", s.text));\n    list.appendChild(li);\n  });\n  var sum = $(\"splitSummary\"); sum.textContent = \"\";\n  sum.appendChild(document.createTextNode(plural(res.shares.length, \"share\", \"shares\") + \". Any \" + k + \" rebuild the secret.\"));\n  if(res.setId){\n    sum.appendChild(document.createTextNode(\" Set \"));\n    sum.appendChild(el(\"code\", null, res.setId));\n    sum.appendChild(document.createTextNode(\".\"));\n  } else {\n    sum.appendChild(document.createTextNode(\" Plain format: these shares carry no checks.\"));\n  }\n  $(\"cautionText\").textContent = \"Put each share somewhere different. Anyone holding \" + k + \" shares has the secret. Copy all puts every share on the clipboard together. Paste them somewhere temporary, separate them at once, then press Empty clipboard.\";\n}\nvar splitHost = { box: $(\"splitFallback\"), area: $(\"splitFallbackText\") };\n\n$(\"btnSplit\").addEventListener(\"click\", function(){\n  if(splitState === \"working\") return;\n  $(\"splitMsg\").textContent = \"\"; $(\"splitMsg\").className = \"msg\"; $(\"splitMsg\").setAttribute(\"role\", \"status\");\n  var s = readSecret();\n  if(s.err){ showSplitError(s.err); return; }\n  var ver = inputVer, cver = clearVer;\n  var n = numVal(\"nShares\"), k = numVal(\"kNeeded\"), opts = splitOptions();\n  splitState = \"working\";\n  var btn = $(\"btnSplit\"); btn.disabled = true; btn.classList.add(\"working\");\n  setIcon(btn, \"refresh\", \"Splitting...\");\n  wipeSplitResult(); splitState = \"working\";\n  $(\"splitMsg\").textContent = \"Working...\";\n  frame(function(){\n    var bytes = s.bytes, copy = bytes.slice();\n    try {\n      var res = C.split(bytes, n, k, opts);\n      /* self check: rebuild from the first k shares and compare */\n      var back = C.combine(res.shares.slice(0, k));\n      var good = eqBytes(back.secret, copy);\n      C.wipe(back.secret);\n      if(!good){ throw new Error(\"selfcheck\"); }\n      if(cver !== clearVer){ splitState = \"idle\"; $(\"splitMsg\").textContent = \"\"; }\n      else {\n        renderShares(res, k);\n        $(\"splitResult\").hidden = false;\n        splitState = \"done\";\n        $(\"splitMsg\").textContent = \"\";\n        if(ver !== inputVer) markStale();\n        else $(\"splitResultTitle\").focus();\n      }\n    } catch(e){\n      wipeSplitResult();\n      showSplitError(e && e.code ? errOf(e) : \"The self check failed, so these shares were discarded. Try again.\");\n    } finally {\n      C.wipe(bytes); C.wipe(copy);\n      btn.classList.remove(\"working\");\n      setIcon(btn, \"scissors\", \"Split\");\n      updateSplitButton();\n    }\n  });\n});\n\n$(\"btnCopyAll\").addEventListener(\"click\", function(){\n  copyText(shareTexts.join(\"\\n\"), \"Copied \" + plural(shareTexts.length, \"share\", \"shares\"), splitHost);\n});\n$(\"btnClearClip\").addEventListener(\"click\", function(){\n  if(navigator.clipboard && navigator.clipboard.writeText){\n    navigator.clipboard.writeText(\"\").then(function(){ toast(\"Clipboard emptied\"); },\n      function(){ toast(\"Clipboard could not be emptied\"); });\n  } else { toast(\"Clipboard could not be emptied\"); }\n});\n$(\"btnCheck\").addEventListener(\"click\", function(){\n  var m = $(\"checkMsg\"); m.className = \"msg\"; m.textContent = \"Checking...\";\n  var k = splitK;\n  var s = readSecret();\n  frame(function(){\n    try {\n      if(s.err) throw new Error(\"x\");\n      var idx = shareTexts.map(function(_, i){ return i; });\n      var r = new Uint8Array(idx.length); crypto.getRandomValues(r);\n      idx.sort(function(a, b){ return r[a] - r[b]; });\n      var pick = idx.slice(0, k).map(function(i){ return shareTexts[i]; });\n      var res = C.combine(pick);\n      var same = eqBytes(res.secret, s.bytes);\n      C.wipe(res.secret);\n      if(same){ m.textContent = plural(k, \"share\", \"shares\") + \" rebuilt the secret.\"; m.className = \"msg ok\"; }\n      else { m.textContent = \"These shares did not rebuild the secret you entered. Do not use them.\"; m.className = \"msg err\"; }\n    } catch(e){\n      m.textContent = e && e.code ? errOf(e) : \"The check could not run. Split again.\";\n      m.className = \"msg err\";\n    } finally { if(s.bytes) C.wipe(s.bytes); }\n  });\n});\n\n/* ================= COMBINE ================= */\nvar combined = null;   // { secret: Uint8Array, text: string|null, res }\nvar lastTokens = [];\n\nfunction plainThreshold(){\n  var v = $(\"cThr\").value.trim();\n  if(v === \"\") return { value: undefined };\n  var n = Number(v);\n  if(Number.isInteger(n) && n >= 2 && n <= 255) return { value: n };\n  return { bad: true };\n}\nfunction setStatus(kind, text){\n  var s = $(\"cStatus\");\n  s.className = \"status \" + kind;\n  s.innerHTML = icon(kind === \"ready\" ? \"ok\" : kind === \"bad\" ? \"alert\" : \"info\");\n  s.appendChild(el(\"span\", null, text));\n  $(\"shares\").setAttribute(\"aria-invalid\", String(kind === \"bad\"));\n}\n\nvar ZERO_WIDTH = /[\\u200B\\u200C\\u200D\\uFEFF\\u2060]/g;\nvar ROW_CAP = 300;\nvar combineVer = 0;        // bumped on every Combine input\nvar lastValid = [];        // tokens that parsed, in input order\nvar PRIORITY = [\"MIXED_FORMATS\", \"MIXED_SETS\", \"LENGTH_MISMATCH\", \"DUPLICATE_X\"];\n\n/* An unparsed entry that looks like a damaged share gets its specific message. */\nfunction looksLikeShare(raw){\n  return /ss1/i.test(raw) || (raw.length >= 20 && /[0-9a-f]{12}/i.test(raw));\n}\n\nfunction refreshCombine(){\n  var text = $(\"shares\").value.replace(ZERO_WIDTH, \"\");\n  var tokens = C.parseShares(text);\n  lastTokens = tokens;\n  var valid = [];\n  for(var q = 0; q < tokens.length; q++) if(tokens[q].share) valid.push(tokens[q]);\n  lastValid = valid;\n  var ignored = tokens.length - valid.length;\n  var list = $(\"parsedList\"); list.textContent = \"\";\n  var thr = plainThreshold();\n  var btn = $(\"btnCombine\");\n  btn.disabled = true;\n  $(\"cThr\").setAttribute(\"aria-invalid\", String(!!thr.bad));\n  if(!tokens.length){\n    $(\"plainThr\").hidden = true;\n    setStatus(\"idle\", \"Paste at least two shares.\");\n    return;\n  }\n  var shares = valid.map(function(t){ return t.share; });\n  var st = C.validateShares(shares, thr.value != null ? { threshold: thr.value } : undefined);\n\n  /* the problem code every flagged row and the status line agree on */\n  var top = null, byCode = {};\n  st.problems.forEach(function(p){ byCode[p.code] = p.indices; });\n  for(var pi = 0; pi < PRIORITY.length; pi++) if(byCode[PRIORITY[pi]]){ top = PRIORITY[pi]; break; }\n  var flagged = {}, minorityCode = null;\n  if(top === \"MIXED_FORMATS\"){\n    var nSs = 0, nPl = 0;\n    shares.forEach(function(sh){ if(sh.format === \"ss1\") nSs++; else nPl++; });\n    if(nSs !== nPl){\n      var minor = nSs < nPl ? \"ss1\" : \"plain\";\n      minorityCode = minor === \"ss1\" ? \"TYPO_MINOR_SANCTUM\" : \"TYPO_MINOR_PLAIN\";\n      shares.forEach(function(sh, i){ if(sh.format === minor) flagged[i] = true; });\n    }\n  } else if(top){\n    byCode[top].forEach(function(i){ flagged[i] = true; });\n  }\n\n  var setIds = [], seenSet = {}, anyPlain = false;\n  shares.forEach(function(sh){\n    if(sh.format === \"plain\") anyPlain = true;\n    if(sh.setId && !seenSet[sh.setId]){ seenSet[sh.setId] = true; setIds.push(sh.setId); }\n  });\n  var dupX = null;\n  if(top === \"DUPLICATE_X\"){ var di = byCode.DUPLICATE_X[0]; dupX = shares[di].x; }\n\n  var vi = 0, seen = {};\n  var rendered = Math.min(tokens.length, ROW_CAP);\n  for(var ti = 0; ti < rendered; ti++){\n    var t = tokens[ti];\n    var li = el(\"li\");\n    if(t.share){\n      var sh = t.share, myIdx = vi++;\n      var key = sh.format + \"|\" + (sh.setId || \"\") + \"|\" + C.toHex(sh.body);\n      var dupe = seen[key]; seen[key] = true;\n      var isBad = !dupe && flagged[myIdx];\n      li.className = dupe ? \"note\" : (isBad ? \"bad\" : \"ok\");\n      li.innerHTML = icon(dupe ? \"info\" : isBad ? \"x\" : \"ok\");\n      li.appendChild(el(\"span\", \"who\", \"Share \" + sh.x));\n      li.appendChild(document.createTextNode(\" \"));\n      if(sh.setId){ li.appendChild(el(\"span\", \"sid\", \"set \" + sh.setId)); li.appendChild(document.createTextNode(\" \")); }\n      var msg = dupe ? \"same as a share above, ignored\"\n        : isBad ? errText(minorityCode || top, { x: sh.x }, { setIds: setIds })\n        : \"ok\";\n      li.appendChild(el(\"span\", dupe || isBad ? null : \"muted\", msg));\n    } else {\n      var likely = looksLikeShare(t.raw);\n      li.className = likely ? \"bad\" : \"note\";\n      li.innerHTML = icon(likely ? \"x\" : \"info\");\n      li.appendChild(el(\"span\", \"who\", \"Entry \" + (t.index + 1)));\n      li.appendChild(document.createTextNode(\" \"));\n      li.appendChild(el(\"span\", null, likely ? errText(t.error.code, {}, { onShare: true }) + \" It is ignored.\" : \"Not a share, ignored\"));\n    }\n    list.appendChild(li);\n  }\n  if(tokens.length > rendered){\n    var more = el(\"li\", \"note\");\n    more.innerHTML = icon(\"info\");\n    more.appendChild(el(\"span\", null, \"and \" + (tokens.length - rendered) + \" more entries not shown\"));\n    list.appendChild(more);\n  }\n\n  $(\"plainThr\").hidden = !(anyPlain || st.format === \"plain\" || st.format === \"mixed\");\n  var junkNote = ignored ? \" \" + (ignored === 1 ? \"1 entry was not recognised and is ignored.\" : ignored + \" entries were not recognised and are ignored.\") : \"\";\n  var dupNote = st.ignoredDuplicates ? \" \" + plural(st.ignoredDuplicates, \"repeated share\", \"repeated shares\") + \" ignored.\" : \"\";\n  if(thr.bad){\n    setStatus(\"bad\", errText(\"BAD_OPTION\", { option: \"threshold\" }));\n    return;\n  }\n  if(st.count === 0){\n    setStatus(\"idle\", \"No shares recognised yet.\" + junkNote);\n    return;\n  }\n  if(top){\n    setStatus(\"bad\", errText(top, { x: dupX }, { setIds: setIds }) + dupNote + junkNote);\n    return;\n  }\n  if(st.format === \"plain\" && thr.value == null){\n    if(st.count < 2) setStatus(\"idle\", \"1 valid share. Paste at least two.\" + dupNote + junkNote);\n    else { setStatus(\"ready\", plural(st.count, \"share\", \"shares\") + \" pasted. Plain shares do not say how many are needed, so all of them will be used.\" + dupNote + junkNote); btn.disabled = false; }\n    return;\n  }\n  if(st.missing > 0){\n    setStatus(\"idle\", st.count + \" of \" + st.needed + \" needed. Add \" + st.missing + \" more.\" + dupNote + junkNote);\n    return;\n  }\n  setStatus(\"ready\", st.count + \" of \" + st.needed + \" needed. Ready.\" + dupNote + junkNote);\n  btn.disabled = false;\n}\n\nfunction wipeCombined(){\n  if(combined){ C.wipe(combined.secret); combined = null; }\n  $(\"cOut\").textContent = \"\";\n  $(\"combineResult\").hidden = true;\n  $(\"cVerdict\").textContent = \"\"; $(\"cWarnings\").textContent = \"\";\n  $(\"cHash\").textContent = \"Show the secret to see this value.\";\n  $(\"combineFallback\").hidden = true; $(\"combineFallbackText\").value = \"\";\n}\nfunction onSharesInput(){\n  combineVer++;\n  wipeCombined();\n  $(\"combineErr\").hidden = true;\n  refreshCombine();\n  bumpIdle();\n}\n$(\"shares\").addEventListener(\"input\", onSharesInput);\n$(\"cThr\").addEventListener(\"input\", onSharesInput);\n\n$(\"btnCClear\").addEventListener(\"click\", function(){\n  combineVer++;\n  $(\"shares\").value = \"\"; $(\"cThr\").value = \"\";\n  wipeCombined(); $(\"combineErr\").hidden = true; refreshCombine();\n  combineShown = false; $(\"cHashBox\").open = false;\n  $(\"shares\").focus(); toast(\"Cleared\");\n});\n$(\"cWipe\").addEventListener(\"click\", function(){\n  combineVer++;\n  wipeCombined(); combineShown = false; $(\"cHashBox\").open = false;\n  $(\"shares\").focus(); toast(\"Cleared\");\n});\n\n$(\"btnCombine\").addEventListener(\"click\", function(){\n  var btn = $(\"btnCombine\");\n  var thr = plainThreshold();\n  if(thr.bad) return;\n  wipeCombined(); $(\"combineErr\").hidden = true;\n  btn.disabled = true; btn.classList.add(\"working\"); setIcon(btn, \"refresh\", \"Combining...\");\n  var vtokens = lastValid, ver = combineVer;\n  var inputs = vtokens.map(function(t){ return t.share; });\n  frame(function(){\n    try {\n      var res = C.combine(inputs, thr.value != null ? { threshold: thr.value } : undefined);\n      if(ver !== combineVer){ C.wipe(res.secret); }\n      else showCombined(res, vtokens);\n    } catch(e){\n      if(ver === combineVer){\n        var st = C.validateShares(inputs);\n        $(\"combineErrText\").textContent = errOf(e, { tokens: vtokens, need: st && st.needed ? st.needed : null });\n        $(\"combineErr\").hidden = false;\n      }\n    } finally {\n      btn.classList.remove(\"working\"); setIcon(btn, \"combine\", \"Combine\");\n      refreshCombine();\n    }\n  });\n});\n\nfunction showCombined(res, tokens){\n  var text = C.utf8Decode(res.secret);\n  combined = { secret: res.secret, text: text, res: res };\n  combineShown = false;\n  viewMode = text === null ? \"hex\" : \"text\";\n  var v = $(\"cVerdict\"); v.textContent = \"\";\n  var line = el(\"p\", \"okline\");\n  if(res.format === \"ss1\" && res.verified){\n    line.innerHTML = icon(\"ok\");\n    line.appendChild(el(\"span\", null, \"The shares belong to one set and agree with each other.\"));\n    v.appendChild(line);\n    v.appendChild(el(\"p\", \"hint\", \"This does not prove the secret is the original. If you recorded the SHA-256 when you split, compare it below. That is the stronger check.\"));\n  } else {\n    line.className = \"okline neutral\";\n    line.innerHTML = icon(\"info\");\n    line.appendChild(el(\"span\", null, \"These shares carry no check value. If you have the SHA-256 from when you split, compare it below. That is the stronger check.\"));\n    v.appendChild(line);\n  }\n  var w = $(\"cWarnings\"); w.textContent = \"\";\n  (res.warnings || []).forEach(function(wn){\n    if(wn.code !== \"EXTRA_SHARE_MISMATCH\") return;\n    var xs = wn.inputIndices.map(function(i){ var t = tokens[i]; return t && t.share ? t.share.x : (i + 1); });\n    var p = el(\"p\", \"okline warn\");\n    p.innerHTML = icon(\"alert\");\n    p.appendChild(el(\"span\", null, (xs.length > 1 ? \"Shares \" : \"Share \") + xs.join(\", \") + (xs.length > 1 ? \" do not match the others. \" : \" does not match the others. \") +\n      \"The secret was rebuilt from \" + plural(res.sharesUsed, \"share\", \"shares\") + \". Replace \" + (xs.length > 1 ? \"those shares.\" : \"that share.\")));\n    w.appendChild(p);\n  });\n  var note = $(\"cViewNote\");\n  note.hidden = text !== null;\n  note.textContent = text === null ? \"This is not valid text, so it is shown as hex.\" : \"\";\n  $(\"viewText\").disabled = text === null;\n  $(\"combineResult\").hidden = false;\n  applyCombineView();\n  $(\"combineResultTitle\").focus();\n}\nfunction currentView(){\n  if(!combined) return \"\";\n  return viewMode === \"text\" && combined.text !== null ? combined.text : C.toHex(combined.secret);\n}\nfunction applyCombineView(){\n  $(\"cToggle\").setAttribute(\"aria-pressed\", String(combineShown));\n  setIcon($(\"cToggle\"), combineShown ? \"eye-off\" : \"eye\", combineShown ? \"Hide\" : \"Show\");\n  $(\"viewText\").setAttribute(\"aria-pressed\", String(viewMode === \"text\"));\n  $(\"viewHex\").setAttribute(\"aria-pressed\", String(viewMode === \"hex\"));\n  $(\"cBox\").classList.toggle(\"shown\", combineShown);\n  $(\"cOut\").setAttribute(\"aria-label\", combineShown ? \"Rebuilt secret\" : \"Rebuilt secret, hidden\");\n  if(!combined){ $(\"cOut\").textContent = \"\"; return; }\n  var n = combined.secret.length;\n  $(\"cMeta\").textContent = plural(n, \"byte\", \"bytes\");\n  $(\"cOut\").textContent = combineShown ? currentView() : new Array(Math.min(n, 40) + 1).join(\"•\");\n  refreshCombineHash();\n}\nfunction refreshCombineHash(){\n  if(!$(\"cHashBox\").open) return;\n  var out = $(\"cHash\");\n  if(!combined || !combineShown){ out.textContent = \"Show the secret to see this value.\"; return; }\n  out.textContent = C.toHex(C.sha256(combined.secret));\n}\n$(\"cHashBox\").addEventListener(\"toggle\", refreshCombineHash);\n$(\"cToggle\").addEventListener(\"click\", function(){ combineShown = !combineShown; applyCombineView(); bumpIdle(); });\n$(\"viewText\").addEventListener(\"click\", function(){ if(combined && combined.text !== null){ viewMode = \"text\"; applyCombineView(); } });\n$(\"viewHex\").addEventListener(\"click\", function(){ viewMode = \"hex\"; applyCombineView(); });\n$(\"cCopy\").addEventListener(\"click\", function(){\n  if(!combined) return;\n  copyText(currentView(), \"Copied the secret\", { box: $(\"combineFallback\"), area: $(\"combineFallbackText\") });\n});\n\n/* ---------- start ---------- */\napplySplitMask(); applyCombineView();\nupdateSplitButton(); updateSentence(); updateLabelState(); updatePadState(); refreshCombine();\n})();\n</script>\n</body>\n</html>\n","signature":{"payload":"eyJzY2hlbWEiOjEsImFwcF9pZCI6InNoLmVuaWdtYS5zaGFtaXIiLCJuYW1lIjoiU2hhbWlyIFNlY3JldCBTaGFyaW5nIiwidmVyc2lvbiI6IjEuMC4yIiwiY2hlY2tzdW0iOiJzaGEyNTY6MTYwMWU3MDZiMTY4OGZlNTk4NjQ3NDQzMjc5MzRiOGNjNmNjMDM2YjNmY2NjMGFkZGE5MjBiMWQxNDZiYmY5NCIsImRlY2xhcmVkIjpbXSwiaXNzdWVkX2F0IjoxNzkwOTAwOTAxfQ","sig":"E8rJPnnnjZc4XsrL1tE_SBi0skVCuEnlFV2H8kg7L2S0THsX7yBmSLeavJGVu_DOS_IwpGmwg1q0Oa7khve4AA","keyid":"ed25519:2daedA0n4NSI2p2McCqLBfGiKRmR5XCm8lypbQTT6hg","chain":[{"payload":"eyJzY2hlbWEiOjEsInN1YmplY3QiOiJlZDI1NTE5OjJkYWVkQTBuNE5TSTJwMk1jQ3FMQmZHaUtSbVI1WENtOGx5cGJRVFQ2aGciLCJuYW1lIjoiRW5pZ21hIFRlY2hub2xvZ2llcyBTb2x1dGlvbnMiLCJub3RfYmVmb3JlIjoxNzkwNzk0NDE5LCJub3RfYWZ0ZXIiOjE4NTM5NTI4MTYsInNjb3BlIjpbInNoLmVuaWdtYS4iXX0","sig":"-lytziD13TmzrLBjPz4mtYuWsjJnBeVMpn7cCtglpTgd_F2tlTZG9re7AW_DGRxRS1uwEbi1kI0GszPfsg10CQ","keyid":"ed25519:GmNzEGh_57SWBOBLNV7kEnhykqoCkcCCMAnbkh0IVHY"}]}}