| 1 | /* Shared look for the home (backup) pages — /enroll, /restore, and the manage |
| 2 | page. The tokens mirror the messenger's "classic" theme: a user lands here |
| 3 | mid-flow from a chat and goes straight back, so the round trip must read as |
| 4 | one product. (The chat's own theme choice can't cross origins — passkeys and |
| 5 | localStorage are bound to the instance — so classic-neutral is the deliberate |
| 6 | default rather than a guess at the user's theme.) */ |
| 7 | |
| 8 | :root { |
| 9 | color-scheme: light dark; |
| 10 | --bg: light-dark(#f6f7fb, #15171c); |
| 11 | --panel: light-dark(#ffffff, #1d2026); |
| 12 | --ink: light-dark(#1c2333, #e7e9f0); |
| 13 | --muted: light-dark(#5a6170, #9aa1b3); |
| 14 | --border: light-dark(#e7e9f0, rgba(255, 255, 255, 0.10)); |
| 15 | --accent: light-dark(#5666f0, #7d8bff); |
| 16 | --accent-ink: light-dark(#ffffff, #0a0e2a); |
| 17 | --quiet: light-dark(#eef0f6, #2a2e38); |
| 18 | --shadow: light-dark(rgba(20, 30, 80, 0.10), rgba(0, 0, 0, 0.55)); |
| 19 | --danger: light-dark(#c2410c, #f0936b); |
| 20 | --ok: light-dark(#237a41, #58b97e); /* dark enough for AA on the panel at status size */ |
| 21 | /* Plain SF (system), not the rounded variant — the rounded display face read |
| 22 | as toy-like at heading sizes and was explicitly voted off the page. */ |
| 23 | --font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
| 24 | } |
| 25 | |
| 26 | * { box-sizing: border-box; } |
| 27 | |
| 28 | body { |
| 29 | margin: 0; |
| 30 | min-height: 100svh; |
| 31 | padding: 24px 16px calc(32px + env(safe-area-inset-bottom)); |
| 32 | font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; |
| 33 | background: var(--bg); |
| 34 | color: var(--ink); |
| 35 | display: grid; |
| 36 | place-items: center; |
| 37 | } |
| 38 | |
| 39 | /* The ceremony card (enroll / restore) — same geometry as the messenger's |
| 40 | join/onboarding card, so arriving here doesn't feel like leaving the app. */ |
| 41 | .card { |
| 42 | background: var(--panel); |
| 43 | border: 1px solid var(--border); |
| 44 | border-radius: 24px; |
| 45 | padding: 28px; |
| 46 | width: 100%; |
| 47 | max-width: 420px; |
| 48 | box-shadow: 0 12px 40px var(--shadow); |
| 49 | text-align: center; |
| 50 | } |
| 51 | .card h1 { margin: 14px 0 6px; font-size: 22px; font-family: var(--font-display); } |
| 52 | .card .sub { margin: 0 0 20px; color: var(--muted); font-size: 15px; } |
| 53 | |
| 54 | /* The entry tile — the confirmation gate. It names exactly what's being saved |
| 55 | (or unlocked), so it gets the hero spot: the chat's photo (or a chat glyph) |
| 56 | wearing a key badge that seals to a check once the backup is written. */ |
| 57 | .entry { display: flex; flex-direction: column; align-items: center; gap: 2px; } |
| 58 | .entry-badge { position: relative; width: 72px; height: 72px; margin-bottom: 10px; } |
| 59 | .entry-photo { width: 72px; height: 72px; border-radius: 18px; object-fit: cover; display: block; } |
| 60 | .entry-glyph { |
| 61 | width: 72px; height: 72px; border-radius: 18px; |
| 62 | display: grid; place-items: center; |
| 63 | background: color-mix(in srgb, var(--accent) 14%, var(--panel)); |
| 64 | color: var(--accent); |
| 65 | } |
| 66 | .entry-glyph svg { width: 34px; height: 34px; } |
| 67 | .badge { |
| 68 | position: absolute; right: -8px; bottom: -8px; |
| 69 | width: 30px; height: 30px; border-radius: 50%; |
| 70 | display: grid; place-items: center; |
| 71 | background: var(--accent); color: var(--accent-ink); |
| 72 | border: 3px solid var(--panel); |
| 73 | transition: background 0.25s ease; |
| 74 | } |
| 75 | .badge svg { width: 15px; height: 15px; } |
| 76 | .badge .check { display: none; } |
| 77 | .badge.sealed { background: var(--ok); color: #fff; } |
| 78 | .badge.sealed .key { display: none; } |
| 79 | .badge.sealed .check { display: block; } |
| 80 | .entry .name { font-weight: 700; font-size: 18px; font-family: var(--font-display); } |
| 81 | .entry .host { color: var(--muted); font-size: 13px; overflow-wrap: anywhere; } |
| 82 | |
| 83 | /* Action buttons stack full-width; flow steps that swap in a question (the |
| 84 | replace confirmation) style its text via .actions p. */ |
| 85 | .actions { display: flex; flex-direction: column; gap: 10px; } |
| 86 | .actions p { margin: 0 0 4px; font-size: 15px; } |
| 87 | |
| 88 | /* The two-intent divider: "Sign in" above it, the create path for people who |
| 89 | are new below — both visible up front, so nobody has to fail a ceremony to |
| 90 | discover which path was theirs. Muted rules either side of a short label; |
| 91 | --muted is the status-text colour, so contrast holds in both themes. */ |
| 92 | .path-divider { |
| 93 | display: flex; align-items: center; gap: 10px; |
| 94 | margin: 2px 0; color: var(--muted); font-size: 13px; |
| 95 | } |
| 96 | .path-divider::before, .path-divider::after { |
| 97 | content: ""; flex: 1; height: 1px; background: var(--border); |
| 98 | } |
| 99 | |
| 100 | /* The demo offer sits under the create path, quieter still: the honest |
| 101 | one-liner does the explaining (--muted keeps contrast in both themes). */ |
| 102 | #demo-offer { display: flex; flex-direction: column; gap: 8px; } |
| 103 | #demo-offer.hidden { display: none; } |
| 104 | .demo-hint { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.4; } |
| 105 | |
| 106 | button { |
| 107 | font: inherit; |
| 108 | font-weight: 600; |
| 109 | cursor: pointer; |
| 110 | border: none; |
| 111 | border-radius: 999px; |
| 112 | padding: 11px 18px; |
| 113 | background: var(--accent); |
| 114 | color: var(--accent-ink); |
| 115 | line-height: 1.2; |
| 116 | } |
| 117 | button.secondary { background: var(--quiet); color: var(--ink); } |
| 118 | button:disabled { opacity: 0.5; cursor: default; } |
| 119 | button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } |
| 120 | |
| 121 | input { |
| 122 | font: inherit; |
| 123 | border: 1px solid var(--border); |
| 124 | border-radius: 12px; |
| 125 | padding: 10px 14px; |
| 126 | background: var(--panel); |
| 127 | color: var(--ink); |
| 128 | } |
| 129 | input:focus-visible { outline: 2px solid var(--accent); border-color: transparent; } |
| 130 | |
| 131 | /* CLI paste handoff: the sealed sign-in code a terminal user copies back. |
| 132 | Monospace + break-anywhere so the base64url block wraps cleanly. */ |
| 133 | .code-wrap { display: grid; gap: 10px; margin-top: 14px; justify-items: center; } |
| 134 | .code { |
| 135 | font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; |
| 136 | width: 100%; |
| 137 | box-sizing: border-box; |
| 138 | border: 1px solid var(--border); |
| 139 | border-radius: 12px; |
| 140 | padding: 10px 12px; |
| 141 | background: var(--panel); |
| 142 | color: var(--ink); |
| 143 | word-break: break-all; |
| 144 | resize: none; |
| 145 | } |
| 146 | .code:focus-visible { outline: 2px solid var(--accent); border-color: transparent; } |
| 147 | |
| 148 | /* Inline status only — never a floating toast (product-wide rule). */ |
| 149 | .status { margin: 14px 0 0; font-size: 14px; color: var(--muted); } |
| 150 | .status.error { color: var(--danger); } |
| 151 | .status.ok { color: var(--ok); } |
| 152 | .status:empty { display: none; } |
| 153 | /* a ceremony step in flight gets a small inline spinner */ |
| 154 | .status.busy::before { |
| 155 | content: ""; |
| 156 | display: inline-block; vertical-align: -2px; |
| 157 | width: 13px; height: 13px; margin-right: 8px; |
| 158 | border-radius: 50%; |
| 159 | border: 2px solid color-mix(in srgb, var(--ink) 15%, transparent); |
| 160 | border-top-color: var(--accent); |
| 161 | animation: home-spin 0.7s linear infinite; |
| 162 | } |
| 163 | @keyframes home-spin { to { transform: rotate(360deg); } } |
| 164 | |
| 165 | /* Text swapped in for the final "all done" state eases into place instead of |
| 166 | snapping mid-glance. */ |
| 167 | .swap-in { animation: home-swap-in 0.35s ease; } |
| 168 | @keyframes home-swap-in { from { opacity: 0; transform: translateY(3px); } } |
| 169 | |
| 170 | @media (prefers-reduced-motion: reduce) { |
| 171 | .status.busy::before { animation: none; opacity: 0.5; } |
| 172 | .badge { transition: none; } |
| 173 | .swap-in { animation: none; } |
| 174 | } |
| 175 | |
| 176 | /* The single-serve "space is ready" confirmation: a sealed-check moment, then a |
| 177 | full-width "set up" link and a quiet Done. Centered within the create screen. */ |
| 178 | #new-space-result { margin-top: 10px; text-align: center; } |
| 179 | #new-space-result p { margin: 0 0 12px; } |
| 180 | .ok-check { |
| 181 | width: 46px; height: 46px; margin: 6px auto 12px; border-radius: 50%; |
| 182 | display: grid; place-items: center; background: var(--ok); color: #fff; |
| 183 | } |
| 184 | .ok-check svg { width: 22px; height: 22px; } |
| 185 | .created-name { font-family: var(--font-display); font-weight: 400; font-size: 17px; margin: 0 0 4px; } |
| 186 | a.btn-link { |
| 187 | display: block; |
| 188 | font: inherit; |
| 189 | font-weight: 600; |
| 190 | text-align: center; |
| 191 | text-decoration: none; |
| 192 | border-radius: 999px; |
| 193 | padding: 11px 18px; |
| 194 | background: var(--accent); |
| 195 | color: var(--accent-ink); |
| 196 | line-height: 1.2; |
| 197 | } |
| 198 | a.btn-link:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } |
| 199 | |
| 200 | .hidden { display: none; } |
| 201 | |