Files
nostr_quantum_preparation/www/index.html
T

643 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; connect-src wss: https:; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none';" />
<meta name="referrer" content="no-referrer" />
<title>Nostr Quantum Preparation</title>
<link rel="stylesheet" href="./css/client.css" />
<link rel="shortcut icon" type="image/x-icon" href="data:image/x-icon;," />
<style>
#divHeaderText {
font-size: calc(max(var(--header-height), var(--header-min-height)) * 0.5) !important;
}
#divHeaderFlexRight {
padding-right: 20px;
}
#divBody {
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: center !important;
justify-content: flex-start !important;
align-content: flex-start !important;
}
.pq-container {
max-width: 700px;
width: 100%;
padding: 10px;
}
/* CHECKLIST */
.pq-checklist {
background: var(--secondary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 20px 25px;
margin-bottom: 20px;
}
.pq-checklist-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: var(--primary-color);
}
.pq-checklist-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
font-size: 14px;
color: var(--muted-color);
border-bottom: 1px solid var(--border-color);
}
.pq-checklist-item:last-child { border-bottom: none; }
.pq-checklist-item.pq-done {
color: var(--primary-color);
}
.pq-checklist-box {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: 2px solid var(--muted-color);
border-radius: 4px;
flex-shrink: 0;
font-size: 12px;
font-weight: bold;
}
.pq-checklist-item.pq-done .pq-checklist-box {
background: var(--accent-color);
border-color: var(--accent-color);
color: var(--secondary-color);
}
.pq-checklist-item.pq-active {
color: var(--primary-color);
font-weight: bold;
}
.pq-checklist-item.pq-active .pq-checklist-box {
border-color: var(--accent-color);
}
/* Braille spinner shown in the checklist box while a step is working
(e.g. step 6 waiting for Bitcoin confirmation). The frames are
advanced by JS (setStepSpinner); this class just restyles the box. */
.pq-checklist-box.pq-spinner {
border: none;
color: var(--accent-color);
font-size: 18px;
line-height: 20px;
}
.pq-checklist-link {
color: var(--accent-color);
text-decoration: underline;
}
/* CARDS */
.pq-card {
background: var(--secondary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 25px;
margin-bottom: 20px;
}
.pq-card-title {
font-size: 18px;
font-weight: bold;
color: var(--primary-color);
margin-bottom: 15px;
}
.pq-info-text {
font-size: 14px;
color: var(--primary-color);
line-height: 1.6;
margin-bottom: 15px;
}
.pq-info-text strong { color: var(--primary-color); }
.pq-warning {
background: var(--primary-color);
color: var(--secondary-color);
border-radius: var(--border-radius);
padding: 12px 15px;
margin: 15px 0;
font-size: 13px;
line-height: 1.5;
}
.pq-warning strong { color: var(--accent-color); }
.pq-seed-display {
background: var(--secondary-color);
color: var(--primary-color);
border: var(--border);
border-radius: var(--border-radius);
padding: 20px;
margin: 15px 0;
font-size: 16px;
line-height: 1.8;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 20px;
word-break: break-word;
}
.pq-seed-word { display: flex; align-items: center; gap: 8px; }
.pq-seed-number { color: var(--muted-color); font-size: 12px; min-width: 20px; }
.pq-button {
background: var(--primary-color);
color: var(--secondary-color);
border: var(--border-width) solid var(--primary-color);
border-radius: var(--border-radius);
padding: 12px 24px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: opacity 0.2s;
width: 100%;
}
.pq-button:hover { opacity: 0.7; }
.pq-button:disabled { opacity: 0.3; cursor: not-allowed; }
.pq-button-secondary {
background: transparent;
color: var(--primary-color);
border: var(--border-width) solid var(--primary-color);
}
.pq-button-row { display: flex; gap: 10px; margin: 10px 0; }
.pq-button-row .pq-button { flex: 1; }
.pq-checkbox-row {
display: flex; align-items: center; gap: 8px;
margin: 15px 0; font-size: 14px; cursor: pointer; color: var(--primary-color);
}
.pq-checkbox-row input {
width: 18px; height: 18px; cursor: pointer;
appearance: none; -webkit-appearance: none;
border: var(--border-width) solid var(--primary-color);
background: transparent;
display: inline-grid;
place-content: center;
box-sizing: border-box;
}
.pq-checkbox-row input::before {
content: "";
width: 100%; height: 100%;
transform: scale(0);
transition: transform 0.1s ease-in-out;
background: #d00;
border-radius: 1px;
}
.pq-checkbox-row input:checked::before {
transform: scale(1);
}
.pq-checkbox-row input:checked {
border-color: #d00;
background: #d00;
}
.pq-seed-toggle {
flex: 1;
opacity: 0.5;
}
.pq-seed-toggle-active {
opacity: 1;
border-color: var(--accent-color);
color: var(--accent-color);
}
.pq-progress-bar {
background: var(--muted-color); border-radius: 4px; height: 8px; margin: 15px 0; overflow: hidden;
}
.pq-progress-fill { background: var(--accent-color); height: 100%; width: 0%; transition: width 0.3s; }
.pq-key-list { display: flex; flex-direction: column; gap: 10px; margin: 15px 0; }
.pq-key-item {
display: flex; align-items: flex-start; gap: 12px; padding: 12px;
background: var(--primary-color); color: var(--secondary-color); border-radius: var(--border-radius);
}
.pq-key-icon { font-size: 20px; flex-shrink: 0; }
.pq-key-details { flex: 1; min-width: 0; }
.pq-key-name { font-size: 14px; font-weight: bold; margin-bottom: 2px; }
.pq-key-meta { font-size: 12px; color: var(--primary-color); margin-bottom: 4px; }
.pq-key-pubkey { font-size: 11px; color: var(--primary-color); word-break: break-all; }
.pq-status {
padding: 10px 15px; border-radius: var(--border-radius); margin: 10px 0; font-size: 14px; text-align: left;
}
.pq-status-info { background: var(--secondary-color); border: var(--border); }
.pq-status-success { background: var(--accent-color); color: var(--secondary-color); }
.pq-status-error { background: var(--accent-color); color: var(--secondary-color); }
.pq-event-preview {
background: var(--secondary-color); color: var(--primary-color); border: var(--border);
border-radius: var(--border-radius);
padding: 15px; font-size: 11px; white-space: pre-wrap; word-break: break-all;
max-height: 400px; overflow-y: auto; text-align: left;
}
.pq-npub-display {
font-size: 12px; word-break: break-all;
background: var(--primary-color); color: var(--secondary-color);
padding: 8px 12px; border-radius: var(--border-radius); margin: 10px 0;
}
.pq-relay-input {
width: 100%; background: var(--secondary-color); border: var(--border);
border-radius: var(--border-radius); padding: 10px 14px; font-size: 14px;
color: var(--primary-color); margin: 8px 0;
}
.pq-relay-input:focus { outline: none; border-color: var(--accent-color); }
.pq-hidden { display: none !important; }
ol { margin: 10px 0 10px 20px; line-height: 1.8; }
</style>
</head>
<body>
<div id="divHeader">
<div id="divHeaderFlexLeft"></div>
<div id="divHeaderFlexCenter">
<div class="divHeaderText" id="divHeaderText">Nostr Quantum Preparation</div>
</div>
<div id="divHeaderFlexRight">
<button class="pq-button pq-button-secondary pq-hidden" id="pqSignOutBtn" style="width: auto; padding: 6px 16px; font-size: 13px;">Sign Out</button>
</div>
</div>
<div id="divBody">
<div class="pq-container">
<!-- ================================================================
PERSISTENT CHECKLIST (always visible at top)
================================================================ -->
<div class="pq-checklist">
<div class="pq-checklist-title">Preparation Steps</div>
<div class="pq-checklist-item" id="step-1">
<span class="pq-checklist-box"></span>
<span>1. Sign in with your Nostr identity</span>
</div>
<div class="pq-checklist-item" id="step-2">
<span class="pq-checklist-box"></span>
<span>2. Generate a seed phrase</span>
</div>
<div class="pq-checklist-item" id="step-3">
<span class="pq-checklist-box"></span>
<span>3. Derive post-quantum keys from seed</span>
</div>
<div class="pq-checklist-item" id="step-4">
<span class="pq-checklist-box"></span>
<span>4. Sign the key-link event</span>
</div>
<div class="pq-checklist-item" id="step-5">
<span class="pq-checklist-box"></span>
<span>5. Publish events to relays</span>
</div>
<div class="pq-checklist-item" id="step-6">
<span class="pq-checklist-box"></span>
<span>6. Timestamp the event with OpenTimestamps</span>
</div>
<div class="pq-checklist-item" id="step-7">
<span class="pq-checklist-box"></span>
<span>7. <a href="./verify.html" class="pq-checklist-link">Verify the event signatures</a></span>
</div>
</div>
<!-- ================================================================
STEP CONTENT (shown/hidden below checklist)
================================================================ -->
<!-- STEP 0: NOT AUTHENTICATED -->
<div id="pqNotAuthed" class="pq-card">
<div class="pq-card-title">Nostr Quantum Preparation</div>
<div class="pq-info-text" style="font-size: 16px; color: var(--primary-color);">
Link your Nostr identity to post-quantum keys — before quantum computers arrive
</div>
<div class="pq-info-text">
Quantum computers will eventually break the cryptography Nostr uses today. When that
happens, anyone who recorded your public key could forge your signature, impersonate you,
and read your encrypted messages. This tool lets you prepare now: it generates a new seed
phrase, creates post-quantum keys from it, and publishes a signed statement linking those
keys to your current Nostr identity. The statement is then anchored to a Bitcoin block, so
no one can backdate a fake one later.
</div>
<div class="pq-info-text">
Your current nsec, npub, your followers, and your social graph stay exactly as they are.
This program doesn't change anything about your current nostr setup.
</div>
<div class="pq-info-text">
Upon completing this process, you should have saved 2 things:
<ol style="margin: 8px 0 0 0; padding-left: 22px;">
<li>A new seed phrase. This should be kept secret and private.</li>
<li>A proof archive. This information is published to relays, but keeping a copy yourself is useful in case the relays don't keep the event.</li>
</ol>
In case quantum computers break nostr, you can use your seed phrase to create a new nostr
identity, and prove that your old nostr identity created this new one.
</div>
<div class="pq-info-text" style="font-size: 13px; color: var(--accent-color);">
<strong>Warning:</strong> Don't enter a valuable existing seed phrase. This tool doesn't make
your Nostr identity post-quantum secure on its own — it creates a public link your future self
can prove is yours. Run through it once with a test identity before you use it on your real one.
</div>
<div class="pq-info-text" id="noSignerWarning" style="color: var(--accent-color); display: none;">
No Nostr signer detected. Install a NIP-07 browser extension (like nos2x or Alby) and reload.
</div>
<div id="pqSignInError"></div>
<button class="pq-button" id="pqSignInBtn">Sign In to Continue</button>
</div>
<!-- STEP 1: AUTHENTICATED - INTRO -->
<div id="pqAuthed" class="pq-card pq-hidden">
<div class="pq-card-title">Post-Quantum Preparation</div>
<div class="pq-info-text">
<strong>Connected as:</strong>
<div class="pq-npub-display" id="pqNpub"></div>
</div>
<div class="pq-info-text">
<strong>Current Bitcoin block height:</strong>
<div class="pq-npub-display" id="pqBlockHeight">Loading...</div>
</div>
<div class="pq-info-text">
This tool will:
<ol>
<li>Generate a new <strong>seed phrase</strong> (your recovery backup)</li>
<li>Derive <strong>post-quantum keys</strong> from that seed via BIP32 paths (ML-DSA-44, ML-DSA-65, SLH-DSA-128s, Falcon-512, ML-KEM-768)</li>
<li>Sign a <strong>key-link event</strong> linking your identity to the post-quantum keys</li>
<li>Publish the event to relays</li>
</ol>
</div>
<button class="pq-button" id="pqStartBtn">Start Preparation</button>
</div>
<!-- STEP 2: SEED PHRASE GENERATION -->
<div id="pqSeedStep" class="pq-card pq-hidden">
<div class="pq-card-title">Your Seed Phrase</div>
<div class="pq-info-text">
Your seed phrase is your <strong>recovery root</strong>. All post-quantum keys
are derived from it. It is processed entirely in your browser and never sent to any server.
The code is open source and auditable.
</div>
<div class="pq-info-text" style="font-size: 13px; color: var(--accent-color);">
<strong>Back this up offline.</strong> Anyone with this phrase can derive your PQ private keys.
24 words (256 bits) is recommended; 12 words (128 bits) is offered for testing only and is
weaker under a quantum brute-force model.
</div>
<!-- Mode toggle + copy row (always visible) -->
<div class="pq-button-row" style="margin-bottom: 15px;">
<button class="pq-button pq-button-secondary pq-seed-toggle pq-seed-toggle-active" id="pqSeedToggleGenerate">Generate New Seed</button>
<button class="pq-button pq-button-secondary pq-seed-toggle" id="pqSeedToggleOwn">I Have a Seed Phrase</button>
<button class="pq-button pq-button-secondary" id="pqCopySeedBtn">Copy</button>
</div>
<!-- Generate mode -->
<div id="pqSeedGeneratePanel">
<!-- Word-count selector -->
<div class="pq-info-text" style="margin-bottom: 10px; font-size: 13px;">
<label style="margin-right: 12px;"><input type="radio" name="pqWordCount" value="256" id="pqWordCount24" checked> 24 words (recommended)</label>
<label><input type="radio" name="pqWordCount" value="128" id="pqWordCount12"> 12 words (testing only)</label>
</div>
<div class="pq-seed-display" id="pqSeedDisplay"></div>
<div class="pq-info-text" style="margin-top: 12px; font-size: 13px; color: var(--accent-color);">
<strong>Verify out-of-band after linking:</strong> This seed phrase is your backup — but
this browser cannot cryptographically prove the published PQ keys came from it. After completing
the key link, take your seed phrase to a <em>different</em> device or browser, derive the keys
independently, and confirm the public keys match the published event. This is the only way to
catch a compromised browser that might have substituted attacker keys.
</div>
<div class="pq-info-text" style="margin-top: 15px; font-size: 13px;">
<strong>Optional:</strong> Add extra entropy by moving your mouse and typing in the box below.
This mixes your input with the browser's random number generator, making the seed unpredictable
even if the browser's RNG is compromised. You can skip this — a seed has already been generated for you above.
</div>
<div id="pqEntropyBox" style="background: var(--secondary-color); color: var(--primary-color); border: var(--border); border-radius: var(--border-radius); padding: 15px; font-size: 13px; min-height: 60px; margin: 10px 0; cursor: text; text-align: left;" tabindex="0">
<span id="pqEntropyHint">Optional: click here and type random characters, move your mouse to add entropy...</span>
<div style="margin-top: 8px; height: 6px; background: var(--muted-color); border-radius: 3px; overflow: hidden;">
<div id="pqEntropyBar" style="height: 100%; width: 0%; background: var(--accent-color); transition: width 0.2s;"></div>
</div>
</div>
<div class="pq-button-row">
<button class="pq-button pq-button-secondary pq-hidden" id="pqRegenerateWithEntropyBtn" disabled>Generate with Extra Entropy</button>
</div>
<label class="pq-checkbox-row">
<input type="checkbox" id="pqSeedConfirmed" />
I have written down my seed phrase
</label>
<button class="pq-button" id="pqSeedContinueBtn" disabled>Continue</button>
</div>
<!-- Bring your own mode -->
<div id="pqSeedOwnPanel" style="display: none;">
<div class="pq-info-text">
Paste your 12 or 24-word BIP39 seed phrase below. It will be validated and used to
derive your post-quantum keys. Make sure you generated it offline or with a trusted tool.
</div>
<textarea class="pq-textarea" id="pqSeedInput" style="min-height: 80px; width: 100%; background: var(--secondary-color); border: var(--border); border-radius: var(--border-radius); padding: 12px; font-size: 14px; color: var(--primary-color); font-family: monospace; margin: 10px 0;" placeholder="abandon ability able about above absent absorb abstract absurd abuse access accident..."></textarea>
<div id="pqSeedValidation" style="font-size: 13px; margin: 5px 0;"></div>
<button class="pq-button" id="pqSeedOwnContinueBtn" disabled>Continue</button>
</div>
</div>
<!-- STEP 3: DERIVING PQ KEYS -->
<div id="pqDeriveStep" class="pq-card pq-hidden">
<div class="pq-card-title">Deriving Post-Quantum Keys</div>
<div class="pq-info-text">
Deriving post-quantum keys from your seed phrase via BIP32 paths.
All keys are generated deterministically — the same seed will always produce the same keys.
</div>
<div class="pq-progress-bar">
<div class="pq-progress-fill" id="pqDeriveProgress"></div>
</div>
<div class="pq-checklist" id="pqKeyList" style="margin-bottom: 15px;">
<div class="pq-checklist-item" id="pqKeyMlDsa44">
<span class="pq-checklist-box"></span>
<span>ML-DSA-44 (Dilithium) — FIPS 204 · Cat 2 · 1312-byte pubkey · m/44'/1237'/0'/0/1
<div class="pq-key-pubkey" id="pqKeyMlDsa44Pub" style="margin-top: 2px;"></div>
</span>
</div>
<div class="pq-checklist-item" id="pqKeyMlDsa65">
<span class="pq-checklist-box"></span>
<span>ML-DSA-65 (Dilithium) — FIPS 204 · Cat 3 · 1952-byte pubkey · m/44'/1237'/0'/0/2
<div class="pq-key-pubkey" id="pqKeyMlDsa65Pub" style="margin-top: 2px;"></div>
</span>
</div>
<div class="pq-checklist-item" id="pqKeySlhDsa">
<span class="pq-checklist-box"></span>
<span>SLH-DSA-128s (SPHINCS+) — FIPS 205 · Cat 1 · 32-byte pubkey · m/44'/1237'/0'/0/3+4
<div class="pq-key-pubkey" id="pqKeySlhDsaPub" style="margin-top: 2px;"></div>
</span>
</div>
<div class="pq-checklist-item" id="pqKeyFalcon">
<span class="pq-checklist-box"></span>
<span>Falcon-512 — FIPS 206 (draft) · Cat 1 · 897-byte pubkey · m/44'/1237'/0'/0/5+6
<div class="pq-key-pubkey" id="pqKeyFalconPub" style="margin-top: 2px;"></div>
<div style="font-size: 11px; color: var(--accent-color); margin-top: 4px;">⚠ Draft standard — may need re-issuing if FIPS 206 changes</div>
</span>
</div>
<div class="pq-checklist-item" id="pqKeyMlKem">
<span class="pq-checklist-box"></span>
<span>ML-KEM-768 (Kyber) — FIPS 203 · Cat 3 · 1184-byte pubkey · m/44'/1237'/0'/0/7+8
<div class="pq-key-pubkey" id="pqKeyMlKemPub" style="margin-top: 2px;"></div>
</span>
</div>
</div>
<div id="pqDeriveStatus"></div>
<button class="pq-button pq-hidden" id="pqDeriveContinueBtn">Continue to Signing</button>
</div>
<!-- STEP 4: SIGNING -->
<div id="pqSignStep" class="pq-card pq-hidden">
<div class="pq-card-title">Sign Key-Link Event</div>
<div class="pq-info-text">
We'll now create two events: a public announcement (kind 1, visible in Nostr feeds) and
a proof carrier (kind 9999, which holds the Bitcoin timestamp). Each post-quantum key signs
the announcement text, and your signer signs both events with your current Nostr key. The
hash of the signed announcement is then sent to OpenTimestamps to be anchored in Bitcoin.
</div>
<div id="pqSignStatus"></div>
<button class="pq-button" id="pqSignBtn">Sign Event</button>
<div id="pqEventPreviewWrap" class="pq-hidden" style="margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;">Event preview:</div>
<div class="pq-event-preview" id="pqEventPreview"></div>
</div>
</div>
<!-- STEP 5: REVIEW & PUBLISH -->
<div id="pqPublishStep" class="pq-card pq-hidden">
<div class="pq-card-title">Review & Publish Events</div>
<div class="pq-info-text">
Two events have been signed: a kind 1 announcement (public, visible in Nostr feeds) and
a kind 9999 proof carrier (carries the OpenTimestamps proof). Review them below, then publish
both to relays when ready.
</div>
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>Kind 1 announcement event:</strong></div>
<div id="pqKind1IdDisplay" style="margin: 5px 0; font-size: 11px; color: var(--primary-color); word-break: break-all;"></div>
<div class="pq-event-preview" id="pqKind1Preview" style="max-height: 200px;"></div>
<div class="pq-info-text" style="margin-top: 15px; margin-bottom: 5px;"><strong>Kind 9999 proof carrier event:</strong></div>
<div id="pqEventIdDisplay" style="margin: 5px 0; font-size: 11px; color: var(--primary-color); word-break: break-all;"></div>
<div class="pq-event-preview" id="pqSuccessEventPreview"></div>
<div class="pq-info-text" id="pqEventSizeWarning" style="display: none; font-size: 13px; margin-top: 8px;"></div>
<div class="pq-button-row" style="margin-top: 10px;">
<button class="pq-button pq-button-secondary" id="pqCopyProofCarrierBtn">Copy Proof Carrier Event</button>
</div>
<div style="margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;"><strong>Relays to publish to:</strong></div>
<div id="pqRelayList" style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px;"></div>
<div class="pq-button-row">
<input type="text" class="pq-relay-input" id="pqRelayAddInput" style="flex: 1; margin: 0;" placeholder="wss://relay.example.com" />
<button class="pq-button pq-button-secondary" id="pqRelayAddBtn" style="flex: 0 0 auto; width: auto; padding: 12px 20px;">Add Relay</button>
</div>
</div>
<div id="pqPublishStatus"></div>
<div class="pq-button-row" style="margin-top: 15px;">
<button class="pq-button" id="pqPublishBtn">Publish Both Events to Relays</button>
<button class="pq-button pq-button-secondary" id="pqDownloadArchiveBtn" title="Download a self-contained proof archive (kind 1 event + proof carrier + OTS proof) so you can verify it offline even if relays delete it.">Download Proof Archive</button>
</div>
<div class="pq-button-row">
<button class="pq-button pq-button-secondary pq-hidden" id="pqOtsContinueBtn">Continue to OpenTimestamps</button>
</div>
</div>
<!-- STEP 6: OPENTIMESTAMPS -->
<div id="pqOtsStep" class="pq-card pq-hidden">
<div class="pq-card-title">OpenTimestamps Attestation - Leave This Page Open</div>
<div class="pq-info-text">
Your event's hash was already sent to OpenTimestamps in the previous step, and a
<strong>pending</strong> timestamp proof is embedded in the event you just published.
OpenTimestamps anchors that hash into the Bitcoin blockchain, proving your event existed at
this moment — so a future quantum attacker can't fake an older event and claim it came first.
</div>
<div class="pq-info-text">
Bitcoin confirmation typically takes <strong>10-30 minutes</strong>. You can
<strong style="text-decoration: underline;">leave this page open</strong>
to watch the progress below — the polling log updates every minute. Once the Bitcoin attestation is
confirmed, this page will automatically sign and publish a <strong>new</strong> kind 9999 event carrying
the upgraded (confirmed) proof, referencing the original via an <code>upgrade_of</code> tag. The original
event remains on relays permanently. You're free to close the page; the workflow resumes on your next visit.
</div>
<div id="pqOtsStatus"></div>
<!-- Polling log window -->
<div class="pq-info-text" style="margin-top: 10px; margin-bottom: 5px;">Polling log:</div>
<div id="pqOtsLog" style="background: var(--secondary-color); color: var(--primary-color); border: var(--border); border-radius: var(--border-radius); padding: 10px; font-size: 11px; font-family: monospace; height: 120px; overflow-y: auto; text-align: left; margin-bottom: 15px;"></div>
<div class="pq-checklist" style="margin-bottom: 15px;">
<div class="pq-checklist-item pq-done" id="pqOtsSubmit">
<span class="pq-checklist-box"></span>
<span>Submit hash to OpenTimestamps — pending .ots proof created</span>
</div>
<div class="pq-checklist-item pq-done" id="pqOtsPendingPublish">
<span class="pq-checklist-box"></span>
<span>Pending proof embedded in kind 9999 event</span>
</div>
<div class="pq-checklist-item pq-active" id="pqOtsConfirm">
<span class="pq-checklist-box"></span>
<span>Wait for Bitcoin confirmation
<div class="pq-key-meta" id="pqOtsConfirmDetail" style="margin-top: 2px;">Polling every 60 seconds...</div>
</span>
</div>
<div class="pq-checklist-item" id="pqOtsConfirmedPublish">
<span class="pq-checklist-box"></span>
<span>Publish new kind 9999 with confirmed proof — automatic on confirmation</span>
</div>
</div>
<div id="pqOtsProofWrap" class="pq-hidden" style="margin-top: 15px;">
<div class="pq-info-text" style="margin-bottom: 5px;">OTS proof file (base64):</div>
<div class="pq-event-preview" id="pqOtsProof" style="max-height: 150px;"></div>
<div class="pq-button-row" style="margin-top: 10px;">
<button class="pq-button pq-button-secondary" id="pqOtsDownloadBtn">Download .ots file</button>
</div>
</div>
</div>
</div>
</div>
<div id="divFooter">
<div id="divFooterLeft" class="divFooterBox"></div>
<div id="divFooterCenter" class="divFooterBox"></div>
<div id="divFooterRight" class="divFooterBox"></div>
</div>
<!-- SCRIPTS -->
<script src="/nostr-login-lite/nostr.bundle.js" integrity="sha384-LNnPDD++DaWxljIhMLmfaoEoKB0B1HmACC6gNGLG+Qnmosprf/AX7u84pVY8xMpM" crossorigin="anonymous"></script>
<script src="/nostr-login-lite/nostr-lite.js" integrity="sha384-IQwa65eDC5trGjKn4cuEazmFiHTHD65gIqsjpzDtouc+j0MlyI927SZgHMWSi2aC" crossorigin="anonymous"></script>
<!-- Version display: fetch version.json and update title + header (G56-08: extracted to external file, self-initializing) -->
<script src="./js/version-display.js"></script>
<!-- Main application module (G56-08: extracted to external file for CSP compliance) -->
<script type="module" src="./js/index-app.mjs"></script>
</body>
</html>