eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1<!doctype html>
2<html lang="en">
3<head>
4<meta charset="utf-8">
5<meta name="github-client-id" content="__GITHUB_CLIENT_ID__">
6 <meta name="viewport" content="width=device-width, initial-scale=1">
7<meta name="theme-color" content="#f6f7fb" media="(prefers-color-scheme: light)">
8<meta name="theme-color" content="#15171c" media="(prefers-color-scheme: dark)">
9<title>Sign in with Eleven</title>
10<link rel="icon" type="image/png" href="/static/icon-192.png">
11<link rel="apple-touch-icon" href="/static/icon-512.png">
12<link rel="stylesheet" href="/static/home.css">
13<style>
14 /* The landing is a single centered card: the Eleven mark, the sign-in
15 doors (composed per browser — see composeCard in home.js), and a
16 reassuring line. Your spaces appear only once you've signed in. */
17 /* Center the card + hint as one group. The shared body is a stretch grid
18 (fine for the single-card ceremony pages); with two children here it would
19 otherwise push them to opposite ends of the viewport. */
20 body.landing { align-content: center; }
21 .brand { display: flex; flex-direction: column; align-items: center; gap: 10px; margin-bottom: 22px; }
22 /* The real app icon (web/static/icon-*.png, copied into home's static). The
23 PNG is already a rounded superellipse; the radius just cleans the edge. */
24 .brand-mark { width: 64px; height: 64px; border-radius: 15px; display: block; }
25 /* System font (San Francisco on Apple) for the wordmark — not the rounded
26 display font. */
27 .brand-word {
28 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;
29 font-weight: 500; font-size: 26px; letter-spacing: -0.02em;
30 }
31 /* The doors (#1112): home.js composes the card — ONE primary door (the
32 last-used method, else the platform default), the email door beneath it
33 (unless it IS the primary), and the quiet "Other ways to sign in" link
34 to the all-ways step; demoted doors don't render on the main card at
35 all. The Apple button is Apple's standard black look, hand-built (no
36 Apple JS SDK — the CSP pins scripts to self); the hairline keeps it from
37 dissolving into a dark panel. */
38 .apple-btn {
39 display: flex; align-items: center; justify-content: center; gap: 8px;
40 background: #000; color: #fff;
41 border: 1px solid light-dark(#000, rgba(255,255,255,.28));
42 position: relative; /* anchors the loading ring */
43 }
44 /* This inline sheet loads after home.css, so .apple-btn's display beats the
45 global .hidden at equal specificity — restate the hide here (the same
46 trap .icon-add.hidden answers below). Demoted doors hide this way. */
47 .apple-btn.hidden { display: none; }
48 /* The passkey mark rides inline in non-flex buttons: baseline-tuck + gap. */
49 .pk-ico, .btn-ico { vertical-align: -2.5px; margin-right: 7px; }
50 /* Google's door, to their light-button spec: white, hairline border, the
51 official four-colour G (inline SVG — their rendered button needs their
52 JS, which the CSP rightly blocks; hand-built like Apple's). */
53 .google-btn, .ms-btn {
54 display: flex; align-items: center; justify-content: center; gap: 8px;
55 background: light-dark(#fff, #131314); color: light-dark(#1f1f1f, #e3e3e3);
56 border: 1px solid light-dark(#747775, #8e918f);
57 position: relative; /* anchors the loading ring */
58 }
59 .google-btn.hidden, .ms-btn.hidden { display: none; }
60 .gh-btn {
61 display: flex; align-items: center; justify-content: center; gap: 8px;
62 background: #24292f; color: #fff;
63 border: 1px solid light-dark(#24292f, rgba(255,255,255,.28));
64 position: relative; /* anchors the loading ring */
65 }
66 .gh-btn.hidden { display: none; }
67 /* A working button wears a ring (Apple hand-over, Send code): the wait
68 needs a face even when the server is instant. White reads on both the
69 black and accent buttons — the only wearers. Reduced motion keeps a
70 static ring (progress must still be visible). */
71 button.loading { color: transparent; position: relative; }
72 button.loading svg { opacity: 0; }
73 button.loading::after {
74 content: ""; position: absolute; inset: 0; margin: auto;
75 width: 18px; height: 18px; border-radius: 50%;
76 border: 2px solid rgba(255,255,255,.35); border-top-color: #fff;
77 animation: btn-spin .8s linear infinite;
78 }
79 @keyframes btn-spin { to { transform: rotate(360deg); } }
80 @media (prefers-reduced-motion: reduce) { button.loading::after { animation: none; } }
81 /* A brief working beat for meaningful transitions that are technically
82 instant (sign out): the card holds with a ring so the change reads as
83 an event, not a glitch. Case-by-case, per the project memory. */
84 main.card.waiting > * { visibility: hidden; }
85 main.card.waiting::after {
86 content: ""; position: absolute; inset: 0; margin: auto;
87 width: 22px; height: 22px; border-radius: 50%;
88 border: 3px solid color-mix(in srgb, var(--accent) 30%, transparent);
89 border-top-color: var(--accent);
90 animation: btn-spin .8s linear infinite;
91 }
92 @media (prefers-reduced-motion: reduce) { main.card.waiting::after { animation: none; } }
93 /* Tapping Apple clears the stage: everything but the spinning button fades
94 away, so the wait for the sheet is just the button at work. The pressed
95 button wears .loading, and the door can be on the main card OR the
96 all-ways step, so the fade is "everything that isn't the working button"
97 on either surface. The smoke cover (html.siwa-wait, below with the
98 loading chrome) follows on a timer — we can't hear the overlay appear. */
99 #signed-out .actions > *, #otherways .actions > *,
100 #methods .actions > *, #methods-confirm .actions > *, #methods-combine .actions > *,
101 #otherways .icon-back, #otherways .create-title, #newhere { transition: opacity .3s ease, transform .3s ease; }
102 main.card.apple-pending :is(#signed-out, #otherways, #methods, #methods-confirm, #methods-combine) .actions > :not(.loading),
103 main.card.apple-pending #otherways .icon-back,
104 main.card.apple-pending #otherways .create-title,
105 main.card.apple-pending #newhere {
106 opacity: 0; transform: translateY(4px); pointer-events: none;
107 }
108 body:has(main.card.apple-pending) .reassure,
109 body:has(main.card.apple-pending) .cookie-ask,
110 body:has(main.card.apple-pending) .demo-card { opacity: 0; pointer-events: none; }
111 @media (prefers-reduced-motion: reduce) {
112 #signed-out .actions > *, #otherways .actions > *,
113 #methods .actions > *, #methods-confirm .actions > *, #methods-combine .actions > *,
114 #otherways .icon-back, #otherways .create-title, #newhere { transition: none; }
115 }
116 .apple-btn svg { display: block; flex: none; }
117 /* The quiet in-card link style: the demoted passkey door wears it, and so
118 does the "Other ways to sign in" link (#1112) — one vocabulary. */
119 .passkey-link {
120 background: none; border: none; padding: 6px 0; font: inherit;
121 font-size: 14px; font-weight: 600; color: var(--muted);
122 cursor: pointer; text-decoration: underline;
123 }
124 .passkey-link:disabled { opacity: .5; cursor: default; }
125 .passkey-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
126 /* The email door, a card step of its own: heading, an address row, the
127 no-spam note, then the code row once a code is on its way (home.js
128 drives the two steps). */
129 #email-door h2, #otherways h2 { margin-bottom: 14px; }
130 #email-flow { display: flex; flex-direction: column; gap: 8px; }
131 #email-flow form { display: flex; gap: 8px; }
132 #email-flow form.hidden { display: none; }
133 #email-flow input { flex: 1; min-width: 0; }
134 #email-flow button { flex: none; }
135 .anchor-note { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.4; }
136 /* The after-failure explainer: a quiet inset, left-aligned so the longer copy
137 reads as a message rather than more hero text. It also hosts the ONLY
138 create path — full-width like the doors above it. */
139 #newhere {
140 margin: 14px 0 0; padding: 14px; border-radius: 14px;
141 background: var(--quiet); text-align: left; font-size: 14px; line-height: 1.45;
142 }
143 #newhere p { margin: 0; }
144 #newhere .path-divider { margin: 12px 0 10px; }
145 #otherways .path-divider { margin: 16px 0 12px; font-size: 13px; }
146 #newhere button { width: 100%; }
147 /* The sealed-list prompt (an email/Apple sign-in that can't decrypt yet). */
148 #vault-sealed { margin-top: 6px; }
149 #vault-sealed button { width: 100%; }
150 /* The reassurance sits OUTSIDE the card, under it. */
151 /* The invite card is a guest, not the host: veiled with the rest during
152 boot/compose, and its fade TRAILS the main card's reveal by a beat. */
153 html.booting .invite-card, html.composing .invite-card { opacity: 0; }
154 .invite-card {
155 transition: opacity .4s ease .35s;
156 background: var(--panel); border: 1px solid var(--border);
157 border-radius: 18px; padding: 16px 20px; width: 100%; max-width: 420px;
158 box-shadow: 0 8px 24px var(--shadow); margin: 14px auto 0;
159 text-align: center; color: var(--muted); font-size: 14px; line-height: 1.5;
160 }
161 .invite-card p { margin: 0; }
162 body:has(#vault.hidden) .invite-card { display: none; }
163 .invite-card.hidden { display: none; }
164 .cookie-ask { text-align: center; margin: 8px 0 0; }
165 .cookie-ask button {
166 background: none; border: none; padding: 4px 8px; cursor: pointer;
167 color: var(--muted); font-size: 13px; text-decoration: underline;
168 }
169 #cookie-dialog {
170 border: none; border-radius: 22px; padding: 26px 28px; max-width: 380px;
171 background: var(--panel); color: inherit; text-align: center;
172 box-shadow: 0 18px 50px var(--shadow);
173 }
174 #cookie-dialog::backdrop { background: rgba(0, 0, 0, .35); }
175 #cookie-dialog h2 { margin: 0 0 10px; font-size: 20px; }
176 #cookie-dialog p { margin: 0 0 10px; line-height: 1.45; }
177 #cookie-dialog .cookie-small { color: var(--muted); font-size: 13px; }
178 #cookie-close { margin-top: 6px; }
179 .reassure {
180 max-width: 360px; text-align: center; margin: 16px auto 0;
181 color: var(--muted); font-size: 13px; line-height: 1.45;
182 }
183 /* The demo offer's own card — it isn't a sign-in, so it leaves the sign-in
184 card entirely: a little sales assistant peeking out a shop door. Same
185 card vocabulary but tinted toward the brand purple (an 8% dilution —
186 muted text holds ≥5.6:1 on it in both themes, comfortably AA). Narrow
187 screens stack it below as a full-width card, kept straight; wide ones
188 tuck it beside the main card as a tilted flap (absolute against the
189 body, its inner edge sliding under the card — main.card's z-index keeps
190 the card on top; the tilt is static, so no reduced-motion concern). It
191 follows the doors: any state that hides them (signed in, the email
192 step) hides it too — CSS-only, so the JS toggle stays the feature
193 flag's + the newcomer gate's alone. */
194 body.landing { position: relative; }
195 .demo-card {
196 background: color-mix(in srgb, var(--accent) 8%, var(--panel));
197 border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--border));
198 border-radius: 18px;
199 padding: 18px;
200 width: 100%;
201 max-width: 420px;
202 box-shadow: 0 8px 24px var(--shadow);
203 margin: 14px auto 0;
204 text-align: center;
205 }
206 /* The shopkeeper pops their head out a beat after the door opens: the
207 card animates from tucked-away to its resting peek, delayed so the
208 main card registers first. Keyframes animate the FULL transform (the
209 resting pose differs per layout). Reduced motion: no pop, the card is
210 simply there. */
211 .demo-card:not(.hidden) {
212 animation: demo-rise 480ms cubic-bezier(.34,1.56,.64,1) 1400ms both;
213 }
214 @keyframes demo-rise {
215 from { opacity: 0; transform: translateY(10px); }
216 to { opacity: 1; transform: translateY(0); }
217 }
218 @media (prefers-reduced-motion: reduce) {
219 .demo-card:not(.hidden) { animation: none; }
220 }
221 /* The button must read as a BUTTON on the tinted card — plain --quiet
222 nearly matches the lavender and the pill vanished (Paul's catch). */
223 .demo-card #try-demo {
224 background: var(--panel);
225 border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border));
226 box-shadow: 0 1px 4px var(--shadow);
227 }
228 body:has(#signed-out.hidden) .demo-card { display: none; }
229 /* The cookie question belongs to the sign-in screen only — it follows the
230 doors, exactly like the reassure line beside it. */
231 body:has(#signed-out.hidden) .cookie-ask { display: none; }
232 @media (min-width: 960px) {
233 .demo-card {
234 position: absolute; left: calc(50% + 170px); top: 50%;
235 width: 252px; margin: 0;
236 transform: translateY(-50%) rotate(3deg);
237 /* The pivot is the shopkeeper's FEET, well below the fold — the lean
238 arcs from a body planted off-screen, not a hinge at the card edge.
239 The far origin makes rotate(3deg) throw the card ~25px rightward,
240 so `left` is trimmed to land the same resting tuck as before. */
241 transform-origin: left calc(100% + 50vh);
242 z-index: 0; /* the 16px overlap tucks behind the main card's edge */
243 }
244 .demo-card:not(.hidden) {
245 animation: demo-peek 560ms cubic-bezier(.34,1.56,.64,1) 1400ms both;
246 }
247 @keyframes demo-peek {
248 from { opacity: 1; transform: translateY(-50%) translateX(-240px) rotate(0deg); }
249 to { opacity: 1; transform: translateY(-50%) translateX(0) rotate(3deg); }
250 }
251 }
252 .linkish {
253 background: none; border: none; padding: 0; font: inherit; font-weight: 600;
254 color: var(--accent); cursor: pointer; text-decoration: underline;
255 }
256 /* Your spaces, revealed after sign-in. */
257 #vault, #create { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--border); text-align: left; }
258 /* Screens fade + rise as they take over the card. */
259 @keyframes screen-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
260 .screen { animation: screen-in .18s ease; }
261 @media (prefers-reduced-motion: reduce) { .screen { animation: none; } }
262 .vault-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; margin-bottom: 6px; }
263 /* Three children now (heading, +, kebab): all the free space goes to the
264 heading's auto margin, so the two controls sit together on the right. */
265 .vault-head h2 { margin-right: auto; }
266 #vault h2 { font-size: 19px; font-weight: 400; margin: 0; font-family: var(--font-display); letter-spacing: -0.01em; }
267 /* The + that starts a new space, inline with the heading. */
268 .icon-add {
269 flex: none; width: 30px; height: 30px; border-radius: 50%;
270 display: grid; place-items: center; padding: 0; border: none; cursor: pointer;
271 background: var(--accent); color: var(--accent-ink);
272 }
273 .icon-add svg { display: block; }
274 /* This inline sheet loads after home.css, so its .icon-add display beats
275 the global .hidden at equal specificity — restate the hide here (the
276 same trap .menu-pop.hidden answers below). The + showed on empty lists
277 for exactly this reason (Paul's catch). */
278 .icon-add.hidden { display: none; }
279 .icon-add:hover { filter: brightness(1.06); }
280 .icon-add:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
281 /* The account kebab (issue #1098's single control) + its popup menu. The
282 kebab is the +'s quiet sibling — same 30px round target, no fill until
283 hover/open. The menu hangs under it, right-aligned so it opens inward
284 (never clipping the card edge), in the card vocabulary. */
285 /* The kebab lives at the CARD's top-right corner — account-scope, above
286 the list's own controls (the + keeps the header row's end slot). Inside
287 #vault so it exists only for signed-in views; anchored to main.card's
288 position. */
289 .kebab-wrap { position: absolute; top: 16px; right: 16px; }
290 .icon-kebab {
291 flex: none; width: 30px; height: 30px; border-radius: 50%;
292 display: grid; place-items: center; padding: 0; border: none; cursor: pointer;
293 background: none; color: var(--muted);
294 }
295 .icon-kebab svg { display: block; }
296 .icon-kebab:hover, .icon-kebab[aria-expanded="true"] { background: var(--quiet); color: var(--ink); }
297 .icon-kebab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
298 .menu-pop {
299 position: absolute; top: calc(100% + 6px); right: 0; z-index: 5;
300 min-width: 160px; padding: 6px;
301 display: flex; flex-direction: column;
302 background: var(--panel); border: 1px solid var(--border); border-radius: 14px;
303 box-shadow: 0 12px 32px var(--shadow);
304 animation: menu-in .12s ease;
305 }
306 .menu-pop.hidden { display: none; }
307 @keyframes menu-in { from { opacity: 0; } }
308 @media (prefers-reduced-motion: reduce) { .menu-pop { animation: none; } }
309 .menu-pop [role="menuitem"] {
310 font: inherit; font-weight: 600; text-align: left; cursor: pointer;
311 background: none; border: none; border-radius: 9px; padding: 9px 12px;
312 color: var(--ink);
313 }
314 .menu-pop [role="menuitem"]:hover { background: var(--quiet); }
315 .menu-pop [role="menuitem"]:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
316 /* Pointer-born opens: focus still lands in the menu, but the ring is a
317 keyboard courtesy — the focused item shows the hover wash instead. */
318 .menu-pop.pointer-open [role="menuitem"]:focus-visible { outline: none; background: var(--quiet); }
319 /* Sign-in options (issue #1098): the method rows — friendly name + when
320 it was added, and a small per-row kebab whose menu holds Add another … /
321 Remove (removal arms to an inline are-you-sure, never a browser
322 confirm()). List left-aligned like the spaces list; the steps centre
323 their titles like the other card steps. */
324 #methods, #methods-confirm, #methods-email, #methods-passkey, #methods-combine { text-align: center; }
325 ul.methods { list-style: none; margin: 0 0 14px; padding: 0; text-align: left; }
326 ul.methods li { display: flex; align-items: center; gap: 8px; padding: 7px 6px; border-radius: 10px; }
327 ul.methods .method-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
328 ul.methods .method-name { font-weight: 600; line-height: 1.25; }
329 ul.methods .method-added { color: var(--muted); font-size: .72rem; line-height: 1.3; }
330 #methods-list > p.empty { color: var(--muted); font-size: .9rem; margin: 6px 0 14px; text-align: left; }
331 /* The row's inline label editor (the sub-line become a small form). */
332 ul.methods .label-edit { display: flex; gap: 6px; margin-top: 4px; }
333 ul.methods .label-edit input { flex: 1; min-width: 0; font-size: .85rem; padding: 6px 8px; }
334 /* The per-row kebab anchors its own .menu-pop (the account menu's
335 vocabulary, one wrap per row). */
336 .row-kebab-wrap { position: relative; flex: none; }
337 /* The email mini-flow rows, shared by the confirm step's re-verify and the
338 add-email step. This inline sheet loads after home.css, so the display
339 rules beat the global .hidden at equal specificity — restate the hides
340 (the .icon-add trap above). */
341 .email-mini { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
342 .email-mini.hidden { display: none; }
343 .email-mini form { display: flex; gap: 8px; }
344 .email-mini form.hidden { display: none; }
345 .email-mini input { flex: 1; min-width: 0; }
346 .email-mini button { flex: none; }
347 .email-mini .passkey-link { align-self: flex-start; }
348 /* The combine offer rides under whichever status line showed the 409. */
349 #combine-offer { margin-top: 10px; }
350 #combine-prove .anchor-note, #combine-vault #combine-vault-note { text-align: left; }
351 /* The prove note needs the same air above its button as the vault stage's
352 sub gets — without it the explainer sits flush on Continue with Apple. */
353 #combine-prove .anchor-note { margin-bottom: 14px; }
354 #combine-vault .create-sub { margin-bottom: 14px; }
355 /* Low-emphasis, full-width: Done (confirmation) and Cancel (archive). */
356 .wide-quiet {
357 width: 100%; margin-top: 18px; font: inherit; font-weight: 600; cursor: pointer;
358 background: var(--quiet); color: var(--ink); border: none; border-radius: 999px; padding: 11px 18px;
359 }
360 .wide-quiet:hover { filter: brightness(0.98); }
361 .wide-quiet:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
362 /* Create screen: the goes-to-eleven fader (fader.js). Centered and spare —
363 one line, one instrument. Feel iterated in prototypes/space-lever.html. */
364 #create { text-align: center; }
365 /* Back from the create screen: a caret in a quiet circle, pinned to the
366 card's top-left corner (iOS-modal style) rather than a text link. */
367 .icon-back {
368 position: absolute; top: 16px; left: 16px;
369 width: 34px; height: 34px; border-radius: 50%;
370 display: grid; place-items: center; padding: 0; border: none; cursor: pointer;
371 background: var(--quiet); color: var(--ink);
372 }
373 .icon-back svg { display: block; }
374 .icon-back:hover { filter: brightness(0.97); }
375 .icon-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
376 .create-title {
377 font-family: var(--font-display); font-size: 23px; font-weight: 400;
378 line-height: 1.2; letter-spacing: -0.015em; margin: 20px 0 4px;
379 }
380 .create-sub { color: var(--muted); font-size: 14px; margin: 0 0 24px; }
381 /* The stage: dust + confetti behind the card, full-viewport. */
382 #stage-dust { position: fixed; inset: 0; width: 100vw; height: 100vh; pointer-events: none; }
383 main.card { position: relative; z-index: 1; will-change: transform; }
384 /* VU colours: desk vernacular (green/amber/red) on purpose — this is a meter. */
385 :root { --led-g: light-dark(#31a24c, #3fd063); --led-a: light-dark(#e8a23a, #f0b45a);
386 --led-r: light-dark(#e5484d, #ff5d63); --slot: light-dark(#d3d7e2, #0e1014); }
387 /* The instrument is not a document: a drag that wanders off the fader must
388 never start selecting the headline mid-slide. */
389 #create-pitch { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
390 .strip { padding: 4px 2px 10px; }
391 .meter { display: flex; gap: 4px; margin: 0 2px 16px; }
392 .led {
393 flex: 1; height: 9px; border-radius: 2px;
394 background: color-mix(in srgb, var(--ink) 10%, transparent);
395 box-shadow: inset 0 1px 1px rgba(0,0,0,.18);
396 transition: background .06s;
397 }
398 .led.on-g { background: var(--led-g); box-shadow: 0 0 6px color-mix(in srgb, var(--led-g) 60%, transparent); }
399 .led.on-a { background: var(--led-a); box-shadow: 0 0 6px color-mix(in srgb, var(--led-a) 60%, transparent); }
400 .led.on-r { background: var(--led-r); box-shadow: 0 0 8px color-mix(in srgb, var(--led-r) 70%, transparent); }
401 @keyframes led-fury { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
402 .led.fury { animation: led-fury .14s linear infinite; }
403 .fader { position: relative; height: 46px; margin: 0 2px; touch-action: none; cursor: grab; }
404 .fader.dragging { cursor: grabbing; }
405 .slot {
406 position: absolute; left: 0; right: 0; top: 50%; height: 6px; translate: 0 -50%;
407 border-radius: 3px; background: var(--slot);
408 box-shadow: inset 0 1px 3px rgba(0,0,0,.35), inset 0 -1px 0 rgba(255,255,255,.25);
409 }
410 .tick {
411 position: absolute; top: 4px; width: 1.5px; height: 7px; translate: -50% 0;
412 background: color-mix(in srgb, var(--muted) 55%, transparent); border-radius: 1px;
413 }
414 .tick.major { height: 10px; top: 1px; }
415 .num {
416 position: absolute; top: 100%; translate: -50% 0; padding-top: 2px;
417 font-size: 11px; font-weight: 600; color: var(--muted);
418 font-variant-numeric: tabular-nums;
419 }
420 .num.lit { color: var(--ink); }
421 .num.eleven { font-weight: 800; transition: color .12s, transform .12s; }
422 .num.eleven.arming { color: var(--led-r); }
423 .num.eleven.hit { color: var(--led-r); transform: scale(1.4); }
424 .cap {
425 position: absolute; top: 50%; translate: -50% -50%;
426 width: 30px; height: 44px; border-radius: 6px;
427 background: linear-gradient(180deg,
428 light-dark(#ffffff, #343946) 0%,
429 light-dark(#e7eaf1, #262a33) 48%,
430 light-dark(#d9dde8, #1e222a) 52%,
431 light-dark(#eef0f6, #2b3039) 100%);
432 border: 1px solid light-dark(#c4c9d6, #0c0e12);
433 box-shadow: 0 3px 8px var(--shadow), inset 0 1px 0 rgba(255,255,255,.55);
434 cursor: grab;
435 }
436 .cap::before, .cap::after { content: ""; position: absolute; left: 4px; right: 4px; height: 1px;
437 background: light-dark(rgba(0,0,0,.10), rgba(255,255,255,.08)); }
438 .cap::before { top: 9px; box-shadow: 0 4px 0 light-dark(rgba(0,0,0,.10), rgba(255,255,255,.08)); }
439 .cap::after { bottom: 9px; box-shadow: 0 -4px 0 light-dark(rgba(0,0,0,.10), rgba(255,255,255,.08)); }
440 .stripe {
441 position: absolute; top: 6px; bottom: 6px; left: 50%; width: 3px; translate: -50% 0;
442 border-radius: 2px; background: var(--accent); z-index: 1;
443 }
444 .fader.dragging .cap { cursor: grabbing; }
445 .cap:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
446 /* The wait: still AT eleven around it (the fader keeps the card rattling). */
447 #create-building { padding: 18px 0 6px; }
448 .build-spin {
449 width: 34px; height: 34px; margin: 4px auto 14px; border-radius: 50%;
450 border: 3px solid color-mix(in srgb, var(--ink) 12%, transparent);
451 border-top-color: var(--accent);
452 animation: spin .7s linear infinite;
453 }
454 @keyframes spin { to { transform: rotate(360deg); } }
455 #create-building h3 { font-size: 17px; font-weight: 400; margin: 0 0 4px; }
456 #create-building p { color: var(--muted); font-size: 14px; margin: 0; }
457 /* Reduced motion: no strobe, no spinner spin, no transitions — states still
458 change, instantly. (Dust, confetti, shake, and VU flicker are skipped in
459 fader.js under the same query.) */
460 @media (prefers-reduced-motion: reduce) {
461 .led.fury { animation: none; }
462 .led, .num.eleven { transition: none; }
463 .build-spin { animation: none; opacity: .5; }
464 }
465 /* Archive screen. The list is quiet; the permanent delete is deliberately loud. */
466 #archive { text-align: center; }
467 #destroy-zone { margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--border); }
468 /* When destroying is the only action (last admin + owner), the zone IS the
469 primary action — no footnote separator; the button sits where Delete did. */
470 #destroy-zone.primary { margin-top: 0; padding-top: 0; border-top: none; }
471 .danger-link { color: var(--danger); font-size: .9rem; }
472 .danger-link:disabled { opacity: .4; cursor: default; text-decoration: none; }
473 .danger-ink { color: var(--danger); }
474 .stern-copy b { color: var(--ink); overflow-wrap: anywhere; }
475 #delete-arm {
476 width: 100%; margin: 6px 0 12px; padding: 10px 12px; text-align: center;
477 font: inherit; font-size: .95rem;
478 color: var(--ink); background: var(--quiet);
479 border: 1px solid var(--border); border-radius: 10px;
480 }
481 #delete-arm:focus-visible { outline: none; border-color: var(--danger); background: var(--panel); }
482 .btn-danger {
483 width: 100%; font: inherit; font-weight: 700; cursor: pointer;
484 background: var(--danger); color: #fff; border: none; border-radius: 999px; padding: 11px 18px;
485 }
486 .btn-danger:disabled { opacity: .4; cursor: default; }
487 .btn-danger:focus-visible { outline: 2px solid var(--danger); outline-offset: 2px; }
488 #archive-stern .wide-quiet { margin-top: 10px; }
489 #archive-checking { padding: 34px 0 12px; }
490 /* The per-row archive: a box glyph that reveals on hover, like the drag grip.
491 Reversible, so it stays neutral (not danger-red). */
492 .row-archive {
493 flex: none; display: grid; place-items: center; width: 22px; height: 30px;
494 padding: 0; border: none; background: none; color: var(--muted);
495 border-radius: 7px; cursor: pointer;
496 opacity: 0; transition: opacity .12s ease, background .12s ease, color .12s ease;
497 }
498 .row-archive svg { display: block; }
499 .row-archive:hover { background: var(--quiet); color: var(--ink); }
500 .row-archive:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
501 @media (hover: hover) { ul.servers li:hover .row-archive { opacity: .5; } }
502 @media (hover: none) { .row-archive { opacity: .4; } }
503 @media (prefers-reduced-motion: reduce) { .row-archive { transition: none; } }
504
505 /* The per-row Upgrade: a quiet pill that reveals on hover like the archive
506 glyph — shown only on spaces this account created, when the provisioner
507 offers billing. Always focusable (focus reveals it), ≥24px tall. */
508 .row-upgrade {
509 flex: none; font: inherit; font-size: .72rem; font-weight: 600; cursor: pointer;
510 min-height: 24px; padding: 3px 10px; border-radius: 999px;
511 color: var(--muted); background: none; border: 1px solid var(--border);
512 opacity: 0; transition: opacity .12s ease, color .12s ease, border-color .12s ease;
513 }
514 .row-upgrade:hover { color: var(--accent); border-color: var(--accent); }
515 .row-upgrade:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
516 .row-upgrade:disabled { opacity: .4; cursor: default; }
517 @media (hover: hover) { ul.servers li:hover .row-upgrade { opacity: .7; } }
518 @media (hover: none) { .row-upgrade { opacity: .55; } }
519 @media (prefers-reduced-motion: reduce) { .row-upgrade { transition: none; } }
520 /* Upgrade screen: one button per plan — quiet rows, name left, price right. */
521 #upgrade { text-align: center; }
522 #upgrade-tiers { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
523 .tier-btn {
524 display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
525 width: 100%; padding: 12px 14px; border-radius: 12px; cursor: pointer;
526 font: inherit; text-align: left;
527 color: var(--ink); background: var(--quiet); border: 1px solid var(--border);
528 }
529 .tier-btn:hover { border-color: var(--accent); }
530 .tier-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
531 .tier-btn:disabled { opacity: .5; cursor: default; }
532 .tier-btn .tier-name { font-weight: 700; }
533 .tier-btn .tier-price { color: var(--muted); font-size: .9rem; font-variant-numeric: tabular-nums; }
534 .tier-btn .tier-approx { color: var(--muted); font-size: .78rem; font-variant-numeric: tabular-nums; }
535 .tier-btn .tier-band-std {
536 font-size: .62rem; font-weight: 600; color: var(--muted);
537 padding: 1px 8px; border: 1px solid var(--border); border-radius: 999px;
538 }
539 /* Plan screen (create step 1): the name field + who-pays picker. Selection
540 mirrors the band chips — border + weight, never colour alone. */
541 #create-plan { text-align: center; }
542 #plan-name {
543 width: 100%; margin-top: 8px; padding: 12px 14px; font: inherit; text-align: center;
544 color: var(--ink); background: var(--quiet); border: 1px solid var(--border); border-radius: 12px;
545 }
546 #plan-name:focus-visible { outline: none; border-color: var(--accent); background: var(--panel); }
547 #plan-tiers-wrap { margin-top: 16px; }
548 #plan-tiers { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
549 .tier-btn.selected { border-color: var(--accent); border-width: 2px; padding: 11px 13px; font-weight: 600; }
550 .tier-btn .tier-sub { color: var(--muted); font-size: .78rem; flex-basis: 100%; text-align: left; }
551 .tier-btn.wrap { flex-wrap: wrap; }
552 #create-plan .actions { margin-top: 16px; }
553
554 /* The honor-band row: four open prices, one selected — selection is shown
555 by weight + border, never colour alone. */
556 .band-row { display: flex; justify-content: center; gap: 6px; margin-top: 12px; flex-wrap: wrap; }
557 .band-chip {
558 font: inherit; font-size: .82rem; cursor: pointer; padding: 5px 12px;
559 min-height: 28px; border-radius: 999px;
560 color: var(--ink); background: none; border: 1px solid var(--border);
561 }
562 .band-chip.selected { border-color: var(--accent); border-width: 2px; font-weight: 700; padding: 4px 11px; }
563 .band-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
564 .band-note, .fx-note, .cap-note { color: var(--muted); font-size: .8rem; margin: 8px 0 0; }
565 .fx-note .fx-currency {
566 font: inherit; font-size: .8rem; color: var(--ink);
567 background: var(--quiet); border: 1px solid var(--border); border-radius: 6px; padding: 1px 4px;
568 }
569 /* The tier badge on an upgraded space's row, with Manage beside it. */
570 .badge-row { display: inline-flex; align-items: center; gap: 8px; align-self: flex-start; margin-top: 2px; }
571 .row-manage {
572 font: inherit; font-size: .62rem; font-weight: 600; cursor: pointer;
573 padding: 1px 8px; border-radius: 999px; line-height: inherit;
574 color: var(--muted); background: none; border: 1px solid var(--border);
575 /* Same visual height as the badge; the tap target stays ≥24px via an
576 invisible hit-area extension, not visual bulk. */
577 position: relative;
578 }
579 .row-manage::before { content: ""; position: absolute; inset: -6px; border-radius: 999px; }
580 /* "ends 11 Jul 2027" beside a cancelled subscription's badge. */
581 .sub-ends { color: var(--muted); font-size: .72rem; }
582 .row-manage:hover { color: var(--accent); border-color: var(--accent); }
583 .row-manage:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
584 .row-manage:disabled { opacity: .4; cursor: default; }
585 .supporter-badge {
586 font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
587 color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
588 border-radius: 999px; padding: 1px 7px;
589 }
590
591 /* The inline "Archived <name> · Undo" note — occupies layout, never a toast. */
592 .undo-note {
593 display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
594 margin: 0 0 10px; padding: 8px 12px; border-radius: 10px;
595 background: var(--quiet); color: var(--muted); font-size: .85rem;
596 }
597 /* The "Archived (N)" foot button: a quiet full-width row that opens the archive. */
598 .archived-open {
599 display: flex; align-items: center; justify-content: space-between; gap: 8px;
600 width: 100%; margin-top: 12px; padding: 9px 12px; border-radius: 10px;
601 font: inherit; font-size: .9rem; font-weight: 600; cursor: pointer; text-align: left;
602 color: var(--muted); background: none; border: 1px solid var(--border);
603 transition: background .12s ease, color .12s ease;
604 }
605 .archived-open > span:first-child { display: inline-flex; align-items: center; gap: 8px; }
606 .archived-open svg { display: block; opacity: .85; }
607 .archived-open .chev { font-size: 1.1rem; line-height: 1; color: var(--muted); }
608 .archived-open:hover { background: var(--quiet); color: var(--ink); }
609 .archived-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
610 @media (prefers-reduced-motion: reduce) { .archived-open { transition: none; } }
611
612 /* Archive list rows: name + Unarchive + Delete pills. Quiet, non-draggable. */
613 ul.servers.archived li { gap: 6px; }
614 ul.servers.archived li .name { font-weight: 600; line-height: 1.25; color: var(--ink); }
615 ul.servers.archived li .srv-main { text-align: left; }
616 .pill {
617 flex: none; font: inherit; font-size: .8rem; font-weight: 600; cursor: pointer;
618 padding: 5px 10px; border-radius: 999px;
619 color: var(--ink); background: var(--quiet); border: 1px solid transparent;
620 }
621 .pill:hover { filter: brightness(0.97); }
622 .pill.danger-ink { color: var(--danger); }
623 .pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
624 #archive-list > p.empty { color: var(--muted); font-size: .9rem; margin: 6px 0 0; }
625 /* Section headings: "Starred" and "More" — only shown once some are starred. */
626 .spaces-head {
627 font-size: .7rem; text-transform: uppercase; letter-spacing: .05em;
628 color: var(--muted); font-family: var(--font-display); margin: 12px 0 3px;
629 }
630 .spaces-head:first-child { margin-top: 0; }
631 #spaces > p.empty { color: var(--muted); font-size: .9rem; margin: 6px 0 0; }
632 /* Fuzzy search over the list — appears once there are a few spaces to sift. */
633 #space-search {
634 width: 100%; margin: 4px 0 12px; padding: 8px 12px;
635 font: inherit; font-size: .9rem;
636 color: var(--ink); background: var(--quiet);
637 border: 1px solid transparent; border-radius: 10px;
638 }
639 #space-search::placeholder { color: var(--muted); }
640 #space-search:focus-visible { outline: none; border-color: var(--accent); background: var(--panel); }
641 ul.servers { list-style: none; margin: 0; padding: 0; }
642 ul.servers li {
643 display: flex; align-items: center; gap: 8px;
644 padding: 5px 6px; border-radius: 10px;
645 /* No separator lines: the hover wash (below) is the interactive cue, and the
646 lifted row + the gap it leaves carry the structure while dragging. */
647 }
648 /* Tight line-heights so a row's name + meta lines read as one compact block,
649 not airy lines (the card body sets 1.5, which is too loose for a list). */
650 .srv-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
651 ul.servers li a.name { font-weight: 600; line-height: 1.25; color: var(--ink); text-decoration: none; }
652 ul.servers li a.name:hover { text-decoration: underline; }
653 /* The storage meter's reading, under the name — quieter still. */
654 ul.servers li .srv-usage { color: var(--muted); opacity: .75; font-size: .72rem; line-height: 1.3; }
655 /* Star toggle: a quiet outline star that fills gold when on. */
656 .star {
657 flex: none; background: none; border: none; padding: 2px; cursor: pointer;
658 font-size: 18px; line-height: 1; color: var(--muted);
659 }
660 .star.on { color: #f2b705; }
661 .star:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
662
663 /* Drag-to-reorder. Idle rows are calm; hover reveals the affordance — a soft
664 brand-tinted wash on the row, and a grip that fades in on the right. Grabbing
665 lifts the row off the card (shadow) while the others slide aside. */
666 .drag-handle {
667 flex: none; display: grid; place-items: center; width: 22px; height: 30px;
668 padding: 0; border: none; background: none; color: var(--muted);
669 border-radius: 7px; cursor: grab; touch-action: none;
670 -webkit-tap-highlight-color: transparent;
671 opacity: 0; transition: opacity .12s ease, background .12s ease, color .12s ease;
672 }
673 .drag-handle svg { fill: currentColor; display: block; }
674 .drag-handle:hover { background: var(--quiet); color: var(--ink); }
675 .drag-handle:active { cursor: grabbing; }
676 .drag-handle:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
677 @media (hover: hover) {
678 ul.servers li:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }
679 ul.servers li:hover .drag-handle { opacity: .5; }
680 }
681 @media (hover: none) { .drag-handle { opacity: .4; } } /* touch: no hover to reveal it */
682 ul.servers li.dragging {
683 background: var(--panel); box-shadow: 0 10px 28px var(--shadow);
684 position: relative; z-index: 3; cursor: grabbing;
685 }
686 ul.servers li.dragging .drag-handle { opacity: 1; color: var(--ink); }
687 @media (prefers-reduced-motion: reduce) { .drag-handle { transition: none; } }
688
689 /* Ambient smoke + motes (bootscene.js), pinned behind ALL content (z-index -1,
690 above the page bg but under the card/text). Started on load for everyone and
691 left running — the page's living backdrop. Dimmed as a background; fuller
692 during the loader splash (html.booting), so it recedes as the content lands. */
693 .boot-canvas {
694 position: fixed; inset: 0; width: 100%; height: 100%;
695 z-index: -1; pointer-events: none;
696 opacity: .4; transition: opacity .6s ease;
697 }
698 html.booting .boot-canvas { opacity: 1; }
699 /* The returning-member loading chrome: the Eleven lockup + tagline centred over
700 the smoke while the spaces list decrypts. Shown only when html.booting (a
701 live session, set pre-paint), which also hides the content beneath so the
702 Sign-in screen never flashes. On finish bootscene.js fades this out
703 (.cover-out) and drops html.booting — the card fades in, the smoke stays. */
704 .app-loading {
705 position: fixed; inset: 0; z-index: 2;
706 display: none; place-items: center; align-content: center;
707 transition: opacity .5s ease;
708 }
709 html.booting .app-loading { display: grid; }
710 /* A session refresh shouldn't feel like a journey: on the non-siwa booting
711 paths the pageantry — the lockup chrome and the smoke's full ramp —
712 holds for 500ms before appearing at all. The cover still BLOCKS from
713 frame zero (the content-hiding rules below are undelayed); only its
714 visibility waits. A load that lifts inside the window never shows
715 chrome: bootscene's fast path reveals the content near-instantly
716 (html.boot-fast trims the content fade to an imperceptible beat, and
717 comes off right after so the stage-clear fades keep their timing). The
718 siwa journey face (html.siwa-wait) deliberately keeps its frame-zero
719 chrome. Opacity-only, so it stays under reduced motion like the fades. */
720 html.booting:not(.siwa-wait) .app-loading { animation: cover-wait .45s ease .5s both; }
721 @keyframes cover-wait { from { opacity: 0; } to { opacity: 1; } }
722 html.booting:not(.siwa-wait) .boot-canvas { transition-delay: .5s; }
723 html.boot-fast main.card, html.boot-fast .reassure,
724 html.boot-fast .demo-card, html.boot-fast .cookie-ask { transition-duration: .08s; }
725 /* Hide the content (card, its outside-the-card reassurance, and the demo
726 card) beneath the loader so nothing flashes; all fade back in when
727 booting drops. */
728 html.booting main.card, html.booting .reassure, html.booting .demo-card, html.booting .cookie-ask,
729 html.composing main.card, html.composing .reassure, html.composing .demo-card, html.composing .cookie-ask { opacity: 0; }
730 /* The composing lift is quick — a plain visitor shouldn't feel a fade. */
731 html.compose-reveal main.card, html.compose-reveal .reassure,
732 html.compose-reveal .demo-card, html.compose-reveal .cookie-ask { transition-duration: .12s; }
733 main.card, .reassure, .demo-card, .cookie-ask { transition: opacity .5s ease; }
734 /* cover-out keeps the chrome displayed (booting has already dropped) so the
735 fade actually plays; the timeout in finishHomeLoader then parks it. */
736 .app-loading.cover-out { display: grid; opacity: 0; pointer-events: none; }
737 .boot-brand { display: flex; flex-direction: column; align-items: center; gap: 10px; }
738 .boot-goesto {
739 position: absolute; left: 0; right: 0; bottom: 18px; margin: 0;
740 text-align: center; color: var(--muted); font-size: 13px; letter-spacing: .02em;
741 }
742 /* The Apple round trip's waiting face (html.siwa-wait, both legs of the
743 trip): the same cover, but a ring where the lockup sits — no Eleven
744 brand until the sign-in has actually confirmed. Verify success drops
745 siwa-wait and stamps .brand-in: the lockup fades in (~400ms), dwells a
746 beat, then the cover lifts while the spaces screen slides up beneath it
747 (#vault.arrive). Reduced motion: instant swaps, no slide — the ring's
748 own static-under-reduce rule is .build-spin's. */
749 .boot-ring { display: none; }
750 html.siwa-wait .app-loading .boot-ring { display: block; }
751 html.siwa-wait .app-loading .boot-brand,
752 html.siwa-wait .app-loading .boot-goesto { display: none; }
753 .app-loading.brand-in .boot-brand,
754 .app-loading.brand-in .boot-goesto { animation: brand-in .4s ease both; }
755 @keyframes brand-in { from { opacity: 0; } }
756 #vault.arrive { animation: vault-arrive .35s ease-out; }
757 @keyframes vault-arrive { from { opacity: 0; transform: translateY(14px); } }
758 @media (prefers-reduced-motion: reduce) {
759 .app-loading.brand-in .boot-brand,
760 .app-loading.brand-in .boot-goesto { animation: none; }
761 #vault.arrive { animation: none; }
762 }
763</style>
764<script>
765 // Pre-paint gate: if this browser holds a live session — the tab's own, or
766 // a stay-signed-in one in localStorage (home_session_expires is a plain
767 // epoch-ms key exactly so this check needs no JSON) — or is landing back
768 // from Apple's sign-in (#siwa= fragment) or the mail's magic link (#email=
769 // fragment), show the loading cover from the very first frame, so no path
770 // ever flashes the Sign-in screen. Both fragment landings get the WAITING
771 // face (smoke + ring, no lockup — siwa-wait): the brand only lands once the
772 // sign-in has confirmed. Signed-out visitors skip it all; home.js lifts the
773 // cover on every path it was raised for.
774 try {
775 // The card never paints uncomposed: composeCard() decides its doors
776 // (last-used/platform), so the default markup must not get a frame.
777 // home.js lifts this at the end of module init on every path.
778 document.documentElement.classList.add("composing");
779 if (location.hash.indexOf("#siwa=") === 0 || location.hash.indexOf("#oidc=") === 0 ||
780 location.hash.indexOf("#oauth=") === 0 || location.hash.indexOf("#email=") === 0)
781 document.documentElement.classList.add("booting", "siwa-wait");
782 else if ((sessionStorage.getItem("home_token") && sessionStorage.getItem("home_dek")) ||
783 +localStorage.getItem("home_session_expires") > Date.now())
784 document.documentElement.classList.add("booting");
785 } catch (e) {}
786</script>
787</head>
788<body class="landing">
789<!-- Persistent ambient background (bootscene.js): soft smoke + rising dust motes
790 drifting behind everything. Started on load for all visitors and left running
791 — the returning-member loading chrome (below) fades over it, then lifts to
792 leave the smoke as the page's living backdrop. -->
793<canvas class="boot-canvas" id="boot-canvas" aria-hidden="true"></canvas>
794<!-- Stage for the create-fader's dust + confetti (fader.js) — full-viewport,
795 behind the card, drawn only while the create screen is live. -->
796<canvas id="stage-dust" aria-hidden="true"></canvas>
797<!-- The returning-member loading chrome: the Eleven lockup + tagline over the
798 smoke while the spaces list decrypts, so a refresh doesn't flash the Sign-in
799 screen first. Shown only when html.booting (a live session — see the
800 pre-paint gate above); it fades out once the list is ready, and the smoke
801 behind it stays. -->
802<div id="app-loading" class="app-loading" aria-hidden="true">
803 <div class="boot-brand">
804 <img class="brand-mark" src="/static/icon-512.png" width="64" height="64" alt="Eleven" />
805 <div class="brand-word">Eleven</div>
806 </div>
807 <!-- The Apple wait face (html.siwa-wait): a ring where the lockup sits —
808 the brand only lands once the sign-in has confirmed. -->
809 <div class="build-spin boot-ring" aria-hidden="true"></div>
810 <p class="boot-goesto">The group chat of dreams</p>
811</div>
812<main class="card">
813 <div class="brand">
814 <img class="brand-mark" src="/static/icon-512.png" width="64" height="64" alt="Eleven" />
815 <div class="brand-word">Eleven</div>
816 </div>
817
818 <!-- The signed-out doors; hidden once you're in (see showVault). home.js
819 composes them (#1112): ONE primary door — the last-used method when
820 this browser has one, else the platform default — with the email door
821 beneath it (unless it IS the primary) and the quiet "Other ways to
822 sign in" link to the all-ways step below. Demoted doors don't render
823 here. Apple and email are find-or-create, one button each, no sign-in
824 vs sign-up split. Create-a-passkey appears only after a failed
825 ceremony (#newhere). -->
826 <div id="signed-out">
827 <div class="actions" id="main-doors">
828 <button id="apple-signin" class="apple-btn" type="button"><svg viewBox="0 0 814 1000" width="14" height="17" fill="currentColor" aria-hidden="true"><path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/></svg>Continue with Apple</button>
829 <button id="google-signin" class="google-btn hidden" type="button"><svg viewBox="0 0 48 48" width="16" height="16" aria-hidden="true"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>Continue with Google</button>
830 <button id="microsoft-signin" class="ms-btn hidden" type="button"><svg viewBox="0 0 21 21" width="15" height="15" aria-hidden="true"><rect x="1" y="1" width="9" height="9" fill="#F25022"/><rect x="11" y="1" width="9" height="9" fill="#7FBA00"/><rect x="1" y="11" width="9" height="9" fill="#00A4EF"/><rect x="11" y="11" width="9" height="9" fill="#FFB900"/></svg>Continue with Microsoft</button>
831 <button id="github-signin" class="gh-btn hidden" type="button"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>Continue with GitHub</button>
832 <button id="email-signin" class="secondary" type="button">Continue with email</button>
833 <button id="signin" class="passkey-link" type="button"><svg class="pk-ico" viewBox="0 0 16 16" width="15" height="15" fill="currentColor" aria-hidden="true"><circle cx="6" cy="4.4" r="2.7"/><path d="M1.2 13.9a4.8 4.8 0 0 1 9.6 0z"/><path fill-rule="evenodd" d="M12.1 7.3a2.1 2.1 0 0 1 2.06 2.52l1.6 1.6-1.2 1.2-.7-.7-.65.65-.85-.85-.62.62a2.1 2.1 0 1 1 .36-4.94zm-.9 1.6a.8.8 0 1 0 0 1.6.8.8 0 0 0 0-1.6z"/></svg>Sign in with a passkey</button>
834 <button id="other-ways" class="passkey-link" type="button">Other ways to sign in</button>
835 </div>
836
837 <!-- Revealed only after a failed or cancelled passkey ceremony — the one
838 place the create path lives now. Cancelled and no-passkey are
839 deliberately indistinguishable (NotAllowedError either way), so the
840 explainer serves both readings: a returning member likely picked the
841 wrong passkey — try again — and someone genuinely new creates theirs
842 here. -->
843 <div id="newhere" class="hidden screen">
844 <p><strong>That didn't work?</strong> It might just be the wrong passkey —
845 your Eleven sign-in is one key that opens every space you belong to, so
846 if you've signed in before, try again and choose it from the list.</p>
847 <div class="path-divider">New here?</div>
848 <button id="create-passkey" class="secondary" type="button"><svg class="pk-ico" viewBox="0 0 16 16" width="15" height="15" fill="currentColor" aria-hidden="true"><circle cx="6" cy="4.4" r="2.7"/><path d="M1.2 13.9a4.8 4.8 0 0 1 9.6 0z"/><path fill-rule="evenodd" d="M12.1 7.3a2.1 2.1 0 0 1 2.06 2.52l1.6 1.6-1.2 1.2-.7-.7-.65.65-.85-.85-.62.62a2.1 2.1 0 1 1 .36-4.94zm-.9 1.6a.8.8 0 1 0 0 1.6.8.8 0 0 0 0-1.6z"/></svg>Create your passkey</button>
849 </div>
850 </div>
851
852 <!-- The email door (email.go), a card step of its own — the doors swap out
853 for it (same screen-swap pattern as the signed-in screens): address →
854 code → session. Forms so Enter submits and native validation gates;
855 home.js drives the steps. -->
856 <div id="email-door" class="hidden screen">
857 <button id="email-back" class="icon-back" type="button" aria-label="Back to sign-in"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
858 <h2 class="create-title">Sign in with email</h2>
859 <div id="email-flow">
860 <form id="email-form">
861 <input id="email-addr" type="email" required autocomplete="email"
862 placeholder="you@example.com" aria-label="Your email address">
863 <button id="email-send" type="submit">Send code</button>
864 </form>
865 <p class="anchor-note" id="email-note">We never keep your email address. No spam, ever.</p>
866 <form id="email-code-form" class="hidden">
867 <input id="email-code" type="text" required inputmode="numeric" pattern="[0-9]{6}"
868 maxlength="6" autocomplete="one-time-code" placeholder="6-digit code"
869 aria-label="The 6-digit code from your email">
870 <button id="email-verify" type="submit">Sign in</button>
871 </form>
872 </div>
873 </div>
874
875 <!-- The all-ways step (#1112): every door on one card, reached from the
876 quiet "Other ways to sign in" link. Same screen-swap pattern as the
877 email door (it hides #signed-out, so the greeter/cookie/reassure
878 visibility rules follow along), and each row invokes the SAME handler
879 as the main card's version of that door. -->
880 <div id="otherways" class="hidden screen">
881 <button id="otherways-back" class="icon-back" type="button" aria-label="Back to sign-in"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
882 <h2 class="create-title">Other ways to sign in</h2>
883 <p class="create-sub">We don't store your data no matter what method you use.</p>
884 <div class="actions">
885 <button id="otherways-passkey" class="secondary" type="button"><svg class="pk-ico" viewBox="0 0 16 16" width="15" height="15" fill="currentColor" aria-hidden="true"><circle cx="6" cy="4.4" r="2.7"/><path d="M1.2 13.9a4.8 4.8 0 0 1 9.6 0z"/><path fill-rule="evenodd" d="M12.1 7.3a2.1 2.1 0 0 1 2.06 2.52l1.6 1.6-1.2 1.2-.7-.7-.65.65-.85-.85-.62.62a2.1 2.1 0 1 1 .36-4.94zm-.9 1.6a.8.8 0 1 0 0 1.6.8.8 0 0 0 0-1.6z"/></svg>Sign in with a passkey</button>
886 <button id="otherways-email" class="secondary" type="button">Continue with email</button>
887 </div>
888 <div class="path-divider">For those oppressed by convenience…</div>
889 <div class="actions">
890 <button id="otherways-apple" class="apple-btn" type="button"><svg viewBox="0 0 814 1000" width="14" height="17" fill="currentColor" aria-hidden="true"><path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/></svg>Continue with Apple</button>
891 <button id="otherways-github" class="gh-btn" type="button"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>Continue with GitHub</button>
892 <button id="otherways-microsoft" class="ms-btn" type="button"><svg viewBox="0 0 21 21" width="15" height="15" aria-hidden="true"><rect x="1" y="1" width="9" height="9" fill="#F25022"/><rect x="11" y="1" width="9" height="9" fill="#7FBA00"/><rect x="1" y="11" width="9" height="9" fill="#00A4EF"/><rect x="11" y="11" width="9" height="9" fill="#FFB900"/></svg>Continue with Microsoft</button>
893 <button id="otherways-google" class="google-btn" type="button"><svg viewBox="0 0 48 48" width="16" height="16" aria-hidden="true"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>Continue with Google</button>
894 </div>
895 </div>
896
897 <!-- Inline status only — never a floating toast (product-wide rule). -->
898 <p class="status" id="status"></p>
899
900 <!-- Signed in: your spaces. The + (shown only where this home server offers
901 provisioning) opens the create screen below. -->
902 <div id="vault" class="hidden screen">
903 <div class="vault-head">
904 <h2>Your Eleven spaces</h2>
905 <button id="open-create" class="icon-add hidden" type="button" aria-label="Create a new space"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M8 2v12M2 8h12" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg></button>
906 <!-- The account menu (issue #1098's single control): everything about
907 the account itself hangs off this one kebab — "Sign-in options"
908 (the #methods step) and Sign out; account-merge joins it later. -->
909 <div class="kebab-wrap">
910 <button id="account-menu-btn" class="icon-kebab" type="button" aria-label="Account menu"
911 aria-haspopup="menu" aria-expanded="false"><svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" fill="currentColor"><circle cx="8" cy="3" r="1.5"/><circle cx="8" cy="8" r="1.5"/><circle cx="8" cy="13" r="1.5"/></svg></button>
912 <div id="account-menu" class="menu-pop hidden" role="menu" aria-label="Account menu">
913 <button id="how-signin" type="button" role="menuitem">Sign-in options</button>
914 <button id="signout" type="button" role="menuitem">Sign out</button>
915 </div>
916 </div>
917 </div>
918 <input id="space-search" class="hidden" type="search" inputmode="search"
919 autocomplete="off" spellcheck="false" placeholder="Search spaces" aria-label="Search your spaces">
920 <!-- The sealed-list state (an email/Apple sign-in): the account is real and
921 the token works, but the list only opens under a passkey's PRF — the
922 identity-vs-key split, made visible. The prompt IS the trigger. -->
923 <div id="vault-sealed" class="hidden">
924 <button id="unlock-passkey" type="button" class="secondary"><svg class="pk-ico" viewBox="0 0 16 16" width="15" height="15" fill="currentColor" aria-hidden="true"><circle cx="6" cy="4.4" r="2.7"/><path d="M1.2 13.9a4.8 4.8 0 0 1 9.6 0z"/><path fill-rule="evenodd" d="M12.1 7.3a2.1 2.1 0 0 1 2.06 2.52l1.6 1.6-1.2 1.2-.7-.7-.65.65-.85-.85-.62.62a2.1 2.1 0 1 1 .36-4.94zm-.9 1.6a.8.8 0 1 0 0 1.6.8.8 0 0 0 0-1.6z"/></svg>Unlock your spaces with your passkey</button>
925 </div>
926 <div id="spaces"></div>
927 <p class="status" id="vault-status"></p>
928 </div>
929
930 <!-- Sign-in options (issue #1098): every way into this account, reached
931 from the account kebab. Rows come from GET /api/anchors, each with its
932 own small kebab (Add another …, Remove — removal keeps an inline
933 are-you-sure beat; the server's last-method refusal shows verbatim).
934 A kind with ZERO linked entries gets a big first-add button below the
935 list instead (home.js builds both from METHOD_KINDS). Every flow is a
936 SERIAL card step — the confirm beat, add-email, and add-passkey each
937 swap the whole card, one screen at a time, each with a back. -->
938 <div id="methods" class="hidden screen">
939 <button id="methods-back" class="icon-back" type="button" aria-label="Back to your spaces"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
940 <h2 class="create-title" id="methods-title" tabindex="-1">Sign-in options</h2>
941 <p class="create-sub">Every way into your account. Add a second way so
942 you're never locked out, or remove one you no longer use.</p>
943 <div id="methods-list"></div>
944 <div id="methods-add" class="actions"></div>
945 <p class="status" id="methods-status"></p>
946 <!-- The combine doorway (#1098): a link-mode 409 ("already signs in
947 somewhere else") grows this offer under whichever status line showed
948 it — home.js moves the node beside the error (list or add-email
949 step) and reveals it. -->
950 <div id="combine-offer" class="actions hidden">
951 <button id="combine-offer-go" class="secondary" type="button">Combine the accounts</button>
952 </div>
953 </div>
954
955 <!-- Confirm it's you — its own serial step, FIRST whenever the proof
956 pocket is empty: list → here → the add step (or the removal) → list.
957 home.js offers EVERY door the account can actually run — the server
958 takes a fresh proof from any of them — last-used first (the email
959 button unfolds into the code mini-form). -->
960 <div id="methods-confirm" class="hidden screen">
961 <button id="methods-confirm-back" class="icon-back" type="button" aria-label="Back to sign-in options"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
962 <h2 class="create-title" id="methods-confirm-title" tabindex="-1">Confirm it&rsquo;s you</h2>
963 <p class="create-sub" id="methods-confirm-why"></p>
964 <div class="actions" id="methods-confirm-actions">
965 <button id="methods-confirm-passkey" class="secondary hidden" type="button">Confirm with your passkey</button>
966 <button id="methods-confirm-apple" class="apple-btn hidden" type="button"><svg viewBox="0 0 814 1000" width="14" height="17" fill="currentColor" aria-hidden="true"><path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/></svg>Confirm with Apple</button>
967 <button id="methods-confirm-google" class="google-btn hidden" type="button"><svg viewBox="0 0 48 48" width="16" height="16" aria-hidden="true"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>Confirm with Google</button>
968 <button id="methods-confirm-microsoft" class="ms-btn hidden" type="button"><svg viewBox="0 0 21 21" width="15" height="15" aria-hidden="true"><rect x="1" y="1" width="9" height="9" fill="#F25022"/><rect x="11" y="1" width="9" height="9" fill="#7FBA00"/><rect x="1" y="11" width="9" height="9" fill="#00A4EF"/><rect x="11" y="11" width="9" height="9" fill="#FFB900"/></svg>Confirm with Microsoft</button>
969 <button id="methods-confirm-github" class="gh-btn hidden" type="button"><svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>Confirm with GitHub</button>
970 <button id="methods-confirm-email" class="secondary hidden" type="button">Email me a code</button>
971 </div>
972 <div id="methods-cemail" class="email-mini hidden">
973 <form id="methods-cemail-form">
974 <input id="methods-cemail-addr" type="email" required autocomplete="email"
975 placeholder="you@example.com" aria-label="The email address you sign in with">
976 <button id="methods-cemail-send" type="submit">Send code</button>
977 </form>
978 <form id="methods-cemail-code-form" class="hidden">
979 <input id="methods-cemail-code" type="text" required inputmode="numeric" pattern="[0-9]{6}"
980 maxlength="6" autocomplete="one-time-code" placeholder="6-digit code"
981 aria-label="The 6-digit code from your email">
982 <button id="methods-cemail-verify" type="submit">Continue</button>
983 </form>
984 </div>
985 <p class="status" id="methods-confirm-status"></p>
986 </div>
987
988 <!-- Add an email address — its own serial step (the sign-in door's
989 two-step shape: address → code), posting verify with link:true. -->
990 <div id="methods-email" class="hidden screen">
991 <button id="methods-email-back" class="icon-back" type="button" aria-label="Back to sign-in options"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
992 <h2 class="create-title" id="methods-email-title" tabindex="-1">Add an email address</h2>
993 <p class="create-sub">A 6-digit code confirms it's yours. We never keep the
994 address — only a fingerprint that lets you sign in with it.</p>
995 <div class="email-mini">
996 <form id="methods-email-form">
997 <input id="methods-email-addr" type="email" required autocomplete="email"
998 placeholder="you@example.com" aria-label="Your email address">
999 <button id="methods-email-send" type="submit">Send code</button>
1000 </form>
1001 <form id="methods-email-code-form" class="hidden">
1002 <input id="methods-email-code" type="text" required inputmode="numeric" pattern="[0-9]{6}"
1003 maxlength="6" autocomplete="one-time-code" placeholder="6-digit code"
1004 aria-label="The 6-digit code from your email">
1005 <button id="methods-email-verify" type="submit">Add email</button>
1006 </form>
1007 </div>
1008 <p class="status" id="methods-email-status"></p>
1009 </div>
1010
1011 <!-- Add another passkey — its own serial step: one button, so the
1012 ceremony fires inside a fresh tap (Safari activation) with the
1013 proof-carrying options prefetched when the step opened. -->
1014 <div id="methods-passkey" class="hidden screen">
1015 <button id="methods-passkey-back" class="icon-back" type="button" aria-label="Back to sign-in options"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
1016 <h2 class="create-title" id="methods-passkey-title" tabindex="-1">Add another passkey</h2>
1017 <p class="create-sub">Create a passkey on this device or in your password
1018 manager — one more one-tap way into this account.</p>
1019 <div class="actions">
1020 <button id="methods-passkey-create" class="secondary" type="button">Create the passkey</button>
1021 </div>
1022 <p class="status" id="methods-passkey-status"></p>
1023 </div>
1024
1025 <!-- Combine your accounts (#1098) — the serial step behind the 409 offer.
1026 Three stages, one visible at a time (home.js drives): prove the OTHER
1027 account is yours (a fresh email code, or an Apple round trip via the
1028 merge-apple intent), sort out its saved spaces (unlock with ITS
1029 passkey and bring them over; or, behind an inline are-you-sure,
1030 combine without them — destroying its sealed list), then the merge
1031 call moves every sign-in method and device over. Cancel anywhere
1032 leaves both accounts untouched — nothing destructive happens before
1033 the explicit beats. -->
1034 <div id="methods-combine" class="hidden screen">
1035 <button id="combine-back" class="icon-back" type="button" aria-label="Back to sign-in options"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
1036 <h2 class="create-title" id="combine-title" tabindex="-1">Combine your accounts</h2>
1037 <p class="create-sub">Merge your spaces list from two accounts.</p>
1038 <div id="combine-prove" class="hidden">
1039 <p class="anchor-note" id="combine-prove-note"></p>
1040 <div id="combine-code" class="email-mini hidden">
1041 <form id="combine-code-form">
1042 <input id="combine-code-input" type="text" required inputmode="numeric" pattern="[0-9]{6}"
1043 maxlength="6" autocomplete="one-time-code" placeholder="6-digit code"
1044 aria-label="The 6-digit code from your email">
1045 <button id="combine-code-verify" type="submit">Continue</button>
1046 </form>
1047 <button id="combine-resend" class="passkey-link" type="button">Send a new code</button>
1048 </div>
1049 <div class="actions">
1050 <button id="combine-oidc-go" class="hidden" type="button"></button>
1051 <button id="combine-apple-go" class="apple-btn hidden" type="button"><svg viewBox="0 0 814 1000" width="14" height="17" fill="currentColor" aria-hidden="true"><path d="M788.1 340.9c-5.8 4.5-108.2 62.2-108.2 190.5 0 148.4 130.3 200.9 134.2 202.2-.6 3.2-20.7 71.9-68.7 141.9-42.8 61.6-87.5 123.1-155.5 123.1s-85.5-39.5-164-39.5c-76.5 0-103.7 40.8-165.9 40.8s-105.6-57-155.5-127C46.7 790.7 0 663 0 541.8c0-194.4 126.4-297.5 250.8-297.5 66.1 0 121.2 43.4 162.7 43.4 39.5 0 101.1-46 176.3-46 28.5 0 130.9 2.6 198.3 99.2zm-234-181.5c31.1-36.9 53.1-88.1 53.1-139.3 0-7.1-.6-14.3-1.9-20.1-50.6 1.9-110.8 33.7-147.1 75.8-28.5 32.4-55.1 83.6-55.1 135.5 0 7.8 1.3 15.6 1.9 18.1 3.2.6 8.4 1.3 13.6 1.3 45.4 0 102.5-30.4 135.5-71.3z"/></svg>Continue with Apple</button>
1052 </div>
1053 </div>
1054 <div id="combine-vault" class="hidden">
1055 <p class="create-sub" id="combine-vault-note"></p>
1056 <div class="actions">
1057 <button id="combine-unlock" class="secondary hidden" type="button">Unlock its spaces with its passkey</button>
1058 <button id="combine-skip" class="passkey-link hidden" type="button">Combine without its spaces…</button>
1059 </div>
1060 <div id="combine-skip-confirm" class="hidden">
1061 <p class="create-sub stern-copy danger-ink" id="combine-skip-note">Without unlocking them, the
1062 other account's saved spaces list is <b>deleted forever</b> when the accounts combine —
1063 the spaces themselves keep running, but that account's saved ways back into them are
1064 lost. <b>This cannot be undone.</b></p>
1065 <div class="actions">
1066 <button id="combine-skip-yes" class="btn-danger" type="button">Delete its spaces list and combine</button>
1067 <button id="combine-skip-no" class="wide-quiet" type="button">Keep it — go back</button>
1068 </div>
1069 </div>
1070 </div>
1071 <p class="status" id="combine-status"></p>
1072 </div>
1073
1074 <!-- Name a space (create, step 1 of 2). The slider screen (step 2) then
1075 creates the space immediately — always free, never pay-gated: the plan
1076 question is Eleven's, inside the space (#1374). The tier cards below
1077 appear only when a create bounces off a gate paying genuinely fixes
1078 (the free quota, or the pool under budget pressure); picking one hands
1079 off to Stripe and the space is created the moment it's paid. -->
1080 <div id="create-plan" class="hidden screen">
1081 <button id="plan-back" class="icon-back" type="button" aria-label="Back to your spaces"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
1082 <h2 class="create-title">Create a space</h2>
1083 <label class="create-sub" for="plan-name">What's your space called?</label>
1084 <input id="plan-name" type="text" maxlength="60" autocomplete="off" placeholder="e.g. The Campbells">
1085 <div id="plan-tiers-wrap" class="hidden">
1086 <p class="create-sub" id="plan-tiers-lead">Pick a plan to create it now:</p>
1087 <div id="plan-tiers" role="group" aria-label="Pick a plan"></div>
1088 <p class="fx-note hidden" id="plan-fx"></p>
1089 </div>
1090 <div class="actions">
1091 <button id="plan-continue" class="secondary" type="button" disabled>Continue</button>
1092 </div>
1093 <p class="status" id="plan-status"></p>
1094 </div>
1095
1096 <!-- Create a space: a focused screen — one line, one button. On success the
1097 pitch is replaced by a single-serve confirmation with a Done. -->
1098 <div id="create" class="hidden screen">
1099 <button id="create-back" class="icon-back" type="button" aria-label="Back to your spaces"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
1100 <div id="create-pitch">
1101 <h2 class="create-title">Slide to create</h2>
1102 <p class="create-sub" id="create-sub-line">Your new space will be created immediately.</p>
1103 <!-- The goes-to-eleven fader (fader.js): slide 1→10, hold the pull for a
1104 beat and it snaps to eleven — which fires the real create. -->
1105 <div class="strip">
1106 <div class="meter" id="fader-meter"></div>
1107 <div class="fader" id="fader">
1108 <div class="slot"></div>
1109 <div class="cap" role="slider" aria-label="Slide to create"
1110 aria-valuemin="1" aria-valuemax="11" aria-valuenow="1" tabindex="0"><div class="stripe"></div></div>
1111 </div>
1112 </div>
1113 <p class="status" id="new-space-status"></p>
1114 </div>
1115 <div id="create-building" class="hidden">
1116 <div class="build-spin" aria-hidden="true"></div>
1117 <h3>Hold on…</h3>
1118 <p>Building your space</p>
1119 </div>
1120 <div id="new-space-result" class="hidden">
1121 <div id="new-space-body"></div>
1122 <button id="create-done" type="button" class="wide-quiet">Done</button>
1123 </div>
1124 </div>
1125
1126 <!-- Upgrade a space: pick a plan; the payment itself happens on Stripe's
1127 hosted page, so this screen is just the choice + the handoff. -->
1128 <div id="upgrade" class="hidden screen">
1129 <button id="upgrade-back" class="icon-back" type="button" aria-label="Back to your spaces"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
1130 <h2 class="create-title">Upgrade <span id="upgrade-host"></span></h2>
1131 <p class="create-sub">An upgraded space doesn't count toward your space limit.
1132 You'll pay on Stripe's secure page — we never see your card, name, or email.</p>
1133 <!-- The honor-band row (docs/pricing-bands.md): four open prices, any
1134 selectable, no questions asked. Hidden while the price book has no
1135 bands; home.js fills it. -->
1136 <div id="upgrade-bands" class="band-row hidden" role="group" aria-label="Choose your price"></div>
1137 <p id="upgrade-band-note" class="band-note"></p>
1138 <div id="upgrade-tiers" role="group" aria-label="Choose a plan"></div>
1139 <p id="upgrade-fx" class="fx-note hidden"></p>
1140 <p id="upgrade-cap" class="cap-note"></p>
1141 <p class="status" id="upgrade-status"></p>
1142 </div>
1143
1144 <!-- Keep a demo space: the in-space countdown banner deep-links here as
1145 #claim=<host>. One confirm, one call — the provisioner does the real
1146 work (ownership, quota, expiry guard); success folds the space into
1147 this account's list exactly like a create. -->
1148 <div id="claim" class="hidden screen">
1149 <button id="claim-back" class="icon-back" type="button" aria-label="Back to your spaces"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
1150 <h2 class="create-title">Keep <span id="claim-host"></span></h2>
1151 <p class="create-sub">Claiming stops this demo space's 24-hour countdown —
1152 it becomes one of your free spaces, saved to your list, and everything
1153 in it stays.</p>
1154 <div class="actions">
1155 <button id="claim-go" type="button">Keep this space</button>
1156 <a id="claim-open" class="btn-link hidden" href="#">Open your space</a>
1157 </div>
1158 <p class="status" id="claim-status"></p>
1159 </div>
1160
1161 <!-- The archive: spaces you've set aside, hidden from the main list. Bring one
1162 back (unarchive), or delete it forever behind a stern typed-confirmation —
1163 "delete your access" (held while the space reports this is its ONLY admin
1164 key), and for spaces this account created, the deeper "destroy for
1165 everyone" (fresh-passkey ceremony). -->
1166 <div id="archive" class="hidden screen">
1167 <button id="archive-back" class="icon-back" type="button" aria-label="Back to your spaces"><svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><path d="M10 3 5 8l5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
1168 <div id="archive-main">
1169 <h2 class="create-title">Archived spaces</h2>
1170 <p class="create-sub">Hidden from your list. Bring one back anytime, or remove it for good.</p>
1171 <div id="archive-list"></div>
1172 </div>
1173 <!-- Shown while the delete step's checks run (ownership + last-admin), so
1174 the stern panel appears fully assembled instead of pieces popping in.
1175 Only appears at all when the answers take more than a beat (home.js). -->
1176 <div id="archive-checking" class="hidden">
1177 <div class="build-spin" aria-hidden="true"></div>
1178 <p class="create-sub">Checking with the space…</p>
1179 </div>
1180 <div id="archive-stern" class="hidden">
1181 <h2 class="create-title danger-ink" id="stern-title">Delete your access?</h2>
1182 <p class="create-sub stern-copy" id="stern-intro">Deleting <b id="stern-name"></b> removes your only
1183 saved way back into <b id="stern-host"></b>. If you're not signed in to it on
1184 another device, you'll lose access <b>forever</b> — there will be no way back
1185 in. <b>This cannot be undone.</b></p>
1186 <!-- The last-admin hold: revealed when the space itself reports this is its
1187 ONLY admin key — deleting the last saved copy would orphan the space. -->
1188 <p class="create-sub stern-copy danger-ink hidden" id="sole-admin-note"></p>
1189 <input id="delete-arm" type="text" autocomplete="off" autocapitalize="off"
1190 spellcheck="false" aria-label="Type the space address to confirm">
1191 <button id="delete-access" type="button" class="btn-danger" disabled>Delete forever</button>
1192 <!-- Owner-only, revealed once ownership is confirmed: also destroy the space
1193 itself, for everyone. Gated by the same typed confirmation + a passkey.
1194 When the last-admin hold engages too, destroying is the only real action
1195 left, so home.js promotes this from a footnote link to THE button. -->
1196 <div id="destroy-zone" class="hidden">
1197 <p class="create-sub stern-copy" id="destroy-note">You created this space — deleting just
1198 your access keeps it <b>counting toward your space limit</b>, with nobody able to manage
1199 it. Destroying it for everyone frees that slot: every message, photo, and member in
1200 it.<span id="destroy-paid" class="hidden"> Its subscription is cancelled with it —
1201 no further charges.</span></p>
1202 <button id="destroy-forever" type="button" class="linkish danger-link" disabled>Destroy the space for everyone…</button>
1203 </div>
1204 <button id="archive-cancel" type="button" class="wide-quiet">Cancel</button>
1205 </div>
1206 <p class="status" id="archive-status"></p>
1207 </div>
1208</main>
1209
1210<!-- Reassurance lives outside the card, under it; hidden once signed in. -->
1211<p class="reassure" id="reassure">No signup required, we don't store your email
1212 or any personal details. We can't spam what we don't have.</p>
1213<p class="cookie-ask"><button id="cookie-ask" type="button">Where's the cookie banner?</button></p>
1214<!-- The invite-only pitch: its own quiet card below the main one, shown only
1215 for a signed-in, truly-empty account (renderSpaces toggles it). -->
1216<aside id="invite-note" class="invite-card hidden">
1217 <p>🎉 Need an invite? Ask your socials!</p>
1218</aside>
1219<dialog id="cookie-dialog" aria-labelledby="cookie-title">
1220 <h2 id="cookie-title">There isn't one.</h2>
1221 <p>There are no cookies. We're not tracking you. We don't want your data.</p>
1222 <p class="cookie-small">This page keeps your sign-in on your own device — and nothing about you anywhere else.</p>
1223 <button id="cookie-close" type="button" class="secondary">Nice</button>
1224</dialog>
1225<!-- The demo offer (demo.go) lives OUTSIDE the sign-in card — it isn't a
1226 sign-in. Its own tinted card: below everything on narrow screens, a
1227 tilted flap peeking out beside the main card on wide ones (see
1228 .demo-card). Newcomers only — shown when /api/features offers demos AND
1229 this browser has never signed in (the #1112 last-used marker, home.js),
1230 and only while the sign-in doors are up (the body:has rule). -->
1231<aside class="demo-card hidden" id="demo-offer" aria-label="Try a demo space">
1232 <div class="path-divider">👋 Just looking?</div>
1233 <button id="try-demo" class="secondary" type="button">Try a demo space</button>
1234 <p class="demo-hint">No sign-in needed. Demo spaces disappear after 24 hours — claiming one keeps it.</p>
1235</aside>
1236<script type="module" src="/static/home.js"></script>
1237</body>
1238</html>
1239