Skip to content

Instantly share code, notes, and snippets.

@secdev02
Last active June 21, 2026 12:42
Show Gist options
  • Select an option

  • Save secdev02/d815a34f3858a828aaccb71f48b55419 to your computer and use it in GitHub Desktop.

Select an option

Save secdev02/d815a34f3858a828aaccb71f48b55419 to your computer and use it in GitHub Desktop.
HTML One Time Pad
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OTP Substitution Cipher</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #ffffff;
--bg2: #f5f5f3;
--bg3: #ebebea;
--text: #1a1a19;
--text2: #6b6b67;
--text3: #a0a09c;
--border: rgba(0,0,0,0.12);
--border2: rgba(0,0,0,0.22);
--info-bg: #e6f1fb;
--info-text: #185fa5;
--info-border: #b5d4f4;
--danger-bg: #fcebeb;
--danger-text: #a32d2d;
--danger-border: #f7c1c1;
--success-bg: #eaf3de;
--success-text: #3b6d11;
--success-border: #c0dd97;
--radius: 8px;
--radius-lg: 12px;
--mono: 'Courier New', Courier, monospace;
--sans: system-ui, -apple-system, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1e1e1c;
--bg2: #2a2a28;
--bg3: #323230;
--text: #f0efe9;
--text2: #a0a09c;
--text3: #6b6b67;
--border: rgba(255,255,255,0.12);
--border2: rgba(255,255,255,0.22);
--info-bg: #0c2d4a;
--info-text: #85b7eb;
--info-border: #185fa5;
--danger-bg: #3a1010;
--danger-text: #f09595;
--danger-border: #a32d2d;
--success-bg: #17340a;
--success-text: #97c459;
--success-border: #3b6d11;
}
}
body {
font-family: var(--sans);
background: var(--bg);
color: var(--text);
min-height: 100vh;
padding: 2rem 1rem;
}
.container { max-width: 720px; margin: 0 auto; }
h1 { font-size: 20px; font-weight: 500; margin-bottom: 0.25rem; }
.subtitle {
font-size: 13px;
color: var(--text2);
margin-bottom: 1.75rem;
line-height: 1.6;
}
.tabs {
display: flex;
border-bottom: 0.5px solid var(--border);
margin-bottom: 1.5rem;
}
.tab {
padding: 8px 18px;
font-size: 13px;
cursor: pointer;
border: none;
background: transparent;
color: var(--text2);
border-bottom: 2px solid transparent;
margin-bottom: -0.5px;
transition: color 0.15s;
font-family: var(--sans);
}
.tab.active { color: var(--text); border-bottom-color: var(--text); }
.tab:hover { color: var(--text); }
.panel { display: none; }
.panel.active { display: block; }
label {
display: block;
font-size: 11px;
color: var(--text2);
margin-bottom: 5px;
letter-spacing: 0.05em;
text-transform: uppercase;
}
textarea {
width: 100%;
height: 110px;
resize: vertical;
font-family: var(--mono);
font-size: 12px;
background: var(--bg2);
color: var(--text);
border: 0.5px solid var(--border);
border-radius: var(--radius);
padding: 9px 11px;
line-height: 1.6;
}
textarea:focus, input:focus, select:focus {
outline: none;
border-color: var(--border2);
}
input[type="text"], input[type="number"] {
font-family: var(--mono);
font-size: 12px;
padding: 7px 10px;
background: var(--bg2);
color: var(--text);
border: 0.5px solid var(--border);
border-radius: var(--radius);
width: 100%;
}
select {
font-size: 12px;
padding: 7px 10px;
background: var(--bg2);
color: var(--text);
border: 0.5px solid var(--border);
border-radius: var(--radius);
width: 100%;
}
button {
font-size: 12px;
font-family: var(--sans);
padding: 7px 14px;
border-radius: var(--radius);
border: 0.5px solid var(--border2);
background: transparent;
color: var(--text);
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
white-space: nowrap;
}
button:hover { background: var(--bg2); }
button:active { transform: scale(0.98); }
button.primary {
background: var(--info-bg);
color: var(--info-text);
border-color: var(--info-border);
}
button.primary:hover { opacity: 0.85; }
button.danger {
background: var(--danger-bg);
color: var(--danger-text);
border-color: var(--danger-border);
}
button.success {
background: var(--success-bg) !important;
color: var(--success-text) !important;
border-color: var(--success-border) !important;
}
.row {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 1rem;
align-items: flex-end;
}
.col { flex: 1; min-width: 110px; }
.mono-box {
font-family: var(--mono);
font-size: 12px;
background: var(--bg2);
border: 0.5px solid var(--border);
border-radius: var(--radius);
padding: 10px 12px;
color: var(--text);
word-break: break-all;
white-space: pre-wrap;
line-height: 1.8;
min-height: 48px;
}
.output-wrap {
position: relative;
margin-bottom: 0.5rem;
}
.output-actions {
display: flex;
gap: 6px;
margin-top: 6px;
flex-wrap: wrap;
}
.pad-out {
font-family: var(--mono);
font-size: 12px;
background: var(--bg2);
border: 0.5px solid var(--border);
border-radius: var(--radius);
padding: 10px 12px;
color: var(--text);
max-height: 200px;
overflow-y: auto;
line-height: 2;
word-break: break-all;
white-space: pre-wrap;
}
.note {
font-size: 11px;
color: var(--text3);
line-height: 1.6;
margin-top: 8px;
}
.sec { margin-bottom: 1.25rem; }
.badge {
display: inline-block;
font-size: 11px;
padding: 2px 8px;
border-radius: var(--radius);
margin-left: 6px;
background: var(--info-bg);
color: var(--info-text);
border: 0.5px solid var(--info-border);
}
.divider {
border: none;
border-top: 0.5px solid var(--border);
margin: 1.25rem 0;
}
.status { font-size: 11px; color: var(--danger-text); margin-top: 4px; }
#userSeedRow { display: none; }
.save-all-bar {
margin-top: 2rem;
padding-top: 1.25rem;
border-top: 0.5px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.save-all-label {
font-size: 11px;
color: var(--text3);
letter-spacing: 0.05em;
text-transform: uppercase;
white-space: nowrap;
}
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(60px);
background: var(--text);
color: var(--bg);
font-size: 12px;
padding: 8px 18px;
border-radius: var(--radius);
opacity: 0;
transition: opacity 0.2s, transform 0.2s;
pointer-events: none;
z-index: 999;
white-space: nowrap;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
<div class="container">
<h1>OTP Substitution Cipher</h1>
<p class="subtitle">
One-time pad generator (L'Ecuyer PRNG + Bays-Durham shuffle) combined with a positional substitution cipher.
Everything runs in your browser &mdash; nothing is transmitted.
</p>
<div class="tabs">
<button class="tab active" onclick="switchTab('otp', this)">OTP generator</button>
<button class="tab" onclick="switchTab('encode', this)">Encode</button>
<button class="tab" onclick="switchTab('decode', this)">Decode</button>
<button class="tab" onclick="switchTab('keys', this)">Keys</button>
</div>
<!-- OTP GENERATOR -->
<div class="panel active" id="panel-otp">
<div class="row">
<div class="col">
<label>Number of keys</label>
<input type="number" id="numKeys" value="10" min="1" max="500">
</div>
<div class="col">
<label>Key length (chars)</label>
<input type="number" id="keyLen" value="20" min="4" max="200">
</div>
<div class="col">
<label>Group size (0 = none)</label>
<input type="number" id="grpLen" value="5" min="0" max="20">
</div>
</div>
<div class="row">
<div class="col">
<label>Composition</label>
<select id="composition">
<option value="alpha">Alphabetic</option>
<option value="numeric">Numeric</option>
<option value="wordlike">Word-like</option>
<option value="gibberish">Gibberish (most secure)</option>
</select>
</div>
<div class="col">
<label>Case</label>
<select id="letterCase">
<option value="upper">Upper case</option>
<option value="lower">Lower case</option>
<option value="mixed">Mixed case</option>
</select>
</div>
<div class="col">
<label>Seed</label>
<select id="seedMode" onchange="toggleSeed()">
<option value="clock">From clock</option>
<option value="user">User-defined</option>
</select>
</div>
</div>
<div class="row" id="userSeedRow">
<div class="col" style="flex:2">
<label>Custom seed (digits or any text)</label>
<input type="text" id="userSeed" placeholder="Enter seed string or number">
</div>
</div>
<div class="row">
<button class="primary" onclick="generatePad()">Generate pad</button>
<button class="danger" onclick="loadPadAsCipherKey()">Use pad as cipher key &rarr;</button>
</div>
<label>Generated one-time pad</label>
<div class="pad-out" id="padOutput">Press "Generate pad" to create keys.</div>
<div class="output-actions">
<button onclick="copyTextFrom('padOutput', this)">Copy pad</button>
<button onclick="saveText('padOutput', 'otp-pad.txt')">Save pad as .txt</button>
</div>
<p class="note">
L'Ecuyer PRNG with Bays-Durham shuffle &mdash; same algorithm as Fourmilab's OTP generator.
"Use pad as cipher key" extracts the first 26 unique letters from the pad as your substitution key.
</p>
</div>
<!-- ENCODE -->
<div class="panel" id="panel-encode">
<div class="sec">
<label>Active cipher key <span class="badge" id="enc-key-badge">none</span></label>
<div class="mono-box" id="enc-key-display" style="font-size:11px">
No key loaded. Generate a pad and press "Use as cipher key", or set one in the Keys tab.
</div>
</div>
<div class="sec">
<label>Plaintext input</label>
<textarea id="plainInput" placeholder="Paste your proprietary content here..."></textarea>
</div>
<div class="row">
<button class="primary" onclick="doEncode()">Encode &rarr;</button>
<button onclick="document.getElementById('plainInput').value='';document.getElementById('cipherOutput').textContent=''">Clear</button>
</div>
<label>Ciphertext output</label>
<div class="mono-box" id="cipherOutput"></div>
<div class="output-actions">
<button onclick="copyTextFrom('cipherOutput', this)">Copy ciphertext</button>
<button onclick="saveText('cipherOutput', 'ciphertext.txt')">Save as .txt</button>
</div>
<p class="note">
Substitution cipher with positional shift. The same plaintext letter encodes differently
depending on its position &mdash; defeats simple frequency analysis.
</p>
</div>
<!-- DECODE -->
<div class="panel" id="panel-decode">
<div class="sec">
<label>Active cipher key <span class="badge" id="dec-key-badge">none</span></label>
<div class="mono-box" id="dec-key-display" style="font-size:11px">No key loaded.</div>
</div>
<div class="sec">
<label>Ciphertext input</label>
<textarea id="cipherInput" placeholder="Paste encoded text here..."></textarea>
</div>
<div class="row">
<button class="primary" onclick="doDecode()">Decode &rarr;</button>
<button onclick="document.getElementById('cipherInput').value='';document.getElementById('plainOutput').textContent=''">Clear</button>
</div>
<label>Plaintext output</label>
<div class="mono-box" id="plainOutput"></div>
<div class="output-actions">
<button onclick="copyTextFrom('plainOutput', this)">Copy plaintext</button>
<button onclick="saveText('plainOutput', 'plaintext.txt')">Save as .txt</button>
</div>
</div>
<!-- KEYS -->
<div class="panel" id="panel-keys">
<div class="sec">
<label>Active substitution key</label>
<div class="mono-box" id="keyDisplay" style="font-size:11px">No key loaded.</div>
<div class="output-actions">
<button onclick="copyTextFrom('keyDisplay', this)">Copy key</button>
<button onclick="saveKeyFile()">Save key as .txt</button>
</div>
</div>
<hr class="divider">
<div class="sec">
<label>Generate random key (independent of OTP)</label>
<div class="row">
<button onclick="genRandKey()">Generate random key</button>
</div>
</div>
<hr class="divider">
<div class="sec">
<label>Import key &mdash; 26 unique A&ndash;Z letters</label>
<div class="row">
<div class="col" style="flex:2">
<input type="text" id="importField" maxlength="26"
placeholder="QWERTYUIOPASDFGHJKLZXCVBNM"
oninput="this.value=this.value.toUpperCase()">
</div>
<button onclick="importKey()">Import</button>
</div>
<div id="importErr" class="status"></div>
</div>
<hr class="divider">
<p class="note">
<strong>How it works:</strong> Each plaintext letter is first substituted via the key mapping,
then shifted by its position index mod 26. Identical letters encode differently throughout
the text, making LLM pattern-matching and frequency analysis ineffective without the key.
</p>
</div>
<!-- SAVE ALL BAR -->
<div class="save-all-bar">
<span class="save-all-label">Save bundle</span>
<button onclick="saveBundle()">Save all as .txt</button>
<button onclick="copyBundle(this)">Copy all to clipboard</button>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
// ── L'Ecuyer two-sequence PRNG with Bays-Durham shuffle ──────────────────
var rng = (function () {
var m1 = 2147483563, m2 = 2147483399;
var a1 = 40014, a2 = 40692;
var q1 = 53668, q2 = 52774;
var r1 = 12211, r2 = 3791;
var TABLE_SIZE = 32;
var s1, s2, y, table;
function init(seed) {
seed = Math.abs(Math.floor(seed)) % 2147483647 || 1;
s1 = seed; s2 = seed;
table = new Array(TABLE_SIZE);
for (var i = TABLE_SIZE + 7; i >= 0; i--) {
var k = Math.floor(s1 / q1);
s1 = a1 * (s1 - k * q1) - r1 * k;
if (s1 < 0) s1 += m1;
if (i < TABLE_SIZE) table[i] = s1;
}
y = table[0];
}
function rand() {
var k = Math.floor(s1 / q1);
s1 = a1 * (s1 - k * q1) - r1 * k;
if (s1 < 0) s1 += m1;
k = Math.floor(s2 / q2);
s2 = a2 * (s2 - k * q2) - r2 * k;
if (s2 < 0) s2 += m2;
var j = Math.floor(y / (1 + (m1 - 1) / TABLE_SIZE));
y = table[j] - s2;
table[j] = s1;
if (y < 1) y += m1 - 1;
return y / (m1 - 1);
}
function seedFromStr(s) {
if (/^\d+$/.test(s)) return parseInt(s, 10) % 2147483647;
var h = 0;
for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) & 0x7fffffff;
return h || 1;
}
return { init: init, rand: rand, seedFromStr: seedFromStr };
})();
// ── constants ─────────────────────────────────────────────────────────────
var AZ = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
var VOWELS = 'aeiou';
var CONSONANTS = 'bcdfghjklmnpqrstvwxyz';
var GIBBERISH = 'abcdefghijklmnopqrstuvwxyz0123456789!@#%^&*_+=~|;:,.?';
var cipherKey = null;
// ── toast notification ────────────────────────────────────────────────────
function showToast(msg) {
var t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(function () { t.classList.remove('show'); }, 1800);
}
// ── key management ────────────────────────────────────────────────────────
function updateKeyDisplays() {
if (!cipherKey) return;
var line = 'Plain: ' + AZ + '\nCipher: ' + cipherKey;
document.getElementById('keyDisplay').textContent = line;
document.getElementById('enc-key-display').textContent = line;
document.getElementById('dec-key-display').textContent = line;
document.getElementById('enc-key-badge').textContent = cipherKey.slice(0, 6) + '...';
document.getElementById('dec-key-badge').textContent = cipherKey.slice(0, 6) + '...';
}
function genRandKey() {
var az = AZ.split('');
for (var i = az.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var t = az[i]; az[i] = az[j]; az[j] = t;
}
cipherKey = az.join('');
updateKeyDisplays();
}
function importKey() {
var raw = document.getElementById('importField').value.trim().toUpperCase();
var err = document.getElementById('importErr');
if (raw.length !== 26) { err.textContent = 'Must be exactly 26 characters.'; return; }
var s = new Set(raw.split(''));
if (s.size !== 26) { err.textContent = 'All 26 letters must appear exactly once.'; return; }
for (var i = 0; i < raw.length; i++) {
if (AZ.indexOf(raw[i]) < 0) { err.textContent = 'Only A-Z letters allowed.'; return; }
}
err.textContent = '';
cipherKey = raw;
updateKeyDisplays();
showToast('Key imported');
}
// ── OTP generation ────────────────────────────────────────────────────────
function applyCase(ch, mode) {
if (mode === 'upper') return ch.toUpperCase();
if (mode === 'lower') return ch.toLowerCase();
return rng.rand() < 0.5 ? ch.toUpperCase() : ch.toLowerCase();
}
function genChar(comp, cs) {
var r = rng.rand();
if (comp === 'numeric') return String(Math.floor(r * 10));
if (comp === 'alpha') return applyCase(AZ[Math.floor(r * 26)], cs);
if (comp === 'wordlike') {
var pool = rng.rand() < 0.4 ? VOWELS : CONSONANTS;
return applyCase(pool[Math.floor(rng.rand() * pool.length)], cs);
}
var c = GIBBERISH[Math.floor(rng.rand() * GIBBERISH.length)];
return /[a-z]/.test(c) ? applyCase(c, cs) : c;
}
function generatePad() {
var n = Math.max(1, Math.min(500, parseInt(document.getElementById('numKeys').value) || 10));
var kl = Math.max(1, Math.min(200, parseInt(document.getElementById('keyLen').value) || 20));
var gl = Math.max(0, parseInt(document.getElementById('grpLen').value) || 0);
var comp = document.getElementById('composition').value;
var cs = document.getElementById('letterCase').value;
var seedMode = document.getElementById('seedMode').value;
var seed;
if (seedMode === 'user') {
var sv = document.getElementById('userSeed').value.trim();
seed = sv ? rng.seedFromStr(sv) : Date.now() % 2147483647;
} else {
seed = Date.now() % 2147483647;
}
rng.init(seed);
var lines = [];
for (var i = 0; i < n; i++) {
var chars = [];
for (var j = 0; j < kl; j++) chars.push(genChar(comp, cs));
var key;
if (gl > 0) {
var groups = [];
for (var k = 0; k < chars.length; k += gl) groups.push(chars.slice(k, k + gl).join(''));
key = groups.join('-');
} else {
key = chars.join('');
}
lines.push(key);
}
document.getElementById('padOutput').textContent = lines.join('\n');
}
function loadPadAsCipherKey() {
var raw = document.getElementById('padOutput').textContent;
if (!raw || raw === 'Press "Generate pad" to create keys.') { alert('Generate a pad first.'); return; }
var letters = raw.replace(/[^a-zA-Z]/g, '').toUpperCase();
var seen = new Set();
var key = '';
for (var i = 0; i < letters.length && key.length < 26; i++) {
if (!seen.has(letters[i])) { seen.add(letters[i]); key += letters[i]; }
}
if (key.length < 26) {
alert('Pad does not contain enough unique letters (got ' + key.length + '/26). Try a larger pad.');
return;
}
cipherKey = key;
updateKeyDisplays();
switchTab('encode', document.querySelectorAll('.tab')[1]);
showToast('Key loaded from pad');
}
// ── cipher ────────────────────────────────────────────────────────────────
function encodeChar(ch, pos) {
var up = ch.toUpperCase(), idx = AZ.indexOf(up);
if (idx < 0) return ch;
var mapped = AZ.indexOf(cipherKey[idx]);
var shifted = (mapped + (pos % 26)) % 26;
var enc = AZ[shifted];
return ch === up ? enc : enc.toLowerCase();
}
function decodeChar(ch, pos) {
var up = ch.toUpperCase(), idx = AZ.indexOf(up);
if (idx < 0) return ch;
var unshifted = ((idx - (pos % 26)) + 26) % 26;
var dec = AZ[cipherKey.indexOf(AZ[unshifted])];
return ch === up ? dec : dec.toLowerCase();
}
function applyTransform(text, fn) {
var pos = 0;
return text.split('').map(function (c) {
var out = fn(c, pos);
if (AZ.indexOf(c.toUpperCase()) >= 0) pos++;
return out;
}).join('');
}
function doEncode() {
if (!cipherKey) { alert('No key loaded.'); return; }
document.getElementById('cipherOutput').textContent =
applyTransform(document.getElementById('plainInput').value, encodeChar);
showToast('Encoded');
}
function doDecode() {
if (!cipherKey) { alert('No key loaded.'); return; }
document.getElementById('plainOutput').textContent =
applyTransform(document.getElementById('cipherInput').value, decodeChar);
showToast('Decoded');
}
// ── save / copy helpers ───────────────────────────────────────────────────
function getTextFrom(id) {
return document.getElementById(id).textContent || '';
}
function flashBtn(btn, label) {
var orig = btn.textContent;
btn.textContent = label;
btn.classList.add('success');
setTimeout(function () {
btn.textContent = orig;
btn.classList.remove('success');
}, 1400);
}
function copyTextFrom(id, btn) {
var t = getTextFrom(id);
if (!t) { showToast('Nothing to copy'); return; }
navigator.clipboard.writeText(t).then(function () {
if (btn) flashBtn(btn, 'Copied!');
showToast('Copied to clipboard');
}).catch(function () {
var ta = document.createElement('textarea');
ta.value = t;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
if (btn) flashBtn(btn, 'Copied!');
showToast('Copied to clipboard');
});
}
function saveText(id, filename) {
var t = getTextFrom(id);
if (!t) { showToast('Nothing to save'); return; }
triggerDownload(t, filename);
showToast('Saved ' + filename);
}
function saveKeyFile() {
if (!cipherKey) { showToast('No key loaded'); return; }
var content = 'OTP Substitution Cipher Key\n';
content += 'Generated: ' + new Date().toISOString() + '\n\n';
content += 'Plain: ' + AZ + '\n';
content += 'Cipher: ' + cipherKey + '\n';
triggerDownload(content, 'cipher-key.txt');
showToast('Saved cipher-key.txt');
}
function bundleContent() {
var ts = new Date().toISOString();
var pad = getTextFrom('padOutput');
var cipher = getTextFrom('cipherOutput');
var plain = getTextFrom('plainOutput');
var key = cipherKey ? ('Plain: ' + AZ + '\nCipher: ' + cipherKey) : 'No key loaded';
var out = '';
out += '========================================\n';
out += ' OTP Substitution Cipher — Session Bundle\n';
out += ' ' + ts + '\n';
out += '========================================\n\n';
out += '--- CIPHER KEY ---\n' + key + '\n\n';
if (pad && pad !== 'Press "Generate pad" to create keys.') {
out += '--- ONE-TIME PAD ---\n' + pad + '\n\n';
}
if (cipher) {
out += '--- CIPHERTEXT ---\n' + cipher + '\n\n';
}
if (plain) {
out += '--- PLAINTEXT ---\n' + plain + '\n\n';
}
return out;
}
function saveBundle() {
var content = bundleContent();
triggerDownload(content, 'otp-cipher-bundle.txt');
showToast('Saved otp-cipher-bundle.txt');
}
function copyBundle(btn) {
var content = bundleContent();
navigator.clipboard.writeText(content).then(function () {
if (btn) flashBtn(btn, 'Copied!');
showToast('Bundle copied to clipboard');
}).catch(function () {
var ta = document.createElement('textarea');
ta.value = content;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
if (btn) flashBtn(btn, 'Copied!');
showToast('Bundle copied to clipboard');
});
}
function triggerDownload(text, filename) {
var blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(function () {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
}
// ── navigation ────────────────────────────────────────────────────────────
function switchTab(name, el) {
document.querySelectorAll('.tab').forEach(function (t) { t.classList.remove('active'); });
document.querySelectorAll('.panel').forEach(function (p) { p.classList.remove('active'); });
if (el) el.classList.add('active');
document.getElementById('panel-' + name).classList.add('active');
}
function toggleSeed() {
var m = document.getElementById('seedMode').value;
document.getElementById('userSeedRow').style.display = m === 'user' ? 'flex' : 'none';
}
genRandKey();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment