Theseus UX polish: BCDN/BCNR terminology + soft-mode prompt redesign

Terminology (user-facing labels now match the product/registry distinction):
- BCDN = Bitcoin Cash Domain Names (the product — what you view)
- BCNR = Bitcoin Cash Name Registry (the on-chain system that backs it)
Chrome badge, popover, Settings section and 'Open with…' prompt all show BCDN
for a resolved name. BCNR stays only where the registry itself is the subject.

'Open with…' prompt (collision.html):
- Larger window (640x520, was 480x340)
- Radio-select pattern with explicit Open / Cancel buttons (was auto-fire on click)
- Enter = Open, Esc = Cancel, keyboard-first
- Body copy per operator spec (no product-name-in-parens; blockchain-generic)
- Card titles kept short: 'BCNR / BCDN' + 'ICANN / IANA'

Address-bar chip:
- Says BCDN (not BCNR) for on-chain names; ICANN for web
- Dropped the '·.tld' suffix — TLD is already in the URL bar
- On collision candidates: shows BOTH chips (BCDN | ICANN) with active
  highlighted; clicking the inactive one flips the tab (via collision-switch)

Settings section renamed Naming → Registries. Copy rewritten in the same
BCDN/BCNR frame; policy labels are now 'BCDN first' / 'ICANN first' / 'Ask
each time'.

Passive 'also on BCNR' bar copy updated to BCDN.

Verified: all preload/settings/main/chrome JS + HTML parse; no secrets in
staging.
This commit is contained in:
Local Dev 2026-08-02 13:02:47 +02:00
parent d0db4ac495
commit 75e468ae39
5 changed files with 151 additions and 73 deletions

View file

@ -37,8 +37,20 @@
.ic:hover { background: var(--line2); color: var(--ink); }
.ic:active { background: var(--line); }
.ic:disabled { opacity: .28; cursor: default; background: transparent; }
/* indeterminate loading bar under the toolbar */
.loadbar { height: 2px; overflow: hidden; background: transparent; }
/* downloads button — a small badge sits over the icon when there's activity */
.dlbtn { position: relative; }
.dlbtn.spin::before { content: ""; position: absolute; inset: 4px; border-radius: 999px;
border: 1.5px solid transparent; border-top-color: #4b7bec; animation: dlspin .9s infinite linear; }
@keyframes dlspin { to { transform: rotate(360deg); } }
.dlbadge { position: absolute; top: 2px; right: 2px; min-width: 14px; height: 14px; padding: 0 3px;
background: #4b7bec; color: #fff; border-radius: 7px; font-size: 9.5px; font-weight: 700;
display: grid; place-items: center; line-height: 1; box-shadow: 0 0 0 2px var(--bg); }
.dlbtn.done .dlbadge { background: #4fd1a5; }
.dlbtn.err .dlbadge { background: #f6768a; }
/* indeterminate loading bar under the toolbar — collapses when idle so it
doesn't take a permanent 2px strip below the address bar */
.loadbar { height: 0; overflow: hidden; background: transparent; transition: height .12s linear; }
.loadbar.on { height: 2px; }
.loadbar.on::after { content: ""; display: block; height: 100%; width: 35%; border-radius: 2px;
background: linear-gradient(90deg, transparent, #4b7bec, #d6ff3d, transparent); animation: loadslide 1.1s infinite linear; }
@keyframes loadslide { 0% { transform: translateX(-110%); } 100% { transform: translateX(400%); } }
@ -58,10 +70,14 @@
.secbadge.warn svg .shk { stroke: #f6ad55; } .secbadge.warn svg .bdy { fill: #f6ad55; }
input { padding: 7px 6px; border-radius: 999px; border: none; background: transparent; color: inherit; font-size: 13.5px; outline: none; }
#url { flex: 1; }
/* minimal registry indicator at the END of the address bar */
/* registry indicator at the END of the address bar — one or two chips */
#reg { display: inline-flex; gap: 4px; align-items: center; }
.reg { font-size: 10.5px; letter-spacing: .03em; padding: 2px 9px; border-radius: 999px; white-space: nowrap;
background: rgba(214,255,61,.13); color: #d6ff3d; border: 1px solid #d6ff3d33; }
.reg.icann { background: rgba(139,152,169,.14); color: #9aa6b6; border-color: var(--line2); }
background: rgba(214,255,61,.13); color: #d6ff3d; border: 1px solid #d6ff3d33; cursor: default; user-select: none; }
.reg.icann { background: rgba(76,158,255,.13); color: #4c9eff; border-color: rgba(76,158,255,.28); }
/* collision candidate: two chips shown together; inactive one is dim + clickable */
.reg.switch { cursor: pointer; opacity: .55; }
.reg.switch:hover { opacity: 1; }
.star { border: none; background: transparent; cursor: pointer; color: var(--dim); font-size: 15px; padding: 2px 4px; border-radius: 6px; }
.star:hover { background: var(--line2); color: var(--ink); } .star.on { color: #ffd23d; }
/* search box: engine icon (click = pick engine) + a wide typing area */
@ -125,6 +141,10 @@
<button id="engineBtn" class="engine-btn" title="Choose search engine"><span id="engineIcon"></span><span class="cv"></span></button>
<input id="search" placeholder="Search" spellcheck="false">
</div>
<button class="ic dlbtn" id="downloads" title="Downloads">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2 V10 M4.5 6.5 L8 10 L11.5 6.5"/><path d="M3 12.5 L3 13.5 L13 13.5 L13 12.5"/></svg>
<span class="dlbadge" id="dlbadge" hidden>0</span>
</button>
<button class="tor" id="tor" title="Route traffic through Tor">🧅 Tor: Off</button>
<button class="logo" id="logo" title="Settings"><span>⛓ Theseus</span><span class="gear"></span></button>
</div>
@ -136,9 +156,9 @@
<a href="#" id="tordismiss">got it</a>
</div>
<div id="bcnrbar" class="bcnrbar" hidden>
<span class="breg" id="bcnrreg">BCNR</span>
<span><b id="bcnrhost"></b> also exists on the <b>BCNR</b> decentralized registry.</span>
<button id="bcnropen" class="bopen">Open on BCNR</button>
<span class="breg" id="bcnrreg">BCDN</span>
<span><b id="bcnrhost"></b> also exists on <b>BCDN</b> — the decentralized registry.</span>
<button id="bcnropen" class="bopen">Open on BCDN</button>
<a href="#" id="bcnrdismiss" class="bdismiss">dismiss</a>
</div>
<script>
@ -176,6 +196,27 @@
T.getSearchEngines().then(setEnginePlaceholder);
T.onEngines && T.onEngines(setEnginePlaceholder);
// ---- downloads button: badge + click toggles the panel ----
const dlBtn = $("downloads"), dlBadge = $("dlbadge");
function renderDownloads(items) {
const list = items || [];
const active = list.filter((d) => d.state === "progressing").length;
const doneNew = list.filter((d) => d.state === "completed").length;
const failed = list.filter((d) => d.state === "interrupted").length;
dlBtn.classList.toggle("spin", active > 0);
dlBtn.classList.toggle("done", active === 0 && doneNew > 0 && failed === 0);
dlBtn.classList.toggle("err", active === 0 && failed > 0);
const badge = active || (doneNew + failed);
if (badge > 0) { dlBadge.hidden = false; dlBadge.textContent = badge > 99 ? "99+" : String(badge); }
else { dlBadge.hidden = true; }
}
dlBtn.onclick = () => {
const r = dlBtn.getBoundingClientRect();
T.toggleDownloads({ x: Math.round(r.right - 340), y: Math.round(r.bottom + 4) });
};
T.getDownloads && T.getDownloads().then(renderDownloads);
T.onDownloads && T.onDownloads(renderDownloads);
// ---- favorites bar (new-tab page only) ----
let current = { url: "", title: "" }, bookmarks = [];
function renderBookmarks() {
@ -208,19 +249,34 @@
const b = $("secbadge");
let cls = "secbadge", title = "Site information";
if (!d || d.kind === "home") title = "Theseus";
else if (d.kind === "ok") title = "Secure · " + (d.source || "BCNR") + " — click for details";
else if (d.kind === "web") title = "Connection — click for details";
else if (d.kind === "ok") title = "Secure · BCDN via " + (d.source || "on-chain") + " — click for details";
else if (d.kind === "web") title = "Connection · ICANN — click for details";
else if (d.kind === "resolving") title = "Resolving…";
else if (d.kind === "nxdomain") { cls = "secbadge warn"; title = "Not registered on BCNR"; }
else if (d.kind === "nxdomain") { cls = "secbadge warn"; title = "Not registered on BCDN"; }
else if (d.kind === "error") { cls = "secbadge warn"; title = "Resolution error"; }
b.className = cls; b.title = title;
}
function setReg(d) {
const r = $("reg");
if (!d || d.kind === "home") { r.hidden = true; return; }
if (!d || d.kind === "home" || d.kind === "resolving") { r.hidden = true; r.innerHTML = ""; return; }
r.hidden = false;
if (d.kind === "web") { r.textContent = "ICANN"; r.className = "reg icann"; }
else { r.textContent = (d.registry || "BCNR") + (d.tld ? " ·." + d.tld : ""); r.className = "reg"; }
r.className = "reg-wrap"; // container; individual chips carry their own classes
// Two chips when this host is a collision candidate — could be BCDN OR ICANN.
// The active one is highlighted; the other is clickable to switch.
const isCand = !!d.tld && (d.kind === "ok" || d.kind === "web") && d.bcnrNativeTld === false;
const active = d.kind === "ok" ? "bcdn" : d.kind === "web" ? "icann" : null;
const bcdn = `<span class="reg${active === "bcdn" ? "" : " switch"}" data-c="bcdn" title="${active === "bcdn" ? "Open with BCDN (current)" : "Switch to BCDN"}">BCDN</span>`;
const icann = `<span class="reg icann${active === "icann" ? "" : " switch"}" data-c="icann" title="${active === "icann" ? "Open with ICANN (current)" : "Switch to ICANN"}">ICANN</span>`;
if (isCand) {
r.innerHTML = bcdn + icann;
for (const chip of r.querySelectorAll(".reg.switch")) {
chip.onclick = () => T.collisionSwitch({ choice: chip.dataset.c, remember: "no" });
}
} else if (d.kind === "web") {
r.innerHTML = `<span class="reg icann">ICANN</span>`;
} else {
r.innerHTML = `<span class="reg">BCDN</span>`;
}
}
// ---- Tor ----
@ -231,7 +287,7 @@
// ---- passive BCNR offer ----
T.onBcnrOffer((d) => {
const bar = $("bcnrbar");
if (d && d.host) { $("bcnrhost").textContent = d.host; $("bcnrreg").textContent = d.registry || "BCNR"; bar.hidden = false; }
if (d && d.host) { $("bcnrhost").textContent = d.host; $("bcnrreg").textContent = "BCDN"; bar.hidden = false; }
else bar.hidden = true;
syncHeight();
});

View file

@ -14,53 +14,69 @@
:root { --bg:#f7f7f8; --fg:#111827; --muted:#6b7280; --border:#e5e7eb; --btn:#fff; --btn-hover:#f0f2f5; }
}
* { box-sizing: border-box; }
html, body { margin:0; height:100%; }
body { background:var(--bg); color:var(--fg); font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif; display:flex; flex-direction:column; padding:16px 18px; }
h1 { margin:0 0 4px; font-size:15px; font-weight:600; }
.sub { color:var(--muted); font-size:12.5px; margin-bottom:14px; }
html, body { margin:0; height:100%; overflow:hidden; }
body { background:var(--bg); color:var(--fg); font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif; display:flex; flex-direction:column; padding:26px 34px; }
h1 { margin:0 0 6px; font-size:18px; font-weight:600; }
.sub { color:var(--muted); font-size:13.5px; margin-bottom:22px; }
.host { color:var(--fg); font-weight:600; }
.choices { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:12px; }
.choice { border:1px solid var(--border); border-radius:8px; padding:12px; cursor:pointer; background:var(--btn); text-align:left; color:var(--fg); }
.choices { display:grid; grid-template-columns:1fr 1fr; gap:14px; margin-bottom:22px; }
.choice { border:2px solid var(--border); border-radius:10px; padding:18px; background:var(--btn); text-align:left; color:var(--fg); cursor:pointer; transition: border-color .12s, background .12s; }
.choice:hover { background:var(--btn-hover); }
.choice .icon { font-size:20px; }
.choice .title { font-weight:600; margin:6px 0 2px; }
.choice .desc { font-size:11.5px; color:var(--muted); }
.choice.bcnr .icon, .choice.bcnr .title { color:var(--accent); }
.choice.icann .icon, .choice.icann .title { color:var(--icann); }
fieldset { border:0; padding:0; margin:0 0 10px; }
fieldset .row { display:flex; align-items:center; gap:8px; padding:4px 0; font-size:12.5px; color:var(--muted); }
fieldset .row input { accent-color:var(--accent); }
.actions { display:flex; justify-content:flex-end; gap:8px; margin-top:auto; }
button.cancel { background:transparent; color:var(--muted); border:0; padding:8px 12px; cursor:pointer; }
button.cancel:hover { color:var(--fg); }
kbd { border:1px solid var(--border); border-radius:3px; padding:0 4px; font:11px monospace; color:var(--muted); }
.choice.sel.bcdn { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--btn)); }
.choice.sel.icann { border-color: var(--icann); background: color-mix(in srgb, var(--icann) 12%, var(--btn)); }
.choice .head { display:flex; align-items:center; gap:10px; margin-bottom:8px; }
.choice .radio { flex:none; width:16px; height:16px; border-radius:50%; border:2px solid var(--border); position:relative; }
.choice.sel.bcdn .radio { border-color: var(--accent); background: radial-gradient(circle, var(--accent) 40%, transparent 42%); }
.choice.sel.icann .radio { border-color: var(--icann); background: radial-gradient(circle, var(--icann) 40%, transparent 42%); }
.choice .icon { font-size:20px; line-height:1; }
.choice .title { font-weight:600; font-size:15px; }
.choice.bcdn .title { color: var(--accent); }
.choice.icann .title { color: var(--icann); }
.choice .desc { font-size:13px; color:var(--muted); }
fieldset { border:0; padding:0; margin:0 0 22px; display:flex; flex-direction:column; gap:8px; }
fieldset legend { color:var(--muted); font-size:12.5px; padding:0; margin-bottom:6px; text-transform:uppercase; letter-spacing:.03em; }
fieldset .row { display:flex; align-items:center; gap:9px; font-size:13.5px; padding:2px 0; }
fieldset .row input { accent-color:var(--accent); width:15px; height:15px; }
fieldset .row label { color:var(--fg); cursor:pointer; }
.actions { display:flex; justify-content:flex-end; gap:10px; margin-top:auto; }
button.act { border:1px solid var(--border); background:var(--btn); color:var(--fg); padding:10px 20px; border-radius:8px; font-size:14px; cursor:pointer; }
button.act:hover { background: var(--btn-hover); }
button.act.primary { background:var(--accent); color:#111; border-color:var(--accent); font-weight:600; }
button.act.primary:hover { filter:brightness(1.05); }
kbd { border:1px solid var(--border); border-radius:3px; padding:0 5px; font:11px monospace; color:var(--muted); margin-left:6px; }
</style>
</head>
<body>
<h1><span class="host" id="host"></span> exists in two registries</h1>
<div class="sub">Choose which one to open. You can change this later in Settings.</div>
<div class="choices">
<button class="choice bcnr" data-choice="bcnr" title="Open the on-chain name">
<div class="icon"></div>
<div class="title">Bitcoin Cash Name Registry</div>
<div class="desc">On-chain registration (BCNR). Owned by the holder of a name certificate on the Bitcoin Cash Blockchain.</div>
</button>
<button class="choice icann" data-choice="icann" title="Open the ICANN-served address">
<div class="icon">🌐</div>
<div class="title">ICANN / IANA</div>
<div class="choices" id="choices">
<div class="choice bcdn sel" data-choice="bcdn" tabindex="0">
<div class="head">
<span class="radio"></span><span class="icon"></span>
<span class="title">BCNR / BCDN</span>
</div>
<div class="desc">On-chain registration. Owned by the holder of a digital name certificate. The registry served by the blockchain root.</div>
</div>
<div class="choice icann" data-choice="icann" tabindex="0">
<div class="head">
<span class="radio"></span><span class="icon">🌐</span>
<span class="title">ICANN / IANA</span>
</div>
<div class="desc">DNS service coordinated by the ICANN. The registry served by the incumbent root.</div>
</button>
</div>
</div>
<fieldset>
<div class="row"><input type="radio" name="remember" id="rem-no" value="no" checked><label for="rem-no">Ask again</label></div>
<div class="row"><input type="radio" name="remember" id="rem-name" value="name"><label for="rem-name">Always use this choice for <span class="host" id="host2"></span></label></div>
<div class="row"><input type="radio" name="remember" id="rem-tld" value="tld"><label for="rem-tld">Always use this choice for all <span id="tld2"></span> names</label></div>
<legend>Remember this choice</legend>
<div class="row"><input type="radio" name="remember" id="rem-no" value="no" checked><label for="rem-no">Ask again next time</label></div>
<div class="row"><input type="radio" name="remember" id="rem-name" value="name"><label for="rem-name">Always use for <span class="host" id="host2"></span></label></div>
<div class="row"><input type="radio" name="remember" id="rem-tld" value="tld"><label for="rem-tld">Always use for all <span id="tld"></span> names</label></div>
</fieldset>
<div class="actions">
<button class="cancel" id="cancel">Cancel <kbd>Esc</kbd></button>
<button class="act" id="cancel">Cancel<kbd>Esc</kbd></button>
<button class="act primary" id="confirm">Open<kbd>Enter</kbd></button>
</div>
<script>
@ -69,25 +85,31 @@
document.getElementById("host").textContent = host;
document.getElementById("host2").textContent = host;
document.getElementById("tld").textContent = "." + tld;
document.getElementById("tld2").textContent = "." + tld;
document.title = "Open " + host + " with…";
function remember() {
const r = document.querySelector('input[name="remember"]:checked');
return r ? r.value : "no";
let choice = "bcdn"; // default preselected
const bcdnEl = document.querySelector('.choice[data-choice="bcdn"]');
const icannEl = document.querySelector('.choice[data-choice="icann"]');
function select(c) {
choice = c;
bcdnEl.classList.toggle("sel", c === "bcdn");
icannEl.classList.toggle("sel", c === "icann");
}
for (const btn of document.querySelectorAll(".choice")) {
btn.addEventListener("click", () => {
window.collisionApi.choose(host, { choice: btn.dataset.choice, remember: remember() });
});
for (const el of [bcdnEl, icannEl]) {
el.addEventListener("click", () => select(el.dataset.choice));
el.addEventListener("keydown", (e) => { if (e.key === " " || e.key === "Enter") { e.preventDefault(); select(el.dataset.choice); } });
}
document.getElementById("cancel").addEventListener("click", () => {
window.collisionApi.choose(host, { choice: "cancel", remember: "no" });
});
function remember() { return document.querySelector('input[name="remember"]:checked')?.value || "no"; }
function confirm() {
// Map "bcdn" (user-facing) -> "bcnr" (internal key used by main.js/collisions.json)
window.collisionApi.choose(host, { choice: choice === "bcdn" ? "bcnr" : "icann", remember: remember() });
}
function cancel() { window.collisionApi.choose(host, { choice: "cancel", remember: "no" }); }
document.getElementById("confirm").addEventListener("click", confirm);
document.getElementById("cancel").addEventListener("click", cancel);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") window.collisionApi.choose(host, { choice: "cancel", remember: "no" });
if (e.key === "1") window.collisionApi.choose(host, { choice: "bcnr", remember: remember() });
if (e.key === "2") window.collisionApi.choose(host, { choice: "icann", remember: remember() });
if (e.key === "Escape") cancel();
if (e.key === "Enter" && e.target.tagName !== "BUTTON") confirm();
});
</script>
</body>

View file

@ -214,7 +214,7 @@ function collisionPromptOnce({ host, tld }) {
if (collisionWin && !collisionWin.isDestroyed()) { try { collisionWin.close(); } catch {} }
const parent = BrowserWindow.getFocusedWindow() || win;
collisionWin = new BrowserWindow({
parent, modal: true, width: 480, height: 340, resizable: false, minimizable: false,
parent, modal: true, width: 640, height: 520, resizable: false, minimizable: false,
maximizable: false, autoHideMenuBar: true, title: "Open with…", show: false,
webPreferences: { preload: path.join(__dirname, "collision-preload.js"), sandbox: true, contextIsolation: true },
});

View file

@ -61,7 +61,7 @@
<div class="switcher" id="switcher" style="display:none">
<div class="title">Open this name with</div>
<div class="row">
<button id="sw-bcnr" data-choice="bcnr">⛓ BCNR</button>
<button id="sw-bcnr" data-choice="bcnr">⛓ BCDN</button>
<button id="sw-icann" data-choice="icann" class="icann">🌐 ICANN</button>
</div>
<div class="rem">
@ -87,11 +87,11 @@
if (!d || d.kind === "home") {
cls = "hero neutral"; ico = GLOBE; status = "Theseus"; $("host").textContent = "";
sub = "The decentralized-web navigator. It resolves BCNR names straight from the Bitcoin Cash chain.";
sub = "The decentralized-web navigator. It resolves BCDN names straight from the blockchain.";
} else if (d.kind === "resolving") {
cls = "hero neutral"; ico = GLOBE; status = "Resolving…"; sub = "Looking this name up on the Bitcoin Cash chain.";
} else if (d.kind === "nxdomain") {
cls = "hero warn"; ico = WARN; status = "Not registered"; sub = "This name is not registered on the BCNR chain.";
cls = "hero warn"; ico = WARN; status = "Not registered"; sub = "This name is not registered on BCDN (the on-chain registry).";
} else if (d.kind === "error") {
cls = "hero warn"; ico = WARN; status = "Resolution error"; sub = esc(d.error || "The name could not be resolved.");
} else if (d.kind === "web") {
@ -101,7 +101,7 @@
status = "Connection secure";
sub = "Served over an encrypted connection from the <b>decentralized web</b> — no host, no registrar, no certificate authority.";
details = row("Served from", esc(d.source) || "chain")
+ row("Registry", (d.registry || "BCNR") + (d.tld ? " · ." + d.tld : ""))
+ row("Registry", "BCDN — Bitcoin Cash Domain Names")
+ (d.records && d.records.length ? row("Records", `<code>${esc(d.records.join(", "))}</code>`) : "")
+ (d.category ? row("Certificate", `<code>${esc(String(d.category).slice(0, 20))}…</code>`) : "");
}

View file

@ -131,7 +131,7 @@
<div class="brand">⛓ Theseus</div>
<a data-sec="general" class="active">General</a>
<a data-sec="search">Search</a>
<a data-sec="naming">Naming</a>
<a data-sec="naming">Registries</a>
<a data-sec="performance">Performance</a>
<a data-sec="privacy">Privacy</a>
</nav>
@ -196,15 +196,15 @@
</section>
<!-- NAMING -->
<section id="naming" hidden>
<h1>Naming</h1>
<p class="lede">How Theseus picks between the <b>Bitcoin Cash Name Registry (BCNR)</b> and <b>ICANN</b> when a name exists in both.</p>
<h1>Registries</h1>
<p class="lede">How Theseus picks between <b>BCDN</b> (Bitcoin Cash Domain Names, on-chain) and <b>ICANN</b> (traditional DNS) when a name exists in both registries.</p>
<div class="row" style="flex-direction:column;align-items:stretch;gap:10px">
<div class="txt"><div class="t">Collision policy</div>
<div class="d">A name only exists in both registries when its TLD isn't BCNR-native (e.g. <code>.de</code>). BCNR-native TLDs (<code>.bch</code>, <code>.p2p</code>, …) never conflict.</div></div>
<div class="d">A collision only happens when a BCDN name is registered under a TLD that ICANN also serves (e.g. <code>.de</code>). BCNR-native TLDs (<code>.bch</code>, <code>.p2p</code>, …) never conflict — they only exist on BCDN.</div></div>
<div id="policyList" style="display:flex;flex-direction:column;gap:6px">
<label class="polrow"><input type="radio" name="collisionPolicy" value="bcnr-first"><span><b>BCNR first</b> <span class="pmuted">— BCNR wins conflicts; falls back to ICANN.</span></span></label>
<label class="polrow"><input type="radio" name="collisionPolicy" value="icann-first"><span><b>ICANN first</b> <span class="pmuted">— ICANN wins conflicts; BCNR fills gaps.</span></span></label>
<label class="polrow"><input type="radio" name="collisionPolicy" value="soft"><span><b>Ask each time</b> <span class="pmuted">— an "Open with…" prompt on conflict, remembered per name.</span></span></label>
<label class="polrow"><input type="radio" name="collisionPolicy" value="bcnr-first"><span><b>BCDN first</b> <span class="pmuted">— BCDN wins conflicts; falls back to ICANN for anything BCDN doesn't have.</span></span></label>
<label class="polrow"><input type="radio" name="collisionPolicy" value="icann-first"><span><b>ICANN first</b> <span class="pmuted">— ICANN wins conflicts; BCDN fills gaps for non-ICANN TLDs.</span></span></label>
<label class="polrow"><input type="radio" name="collisionPolicy" value="soft"><span><b>Ask each time</b> <span class="pmuted">— an "Open with…" prompt on conflict, remembered per name or per TLD.</span></span></label>
</div>
</div>
<div class="row" style="flex-direction:column;align-items:stretch;gap:8px">