sirius/admin/index.html
Local Dev 105a245b2f site-sirius-x: only paint .X acid; keep Sirius white
Brand button was wrapping the whole "Sirius.X" label in <span class="g">,
so the entire name rendered acid green. Consistency across the ecosystem
is Sirius (ink) + .X (acid), like Theseus.X and Hephaestus.X. Split the
span across every page (index, tld, portal, admin, brand, docs, market,
storage, theseus/) so only the .X tail carries the acid tint.
2026-09-22 01:28:25 +02:00

465 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin — Sirius.X</title>
<meta name="description" content="Sirius.x operator admin — mint TLDs, manage the registry, verify chain health. Operator-key gated.">
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml">
<link rel="mask-icon" href="../assets/favicon.svg" color="#d6ff3d">
<meta name="robots" content="noindex,nofollow">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500;9..144,600;9..144,700&family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Ubuntu:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#050810; --panel:#141a24; --panel2:#0a0f1c; --line:rgba(255,255,255,.06);
--ink:#f1f4fa; --mut:#b8c2d4; --dim:#6a7488; --acid:#d6ff3d;
--ok:#4fd1a5; --taken:#f6768a; --warn:#ffc75f;
}
*{box-sizing:border-box}
body{margin:0;background:radial-gradient(ellipse 1200px 600px at 50% -5%,#131a2a 0%,transparent 60%),radial-gradient(ellipse 900px 500px at 100% 100%,#0a0f1c 0%,transparent 55%),radial-gradient(ellipse 900px 500px at 0% 100%,#0c1424 0%,transparent 55%),var(--bg);
color:var(--ink);font:16px/1.65 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-height:100vh}
a{color:var(--acid)}
.wrap{max-width:900px;margin:0 auto;padding:0 1.2rem}
.topnav{position:sticky;top:0;z-index:20;display:flex;gap:2px;align-items:center;flex-wrap:wrap;
padding:10px 1.2rem;background:rgba(11,14,20,.82);backdrop-filter:blur(10px)}
.topnav .brand{font-weight:600;color:var(--ink);text-decoration:none;margin-right:14px;font-size:15px;letter-spacing:.2px}
.topnav .brand .g{color:var(--acid)}
.topnav a{color:var(--mut);text-decoration:none;padding:6px 12px;border-radius:8px;font-size:14px}
.topnav a:hover{color:var(--ink);background:var(--panel)}
.topnav a.here{color:var(--acid);background:rgba(214,255,61,.08)}
.topnav a.portal{margin-left:auto;background:rgba(214,255,61,.10);color:var(--acid);font-weight:500}
.topnav a.portal:hover{background:rgba(214,255,61,.18)}
/* Adaptive side nav — same shape as index.html and Theseus.X */
:root{--sn-w:200px}
.sidenav{position:fixed;top:56px;left:0;bottom:0;width:var(--sn-w);z-index:15;
background:transparent;padding:32px 12px 22px 18px;
overflow-y:auto;pointer-events:none;transition:width .15s ease}
.sidenav a{display:flex;align-items:center;gap:14px;color:var(--mut);text-decoration:none;
padding:9px 12px;border-radius:10px;
font-size:14.5px;font-weight:500;opacity:.35;pointer-events:auto;
transition:opacity .12s,color .12s,background .12s,backdrop-filter .12s}
.sidenav a .ic{flex:none;width:22px;font-size:18px;line-height:1;text-align:center;
display:none;justify-content:center;align-items:center}
.sidenav a .lb{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sidenav a:hover{opacity:1;color:var(--ink);background:rgba(11,14,20,.72);backdrop-filter:blur(8px)}
.sidenav a.active{opacity:.85;color:var(--acid)}
.sidenav a.active:hover{opacity:1}
section{scroll-margin-top:70px}
@media (max-width:1180px){
:root{--sn-w:64px}
.sidenav{padding:22px 6px}
.sidenav a{justify-content:center;padding:12px 4px;gap:0}
.sidenav a .ic{display:inline-flex}
.sidenav a .lb{display:none}
}
@media (max-width:720px){
:root{--sn-w:0}
body{padding-bottom:64px}
.sidenav{top:auto;bottom:0;left:0;right:0;width:100%;height:auto;
padding:4px;flex-direction:row;background:rgba(11,14,20,.52);
display:flex;justify-content:space-around;overflow-x:auto;
/* Hide horizontal scrollbar chrome; icons still swipe/scroll when they overflow. */
scrollbar-width:none;-ms-overflow-style:none}
.sidenav::-webkit-scrollbar{display:none;width:0;height:0}
.sidenav a{flex:1;min-width:44px;padding:8px 4px;justify-content:center;gap:0;opacity:.5;font-size:15px}
.sidenav a .ic{display:inline-flex}
.sidenav a .lb{display:none}
}
header.hero{text-align:center;padding:2.8rem 1.2rem 1rem}
.mark{font-size:40px;line-height:1}
h1{font-size:clamp(1.5rem,4vw,2rem);margin:.3rem 0 .3rem}
h1 .g{color:var(--acid)}
.tag{color:var(--mut);font-size:.98rem;max-width:600px;margin:0 auto}
h2{font-size:1.2rem;margin:0 0 .35rem}
h3{font-size:.98rem;margin:0 0 .3rem;font-family:ui-monospace,monospace}
section{padding:1.8rem 0;border-top:1px solid var(--line);margin-top:1.6rem}
section:first-of-type{border-top:none;margin-top:.8rem}
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:16px 18px;margin-top:.8rem}
label{display:block;font-size:11.5px;color:var(--mut);margin:12px 0 4px;text-transform:uppercase;letter-spacing:.4px}
.field-hint{color:var(--dim);font-size:11px;margin-left:8px;text-transform:none;letter-spacing:0}
input[type=text],textarea{
width:100%;padding:9px 12px;border-radius:8px;border:1px solid #ffffff22;background:#0e131b;
color:var(--ink);font-size:13px;font-family:ui-monospace,monospace;outline:none
}
textarea{min-height:68px;resize:vertical}
input:focus,textarea:focus{border-color:#4b7bec}
.btn{display:inline-block;text-decoration:none;padding:9px 16px;border-radius:9px;background:#4b7bec;color:#fff;font-size:13px;border:none;cursor:pointer;font-family:inherit}
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink)}
.btn.acid{background:var(--acid);color:#0b0e14;font-weight:600}
.btn.small{padding:5px 10px;font-size:12px}
.btn:hover{filter:brightness(1.12)}
.btn:disabled{opacity:.45;cursor:not-allowed}
.row{display:flex;gap:9px;flex-wrap:wrap;margin-top:12px;align-items:center}
code{background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:12.5px;word-break:break-all}
.status{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:10px 14px;font-size:13px;color:var(--mut);margin-top:10px}
.status.ok{border-left:3px solid var(--ok)}
.status.err{border-left:3px solid var(--taken);color:var(--taken)}
.status.warn{border-left:3px solid var(--warn);color:var(--warn)}
.hidden{display:none}
.badge{display:inline-block;font-size:10.5px;padding:2px 8px;border-radius:999px;font-weight:600;letter-spacing:.3px}
.b-op{background:rgba(214,255,61,.14);color:var(--acid)}
.b-other{background:rgba(139,152,169,.14);color:var(--mut)}
.b-mem{background:rgba(255,199,95,.14);color:var(--warn)}
table.tlds{width:100%;border-collapse:collapse;margin-top:1rem;font-size:13px}
table.tlds th,table.tlds td{padding:8px 10px;text-align:left;border-bottom:1px solid var(--line);vertical-align:top}
table.tlds th{color:var(--mut);font-weight:500;font-size:11.5px;text-transform:uppercase;letter-spacing:.4px}
table.tlds .tld{font-family:ui-monospace,monospace;color:var(--acid);font-size:13.5px;font-weight:600}
table.tlds .cat{font-family:ui-monospace,monospace;color:var(--mut);font-size:11.5px}
table.tlds .records{font-family:ui-monospace,monospace;color:var(--dim);font-size:11.5px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.steps-log{margin-top:10px;font-family:ui-monospace,monospace;font-size:12px;color:var(--mut);background:#0e131b;border:1px solid var(--line);border-radius:8px;padding:10px 12px;max-height:140px;overflow-y:auto;display:none}
.steps-log.on{display:block}
.steps-log div::before{content:" ";color:var(--acid)}
.msg{font-size:13px;margin-top:10px}
.msg.ok{color:var(--ok)} .msg.err{color:var(--taken)}
footer{border-top:1px solid var(--line);padding:1.8rem 0 3rem;color:var(--dim);font-size:12.5px;text-align:center;margin-top:2rem}
/* Deviant-family fonts + Fraunces/Ubuntu reader toggle. */
h1,h2,h3{font-family:'Fraunces',Georgia,serif;font-feature-settings:"ss01","ss02"}
code,.mono{font-family:'JetBrains Mono',ui-monospace,monospace}
body{font-family:'DM Sans','Inter',system-ui,sans-serif !important;font-feature-settings:"ss01","ss02"}
html[data-font-cmp="ubuntu"] body,html[data-font-cmp="ubuntu"] h1,html[data-font-cmp="ubuntu"] h2,html[data-font-cmp="ubuntu"] h3,html[data-font-cmp="ubuntu"] p,html[data-font-cmp="ubuntu"] a,html[data-font-cmp="ubuntu"] button,html[data-font-cmp="ubuntu"] span,html[data-font-cmp="ubuntu"] div,html[data-font-cmp="ubuntu"] label,html[data-font-cmp="ubuntu"] input,html[data-font-cmp="ubuntu"] li{font-family:'Ubuntu',system-ui,sans-serif !important}
html[data-font-cmp="ubuntu"] code,html[data-font-cmp="ubuntu"] .mono{font-family:'JetBrains Mono',ui-monospace,monospace !important}
.font-picker{display:inline-flex;align-items:center;gap:2px;padding:2px;margin:0 10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:999px;font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:.12em;text-transform:uppercase}
.font-picker button{background:transparent;border:none;color:var(--dim);padding:4px 10px;border-radius:999px;cursor:pointer;font-family:inherit;font-size:inherit;letter-spacing:inherit;text-transform:inherit}
.font-picker button:hover{color:var(--ink)}
.font-picker button.active{background:var(--acid);color:var(--bg);font-weight:600}
</style>
</head>
<body>
<nav class="topnav">
<a class="brand" href="../"><svg width="18" height="18" viewBox="0 0 64 64" style="vertical-align:-4px;margin-right:6px" aria-hidden="true"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M32 4 L36.4 27.6 L60 32 L36.4 36.4 L32 60 L27.6 36.4 L4 32 L27.6 27.6 Z" fill="#d6ff3d"/></svg>Sirius<span class="g">.X</span></a>
<a href="../tld.html">🌐 TLD</a>
<a href="../theseus/"><svg width="14" height="14" viewBox="0 0 64 64" style="vertical-align:-2px;margin-right:4px" aria-hidden="true"><rect width="64" height="64" rx="12" fill="#0b0e14"/><path d="M32 6 L38 32 L26 32 Z" fill="#e04e4e"/><path d="M38 32 L32 58 L26 32 Z" fill="#d6ff3d"/><circle cx="32" cy="32" r="3" fill="#0b0e14"/></svg>Theseus<span class="g">.X</span></a>
<a href="./" class="here">🛠 Admin</a>
<a href="../portal.html" class="portal">🔑 Sign in</a>
</nav>
<aside class="sidenav" aria-label="On this page">
<a href="#signin" title="Sign in" ><span class="ic" aria-hidden="true">🔑</span><span class="lb">Sign in</span></a>
<a href="#me" title="Signed in" ><span class="ic" aria-hidden="true">👤</span><span class="lb">Session</span></a>
<a href="#mint" title="Mint a new TLD" ><span class="ic" aria-hidden="true">✏️</span><span class="lb">Mint TLD</span></a>
<a href="#registry" title="TLD registry" ><span class="ic" aria-hidden="true">📋</span><span class="lb">Registry</span></a>
</aside>
<header class="hero">
<div class="mark">🛠</div>
<h1><span class="g">Admin</span> panel</h1>
<p class="tag">Operator-side tools: mint new TLDs, browse the registry, verify chain health.
Sign in with the wallet the ecosystem knows as the operator; unfamiliar wallets get
read-only access to the registry.</p>
</header>
<div class="wrap">
<section id="signin">
<h2>Sign in</h2>
<p class="status">Chipnet-only. Keys stay in this browser tab; a mint transaction is signed
locally and broadcast direct to the electrum. Operator gate is client-side — the covenant
that enforces it on-chain is a mainnet requirement (Decentralized.DNS/DESIGN-tld-registry.md §5.2).</p>
<div class="card">
<label for="seed">Recovery phrase</label>
<textarea id="seed" placeholder="word1 word2 … word12" spellcheck="false" autocomplete="off"></textarea>
<div class="row">
<button class="btn acid" id="signin-btn">Sign in →</button>
<span class="field-hint" id="signin-hint">seed stays in memory only</span>
</div>
<div id="signin-err" class="msg err hidden"></div>
</div>
</section>
<section id="me" class="hidden">
<h2>Signed in <span class="badge b-op" id="me-role" style="margin-left:8px">operator</span></h2>
<div class="card">
<label>Address</label>
<div><code id="me-addr"></code></div>
<label style="margin-top:12px">Token address (holds the TLD certificates)</label>
<div><code id="me-taddr"></code></div>
<div class="row">
<button class="btn ghost small" id="signout-btn">Sign out</button>
</div>
</div>
</section>
<section id="mint" class="hidden">
<h2>Mint a new TLD</h2>
<p class="tag" style="max-width:none">Adds a per-TLD certificate to the TLD beacon. The cert
lands in your token address. Cost is chain fees + beacon dust (~1,300 chipnet sats).</p>
<div class="card">
<label>TLD label <span class="field-hint">116 chars, [a-z0-9-]</span></label>
<input type="text" id="mint-tld" placeholder="e.g. shop, dao, wiki" maxlength="16">
<label>Records JSON <span class="field-hint">optional; leave empty for defaults</span></label>
<textarea id="mint-records" placeholder='e.g. {"policy":"open","fee_bps":500}'>{}</textarea>
<div class="row">
<button class="btn acid" id="mint-btn">Mint TLD →</button>
<span class="field-hint" id="mint-hint"></span>
</div>
<div class="steps-log" id="mint-log"></div>
<div class="msg" id="mint-msg"></div>
</div>
</section>
<section id="registry">
<h2>TLD registry</h2>
<p class="tag" style="max-width:none;font-size:.92rem">Every TLD registered on the beacon. Operators
(signed-in) can toggle a TLD's visibility on the public /api/tlds listing — hidden TLDs stop
appearing in the name-search UIs, but on-chain registrations under them keep resolving.</p>
<div class="card" id="op-token-card" style="margin-top:.8rem">
<label>Operator token <span class="field-hint">stored per browser tab, not sent anywhere except the /api/tld-visibility POST</span></label>
<input type="password" id="op-token" placeholder="paste BNS_OPERATOR_TOKEN">
<div class="row">
<button class="btn ghost small" id="op-token-save">Save token</button>
<span id="op-token-msg" class="dim"></span>
</div>
</div>
<div class="status" id="registry-status">Loading current registry…</div>
<div id="registry-table"></div>
</section>
</div>
<footer id="site-footer"></footer>
<script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js?v=20260920usd" } }
</script>
<script type="module">
// Cache-buster bump when the bundle changes (Chrome's ES-module map caches
// aggressively per plain URL). Portal.html has the same pattern.
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260908split";
const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
// Operator gate: address that owns the chipnet TLD beacon + seeded TLDs.
// Client-side only — the covenant that enforces this on-chain lands with the
// mainnet TLD-registry work. For chipnet this is honour-system; a curious
// visitor can bypass by pasting any seed, but non-operator UPDs/TREGs won't
// carry the countersignature future clients will check.
const OPERATOR_ADDRESS = "bchtest:qzew3dqcsj0guwe5y3uxf9q2xs5ajgy9lca4z093cu";
let wallet = null;
let isOperator = false;
// ---------- sign in ----------
$("signin-btn").addEventListener("click", async () => {
const btn = $("signin-btn");
const errBox = $("signin-err");
errBox.classList.add("hidden");
const raw = $("seed").value.trim();
if (!raw) { errBox.textContent = "Enter your seed phrase."; errBox.classList.remove("hidden"); return; }
btn.disabled = true; btn.textContent = "Unlocking…";
try {
wallet = await BNS.BuiltInWallet.fromMnemonic(raw);
} catch (e) {
errBox.textContent = "Could not unlock: " + (e.message || e);
errBox.classList.remove("hidden");
btn.disabled = false; btn.textContent = "Sign in →";
return;
}
$("seed").value = "";
btn.disabled = false; btn.textContent = "Sign in →";
isOperator = wallet.address === OPERATOR_ADDRESS;
await enterAdmin();
});
$("signout-btn").addEventListener("click", () => {
wallet = null; isOperator = false;
$("me").classList.add("hidden");
$("mint").classList.add("hidden");
$("signin").classList.remove("hidden");
});
async function enterAdmin() {
$("signin").classList.add("hidden");
$("me").classList.remove("hidden");
$("me-addr").textContent = wallet.address;
$("me-taddr").textContent = wallet.tokenAddress;
const role = $("me-role");
role.textContent = isOperator ? "operator" : "read-only";
role.className = "badge " + (isOperator ? "b-op" : "b-other");
if (isOperator) {
$("mint").classList.remove("hidden");
}
}
// ---------- operator token ----------
// Stored in sessionStorage so refreshing the tab keeps the token for the
// session but a full browser close forgets it. Not localStorage — the
// weaker persistence is the right default for a POST-write credential.
const opToken = () => { try { return sessionStorage.getItem("bnsOperatorToken") || ""; } catch { return ""; } };
function setOpTokenMsg(text, ok) {
const el = $("op-token-msg"); el.textContent = text;
el.style.color = ok === true ? "var(--ok)" : ok === false ? "var(--taken)" : "var(--dim)";
}
$("op-token").value = opToken();
$("op-token-save").addEventListener("click", () => {
try { sessionStorage.setItem("bnsOperatorToken", $("op-token").value.trim()); } catch {}
setOpTokenMsg(opToken() ? "saved for this session" : "cleared", opToken() ? true : null);
});
// ---------- TLD registry with hide/unhide ----------
// Fetches BOTH /api/tlds?include_hidden=1 (so we see the hidden ones the
// public listing filters out) and the current /api/tld-visibility. Renders
// one row per TLD with a checkbox for the hidden flag — flipping it POSTs
// to /api/tld-visibility with the operator token.
async function loadRegistry() {
const status = $("registry-status");
const table = $("registry-table");
status.className = "status";
status.textContent = "Loading current registry from the chain…";
table.innerHTML = "";
try {
const [rTlds, rVis] = await Promise.all([
fetch("https://silentmode.st/api/tlds?include_hidden=1"),
fetch("https://silentmode.st/api/tld-visibility"),
]);
if (!rTlds.ok) throw new Error("api/tlds " + rTlds.status);
if (!rVis.ok) throw new Error("api/tld-visibility " + rVis.status);
const body = await rTlds.json();
const vis = await rVis.json();
const hidden = new Set(vis.hidden || []);
const tlds = (body.tlds || []).sort((a, b) => a.tld.localeCompare(b.tld));
status.className = "status ok";
status.textContent = `${tlds.length} TLDs on the beacon · ${hidden.size} hidden from public listing`;
let html = '<table class="tlds"><thead><tr><th>TLD</th><th>Names</th><th>Advertised</th><th>Hidden from public</th></tr></thead><tbody>';
for (const t of tlds) {
const isHidden = hidden.has(t.tld);
html += `<tr>
<td class="tld">.${esc(t.tld)}</td>
<td class="cat">${t.registered_count ?? 0}</td>
<td class="cat">${t.in_advertised ? "yes" : ""}</td>
<td><label class="row" style="gap:6px;margin:0;font-size:12.5px;color:var(--mut)">
<input type="checkbox" data-hide-tld="${esc(t.tld)}" ${isHidden ? "checked" : ""}>
${isHidden ? '<span class="badge b-mem">hidden</span>' : '<span class="dim">public</span>'}
</label></td>
</tr>`;
}
html += "</tbody></table>";
table.innerHTML = html;
// Wire visibility checkboxes.
table.querySelectorAll("input[data-hide-tld]").forEach((cb) => {
cb.addEventListener("change", async (e) => {
const tld = e.target.getAttribute("data-hide-tld");
const hide = e.target.checked;
if (!opToken()) {
e.target.checked = !hide;
setOpTokenMsg("paste your operator token first — required for /api/tld-visibility writes", false);
return;
}
e.target.disabled = true;
try {
const r = await fetch("https://silentmode.st/api/tld-visibility", {
method: "POST",
headers: { "content-type": "application/json", authorization: "Bearer " + opToken() },
body: JSON.stringify({ tld, hide }),
});
if (!r.ok) {
const j = await r.json().catch(() => ({}));
throw new Error(j.error || "http " + r.status);
}
setOpTokenMsg(`.${tld} is now ${hide ? "hidden" : "public"}`, true);
setTimeout(loadRegistry, 400); // refresh row states + counts
} catch (err) {
e.target.checked = !hide;
setOpTokenMsg("update failed: " + err.message, false);
} finally {
e.target.disabled = false;
}
});
});
} catch (e) {
status.className = "status err";
status.textContent = "Could not load registry: " + (e.message || e);
}
}
loadRegistry();
// ---------- mint a new TLD ----------
$("mint-btn").addEventListener("click", async () => {
if (!wallet || !isOperator) return;
const tld = $("mint-tld").value.trim().toLowerCase();
if (!/^[a-z0-9-]{1,16}$/.test(tld)) {
$("mint-msg").className = "msg err";
$("mint-msg").textContent = "TLD label must be 116 chars [a-z0-9-]";
return;
}
let records = {};
const rawRecords = $("mint-records").value.trim();
if (rawRecords) {
try { records = JSON.parse(rawRecords); }
catch (e) { $("mint-msg").className = "msg err"; $("mint-msg").textContent = "Records JSON is invalid: " + e.message; return; }
}
const btn = $("mint-btn");
const msg = $("mint-msg");
const log = $("mint-log");
log.className = "steps-log on"; log.innerHTML = "";
msg.textContent = ""; msg.className = "msg";
const push = (t) => { const d = document.createElement("div"); d.textContent = t; log.appendChild(d); log.scrollTop = log.scrollHeight; };
btn.disabled = true; btn.textContent = "Signing…";
let el = null;
try {
el = await BNS.connect();
push("connected to chipnet");
const res = await BNS.registerTldWithBuiltInWallet(el, {
wallet, tld, records,
onProgress: (step) => push(step),
});
push("broadcast: " + (res.txid || res.txId || "(unknown)"));
msg.className = "msg ok";
msg.textContent = "Minted .${tld}. Category " + (res.category || "unknown") + ". Registry refresh below in ~30s.";
btn.textContent = "Mint TLD →"; btn.disabled = false;
$("mint-tld").value = "";
$("mint-records").value = "{}";
setTimeout(loadRegistry, 3000);
} catch (e) {
msg.className = "msg err";
msg.textContent = "Mint failed: " + (e.message || e);
push("error: " + (e.message || e));
btn.disabled = false; btn.textContent = "Mint TLD →";
} finally {
try { el?.close?.(); } catch {}
}
});
</script>
<script src="../js/pin-escrow.js?v=20260909pin"></script>
<script src="../js/pricing.js?v=20260920bits"></script>
<script defer src="../js/site-footer.js?v=20260907rel"></script>
<script defer src="../js/theme.js?v=20260921mob"></script>
<script defer src="../js/profile-menu.js?v=20260921mob"></script>
<script>
// Sidenav scroll-spy: highlight the section currently in view.
// Uses scroll-position tracking so short sections at the end of the page
// still activate their link when the reader scrolls to them.
(() => {
const links = document.querySelectorAll(".sidenav a[href^='#']");
if (!links.length) return;
const entries = [];
links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) entries.push({ el, link: a }); });
if (!entries.length) return;
const setActive = (link) => { links.forEach((l) => l.classList.remove("active")); link.classList.add("active"); };
const update = () => {
const doc = document.documentElement;
const scrollBottom = window.scrollY + window.innerHeight;
if (scrollBottom >= (doc.scrollHeight - 4)) { setActive(entries[entries.length - 1].link); return; }
const line = window.scrollY + window.innerHeight * 0.35;
let active = entries[0].link;
for (const e of entries) {
if (e.el.getBoundingClientRect().top + window.scrollY <= line) active = e.link;
else break;
}
setActive(active);
};
window.addEventListener("scroll", update, { passive: true });
window.addEventListener("resize", update);
update();
})();
</script>
</body>
</html>