eleven / ocho Public

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

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: the who-pays picker, shown only when a create bounces off a
540 gate paying fixes. Selection mirrors the band chips — border + weight,
541 never colour alone. */
542 #create-plan { text-align: center; }
543 #plan-tiers-wrap { margin-top: 16px; }
544 #plan-tiers { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
545 .tier-btn.selected { border-color: var(--accent); border-width: 2px; padding: 11px 13px; font-weight: 600; }
546 .tier-btn .tier-sub { color: var(--muted); font-size: .78rem; flex-basis: 100%; text-align: left; }
547 .tier-btn.wrap { flex-wrap: wrap; }
548 #create-plan .actions { margin-top: 16px; }
549
550 /* The honor-band row: four open prices, one selected — selection is shown
551 by weight + border, never colour alone. */
552 .band-row { display: flex; justify-content: center; gap: 6px; margin-top: 12px; flex-wrap: wrap; }
553 .band-chip {
554 font: inherit; font-size: .82rem; cursor: pointer; padding: 5px 12px;
555 min-height: 28px; border-radius: 999px;
556 color: var(--ink); background: none; border: 1px solid var(--border);
557 }
558 .band-chip.selected { border-color: var(--accent); border-width: 2px; font-weight: 700; padding: 4px 11px; }
559 .band-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
560 .band-note, .fx-note, .cap-note { color: var(--muted); font-size: .8rem; margin: 8px 0 0; }
561 .fx-note .fx-currency {
562 font: inherit; font-size: .8rem; color: var(--ink);
563 background: var(--quiet); border: 1px solid var(--border); border-radius: 6px; padding: 1px 4px;
564 }
565 /* The tier badge on an upgraded space's row, with Manage beside it. */
566 .badge-row { display: inline-flex; align-items: center; gap: 8px; align-self: flex-start; margin-top: 2px; }
567 .row-manage {
568 font: inherit; font-size: .62rem; font-weight: 600; cursor: pointer;
569 padding: 1px 8px; border-radius: 999px; line-height: inherit;
570 color: var(--muted); background: none; border: 1px solid var(--border);
571 /* Same visual height as the badge; the tap target stays ≥24px via an
572 invisible hit-area extension, not visual bulk. */
573 position: relative;
574 }
575 .row-manage::before { content: ""; position: absolute; inset: -6px; border-radius: 999px; }
576 /* "ends 11 Jul 2027" beside a cancelled subscription's badge. */
577 .sub-ends { color: var(--muted); font-size: .72rem; }
578 .row-manage:hover { color: var(--accent); border-color: var(--accent); }
579 .row-manage:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
580 .row-manage:disabled { opacity: .4; cursor: default; }
581 .supporter-badge {
582 font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
583 color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
584 border-radius: 999px; padding: 1px 7px;
585 }
586
587 /* The inline "Archived <name> · Undo" note — occupies layout, never a toast. */
588 .undo-note {
589 display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
590 margin: 0 0 10px; padding: 8px 12px; border-radius: 10px;
591 background: var(--quiet); color: var(--muted); font-size: .85rem;
592 }
593 /* The "Archived (N)" foot button: a quiet full-width row that opens the archive. */
594 .archived-open {
595 display: flex; align-items: center; justify-content: space-between; gap: 8px;
596 width: 100%; margin-top: 12px; padding: 9px 12px; border-radius: 10px;
597 font: inherit; font-size: .9rem; font-weight: 600; cursor: pointer; text-align: left;
598 color: var(--muted); background: none; border: 1px solid var(--border);
599 transition: background .12s ease, color .12s ease;
600 }
601 .archived-open > span:first-child { display: inline-flex; align-items: center; gap: 8px; }
602 .archived-open svg { display: block; opacity: .85; }
603 .archived-open .chev { font-size: 1.1rem; line-height: 1; color: var(--muted); }
604 .archived-open:hover { background: var(--quiet); color: var(--ink); }
605 .archived-open:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
606 @media (prefers-reduced-motion: reduce) { .archived-open { transition: none; } }
607
608 /* Archive list rows: name + Unarchive + Delete pills. Quiet, non-draggable. */
609 ul.servers.archived li { gap: 6px; }
610 ul.servers.archived li .name { font-weight: 600; line-height: 1.25; color: var(--ink); }
611 ul.servers.archived li .srv-main { text-align: left; }
612 .pill {
613 flex: none; font: inherit; font-size: .8rem; font-weight: 600; cursor: pointer;
614 padding: 5px 10px; border-radius: 999px;
615 color: var(--ink); background: var(--quiet); border: 1px solid transparent;
616 }
617 .pill:hover { filter: brightness(0.97); }
618 .pill.danger-ink { color: var(--danger); }
619 .pill:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
620 #archive-list > p.empty { color: var(--muted); font-size: .9rem; margin: 6px 0 0; }
621 /* Section headings: "Starred" and "More" — only shown once some are starred. */
622 .spaces-head {
623 font-size: .7rem; text-transform: uppercase; letter-spacing: .05em;
624 color: var(--muted); font-family: var(--font-display); margin: 12px 0 3px;
625 }
626 .spaces-head:first-child { margin-top: 0; }
627 #spaces > p.empty { color: var(--muted); font-size: .9rem; margin: 6px 0 0; }
628 /* Fuzzy search over the list — appears once there are a few spaces to sift. */
629 #space-search {
630 width: 100%; margin: 4px 0 12px; padding: 8px 12px;
631 font: inherit; font-size: .9rem;
632 color: var(--ink); background: var(--quiet);
633 border: 1px solid transparent; border-radius: 10px;
634 }
635 #space-search::placeholder { color: var(--muted); }
636 #space-search:focus-visible { outline: none; border-color: var(--accent); background: var(--panel); }
637 ul.servers { list-style: none; margin: 0; padding: 0; }
638 ul.servers li {
639 display: flex; align-items: center; gap: 8px;
640 padding: 5px 6px; border-radius: 10px;
641 /* No separator lines: the hover wash (below) is the interactive cue, and the
642 lifted row + the gap it leaves carry the structure while dragging. */
643 }
644 /* Tight line-heights so a row's name + meta lines read as one compact block,
645 not airy lines (the card body sets 1.5, which is too loose for a list). */
646 .srv-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
647 ul.servers li a.name { font-weight: 600; line-height: 1.25; color: var(--ink); text-decoration: none; }
648 ul.servers li a.name:hover { text-decoration: underline; }
649 /* The storage meter's reading, under the name — quieter still. */
650 ul.servers li .srv-usage { color: var(--muted); opacity: .75; font-size: .72rem; line-height: 1.3; }
651 /* Star toggle: a quiet outline star that fills gold when on. */
652 .star {
653 flex: none; background: none; border: none; padding: 2px; cursor: pointer;
654 font-size: 18px; line-height: 1; color: var(--muted);
655 }
656 .star.on { color: #f2b705; }
657 .star:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
658
659 /* Drag-to-reorder. Idle rows are calm; hover reveals the affordance — a soft
660 brand-tinted wash on the row, and a grip that fades in on the right. Grabbing
661 lifts the row off the card (shadow) while the others slide aside. */
662 .drag-handle {
663 flex: none; display: grid; place-items: center; width: 22px; height: 30px;
664 padding: 0; border: none; background: none; color: var(--muted);
665 border-radius: 7px; cursor: grab; touch-action: none;
666 -webkit-tap-highlight-color: transparent;
667 opacity: 0; transition: opacity .12s ease, background .12s ease, color .12s ease;
668 }
669 .drag-handle svg { fill: currentColor; display: block; }
670 .drag-handle:hover { background: var(--quiet); color: var(--ink); }
671 .drag-handle:active { cursor: grabbing; }
672 .drag-handle:focus-visible { opacity: 1; outline: 2px solid var(--accent); outline-offset: 1px; }
673 @media (hover: hover) {
674 ul.servers li:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }
675 ul.servers li:hover .drag-handle { opacity: .5; }
676 }
677 @media (hover: none) { .drag-handle { opacity: .4; } } /* touch: no hover to reveal it */
678 ul.servers li.dragging {
679 background: var(--panel); box-shadow: 0 10px 28px var(--shadow);
680 position: relative; z-index: 3; cursor: grabbing;
681 }
682 ul.servers li.dragging .drag-handle { opacity: 1; color: var(--ink); }
683 @media (prefers-reduced-motion: reduce) { .drag-handle { transition: none; } }
684
685 /* Ambient smoke + motes (bootscene.js), pinned behind ALL content (z-index -1,
686 above the page bg but under the card/text). Started on load for everyone and
687 left running — the page's living backdrop. Dimmed as a background; fuller
688 during the loader splash (html.booting), so it recedes as the content lands. */
689 .boot-canvas {
690 position: fixed; inset: 0; width: 100%; height: 100%;
691 z-index: -1; pointer-events: none;
692 opacity: .4; transition: opacity .6s ease;
693 }
694 html.booting .boot-canvas { opacity: 1; }
695 /* The returning-member loading chrome: the Eleven lockup + tagline centred over
696 the smoke while the spaces list decrypts. Shown only when html.booting (a
697 live session, set pre-paint), which also hides the content beneath so the
698 Sign-in screen never flashes. On finish bootscene.js fades this out
699 (.cover-out) and drops html.booting — the card fades in, the smoke stays. */
700 .app-loading {
701 position: fixed; inset: 0; z-index: 2;
702 display: none; place-items: center; align-content: center;
703 transition: opacity .5s ease;
704 }
705 html.booting .app-loading { display: grid; }
706 /* A session refresh shouldn't feel like a journey: on the non-siwa booting
707 paths the pageantry — the lockup chrome and the smoke's full ramp —
708 holds for 500ms before appearing at all. The cover still BLOCKS from
709 frame zero (the content-hiding rules below are undelayed); only its
710 visibility waits. A load that lifts inside the window never shows
711 chrome: bootscene's fast path reveals the content near-instantly
712 (html.boot-fast trims the content fade to an imperceptible beat, and
713 comes off right after so the stage-clear fades keep their timing). The
714 siwa journey face (html.siwa-wait) deliberately keeps its frame-zero
715 chrome. Opacity-only, so it stays under reduced motion like the fades. */
716 html.booting:not(.siwa-wait) .app-loading { animation: cover-wait .45s ease .5s both; }
717 @keyframes cover-wait { from { opacity: 0; } to { opacity: 1; } }
718 html.booting:not(.siwa-wait) .boot-canvas { transition-delay: .5s; }
719 html.boot-fast main.card, html.boot-fast .reassure,
720 html.boot-fast .demo-card, html.boot-fast .cookie-ask { transition-duration: .08s; }
721 /* Hide the content (card, its outside-the-card reassurance, and the demo
722 card) beneath the loader so nothing flashes; all fade back in when
723 booting drops. */
724 html.booting main.card, html.booting .reassure, html.booting .demo-card, html.booting .cookie-ask,
725 html.composing main.card, html.composing .reassure, html.composing .demo-card, html.composing .cookie-ask { opacity: 0; }
726 /* The composing lift is quick — a plain visitor shouldn't feel a fade. */
727 html.compose-reveal main.card, html.compose-reveal .reassure,
728 html.compose-reveal .demo-card, html.compose-reveal .cookie-ask { transition-duration: .12s; }
729 main.card, .reassure, .demo-card, .cookie-ask { transition: opacity .5s ease; }
730 /* cover-out keeps the chrome displayed (booting has already dropped) so the
731 fade actually plays; the timeout in finishHomeLoader then parks it. */
732 .app-loading.cover-out { display: grid; opacity: 0; pointer-events: none; }
733 .boot-brand { display: flex; flex-direction: column; align-items: center; gap: 10px; }
734 .boot-goesto {
735 position: absolute; left: 0; right: 0; bottom: 18px; margin: 0;
736 text-align: center; color: var(--muted); font-size: 13px; letter-spacing: .02em;
737 }
738 /* The Apple round trip's waiting face (html.siwa-wait, both legs of the
739 trip): the same cover, but a ring where the lockup sits — no Eleven
740 brand until the sign-in has actually confirmed. Verify success drops
741 siwa-wait and stamps .brand-in: the lockup fades in (~400ms), dwells a
742 beat, then the cover lifts while the spaces screen slides up beneath it
743 (#vault.arrive). Reduced motion: instant swaps, no slide — the ring's
744 own static-under-reduce rule is .build-spin's. */
745 .boot-ring { display: none; }
746 html.siwa-wait .app-loading .boot-ring { display: block; }
747 html.siwa-wait .app-loading .boot-brand,
748 html.siwa-wait .app-loading .boot-goesto { display: none; }
749 .app-loading.brand-in .boot-brand,
750 .app-loading.brand-in .boot-goesto { animation: brand-in .4s ease both; }
751 @keyframes brand-in { from { opacity: 0; } }
752 #vault.arrive { animation: vault-arrive .35s ease-out; }
753 @keyframes vault-arrive { from { opacity: 0; transform: translateY(14px); } }
754 @media (prefers-reduced-motion: reduce) {
755 .app-loading.brand-in .boot-brand,
756 .app-loading.brand-in .boot-goesto { animation: none; }
757 #vault.arrive { animation: none; }
758 }
759</style>
760<script>
761 // Pre-paint gate: if this browser holds a live session — the tab's own, or
762 // a stay-signed-in one in localStorage (home_session_expires is a plain
763 // epoch-ms key exactly so this check needs no JSON) — or is landing back
764 // from Apple's sign-in (#siwa= fragment) or the mail's magic link (#email=
765 // fragment), show the loading cover from the very first frame, so no path
766 // ever flashes the Sign-in screen. Both fragment landings get the WAITING
767 // face (smoke + ring, no lockup — siwa-wait): the brand only lands once the
768 // sign-in has confirmed. Signed-out visitors skip it all; home.js lifts the
769 // cover on every path it was raised for.
770 try {
771 // The card never paints uncomposed: composeCard() decides its doors
772 // (last-used/platform), so the default markup must not get a frame.
773 // home.js lifts this at the end of module init on every path.
774 document.documentElement.classList.add("composing");
775 if (location.hash.indexOf("#siwa=") === 0 || location.hash.indexOf("#oidc=") === 0 ||
776 location.hash.indexOf("#oauth=") === 0 || location.hash.indexOf("#email=") === 0)
777 document.documentElement.classList.add("booting", "siwa-wait");
778 else if ((sessionStorage.getItem("home_token") && sessionStorage.getItem("home_dek")) ||
779 +localStorage.getItem("home_session_expires") > Date.now())
780 document.documentElement.classList.add("booting");
781 } catch (e) {}
782</script>
783</head>
784<body class="landing">
785<!-- Persistent ambient background (bootscene.js): soft smoke + rising dust motes
786 drifting behind everything. Started on load for all visitors and left running
787 — the returning-member loading chrome (below) fades over it, then lifts to
788 leave the smoke as the page's living backdrop. -->
789<canvas class="boot-canvas" id="boot-canvas" aria-hidden="true"></canvas>
790<!-- Stage for the create-fader's dust + confetti (fader.js) — full-viewport,
791 behind the card, drawn only while the create screen is live. -->
792<canvas id="stage-dust" aria-hidden="true"></canvas>
793<!-- The returning-member loading chrome: the Eleven lockup + tagline over the
794 smoke while the spaces list decrypts, so a refresh doesn't flash the Sign-in
795 screen first. Shown only when html.booting (a live session — see the
796 pre-paint gate above); it fades out once the list is ready, and the smoke
797 behind it stays. -->
798<div id="app-loading" class="app-loading" aria-hidden="true">
799 <div class="boot-brand">
800 <img class="brand-mark" src="/static/icon-512.png" width="64" height="64" alt="Eleven" />
801 <div class="brand-word">Eleven</div>
802 </div>
803 <!-- The Apple wait face (html.siwa-wait): a ring where the lockup sits —
804 the brand only lands once the sign-in has confirmed. -->
805 <div class="build-spin boot-ring" aria-hidden="true"></div>
806 <p class="boot-goesto">The group chat of dreams</p>
807</div>
808<main class="card">
809 <div class="brand">
810 <img class="brand-mark" src="/static/icon-512.png" width="64" height="64" alt="Eleven" />
811 <div class="brand-word">Eleven</div>
812 </div>
813
814 <!-- The signed-out doors; hidden once you're in (see showVault). home.js
815 composes them (#1112): ONE primary door — the last-used method when
816 this browser has one, else the platform default — with the email door
817 beneath it (unless it IS the primary) and the quiet "Other ways to
818 sign in" link to the all-ways step below. Demoted doors don't render
819 here. Apple and email are find-or-create, one button each, no sign-in
820 vs sign-up split. Create-a-passkey appears only after a failed
821 ceremony (#newhere). -->
822 <div id="signed-out">
823 <div class="actions" id="main-doors">
824 <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>
825 <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>
826 <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>
827 <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>
828 <button id="email-signin" class="secondary" type="button">Continue with email</button>
829 <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>
830 <button id="other-ways" class="passkey-link" type="button">Other ways to sign in</button>
831 </div>
832
833 <!-- Revealed only after a failed or cancelled passkey ceremony — the one
834 place the create path lives now. Cancelled and no-passkey are
835 deliberately indistinguishable (NotAllowedError either way), so the
836 explainer serves both readings: a returning member likely picked the
837 wrong passkey — try again — and someone genuinely new creates theirs
838 here. -->
839 <div id="newhere" class="hidden screen">
840 <p><strong>That didn't work?</strong> It might just be the wrong passkey —
841 your Eleven sign-in is one key that opens every space you belong to, so
842 if you've signed in before, try again and choose it from the list.</p>
843 <div class="path-divider">New here?</div>
844 <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>
845 </div>
846 </div>
847
848 <!-- The email door (email.go), a card step of its own — the doors swap out
849 for it (same screen-swap pattern as the signed-in screens): address →
850 code → session. Forms so Enter submits and native validation gates;
851 home.js drives the steps. -->
852 <div id="email-door" class="hidden screen">
853 <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>
854 <h2 class="create-title">Sign in with email</h2>
855 <div id="email-flow">
856 <form id="email-form">
857 <input id="email-addr" type="email" required autocomplete="email"
858 placeholder="you@example.com" aria-label="Your email address">
859 <button id="email-send" type="submit">Send code</button>
860 </form>
861 <p class="anchor-note" id="email-note">We never keep your email address. No spam, ever.</p>
862 <form id="email-code-form" class="hidden">
863 <input id="email-code" type="text" required inputmode="numeric" pattern="[0-9]{6}"
864 maxlength="6" autocomplete="one-time-code" placeholder="6-digit code"
865 aria-label="The 6-digit code from your email">
866 <button id="email-verify" type="submit">Sign in</button>
867 </form>
868 </div>
869 </div>
870
871 <!-- The all-ways step (#1112): every door on one card, reached from the
872 quiet "Other ways to sign in" link. Same screen-swap pattern as the
873 email door (it hides #signed-out, so the greeter/cookie/reassure
874 visibility rules follow along), and each row invokes the SAME handler
875 as the main card's version of that door. -->
876 <div id="otherways" class="hidden screen">
877 <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>
878 <h2 class="create-title">Other ways to sign in</h2>
879 <p class="create-sub">We don't store your data no matter what method you use.</p>
880 <div class="actions">
881 <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>
882 <button id="otherways-email" class="secondary" type="button">Continue with email</button>
883 </div>
884 <div class="path-divider">For those oppressed by convenience…</div>
885 <div class="actions">
886 <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>
887 <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>
888 <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>
889 <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>
890 </div>
891 </div>
892
893 <!-- Inline status only — never a floating toast (product-wide rule). -->
894 <p class="status" id="status"></p>
895
896 <!-- Signed in: your spaces. The + (shown only where this home server offers
897 provisioning) opens the create screen below. -->
898 <div id="vault" class="hidden screen">
899 <div class="vault-head">
900 <h2>Your Eleven spaces</h2>
901 <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>
902 <!-- The account menu (issue #1098's single control): everything about
903 the account itself hangs off this one kebab — "Sign-in options"
904 (the #methods step) and Sign out; account-merge joins it later. -->
905 <div class="kebab-wrap">
906 <button id="account-menu-btn" class="icon-kebab" type="button" aria-label="Account menu"
907 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>
908 <div id="account-menu" class="menu-pop hidden" role="menu" aria-label="Account menu">
909 <button id="how-signin" type="button" role="menuitem">Sign-in options</button>
910 <button id="signout" type="button" role="menuitem">Sign out</button>
911 </div>
912 </div>
913 </div>
914 <input id="space-search" class="hidden" type="search" inputmode="search"
915 autocomplete="off" spellcheck="false" placeholder="Search spaces" aria-label="Search your spaces">
916 <!-- The sealed-list state (an email/Apple sign-in): the account is real and
917 the token works, but the list only opens under a passkey's PRF — the
918 identity-vs-key split, made visible. The prompt IS the trigger. -->
919 <div id="vault-sealed" class="hidden">
920 <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>
921 </div>
922 <div id="spaces"></div>
923 <p class="status" id="vault-status"></p>
924 </div>
925
926 <!-- Sign-in options (issue #1098): every way into this account, reached
927 from the account kebab. Rows come from GET /api/anchors, each with its
928 own small kebab (Add another …, Remove — removal keeps an inline
929 are-you-sure beat; the server's last-method refusal shows verbatim).
930 A kind with ZERO linked entries gets a big first-add button below the
931 list instead (home.js builds both from METHOD_KINDS). Every flow is a
932 SERIAL card step — the confirm beat, add-email, and add-passkey each
933 swap the whole card, one screen at a time, each with a back. -->
934 <div id="methods" class="hidden screen">
935 <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>
936 <h2 class="create-title" id="methods-title" tabindex="-1">Sign-in options</h2>
937 <p class="create-sub">Every way into your account. Add a second way so
938 you're never locked out, or remove one you no longer use.</p>
939 <div id="methods-list"></div>
940 <div id="methods-add" class="actions"></div>
941 <p class="status" id="methods-status"></p>
942 <!-- The combine doorway (#1098): a link-mode 409 ("already signs in
943 somewhere else") grows this offer under whichever status line showed
944 it — home.js moves the node beside the error (list or add-email
945 step) and reveals it. -->
946 <div id="combine-offer" class="actions hidden">
947 <button id="combine-offer-go" class="secondary" type="button">Combine the accounts</button>
948 </div>
949 </div>
950
951 <!-- Confirm it's you — its own serial step, FIRST whenever the proof
952 pocket is empty: list → here → the add step (or the removal) → list.
953 home.js offers EVERY door the account can actually run — the server
954 takes a fresh proof from any of them — last-used first (the email
955 button unfolds into the code mini-form). -->
956 <div id="methods-confirm" class="hidden screen">
957 <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>
958 <h2 class="create-title" id="methods-confirm-title" tabindex="-1">Confirm it&rsquo;s you</h2>
959 <p class="create-sub" id="methods-confirm-why"></p>
960 <div class="actions" id="methods-confirm-actions">
961 <button id="methods-confirm-passkey" class="secondary hidden" type="button">Confirm with your passkey</button>
962 <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>
963 <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>
964 <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>
965 <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>
966 <button id="methods-confirm-email" class="secondary hidden" type="button">Email me a code</button>
967 </div>
968 <div id="methods-cemail" class="email-mini hidden">
969 <form id="methods-cemail-form">
970 <input id="methods-cemail-addr" type="email" required autocomplete="email"
971 placeholder="you@example.com" aria-label="The email address you sign in with">
972 <button id="methods-cemail-send" type="submit">Send code</button>
973 </form>
974 <form id="methods-cemail-code-form" class="hidden">
975 <input id="methods-cemail-code" type="text" required inputmode="numeric" pattern="[0-9]{6}"
976 maxlength="6" autocomplete="one-time-code" placeholder="6-digit code"
977 aria-label="The 6-digit code from your email">
978 <button id="methods-cemail-verify" type="submit">Continue</button>
979 </form>
980 </div>
981 <p class="status" id="methods-confirm-status"></p>
982 </div>
983
984 <!-- Add an email address — its own serial step (the sign-in door's
985 two-step shape: address → code), posting verify with link:true. -->
986 <div id="methods-email" class="hidden screen">
987 <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>
988 <h2 class="create-title" id="methods-email-title" tabindex="-1">Add an email address</h2>
989 <p class="create-sub">A 6-digit code confirms it's yours. We never keep the
990 address — only a fingerprint that lets you sign in with it.</p>
991 <div class="email-mini">
992 <form id="methods-email-form">
993 <input id="methods-email-addr" type="email" required autocomplete="email"
994 placeholder="you@example.com" aria-label="Your email address">
995 <button id="methods-email-send" type="submit">Send code</button>
996 </form>
997 <form id="methods-email-code-form" class="hidden">
998 <input id="methods-email-code" type="text" required inputmode="numeric" pattern="[0-9]{6}"
999 maxlength="6" autocomplete="one-time-code" placeholder="6-digit code"
1000 aria-label="The 6-digit code from your email">
1001 <button id="methods-email-verify" type="submit">Add email</button>
1002 </form>
1003 </div>
1004 <p class="status" id="methods-email-status"></p>
1005 </div>
1006
1007 <!-- Add another passkey — its own serial step: one button, so the
1008 ceremony fires inside a fresh tap (Safari activation) with the
1009 proof-carrying options prefetched when the step opened. -->
1010 <div id="methods-passkey" class="hidden screen">
1011 <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>
1012 <h2 class="create-title" id="methods-passkey-title" tabindex="-1">Add another passkey</h2>
1013 <p class="create-sub">Create a passkey on this device or in your password
1014 manager — one more one-tap way into this account.</p>
1015 <div class="actions">
1016 <button id="methods-passkey-create" class="secondary" type="button">Create the passkey</button>
1017 </div>
1018 <p class="status" id="methods-passkey-status"></p>
1019 </div>
1020
1021 <!-- Combine your accounts (#1098) — the serial step behind the 409 offer.
1022 Three stages, one visible at a time (home.js drives): prove the OTHER
1023 account is yours (a fresh email code, or an Apple round trip via the
1024 merge-apple intent), sort out its saved spaces (unlock with ITS
1025 passkey and bring them over; or, behind an inline are-you-sure,
1026 combine without them — destroying its sealed list), then the merge
1027 call moves every sign-in method and device over. Cancel anywhere
1028 leaves both accounts untouched — nothing destructive happens before
1029 the explicit beats. -->
1030 <div id="methods-combine" class="hidden screen">
1031 <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>
1032 <h2 class="create-title" id="combine-title" tabindex="-1">Combine your accounts</h2>
1033 <p class="create-sub">Merge your spaces list from two accounts.</p>
1034 <div id="combine-prove" class="hidden">
1035 <p class="anchor-note" id="combine-prove-note"></p>
1036 <div id="combine-code" class="email-mini hidden">
1037 <form id="combine-code-form">
1038 <input id="combine-code-input" type="text" required inputmode="numeric" pattern="[0-9]{6}"
1039 maxlength="6" autocomplete="one-time-code" placeholder="6-digit code"
1040 aria-label="The 6-digit code from your email">
1041 <button id="combine-code-verify" type="submit">Continue</button>
1042 </form>
1043 <button id="combine-resend" class="passkey-link" type="button">Send a new code</button>
1044 </div>
1045 <div class="actions">
1046 <button id="combine-oidc-go" class="hidden" type="button"></button>
1047 <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>
1048 </div>
1049 </div>
1050 <div id="combine-vault" class="hidden">
1051 <p class="create-sub" id="combine-vault-note"></p>
1052 <div class="actions">
1053 <button id="combine-unlock" class="secondary hidden" type="button">Unlock its spaces with its passkey</button>
1054 <button id="combine-skip" class="passkey-link hidden" type="button">Combine without its spaces…</button>
1055 </div>
1056 <div id="combine-skip-confirm" class="hidden">
1057 <p class="create-sub stern-copy danger-ink" id="combine-skip-note">Without unlocking them, the
1058 other account's saved spaces list is <b>deleted forever</b> when the accounts combine —
1059 the spaces themselves keep running, but that account's saved ways back into them are
1060 lost. <b>This cannot be undone.</b></p>
1061 <div class="actions">
1062 <button id="combine-skip-yes" class="btn-danger" type="button">Delete its spaces list and combine</button>
1063 <button id="combine-skip-no" class="wide-quiet" type="button">Keep it — go back</button>
1064 </div>
1065 </div>
1066 </div>
1067 <p class="status" id="combine-status"></p>
1068 </div>
1069
1070 <!-- The plan screen: NOT part of the happy path. Creating a space is the
1071 slider and nothing else — no name asked (Eleven asks inside the space,
1072 where the answer is a message in a conversation, not a form field), no
1073 plan asked (always free at birth, #1374). This screen appears only when
1074 a create bounces off a gate paying genuinely fixes (the free quota, or
1075 the pool under budget pressure); picking a tier hands off to Stripe and
1076 the space is created the moment it's paid. -->
1077 <div id="create-plan" class="hidden screen">
1078 <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>
1079 <h2 class="create-title">Create a space</h2>
1080 <div id="plan-tiers-wrap" class="hidden">
1081 <p class="create-sub" id="plan-tiers-lead">Pick a plan to create it now:</p>
1082 <div id="plan-tiers" role="group" aria-label="Pick a plan"></div>
1083 <p class="fx-note hidden" id="plan-fx"></p>
1084 </div>
1085 <div class="actions">
1086 <button id="plan-continue" class="secondary" type="button">Continue</button>
1087 </div>
1088 <p class="status" id="plan-status"></p>
1089 </div>
1090
1091 <!-- Create a space: a focused screen — one line, one button. On success the
1092 pitch is replaced by a single-serve confirmation with a Done. -->
1093 <div id="create" class="hidden screen">
1094 <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>
1095 <div id="create-pitch">
1096 <h2 class="create-title">Slide to create</h2>
1097 <p class="create-sub" id="create-sub-line">Your new space will be created immediately.</p>
1098 <!-- The goes-to-eleven fader (fader.js): slide 1→10, hold the pull for a
1099 beat and it snaps to eleven — which fires the real create. -->
1100 <div class="strip">
1101 <div class="meter" id="fader-meter"></div>
1102 <div class="fader" id="fader">
1103 <div class="slot"></div>
1104 <div class="cap" role="slider" aria-label="Slide to create"
1105 aria-valuemin="1" aria-valuemax="11" aria-valuenow="1" tabindex="0"><div class="stripe"></div></div>
1106 </div>
1107 </div>
1108 <p class="status" id="new-space-status"></p>
1109 </div>
1110 <div id="create-building" class="hidden">
1111 <div class="build-spin" aria-hidden="true"></div>
1112 <h3>Hold on…</h3>
1113 <p>Building your space</p>
1114 </div>
1115 <div id="new-space-result" class="hidden">
1116 <div id="new-space-body"></div>
1117 <button id="create-done" type="button" class="wide-quiet">Done</button>
1118 </div>
1119 </div>
1120
1121 <!-- Upgrade a space: pick a plan; the payment itself happens on Stripe's
1122 hosted page, so this screen is just the choice + the handoff. -->
1123 <div id="upgrade" class="hidden screen">
1124 <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>
1125 <h2 class="create-title">Upgrade <span id="upgrade-host"></span></h2>
1126 <p class="create-sub">An upgraded space doesn't count toward your space limit.
1127 You'll pay on Stripe's secure page — we never see your card, name, or email.</p>
1128 <!-- The honor-band row (docs/pricing-bands.md): four open prices, any
1129 selectable, no questions asked. Hidden while the price book has no
1130 bands; home.js fills it. -->
1131 <div id="upgrade-bands" class="band-row hidden" role="group" aria-label="Choose your price"></div>
1132 <p id="upgrade-band-note" class="band-note"></p>
1133 <div id="upgrade-tiers" role="group" aria-label="Choose a plan"></div>
1134 <p id="upgrade-fx" class="fx-note hidden"></p>
1135 <p id="upgrade-cap" class="cap-note"></p>
1136 <p class="status" id="upgrade-status"></p>
1137 </div>
1138
1139 <!-- Keep a demo space: the in-space countdown banner deep-links here as
1140 #claim=<host>. One confirm, one call — the provisioner does the real
1141 work (ownership, quota, expiry guard); success folds the space into
1142 this account's list exactly like a create. -->
1143 <div id="claim" class="hidden screen">
1144 <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>
1145 <h2 class="create-title">Keep <span id="claim-host"></span></h2>
1146 <p class="create-sub">Claiming stops this demo space's 24-hour countdown —
1147 it becomes one of your free spaces, saved to your list, and everything
1148 in it stays.</p>
1149 <div class="actions">
1150 <button id="claim-go" type="button">Keep this space</button>
1151 <a id="claim-open" class="btn-link hidden" href="#">Open your space</a>
1152 </div>
1153 <p class="status" id="claim-status"></p>
1154 </div>
1155
1156 <!-- The archive: spaces you've set aside, hidden from the main list. Bring one
1157 back (unarchive), or delete it forever behind a stern typed-confirmation —
1158 "delete your access" (held while the space reports this is its ONLY admin
1159 key), and for spaces this account created, the deeper "destroy for
1160 everyone" (fresh-passkey ceremony). -->
1161 <div id="archive" class="hidden screen">
1162 <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>
1163 <div id="archive-main">
1164 <h2 class="create-title">Archived spaces</h2>
1165 <p class="create-sub">Hidden from your list. Bring one back anytime, or remove it for good.</p>
1166 <div id="archive-list"></div>
1167 </div>
1168 <!-- Shown while the delete step's checks run (ownership + last-admin), so
1169 the stern panel appears fully assembled instead of pieces popping in.
1170 Only appears at all when the answers take more than a beat (home.js). -->
1171 <div id="archive-checking" class="hidden">
1172 <div class="build-spin" aria-hidden="true"></div>
1173 <p class="create-sub">Checking with the space…</p>
1174 </div>
1175 <div id="archive-stern" class="hidden">
1176 <h2 class="create-title danger-ink" id="stern-title">Delete your access?</h2>
1177 <p class="create-sub stern-copy" id="stern-intro">Deleting <b id="stern-name"></b> removes your only
1178 saved way back into <b id="stern-host"></b>. If you're not signed in to it on
1179 another device, you'll lose access <b>forever</b> — there will be no way back
1180 in. <b>This cannot be undone.</b></p>
1181 <!-- The last-admin hold: revealed when the space itself reports this is its
1182 ONLY admin key — deleting the last saved copy would orphan the space. -->
1183 <p class="create-sub stern-copy danger-ink hidden" id="sole-admin-note"></p>
1184 <input id="delete-arm" type="text" autocomplete="off" autocapitalize="off"
1185 spellcheck="false" aria-label="Type the space address to confirm">
1186 <button id="delete-access" type="button" class="btn-danger" disabled>Delete forever</button>
1187 <!-- Owner-only, revealed once ownership is confirmed: also destroy the space
1188 itself, for everyone. Gated by the same typed confirmation + a passkey.
1189 When the last-admin hold engages too, destroying is the only real action
1190 left, so home.js promotes this from a footnote link to THE button. -->
1191 <div id="destroy-zone" class="hidden">
1192 <p class="create-sub stern-copy" id="destroy-note">You created this space — deleting just
1193 your access keeps it <b>counting toward your space limit</b>, with nobody able to manage
1194 it. Destroying it for everyone frees that slot: every message, photo, and member in
1195 it.<span id="destroy-paid" class="hidden"> Its subscription is cancelled with it —
1196 no further charges.</span></p>
1197 <button id="destroy-forever" type="button" class="linkish danger-link" disabled>Destroy the space for everyone…</button>
1198 </div>
1199 <button id="archive-cancel" type="button" class="wide-quiet">Cancel</button>
1200 </div>
1201 <p class="status" id="archive-status"></p>
1202 </div>
1203</main>
1204
1205<!-- Reassurance lives outside the card, under it; hidden once signed in. -->
1206<p class="reassure" id="reassure">No signup required, we don't store your email
1207 or any personal details. We can't spam what we don't have.</p>
1208<p class="cookie-ask"><button id="cookie-ask" type="button">Where's the cookie banner?</button></p>
1209<!-- The invite-only pitch: its own quiet card below the main one, shown only
1210 for a signed-in, truly-empty account (renderSpaces toggles it). -->
1211<aside id="invite-note" class="invite-card hidden">
1212 <p>🎉 Need an invite? Ask your socials!</p>
1213</aside>
1214<dialog id="cookie-dialog" aria-labelledby="cookie-title">
1215 <h2 id="cookie-title">There isn't one.</h2>
1216 <p>There are no cookies. We're not tracking you. We don't want your data.</p>
1217 <p class="cookie-small">This page keeps your sign-in on your own device — and nothing about you anywhere else.</p>
1218 <button id="cookie-close" type="button" class="secondary">Nice</button>
1219</dialog>
1220<!-- The demo offer (demo.go) lives OUTSIDE the sign-in card — it isn't a
1221 sign-in. Its own tinted card: below everything on narrow screens, a
1222 tilted flap peeking out beside the main card on wide ones (see
1223 .demo-card). Newcomers only — shown when /api/features offers demos AND
1224 this browser has never signed in (the #1112 last-used marker, home.js),
1225 and only while the sign-in doors are up (the body:has rule). -->
1226<aside class="demo-card hidden" id="demo-offer" aria-label="Try a demo space">
1227 <div class="path-divider">👋 Just looking?</div>
1228 <button id="try-demo" class="secondary" type="button">Try a demo space</button>
1229 <p class="demo-hint">No sign-in needed. Demo spaces disappear after 24 hours — claiming one keeps it.</p>
1230</aside>
1231<script type="module" src="/static/home.js"></script>
1232</body>
1233</html>
1234