sealkeep 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/ARCHITECTURE.md +201 -0
  2. package/CHANGELOG.md +218 -0
  3. package/CONTROL_PLANE.md +86 -0
  4. package/LICENSE +34 -0
  5. package/README.md +249 -0
  6. package/THIRD_PARTY.md +22 -0
  7. package/THREAT_MODEL.md +107 -0
  8. package/dist/packages/vaultline-crypto/src/aead.d.ts +12 -0
  9. package/dist/packages/vaultline-crypto/src/aead.js +24 -0
  10. package/dist/packages/vaultline-crypto/src/chunk-access.d.ts +39 -0
  11. package/dist/packages/vaultline-crypto/src/chunk-access.js +93 -0
  12. package/dist/packages/vaultline-crypto/src/envelope.d.ts +71 -0
  13. package/dist/packages/vaultline-crypto/src/envelope.js +188 -0
  14. package/dist/packages/vaultline-crypto/src/format.d.ts +106 -0
  15. package/dist/packages/vaultline-crypto/src/format.js +43 -0
  16. package/dist/packages/vaultline-crypto/src/index.d.ts +5 -0
  17. package/dist/packages/vaultline-crypto/src/index.js +5 -0
  18. package/dist/packages/vaultline-crypto/src/recipients.d.ts +42 -0
  19. package/dist/packages/vaultline-crypto/src/recipients.js +129 -0
  20. package/dist/packages/vaultline-crypto/src/sha256-stream.d.ts +41 -0
  21. package/dist/packages/vaultline-crypto/src/sha256-stream.js +206 -0
  22. package/dist/packages/vaultline-crypto/src/stream.d.ts +139 -0
  23. package/dist/packages/vaultline-crypto/src/stream.js +477 -0
  24. package/dist/site/index.html +1542 -0
  25. package/dist/site.zip +0 -0
  26. package/dist/src/activity.d.ts +22 -0
  27. package/dist/src/activity.js +52 -0
  28. package/dist/src/adapters.d.ts +212 -0
  29. package/dist/src/adapters.js +533 -0
  30. package/dist/src/audit.d.ts +24 -0
  31. package/dist/src/audit.js +41 -0
  32. package/dist/src/autopilot.d.ts +77 -0
  33. package/dist/src/autopilot.js +148 -0
  34. package/dist/src/bip39-wordlist.d.ts +15 -0
  35. package/dist/src/bip39-wordlist.js +272 -0
  36. package/dist/src/branding.d.ts +31 -0
  37. package/dist/src/branding.js +31 -0
  38. package/dist/src/chunk-store.d.ts +142 -0
  39. package/dist/src/chunk-store.js +502 -0
  40. package/dist/src/cli.d.ts +2 -0
  41. package/dist/src/cli.js +2035 -0
  42. package/dist/src/cloud.d.ts +434 -0
  43. package/dist/src/cloud.js +851 -0
  44. package/dist/src/control-plane/auth.d.ts +62 -0
  45. package/dist/src/control-plane/auth.js +123 -0
  46. package/dist/src/control-plane/server.d.ts +31 -0
  47. package/dist/src/control-plane/server.js +263 -0
  48. package/dist/src/control-plane/store.d.ts +101 -0
  49. package/dist/src/control-plane/store.js +82 -0
  50. package/dist/src/control-plane-cli.d.ts +2 -0
  51. package/dist/src/control-plane-cli.js +37 -0
  52. package/dist/src/control-plane-server.d.ts +10 -0
  53. package/dist/src/control-plane-server.js +11 -0
  54. package/dist/src/control-plane.d.ts +78 -0
  55. package/dist/src/control-plane.js +61 -0
  56. package/dist/src/crypto.d.ts +56 -0
  57. package/dist/src/crypto.js +132 -0
  58. package/dist/src/daemon.d.ts +52 -0
  59. package/dist/src/daemon.js +142 -0
  60. package/dist/src/dashboard-cli.d.ts +2 -0
  61. package/dist/src/dashboard-cli.js +20 -0
  62. package/dist/src/disk.d.ts +110 -0
  63. package/dist/src/disk.js +169 -0
  64. package/dist/src/doctor.d.ts +11 -0
  65. package/dist/src/doctor.js +198 -0
  66. package/dist/src/enroll.d.ts +27 -0
  67. package/dist/src/enroll.js +136 -0
  68. package/dist/src/errors.d.ts +26 -0
  69. package/dist/src/errors.js +23 -0
  70. package/dist/src/heartbeat.d.ts +89 -0
  71. package/dist/src/heartbeat.js +120 -0
  72. package/dist/src/index-sync.d.ts +53 -0
  73. package/dist/src/index-sync.js +147 -0
  74. package/dist/src/leakscan.d.ts +48 -0
  75. package/dist/src/leakscan.js +222 -0
  76. package/dist/src/local-api.d.ts +132 -0
  77. package/dist/src/local-api.js +1757 -0
  78. package/dist/src/managed-chunks.d.ts +55 -0
  79. package/dist/src/managed-chunks.js +108 -0
  80. package/dist/src/mcp-install.d.ts +52 -0
  81. package/dist/src/mcp-install.js +140 -0
  82. package/dist/src/mcp.d.ts +1 -0
  83. package/dist/src/mcp.js +59 -0
  84. package/dist/src/migrate.d.ts +35 -0
  85. package/dist/src/migrate.js +88 -0
  86. package/dist/src/mnemonic.d.ts +60 -0
  87. package/dist/src/mnemonic.js +134 -0
  88. package/dist/src/net.d.ts +2 -0
  89. package/dist/src/net.js +16 -0
  90. package/dist/src/notify.d.ts +46 -0
  91. package/dist/src/notify.js +84 -0
  92. package/dist/src/offload.d.ts +117 -0
  93. package/dist/src/offload.js +331 -0
  94. package/dist/src/onboarding.d.ts +10 -0
  95. package/dist/src/onboarding.js +44 -0
  96. package/dist/src/packages.d.ts +126 -0
  97. package/dist/src/packages.js +114 -0
  98. package/dist/src/passkey.d.ts +26 -0
  99. package/dist/src/passkey.js +54 -0
  100. package/dist/src/password-lock.d.ts +19 -0
  101. package/dist/src/password-lock.js +156 -0
  102. package/dist/src/paths.d.ts +9 -0
  103. package/dist/src/paths.js +24 -0
  104. package/dist/src/providers/gcs.d.ts +133 -0
  105. package/dist/src/providers/gcs.js +235 -0
  106. package/dist/src/providers/gdrive.d.ts +156 -0
  107. package/dist/src/providers/gdrive.js +335 -0
  108. package/dist/src/providers/index.d.ts +45 -0
  109. package/dist/src/providers/index.js +74 -0
  110. package/dist/src/providers/s3.d.ts +174 -0
  111. package/dist/src/providers/s3.js +345 -0
  112. package/dist/src/providers/sigv4.d.ts +78 -0
  113. package/dist/src/providers/sigv4.js +112 -0
  114. package/dist/src/queue.d.ts +185 -0
  115. package/dist/src/queue.js +286 -0
  116. package/dist/src/recovery.d.ts +40 -0
  117. package/dist/src/recovery.js +132 -0
  118. package/dist/src/rehydrate.d.ts +43 -0
  119. package/dist/src/rehydrate.js +66 -0
  120. package/dist/src/restore.d.ts +34 -0
  121. package/dist/src/restore.js +80 -0
  122. package/dist/src/retention.d.ts +251 -0
  123. package/dist/src/retention.js +446 -0
  124. package/dist/src/rotate.d.ts +47 -0
  125. package/dist/src/rotate.js +95 -0
  126. package/dist/src/search.d.ts +147 -0
  127. package/dist/src/search.js +677 -0
  128. package/dist/src/secrets.d.ts +86 -0
  129. package/dist/src/secrets.js +220 -0
  130. package/dist/src/service.d.ts +73 -0
  131. package/dist/src/service.js +197 -0
  132. package/dist/src/share.d.ts +34 -0
  133. package/dist/src/share.js +68 -0
  134. package/dist/src/spool.d.ts +97 -0
  135. package/dist/src/spool.js +213 -0
  136. package/dist/src/start-tui.d.ts +17 -0
  137. package/dist/src/start-tui.js +113 -0
  138. package/dist/src/start.d.ts +75 -0
  139. package/dist/src/start.js +101 -0
  140. package/dist/src/storage-setup.d.ts +49 -0
  141. package/dist/src/storage-setup.js +222 -0
  142. package/dist/src/storage-targets.d.ts +40 -0
  143. package/dist/src/storage-targets.js +147 -0
  144. package/dist/src/stream-to-cloud.d.ts +76 -0
  145. package/dist/src/stream-to-cloud.js +820 -0
  146. package/dist/src/sync-rules.d.ts +85 -0
  147. package/dist/src/sync-rules.js +125 -0
  148. package/dist/src/trash.d.ts +15 -0
  149. package/dist/src/trash.js +63 -0
  150. package/dist/src/tui.d.ts +18 -0
  151. package/dist/src/tui.js +179 -0
  152. package/dist/src/types.d.ts +191 -0
  153. package/dist/src/types.js +3 -0
  154. package/dist/src/ui-server.d.ts +187 -0
  155. package/dist/src/ui-server.js +293 -0
  156. package/dist/src/ui.d.ts +41 -0
  157. package/dist/src/ui.js +102 -0
  158. package/dist/src/update.d.ts +30 -0
  159. package/dist/src/update.js +56 -0
  160. package/dist/src/upload.d.ts +46 -0
  161. package/dist/src/upload.js +80 -0
  162. package/dist/src/vault.d.ts +208 -0
  163. package/dist/src/vault.js +812 -0
  164. package/dist/src/watcher.d.ts +34 -0
  165. package/dist/src/watcher.js +121 -0
  166. package/dist/src/worker.d.ts +52 -0
  167. package/dist/src/worker.js +190 -0
  168. package/package.json +65 -0
  169. package/web/app.js +1372 -0
  170. package/web/index.html +476 -0
  171. package/web/rail.js +308 -0
  172. package/web/retention.html +17 -0
  173. package/web/rules-view.js +249 -0
  174. package/web/sessions-view.js +448 -0
  175. package/web/sessions.html +17 -0
  176. package/web/setup-api.js +181 -0
  177. package/web/setup-logic.js +394 -0
  178. package/web/setup.html +419 -0
  179. package/web/setup.js +697 -0
  180. package/web/style.css +990 -0
package/web/style.css ADDED
@@ -0,0 +1,990 @@
1
+ /*
2
+ * Vaultline dashboard.
3
+ *
4
+ * Two worlds, one rule between them. Above the line is what you can still read:
5
+ * source paths, agent names, sizes. Below it is what only you can open. The
6
+ * boundary strip in the header is that rule, drawn with live numbers on each side.
7
+ *
8
+ * Type: monospace is the product's native voice, so it carries display and data.
9
+ * Prose sits in the system sans to stay readable at small sizes.
10
+ */
11
+
12
+ :root {
13
+ /* Vaultline design system — the ten tokens from site/index.html, the source
14
+ of truth (see design-system/README.md). The older dashboard names below are
15
+ kept as aliases so existing rules resolve to the system's colours without a
16
+ full rename. The page is achromatic: the ONE saturated block is ciphertext
17
+ (--seal); --live marks what is interactive or currently true; --warn is
18
+ rationed to where inaction loses data. */
19
+ --paper: #f4f4f3;
20
+ --card: #fbfbfa;
21
+ --ink: #111316;
22
+ --soft: #5e646c;
23
+ --rule: #dadcde;
24
+ --seal: #101a3d;
25
+ --seal-ink: #96a6e0;
26
+ --live: #2a3fb0;
27
+ --warn: #8c3220;
28
+ --warn-bg: #f4e6e2;
29
+
30
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, "DejaVu Sans Mono", Consolas, monospace;
31
+ /* One typeface: monospace, including display sizes. --sans is aliased to it so
32
+ any rule still asking for a second face complies instead of breaking the
33
+ rule the whole system is built on. */
34
+ --sans: var(--mono);
35
+
36
+ /* Compatibility aliases: prior dashboard rules reference these names. */
37
+ --panel: var(--card);
38
+ --rule-soft: #e6e7e9;
39
+ --text: var(--ink);
40
+ --text-2: var(--soft);
41
+ --text-3: #8b9099;
42
+ --signal: var(--live);
43
+ --verified: var(--live);
44
+ --held: var(--soft);
45
+ --alert: var(--warn);
46
+ /* Were dark-navy fills for the old rail; now the solid-ink fill the system
47
+ uses for a filled control. Lazy substitution keeps them theme-correct. */
48
+ --ink-2: var(--ink);
49
+ --ink-3: var(--soft);
50
+ /* Rail is now part of the light page, not a dark slab. These name faint
51
+ surfaces used for nav hover/active and glyphs. */
52
+ --rail-active: #ecedf0;
53
+ --rail-glyph: #9aa0a8;
54
+
55
+ --rail: 264px;
56
+ --gap: 20px;
57
+ }
58
+
59
+ /* ── Dark ─────────────────────────────────────────────── */
60
+ /*
61
+ * Tokens only, never components.
62
+ *
63
+ * Dark mode used to exist on exactly two pages, each with its own private
64
+ * palette, so a dark-mode machine flipped from warm paper to charcoal on the
65
+ * click into Sessions and back again on the way out. Consolidating those pages
66
+ * onto this sheet removed the flip by removing the dark side altogether, which
67
+ * is the wrong half to keep — this restores it for the whole product from one
68
+ * place, so the two can no longer disagree.
69
+ *
70
+ * The rail is already near-black in both themes, which is what lets the dark
71
+ * palette stay a change of ground rather than a redesign.
72
+ */
73
+ @media (prefers-color-scheme: dark) {
74
+ :root {
75
+ --paper: #0d0f11;
76
+ --card: #14171a;
77
+ --ink: #e7e9ec;
78
+ --soft: #8d949c;
79
+ --rule: #24282d;
80
+ --seal: #111c44;
81
+ --seal-ink: #a2b2ea;
82
+ --live: #8195f2;
83
+ --warn: #d97a62;
84
+ --warn-bg: #2b1a16;
85
+
86
+ --rule-soft: #1e2226;
87
+ --text-3: #6f757d;
88
+ --rail-active: #1c2024;
89
+ --rail-glyph: #7d838b;
90
+ }
91
+
92
+ /* Form fields take an explicit ground so they are not blind on a dark page. */
93
+ input[type="text"], input[type="number"], input[type="password"],
94
+ input[type="search"], textarea, select {
95
+ background: var(--card);
96
+ border-color: var(--rule);
97
+ color: var(--ink);
98
+ }
99
+ .check-fail { background: var(--warn-bg); }
100
+ tbody tr:hover { background: rgba(129, 149, 242, 0.08); }
101
+ }
102
+
103
+ * { box-sizing: border-box; }
104
+
105
+ body {
106
+ margin: 0;
107
+ display: flex;
108
+ min-height: 100vh;
109
+ background: var(--paper);
110
+ color: var(--text);
111
+ font-family: var(--sans);
112
+ font-size: 14px;
113
+ line-height: 1.6;
114
+ -webkit-font-smoothing: antialiased;
115
+ }
116
+
117
+ /* ── Rail ─────────────────────────────────────────────── */
118
+
119
+ aside {
120
+ width: var(--rail);
121
+ flex-shrink: 0;
122
+ background: var(--card);
123
+ color: var(--ink);
124
+ border-right: 1px solid var(--rule);
125
+ padding: 30px 16px;
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: 34px;
129
+ }
130
+
131
+ .brand {
132
+ font-family: var(--mono);
133
+ font-size: 21px;
134
+ letter-spacing: -0.03em;
135
+ padding: 0 12px;
136
+ color: var(--ink);
137
+ font-weight: 700;
138
+ }
139
+ .brand span { color: var(--live); margin-right: 6px; }
140
+
141
+ nav { display: grid; gap: 2px; }
142
+ /* The Sessions entry is an anchor rather than a view button, so it inherits
143
+ every button rule here and adds only what an anchor needs. */
144
+ nav a.navlink,
145
+ nav button {
146
+ appearance: none;
147
+ border: 0;
148
+ background: transparent;
149
+ color: var(--soft);
150
+ text-align: left;
151
+ padding: 11px 14px;
152
+ font: 500 13.5px var(--mono);
153
+ letter-spacing: -0.01em;
154
+ border-radius: 5px;
155
+ cursor: pointer;
156
+ display: flex;
157
+ align-items: baseline;
158
+ gap: 10px;
159
+ transition: background 120ms ease, color 120ms ease;
160
+ }
161
+ nav a.navlink { text-decoration: none; }
162
+ nav a.navlink:hover { background: var(--rail-active); color: var(--ink); }
163
+ /* Links get the same active treatment as view buttons: on Sessions and
164
+ Retention the current page is a link, and without this nothing in the rail
165
+ looked selected at all. */
166
+ nav a.navlink.active { background: var(--rail-active); color: var(--ink); font-weight: 700; }
167
+ nav a.navlink.active .glyph { color: var(--live); }
168
+ nav a.navlink .glyph,
169
+ nav button .glyph { color: var(--rail-glyph); transition: color 120ms ease; }
170
+ nav button:hover { background: var(--rail-active); color: var(--ink); }
171
+ nav button.active { background: var(--rail-active); color: var(--ink); font-weight: 700; }
172
+ nav button.active .glyph { color: var(--live); }
173
+ nav button .count { margin-left: auto; font-size: 11.5px; color: var(--soft); }
174
+
175
+ .privacy {
176
+ margin-top: auto;
177
+ border-top: 1px solid var(--rule);
178
+ padding: 18px 12px 0;
179
+ font-family: var(--mono);
180
+ font-size: 12px;
181
+ color: var(--soft);
182
+ line-height: 1.65;
183
+ }
184
+ .privacy b { display: block; color: var(--ink); margin-bottom: 7px; font-weight: 600; }
185
+
186
+ /* ── Main ─────────────────────────────────────────────── */
187
+
188
+ main {
189
+ flex: 1;
190
+ min-width: 0;
191
+ padding: 40px 44px 56px;
192
+ max-width: 1440px;
193
+ }
194
+
195
+ header { margin-bottom: 30px; }
196
+
197
+ h1 {
198
+ font-family: var(--mono);
199
+ font-size: 32px;
200
+ line-height: 1.15;
201
+ letter-spacing: -0.035em;
202
+ font-weight: 700;
203
+ margin: 0 0 8px;
204
+ }
205
+ .lede { margin: 0; color: var(--text-2); max-width: 62ch; }
206
+
207
+ /* Section labels are "a rule plus a word": a short hairline in the accent, then
208
+ the word at reading size — no uppercase, no tracking games. With one mono face
209
+ everywhere, the old uppercase-tracked eyebrow just read as decoration. */
210
+ h2 {
211
+ font-family: var(--mono);
212
+ font-size: 12.5px;
213
+ letter-spacing: 0;
214
+ color: var(--live);
215
+ margin: 34px 0 13px;
216
+ font-weight: 600;
217
+ }
218
+ h3 {
219
+ font-family: var(--mono);
220
+ font-size: 12px;
221
+ letter-spacing: 0;
222
+ color: var(--live);
223
+ margin: 22px 0 10px;
224
+ font-weight: 600;
225
+ }
226
+ h2::before, h3::before {
227
+ content: "";
228
+ display: inline-block;
229
+ width: 1.5rem;
230
+ height: 1px;
231
+ background: currentColor;
232
+ vertical-align: 0.28em;
233
+ margin-right: 0.6rem;
234
+ }
235
+
236
+ /* ── The boundary: the one device this page is built around ── */
237
+
238
+ .boundary {
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 14px;
242
+ margin: 26px 0 6px;
243
+ font-family: var(--mono);
244
+ font-size: 12px;
245
+ color: var(--text-3);
246
+ }
247
+ .boundary::before,
248
+ .boundary::after {
249
+ content: "";
250
+ flex: 1;
251
+ height: 1px;
252
+ background: linear-gradient(90deg, transparent, var(--rule) 18%, var(--rule) 82%, transparent);
253
+ }
254
+ .boundary b {
255
+ color: var(--signal);
256
+ font-weight: 600;
257
+ letter-spacing: 0.02em;
258
+ white-space: nowrap;
259
+ }
260
+ .boundary .side { white-space: nowrap; }
261
+ .boundary .side.sealed { color: var(--ink-3); }
262
+
263
+ /* ── Metrics ──────────────────────────────────────────── */
264
+
265
+ .metrics {
266
+ display: grid;
267
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
268
+ gap: var(--gap);
269
+ }
270
+ .metric {
271
+ background: var(--panel);
272
+ border: 1px solid var(--rule);
273
+ border-radius: 3px;
274
+ padding: 18px 20px 16px;
275
+ }
276
+ .metric b {
277
+ display: block;
278
+ font-family: var(--mono);
279
+ font-size: 30px;
280
+ letter-spacing: -0.045em;
281
+ line-height: 1.1;
282
+ font-weight: 700;
283
+ }
284
+ .metric b.small { font-size: 15px; letter-spacing: -0.02em; word-break: break-all; line-height: 1.35; }
285
+ .metric small { display: block; margin-top: 8px; font-size: 12px; color: var(--text-3); line-height: 1.5; }
286
+
287
+ /* ── Panels, tables ───────────────────────────────────── */
288
+
289
+ .panel {
290
+ background: var(--panel);
291
+ border: 1px solid var(--rule);
292
+ border-radius: 3px;
293
+ overflow: hidden;
294
+ }
295
+
296
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
297
+ th {
298
+ text-align: left;
299
+ font: 600 10.5px var(--mono);
300
+ text-transform: uppercase;
301
+ letter-spacing: 0.1em;
302
+ color: var(--text-3);
303
+ padding: 12px 18px;
304
+ border-bottom: 1px solid var(--rule);
305
+ white-space: nowrap;
306
+ }
307
+ td {
308
+ padding: 12px 18px;
309
+ border-bottom: 1px solid var(--rule-soft);
310
+ vertical-align: top;
311
+ word-break: break-word;
312
+ }
313
+ tbody tr:last-child td { border-bottom: 0; }
314
+ tbody tr:hover { background: rgba(31, 95, 255, 0.028); }
315
+ td.num, th.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
316
+ td.path { font-family: var(--mono); font-size: 12.5px; color: var(--text-2); }
317
+ td.muted { color: var(--text-3); }
318
+
319
+ /* ── Badges ───────────────────────────────────────────── */
320
+
321
+ .badge {
322
+ display: inline-block;
323
+ font: 600 11px var(--mono);
324
+ letter-spacing: 0.02em;
325
+ padding: 3px 8px;
326
+ border-radius: 3px;
327
+ white-space: nowrap;
328
+ border: 1px solid transparent;
329
+ }
330
+ .badge-verified { color: var(--verified); background: #e9f4ec; border-color: #c6e3d0; }
331
+ .badge-local { color: var(--text-3); background: #f1efec; border-color: var(--rule); }
332
+ .badge-held { color: var(--held); background: #fbf3e2; border-color: #eddcb6; }
333
+ .badge-free { color: var(--signal); background: #eaf0ff; border-color: #c6d6ff; }
334
+ .badge-alert { color: var(--alert); background: #fbeeeb; border-color: #eecabf; }
335
+
336
+ /* ── Checks ───────────────────────────────────────────── */
337
+
338
+ .check {
339
+ display: grid;
340
+ grid-template-columns: 160px 1fr;
341
+ gap: 16px;
342
+ padding: 13px 18px;
343
+ border-bottom: 1px solid var(--rule-soft);
344
+ border-left: 3px solid transparent;
345
+ font-size: 13px;
346
+ }
347
+ .check:last-child { border-bottom: 0; }
348
+ .check strong { font: 600 12.5px var(--mono); }
349
+ .check span { color: var(--text-2); }
350
+ .check-pass { border-left-color: var(--verified); }
351
+ .check-warn { border-left-color: var(--held); }
352
+ .check-fail { border-left-color: var(--alert); background: #fdf4f2; }
353
+
354
+ /* ── Agents ───────────────────────────────────────────── */
355
+
356
+ .agent {
357
+ display: grid;
358
+ grid-template-columns: 150px 1fr auto;
359
+ align-items: center;
360
+ gap: 16px;
361
+ padding: 15px 18px;
362
+ border-bottom: 1px solid var(--rule-soft);
363
+ }
364
+ .agent:last-child { border-bottom: 0; }
365
+ .agent strong { font: 600 14px var(--mono); }
366
+ .agent span { font-family: var(--mono); font-size: 12px; color: var(--text-3); word-break: break-all; }
367
+
368
+ /* ── Controls ─────────────────────────────────────────── */
369
+
370
+ button.action {
371
+ appearance: none;
372
+ font: 600 13px var(--mono);
373
+ padding: 9px 16px;
374
+ border-radius: 3px;
375
+ border: 1px solid var(--signal);
376
+ background: transparent;
377
+ color: var(--signal);
378
+ cursor: pointer;
379
+ transition: background 120ms ease, color 120ms ease;
380
+ }
381
+ button.action:hover { background: var(--signal); color: #fff; }
382
+ button.action.danger { border-color: var(--alert); color: var(--alert); }
383
+ button.action.danger:hover { background: var(--alert); color: #fff; }
384
+ button.action:disabled { border-color: var(--rule); color: var(--text-3); cursor: not-allowed; background: transparent; }
385
+
386
+ .row { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
387
+ .row .field { flex: 1; min-width: 180px; }
388
+ .actions { display: flex; gap: 10px; flex-wrap: wrap; }
389
+ .note { font-size: 12.5px; color: var(--text-3); margin: 0; line-height: 1.6; }
390
+ .pad { padding: 20px; display: grid; gap: 14px; }
391
+
392
+ /* ── Form fields: the settings screen is the first editable surface,
393
+ so text/number/select/textarea inherit the token input's own look
394
+ rather than the browser default. ───────────────────────────────── */
395
+
396
+ .field { display: grid; gap: 6px; }
397
+ /* :not(.opt) rather than letting .opt override after the cascade: a checkbox
398
+ row is a label too, and this keeps the two from fighting over specificity. */
399
+ .field > label:not(.opt) {
400
+ font-family: var(--mono);
401
+ font-size: 11px;
402
+ text-transform: uppercase;
403
+ letter-spacing: 0.1em;
404
+ color: var(--text-3);
405
+ font-weight: 600;
406
+ }
407
+ .field .note { margin-top: -2px; }
408
+
409
+ input[type="text"], input[type="number"], input[type="password"], textarea, select {
410
+ font: 13px var(--mono);
411
+ padding: 9px 11px;
412
+ border: 1px solid var(--rule);
413
+ border-radius: 3px;
414
+ background: #fff;
415
+ color: var(--text);
416
+ width: 100%;
417
+ }
418
+ textarea { resize: vertical; min-height: 4.4em; line-height: 1.5; font-size: 12.5px; }
419
+ select {
420
+ appearance: none;
421
+ background-image: linear-gradient(45deg, transparent 50%, var(--text-3) 50%), linear-gradient(135deg, var(--text-3) 50%, transparent 50%);
422
+ background-position: calc(100% - 18px) center, calc(100% - 13px) center;
423
+ background-size: 5px 5px, 5px 5px;
424
+ background-repeat: no-repeat;
425
+ padding-right: 32px;
426
+ }
427
+
428
+ .opt { display: flex; align-items: baseline; gap: 9px; font-size: 13px; cursor: pointer; }
429
+ .opt input[type="checkbox"] { accent-color: var(--signal); width: 15px; height: 15px; flex-shrink: 0; }
430
+ .opt + .opt { margin-top: 8px; }
431
+ /* A radio choice whose description sits under its title, not welded to it.
432
+ Without this the retention page read "Keep everythingNever reclaim a source
433
+ file" — two sentences with no gap, which is what an unstyled label does. */
434
+ .opt > div b, .opt > div strong { display: block; font-weight: 600; }
435
+ .opt > div span { display: block; color: var(--text-2); margin-top: 3px; line-height: 1.55; }
436
+ .opt input[type="radio"] { accent-color: var(--signal); width: 15px; height: 15px; flex-shrink: 0; margin-top: 2px; }
437
+
438
+ form h3:first-child { margin-top: 0; }
439
+
440
+ /* ── Empty states: an invitation, not an apology ───────── */
441
+
442
+ .empty { padding: 40px 24px; text-align: center; }
443
+ .empty p { margin: 0 0 6px; font: 600 14px var(--mono); }
444
+ .empty small { color: var(--text-3); font-size: 12.5px; }
445
+ .empty code, code {
446
+ font-family: var(--mono);
447
+ font-size: 12px;
448
+ background: #efece7;
449
+ border: 1px solid var(--rule);
450
+ border-radius: 3px;
451
+ padding: 2px 6px;
452
+ }
453
+
454
+ /* ── Gate ─────────────────────────────────────────────── */
455
+
456
+ .gate { max-width: 640px; }
457
+ #token-form { display: flex; gap: 10px; flex-wrap: wrap; }
458
+ #token-input {
459
+ flex: 1;
460
+ min-width: 240px;
461
+ font: 13px var(--mono);
462
+ padding: 10px 12px;
463
+ border: 1px solid var(--rule);
464
+ border-radius: 3px;
465
+ background: #fff;
466
+ color: var(--text);
467
+ }
468
+ #token-input:focus-visible, button:focus-visible, nav button:focus-visible,
469
+ input:focus-visible, textarea:focus-visible, select:focus-visible {
470
+ outline: 2px solid var(--signal);
471
+ outline-offset: 2px;
472
+ }
473
+ .error { color: var(--alert); font-size: 13px; margin: 0; }
474
+
475
+ /* ── Status pill ──────────────────────────────────────── */
476
+
477
+ .pill {
478
+ display: inline-flex;
479
+ align-items: center;
480
+ gap: 7px;
481
+ font: 500 12px var(--mono);
482
+ color: var(--text-2);
483
+ }
484
+ .pill::before {
485
+ content: "";
486
+ width: 6px;
487
+ height: 6px;
488
+ border-radius: 50%;
489
+ background: var(--text-3);
490
+ }
491
+ .pill.ok::before { background: var(--verified); }
492
+ .pill.warn::before { background: var(--held); }
493
+ .pill.busy::before { background: var(--signal); animation: pulse 1.1s ease-in-out infinite; }
494
+
495
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
496
+
497
+ footer {
498
+ margin-top: 40px;
499
+ padding-top: 18px;
500
+ border-top: 1px solid var(--rule);
501
+ display: flex;
502
+ justify-content: space-between;
503
+ gap: 20px;
504
+ flex-wrap: wrap;
505
+ font-size: 12.5px;
506
+ color: var(--text-3);
507
+ }
508
+ footer b { font: 600 12.5px var(--mono); color: var(--text-2); display: block; margin-bottom: 4px; }
509
+
510
+ [hidden] { display: none !important; }
511
+
512
+
513
+ /* ── Account card in the rail ─────────────────────────── */
514
+ /* The identity strip: first thing in the rail, right under the wordmark —
515
+ who this vault belongs to, the plan, and how much of it is used. It sits
516
+ above navigation because the design system's cards spec puts identity first;
517
+ it was previously parked at the bottom, where a paying customer's one status
518
+ read lived below the fold of attention. Enters once, settling downward. */
519
+ .acct {
520
+ padding: 2px 12px 14px;
521
+ display: grid;
522
+ gap: 9px;
523
+ border-bottom: 1px solid var(--rule);
524
+ animation: acct-in 480ms cubic-bezier(0.2, 0.7, 0.3, 1) both;
525
+ }
526
+ @keyframes acct-in {
527
+ from { opacity: 0; transform: translateY(-5px); }
528
+ to { opacity: 1; transform: none; }
529
+ }
530
+ .acct-email {
531
+ font: 11px var(--mono);
532
+ color: var(--soft);
533
+ overflow: hidden;
534
+ text-overflow: ellipsis;
535
+ white-space: nowrap;
536
+ }
537
+ .acct-plan {
538
+ position: relative;
539
+ overflow: hidden;
540
+ display: inline-flex;
541
+ align-items: center;
542
+ gap: 7px;
543
+ align-self: start;
544
+ font: 600 11.5px var(--mono);
545
+ letter-spacing: 0;
546
+ padding: 5px 11px;
547
+ border-radius: 99px;
548
+ border: 1px solid var(--rule);
549
+ color: var(--soft);
550
+ }
551
+ .acct-plan::before {
552
+ content: "";
553
+ width: 6px; height: 6px; border-radius: 99px;
554
+ background: var(--soft);
555
+ }
556
+ /* Pro is the system's plan chip: the accent as border and text, weight 700.
557
+ Its one flourish is a single sheen that crosses the chip once on load — the
558
+ same "settles, then holds still" motion grammar as the boundary device — and
559
+ a faint accent tint on hover. Never an infinite loop: a sidebar that never
560
+ stops moving reads as broken, not premium. */
561
+ .acct-plan.pro {
562
+ color: var(--live);
563
+ border-color: var(--live);
564
+ font-weight: 700;
565
+ }
566
+ .acct-plan.pro::before {
567
+ background: var(--live);
568
+ }
569
+ .acct-plan.pro::after {
570
+ content: "";
571
+ position: absolute;
572
+ inset: -1px;
573
+ background: linear-gradient(115deg, transparent 38%, var(--live) 50%, transparent 62%);
574
+ opacity: 0;
575
+ animation: acct-sheen-once 1100ms ease-out 300ms 1 both;
576
+ pointer-events: none;
577
+ }
578
+ @keyframes acct-sheen-once {
579
+ 0% { transform: translateX(-110%); opacity: 0.3; }
580
+ 80% { opacity: 0.22; }
581
+ 100% { transform: translateX(110%); opacity: 0; }
582
+ }
583
+ .acct-plan.pro:hover {
584
+ background: color-mix(in srgb, var(--live) 8%, transparent);
585
+ }
586
+ .acct-meter { height: 4px; border-radius: 99px; background: var(--rule); overflow: hidden; }
587
+ .acct-fill { height: 100%; border-radius: 99px; background: var(--live); transition: width 400ms ease; }
588
+ .acct-usage { font: 11px var(--mono); color: var(--soft); font-variant-numeric: tabular-nums; min-height: 13px; }
589
+ /* Skeleton discipline: every slot holds its height before data arrives, so the
590
+ card never grows and the nav under it never moves. The door slot reserves
591
+ the button's room even for a vault that will not get one — a permanently
592
+ quiet 34px beats a nav that jumps for everyone else. */
593
+ .acct-email { min-height: 13px; }
594
+ .acct-facts li { min-height: 15px; }
595
+ .acct-door-slot { min-height: 34px; }
596
+ .acct-door-slot .acct-door { margin-top: 0; }
597
+ .acct-facts { list-style: none; margin: 2px 0 0; padding: 0; display: grid; gap: 7px; }
598
+ .acct-facts li { display: flex; align-items: baseline; gap: 8px; font: 11.5px var(--mono); color: var(--soft); line-height: 1.4; }
599
+ .acct-facts .dot { width: 6px; height: 6px; border-radius: 99px; background: var(--soft); flex-shrink: 0; transform: translateY(1px); }
600
+ .acct-facts li.good .dot { background: var(--live); }
601
+ .acct-facts li.warn .dot { background: var(--warn); }
602
+ .acct-facts b { color: var(--ink); font-weight: 600; }
603
+
604
+ /* ══ First run ═══════════════════════════════════════════════════════
605
+ setup.html. Same rail, same rules, same two typefaces — a wizard that
606
+ looked like a different product would undo the trust it exists to build.
607
+ The only new idea is the trace on screen one: one session's life drawn
608
+ down a spine, with the boundary strip cutting across it at the moment
609
+ the file stops being readable. The dashboard reports that line; this
610
+ page is where it gets explained.
611
+ ══════════════════════════════════════════════════════════════════ */
612
+
613
+ main.wizard { max-width: 940px; }
614
+ main.wizard > .pill { margin: 0 0 20px; }
615
+ .screen > header { margin-bottom: 26px; }
616
+ h1:focus-visible { outline: 2px solid var(--signal); outline-offset: 4px; }
617
+
618
+ .sr-only {
619
+ position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
620
+ overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
621
+ }
622
+
623
+ /* ── The step rail ────────────────────────────────────────── */
624
+
625
+ nav.steps .step .glyph { font-variant-numeric: tabular-nums; width: 1.15em; text-align: center; }
626
+ nav.steps .step.done .glyph { color: var(--verified); }
627
+ nav.steps .step.current { background: var(--ink-2); color: #fff; }
628
+ nav.steps .step.current .glyph { color: var(--signal); }
629
+ nav.steps .step.locked { opacity: 0.42; cursor: default; }
630
+ nav.steps .step.locked:hover { background: transparent; color: var(--soft); }
631
+ nav.steps .step.locked.current:hover { background: var(--ink-2); color: #fff; }
632
+
633
+ /* ── Screen one: one file's life, across the line ─────────── */
634
+
635
+ .trace {
636
+ background: var(--panel);
637
+ border: 1px solid var(--rule);
638
+ border-radius: 3px;
639
+ padding: 20px 24px;
640
+ margin-bottom: var(--gap);
641
+ }
642
+ .trace-row {
643
+ position: relative;
644
+ display: grid;
645
+ grid-template-columns: 1fr auto;
646
+ gap: 3px 16px;
647
+ padding: 13px 0 13px 28px;
648
+ }
649
+ /* The spine, and the node that fills in the moment the file is sealed. */
650
+ .trace-row::before { content: ""; position: absolute; left: 5px; top: 0; bottom: 0; width: 1px; background: var(--rule); }
651
+ .trace-row::after {
652
+ content: ""; position: absolute; left: 1px; top: 18px;
653
+ width: 9px; height: 9px; border-radius: 50%;
654
+ background: var(--panel); border: 1px solid var(--text-3);
655
+ }
656
+ .trace-row:first-child::before { top: 22px; }
657
+ .trace-row:last-child::before { bottom: auto; height: 22px; }
658
+ .trace-row.sealed::after { background: var(--ink); border-color: var(--ink); }
659
+ /* Placed explicitly: the state is a stamp on the statement, so it belongs on
660
+ its line — auto-placement puts it below the paragraph instead. */
661
+ .trace-row b { grid-area: 1 / 1; font: 600 13.5px var(--mono); letter-spacing: -0.015em; }
662
+ .trace-row .state {
663
+ grid-area: 1 / 2; align-self: baseline;
664
+ font: 600 10.5px var(--mono); text-transform: uppercase; letter-spacing: 0.1em;
665
+ color: var(--text-3); white-space: nowrap;
666
+ }
667
+ .trace-row p { grid-area: 2 / 1 / auto / -1; margin: 0; color: var(--text-2); max-width: 68ch; }
668
+ .trace-row.sealed .state { color: var(--ink-3); }
669
+ .trace .boundary { margin: 8px 0; }
670
+
671
+ /* ── Callouts and prose blocks ────────────────────────────── */
672
+
673
+ .callout p { margin: 0; color: var(--text-2); max-width: 72ch; }
674
+ .callout h3 { margin: 0; }
675
+ .callout h3 + p { margin-top: -6px; }
676
+ .callout.warn { border-left: 3px solid var(--alert); }
677
+ .callout.warn p { color: var(--text); }
678
+ ul.plain { margin: 0; padding-left: 19px; display: grid; gap: 8px; color: var(--text-2); max-width: 72ch; }
679
+ ul.plain b { color: var(--text); }
680
+ .explain summary { font: 600 13px var(--mono); color: var(--signal); cursor: pointer; }
681
+ .explain p { margin: 0; color: var(--text-2); max-width: 72ch; }
682
+ .explain a { color: var(--signal); }
683
+
684
+ /* ── Footer of each screen ────────────────────────────────── */
685
+
686
+ .wizfoot {
687
+ display: flex; align-items: center; justify-content: space-between;
688
+ gap: 16px; flex-wrap: wrap;
689
+ margin-top: var(--gap); padding-top: 18px;
690
+ border-top: 1px solid var(--rule);
691
+ }
692
+ .wizfoot .note { max-width: 46ch; }
693
+
694
+ a.action {
695
+ display: inline-block;
696
+ font: 600 13px var(--mono);
697
+ padding: 9px 16px;
698
+ border-radius: 3px;
699
+ border: 1px solid var(--signal);
700
+ background: transparent;
701
+ color: var(--signal);
702
+ text-decoration: none;
703
+ transition: background 120ms ease, color 120ms ease;
704
+ }
705
+ a.action:hover { background: var(--signal); color: #fff; }
706
+ .action.primary { background: var(--signal); color: #fff; }
707
+ .action.primary:hover { background: var(--ink-2); border-color: var(--ink-2); color: #fff; }
708
+ /* A locked "Finish setup" is on screen the whole time someone is transcribing
709
+ 24 words, so it stays readable rather than fading to a hint of itself. */
710
+ button.action.primary:disabled { background: var(--rule-soft); border-color: var(--rule); color: var(--text-2); }
711
+ .note.error { color: var(--alert); font-size: 12.5px; }
712
+
713
+ /* ── Screen two: agents ───────────────────────────────────── */
714
+
715
+ .agent-row, .done-row {
716
+ display: grid; gap: 4px;
717
+ padding: 15px 18px;
718
+ border-bottom: 1px solid var(--rule-soft);
719
+ }
720
+ .agent-row:last-child, .done-row:last-child { border-bottom: 0; }
721
+ .agent-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
722
+ .agent-head b { font: 600 14px var(--mono); }
723
+ .agent-name { font: 600 14px var(--mono); cursor: pointer; }
724
+ .agent-path { margin: 0; font: 12px var(--mono); color: var(--text-3); word-break: break-all; }
725
+ .agent-row input[type="checkbox"] { accent-color: var(--signal); width: 15px; height: 15px; flex-shrink: 0; }
726
+ .agent-row input[type="checkbox"]:disabled + .agent-name { cursor: default; color: var(--text-2); }
727
+ .agent-row.state-missing { opacity: 0.6; }
728
+ .agent-row.state-unwritable { border-left: 3px solid var(--alert); padding-left: 15px; background: #fdf4f2; }
729
+
730
+ /* ── Screen three: where archives go ──────────────────────── */
731
+
732
+ .choices { border: 0; margin: 0 0 var(--gap); padding: 0; display: grid; gap: 10px; }
733
+ .choice {
734
+ display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: start;
735
+ background: var(--panel); border: 1px solid var(--rule); border-radius: 3px;
736
+ padding: 14px 16px; cursor: pointer;
737
+ transition: border-color 120ms ease;
738
+ }
739
+ .choice:hover { border-color: var(--signal); }
740
+ .choice:focus-within { outline: 2px solid var(--signal); outline-offset: 2px; }
741
+ .choice:has(input:checked) { border-color: var(--signal); box-shadow: inset 0 0 0 1px var(--signal); }
742
+ .choice input[type="radio"] { accent-color: var(--signal); width: 15px; height: 15px; margin-top: 3px; }
743
+ .choice-body { display: grid; gap: 4px; }
744
+ .choice-body b { font: 600 14px var(--mono); }
745
+ .choice-body .note { max-width: 68ch; }
746
+ #provider-fields { display: grid; gap: 14px; }
747
+
748
+ /* ── Screen two: sealed or plain ──────────────────────────── */
749
+
750
+ /* The one choice in the wizard that lowers a guarantee. It keeps the shape of
751
+ its neighbour — same card, same radio — and wears the warning colour so the
752
+ difference is visible before a word is read. */
753
+ .choice.warn { border-left: 3px solid var(--alert); }
754
+ .choice.warn:hover { border-color: var(--alert); }
755
+ .choice.warn:has(input:checked) { border-color: var(--alert); box-shadow: inset 0 0 0 1px var(--alert); background: var(--warn-bg); }
756
+ .choice.warn input[type="radio"] { accent-color: var(--alert); }
757
+
758
+ /* ── Screen three: the free/pro compare ───────────────────── */
759
+
760
+ /* auto-fit rather than a media query: the two columns stack on their own the
761
+ moment they would crowd, so the narrow-screen block has nothing to know. */
762
+ .compare { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 6px 22px; }
763
+ .compare-col b { font: 600 13.5px var(--mono); }
764
+ .compare-col .note { margin: 4px 0 0; max-width: 58ch; }
765
+
766
+ /* ── Screen four: the phrase ──────────────────────────────── */
767
+
768
+ /* Hairlines come from each word's own outline, drawn into the 1px gap it shares
769
+ with its neighbour, rather than from a ruled background showing through. 17 is
770
+ never a whole number of columns, and a ruled background paints the leftover
771
+ cell as a solid block. */
772
+ .phrase {
773
+ list-style: none; margin: var(--gap) 0; padding: 0;
774
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
775
+ gap: 1px; background: var(--panel);
776
+ border: 1px solid var(--rule); border-radius: 3px; overflow: hidden;
777
+ }
778
+ .word { outline: 1px solid var(--rule); padding: 12px 14px; display: flex; align-items: baseline; gap: 10px; }
779
+ .word .ord { font: 600 10.5px var(--mono); color: var(--text-3); font-variant-numeric: tabular-nums; min-width: 1.5em; }
780
+ .word b { font: 700 15px var(--mono); letter-spacing: -0.02em; }
781
+ .phrase-actions { align-items: center; margin-bottom: var(--gap); }
782
+ .phrase-actions .note { flex: 1; min-width: 220px; }
783
+ #verify-fields { align-items: start; }
784
+ #verify-fields .field { flex: 1; min-width: 120px; }
785
+
786
+ @media print {
787
+ aside, .wizfoot, .phrase-actions, #confirm-form, footer, main.wizard > .pill { display: none !important; }
788
+ body { display: block; background: #fff; }
789
+ main.wizard { padding: 0; max-width: none; }
790
+ .phrase { break-inside: avoid; }
791
+ }
792
+
793
+ @media (prefers-reduced-motion: reduce) {
794
+ * { animation: none !important; transition: none !important; }
795
+ }
796
+
797
+ @media (max-width: 900px) {
798
+ body { flex-direction: column; }
799
+ aside { width: 100%; flex-direction: row; align-items: center; gap: 16px; padding: 12px 16px; overflow-x: auto; }
800
+ aside .privacy { display: none; }
801
+ .brand { padding: 0; font-size: 17px; }
802
+ nav { display: flex; gap: 4px; }
803
+ nav button { padding: 8px 11px; font-size: 12.5px; }
804
+ nav button .count { display: none; }
805
+ main { padding: 26px 18px 40px; }
806
+ h1 { font-size: 24px; }
807
+ .boundary { font-size: 11px; gap: 8px; }
808
+ .boundary .side { display: none; }
809
+ .check { grid-template-columns: 1fr; gap: 4px; }
810
+ .agent { grid-template-columns: 1fr; gap: 6px; }
811
+ table { font-size: 12.5px; }
812
+ th, td { padding: 10px 12px; }
813
+
814
+ /* First run, narrow. The rail becomes a strip of numbered steps; the trace
815
+ drops its right-hand state labels above the fold and keeps them below the
816
+ sentence, where they still read in order. */
817
+ nav.steps { flex-wrap: nowrap; }
818
+ nav.steps .step { white-space: nowrap; }
819
+ .trace { padding: 14px 16px; }
820
+ .trace-row { grid-template-columns: 1fr; padding-left: 24px; }
821
+ .trace-row b { grid-area: 1 / 1; }
822
+ .trace-row .state { grid-area: 3 / 1; }
823
+ .trace-row p { grid-area: 2 / 1; }
824
+ .trace .boundary .side { display: none; }
825
+ .wizfoot { align-items: stretch; }
826
+ .wizfoot .actions { flex: 1; }
827
+ .wizfoot .actions .action { flex: 1; text-align: center; }
828
+ .phrase { grid-template-columns: repeat(auto-fill, minmax(118px, 1fr)); }
829
+ }
830
+
831
+ /* ── Sessions and retention ───────────────────────────── */
832
+ /*
833
+ * These two pages carried their own inline stylesheets with their own palette,
834
+ * type scale and radii, so moving between them and the dashboard changed
835
+ * products mid-task — and they were the only two files with a dark-mode block,
836
+ * which meant a dark-mode machine flipped from warm paper to charcoal on the
837
+ * click. What they genuinely need that the dashboard does not is below; the
838
+ * rest they now inherit.
839
+ */
840
+
841
+ .filters {
842
+ display: flex;
843
+ flex-wrap: wrap;
844
+ gap: 8px;
845
+ align-items: center;
846
+ margin: 0 0 12px;
847
+ }
848
+ /* The shared form rule sets width:100% so fields fill a column layout. In a
849
+ filter bar that makes every control its own row, which is how five dropdowns
850
+ became five stacked bars. Here they size to their content. */
851
+ .filters input[type="search"],
852
+ .filters select {
853
+ width: auto;
854
+ font-size: 12.5px;
855
+ padding: 7px 10px;
856
+ }
857
+ .filters select { padding-right: 30px; background-position: right 10px center; }
858
+ .filters input[type="search"] { flex: 1 1 14rem; min-width: 10rem; font-family: var(--sans); }
859
+ .filters .linkish {
860
+ appearance: none;
861
+ border: 0;
862
+ background: none;
863
+ color: var(--signal);
864
+ font: 500 12.5px var(--sans);
865
+ cursor: pointer;
866
+ padding: 6px 4px;
867
+ }
868
+ .filters .linkish:hover { text-decoration: underline; }
869
+
870
+ /* A table of 700 rows keeps its header while you read it. Without this the
871
+ columns scroll away on the first wheel tick and the rest is anonymous. */
872
+ .scroll { max-height: calc(100vh - 26rem); min-height: 18rem; overflow: auto; }
873
+ .scroll thead th {
874
+ position: sticky;
875
+ top: 0;
876
+ background: var(--panel);
877
+ box-shadow: inset 0 -1px var(--rule);
878
+ z-index: 1;
879
+ }
880
+
881
+ /* The plan a customer is paying for. */
882
+ .planbar { padding: 16px 20px; margin-bottom: var(--gap); }
883
+ .plan-head { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; flex-wrap: wrap; }
884
+ .plan-name { font: 600 15px var(--sans); }
885
+ .plan-usage { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 12.5px; color: var(--text-2); }
886
+ .meter { height: 6px; border-radius: 99px; background: var(--rule-soft); overflow: hidden; margin: 10px 0 8px; }
887
+ .meter-fill { height: 100%; background: var(--signal); border-radius: 99px; transition: width 220ms ease; }
888
+
889
+ .rowgrid { display: grid; gap: 16px; grid-template-columns: 1fr 1fr; }
890
+ @media (max-width: 720px) { .rowgrid { grid-template-columns: 1fr; } }
891
+
892
+ /* Filter chips: one click each, combining. */
893
+ /* A row on its way into the vault. --live is the accent for "currently true",
894
+ and a seal in progress is exactly that; the row is unselectable while it
895
+ shows this. */
896
+ td .queued { color: var(--live); font-weight: 600; }
897
+
898
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 0 0 10px; }
899
+ .chip {
900
+ appearance: none;
901
+ border: 1px solid var(--rule);
902
+ background: var(--panel);
903
+ color: var(--text-2);
904
+ font: 500 12.5px var(--sans);
905
+ padding: 5px 11px;
906
+ border-radius: 99px;
907
+ cursor: pointer;
908
+ transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
909
+ }
910
+ .chip:hover { border-color: var(--signal); color: var(--text); }
911
+ .chip.on { background: var(--signal); border-color: var(--signal); color: #fff; }
912
+ .chip-sep { width: 1px; height: 18px; background: var(--rule); margin: 0 4px; }
913
+ .shown { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 4px 0 10px; }
914
+
915
+ /* The paying plan's door to its cloud panel, at the foot of the identity strip. */
916
+ .acct-door { display: block; margin-top: 10px; text-align: center; }
917
+
918
+ /* A backend-controlled offer, shown to vaults without a plan. The entrance
919
+ animation runs once — motion that ends is an arrival, motion that loops is
920
+ an advertisement, and this product does not blink at people. */
921
+ .rail-offer {
922
+ position: relative;
923
+ margin: 12px 0 4px;
924
+ padding: 12px;
925
+ border: 1px solid var(--rule);
926
+ border-radius: 6px;
927
+ background: var(--card);
928
+ animation: rail-offer-in 420ms ease-out 1;
929
+ }
930
+ .rail-offer b { display: block; font-size: 12px; margin-bottom: 4px; padding-right: 18px; }
931
+ .rail-offer p { margin: 0 0 10px; font-size: 11px; color: var(--soft); }
932
+ .rail-offer .action { display: block; text-align: center; }
933
+ .rail-offer-close {
934
+ position: absolute; top: 6px; right: 8px;
935
+ border: 0; background: none; color: var(--soft); cursor: pointer; font-size: 11px; padding: 2px;
936
+ }
937
+ @keyframes rail-offer-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
938
+ @media (prefers-reduced-motion: reduce) { .rail-offer, .rail-upload { animation: none; } }
939
+
940
+ /* ── Upload progress in the rail ──────────────────────── */
941
+ /* Injected by rail.js directly under the identity strip on every page while an
942
+ upload run is active — leaving the dashboard mid-send must not mean losing
943
+ sight of it. Negative margins tuck it inside the rail's section gap so it
944
+ reads as part of the identity block, closed by the same hairline. It is a
945
+ link back to the dashboard's Retention view, where the full progress lives.
946
+ The meter is determinate and the entrance runs once: motion that ends is
947
+ progress, motion that loops is decoration. */
948
+ .rail-upload {
949
+ display: block;
950
+ margin: -20px 0 -10px;
951
+ padding: 0 12px 14px;
952
+ border-bottom: 1px solid var(--rule);
953
+ text-decoration: none;
954
+ color: inherit;
955
+ animation: rail-upload-in 320ms ease-out 1 both;
956
+ }
957
+ /* The shared meter, at the identity strip's own 4px weight. */
958
+ .rail-upload .meter { height: 4px; margin: 0 0 7px; }
959
+ .rail-upload-line {
960
+ font: 11px var(--mono);
961
+ color: var(--soft);
962
+ font-variant-numeric: tabular-nums;
963
+ line-height: 1.5;
964
+ /* One line stays one line: a long failure message ellipsizes rather than
965
+ folding the rail; the full text lives on the strip's title. */
966
+ white-space: nowrap;
967
+ overflow: hidden;
968
+ text-overflow: ellipsis;
969
+ }
970
+ .rail-upload:hover .rail-upload-line { color: var(--ink); }
971
+ @keyframes rail-upload-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
972
+ /* In the narrow rail-as-row layout the tuck has no gap to sit in. */
973
+ @media (max-width: 900px) {
974
+ .rail-upload { margin: 0; border-bottom: 0; padding: 0; min-width: 180px; align-self: center; }
975
+ }
976
+
977
+ /* Retention's diagnosis: each distinct blocker once, with a verb. The rows
978
+ below carry chips, not sentences — the 48th repetition was noise. */
979
+ .hold-summary { padding: 12px 14px 6px; border-bottom: 1px solid var(--rule); }
980
+ .hold-line b { font-weight: 600; }
981
+ .hold-chip {
982
+ display: inline-block;
983
+ margin: 0 .35em .15em 0;
984
+ padding: 0 .45em;
985
+ border: 1px solid var(--rule);
986
+ border-radius: 3px;
987
+ font-size: 11px;
988
+ color: var(--soft);
989
+ white-space: nowrap;
990
+ }