| 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’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 | |