this repo has no description
1<script lang="ts">
2 import type { OutboundMigrationFlow } from '../../lib/migration'
3 import type { ServerDescription } from '../../lib/migration/types'
4 import { getAuthState, logout } from '../../lib/auth.svelte'
5
6 interface Props {
7 flow: OutboundMigrationFlow
8 onBack: () => void
9 onComplete: () => void
10 }
11
12 let { flow, onBack, onComplete }: Props = $props()
13
14 const auth = getAuthState()
15
16 let loading = $state(false)
17 let understood = $state(false)
18 let pdsUrlInput = $state('')
19 let handleInput = $state('')
20 let selectedDomain = $state('')
21 let confirmFinal = $state(false)
22
23 $effect(() => {
24 if (flow.state.step === 'success') {
25 setTimeout(async () => {
26 await logout()
27 onComplete()
28 }, 3000)
29 }
30 })
31
32 $effect(() => {
33 if (flow.state.targetServerInfo?.availableUserDomains?.length) {
34 selectedDomain = flow.state.targetServerInfo.availableUserDomains[0]
35 }
36 })
37
38 async function validatePds(e: Event) {
39 e.preventDefault()
40 loading = true
41 flow.updateField('error', null)
42
43 try {
44 let url = pdsUrlInput.trim()
45 if (!url.startsWith('http://') && !url.startsWith('https://')) {
46 url = `https://${url}`
47 }
48 await flow.validateTargetPds(url)
49 flow.setStep('new-account')
50 } catch (err) {
51 flow.setError((err as Error).message)
52 } finally {
53 loading = false
54 }
55 }
56
57 function proceedToReview() {
58 const fullHandle = handleInput.includes('.')
59 ? handleInput
60 : `${handleInput}.${selectedDomain}`
61
62 flow.updateField('targetHandle', fullHandle)
63 flow.setStep('review')
64 }
65
66 async function startMigration() {
67 if (!auth.session) return
68 loading = true
69 try {
70 await flow.startMigration(auth.session.did)
71 } catch (err) {
72 flow.setError((err as Error).message)
73 } finally {
74 loading = false
75 }
76 }
77
78 async function submitPlcToken(e: Event) {
79 e.preventDefault()
80 loading = true
81 try {
82 await flow.submitPlcToken(flow.state.plcToken)
83 } catch (err) {
84 flow.setError((err as Error).message)
85 } finally {
86 loading = false
87 }
88 }
89
90 async function resendToken() {
91 loading = true
92 try {
93 await flow.resendPlcToken()
94 flow.setError(null)
95 } catch (err) {
96 flow.setError((err as Error).message)
97 } finally {
98 loading = false
99 }
100 }
101
102 function isDidWeb(): boolean {
103 return auth.session?.did?.startsWith('did:web:') ?? false
104 }
105
106 const steps = ['Target', 'Setup', 'Review', 'Transfer', 'Verify', 'Complete']
107 function getCurrentStepIndex(): number {
108 switch (flow.state.step) {
109 case 'welcome': return -1
110 case 'target-pds': return 0
111 case 'new-account': return 1
112 case 'review': return 2
113 case 'migrating': return 3
114 case 'plc-token':
115 case 'finalizing': return 4
116 case 'success': return 5
117 default: return 0
118 }
119 }
120</script>
121
122<div class="outbound-wizard">
123 {#if flow.state.step !== 'welcome'}
124 <div class="step-indicator">
125 {#each steps as stepName, i}
126 <div class="step" class:active={i === getCurrentStepIndex()} class:completed={i < getCurrentStepIndex()}>
127 <div class="step-dot">{i < getCurrentStepIndex() ? '✓' : i + 1}</div>
128 <span class="step-label">{stepName}</span>
129 </div>
130 {#if i < steps.length - 1}
131 <div class="step-line" class:completed={i < getCurrentStepIndex()}></div>
132 {/if}
133 {/each}
134 </div>
135 {/if}
136
137 {#if flow.state.error}
138 <div class="message error">{flow.state.error}</div>
139 {/if}
140
141 {#if flow.state.step === 'welcome'}
142 <div class="step-content">
143 <h2>Migrate Your Account Away</h2>
144 <p>This wizard will help you move your AT Protocol account from this PDS to another one.</p>
145
146 <div class="current-account">
147 <span class="label">Current account:</span>
148 <span class="value">@{auth.session?.handle}</span>
149 </div>
150
151 {#if isDidWeb()}
152 <div class="warning-box">
153 <strong>did:web Migration Notice</strong>
154 <p>
155 Your account uses a did:web identifier ({auth.session?.did}). After migrating, this PDS will
156 continue serving your DID document with an updated service endpoint pointing to your new PDS.
157 </p>
158 <p>
159 You can return here anytime to update the forwarding if you migrate again in the future.
160 </p>
161 </div>
162 {/if}
163
164 <div class="info-box">
165 <h3>What will happen:</h3>
166 <ol>
167 <li>Choose your new PDS</li>
168 <li>Set up your account on the new server</li>
169 <li>Your repository and blobs will be transferred</li>
170 <li>Verify the migration via email</li>
171 <li>Your identity will be updated to point to the new PDS</li>
172 <li>Your account here will be deactivated</li>
173 </ol>
174 </div>
175
176 <div class="warning-box">
177 <strong>Before you proceed:</strong>
178 <ul>
179 <li>You need access to the email registered with this account</li>
180 <li>You will lose access to this account on this PDS</li>
181 <li>Make sure you trust the destination PDS</li>
182 <li>Large accounts may take several minutes to transfer</li>
183 </ul>
184 </div>
185
186 <label class="checkbox-label">
187 <input type="checkbox" bind:checked={understood} />
188 <span>I understand that my account will be moved and deactivated here</span>
189 </label>
190
191 <div class="button-row">
192 <button class="ghost" onclick={onBack}>Cancel</button>
193 <button disabled={!understood} onclick={() => flow.setStep('target-pds')}>
194 Continue
195 </button>
196 </div>
197 </div>
198
199 {:else if flow.state.step === 'target-pds'}
200 <div class="step-content">
201 <h2>Choose Your New PDS</h2>
202 <p>Enter the URL of the PDS you want to migrate to.</p>
203
204 <form onsubmit={validatePds}>
205 <div class="field">
206 <label for="pds-url">PDS URL</label>
207 <input
208 id="pds-url"
209 type="text"
210 placeholder="pds.example.com"
211 bind:value={pdsUrlInput}
212 disabled={loading}
213 required
214 />
215 <p class="hint">The server address of your new PDS (e.g., bsky.social, pds.example.com)</p>
216 </div>
217
218 <div class="button-row">
219 <button type="button" class="ghost" onclick={() => flow.setStep('welcome')} disabled={loading}>Back</button>
220 <button type="submit" disabled={loading || !pdsUrlInput.trim()}>
221 {loading ? 'Checking...' : 'Connect'}
222 </button>
223 </div>
224 </form>
225
226 {#if flow.state.targetServerInfo}
227 <div class="server-info">
228 <h3>Connected to PDS</h3>
229 <div class="info-row">
230 <span class="label">Server:</span>
231 <span class="value">{flow.state.targetPdsUrl}</span>
232 </div>
233 {#if flow.state.targetServerInfo.availableUserDomains.length > 0}
234 <div class="info-row">
235 <span class="label">Available domains:</span>
236 <span class="value">{flow.state.targetServerInfo.availableUserDomains.join(', ')}</span>
237 </div>
238 {/if}
239 <div class="info-row">
240 <span class="label">Invite required:</span>
241 <span class="value">{flow.state.targetServerInfo.inviteCodeRequired ? 'Yes' : 'No'}</span>
242 </div>
243 {#if flow.state.targetServerInfo.links?.termsOfService}
244 <a href={flow.state.targetServerInfo.links.termsOfService} target="_blank" rel="noopener">
245 Terms of Service
246 </a>
247 {/if}
248 {#if flow.state.targetServerInfo.links?.privacyPolicy}
249 <a href={flow.state.targetServerInfo.links.privacyPolicy} target="_blank" rel="noopener">
250 Privacy Policy
251 </a>
252 {/if}
253 </div>
254 {/if}
255 </div>
256
257 {:else if flow.state.step === 'new-account'}
258 <div class="step-content">
259 <h2>Set Up Your New Account</h2>
260 <p>Configure your account details on the new PDS.</p>
261
262 <div class="current-info">
263 <span class="label">Migrating to:</span>
264 <span class="value">{flow.state.targetPdsUrl}</span>
265 </div>
266
267 <div class="field">
268 <label for="new-handle">New Handle</label>
269 <div class="handle-input-group">
270 <input
271 id="new-handle"
272 type="text"
273 placeholder="username"
274 bind:value={handleInput}
275 />
276 {#if flow.state.targetServerInfo && flow.state.targetServerInfo.availableUserDomains.length > 0 && !handleInput.includes('.')}
277 <select bind:value={selectedDomain}>
278 {#each flow.state.targetServerInfo.availableUserDomains as domain}
279 <option value={domain}>.{domain}</option>
280 {/each}
281 </select>
282 {/if}
283 </div>
284 <p class="hint">You can also use your own domain by entering the full handle (e.g., alice.mydomain.com)</p>
285 </div>
286
287 <div class="field">
288 <label for="email">Email Address</label>
289 <input
290 id="email"
291 type="email"
292 placeholder="you@example.com"
293 bind:value={flow.state.targetEmail}
294 oninput={(e) => flow.updateField('targetEmail', (e.target as HTMLInputElement).value)}
295 required
296 />
297 </div>
298
299 <div class="field">
300 <label for="new-password">Password</label>
301 <input
302 id="new-password"
303 type="password"
304 placeholder="Password for your new account"
305 bind:value={flow.state.targetPassword}
306 oninput={(e) => flow.updateField('targetPassword', (e.target as HTMLInputElement).value)}
307 required
308 minlength="8"
309 />
310 <p class="hint">At least 8 characters. This will be your password on the new PDS.</p>
311 </div>
312
313 {#if flow.state.targetServerInfo?.inviteCodeRequired}
314 <div class="field">
315 <label for="invite">Invite Code</label>
316 <input
317 id="invite"
318 type="text"
319 placeholder="Enter invite code"
320 bind:value={flow.state.inviteCode}
321 oninput={(e) => flow.updateField('inviteCode', (e.target as HTMLInputElement).value)}
322 required
323 />
324 <p class="hint">Required by this PDS to create an account</p>
325 </div>
326 {/if}
327
328 <div class="button-row">
329 <button class="ghost" onclick={() => flow.setStep('target-pds')}>Back</button>
330 <button
331 disabled={!handleInput.trim() || !flow.state.targetEmail || !flow.state.targetPassword}
332 onclick={proceedToReview}
333 >
334 Continue
335 </button>
336 </div>
337 </div>
338
339 {:else if flow.state.step === 'review'}
340 <div class="step-content">
341 <h2>Review Migration</h2>
342 <p>Please confirm the details of your migration.</p>
343
344 <div class="review-card">
345 <div class="review-row">
346 <span class="label">Current Handle:</span>
347 <span class="value">@{auth.session?.handle}</span>
348 </div>
349 <div class="review-row">
350 <span class="label">New Handle:</span>
351 <span class="value">@{flow.state.targetHandle}</span>
352 </div>
353 <div class="review-row">
354 <span class="label">DID:</span>
355 <span class="value mono">{auth.session?.did}</span>
356 </div>
357 <div class="review-row">
358 <span class="label">From PDS:</span>
359 <span class="value">{window.location.origin}</span>
360 </div>
361 <div class="review-row">
362 <span class="label">To PDS:</span>
363 <span class="value">{flow.state.targetPdsUrl}</span>
364 </div>
365 <div class="review-row">
366 <span class="label">New Email:</span>
367 <span class="value">{flow.state.targetEmail}</span>
368 </div>
369 </div>
370
371 <div class="warning-box final-warning">
372 <strong>This action cannot be easily undone!</strong>
373 <p>
374 After migration completes, your account on this PDS will be deactivated.
375 To return, you would need to migrate back from the new PDS.
376 </p>
377 </div>
378
379 <label class="checkbox-label">
380 <input type="checkbox" bind:checked={confirmFinal} />
381 <span>I confirm I want to migrate my account to {flow.state.targetPdsUrl}</span>
382 </label>
383
384 <div class="button-row">
385 <button class="ghost" onclick={() => flow.setStep('new-account')} disabled={loading}>Back</button>
386 <button class="danger" onclick={startMigration} disabled={loading || !confirmFinal}>
387 {loading ? 'Starting...' : 'Start Migration'}
388 </button>
389 </div>
390 </div>
391
392 {:else if flow.state.step === 'migrating'}
393 <div class="step-content">
394 <h2>Migration in Progress</h2>
395 <p>Please wait while your account is being transferred...</p>
396
397 <div class="progress-section">
398 <div class="progress-item" class:completed={flow.state.progress.repoExported}>
399 <span class="icon">{flow.state.progress.repoExported ? '✓' : '○'}</span>
400 <span>Export repository</span>
401 </div>
402 <div class="progress-item" class:completed={flow.state.progress.repoImported}>
403 <span class="icon">{flow.state.progress.repoImported ? '✓' : '○'}</span>
404 <span>Import repository to new PDS</span>
405 </div>
406 <div class="progress-item" class:active={flow.state.progress.repoImported && !flow.state.progress.prefsMigrated}>
407 <span class="icon">{flow.state.progress.blobsMigrated === flow.state.progress.blobsTotal && flow.state.progress.blobsTotal > 0 ? '✓' : '○'}</span>
408 <span>Migrate blobs ({flow.state.progress.blobsMigrated}/{flow.state.progress.blobsTotal})</span>
409 </div>
410 <div class="progress-item" class:completed={flow.state.progress.prefsMigrated}>
411 <span class="icon">{flow.state.progress.prefsMigrated ? '✓' : '○'}</span>
412 <span>Migrate preferences</span>
413 </div>
414 </div>
415
416 {#if flow.state.progress.blobsTotal > 0}
417 <div class="progress-bar">
418 <div
419 class="progress-fill"
420 style="width: {(flow.state.progress.blobsMigrated / flow.state.progress.blobsTotal) * 100}%"
421 ></div>
422 </div>
423 {/if}
424
425 <p class="status-text">{flow.state.progress.currentOperation}</p>
426 </div>
427
428 {:else if flow.state.step === 'plc-token'}
429 <div class="step-content">
430 <h2>Verify Migration</h2>
431 <p>A verification code has been sent to your email ({auth.session?.email}).</p>
432
433 <div class="info-box">
434 <p>
435 This code confirms you have access to the account and authorizes updating your identity
436 to point to the new PDS.
437 </p>
438 </div>
439
440 <form onsubmit={submitPlcToken}>
441 <div class="field">
442 <label for="plc-token">Verification Code</label>
443 <input
444 id="plc-token"
445 type="text"
446 placeholder="Enter code from email"
447 bind:value={flow.state.plcToken}
448 oninput={(e) => flow.updateField('plcToken', (e.target as HTMLInputElement).value)}
449 disabled={loading}
450 required
451 />
452 </div>
453
454 <div class="button-row">
455 <button type="button" class="ghost" onclick={resendToken} disabled={loading}>
456 Resend Code
457 </button>
458 <button type="submit" disabled={loading || !flow.state.plcToken}>
459 {loading ? 'Verifying...' : 'Complete Migration'}
460 </button>
461 </div>
462 </form>
463 </div>
464
465 {:else if flow.state.step === 'finalizing'}
466 <div class="step-content">
467 <h2>Finalizing Migration</h2>
468 <p>Please wait while we complete the migration...</p>
469
470 <div class="progress-section">
471 <div class="progress-item" class:completed={flow.state.progress.plcSigned}>
472 <span class="icon">{flow.state.progress.plcSigned ? '✓' : '○'}</span>
473 <span>Sign identity update</span>
474 </div>
475 <div class="progress-item" class:completed={flow.state.progress.activated}>
476 <span class="icon">{flow.state.progress.activated ? '✓' : '○'}</span>
477 <span>Activate account on new PDS</span>
478 </div>
479 <div class="progress-item" class:completed={flow.state.progress.deactivated}>
480 <span class="icon">{flow.state.progress.deactivated ? '✓' : '○'}</span>
481 <span>Deactivate account here</span>
482 </div>
483 </div>
484
485 <p class="status-text">{flow.state.progress.currentOperation}</p>
486 </div>
487
488 {:else if flow.state.step === 'success'}
489 <div class="step-content success-content">
490 <div class="success-icon">✓</div>
491 <h2>Migration Complete!</h2>
492 <p>Your account has been successfully migrated to your new PDS.</p>
493
494 <div class="success-details">
495 <div class="detail-row">
496 <span class="label">Your new handle:</span>
497 <span class="value">@{flow.state.targetHandle}</span>
498 </div>
499 <div class="detail-row">
500 <span class="label">New PDS:</span>
501 <span class="value">{flow.state.targetPdsUrl}</span>
502 </div>
503 <div class="detail-row">
504 <span class="label">DID:</span>
505 <span class="value mono">{auth.session?.did}</span>
506 </div>
507 </div>
508
509 {#if flow.state.progress.blobsFailed.length > 0}
510 <div class="warning-box">
511 <strong>Note:</strong> {flow.state.progress.blobsFailed.length} blobs could not be migrated.
512 These may be images or other media that are no longer available.
513 </div>
514 {/if}
515
516 <div class="next-steps">
517 <h3>Next Steps</h3>
518 <ol>
519 <li>Visit your new PDS at <a href={flow.state.targetPdsUrl} target="_blank" rel="noopener">{flow.state.targetPdsUrl}</a></li>
520 <li>Log in with your new credentials</li>
521 <li>Your followers and following will continue to work</li>
522 </ol>
523 </div>
524
525 <p class="redirect-text">Logging out in a moment...</p>
526 </div>
527
528 {:else if flow.state.step === 'error'}
529 <div class="step-content">
530 <h2>Migration Error</h2>
531 <p>An error occurred during migration.</p>
532
533 <div class="error-box">
534 {flow.state.error}
535 </div>
536
537 <div class="button-row">
538 <button class="ghost" onclick={onBack}>Start Over</button>
539 </div>
540 </div>
541 {/if}
542</div>
543
544<style>
545 .outbound-wizard {
546 max-width: 600px;
547 margin: 0 auto;
548 }
549
550 .step-indicator {
551 display: flex;
552 align-items: center;
553 justify-content: center;
554 margin-bottom: var(--space-8);
555 padding: 0 var(--space-4);
556 }
557
558 .step {
559 display: flex;
560 flex-direction: column;
561 align-items: center;
562 gap: var(--space-2);
563 }
564
565 .step-dot {
566 width: 32px;
567 height: 32px;
568 border-radius: 50%;
569 background: var(--bg-secondary);
570 border: 2px solid var(--border);
571 display: flex;
572 align-items: center;
573 justify-content: center;
574 font-size: var(--text-sm);
575 font-weight: var(--font-medium);
576 color: var(--text-secondary);
577 }
578
579 .step.active .step-dot {
580 background: var(--accent);
581 border-color: var(--accent);
582 color: var(--text-inverse);
583 }
584
585 .step.completed .step-dot {
586 background: var(--success-bg);
587 border-color: var(--success-text);
588 color: var(--success-text);
589 }
590
591 .step-label {
592 font-size: var(--text-xs);
593 color: var(--text-secondary);
594 }
595
596 .step.active .step-label {
597 color: var(--accent);
598 font-weight: var(--font-medium);
599 }
600
601 .step-line {
602 flex: 1;
603 height: 2px;
604 background: var(--border);
605 margin: 0 var(--space-2);
606 margin-bottom: var(--space-6);
607 min-width: 20px;
608 }
609
610 .step-line.completed {
611 background: var(--success-text);
612 }
613
614 .step-content {
615 background: var(--bg-secondary);
616 border-radius: var(--radius-xl);
617 padding: var(--space-6);
618 }
619
620 .step-content h2 {
621 margin: 0 0 var(--space-3) 0;
622 }
623
624 .step-content > p {
625 color: var(--text-secondary);
626 margin: 0 0 var(--space-5) 0;
627 }
628
629 .current-account {
630 background: var(--bg-primary);
631 border-radius: var(--radius-lg);
632 padding: var(--space-4);
633 margin-bottom: var(--space-5);
634 display: flex;
635 justify-content: space-between;
636 align-items: center;
637 }
638
639 .current-account .label {
640 color: var(--text-secondary);
641 }
642
643 .current-account .value {
644 font-weight: var(--font-medium);
645 font-size: var(--text-lg);
646 }
647
648 .info-box {
649 background: var(--accent-muted);
650 border: 1px solid var(--accent);
651 border-radius: var(--radius-lg);
652 padding: var(--space-5);
653 margin-bottom: var(--space-5);
654 }
655
656 .info-box h3 {
657 margin: 0 0 var(--space-3) 0;
658 font-size: var(--text-base);
659 }
660
661 .info-box ol, .info-box ul {
662 margin: 0;
663 padding-left: var(--space-5);
664 }
665
666 .info-box li {
667 margin-bottom: var(--space-2);
668 color: var(--text-secondary);
669 }
670
671 .info-box p {
672 margin: 0;
673 color: var(--text-secondary);
674 }
675
676 .warning-box {
677 background: var(--warning-bg);
678 border: 1px solid var(--warning-border);
679 border-radius: var(--radius-lg);
680 padding: var(--space-5);
681 margin-bottom: var(--space-5);
682 font-size: var(--text-sm);
683 }
684
685 .warning-box strong {
686 color: var(--warning-text);
687 }
688
689 .warning-box p {
690 margin: var(--space-3) 0 0 0;
691 color: var(--text-secondary);
692 }
693
694 .warning-box ul {
695 margin: var(--space-3) 0 0 0;
696 padding-left: var(--space-5);
697 }
698
699 .final-warning {
700 background: var(--error-bg);
701 border-color: var(--error-border);
702 }
703
704 .final-warning strong {
705 color: var(--error-text);
706 }
707
708 .error-box {
709 background: var(--error-bg);
710 border: 1px solid var(--error-border);
711 border-radius: var(--radius-lg);
712 padding: var(--space-5);
713 margin-bottom: var(--space-5);
714 color: var(--error-text);
715 }
716
717 .checkbox-label {
718 display: inline-flex;
719 align-items: flex-start;
720 gap: var(--space-3);
721 cursor: pointer;
722 margin-bottom: var(--space-5);
723 text-align: left;
724 }
725
726 .checkbox-label input[type="checkbox"] {
727 width: 18px;
728 height: 18px;
729 margin: 0;
730 flex-shrink: 0;
731 }
732
733 .button-row {
734 display: flex;
735 gap: var(--space-3);
736 justify-content: flex-end;
737 margin-top: var(--space-5);
738 }
739
740 .field {
741 margin-bottom: var(--space-5);
742 }
743
744 .field label {
745 display: block;
746 margin-bottom: var(--space-2);
747 font-weight: var(--font-medium);
748 }
749
750 .field input, .field select {
751 width: 100%;
752 padding: var(--space-3);
753 border: 1px solid var(--border);
754 border-radius: var(--radius-md);
755 background: var(--bg-primary);
756 color: var(--text-primary);
757 }
758
759 .field input:focus, .field select:focus {
760 outline: none;
761 border-color: var(--accent);
762 }
763
764 .hint {
765 font-size: var(--text-sm);
766 color: var(--text-secondary);
767 margin: var(--space-2) 0 0 0;
768 }
769
770 .handle-input-group {
771 display: flex;
772 gap: var(--space-2);
773 }
774
775 .handle-input-group input {
776 flex: 1;
777 }
778
779 .handle-input-group select {
780 width: auto;
781 }
782
783 .current-info {
784 background: var(--bg-primary);
785 border-radius: var(--radius-lg);
786 padding: var(--space-4);
787 margin-bottom: var(--space-5);
788 display: flex;
789 justify-content: space-between;
790 }
791
792 .current-info .label {
793 color: var(--text-secondary);
794 }
795
796 .current-info .value {
797 font-weight: var(--font-medium);
798 }
799
800 .server-info {
801 background: var(--bg-primary);
802 border-radius: var(--radius-lg);
803 padding: var(--space-4);
804 margin-top: var(--space-5);
805 }
806
807 .server-info h3 {
808 margin: 0 0 var(--space-3) 0;
809 font-size: var(--text-base);
810 color: var(--success-text);
811 }
812
813 .server-info .info-row {
814 display: flex;
815 justify-content: space-between;
816 padding: var(--space-2) 0;
817 font-size: var(--text-sm);
818 }
819
820 .server-info .label {
821 color: var(--text-secondary);
822 }
823
824 .server-info a {
825 display: inline-block;
826 margin-top: var(--space-2);
827 margin-right: var(--space-3);
828 color: var(--accent);
829 font-size: var(--text-sm);
830 }
831
832 .review-card {
833 background: var(--bg-primary);
834 border-radius: var(--radius-lg);
835 padding: var(--space-4);
836 margin-bottom: var(--space-5);
837 }
838
839 .review-row {
840 display: flex;
841 justify-content: space-between;
842 padding: var(--space-3) 0;
843 border-bottom: 1px solid var(--border);
844 }
845
846 .review-row:last-child {
847 border-bottom: none;
848 }
849
850 .review-row .label {
851 color: var(--text-secondary);
852 }
853
854 .review-row .value {
855 font-weight: var(--font-medium);
856 text-align: right;
857 word-break: break-all;
858 }
859
860 .review-row .value.mono {
861 font-family: var(--font-mono);
862 font-size: var(--text-sm);
863 }
864
865 .progress-section {
866 margin-bottom: var(--space-5);
867 }
868
869 .progress-item {
870 display: flex;
871 align-items: center;
872 gap: var(--space-3);
873 padding: var(--space-3) 0;
874 color: var(--text-secondary);
875 }
876
877 .progress-item.completed {
878 color: var(--success-text);
879 }
880
881 .progress-item.active {
882 color: var(--accent);
883 }
884
885 .progress-item .icon {
886 width: 24px;
887 text-align: center;
888 }
889
890 .progress-bar {
891 height: 8px;
892 background: var(--bg-primary);
893 border-radius: 4px;
894 overflow: hidden;
895 margin-bottom: var(--space-4);
896 }
897
898 .progress-fill {
899 height: 100%;
900 background: var(--accent);
901 transition: width 0.3s ease;
902 }
903
904 .status-text {
905 text-align: center;
906 color: var(--text-secondary);
907 font-size: var(--text-sm);
908 }
909
910 .success-content {
911 text-align: center;
912 }
913
914 .success-icon {
915 width: 64px;
916 height: 64px;
917 background: var(--success-bg);
918 color: var(--success-text);
919 border-radius: 50%;
920 display: flex;
921 align-items: center;
922 justify-content: center;
923 font-size: var(--text-2xl);
924 margin: 0 auto var(--space-5) auto;
925 }
926
927 .success-details {
928 background: var(--bg-primary);
929 border-radius: var(--radius-lg);
930 padding: var(--space-4);
931 margin: var(--space-5) 0;
932 text-align: left;
933 }
934
935 .success-details .detail-row {
936 display: flex;
937 justify-content: space-between;
938 padding: var(--space-2) 0;
939 }
940
941 .success-details .label {
942 color: var(--text-secondary);
943 }
944
945 .success-details .value {
946 font-weight: var(--font-medium);
947 }
948
949 .success-details .value.mono {
950 font-family: var(--font-mono);
951 font-size: var(--text-sm);
952 }
953
954 .next-steps {
955 background: var(--accent-muted);
956 border-radius: var(--radius-lg);
957 padding: var(--space-5);
958 margin: var(--space-5) 0;
959 text-align: left;
960 }
961
962 .next-steps h3 {
963 margin: 0 0 var(--space-3) 0;
964 }
965
966 .next-steps ol {
967 margin: 0;
968 padding-left: var(--space-5);
969 }
970
971 .next-steps li {
972 margin-bottom: var(--space-2);
973 }
974
975 .next-steps a {
976 color: var(--accent);
977 }
978
979 .redirect-text {
980 color: var(--text-secondary);
981 font-style: italic;
982 }
983
984 .message.error {
985 background: var(--error-bg);
986 border: 1px solid var(--error-border);
987 color: var(--error-text);
988 padding: var(--space-4);
989 border-radius: var(--radius-lg);
990 margin-bottom: var(--space-5);
991 }
992</style>