@kurtbruns/kestrel 1.10.0 → 1.12.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 (45) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +17 -28
  3. package/cli/reset.mjs +2 -0
  4. package/cli/seed.mjs +2 -0
  5. package/demo/README.md +4 -2
  6. package/demo/posts/0-test-email/index.md +13 -0
  7. package/demo/posts/5-a-quick-note/index.md +2 -0
  8. package/dist/public/dashboard/app.js +302 -293
  9. package/dist/public/dashboard/app.js.map +4 -4
  10. package/dist/public/dashboard/index.html +2 -2
  11. package/dist/public/dashboard/styles.css +434 -54
  12. package/dist/worker/index.js +1472 -470
  13. package/docs/README.md +2 -2
  14. package/docs/get-started/02-deploy.md +12 -5
  15. package/docs/get-started/03-access.md +2 -0
  16. package/docs/get-started/04-ses.md +36 -16
  17. package/docs/get-started/05-verify.md +36 -8
  18. package/docs/get-started/06-go-live.md +13 -55
  19. package/docs/guides/01-connect-claude.md +7 -5
  20. package/docs/guides/03-notifications.md +10 -10
  21. package/docs/guides/09-as-your-list-grows.md +62 -0
  22. package/docs/publishing/01-your-list.md +118 -47
  23. package/docs/publishing/02-sending-rules.md +5 -5
  24. package/docs/publishing/03-name-your-publication.md +7 -93
  25. package/docs/publishing/04-write-a-post.md +37 -66
  26. package/docs/publishing/06-troubleshooting.md +103 -0
  27. package/docs/publishing/07-working-with-claude.md +19 -37
  28. package/docs/publishing/09-share-your-subscribe-link.md +11 -59
  29. package/docs/publishing/10-check-your-setup.md +12 -30
  30. package/docs/publishing/11-email-template.md +34 -0
  31. package/docs/publishing/README.md +12 -11
  32. package/docs/reference/01-configuration.md +6 -6
  33. package/docs/reference/02-sending-domain-dns.md +1 -1
  34. package/migrations/0007_sends_progress_at.sql +12 -0
  35. package/migrations/0008_notifications_sandbox.sql +35 -0
  36. package/migrations/0009_post_email_only.sql +12 -0
  37. package/migrations/0010_unlisted_links.sql +43 -0
  38. package/migrations/0011_sends_render_warnings.sql +13 -0
  39. package/package.json +1 -1
  40. package/template/claude/CLAUDE.md +5 -2
  41. package/template/claude/skills/migrate-list/SKILL.md +2 -2
  42. package/docs/guides/08-try-locally.md +0 -108
  43. package/docs/publishing/05-scheduling-and-sending.md +0 -72
  44. package/docs/publishing/06-after-a-send.md +0 -109
  45. package/docs/publishing/08-import-format.md +0 -103
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Kestrel</title>
7
7
  <link rel="icon" href="/favicon.svg">
8
- <link rel="stylesheet" href="./styles.css?v=abd15153">
8
+ <link rel="stylesheet" href="./styles.css?v=1396c31b">
9
9
  <script>
10
10
  // Sidebar mode guard — set the width-based data-nav on <html> before first paint so the
11
11
  // sidebar renders in its final width with no flash. The collapse toggle's override is
@@ -113,6 +113,6 @@
113
113
  </section>
114
114
  <div id="toasts" role="status" aria-live="polite"></div>
115
115
 
116
- <script src="./app.js?v=183fc216"></script>
116
+ <script src="./app.js?v=fc72c770"></script>
117
117
  </body>
118
118
  </html>
@@ -1558,6 +1558,42 @@ td a:hover {
1558
1558
  color: var(--status-sending-fg);
1559
1559
  border-color: var(--status-sending-line);
1560
1560
  }
1561
+ /* A list's title with its Email only mark (sends/progress.ts): the title wraps within its room
1562
+ and the mark, an envelope and muted words, stays whole on the title's line, so it never drops
1563
+ onto a line of its own. */
1564
+ .title-with-label {
1565
+ display: flex;
1566
+ /* Centered, not on a baseline: the mark's text is smaller and led by an icon, and only
1567
+ their centers line up with the title's. */
1568
+ align-items: center;
1569
+ gap: 8px;
1570
+ min-width: 0;
1571
+ }
1572
+ .title-with-label .title-text {
1573
+ min-width: 0;
1574
+ }
1575
+ /* Where a table's columns are fixed, a long title ends in an ellipsis, so the row stays one
1576
+ line tall; the whole title is a click away in the post. */
1577
+ .list-table .title-with-label .title-text,
1578
+ .dash-drafts .title-with-label .title-text {
1579
+ overflow: hidden;
1580
+ text-overflow: ellipsis;
1581
+ white-space: nowrap;
1582
+ }
1583
+ .email-only-mark {
1584
+ display: inline-flex;
1585
+ align-items: center;
1586
+ gap: 4px;
1587
+ flex: none;
1588
+ font-size: 12.5px;
1589
+ font-weight: 400;
1590
+ color: var(--muted);
1591
+ white-space: nowrap;
1592
+ }
1593
+ .email-only-mark svg {
1594
+ width: 13px;
1595
+ height: 13px;
1596
+ }
1561
1597
  /* Subscriber consent states borrow a hue that matches the state's meaning: confirmed →
1562
1598
  the sent green, unsubscribed → the draft grey, suppressed → danger red (below). Pending
1563
1599
  awaits double-opt-in — a waiting/attention state — so it takes the amber `warn` scale,
@@ -1810,6 +1846,24 @@ td a:hover {
1810
1846
  .list-table .c-delivered {
1811
1847
  width: 176px;
1812
1848
  }
1849
+ /* Every Sent row, on the Sent page and the dashboard, as tall as one with a delivery note under its count, so a row with none (a
1850
+ one-recipient test) sits in the same rhythm as the rest. A row's height is its floor; once
1851
+ the table stacks, each row is a block that takes its own height. */
1852
+ /* The dashboard's Drafts card is half the page wide: its date stays on one line, and the title
1853
+ takes what room is left. */
1854
+ .dash-drafts {
1855
+ table-layout: fixed;
1856
+ }
1857
+ .dash-drafts td.edited {
1858
+ width: 11.5em;
1859
+ white-space: nowrap;
1860
+ }
1861
+ @media (min-width: 721px) {
1862
+ .sent-table tbody tr,
1863
+ .dash-sent tbody tr {
1864
+ height: 56px;
1865
+ }
1866
+ }
1813
1867
  .list-table .c-act {
1814
1868
  width: 56px;
1815
1869
  }
@@ -1965,14 +2019,16 @@ body.web-mode .content {
1965
2019
  padding: 10px 44px;
1966
2020
  text-align: left;
1967
2021
  }
2022
+ /* The reader pages' Dev pill exactly (`.r-dev-tag` in src/lib/page.ts), so the two read as one. */
1968
2023
  .web-bar-tag {
1969
2024
  display: inline-block;
1970
2025
  margin-right: 10px;
1971
- font-size: 10px;
2026
+ font-size: 9.5px;
2027
+ line-height: 1.6;
1972
2028
  letter-spacing: 0.09em;
1973
2029
  text-transform: uppercase;
1974
2030
  font-weight: 700;
1975
- padding: 1px 7px;
2031
+ padding: 2px 7px;
1976
2032
  border-radius: 999px;
1977
2033
  background: var(--accent-on);
1978
2034
  color: var(--accent);
@@ -2415,6 +2471,12 @@ body.web-mode .content {
2415
2471
  border-color: var(--ring);
2416
2472
  box-shadow: 0 0 0 1px var(--ring);
2417
2473
  }
2474
+ /* A scheduled post's body is locked, so focus rings it in grey, as the locked subject's is,
2475
+ rather than in the blue that says "type here" (DESIGN §3). */
2476
+ .composer.composer-locked:focus-within {
2477
+ border-color: var(--muted);
2478
+ box-shadow: 0 0 0 1px var(--muted);
2479
+ }
2418
2480
  .composer-head {
2419
2481
  display: flex;
2420
2482
  align-items: flex-end;
@@ -2621,14 +2683,25 @@ body.web-mode .content {
2621
2683
  }
2622
2684
  .composer-body.locked textarea.editor {
2623
2685
  caret-color: transparent;
2686
+ cursor: default;
2687
+ }
2688
+ /* Its text is muted, as the subject's is, so the body reads as locked at a glance. */
2689
+ .composer-body.locked .md-hl code {
2690
+ opacity: 0.72;
2624
2691
  }
2625
- /* The subject and slug of a scheduled post are read-only too, and readonly (unlike disabled)
2626
- carries no native greying, so they take the same recessed surface as the body: otherwise
2627
- they read as editable, and the refusal pulses a line far below where the keystroke landed. */
2628
- #f-subject[readonly],
2629
- #f-slug[readonly] {
2692
+ /* The subject of a scheduled post is read-only too, and readonly (unlike disabled) carries no
2693
+ native greying, so it is styled to look locked while keeping what disabled would take away:
2694
+ it can still be focused, selected, and copied, and a keystroke still pulses the foot line.
2695
+ The recessed surface, muted text, an arrow cursor, and a grey focus ring in place of the
2696
+ blue one that says "type here" (DESIGN §3). */
2697
+ #f-subject[readonly] {
2630
2698
  background: var(--sunken);
2631
2699
  border-color: var(--line-soft);
2700
+ color: var(--muted);
2701
+ cursor: default;
2702
+ }
2703
+ #f-subject[readonly]:focus-visible {
2704
+ outline-color: var(--muted);
2632
2705
  }
2633
2706
  .composer-body.dragover::after {
2634
2707
  content: "Drop images to upload";
@@ -2696,6 +2769,11 @@ body.web-mode .content {
2696
2769
  }
2697
2770
  }
2698
2771
 
2772
+ /* The editor's subject, body, and actions sit on the page itself, with no card around them:
2773
+ the fields and the composer carry their own borders, aligned with the head and the banner. */
2774
+ .editor-form > label:first-child {
2775
+ margin-top: 6px;
2776
+ }
2699
2777
  .actions-bar {
2700
2778
  display: flex;
2701
2779
  align-items: center;
@@ -2910,13 +2988,10 @@ body.web-mode .content {
2910
2988
  flex: none;
2911
2989
  white-space: nowrap;
2912
2990
  }
2913
- /* The back link at the top of the Send-now view needs no divider — it returns from,
2914
- rather than demotes below, the Schedule view. */
2915
- .modal .altrow-top {
2916
- margin: -4px 0 10px;
2917
- padding-top: 0;
2918
- border-top: 0;
2919
- justify-content: flex-start;
2991
+ /* The Send-now view's way back to the Schedule view, at the start of its button row: it
2992
+ returns from, rather than demotes below, the Schedule view, so it takes no row of its own. */
2993
+ .modal .actions .actions-back {
2994
+ margin-right: auto;
2920
2995
  }
2921
2996
  /* Link — a text-link-styled action (DESIGN §7), not one of the button roles. Reserved
2922
2997
  for a deliberately subordinate choice, like the in-modal Send now that sits below
@@ -3122,16 +3197,6 @@ textarea.is-invalid {
3122
3197
  .modal .dialog-error {
3123
3198
  margin-top: 12px;
3124
3199
  }
3125
- /* Label + info icon grouped at the top of the slug field. */
3126
- .label-row {
3127
- display: flex;
3128
- align-items: center;
3129
- gap: 6px;
3130
- margin: 14px 0 8px;
3131
- }
3132
- .label-row label {
3133
- margin: 0;
3134
- }
3135
3200
  /* Info icon: the field explanation lives in its hover/focus tooltip. The padding
3136
3201
  + equal negative margin enlarge the hover/click target to ~28px without moving
3137
3202
  the glyph or growing the label row. */
@@ -3196,28 +3261,6 @@ textarea.is-invalid {
3196
3261
  visibility: visible;
3197
3262
  transform: translateY(0);
3198
3263
  }
3199
- /* The "Auto-generate from subject" toggle, grouped just under the slug field. */
3200
- .slug-auto-toggle {
3201
- display: inline-flex;
3202
- align-items: center;
3203
- gap: 6px;
3204
- margin-top: 8px;
3205
- font-size: 14px;
3206
- color: var(--muted);
3207
- cursor: pointer;
3208
- user-select: none;
3209
- }
3210
- .slug-auto-toggle input {
3211
- width: 14px;
3212
- height: 14px;
3213
- margin: 0;
3214
- accent-color: var(--ring);
3215
- cursor: pointer;
3216
- }
3217
- /* Slug is muted while it follows the subject; normal once set by hand. */
3218
- #f-slug.slug-auto {
3219
- color: var(--muted);
3220
- }
3221
3264
  /* --- Editor-state banner (DESIGN §5): a standing, in-flow line at the top of the
3222
3265
  editor, in two variants. `scheduled` marks a frozen, soft-locked post (SPEC §6);
3223
3266
  `conflict` warns the draft changed elsewhere (another tab, or Claude), and its two
@@ -3285,16 +3328,327 @@ textarea.is-invalid {
3285
3328
  font-size: 12.5px;
3286
3329
  color: var(--status-scheduled-fg);
3287
3330
  }
3288
-
3289
- .grid2 {
3290
- display: grid;
3291
- grid-template-columns: 1fr 1fr;
3331
+ /* The Email only box under a dialog's time: the label, then its helper beneath. */
3332
+ .listing-check {
3333
+ display: flex;
3334
+ align-items: flex-start;
3292
3335
  gap: 10px;
3336
+ margin: 14px 0 0;
3337
+ font-size: 14px;
3338
+ color: var(--fg);
3339
+ cursor: pointer;
3293
3340
  }
3294
- @media (max-width: 620px) {
3295
- .grid2 {
3296
- grid-template-columns: 1fr;
3341
+ .listing-check input {
3342
+ width: auto;
3343
+ margin: 3px 0 0;
3344
+ flex: 0 0 auto;
3345
+ accent-color: var(--ring);
3346
+ cursor: pointer;
3347
+ }
3348
+ .listing-check strong {
3349
+ font-weight: 600;
3350
+ }
3351
+ .listing-help {
3352
+ display: block;
3353
+ margin-top: 2px;
3354
+ font-size: 13px;
3355
+ line-height: 1.45;
3356
+ color: var(--muted);
3357
+ }
3358
+ /* A scheduled post's banner in two rows (DESIGN §4): the time, its notes, and the controls that
3359
+ act on it; then, under a hairline, where the post is going, on a slightly deeper ground. */
3360
+ .banner-split {
3361
+ display: block;
3362
+ padding: 0;
3363
+ overflow: hidden;
3364
+ }
3365
+ .banner-top {
3366
+ display: flex;
3367
+ align-items: center;
3368
+ justify-content: space-between;
3369
+ gap: 12px;
3370
+ flex-wrap: wrap;
3371
+ padding: 12px 16px;
3372
+ font-size: 14px;
3373
+ }
3374
+ .banner-bottom {
3375
+ display: flex;
3376
+ align-items: center;
3377
+ gap: 8px;
3378
+ padding: 9px 16px;
3379
+ border-top: 1px solid var(--status-scheduled-line);
3380
+ /* A shade deeper than the banner, so the row reads as its footer rather than a strip of its own. */
3381
+ background: color-mix(in srgb, var(--status-scheduled-fg) 6%, var(--status-scheduled-bg));
3382
+ font-size: 13px;
3383
+ }
3384
+ .banner-bottom svg {
3385
+ flex: none;
3386
+ width: 14px;
3387
+ height: 14px;
3388
+ }
3389
+ .sched-dest-icon {
3390
+ display: flex;
3391
+ flex: none;
3392
+ }
3393
+ /* A filled icon takes the row's violet; a stroked one (fill="none") already draws in it. */
3394
+ .banner-bottom svg:not([fill="none"]),
3395
+ #settingsBtn svg {
3396
+ fill: currentColor;
3397
+ }
3398
+ #schedDest {
3399
+ min-width: 0;
3400
+ overflow-wrap: anywhere;
3401
+ }
3402
+ .sched-dest code {
3403
+ font-family: var(--mono);
3404
+ font-size: 12.5px;
3405
+ }
3406
+ .pub-taken code {
3407
+ font-family: var(--mono);
3408
+ font-size: 12.5px;
3409
+ color: var(--fg);
3410
+ }
3411
+ /* The settings form in a dialog under the time, set off from it by a rule. */
3412
+ .dialog-settings {
3413
+ margin-top: 14px;
3414
+ padding-top: 2px;
3415
+ }
3416
+ .dialog-settings .listing-check {
3417
+ margin-top: 0;
3418
+ }
3419
+ /* The archive address as one URL field: the base greyed and fixed, then the slug, the one part
3420
+ to type. The whole field is the label, so a click on the base lands in the slug too. */
3421
+ .pub-address {
3422
+ margin-top: 14px;
3423
+ }
3424
+ .pub-label {
3425
+ display: block;
3426
+ margin: 0 0 6px;
3427
+ font-size: 13px;
3428
+ color: var(--muted);
3429
+ }
3430
+ .url-field {
3431
+ display: flex;
3432
+ align-items: stretch;
3433
+ min-width: 0;
3434
+ margin: 0;
3435
+ overflow: hidden;
3436
+ border: 1px solid var(--line);
3437
+ border-radius: 6px;
3438
+ background: var(--card);
3439
+ font-family: var(--mono);
3440
+ font-size: 13px;
3441
+ cursor: text;
3442
+ }
3443
+ .url-field:focus-within {
3444
+ border-color: var(--ring);
3445
+ box-shadow: 0 0 0 1px var(--ring);
3446
+ }
3447
+ .url-field.is-invalid {
3448
+ border-color: var(--danger-fg);
3449
+ }
3450
+ .url-base {
3451
+ display: flex;
3452
+ align-items: center;
3453
+ padding: 8px 0 8px 10px;
3454
+ white-space: nowrap;
3455
+ color: var(--muted);
3456
+ }
3457
+ .url-field input {
3458
+ flex: 1;
3459
+ min-width: 6em;
3460
+ border: 0;
3461
+ border-radius: 0;
3462
+ padding: 8px 10px 8px 0;
3463
+ font-family: var(--mono);
3464
+ font-size: 13px;
3465
+ font-weight: 600;
3466
+ }
3467
+ .url-field input:focus-visible {
3468
+ outline: none;
3469
+ }
3470
+ .url-field.readonly {
3471
+ cursor: default;
3472
+ background: var(--sunken);
3473
+ }
3474
+ .url-field.readonly input {
3475
+ color: var(--muted);
3476
+ font-weight: 500;
3477
+ background: transparent;
3478
+ }
3479
+ @media (max-width: 480px) {
3480
+ .url-field {
3481
+ flex-direction: column;
3482
+ }
3483
+ .url-base {
3484
+ padding-bottom: 0;
3297
3485
  }
3486
+ .url-field input {
3487
+ padding-left: 10px;
3488
+ }
3489
+ }
3490
+ /* A refused address, in the server's words, under its field. */
3491
+ .pub-error {
3492
+ margin: 8px 0 0;
3493
+ font-size: 13px;
3494
+ color: var(--danger-fg);
3495
+ }
3496
+ /* The Post settings button carries a gear before its words. */
3497
+ #settingsBtn {
3498
+ display: inline-flex;
3499
+ align-items: center;
3500
+ gap: 6px;
3501
+ }
3502
+ #settingsBtn svg {
3503
+ width: 16px;
3504
+ height: 16px;
3505
+ }
3506
+ .pub-taken {
3507
+ display: flex;
3508
+ align-items: flex-start;
3509
+ gap: 6px;
3510
+ margin: 8px 0 0;
3511
+ font-size: 13px;
3512
+ line-height: 1.5;
3513
+ color: var(--muted);
3514
+ }
3515
+ .pub-taken svg {
3516
+ width: 15px;
3517
+ height: 15px;
3518
+ flex: none;
3519
+ margin-top: 2px;
3520
+ }
3521
+ .pub-note {
3522
+ margin: 8px 0 0;
3523
+ font-size: 13px;
3524
+ color: var(--muted);
3525
+ }
3526
+ /* A sent post's page in two parts (DESIGN §3): the post, then its delivery. */
3527
+ .rec-head-page {
3528
+ margin-bottom: 14px;
3529
+ }
3530
+ .rec-part-title {
3531
+ margin: 0 0 10px;
3532
+ font-size: 11px;
3533
+ font-weight: 600;
3534
+ letter-spacing: 0.08em;
3535
+ text-transform: uppercase;
3536
+ color: var(--muted);
3537
+ }
3538
+ .rec-web {
3539
+ display: grid;
3540
+ }
3541
+ .rec-link-label {
3542
+ margin: 0 0 6px;
3543
+ font-size: 12.5px;
3544
+ color: var(--muted);
3545
+ }
3546
+ /* The link as one field: the address, then Copy and Open attached to it. */
3547
+ .rec-link {
3548
+ display: flex;
3549
+ align-items: stretch;
3550
+ min-width: 0;
3551
+ overflow: hidden;
3552
+ border: 1px solid var(--line);
3553
+ border-radius: 7px;
3554
+ background: var(--sunken);
3555
+ }
3556
+ /* An address is one line, shortened at its end, since Copy carries it whole. */
3557
+ .rec-link code {
3558
+ flex: 1 1 auto;
3559
+ align-self: center;
3560
+ min-width: 0;
3561
+ padding: 8px 10px;
3562
+ font-family: var(--mono);
3563
+ font-size: 12.5px;
3564
+ white-space: nowrap;
3565
+ overflow: hidden;
3566
+ text-overflow: ellipsis;
3567
+ }
3568
+ .rec-link button {
3569
+ flex: none;
3570
+ display: inline-flex;
3571
+ align-items: center;
3572
+ gap: 5px;
3573
+ min-height: 35px;
3574
+ padding: 0 11px;
3575
+ border: 0;
3576
+ border-left: 1px solid var(--line);
3577
+ border-radius: 0;
3578
+ background: var(--card);
3579
+ color: var(--muted);
3580
+ font-size: 13px;
3581
+ white-space: nowrap;
3582
+ }
3583
+ .rec-link button:hover {
3584
+ color: var(--fg);
3585
+ background: var(--hover);
3586
+ }
3587
+ .rec-link button.rec-open {
3588
+ color: var(--accent);
3589
+ font-weight: 500;
3590
+ }
3591
+ .rec-link-note {
3592
+ margin: 6px 0 0;
3593
+ font-size: 12.5px;
3594
+ }
3595
+ /* Where the post stands, in words beside the one change it allows (which asks first). */
3596
+ .rec-arch {
3597
+ display: flex;
3598
+ align-items: center;
3599
+ justify-content: space-between;
3600
+ flex-wrap: wrap;
3601
+ gap: 10px 16px;
3602
+ margin: 14px -16px 0;
3603
+ padding: 14px 16px 0;
3604
+ border-top: 1px solid var(--line);
3605
+ }
3606
+ /* A post with no web link yet has only its standing to show, with no rule above it. */
3607
+ .rec-web > .rec-arch:first-child {
3608
+ margin-top: 0;
3609
+ padding-top: 0;
3610
+ border-top: 0;
3611
+ }
3612
+ .rec-arch-what {
3613
+ flex: 1 1 220px;
3614
+ min-width: 0;
3615
+ font-size: 14px;
3616
+ }
3617
+ .rec-arch-what strong {
3618
+ display: flex;
3619
+ align-items: center;
3620
+ gap: 6px;
3621
+ font-weight: 600;
3622
+ }
3623
+ .rec-arch-what svg {
3624
+ width: 16px;
3625
+ height: 16px;
3626
+ flex: none;
3627
+ color: var(--muted);
3628
+ }
3629
+ .rec-arch-what svg:not([fill="none"]) {
3630
+ fill: currentColor;
3631
+ }
3632
+ .rec-arch-note {
3633
+ display: block;
3634
+ margin-top: 2px;
3635
+ font-size: 13px;
3636
+ line-height: 1.45;
3637
+ color: var(--muted);
3638
+ }
3639
+ /* The address Remove from archive takes down, struck through in its dialog. */
3640
+ .url-gone {
3641
+ padding: 8px 10px;
3642
+ background: var(--sunken);
3643
+ border: 1px solid var(--line);
3644
+ border-radius: 6px;
3645
+ font-family: var(--mono);
3646
+ font-size: 13px;
3647
+ color: var(--muted);
3648
+ overflow-wrap: anywhere;
3649
+ }
3650
+
3651
+ @media (max-width: 620px) {
3298
3652
  .tb {
3299
3653
  width: 34px;
3300
3654
  height: 34px;
@@ -4209,7 +4563,8 @@ button.api-filter-clear svg {
4209
4563
  gap: 10px;
4210
4564
  }
4211
4565
 
4212
- /* Setup checklist — shared by the first-run dashboard and Getting started. */
4566
+ /* Setup steps: the first-run checklist, and the regular dashboard's Finish setting up card
4567
+ (one component, two layouts). The card is the compact one. */
4213
4568
  .setup-title {
4214
4569
  margin-top: 0;
4215
4570
  }
@@ -4282,6 +4637,20 @@ button.api-filter-clear svg {
4282
4637
  color: var(--muted);
4283
4638
  word-break: break-all;
4284
4639
  }
4640
+ .setup-card {
4641
+ margin-bottom: 20px;
4642
+ }
4643
+ .setup-card .setup-title {
4644
+ font-size: 16px;
4645
+ }
4646
+ .setup-card .setup-steps li {
4647
+ padding: 8px 2px;
4648
+ }
4649
+ .setup-card .setup-steps li::before {
4650
+ width: 22px;
4651
+ height: 22px;
4652
+ font-size: 12px;
4653
+ }
4285
4654
 
4286
4655
  /* ============================================================
4287
4656
  Settings page. Grouped by intent, made visible: each section
@@ -4378,6 +4747,17 @@ button.copied svg {
4378
4747
  .pub-row button.copy-btn {
4379
4748
  flex: 0 0 auto;
4380
4749
  }
4750
+ /* In a sent post's link field, Copy is a segment of the field like Open: square, full height,
4751
+ and divided from the address, so its hover fills the segment rather than a chip inside it. */
4752
+ .rec-link button.copy-btn {
4753
+ padding: 0 11px;
4754
+ border-left: 1px solid var(--line);
4755
+ border-radius: 0;
4756
+ background: var(--card);
4757
+ }
4758
+ .rec-link button.copy-btn:hover {
4759
+ background: var(--hover);
4760
+ }
4381
4761
 
4382
4762
  /* --- groups: the sections kept together by whom they serve (DESIGN §3). A group has no
4383
4763
  heading, since each section's title already says what it is; a wider gap than the one