sites: scroll-spy tracks scroll position, fires the last section on bottom

The IntersectionObserver-based spy only fired when a section entered a
narrow band 40–45% down the viewport. Short sections at the tail of a
page — Operator key and Roadmap on theseus.x/extensions, whatever came
last on the other pages — never made it into that band because the
page runs out of scroll before their top crosses 40%, so their sidebar
links stayed dim no matter how far the reader scrolled.

Rewrote the spy across every page with a sidebar (theseus.x home /
plug-ins / extensions and sirius.x home / tld / theseus / docs / admin
/ brand): it listens to window scroll, picks the last section whose
top has crossed 35% of the viewport, and — when the window is scrolled
to within 4px of the bottom — snaps to the final section regardless of
where its top is. Every link in the rail can now activate.

theseus.x/extensions also had the spy IIFE bundled with the manifest
loader in one <script> block near the middle of the page, so at run
time the parser hadn't yet reached the last three sections and only
Bundled/Community/Build were ever registered. Moved the spy to its own
<script> just before </body> so it sees every section that ships.
This commit is contained in:
Local Dev 2026-09-20 19:20:30 +02:00
parent 0cb4aec5df
commit f2c3480d43
20 changed files with 1820 additions and 1630 deletions

View file

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

View file

@ -1,299 +1,310 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Brand — Sirius.X</title> <title>Brand — Sirius.X</title>
<meta name="description" content="Sirius.X brand kit — logo, wordmark, avatar, banner, favicon, colour palette, and usage guidance. All assets served as SVG so they stay crisp at any size."> <meta name="description" content="Sirius.X brand kit — logo, wordmark, avatar, banner, favicon, colour palette, and usage guidance. All assets served as SVG so they stay crisp at any size.">
<link rel="icon" href="./favicon.svg" type="image/svg+xml"> <link rel="icon" href="./favicon.svg" type="image/svg+xml">
<link rel="mask-icon" href="./favicon.svg" color="#d6ff3d"> <link rel="mask-icon" href="./favicon.svg" color="#d6ff3d">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:site_name" content="Sirius.X"> <meta property="og:site_name" content="Sirius.X">
<meta property="og:title" content="Sirius.X brand kit"> <meta property="og:title" content="Sirius.X brand kit">
<meta property="og:description" content="Logo, wordmark, avatar, banner, favicon, colour palette. All SVG, downloadable, MIT."> <meta property="og:description" content="Logo, wordmark, avatar, banner, favicon, colour palette. All SVG, downloadable, MIT.">
<meta property="og:url" content="https://silentmode.st/sirius-x/brand/"> <meta property="og:url" content="https://silentmode.st/sirius-x/brand/">
<meta property="og:image" content="https://silentmode.st/sirius-x/brand/banner.svg"> <meta property="og:image" content="https://silentmode.st/sirius-x/brand/banner.svg">
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <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> <style>
:root{ :root{
--bg:#050810; --panel:#141a24; --panel2:#0a0f1c; --line:rgba(255,255,255,.06); --bg:#050810; --panel:#141a24; --panel2:#0a0f1c; --line:rgba(255,255,255,.06);
--ink:#f1f4fa; --mut:#b8c2d4; --dim:#6a7488; --acid:#d6ff3d; --ink:#f1f4fa; --mut:#b8c2d4; --dim:#6a7488; --acid:#d6ff3d;
} }
*{box-sizing:border-box} *{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); 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} color:var(--ink);font:16px/1.65 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-height:100vh}
a{color:var(--acid)} a{color:var(--acid)}
.wrap{max-width:1040px;margin:0 auto;padding:0 1.2rem} .wrap{max-width:1040px;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; .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)} 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{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 .brand .g{color:var(--acid)}
.topnav a{color:var(--mut);text-decoration:none;padding:6px 12px;border-radius:8px;font-size:14px} .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:hover{color:var(--ink);background:var(--panel)}
.topnav a.here{color:var(--acid);background:rgba(214,255,61,.08)} .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{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)} .topnav a.portal:hover{background:rgba(214,255,61,.18)}
/* Adaptive side nav — same shape as index.html and Theseus.X */ /* Adaptive side nav — same shape as index.html and Theseus.X */
:root{--sn-w:200px} :root{--sn-w:200px}
.sidenav{position:fixed;top:56px;left:0;bottom:0;width:var(--sn-w);z-index:15; .sidenav{position:fixed;top:56px;left:0;bottom:0;width:var(--sn-w);z-index:15;
background:transparent;padding:32px 12px 22px 18px; background:transparent;padding:32px 12px 22px 18px;
overflow-y:auto;pointer-events:none;transition:width .15s ease} 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; .sidenav a{display:flex;align-items:center;gap:14px;color:var(--mut);text-decoration:none;
padding:9px 12px;border-radius:10px; padding:9px 12px;border-radius:10px;
font-size:14.5px;font-weight:500;opacity:.35;pointer-events:auto; font-size:14.5px;font-weight:500;opacity:.35;pointer-events:auto;
transition:opacity .12s,color .12s,background .12s,backdrop-filter .12s} 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; .sidenav a .ic{flex:none;width:22px;font-size:18px;line-height:1;text-align:center;
display:none;justify-content:center;align-items:center} display:none;justify-content:center;align-items:center}
.sidenav a .lb{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .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: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{opacity:.85;color:var(--acid)}
.sidenav a.active:hover{opacity:1} .sidenav a.active:hover{opacity:1}
section{scroll-margin-top:70px} section{scroll-margin-top:70px}
@media (max-width:1180px){ @media (max-width:1180px){
:root{--sn-w:64px} :root{--sn-w:64px}
.sidenav{padding:22px 6px} .sidenav{padding:22px 6px}
.sidenav a{justify-content:center;padding:12px 4px;gap:0} .sidenav a{justify-content:center;padding:12px 4px;gap:0}
.sidenav a .ic{display:inline-flex} .sidenav a .ic{display:inline-flex}
.sidenav a .lb{display:none} .sidenav a .lb{display:none}
} }
@media (max-width:720px){ @media (max-width:720px){
:root{--sn-w:0} :root{--sn-w:0}
body{padding-bottom:64px} body{padding-bottom:64px}
.sidenav{top:auto;bottom:0;left:0;right:0;width:100%;height:auto; .sidenav{top:auto;bottom:0;left:0;right:0;width:100%;height:auto;
padding:4px;flex-direction:row;background:rgba(11,14,20,.52); padding:4px;flex-direction:row;background:rgba(11,14,20,.52);
display:flex;justify-content:space-around;overflow-x:auto} display:flex;justify-content:space-around;overflow-x:auto}
.sidenav a{flex:1;min-width:56px;padding:10px 6px;justify-content:center;gap:0;opacity:.5} .sidenav a{flex:1;min-width:56px;padding:10px 6px;justify-content:center;gap:0;opacity:.5}
.sidenav a .ic{display:inline-flex} .sidenav a .ic{display:inline-flex}
.sidenav a .lb{display:none} .sidenav a .lb{display:none}
} }
header.hero{text-align:center;padding:3.5rem 1.2rem 1rem} header.hero{text-align:center;padding:3.5rem 1.2rem 1rem}
.mark{font-size:44px;line-height:1} .mark{font-size:44px;line-height:1}
h1{font-size:clamp(1.7rem,4.6vw,2.3rem);margin:.3rem 0 .3rem} h1{font-size:clamp(1.7rem,4.6vw,2.3rem);margin:.3rem 0 .3rem}
h1 .g{color:var(--acid)} h1 .g{color:var(--acid)}
.tag{color:var(--mut);font-size:1.02rem;max-width:640px;margin:0 auto} .tag{color:var(--mut);font-size:1.02rem;max-width:640px;margin:0 auto}
section{padding:2.2rem 0;border-top:1px solid var(--line);margin-top:2rem} section{padding:2.2rem 0;border-top:1px solid var(--line);margin-top:2rem}
section:first-of-type{border-top:none;margin-top:1.2rem} section:first-of-type{border-top:none;margin-top:1.2rem}
h2{font-size:1.35rem;margin:0 0 .35rem} h2{font-size:1.35rem;margin:0 0 .35rem}
h3{font-size:1rem;margin:0 0 .3rem} h3{font-size:1rem;margin:0 0 .3rem}
.lede{color:var(--mut);margin:0 0 1.4rem;max-width:720px} .lede{color:var(--mut);margin:0 0 1.4rem;max-width:720px}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px} .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px}
.asset{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px 16px 16px;display:flex;flex-direction:column} .asset{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px 16px 16px;display:flex;flex-direction:column}
.asset .preview{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:20px; .asset .preview{background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:20px;
margin-bottom:12px;min-height:130px;display:flex;align-items:center;justify-content:center} margin-bottom:12px;min-height:130px;display:flex;align-items:center;justify-content:center}
.asset .preview.wide{padding:0} .asset .preview.wide{padding:0}
.asset .preview.wide img{width:100%;height:auto;display:block;border-radius:9px} .asset .preview.wide img{width:100%;height:auto;display:block;border-radius:9px}
.asset .preview.avatar{padding:0} .asset .preview.avatar{padding:0}
.asset .preview.avatar img{width:104px;height:104px;display:block;border-radius:14px} .asset .preview.avatar img{width:104px;height:104px;display:block;border-radius:14px}
.asset .preview.favicon img{width:64px;height:64px;display:block} .asset .preview.favicon img{width:64px;height:64px;display:block}
.asset .preview.mono{background:#eef1f7;color:#0b0e14} .asset .preview.mono{background:#eef1f7;color:#0b0e14}
.asset h3{display:flex;align-items:center;justify-content:space-between;gap:10px} .asset h3{display:flex;align-items:center;justify-content:space-between;gap:10px}
.asset .meta{color:var(--dim);font-size:12.5px;margin:2px 0 10px;font-family:ui-monospace,monospace} .asset .meta{color:var(--dim);font-size:12.5px;margin:2px 0 10px;font-family:ui-monospace,monospace}
.asset .row{display:flex;gap:8px;flex-wrap:wrap;margin-top:auto} .asset .row{display:flex;gap:8px;flex-wrap:wrap;margin-top:auto}
.btn{display:inline-block;text-decoration:none;padding:6px 12px;border-radius:8px;background:transparent; .btn{display:inline-block;text-decoration:none;padding:6px 12px;border-radius:8px;background:transparent;
border:1px solid var(--line);color:var(--ink);font-size:12.5px;font-family:inherit;cursor:pointer} border:1px solid var(--line);color:var(--ink);font-size:12.5px;font-family:inherit;cursor:pointer}
.btn:hover{background:var(--panel2)} .btn:hover{background:var(--panel2)}
.btn.acid{background:var(--acid);color:#0b0e14;border-color:var(--acid);font-weight:600} .btn.acid{background:var(--acid);color:#0b0e14;border-color:var(--acid);font-weight:600}
.btn.acid:hover{filter:brightness(1.1)} .btn.acid:hover{filter:brightness(1.1)}
.swatches{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:1rem} .swatches{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:1rem}
.sw{border:1px solid var(--line);border-radius:10px;overflow:hidden} .sw{border:1px solid var(--line);border-radius:10px;overflow:hidden}
.sw .chip{height:70px} .sw .chip{height:70px}
.sw .info{padding:10px 12px;font-size:12.5px;font-family:ui-monospace,monospace;color:var(--mut)} .sw .info{padding:10px 12px;font-size:12.5px;font-family:ui-monospace,monospace;color:var(--mut)}
.sw .info b{color:var(--ink);display:block;font-family:inherit} .sw .info b{color:var(--ink);display:block;font-family:inherit}
.note{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--acid); .note{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--acid);
border-radius:10px;padding:12px 15px;color:var(--mut);font-size:13.5px;margin-top:1.2rem} border-radius:10px;padding:12px 15px;color:var(--mut);font-size:13.5px;margin-top:1.2rem}
.note b{color:var(--ink)} .note b{color:var(--ink)}
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} 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}
footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem} footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
/* Deviant-family fonts + Fraunces/Ubuntu reader toggle. */ /* Deviant-family fonts + Fraunces/Ubuntu reader toggle. */
h1,h2,h3{font-family:'Fraunces',Georgia,serif;font-feature-settings:"ss01","ss02"} h1,h2,h3{font-family:'Fraunces',Georgia,serif;font-feature-settings:"ss01","ss02"}
code,.mono{font-family:'JetBrains Mono',ui-monospace,monospace} 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"} 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"] 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} 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{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{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:hover{color:var(--ink)}
.font-picker button.active{background:var(--acid);color:var(--bg);font-weight:600} .font-picker button.active{background:var(--acid);color:var(--bg);font-weight:600}
</style> </style>
<script type="importmap"> <script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } } { "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js?v=20260920usd" } }
</script> </script>
</head> </head>
<body> <body>
<nav class="topnav"> <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><span class="g">Sirius.X</span></a> <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><span class="g">Sirius.X</span></a>
<a href="../tld.html">🌐 TLD</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="../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="../portal.html" class="portal">🔑 Sign in</a> <a href="../portal.html" class="portal">🔑 Sign in</a>
</nav> </nav>
<aside class="sidenav" aria-label="On this page"> <aside class="sidenav" aria-label="On this page">
<a href="#logo" title="Logo &amp; wordmark" ><span class="ic" aria-hidden="true">✦</span><span class="lb">Logo</span></a> <a href="#logo" title="Logo &amp; wordmark" ><span class="ic" aria-hidden="true">✦</span><span class="lb">Logo</span></a>
<a href="#avatar" title="Avatar &amp; favicon" ><span class="ic" aria-hidden="true">🎭</span><span class="lb">Avatar</span></a> <a href="#avatar" title="Avatar &amp; favicon" ><span class="ic" aria-hidden="true">🎭</span><span class="lb">Avatar</span></a>
<a href="#banner" title="Social banner" ><span class="ic" aria-hidden="true">🖼</span><span class="lb">Banner</span></a> <a href="#banner" title="Social banner" ><span class="ic" aria-hidden="true">🖼</span><span class="lb">Banner</span></a>
<a href="#palette" title="Colour palette" ><span class="ic" aria-hidden="true">🎨</span><span class="lb">Palette</span></a> <a href="#palette" title="Colour palette" ><span class="ic" aria-hidden="true">🎨</span><span class="lb">Palette</span></a>
<a href="#typography" title="Typography" ><span class="ic" aria-hidden="true">🅰️</span><span class="lb">Typography</span></a> <a href="#typography" title="Typography" ><span class="ic" aria-hidden="true">🅰️</span><span class="lb">Typography</span></a>
<a href="#usage" title="Usage &amp; licence" ><span class="ic" aria-hidden="true">📜</span><span class="lb">Usage</span></a> <a href="#usage" title="Usage &amp; licence" ><span class="ic" aria-hidden="true">📜</span><span class="lb">Usage</span></a>
</aside> </aside>
<header class="hero"> <header class="hero">
<div class="mark">🎨</div> <div class="mark">🎨</div>
<h1>Sirius.X <span class="g">brand kit</span></h1> <h1>Sirius.X <span class="g">brand kit</span></h1>
<p class="tag">Logo, wordmark, avatar, banner, favicon, and the colour palette. Every asset ships as <p class="tag">Logo, wordmark, avatar, banner, favicon, and the colour palette. Every asset ships as
SVG so it stays crisp at any size and reads correctly in light and dark contexts. Free to reuse SVG so it stays crisp at any size and reads correctly in light and dark contexts. Free to reuse
with attribution.</p> with attribution.</p>
</header> </header>
<div class="wrap"> <div class="wrap">
<section id="logo"> <section id="logo">
<h2>Logo &amp; wordmark</h2> <h2>Logo &amp; wordmark</h2>
<p class="lede">A 4-point sparkle star plus <b>Sirius.X</b>, with the <code>.x</code> TLD picked out <p class="lede">A 4-point sparkle star plus <b>Sirius.X</b>, with the <code>.x</code> TLD picked out
in acid green. The star alone works when you have no room for the wordmark; the full lockup is in acid green. The star alone works when you have no room for the wordmark; the full lockup is
the default choice everywhere else.</p> the default choice everywhere else.</p>
<div class="grid"> <div class="grid">
<div class="asset"> <div class="asset">
<div class="preview"><img src="./logo.svg" alt="sirius.x logo" style="max-width:100%;height:auto"></div> <div class="preview"><img src="./logo.svg" alt="sirius.x logo" style="max-width:100%;height:auto"></div>
<h3>Wordmark <span class="meta">SVG · 320×64</span></h3> <h3>Wordmark <span class="meta">SVG · 320×64</span></h3>
<p class="lede" style="margin-bottom:.6rem">Full colour, dark backgrounds. Use this by default.</p> <p class="lede" style="margin-bottom:.6rem">Full colour, dark backgrounds. Use this by default.</p>
<div class="row"> <div class="row">
<a class="btn acid" href="./logo.svg" download>Download SVG</a> <a class="btn acid" href="./logo.svg" download>Download SVG</a>
<a class="btn" href="./logo.svg" target="_blank">Open</a> <a class="btn" href="./logo.svg" target="_blank">Open</a>
</div> </div>
</div> </div>
<div class="asset"> <div class="asset">
<div class="preview mono" style="color:#0b0e14"><img src="./logo-mono.svg" alt="sirius.x mono" style="max-width:100%;height:auto;color:inherit"></div> <div class="preview mono" style="color:#0b0e14"><img src="./logo-mono.svg" alt="sirius.x mono" style="max-width:100%;height:auto;color:inherit"></div>
<h3>Wordmark, monochrome <span class="meta">SVG · currentColor</span></h3> <h3>Wordmark, monochrome <span class="meta">SVG · currentColor</span></h3>
<p class="lede" style="margin-bottom:.6rem">Uses <code>currentColor</code>, inherits from the <p class="lede" style="margin-bottom:.6rem">Uses <code>currentColor</code>, inherits from the
surrounding text. Print, embossing, single-colour merch.</p> surrounding text. Print, embossing, single-colour merch.</p>
<div class="row"> <div class="row">
<a class="btn acid" href="./logo-mono.svg" download>Download SVG</a> <a class="btn acid" href="./logo-mono.svg" download>Download SVG</a>
<a class="btn" href="./logo-mono.svg" target="_blank">Open</a> <a class="btn" href="./logo-mono.svg" target="_blank">Open</a>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section id="avatar"> <section id="avatar">
<h2>Avatar &amp; favicon</h2> <h2>Avatar &amp; favicon</h2>
<p class="lede">Square icons for social profiles, Nostr, GitHub, and browser tabs.</p> <p class="lede">Square icons for social profiles, Nostr, GitHub, and browser tabs.</p>
<div class="grid"> <div class="grid">
<div class="asset"> <div class="asset">
<div class="preview avatar"><img src="./avatar.svg" alt="sirius.x avatar"></div> <div class="preview avatar"><img src="./avatar.svg" alt="sirius.x avatar"></div>
<h3>Avatar <span class="meta">SVG · 512×512</span></h3> <h3>Avatar <span class="meta">SVG · 512×512</span></h3>
<p class="lede" style="margin-bottom:.6rem">Square profile picture. Social platforms apply <p class="lede" style="margin-bottom:.6rem">Square profile picture. Social platforms apply
their own round mask, so corners stay hard.</p> their own round mask, so corners stay hard.</p>
<div class="row"> <div class="row">
<a class="btn acid" href="./avatar.svg" download>Download SVG</a> <a class="btn acid" href="./avatar.svg" download>Download SVG</a>
<a class="btn" href="./avatar.svg" target="_blank">Open</a> <a class="btn" href="./avatar.svg" target="_blank">Open</a>
</div> </div>
</div> </div>
<div class="asset"> <div class="asset">
<div class="preview favicon"><img src="./favicon.svg" alt="sirius.x favicon"></div> <div class="preview favicon"><img src="./favicon.svg" alt="sirius.x favicon"></div>
<h3>Favicon <span class="meta">SVG · 64×64, 12px radius</span></h3> <h3>Favicon <span class="meta">SVG · 64×64, 12px radius</span></h3>
<p class="lede" style="margin-bottom:.6rem">Rounded browser-tab icon. Scales down to 16×16 <p class="lede" style="margin-bottom:.6rem">Rounded browser-tab icon. Scales down to 16×16
cleanly; used on every Sirius.X page.</p> cleanly; used on every Sirius.X page.</p>
<div class="row"> <div class="row">
<a class="btn acid" href="./favicon.svg" download>Download SVG</a> <a class="btn acid" href="./favicon.svg" download>Download SVG</a>
<a class="btn" href="./favicon.svg" target="_blank">Open</a> <a class="btn" href="./favicon.svg" target="_blank">Open</a>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section id="banner"> <section id="banner">
<h2>Social banner</h2> <h2>Social banner</h2>
<p class="lede">1200×630 (OpenGraph / Twitter Card canonical). Composition survives aggressive <p class="lede">1200×630 (OpenGraph / Twitter Card canonical). Composition survives aggressive
centre-cropping — the star and tagline both stay visible even after Facebook and Discord chop centre-cropping — the star and tagline both stay visible even after Facebook and Discord chop
the sides.</p> the sides.</p>
<div class="asset"> <div class="asset">
<div class="preview wide"><img src="./banner.svg" alt="sirius.x banner"></div> <div class="preview wide"><img src="./banner.svg" alt="sirius.x banner"></div>
<div class="row"> <div class="row">
<a class="btn acid" href="./banner.svg" download>Download SVG</a> <a class="btn acid" href="./banner.svg" download>Download SVG</a>
<a class="btn" href="./banner.svg" target="_blank">Open</a> <a class="btn" href="./banner.svg" target="_blank">Open</a>
</div> </div>
</div> </div>
</section> </section>
<section id="palette"> <section id="palette">
<h2>Colour palette</h2> <h2>Colour palette</h2>
<p class="lede">Silent Mode family colours. Dark chromes are the default; acid green is the <p class="lede">Silent Mode family colours. Dark chromes are the default; acid green is the
single accent that consistently signals "this is Sirius.X".</p> single accent that consistently signals "this is Sirius.X".</p>
<div class="swatches"> <div class="swatches">
<div class="sw"><div class="chip" style="background:#d6ff3d"></div><div class="info"><b>Acid</b>#d6ff3d — accent, links, active nav</div></div> <div class="sw"><div class="chip" style="background:#d6ff3d"></div><div class="info"><b>Acid</b>#d6ff3d — accent, links, active nav</div></div>
<div class="sw"><div class="chip" style="background:#0b0e14"></div><div class="info"><b>Background</b>#0b0e14 — page canvas</div></div> <div class="sw"><div class="chip" style="background:#0b0e14"></div><div class="info"><b>Background</b>#0b0e14 — page canvas</div></div>
<div class="sw"><div class="chip" style="background:#141a24"></div><div class="info"><b>Panel</b>#141a24 — cards, elevated surfaces</div></div> <div class="sw"><div class="chip" style="background:#141a24"></div><div class="info"><b>Panel</b>#141a24 — cards, elevated surfaces</div></div>
<div class="sw"><div class="chip" style="background:#18202c"></div><div class="info"><b>Panel 2</b>#18202c — nested / step items</div></div> <div class="sw"><div class="chip" style="background:#18202c"></div><div class="info"><b>Panel 2</b>#18202c — nested / step items</div></div>
<div class="sw"><div class="chip" style="background:#e7eaf1"></div><div class="info"><b>Ink</b>#e7eaf1 — body text on dark</div></div> <div class="sw"><div class="chip" style="background:#e7eaf1"></div><div class="info"><b>Ink</b>#e7eaf1 — body text on dark</div></div>
<div class="sw"><div class="chip" style="background:#8b98a9"></div><div class="info"><b>Muted</b>#8b98a9 — secondary text</div></div> <div class="sw"><div class="chip" style="background:#8b98a9"></div><div class="info"><b>Muted</b>#8b98a9 — secondary text</div></div>
<div class="sw"><div class="chip" style="background:#5e6678"></div><div class="info"><b>Dim</b>#5e6678 — tertiary text, dividers</div></div> <div class="sw"><div class="chip" style="background:#5e6678"></div><div class="info"><b>Dim</b>#5e6678 — tertiary text, dividers</div></div>
<div class="sw"><div class="chip" style="background:#4fd1a5"></div><div class="info"><b>OK</b>#4fd1a5 — success / available</div></div> <div class="sw"><div class="chip" style="background:#4fd1a5"></div><div class="info"><b>OK</b>#4fd1a5 — success / available</div></div>
<div class="sw"><div class="chip" style="background:#f6768a"></div><div class="info"><b>Taken</b>#f6768a — errors / unavailable</div></div> <div class="sw"><div class="chip" style="background:#f6768a"></div><div class="info"><b>Taken</b>#f6768a — errors / unavailable</div></div>
<div class="sw"><div class="chip" style="background:#ffc75f"></div><div class="info"><b>Warn</b>#ffc75f — caution, holds</div></div> <div class="sw"><div class="chip" style="background:#ffc75f"></div><div class="info"><b>Warn</b>#ffc75f — caution, holds</div></div>
</div> </div>
</section> </section>
<section id="typography"> <section id="typography">
<h2>Typography</h2> <h2>Typography</h2>
<p class="lede">System font stack, so nothing needs to load and every OS renders the wordmark in <p class="lede">System font stack, so nothing needs to load and every OS renders the wordmark in
the same family the reader is used to.</p> the same family the reader is used to.</p>
<div class="asset" style="max-width:640px"> <div class="asset" style="max-width:640px">
<p style="margin:0;font-family:system-ui, -apple-system, Segoe UI, Roboto, sans-serif; <p style="margin:0;font-family:system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
font-size:36px;font-weight:600;letter-spacing:-.5px">The quick brown fox</p> font-size:36px;font-weight:600;letter-spacing:-.5px">The quick brown fox</p>
<p style="margin:.4rem 0 0;color:var(--mut);font-family:system-ui, -apple-system, Segoe UI, Roboto, sans-serif; <p style="margin:.4rem 0 0;color:var(--mut);font-family:system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
font-size:16px">jumps over the lazy dog · 0123456789 · Sirius.X</p> font-size:16px">jumps over the lazy dog · 0123456789 · Sirius.X</p>
<div class="meta" style="margin-top:12px">system-ui, -apple-system, Segoe UI, Roboto, sans-serif</div> <div class="meta" style="margin-top:12px">system-ui, -apple-system, Segoe UI, Roboto, sans-serif</div>
</div> </div>
<div class="note">The wordmark's <code>.x</code> is a <code>&lt;tspan fill="#d6ff3d"&gt;</code> — <div class="note">The wordmark's <code>.x</code> is a <code>&lt;tspan fill="#d6ff3d"&gt;</code> —
don't render the whole word in acid, and don't render the star without the star's exact 4-point don't render the whole word in acid, and don't render the star without the star's exact 4-point
path (it's a mark, not a generic asterisk).</div> path (it's a mark, not a generic asterisk).</div>
</section> </section>
<section id="usage"> <section id="usage">
<h2>Usage &amp; licence</h2> <h2>Usage &amp; licence</h2>
<p class="lede">All assets are MIT. Reuse them for third-party tools that integrate with Sirius.X <p class="lede">All assets are MIT. Reuse them for third-party tools that integrate with Sirius.X
(wallets, indexers, explorers, blog posts about BCNR). Keep the star's proportions intact and (wallets, indexers, explorers, blog posts about BCNR). Keep the star's proportions intact and
don't recolour the acid to a similar-but-different green — palette drift is what makes brands don't recolour the acid to a similar-but-different green — palette drift is what makes brands
look untrustworthy over time.</p> look untrustworthy over time.</p>
<ul style="color:var(--mut);font-size:14px;padding-left:1.2rem"> <ul style="color:var(--mut);font-size:14px;padding-left:1.2rem">
<li><b style="color:var(--ink)">Do</b> use the mark to indicate "supports Sirius.X" or "hosted on Sirius.X".</li> <li><b style="color:var(--ink)">Do</b> use the mark to indicate "supports Sirius.X" or "hosted on Sirius.X".</li>
<li><b style="color:var(--ink)">Do</b> shrink to the favicon at 16×16 — the SVG scales cleanly.</li> <li><b style="color:var(--ink)">Do</b> shrink to the favicon at 16×16 — the SVG scales cleanly.</li>
<li><b style="color:var(--ink)">Don't</b> imply endorsement of a specific product without asking.</li> <li><b style="color:var(--ink)">Don't</b> imply endorsement of a specific product without asking.</li>
<li><b style="color:var(--ink)">Don't</b> stretch, skew, or re-tint the star.</li> <li><b style="color:var(--ink)">Don't</b> stretch, skew, or re-tint the star.</li>
</ul> </ul>
</section> </section>
</div> </div>
<footer id="site-footer"></footer> <footer id="site-footer"></footer>
<script src="../js/pin-escrow.js?v=20260909pin"></script> <script src="../js/pin-escrow.js?v=20260909pin"></script>
<script src="../js/pricing.js?v=20260920bits"></script> <script src="../js/pricing.js?v=20260920bits"></script>
<script defer src="../js/site-footer.js?v=20260907rel"></script> <script defer src="../js/site-footer.js?v=20260907rel"></script>
<script defer src="../js/theme.js?v=20260920compact"></script> <script defer src="../js/theme.js?v=20260920compact"></script>
<script defer src="../js/profile-menu.js?v=20260920bits"></script> <script defer src="../js/profile-menu.js?v=20260920bits"></script>
<script> <script>
(() => { // Sidenav scroll-spy: highlight the section currently in view.
const links = document.querySelectorAll(".sidenav a[href^='#']"); // Uses scroll-position tracking so short sections at the end of the page
if (!links.length) return; // still activate their link when the reader scrolls to them.
const map = new Map(); (() => {
links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) map.set(el, a); }); const links = document.querySelectorAll(".sidenav a[href^='#']");
const io = new IntersectionObserver((entries) => { if (!links.length) return;
entries.forEach((en) => { const entries = [];
if (!en.isIntersecting) return; links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) entries.push({ el, link: a }); });
const a = map.get(en.target); if (!a) return; if (!entries.length) return;
links.forEach((l) => l.classList.remove("active")); const setActive = (link) => { links.forEach((l) => l.classList.remove("active")); link.classList.add("active"); };
a.classList.add("active"); const update = () => {
}); const doc = document.documentElement;
}, { rootMargin: "-40% 0px -55% 0px", threshold: 0 }); const scrollBottom = window.scrollY + window.innerHeight;
map.forEach((_, el) => io.observe(el)); if (scrollBottom >= (doc.scrollHeight - 4)) { setActive(entries[entries.length - 1].link); return; }
})(); const line = window.scrollY + window.innerHeight * 0.35;
</script> let active = entries[0].link;
</body> for (const e of entries) {
</html> 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>

View file

@ -125,9 +125,9 @@
.font-picker button.active{background:var(--acid);color:var(--bg);font-weight:600} .font-picker button.active{background:var(--acid);color:var(--bg);font-weight:600}
</style> </style>
<script type="importmap"> <script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } } { "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js?v=20260920usd" } }
</script> </script>
<script src="../js/i18n.js?v=20260920f"></script> <script src="../js/i18n.js?v=20260920g"></script>
</head> </head>
<body> <body>
@ -651,20 +651,31 @@ process.exit(0);"</pre>
<script defer src="../js/theme.js?v=20260920compact"></script> <script defer src="../js/theme.js?v=20260920compact"></script>
<script defer src="../js/profile-menu.js?v=20260920bits"></script> <script defer src="../js/profile-menu.js?v=20260920bits"></script>
<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^='#']"); const links = document.querySelectorAll(".sidenav a[href^='#']");
if (!links.length) return; if (!links.length) return;
const map = new Map(); const entries = [];
links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) map.set(el, a); }); links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) entries.push({ el, link: a }); });
const io = new IntersectionObserver((entries) => { if (!entries.length) return;
entries.forEach((en) => { const setActive = (link) => { links.forEach((l) => l.classList.remove("active")); link.classList.add("active"); };
if (!en.isIntersecting) return; const update = () => {
const a = map.get(en.target); if (!a) return; const doc = document.documentElement;
links.forEach((l) => l.classList.remove("active")); const scrollBottom = window.scrollY + window.innerHeight;
a.classList.add("active"); if (scrollBottom >= (doc.scrollHeight - 4)) { setActive(entries[entries.length - 1].link); return; }
}); const line = window.scrollY + window.innerHeight * 0.35;
}, { rootMargin: "-40% 0px -55% 0px", threshold: 0 }); let active = entries[0].link;
map.forEach((_, el) => io.observe(el)); 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> </script>
</body> </body>

View file

@ -609,5 +609,9 @@
"where the page files live (the s3 record)": "wo die Seitendateien liegen (der s3-Eintrag)", "where the page files live (the s3 record)": "wo die Seitendateien liegen (der s3-Eintrag)",
"Not hosted on Sia": "Nicht auf Sia gehostet", "Not hosted on Sia": "Nicht auf Sia gehostet",
"Sirius.X hosting — free, on Sia storage": "Sirius.X-Hosting — kostenlos, auf Sia-Speicher", "Sirius.X hosting — free, on Sia storage": "Sirius.X-Hosting — kostenlos, auf Sia-Speicher",
"Another Sia folder (advanced)": "Anderer Sia-Ordner (erweitert)" "Another Sia folder (advanced)": "Anderer Sia-Ordner (erweitert)",
"Price is fixed in": "Preis festgelegt in",
"US dollars — the BCH amount follows the market": "US-Dollar — der BCH-Betrag folgt dem Markt",
"BCH — the dollar value follows the market": "BCH — der Dollarwert folgt dem Markt",
"Accept between": "Akzeptiere zwischen"
} }

View file

@ -609,5 +609,9 @@
"where the page files live (the s3 record)": "πού βρίσκονται τα αρχεία της σελίδας (η εγγραφή s3)", "where the page files live (the s3 record)": "πού βρίσκονται τα αρχεία της σελίδας (η εγγραφή s3)",
"Not hosted on Sia": "Δεν φιλοξενείται στο Sia", "Not hosted on Sia": "Δεν φιλοξενείται στο Sia",
"Sirius.X hosting — free, on Sia storage": "Φιλοξενία Sirius.X — δωρεάν, στον αποθηκευτικό χώρο Sia", "Sirius.X hosting — free, on Sia storage": "Φιλοξενία Sirius.X — δωρεάν, στον αποθηκευτικό χώρο Sia",
"Another Sia folder (advanced)": "Άλλος φάκελος Sia (για προχωρημένους)" "Another Sia folder (advanced)": "Άλλος φάκελος Sia (για προχωρημένους)",
"Price is fixed in": "Η τιμή είναι σταθερή σε",
"US dollars — the BCH amount follows the market": "Δολάρια ΗΠΑ — το ποσό σε BCH ακολουθεί την αγορά",
"BCH — the dollar value follows the market": "BCH — η αξία σε δολάρια ακολουθεί την αγορά",
"Accept between": "Αποδοχή μεταξύ"
} }

View file

@ -609,5 +609,9 @@
"where the page files live (the s3 record)": "dónde viven los archivos de la página (el registro s3)", "where the page files live (the s3 record)": "dónde viven los archivos de la página (el registro s3)",
"Not hosted on Sia": "No alojado en Sia", "Not hosted on Sia": "No alojado en Sia",
"Sirius.X hosting — free, on Sia storage": "Alojamiento Sirius.X — gratis, en almacenamiento Sia", "Sirius.X hosting — free, on Sia storage": "Alojamiento Sirius.X — gratis, en almacenamiento Sia",
"Another Sia folder (advanced)": "Otra carpeta de Sia (avanzado)" "Another Sia folder (advanced)": "Otra carpeta de Sia (avanzado)",
"Price is fixed in": "El precio se fija en",
"US dollars — the BCH amount follows the market": "Dólares — la cantidad en BCH sigue al mercado",
"BCH — the dollar value follows the market": "BCH — el valor en dólares sigue al mercado",
"Accept between": "Aceptar entre"
} }

View file

@ -609,5 +609,9 @@
"where the page files live (the s3 record)": "où se trouvent les fichiers de la page (l'enregistrement s3)", "where the page files live (the s3 record)": "où se trouvent les fichiers de la page (l'enregistrement s3)",
"Not hosted on Sia": "Pas hébergé sur Sia", "Not hosted on Sia": "Pas hébergé sur Sia",
"Sirius.X hosting — free, on Sia storage": "Hébergement Sirius.X — gratuit, sur le stockage Sia", "Sirius.X hosting — free, on Sia storage": "Hébergement Sirius.X — gratuit, sur le stockage Sia",
"Another Sia folder (advanced)": "Un autre dossier Sia (avancé)" "Another Sia folder (advanced)": "Un autre dossier Sia (avancé)",
"Price is fixed in": "Le prix est fixé en",
"US dollars — the BCH amount follows the market": "Dollars US — le montant en BCH suit le marché",
"BCH — the dollar value follows the market": "BCH — la valeur en dollars suit le marché",
"Accept between": "Accepter entre"
} }

View file

@ -609,5 +609,9 @@
"where the page files live (the s3 record)": "onde ficam os ficheiros da página (o registo s3)", "where the page files live (the s3 record)": "onde ficam os ficheiros da página (o registo s3)",
"Not hosted on Sia": "Não alojado no Sia", "Not hosted on Sia": "Não alojado no Sia",
"Sirius.X hosting — free, on Sia storage": "Alojamento Sirius.X — grátis, no armazenamento Sia", "Sirius.X hosting — free, on Sia storage": "Alojamento Sirius.X — grátis, no armazenamento Sia",
"Another Sia folder (advanced)": "Outra pasta Sia (avançado)" "Another Sia folder (advanced)": "Outra pasta Sia (avançado)",
"Price is fixed in": "O preço é fixado em",
"US dollars — the BCH amount follows the market": "Dólares — o montante em BCH segue o mercado",
"BCH — the dollar value follows the market": "BCH — o valor em dólares segue o mercado",
"Accept between": "Aceitar entre"
} }

View file

@ -609,5 +609,9 @@
"where the page files live (the s3 record)": "где лежат файлы страницы (запись s3)", "where the page files live (the s3 record)": "где лежат файлы страницы (запись s3)",
"Not hosted on Sia": "Не размещено на Sia", "Not hosted on Sia": "Не размещено на Sia",
"Sirius.X hosting — free, on Sia storage": "Хостинг Sirius.X — бесплатно, в хранилище Sia", "Sirius.X hosting — free, on Sia storage": "Хостинг Sirius.X — бесплатно, в хранилище Sia",
"Another Sia folder (advanced)": "Другая папка Sia (для опытных)" "Another Sia folder (advanced)": "Другая папка Sia (для опытных)",
"Price is fixed in": "Цена зафиксирована в",
"US dollars — the BCH amount follows the market": "Доллары США — сумма в BCH следует за рынком",
"BCH — the dollar value follows the market": "BCH — долларовая стоимость следует за рынком",
"Accept between": "Принимать в диапазоне"
} }

1150
index.html

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@
// is the persistent device session (js/session.js): sign in once, come back // is the persistent device session (js/session.js): sign in once, come back
// without a prompt until you sign out. // without a prompt until you sign out.
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market"; import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260920usd";
const API = "https://silentmode.st"; const API = "https://silentmode.st";
const TLD_API = "https://navigate.st/api/tlds?include_hidden=1"; const TLD_API = "https://navigate.st/api/tlds?include_hidden=1";
@ -685,14 +685,34 @@ async function signedHeaders(msg) {
// ---------- sell (marketplace listing) ---------- // ---------- sell (marketplace listing) ----------
const RATE = () => window.siriusPricing?.CHIPNET_SATS_PER_USD || 250000; const RATE = () => window.siriusPricing?.CHIPNET_SATS_PER_USD || 250000;
let activeListing = null; let activeListing = null;
function sellMode() { return $("sell-mode").value; }
function paintSellMode() {
const usd = sellMode() === "usd";
$("sell-usd-terms").classList.toggle("hidden", !usd);
paintBand();
}
function paintBand() {
const usd = Number(String($("sell-usd").value).replace(/[^\d.]/g, "")) || 0;
const band = Math.min(90, Math.max(5, Number(String($("sell-band").value).replace(/\D/g, "")) || 25));
const sats = Math.round(usd * RATE());
$("sell-band-hint").textContent = usd > 0
? `% of today's rate: you will receive between ${fmtBch(Math.round(sats * (1 - band / 100)))} and ${fmtBch(Math.round(sats * (1 + band / 100)))}`
: "% of today's BCH rate";
}
$("sell-mode").addEventListener("change", paintSellMode);
$("sell-band").addEventListener("input", paintBand);
function paintSell() { function paintSell() {
const st = $("sell-status"); const st = $("sell-status");
paintSellMode();
$("sell-rate").textContent = `1 BCH ≈ ${fmtUsd(Math.round(1e8 / RATE() * 100) / 100)}${window.siriusPricing?.priceInfo ? " · " + window.siriusPricing.priceInfo() : ""}`; $("sell-rate").textContent = `1 BCH ≈ ${fmtUsd(Math.round(1e8 / RATE() * 100) / 100)}${window.siriusPricing?.priceInfo ? " · " + window.siriusPricing.priceInfo() : ""}`;
if (activeListing) { if (activeListing) {
st.className = "status ok"; st.textContent = "This name is listed on the market."; st.className = "status ok"; st.textContent = "This name is listed on the market.";
$("sell-form").classList.add("hidden"); $("sell-active").classList.remove("hidden"); $("sell-form").classList.add("hidden"); $("sell-active").classList.remove("hidden");
const p = BigInt(activeListing.price_sats); const p = BigInt(activeListing.price_sats || 0);
$("sell-kv").innerHTML = [["Price", `${esc(fmtBch(p))} (≈ ${esc(fmtUsd(Math.round(Number(p) / RATE() * 100) / 100))})`], ["Listed", esc(new Date(activeListing.created_at).toLocaleString())], ["Offer", `<code>${esc(activeListing.partial_tx.slice(0, 40))}…</code>`], ["Market page", `<a href="./market/" target="_blank" rel="noopener">open →</a>`]] const priceRow = activeListing.kind === "usd"
? ["Price", `${esc(fmtUsd(activeListing.target_cents / 100))} fixed · today ≈ ${esc(fmtBch(Math.round(activeListing.target_cents / 100 * RATE())))} · band ${esc(fmtBch(activeListing.floor_sats))} – ${esc(fmtBch(activeListing.ceil_sats))}`]
: ["Price", `${esc(fmtBch(p))} (≈ ${esc(fmtUsd(Math.round(Number(p) / RATE() * 100) / 100))})`];
$("sell-kv").innerHTML = [priceRow, ["Listed", esc(new Date(activeListing.created_at).toLocaleString())], ["Offer", `<code>${esc(activeListing.partial_tx.slice(0, 40))}…</code>`], ["Market page", `<a href="./market/" target="_blank" rel="noopener">open →</a>`]]
.map(([k, v]) => `<div class="k">${k}</div><div class="v">${v}</div>`).join(""); .map(([k, v]) => `<div class="k">${k}</div><div class="v">${v}</div>`).join("");
} else { } else {
st.className = "status"; st.textContent = "Not for sale. Set a price to list it."; st.className = "status"; st.textContent = "Not for sale. Set a price to list it.";
@ -705,7 +725,7 @@ async function loadListing(name) {
if (current.name === name) paintSell(); if (current.name === name) paintSell();
} }
const bchStr = (sats) => (Number(sats) / 1e8).toFixed(8).replace(/0+$/, "").replace(/\.$/, ""); const bchStr = (sats) => (Number(sats) / 1e8).toFixed(8).replace(/0+$/, "").replace(/\.$/, "");
$("sell-usd").addEventListener("input", () => { const v = Number(String($("sell-usd").value).replace(/[^\d.]/g, "")); $("sell-bch").value = v > 0 ? bchStr(Math.round(v * RATE())) : ""; }); $("sell-usd").addEventListener("input", () => { const v = Number(String($("sell-usd").value).replace(/[^\d.]/g, "")); $("sell-bch").value = v > 0 ? bchStr(Math.round(v * RATE())) : ""; paintBand(); });
$("sell-bch").addEventListener("input", () => { const b = Number(String($("sell-bch").value).replace(/[^\d.]/g, "")); $("sell-usd").value = b > 0 ? String(Math.round(b * 1e8 / RATE() * 100) / 100) : ""; }); $("sell-bch").addEventListener("input", () => { const b = Number(String($("sell-bch").value).replace(/[^\d.]/g, "")); $("sell-usd").value = b > 0 ? String(Math.round(b * 1e8 / RATE() * 100) / 100) : ""; });
$("sell-list").addEventListener("click", async () => { $("sell-list").addEventListener("click", async () => {
if (!current.entry || !wallet) return; if (!current.entry || !wallet) return;
@ -721,6 +741,33 @@ $("sell-list").addEventListener("click", async () => {
const commitment = [...new TextEncoder().encode(raw)].map((b) => b.toString(16).padStart(2, "0")).join(""); const commitment = [...new TextEncoder().encode(raw)].map((b) => b.toString(16).padStart(2, "0")).join("");
const cert = utxos.find((u) => u.token?.category === current.entry.category && u.token?.nft?.commitment === commitment); const cert = utxos.find((u) => u.token?.category === current.entry.category && u.token?.nft?.commitment === commitment);
if (!cert) throw new Error("certificate UTXO not found in this wallet"); if (!cert) throw new Error("certificate UTXO not found in this wallet");
if (sellMode() === "usd") {
// Dollar price: certificate goes into the UsdListing covenant.
const usd = Number(String($("sell-usd").value).replace(/[^\d.]/g, "")) || 0;
if (usd < 0.5) throw new Error("set a dollar price of at least $0.50");
const band = Math.min(90, Math.max(5, Number(String($("sell-band").value).replace(/\D/g, "")) || 25));
const targetCents = Math.round(usd * 100);
const nowSats = Math.round(usd * RATE());
const floorSats = Math.max(10000, Math.round(nowSats * (1 - band / 100))), ceilSats = Math.round(nowSats * (1 + band / 100));
const orc = await (await fetch(`${API}/api/price/oracle`, { cache: "no-store" })).json();
if (!orc.pubkey) throw new Error("the price oracle is not available right now");
push(`moving the certificate into the contract · $${usd} · band ${fmtBch(floorSats)} – ${fmtBch(ceilSats)}`);
const built = BNS.buildUsdListingTx({ name: current.name, certificateUtxo: cert, utxos, sellerAddress: cert.address, oraclePk: orc.pubkey, targetCents, floorSats, ceilSats, records: current.entry.records || {}, addressToLockingBytecode: BNS.addressToLockingBytecode });
const signed = BNS.signInputs(built.transaction, built.sourceOutputs, (h) => wallet.keyFor(h));
const txid = await BNS.broadcast(el, signed.hex);
push("broadcast " + txid.slice(0, 16) + "…");
const listing = { v: 2, kind: "usd", name: raw, seller: cert.address, seller_pkh: built.sellerPkh, category: current.entry.category, target_cents: targetCents, floor_sats: String(floorSats), ceil_sats: String(ceilSats), max_age_secs: BNS.DEFAULT_MAX_AGE_SECS, oracle_pk: orc.pubkey, redeem_hex: built.redeemHex, covenant_address: built.address, outpoint: { txid, vout: 0, satoshis: "1000" }, token: { category: cert.token.category, amount: String(cert.token.amount ?? 0), nft: { capability: cert.token.nft.capability, commitment: cert.token.nft.commitment } } };
let posted = null;
for (let i = 0; i < 6 && !posted; i++) {
const r = await fetch(`${API}/api/market`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(listing) });
const j = await r.json().catch(() => ({}));
if (r.ok) posted = j.listing; else if (r.status === 409) { push("waiting for the contract output to relay…"); await new Promise((res) => setTimeout(res, 4000)); } else throw new Error(j.error || `API ${r.status}`);
}
if (!posted) throw new Error("the market did not see the contract yet — press Refresh in a minute; the certificate is safe in the contract");
activeListing = posted; paintSell(); refreshBalance();
setMsg("sell-msg", `Listed at ${fmtUsd(usd)}. Buyers pay the dollar amount at the oracle rate; you receive between ${fmtBch(floorSats)} and ${fmtBch(ceilSats)}.`, "ok");
return;
}
push("found certificate · signing the offer (SINGLE|ANYONECANPAY)"); push("found certificate · signing the offer (SINGLE|ANYONECANPAY)");
const listing = BNS.buildListing({ name: current.name, certificateUtxo: cert, priceSats: sats, sellerAddress: cert.address, addressToLockingBytecode: BNS.addressToLockingBytecode, keyFor: (h) => wallet.keyFor(h) }); const listing = BNS.buildListing({ name: current.name, certificateUtxo: cert, priceSats: sats, sellerAddress: cert.address, addressToLockingBytecode: BNS.addressToLockingBytecode, keyFor: (h) => wallet.keyFor(h) });
const r = await fetch(`${API}/api/market`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(listing) }); const r = await fetch(`${API}/api/market`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(listing) });
@ -736,6 +783,22 @@ $("sell-cancel").addEventListener("click", async () => {
if (!current.entry || !wallet || !activeListing) return; if (!current.entry || !wallet || !activeListing) return;
if (!confirm(`Cancel the listing for ${current.name}? This also moves the certificate once so the signed offer becomes void.`)) return; if (!confirm(`Cancel the listing for ${current.name}? This also moves the certificate once so the signed offer becomes void.`)) return;
const push = logger("sell-log"); const btn = $("sell-cancel"); btn.disabled = true; btn.textContent = "Cancelling…"; setMsg("sell-msg", ""); const push = logger("sell-log"); const btn = $("sell-cancel"); btn.disabled = true; btn.textContent = "Cancelling…"; setMsg("sell-msg", "");
if (activeListing.kind === "usd") {
let el = null;
try {
el = await BNS.connect(); push("connected to chipnet");
const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses);
const built = BNS.buildUsdCancelTx({ listing: activeListing, sellerAddress: wallet.address, sellerTokenAddress: wallet.tokenAddress, utxos, records: current.entry.records || {}, addressToLockingBytecode: BNS.addressToLockingBytecode, keyFor: (h) => wallet.keyFor(h) });
const txid = await BNS.broadcast(el, built.hex);
push("certificate reclaimed · broadcast " + txid.slice(0, 16) + "…");
const h = await signedHeaders(`BNS-MARKET1\n${current.name}\n{ts}`);
await fetch(`${API}/api/market/${encodeURIComponent(current.name)}`, { method: "DELETE", headers: h }).catch(() => {});
activeListing = null; paintSell(); setTimeout(() => { loadHoldings(); refreshBalance(); }, 3000);
setMsg("sell-msg", "Listing cancelled. The certificate is back in your wallet; every quote issued for that listing is void.", "ok");
} catch (e) { setMsg("sell-msg", "Failed: " + (e.message || e), "err"); push("error: " + (e.message || e)); }
finally { btn.disabled = false; btn.textContent = "Cancel listing →"; try { el?.close?.(); } catch {} }
return;
}
try { try {
const h = await signedHeaders(`BNS-MARKET1\n${current.name}\n{ts}`); const h = await signedHeaders(`BNS-MARKET1\n${current.name}\n{ts}`);
const r = await fetch(`${API}/api/market/${encodeURIComponent(current.name)}`, { method: "DELETE", headers: h }); const r = await fetch(`${API}/api/market/${encodeURIComponent(current.name)}`, { method: "DELETE", headers: h });

View file

@ -22,7 +22,7 @@
(function () { (function () {
var STORE = "sirius-lang"; var STORE = "sirius-lang";
var CACHE = "sirius-i18n:"; var CACHE = "sirius-i18n:";
var VERSION = "20260920f"; // bump when dictionaries change var VERSION = "20260920g"; // bump when dictionaries change
var LANGS = { var LANGS = {
en: "English", en: "English",
de: "Deutsch", de: "Deutsch",

View file

@ -7,7 +7,7 @@
// stays valid while their certificate UTXO is unspent, so the gateway prunes // stays valid while their certificate UTXO is unspent, so the gateway prunes
// stale offers and the buyer's broadcast is the final arbiter. // stale offers and the buyer's broadcast is the final arbiter.
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market"; import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260920usd";
const API = "https://silentmode.st"; const API = "https://silentmode.st";
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
@ -16,6 +16,9 @@ const fmtInt = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const bch = (sats) => (window.siriusPricing?.formatBch || ((s) => (Number(s) / 1e8).toFixed(8).replace(/0+$/, "").replace(/\.$/, "") + " BCH"))(sats); const bch = (sats) => (window.siriusPricing?.formatBch || ((s) => (Number(s) / 1e8).toFixed(8).replace(/0+$/, "").replace(/\.$/, "") + " BCH"))(sats);
const bits = (sats) => (window.siriusPricing?.formatBits || ((s) => (Number(s) / 100).toLocaleString("en-US") + " bits"))(sats); const bits = (sats) => (window.siriusPricing?.formatBits || ((s) => (Number(s) / 100).toLocaleString("en-US") + " bits"))(sats);
const usd = (sats) => { const r = window.siriusPricing?.CHIPNET_SATS_PER_USD || 250000; const v = Number(sats) / r; return (window.siriusPricing?.formatUsd || ((n) => "$" + n.toFixed(2)))(Math.round(v * 100) / 100); }; const usd = (sats) => { const r = window.siriusPricing?.CHIPNET_SATS_PER_USD || 250000; const v = Number(sats) / r; return (window.siriusPricing?.formatUsd || ((n) => "$" + n.toFixed(2)))(Math.round(v * 100) / 100); };
const fmtUsdC = (cents) => (window.siriusPricing?.formatUsd || ((n) => "$" + n.toFixed(2)))(Math.round(Number(cents)) / 100);
const satsForCents = (cents) => Math.round(Number(cents) / 100 * (window.siriusPricing?.CHIPNET_SATS_PER_USD || 400000));
const clampSats = (sats, l) => Math.min(Number(l.ceil_sats), Math.max(Number(l.floor_sats), sats));
const shortAddr = (a) => (a ? a.replace(/^[^:]+:/, "").slice(0, 8) + "…" + a.slice(-5) : "—"); const shortAddr = (a) => (a ? a.replace(/^[^:]+:/, "").slice(0, 8) + "…" + a.slice(-5) : "—");
let listings = []; let listings = [];
@ -53,7 +56,8 @@ function render() {
const keep = tldSel.value; const keep = tldSel.value;
tldSel.innerHTML = `<option value="">All TLDs</option>` + tlds.map((t) => `<option value="${esc(t)}"${t === keep ? " selected" : ""}>.${esc(t)}</option>`).join(""); tldSel.innerHTML = `<option value="">All TLDs</option>` + tlds.map((t) => `<option value="${esc(t)}"${t === keep ? " selected" : ""}>.${esc(t)}</option>`).join("");
let rows = listings.filter((l) => (!q || l.name.includes(q)) && (!tldSel.value || l.tld === tldSel.value)); let rows = listings.filter((l) => (!q || l.name.includes(q)) && (!tldSel.value || l.tld === tldSel.value));
rows.sort((a, b) => sort === "name" ? a.name.localeCompare(b.name) : sort === "new" ? Date.parse(b.created_at) - Date.parse(a.created_at) : Number(a.price_sats) - Number(b.price_sats)); const priceOf = (l) => (l.kind === "usd" ? clampSats(satsForCents(l.target_cents), l) : Number(l.price_sats));
rows.sort((a, b) => sort === "name" ? a.name.localeCompare(b.name) : sort === "new" ? Date.parse(b.created_at) - Date.parse(a.created_at) : priceOf(a) - priceOf(b));
$("count").textContent = `${rows.length} of ${listings.length}`; $("count").textContent = `${rows.length} of ${listings.length}`;
const status = $("status"); const status = $("status");
document.querySelector(".toolbar").hidden = !listings.length; document.querySelector(".toolbar").hidden = !listings.length;
@ -72,7 +76,9 @@ function render() {
$("list").innerHTML = rows.length ? rows.map((l) => ` $("list").innerHTML = rows.length ? rows.map((l) => `
<div class="row"> <div class="row">
<div class="n">${esc(l.label)}.<span class="tld">${esc(l.tld)}</span> ${wallet && l.seller === wallet.address ? '<span class="badge">yours</span>' : ""}</div> <div class="n">${esc(l.label)}.<span class="tld">${esc(l.tld)}</span> ${wallet && l.seller === wallet.address ? '<span class="badge">yours</span>' : ""}</div>
<div class="price"><b>${esc(bch(l.price_sats))}</b><span>≈ ${esc(usd(l.price_sats))}</span></div> <div class="price">${l.kind === "usd"
? `<b>${esc(fmtUsdC(l.target_cents))}</b><span>fixed in USD · today ≈ ${esc(bch(satsForCents(l.target_cents)))}</span>`
: `<b>${esc(bch(l.price_sats))}</b><span>≈ ${esc(usd(l.price_sats))}</span>`}</div>
<div><button class="btn acid small" data-buy="${esc(l.name)}" ${wallet && l.seller === wallet.address ? "disabled" : ""}>Buy →</button></div> <div><button class="btn acid small" data-buy="${esc(l.name)}" ${wallet && l.seller === wallet.address ? "disabled" : ""}>Buy →</button></div>
<div class="meta"><span>seller ${esc(shortAddr(l.seller))}</span><span>·</span><span>listed ${esc(new Date(l.created_at).toLocaleDateString())}</span><span>·</span><a href="${API}/bns/${encodeURIComponent(l.name)}/" target="_blank" rel="noopener">view site →</a></div> <div class="meta"><span>seller ${esc(shortAddr(l.seller))}</span><span>·</span><span>listed ${esc(new Date(l.created_at).toLocaleDateString())}</span><span>·</span><a href="${API}/bns/${encodeURIComponent(l.name)}/" target="_blank" rel="noopener">view site →</a></div>
</div>`).join("") : `<div class="empty">Nothing matches that filter.</div>`; </div>`).join("") : `<div class="empty">Nothing matches that filter.</div>`;
@ -86,13 +92,15 @@ function push(t) { const log = $("buy-steps"); log.className = "steps on"; const
function openBuy(name) { function openBuy(name) {
selected = listings.find((l) => l.name === name); if (!selected) return; selected = listings.find((l) => l.name === name); if (!selected) return;
$("buy-name").textContent = name; $("buy-name").textContent = name;
const price = BigInt(selected.price_sats); const isUsd = selected.kind === "usd";
const check = BNS.verifyListing(selected, { category: selected.category }); const price = BigInt(isUsd ? clampSats(satsForCents(selected.target_cents), selected) : selected.price_sats);
const check = isUsd ? BNS.verifyUsdListing(selected) : BNS.verifyListing(selected, { category: selected.category });
$("buy-kv").innerHTML = [ $("buy-kv").innerHTML = [
["Price", `${esc(bch(price))} (≈ ${esc(usd(price))})`], isUsd ? ["Price", `${esc(fmtUsdC(selected.target_cents))} fixed · about ${esc(bch(price))} at today's rate (the exact amount is set by the oracle quote when you buy, between ${esc(bch(selected.floor_sats))} and ${esc(bch(selected.ceil_sats))})`]
: ["Price", `${esc(bch(price))} (≈ ${esc(usd(price))})`],
["Seller", esc(selected.seller)], ["Seller", esc(selected.seller)],
["You receive", "the name's certificate, to your token address"], ["You receive", "the name's certificate, to your token address"],
["Offer check", check.ok ? "seller signature valid" : "INVALID: " + esc(check.error)], ["Offer check", check.ok ? (isUsd ? "contract terms verified" : "seller signature valid") : "INVALID: " + esc(check.error)],
["Extra", "≈ 16 bits dust + fee"], ["Extra", "≈ 16 bits dust + fee"],
].map(([k, v]) => `<div class="k">${k}</div><div class="v">${v}</div>`).join(""); ].map(([k, v]) => `<div class="k">${k}</div><div class="v">${v}</div>`).join("");
$("buy-steps").className = "steps"; $("buy-steps").innerHTML = ""; setMsg(""); $("buy-steps").className = "steps"; $("buy-steps").innerHTML = ""; setMsg("");
@ -113,9 +121,19 @@ $("buy-go").addEventListener("click", async () => {
el = await BNS.connect(); push("connected to chipnet"); el = await BNS.connect(); push("connected to chipnet");
const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses); const utxos = await BNS.getUtxosForAddresses(el, wallet.watchedAddresses);
push(`wallet has ${utxos.filter((u) => !u.token).length} spendable coin(s)`); push(`wallet has ${utxos.filter((u) => !u.token).length} spendable coin(s)`);
const built = BNS.completeSale({ listing: selected, buyerAddress: wallet.address, buyerTokenAddress: wallet.tokenAddress, utxos, records: selected.records || {}, addressToLockingBytecode: BNS.addressToLockingBytecode }); let built, signed;
push(`built sale · price ${bch(built.costs.priceSats)} · fee ${bits(built.costs.chainFeeSats)}`); if (selected.kind === "usd") {
const signed = BNS.signInputs(built.transaction, built.sourceOutputs, (h) => wallet.keyFor(h), { skip: [built.sellerInputIndex] }); const q = await (await fetch(`${API}/api/price/oracle?txid=${encodeURIComponent(selected.outpoint.txid)}`, { cache: "no-store" })).json();
if (!q.message) throw new Error(q.error || "no oracle quote");
push(`oracle quote: 1 BCH = $${(q.price_cents / 100).toFixed(2)} (median of ${q.median_of} exchanges)`);
built = BNS.buildUsdBuyTx({ listing: selected, quote: q, buyerAddress: wallet.address, buyerTokenAddress: wallet.tokenAddress, utxos, records: selected.records || {}, addressToLockingBytecode: BNS.addressToLockingBytecode });
push(`you pay ${bch(built.priceSats)} · fee ${bits(built.fee)}`);
signed = BNS.signInputs(built.transaction, built.sourceOutputs, (h) => wallet.keyFor(h), { skip: [built.covenantInputIndex] });
} else {
built = BNS.completeSale({ listing: selected, buyerAddress: wallet.address, buyerTokenAddress: wallet.tokenAddress, utxos, records: selected.records || {}, addressToLockingBytecode: BNS.addressToLockingBytecode });
push(`built sale · price ${bch(built.costs.priceSats)} · fee ${bits(built.costs.chainFeeSats)}`);
signed = BNS.signInputs(built.transaction, built.sourceOutputs, (h) => wallet.keyFor(h), { skip: [built.sellerInputIndex] });
}
push("signed your inputs (the seller's signature is already in place)"); push("signed your inputs (the seller's signature is already in place)");
const txid = await BNS.broadcast(el, signed.hex); const txid = await BNS.broadcast(el, signed.hex);
push("broadcast: " + txid); push("broadcast: " + txid);

View file

@ -7,7 +7,7 @@
// Injects its own modal HTML into <body> and its own CSS into <head> on load // Injects its own modal HTML into <body> and its own CSS into <head> on load
// so the host page needs nothing but a single script include. // so the host page needs nothing but a single script include.
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market"; import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260920usd";
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[c])); const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;" }[c]));

View file

@ -11,7 +11,7 @@
// The gateway never holds a key: it verifies each upload's signature // The gateway never holds a key: it verifies each upload's signature
// against the current NFT owner and refuses writes outside bns/<name>/. // against the current NFT owner and refuses writes outside bns/<name>/.
import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260917market"; import * as BNS from "https://silentmode.st/js/bns-register.js?v=20260920usd";
import { initAssistant } from "./studio-ai.js?v=20260920ai"; import { initAssistant } from "./studio-ai.js?v=20260920ai";
const API = "https://silentmode.st"; const API = "https://silentmode.st";
@ -443,9 +443,17 @@ async function publish() {
await putFile("_studio.json", data, "application/json"); await putFile("_studio.json", data, "application/json");
pushStep("saved editor project (_studio.json)"); pushStep("saved editor project (_studio.json)");
dirty = false; $("btn-draft").disabled = true; dirty = false; $("btn-draft").disabled = true;
// Point the name at the folder if it does not already. // Point the name at the folder if it does not already. Re-read the
// registry first: `entry` from boot can be stale or missing (a name
// indexed after the page opened), and a stale view here means a
// needless on-chain transaction on every publish.
try { const r = await fetch(`${API}/api/name/${encodeURIComponent(name)}`, { cache: "no-store" }); if (r.ok) entry = await r.json(); } catch {}
const rec = entry?.records || {}; const rec = entry?.records || {};
if (rec.s3 !== folder) { const norm = (v) => String(v || "").trim().replace(/\/+$/, "") + "/";
const pointsHere = norm(rec.s3) === folder;
if (!pointsHere && !entry) {
pushStep("the registry has not indexed this name yet — files are up; pointing the name is skipped until it appears (or set Hosting in the dashboard)");
} else if (!pointsHere) {
pushStep(`name points at ${rec.s3 ? `"${rec.s3}"` : "nothing"} — setting s3 = ${folder} on chain`); pushStep(`name points at ${rec.s3 ? `"${rec.s3}"` : "nothing"} — setting s3 = ${folder} on chain`);
const next = { ...rec, s3: folder }; const next = { ...rec, s3: folder };
delete next.h; // inline HTML would shadow the Sia site delete next.h; // inline HTML would shadow the Sia site

View file

@ -97,7 +97,7 @@
footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem} footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
@media (max-width:560px){.row{grid-template-columns:1fr auto}.row .price{grid-column:1/-1;text-align:left}} @media (max-width:560px){.row{grid-template-columns:1fr auto}.row .price{grid-column:1/-1;text-align:left}}
</style> </style>
<script src="../js/i18n.js?v=20260920f"></script> <script src="../js/i18n.js?v=20260920g"></script>
</head> </head>
<body> <body>
<nav class="topnav"> <nav class="topnav">
@ -147,11 +147,11 @@
<footer id="site-footer"></footer> <footer id="site-footer"></footer>
<script type="importmap"> <script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } } { "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js?v=20260920usd" } }
</script> </script>
<script src="../js/session.js?v=20260917dash"></script> <script src="../js/session.js?v=20260917dash"></script>
<script src="../js/pricing.js?v=20260920bits"></script> <script src="../js/pricing.js?v=20260920bits"></script>
<script type="module" src="../js/market.js?v=20260920bits"></script> <script type="module" src="../js/market.js?v=20260920usd"></script>
<script defer src="../js/theme.js?v=20260920compact"></script> <script defer src="../js/theme.js?v=20260920compact"></script>
<script defer src="../js/site-footer.js?v=20260917market"></script> <script defer src="../js/site-footer.js?v=20260917market"></script>
<script defer src="../js/profile-menu.js?v=20260920bits"></script> <script defer src="../js/profile-menu.js?v=20260920bits"></script>

View file

@ -259,7 +259,7 @@
.addrow{grid-template-columns:1fr 1fr} .addrow{grid-template-columns:1fr 1fr}
} }
</style> </style>
<script src="./js/i18n.js?v=20260920f"></script> <script src="./js/i18n.js?v=20260920g"></script>
</head> </head>
<body> <body>
@ -360,11 +360,23 @@
<p class="hint">You sign an offer once: "whoever pays me this price gets the certificate". The offer is stored by the gateway and shown on the <a href="./market" target="_blank" rel="noopener">market</a>. A buyer completes it in one transaction — you are paid in the same transaction that moves the name, or not at all. No escrow, nothing to trust.</p> <p class="hint">You sign an offer once: "whoever pays me this price gets the certificate". The offer is stored by the gateway and shown on the <a href="./market" target="_blank" rel="noopener">market</a>. A buyer completes it in one transaction — you are paid in the same transaction that moves the name, or not at all. No escrow, nothing to trust.</p>
<div class="status" id="sell-status">Checking the market…</div> <div class="status" id="sell-status">Checking the market…</div>
<div id="sell-form"> <div id="sell-form">
<label style="margin-top:0">Price is fixed in</label>
<select id="sell-mode" style="max-width:360px">
<option value="usd">US dollars — the BCH amount follows the market</option>
<option value="bch">BCH — the dollar value follows the market</option>
</select>
<div class="row" style="align-items:flex-end"> <div class="row" style="align-items:flex-end">
<div><label style="margin-top:0">Price (USD)</label><input type="text" id="sell-usd" placeholder="25" inputmode="decimal" style="max-width:160px"></div> <div><label style="margin-top:0">Price (USD)</label><input type="text" id="sell-usd" placeholder="25" inputmode="decimal" style="max-width:160px"></div>
<div><label style="margin-top:0">Price (BCH)</label><input type="text" id="sell-bch" placeholder="0.0625" inputmode="decimal" style="max-width:200px"></div> <div><label style="margin-top:0">Price (BCH)</label><input type="text" id="sell-bch" placeholder="0.0625" inputmode="decimal" style="max-width:200px"></div>
<span class="dim" id="sell-rate"></span> <span class="dim" id="sell-rate"></span>
</div> </div>
<div id="sell-usd-terms">
<div class="row" style="align-items:flex-end">
<div><label style="margin-top:0">Accept between</label><input type="text" id="sell-band" value="25" inputmode="numeric" style="max-width:90px"></div>
<span class="dim" id="sell-band-hint"></span>
</div>
<p class="hint">A dollar price lives in a small contract that holds the certificate while it is for sale. A buyer pays the dollar amount at the Sirius.X price oracle's rate, and the contract keeps the payout inside this band whatever the oracle says, so a wrong or old quote can never take the price outside it. Your name's records are frozen while listed; cancelling returns the certificate.</p>
</div>
<div class="row end"><button class="btn acid" id="sell-list">List for sale →</button></div> <div class="row end"><button class="btn acid" id="sell-list">List for sale →</button></div>
</div> </div>
<div id="sell-active" class="hidden"> <div id="sell-active" class="hidden">
@ -677,15 +689,15 @@
<footer id="site-footer"></footer> <footer id="site-footer"></footer>
<script type="importmap"> <script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } } { "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js?v=20260920usd" } }
</script> </script>
<script src="./js/session.js?v=20260917dash"></script> <script src="./js/session.js?v=20260917dash"></script>
<script src="./js/pin-escrow.js?v=20260909pin"></script> <script src="./js/pin-escrow.js?v=20260909pin"></script>
<script src="./js/pricing.js?v=20260920bits"></script> <script src="./js/pricing.js?v=20260920bits"></script>
<script type="module" src="./js/dashboard.js?v=20260920s3"></script> <script type="module" src="./js/dashboard.js?v=20260920usd2"></script>
<script defer src="./js/theme.js?v=20260920compact"></script> <script defer src="./js/theme.js?v=20260920compact"></script>
<script defer src="./js/site-footer.js?v=20260917market"></script> <script defer src="./js/site-footer.js?v=20260917market"></script>
<script type="module" src="./js/register-flow.js?v=20260920bits"></script> <script type="module" src="./js/register-flow.js?v=20260920usd"></script>
<script defer src="./js/profile-menu.js?v=20260920bits"></script> <script defer src="./js/profile-menu.js?v=20260920bits"></script>
</body> </body>
</html> </html>

View file

@ -131,7 +131,7 @@
.muted{color:var(--mut);font-size:13px} .muted{color:var(--mut);font-size:13px}
code{font-family:var(--mono);background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-size:12px} code{font-family:var(--mono);background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-size:12px}
</style> </style>
<script src="./js/i18n.js?v=20260920f"></script> <script src="./js/i18n.js?v=20260920g"></script>
</head> </head>
<body> <body>
<div class="bar"> <div class="bar">
@ -229,7 +229,7 @@
</div> </div>
<script type="importmap"> <script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } } { "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js?v=20260920usd" } }
</script> </script>
<script src="./js/session.js?v=20260917dash"></script> <script src="./js/session.js?v=20260917dash"></script>
<script src="./vendor/grapesjs/grapes.min.js?v=0.23.6"></script> <script src="./vendor/grapesjs/grapes.min.js?v=0.23.6"></script>
@ -244,6 +244,6 @@
<script src="./vendor/grapesjs/style-bg.js?v=2.0.2"></script> <script src="./vendor/grapesjs/style-bg.js?v=2.0.2"></script>
<script src="./vendor/grapesjs/touch.js?v=0.1.1"></script> <script src="./vendor/grapesjs/touch.js?v=0.1.1"></script>
<script src="./vendor/grapesjs/parser-postcss.js?v=1.0.3"></script> <script src="./vendor/grapesjs/parser-postcss.js?v=1.0.3"></script>
<script type="module" src="./js/studio.js?v=20260920pc"></script> <script type="module" src="./js/studio.js?v=20260920usd"></script>
</body> </body>
</html> </html>

View file

@ -1,248 +1,259 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Theseus Navigator — the browser that speaks Bitcoin Cash names — Sirius.X</title> <title>Theseus Navigator — the browser that speaks Bitcoin Cash names — Sirius.X</title>
<meta name="description" content="Theseus Navigator is a Chromium-based browser with native Bitcoin Cash Domain Names resolution built in — no OS-level daemon, no DNS shim, no root-store install. Download for Windows."> <meta name="description" content="Theseus Navigator is a Chromium-based browser with native Bitcoin Cash Domain Names resolution built in — no OS-level daemon, no DNS shim, no root-store install. Download for Windows.">
<!-- Theseus.X has its own compass-mark favicon and logo — reuse them here so <!-- Theseus.X has its own compass-mark favicon and logo — reuse them here so
tabs on this subpage read as Theseus (compass), not Sirius (star), and tabs on this subpage read as Theseus (compass), not Sirius (star), and
shared-link previews carry Theseus's own OG image. Copied from shared-link previews carry Theseus's own OG image. Copied from
silentmode.st/bns/theseus.x/assets/ to keep sirius.x self-contained. --> silentmode.st/bns/theseus.x/assets/ to keep sirius.x self-contained. -->
<link rel="icon" href="./assets/favicon.svg" type="image/svg+xml"> <link rel="icon" href="./assets/favicon.svg" type="image/svg+xml">
<link rel="mask-icon" href="./assets/favicon.svg" color="#d6ff3d"> <link rel="mask-icon" href="./assets/favicon.svg" color="#d6ff3d">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
<meta property="og:site_name" content="Theseus Navigator"> <meta property="og:site_name" content="Theseus Navigator">
<meta property="og:title" content="Theseus Navigator — the browser that speaks Bitcoin Cash names"> <meta property="og:title" content="Theseus Navigator — the browser that speaks Bitcoin Cash names">
<meta property="og:description" content="Native .bch and BCNR name resolution built into a Chromium browser. No OS-level daemon, no root-store install. Download for Windows."> <meta property="og:description" content="Native .bch and BCNR name resolution built into a Chromium browser. No OS-level daemon, no root-store install. Download for Windows.">
<meta property="og:url" content="https://silentmode.st/sirius-x/theseus/"> <meta property="og:url" content="https://silentmode.st/sirius-x/theseus/">
<meta property="og:image" content="https://silentmode.st/sirius-x/theseus/assets/logo.svg"> <meta property="og:image" content="https://silentmode.st/sirius-x/theseus/assets/logo.svg">
<meta name="theme-color" content="#0b0e14"> <meta name="theme-color" content="#0b0e14">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <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> <style>
:root{ :root{
--bg:#050810; --panel:#141a24; --panel2:#0a0f1c; --line:rgba(255,255,255,.06); --bg:#050810; --panel:#141a24; --panel2:#0a0f1c; --line:rgba(255,255,255,.06);
--ink:#f1f4fa; --mut:#b8c2d4; --dim:#6a7488; --acid:#d6ff3d; --ink:#f1f4fa; --mut:#b8c2d4; --dim:#6a7488; --acid:#d6ff3d;
--chain:#d6ff3d; --sia:#b39ddb; --srv:#4fd1a5; --ok:#4fd1a5; --taken:#f6768a; --warn:#ffc75f; --chain:#d6ff3d; --sia:#b39ddb; --srv:#4fd1a5; --ok:#4fd1a5; --taken:#f6768a; --warn:#ffc75f;
} }
*{box-sizing:border-box} *{box-sizing:border-box}
html{scroll-behavior:smooth} html{scroll-behavior:smooth}
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); 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} color:var(--ink);font:16px/1.65 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-height:100vh}
a{color:var(--acid)} a{color:var(--acid)}
.wrap{max-width:980px;margin:0 auto;padding:0 1.2rem} .wrap{max-width:980px;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; .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)} 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{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 .brand .g{color:var(--acid)}
.topnav a{color:var(--mut);text-decoration:none;padding:6px 12px;border-radius:8px;font-size:14px} .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:hover{color:var(--ink);background:var(--panel)}
.topnav a.here{color:var(--acid);background:rgba(214,255,61,.08)} .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{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)} .topnav a.portal:hover{background:rgba(214,255,61,.18)}
/* Adaptive side nav — same shape as index.html and Theseus.X */ /* Adaptive side nav — same shape as index.html and Theseus.X */
:root{--sn-w:200px} :root{--sn-w:200px}
.sidenav{position:fixed;top:56px;left:0;bottom:0;width:var(--sn-w);z-index:15; .sidenav{position:fixed;top:56px;left:0;bottom:0;width:var(--sn-w);z-index:15;
background:transparent;padding:32px 12px 22px 18px; background:transparent;padding:32px 12px 22px 18px;
overflow-y:auto;pointer-events:none;transition:width .15s ease} 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; .sidenav a{display:flex;align-items:center;gap:14px;color:var(--mut);text-decoration:none;
padding:9px 12px;border-radius:10px; padding:9px 12px;border-radius:10px;
font-size:14.5px;font-weight:500;opacity:.35;pointer-events:auto; font-size:14.5px;font-weight:500;opacity:.35;pointer-events:auto;
transition:opacity .12s,color .12s,background .12s,backdrop-filter .12s} 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; .sidenav a .ic{flex:none;width:22px;font-size:18px;line-height:1;text-align:center;
display:none;justify-content:center;align-items:center} display:none;justify-content:center;align-items:center}
.sidenav a .lb{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .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: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{opacity:.85;color:var(--acid)}
.sidenav a.active:hover{opacity:1} .sidenav a.active:hover{opacity:1}
section{scroll-margin-top:70px} section{scroll-margin-top:70px}
@media (max-width:1180px){ @media (max-width:1180px){
:root{--sn-w:64px} :root{--sn-w:64px}
.sidenav{padding:22px 6px} .sidenav{padding:22px 6px}
.sidenav a{justify-content:center;padding:12px 4px;gap:0} .sidenav a{justify-content:center;padding:12px 4px;gap:0}
.sidenav a .ic{display:inline-flex} .sidenav a .ic{display:inline-flex}
.sidenav a .lb{display:none} .sidenav a .lb{display:none}
} }
@media (max-width:720px){ @media (max-width:720px){
:root{--sn-w:0} :root{--sn-w:0}
body{padding-bottom:64px} body{padding-bottom:64px}
.sidenav{top:auto;bottom:0;left:0;right:0;width:100%;height:auto; .sidenav{top:auto;bottom:0;left:0;right:0;width:100%;height:auto;
padding:4px;flex-direction:row;background:rgba(11,14,20,.52); padding:4px;flex-direction:row;background:rgba(11,14,20,.52);
display:flex;justify-content:space-around;overflow-x:auto} display:flex;justify-content:space-around;overflow-x:auto}
.sidenav a{flex:1;min-width:56px;padding:10px 6px;justify-content:center;gap:0;opacity:.5} .sidenav a{flex:1;min-width:56px;padding:10px 6px;justify-content:center;gap:0;opacity:.5}
.sidenav a .ic{display:inline-flex} .sidenav a .ic{display:inline-flex}
.sidenav a .lb{display:none} .sidenav a .lb{display:none}
} }
header.hero{text-align:center;padding:5rem 1.2rem 1rem} header.hero{text-align:center;padding:5rem 1.2rem 1rem}
.mark{font-size:56px;line-height:1} .mark{font-size:56px;line-height:1}
h1{font-size:clamp(2rem,5.5vw,2.9rem);margin:.4rem 0 .3rem} h1{font-size:clamp(2rem,5.5vw,2.9rem);margin:.4rem 0 .3rem}
h1 .g{color:var(--acid)} h1 .g{color:var(--acid)}
.tag{color:var(--mut);font-size:1.05rem;max-width:660px;margin:0 auto} .tag{color:var(--mut);font-size:1.05rem;max-width:660px;margin:0 auto}
.cta{margin-top:1.8rem;display:flex;gap:10px;justify-content:center;flex-wrap:wrap} .cta{margin-top:1.8rem;display:flex;gap:10px;justify-content:center;flex-wrap:wrap}
.btn{display:inline-block;text-decoration:none;padding:11px 20px;border-radius:10px;background:#4b7bec;color:#fff;font-size:14px;border:none;cursor:pointer;font-family:inherit;transition:filter .1s} .btn{display:inline-block;text-decoration:none;padding:11px 20px;border-radius:10px;background:#4b7bec;color:#fff;font-size:14px;border:none;cursor:pointer;font-family:inherit;transition:filter .1s}
.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink)} .btn.ghost{background:transparent;border:1px solid var(--line);color:var(--ink)}
.btn.acid{background:var(--acid);color:#0b0e14;font-weight:600} .btn.acid{background:var(--acid);color:#0b0e14;font-weight:600}
.btn:hover{filter:brightness(1.12)} .btn:hover{filter:brightness(1.12)}
section{padding:2.6rem 0;border-top:1px solid var(--line);margin-top:2.5rem} section{padding:2.6rem 0;border-top:1px solid var(--line);margin-top:2.5rem}
section:first-of-type{border-top:none;margin-top:1.5rem} section:first-of-type{border-top:none;margin-top:1.5rem}
h2{font-size:1.5rem;margin:0 0 .4rem} h2{font-size:1.5rem;margin:0 0 .4rem}
h3{font-size:1.1rem;margin:0 0 .35rem} h3{font-size:1.1rem;margin:0 0 .35rem}
.lede{color:var(--mut);margin:0 0 1.6rem;max-width:720px} .lede{color:var(--mut);margin:0 0 1.6rem;max-width:720px}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px} .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px}
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px 20px} .card{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px 20px}
.card h3{margin:0 0 .4rem;display:flex;align-items:center;gap:8px} .card h3{margin:0 0 .4rem;display:flex;align-items:center;gap:8px}
.card p{margin:0;color:var(--mut);font-size:14px} .card p{margin:0;color:var(--mut);font-size:14px}
.card .cta{margin-top:14px;justify-content:flex-start} .card .cta{margin-top:14px;justify-content:flex-start}
.card .sub{color:var(--dim);font-size:12.5px;margin-top:10px;font-family:ui-monospace,monospace;word-break:break-all} .card .sub{color:var(--dim);font-size:12.5px;margin-top:10px;font-family:ui-monospace,monospace;word-break:break-all}
.dot{width:9px;height:9px;border-radius:50%;display:inline-block} .dot{width:9px;height:9px;border-radius:50%;display:inline-block}
.d-chain{background:var(--chain)} .d-sia{background:var(--sia)} .d-srv{background:var(--srv)} .d-chain{background:var(--chain)} .d-sia{background:var(--sia)} .d-srv{background:var(--srv)}
.note{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--acid); .note{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--acid);
border-radius:10px;padding:14px 18px;color:var(--mut);font-size:14px;margin-top:1.4rem} border-radius:10px;padding:14px 18px;color:var(--mut);font-size:14px;margin-top:1.4rem}
.note.warn{border-left-color:var(--warn)} .note.warn{border-left-color:var(--warn)}
.note b{color:var(--ink)} .note b{color:var(--ink)}
code,.mono{background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:13px} code,.mono{background:#0e131b;border:1px solid var(--line);border-radius:6px;padding:1px 6px;font-family:ui-monospace,monospace;font-size:13px}
pre{background:#0e131b;border:1px solid var(--line);border-radius:10px;padding:12px 14px;overflow-x:auto; pre{background:#0e131b;border:1px solid var(--line);border-radius:10px;padding:12px 14px;overflow-x:auto;
font-family:ui-monospace,monospace;font-size:13px;color:var(--ink);margin:1rem 0} font-family:ui-monospace,monospace;font-size:13px;color:var(--ink);margin:1rem 0}
ol.steps{counter-reset:s;list-style:none;padding:0;margin:1.2rem 0 0;display:grid;gap:10px} ol.steps{counter-reset:s;list-style:none;padding:0;margin:1.2rem 0 0;display:grid;gap:10px}
ol.steps li{counter-increment:s;display:grid;grid-template-columns:30px 1fr;gap:14px;align-items:start; ol.steps li{counter-increment:s;display:grid;grid-template-columns:30px 1fr;gap:14px;align-items:start;
background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:13px 16px} background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:13px 16px}
ol.steps li::before{content:counter(s);grid-column:1;grid-row:1; ol.steps li::before{content:counter(s);grid-column:1;grid-row:1;
display:grid;place-items:center;width:24px;height:24px;border-radius:50%; display:grid;place-items:center;width:24px;height:24px;border-radius:50%;
background:rgba(214,255,61,.14);color:var(--acid);font-size:12.5px;font-weight:700} background:rgba(214,255,61,.14);color:var(--acid);font-size:12.5px;font-weight:700}
ol.steps li > *{grid-column:2;min-width:0} ol.steps li > *{grid-column:2;min-width:0}
ol.steps b{color:var(--ink)} ol.steps span{color:var(--mut);font-size:14px} ol.steps b{color:var(--ink)} ol.steps span{color:var(--mut);font-size:14px}
.pill{display:inline-block;font-size:11px;padding:2px 8px;border-radius:999px;background:rgba(214,255,61,.13);color:var(--acid);margin-left:8px;vertical-align:middle;font-weight:500} .pill{display:inline-block;font-size:11px;padding:2px 8px;border-radius:999px;background:rgba(214,255,61,.13);color:var(--acid);margin-left:8px;vertical-align:middle;font-weight:500}
footer{border-top:1px solid var(--line);padding:2.4rem 0 3.5rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem} footer{border-top:1px solid var(--line);padding:2.4rem 0 3.5rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
/* Deviant-family fonts + Fraunces/Ubuntu reader toggle. */ /* Deviant-family fonts + Fraunces/Ubuntu reader toggle. */
h1,h2,h3{font-family:'Fraunces',Georgia,serif;font-feature-settings:"ss01","ss02"} h1,h2,h3{font-family:'Fraunces',Georgia,serif;font-feature-settings:"ss01","ss02"}
code,.mono{font-family:'JetBrains Mono',ui-monospace,monospace} 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"} 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"] 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} 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{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{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:hover{color:var(--ink)}
.font-picker button.active{background:var(--acid);color:var(--bg);font-weight:600} .font-picker button.active{background:var(--acid);color:var(--bg);font-weight:600}
</style> </style>
<script type="importmap"> <script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } } { "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js?v=20260920usd" } }
</script> </script>
</head> </head>
<body> <body>
<nav class="topnav"> <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><span class="g">Sirius.X</span></a> <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><span class="g">Sirius.X</span></a>
<a href="../tld.html">🌐 TLD</a> <a href="../tld.html">🌐 TLD</a>
<a href="../market/">🛒 Market</a> <a href="../market/">🛒 Market</a>
<a href="./" class="here"><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"/><path d="M6 32 L32 27 L58 32 L32 37 Z" fill="#d6ff3d" opacity=".55"/><circle cx="32" cy="32" r="3" fill="#0b0e14"/></svg>Theseus<span class="g">.X</span></a> <a href="./" class="here"><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"/><path d="M6 32 L32 27 L58 32 L32 37 Z" fill="#d6ff3d" opacity=".55"/><circle cx="32" cy="32" r="3" fill="#0b0e14"/></svg>Theseus<span class="g">.X</span></a>
<a href="../portal.html" class="portal">🔑 Sign in</a> <a href="../portal.html" class="portal">🔑 Sign in</a>
</nav> </nav>
<aside class="sidenav" aria-label="On this page"> <aside class="sidenav" aria-label="On this page">
<a href="#download" title="Download" ><span class="ic" aria-hidden="true">⬇</span><span class="lb">Download</span></a> <a href="#download" title="Download" ><span class="ic" aria-hidden="true">⬇</span><span class="lb">Download</span></a>
<a href="#how" title="How Theseus works" ><span class="ic" aria-hidden="true">⚙️</span><span class="lb">How it works</span></a> <a href="#how" title="How Theseus works" ><span class="ic" aria-hidden="true">⚙️</span><span class="lb">How it works</span></a>
<a href="#verify" title="Verify a download" ><span class="ic" aria-hidden="true">🔐</span><span class="lb">Verify</span></a> <a href="#verify" title="Verify a download" ><span class="ic" aria-hidden="true">🔐</span><span class="lb">Verify</span></a>
</aside> </aside>
<header class="hero"> <header class="hero">
<div class="mark"><img src="./assets/favicon.svg" alt="" width="72" height="72" style="vertical-align:middle;filter:drop-shadow(0 0 24px rgba(214,255,61,.15))"></div> <div class="mark"><img src="./assets/favicon.svg" alt="" width="72" height="72" style="vertical-align:middle;filter:drop-shadow(0 0 24px rgba(214,255,61,.15))"></div>
<h1>Theseus <span class="g">Navigator</span></h1> <h1>Theseus <span class="g">Navigator</span></h1>
<p class="tag">A Chromium-based browser with native <b>Bitcoin Cash Domain Names</b> resolution built in. <p class="tag">A Chromium-based browser with native <b>Bitcoin Cash Domain Names</b> resolution built in.
No OS-level daemon, no DNS shim, no root-store install. Type <code>hello.bch</code> in the No OS-level daemon, no DNS shim, no root-store install. Type <code>hello.bch</code> in the
address bar and it just works.</p> address bar and it just works.</p>
<div class="cta"> <div class="cta">
<a class="btn acid" href="#download">Download for Windows</a> <a class="btn acid" href="#download">Download for Windows</a>
<a class="btn ghost" href="#how">How it works</a> <a class="btn ghost" href="#how">How it works</a>
</div> </div>
</header> </header>
<div class="wrap"> <div class="wrap">
<section id="download"> <section id="download">
<h2>Download</h2> <h2>Download</h2>
<p class="lede">Theseus ships from its own site — <a href="https://silentmode.st/bns/theseus.x/#download" rel="noopener"><b>theseus.x</b></a> — so the version and checksums you see are always the current build, not a snapshot cached here. Windows installer + portable, plus Ariadne Resolver (lighter, no-browser).</p> <p class="lede">Theseus ships from its own site — <a href="https://silentmode.st/bns/theseus.x/#download" rel="noopener"><b>theseus.x</b></a> — so the version and checksums you see are always the current build, not a snapshot cached here. Windows installer + portable, plus Ariadne Resolver (lighter, no-browser).</p>
<div class="cta" style="justify-content:flex-start;margin-top:1.2rem"> <div class="cta" style="justify-content:flex-start;margin-top:1.2rem">
<a class="btn acid" href="https://silentmode.st/bns/theseus.x/#download" rel="noopener">Open theseus.x downloads →</a> <a class="btn acid" href="https://silentmode.st/bns/theseus.x/#download" rel="noopener">Open theseus.x downloads →</a>
<a class="btn ghost" href="https://silentmode.st/bns/theseus.x/" rel="noopener">Full Theseus site</a> <a class="btn ghost" href="https://silentmode.st/bns/theseus.x/" rel="noopener">Full Theseus site</a>
</div> </div>
<div class="note" style="margin-top:1.4rem"><b>Why not list versions here?</b> Theseus updates weekly; a hardcoded version on this page went stale within days last time. theseus.x is the single source of truth — its own operator ships the current build, and Sirius.X links there instead of duplicating.</div> <div class="note" style="margin-top:1.4rem"><b>Why not list versions here?</b> Theseus updates weekly; a hardcoded version on this page went stale within days last time. theseus.x is the single source of truth — its own operator ships the current build, and Sirius.X links there instead of duplicating.</div>
</section> </section>
<section id="how"> <section id="how">
<h2>How Theseus works</h2> <h2>How Theseus works</h2>
<p class="lede">Theseus is a Chromium wrapper (Electron) with two things regular browsers don't <p class="lede">Theseus is a Chromium wrapper (Electron) with two things regular browsers don't
have: in-process BCNR name resolution, and a certificate-verify hook that trusts on-chain have: in-process BCNR name resolution, and a certificate-verify hook that trusts on-chain
TLS fingerprints instead of the OS root store for BCNR hostnames.</p> TLS fingerprints instead of the OS root store for BCNR hostnames.</p>
<ol class="steps"> <ol class="steps">
<li><b>Address bar accepts <code>.bch</code> / <code>.x</code> / any BCNR TLD.</b> <li><b>Address bar accepts <code>.bch</code> / <code>.x</code> / any BCNR TLD.</b>
<span>The URL parser is short-circuited: any hostname whose TLD is in the registry is <span>The URL parser is short-circuited: any hostname whose TLD is in the registry is
resolved by walking the chain via the built-in <code>bns.js</code> — never through DNS.</span></li> resolved by walking the chain via the built-in <code>bns.js</code> — never through DNS.</span></li>
<li><b>Records decide how the site is served.</b> <li><b>Records decide how the site is served.</b>
<span><code>h</code> renders inline HTML from the OP_RETURN payload itself. <code>s3</code> <span><code>h</code> renders inline HTML from the OP_RETURN payload itself. <code>s3</code>
serves from a Sia bucket via the in-app gateway. <code>ip</code> connects to a server by serves from a Sia bucket via the in-app gateway. <code>ip</code> connects to a server by
IP. <code>u</code> redirects. Priority is handled by the shared <code>record-picker</code>.</span></li> IP. <code>u</code> redirects. Priority is handled by the shared <code>record-picker</code>.</span></li>
<li><b>TLS verifies against on-chain fingerprints.</b> <li><b>TLS verifies against on-chain fingerprints.</b>
<span>Electron's <code>setCertificateVerifyProc</code> is intercepted: for BCNR hostnames, <span>Electron's <code>setCertificateVerifyProc</code> is intercepted: for BCNR hostnames,
instead of walking the OS trust chain, the leaf's SHA-256 fingerprint is compared to the instead of walking the OS trust chain, the leaf's SHA-256 fingerprint is compared to the
<code>tls</code> record on chain. Matches → trusted. No <code>tls</code> record → the <code>tls</code> record on chain. Matches → trusted. No <code>tls</code> record → the
browser falls back to the local root CA.</span></li> browser falls back to the local root CA.</span></li>
<li><b>Provenance indicator in the URL bar.</b> <li><b>Provenance indicator in the URL bar.</b>
<span>A tiny badge shows whether the page came from the chain, from Sia, or from an <span>A tiny badge shows whether the page came from the chain, from Sia, or from an
IP-configured server — with the NFT category shown on hover. The decentralized padlock.</span></li> IP-configured server — with the NFT category shown on hover. The decentralized padlock.</span></li>
</ol> </ol>
<div class="note"><b>What Theseus is NOT.</b> Not a wallet — for that, use Deviant, Cashonize, <div class="note"><b>What Theseus is NOT.</b> Not a wallet — for that, use Deviant, Cashonize,
Paytaca, or the built-in browser wallet at Paytaca, or the built-in browser wallet at
<a href="../#search-input">the name search</a>. <a href="../#search-input">the name search</a>.
Not a hosting service — for that, register a name and point its records at any host (Sia, VPS, Not a hosting service — for that, register a name and point its records at any host (Sia, VPS,
your own server). Theseus just makes those names openable in a normal-feeling browser.</div> your own server). Theseus just makes those names openable in a normal-feeling browser.</div>
</section> </section>
<section id="verify"> <section id="verify">
<h2>Verify a download</h2> <h2>Verify a download</h2>
<p class="lede">The binaries are unsigned (Windows will show a SmartScreen warning). Before <p class="lede">The binaries are unsigned (Windows will show a SmartScreen warning). Before
running, verify the SHA-256 against what the operator publishes on-chain — the current running, verify the SHA-256 against what the operator publishes on-chain — the current
hashes live on the theseus.x download page itself, alongside each release:</p> hashes live on the theseus.x download page itself, alongside each release:</p>
<pre>PowerShell: <pre>PowerShell:
Get-FileHash &lt;downloaded-file&gt;.exe Get-FileHash &lt;downloaded-file&gt;.exe
Bash: Bash:
sha256sum &lt;downloaded-file&gt;.exe</pre> sha256sum &lt;downloaded-file&gt;.exe</pre>
<p class="lede">Compare the digest to the hash panel on <p class="lede">Compare the digest to the hash panel on
<a href="https://silentmode.st/bns/theseus.x/#verify" rel="noopener">theseus.x/#verify</a>. <a href="https://silentmode.st/bns/theseus.x/#verify" rel="noopener">theseus.x/#verify</a>.
For a stronger guarantee, sign in with the wallet and check that the release-signing key For a stronger guarantee, sign in with the wallet and check that the release-signing key
matches the on-chain identity for <code>releases.silentmode.bch</code>.</p> matches the on-chain identity for <code>releases.silentmode.bch</code>.</p>
</section> </section>
</div> </div>
<footer id="site-footer"></footer> <footer id="site-footer"></footer>
<script src="../js/pin-escrow.js?v=20260909pin"></script> <script src="../js/pin-escrow.js?v=20260909pin"></script>
<script src="../js/pricing.js?v=20260920bits"></script> <script src="../js/pricing.js?v=20260920bits"></script>
<script defer src="../js/site-footer.js?v=20260907rel"></script> <script defer src="../js/site-footer.js?v=20260907rel"></script>
<script defer src="../js/theme.js?v=20260920compact"></script> <script defer src="../js/theme.js?v=20260920compact"></script>
<script defer src="../js/profile-menu.js?v=20260920bits"></script> <script defer src="../js/profile-menu.js?v=20260920bits"></script>
<script> <script>
(() => { // Sidenav scroll-spy: highlight the section currently in view.
const links = document.querySelectorAll(".sidenav a[href^='#']"); // Uses scroll-position tracking so short sections at the end of the page
if (!links.length) return; // still activate their link when the reader scrolls to them.
const map = new Map(); (() => {
links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) map.set(el, a); }); const links = document.querySelectorAll(".sidenav a[href^='#']");
const io = new IntersectionObserver((entries) => { if (!links.length) return;
entries.forEach((en) => { const entries = [];
if (!en.isIntersecting) return; links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) entries.push({ el, link: a }); });
const a = map.get(en.target); if (!a) return; if (!entries.length) return;
links.forEach((l) => l.classList.remove("active")); const setActive = (link) => { links.forEach((l) => l.classList.remove("active")); link.classList.add("active"); };
a.classList.add("active"); const update = () => {
}); const doc = document.documentElement;
}, { rootMargin: "-40% 0px -55% 0px", threshold: 0 }); const scrollBottom = window.scrollY + window.innerHeight;
map.forEach((_, el) => io.observe(el)); if (scrollBottom >= (doc.scrollHeight - 4)) { setActive(entries[entries.length - 1].link); return; }
})(); const line = window.scrollY + window.innerHeight * 0.35;
</script> let active = entries[0].link;
</body> for (const e of entries) {
</html> 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>

View file

@ -148,9 +148,9 @@
footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem} footer{border-top:1px solid var(--line);padding:2rem 0 3rem;color:var(--dim);font-size:13px;text-align:center;margin-top:2.5rem}
</style> </style>
<script type="importmap"> <script type="importmap">
{ "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js" } } { "imports": { "@bitauth/libauth": "https://silentmode.st/js/libauth.js?v=20260920usd" } }
</script> </script>
<script src="./js/i18n.js?v=20260920f"></script> <script src="./js/i18n.js?v=20260920g"></script>
</head> </head>
<body> <body>
@ -341,7 +341,7 @@
})(); })();
</script> </script>
<script type="module" src="./js/register-flow.js?v=20260920bits"></script> <script type="module" src="./js/register-flow.js?v=20260920usd"></script>
<script src="./js/pricing.js?v=20260920bits"></script> <script src="./js/pricing.js?v=20260920bits"></script>
<script src="./js/session.js?v=20260917dash"></script> <script src="./js/session.js?v=20260917dash"></script>
<script src="./js/pin-escrow.js?v=20260909pin"></script> <script src="./js/pin-escrow.js?v=20260909pin"></script>
@ -349,20 +349,31 @@
<script defer src="./js/site-footer.js?v=20260917market"></script> <script defer src="./js/site-footer.js?v=20260917market"></script>
<script defer src="./js/profile-menu.js?v=20260920bits"></script> <script defer src="./js/profile-menu.js?v=20260920bits"></script>
<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^='#']"); const links = document.querySelectorAll(".sidenav a[href^='#']");
if (!links.length) return; if (!links.length) return;
const map = new Map(); const entries = [];
links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) map.set(el, a); }); links.forEach((a) => { const el = document.querySelector(a.getAttribute("href")); if (el) entries.push({ el, link: a }); });
const io = new IntersectionObserver((entries) => { if (!entries.length) return;
entries.forEach((en) => { const setActive = (link) => { links.forEach((l) => l.classList.remove("active")); link.classList.add("active"); };
if (!en.isIntersecting) return; const update = () => {
const a = map.get(en.target); if (!a) return; const doc = document.documentElement;
links.forEach((l) => l.classList.remove("active")); const scrollBottom = window.scrollY + window.innerHeight;
a.classList.add("active"); if (scrollBottom >= (doc.scrollHeight - 4)) { setActive(entries[entries.length - 1].link); return; }
}); const line = window.scrollY + window.innerHeight * 0.35;
}, { rootMargin: "-40% 0px -55% 0px", threshold: 0 }); let active = entries[0].link;
map.forEach((_, el) => io.observe(el)); 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> </script>
</body> </body>