eleven / ocho Public

web: invite landing leads straight to sign-in — skip becomes small print (#1430)

* web: prejoin card leads straight to sign-in — skip demoted to small print

The invitation landing card presented two equal-weight buttons (sign in /
continue without an account) — a choice where there should be one path.
Now: a single primary CTA, "Sign in with Eleven", with "Continue without
signing in" as a quiet grey link under the caveat (still a real <button>,
keyboard-operable, ≥24px target).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* home: clear the stale 'Checking your sign-in…' status when auto-enrol falls back

A fresh sign-up reaches /enroll with a live session but no blob yet (the
first blob needs a ceremony's PRF), so the auto fast path falls back to the
card — but left its busy status painted, reading as a stuck spinner under
the re-enabled buttons. Clear it on that return.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell pushed by codex agent, operated by paul@keymail.dev 4264a14a99665013b99f22ed1f5e5a273b1006a5 parent 714ce0d
5 files changed, +29 −8
  • changelog/2026-08-06-prejoin-signin-lead.md +4 −0
  • docs/onboarding-and-home-server.md +4 −3
  • home/web/static/enroll.js +5 −1
  • web/pages/join.html +5 −4
  • web/static/style.css +11 −0
diff --git a/changelog/2026-08-06-prejoin-signin-lead.md b/changelog/2026-08-06-prejoin-signin-lead.md
new file mode 100644
index 0000000..cf1af0c
--- /dev/null
+++ b/changelog/2026-08-06-prejoin-signin-lead.md
@@ -0,0 +1,4 @@
+🤖 The invite page now leads with one clear step: **Sign in with Eleven**. No
+more choosing between two ways in — signing in is the path, and "Continue
+without signing in" waits underneath as small print for anyone who'd rather
+skip it.
diff --git a/docs/onboarding-and-home-server.md b/docs/onboarding-and-home-server.md
index 7c3f74f..3bd0222 100644
--- a/docs/onboarding-and-home-server.md
+++ b/docs/onboarding-and-home-server.md
@@ -87,9 +87,10 @@ URL bar. Claim happens only on an explicit choice.
access to this chat — save a passkey"), Apple-style, after they've arrived.
🤖 *Shipped 2026-07-31 (signup-first join):* the sign-in-first ordering above
-now exists as the join page's **lead card** — fresh joiners see "Continue with
-your Eleven sign-in" before the app/web choice, with a quiet "Continue without
-an account" escape. The round trip differs from the sketch in two details:
+now exists as the join page's **lead card** — fresh joiners see "Sign in with
+Eleven" before the app/web choice, with "Continue without signing in" as small
+print (a quiet link, not a competing button — one path, plus an escape hatch;
+Paul, 2026-08-06). The round trip differs from the sketch in two details:
it's always a same-tab navigation (never a popup — Safari blocks WebAuthn in
cross-origin popups), and the fragment parks in the instance origin's
`localStorage` (nonce-stamped, expiring), not `sessionStorage`, so a mobile
diff --git a/home/web/static/enroll.js b/home/web/static/enroll.js
index 63bc891..06480f4 100644
--- a/home/web/static/enroll.js
+++ b/home/web/static/enroll.js
@@ -159,7 +159,11 @@ async function trySessionReuse() {
try {
setStatus("Checking your sign-in…", "busy");
got = await fetchBlob(cand.token); // throws on a dead token (401)
- if (!got) return false; // signed in but no backup yet — the ceremony path knows that case
+ // Signed in but no backup yet (a fresh sign-up — the first blob needs a
+ // ceremony's PRF, which reuse can't supply). Clear the busy status before
+ // falling back: in auto mode nothing else repaints, so "Checking your
+ // sign-in…" would sit under the re-enabled buttons like a stuck spinner.
+ if (!got) { setStatus("", ""); return false; }
dek = await importDEK(cand.dek);
opened = await openBlob(got.bytes, dek);
} catch {
diff --git a/web/pages/join.html b/web/pages/join.html
index 66308a1..d27a6a4 100644
--- a/web/pages/join.html
+++ b/web/pages/join.html
@@ -115,20 +115,21 @@
<!-- The signup-first lead card (docs/superpowers/specs/2026-07-31-signup-first-
join-design.md): fresh joiners see the sign-in door BEFORE the app/web
- choice, so their access is saved to their account from the start. Skippable
- — "Continue without an account" reveals the classic welcome card. -->
+ choice, so their access is saved to their account from the start. ONE path
+ — sign in — not a choice: the skip is deliberately small print (a quiet
+ link revealing the classic welcome card), never a competing button. -->
<template id="prejoin-tpl">
<img id="prejoin-photo" class="onboard-photo hidden" alt="" />
<div class="onboard-icon" id="prejoin-icon">💬</div>
<h1 id="prejoin-title">You're invited to __GROUP_NAME__</h1>
<p id="prejoin-sub">Private, end-to-end encrypted chat.</p>
<p id="prejoin-desc" class="welcome-desc hidden"></p>
- <div class="row"><button id="prejoin-signin">Continue with your Eleven sign-in</button></div>
- <div class="row"><button id="prejoin-skip" class="secondary">Continue without an account</button></div>
+ <div class="row"><button id="prejoin-signin">Sign in with Eleven</button></div>
<p class="welcome-caveat">
Signing in first keeps your access safe — get back into this space from any
device, with nothing to remember.
</p>
+ <div class="row"><button id="prejoin-skip" class="link-quiet">Continue without signing in</button></div>
</template>
<!-- The welcome card (deferred claim — see docs/onboarding-and-home-server.md).
diff --git a/web/static/style.css b/web/static/style.css
index ae62725..79552b7 100644
--- a/web/static/style.css
+++ b/web/static/style.css
@@ -1923,6 +1923,17 @@ select:disabled { opacity: 0.5; cursor: default; }
air between it and the button it annotates. (.card selectors so this outranks
the generic .card p margin rule.) */
.card .welcome-caveat { font-size: 13px; color: var(--muted); margin: 20px auto 4px; max-width: 42ch; }
+/* A quiet escape hatch on the onboarding cards (e.g. the prejoin card's
+ "Continue without signing in"): small print that acts, not a competing
+ button — a real <button> so it stays keyboard-operable and focusable. */
+button.link-quiet {
+ background: none;
+ color: var(--muted);
+ font-size: 13px;
+ font-weight: 400;
+ text-decoration: underline;
+ padding: 6px 8px; /* keeps the tap target ≥24px despite the small type */
+}
/* the space's own description on the welcome card: the admin's note to the
person joining — content to read, not chrome. A tinted panel with
full-strength text sets it apart from the muted boilerplate around it. */