@kurtbruns/kestrel 1.9.0 → 1.11.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 (34) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/README.md +19 -27
  3. package/cli/context.mjs +5 -1
  4. package/dist/public/dashboard/app.js +367 -256
  5. package/dist/public/dashboard/app.js.map +4 -4
  6. package/dist/public/dashboard/index.html +2 -2
  7. package/dist/public/dashboard/styles.css +493 -24
  8. package/dist/worker/index.js +2658 -2065
  9. package/docs/README.md +4 -5
  10. package/docs/get-started/02-deploy.md +12 -5
  11. package/docs/get-started/03-access.md +2 -0
  12. package/docs/get-started/04-ses.md +57 -17
  13. package/docs/get-started/05-verify.md +22 -8
  14. package/docs/get-started/06-go-live.md +13 -53
  15. package/docs/guides/01-connect-claude.md +7 -3
  16. package/docs/guides/03-notifications.md +11 -11
  17. package/docs/guides/09-as-your-list-grows.md +62 -0
  18. package/docs/publishing/01-your-list.md +157 -0
  19. package/docs/publishing/02-sending-rules.md +35 -0
  20. package/docs/publishing/03-name-your-publication.md +20 -0
  21. package/docs/publishing/04-write-a-post.md +55 -0
  22. package/docs/publishing/06-troubleshooting.md +74 -0
  23. package/docs/publishing/07-working-with-claude.md +29 -0
  24. package/docs/publishing/09-share-your-subscribe-link.md +25 -0
  25. package/docs/publishing/10-check-your-setup.md +30 -0
  26. package/docs/publishing/11-email-template.md +34 -0
  27. package/docs/publishing/README.md +27 -0
  28. package/docs/reference/01-configuration.md +6 -6
  29. package/docs/reference/02-sending-domain-dns.md +4 -2
  30. package/package.json +2 -1
  31. package/template/claude/CLAUDE.md +5 -2
  32. package/template/claude/skills/migrate-list/SKILL.md +56 -0
  33. package/docs/guides/08-try-locally.md +0 -108
  34. package/docs/reference/03-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=0dd9edbf">
8
+ <link rel="stylesheet" href="./styles.css?v=abd15153">
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=f4ffb3bd"></script>
116
+ <script src="./app.js?v=496b944d"></script>
117
117
  </body>
118
118
  </html>
@@ -438,6 +438,39 @@ body.nav-resizing .sidebar {
438
438
  font-weight: 600;
439
439
  margin: 0 0 10px;
440
440
  }
441
+ /* The tab per doc set under the docs header: an underlined strip, the current set in the
442
+ foreground over an accent rule, the way tabs read as switchable. Links, since each set's
443
+ index is its own address. */
444
+ .docs-tabs {
445
+ display: flex;
446
+ gap: 24px;
447
+ border-bottom: 1px solid var(--line);
448
+ margin: 0 0 20px;
449
+ }
450
+ .docs-tabs a {
451
+ padding: 8px 0 10px;
452
+ margin-bottom: -1px;
453
+ border-bottom: 2px solid transparent;
454
+ color: var(--muted);
455
+ font-size: 15px;
456
+ font-weight: 600;
457
+ text-decoration: none;
458
+ }
459
+ .docs-tabs a:hover {
460
+ color: var(--fg);
461
+ }
462
+ .docs-tabs a[aria-current="page"] {
463
+ color: var(--fg);
464
+ border-bottom-color: var(--accent);
465
+ }
466
+ /* A doc page's set, in the eyebrow over its title: the link back to that set's index. */
467
+ .doc .doc-set a {
468
+ color: var(--muted);
469
+ text-decoration: none;
470
+ }
471
+ .doc .doc-set a:hover {
472
+ color: var(--accent);
473
+ }
441
474
  .docs-index > h1 {
442
475
  font-size: 28px;
443
476
  line-height: 1.15;
@@ -1492,12 +1525,18 @@ td a:hover {
1492
1525
  text-decoration: underline;
1493
1526
  }
1494
1527
 
1528
+ /* A badge is a plain label, so it shows the default cursor rather than the text one. In a
1529
+ row or card that opens on click it keeps the container's pointer. */
1495
1530
  .badge {
1496
1531
  display: inline-block;
1497
1532
  padding: 2px 8px;
1498
1533
  border-radius: 999px;
1499
1534
  font-size: 12px;
1500
1535
  border: 1px solid var(--line);
1536
+ cursor: default;
1537
+ }
1538
+ .clickable .badge {
1539
+ cursor: inherit;
1501
1540
  }
1502
1541
  .badge.draft {
1503
1542
  background: var(--status-draft-bg);
@@ -1545,6 +1584,16 @@ td a:hover {
1545
1584
  color: var(--danger-fg);
1546
1585
  border-color: var(--danger-line);
1547
1586
  }
1587
+ /* A suppressed address's consent status, kept but not in effect, takes the unsubscribed
1588
+ grey: grey is "not mailed", whatever the label says, so a confirmed-but-suppressed row
1589
+ never reads as one that is mailed. The red flag says why. It overrides the status's own
1590
+ hue (.badge.confirmed and the rest, same specificity) by coming after them, so keep it
1591
+ below them. */
1592
+ .badge.held {
1593
+ background: var(--status-draft-bg);
1594
+ color: var(--status-draft-fg);
1595
+ border-color: var(--status-draft-line);
1596
+ }
1548
1597
  /* Suppression is an overlay on the address, not a consent status, so it rides next to
1549
1598
  the email rather than in the Status column — sized down to read as secondary. */
1550
1599
  .row-flag {
@@ -1554,6 +1603,128 @@ td a:hover {
1554
1603
  vertical-align: middle;
1555
1604
  }
1556
1605
 
1606
+ /* The Subscribers summary: the counts, with the Legend toggle at the card's right edge
1607
+ (at the end of the counts' last line when they wrap), and the legend opening below
1608
+ across the card. The legend is in the page's own colors (not an inverted tooltip),
1609
+ each pill drawn as the list draws it beside its definition. */
1610
+ .sub-summary-head {
1611
+ display: flex;
1612
+ align-items: flex-end;
1613
+ justify-content: space-between;
1614
+ gap: 8px 16px;
1615
+ }
1616
+ /* The negative margin lines the first count's text up with the card's edge. */
1617
+ #subCounts {
1618
+ gap: 10px 12px;
1619
+ margin-left: -8px;
1620
+ }
1621
+ /* Each count is a filter (DESIGN §3): it reads as the plain count it was, and shows on
1622
+ hover that it can be chosen. The chosen one takes the Secondary palette of a pressed
1623
+ toggle, since it switches what you're looking at and commits nothing. */
1624
+ .sub-count {
1625
+ border: 0;
1626
+ background: transparent;
1627
+ color: inherit;
1628
+ font-size: inherit;
1629
+ padding: 4px 8px;
1630
+ margin: -4px 0;
1631
+ border-radius: 6px;
1632
+ }
1633
+ .sub-count:hover {
1634
+ background: var(--hover);
1635
+ color: var(--fg);
1636
+ }
1637
+ .sub-count[aria-pressed="true"] {
1638
+ background: var(--accent-soft);
1639
+ color: var(--accent);
1640
+ }
1641
+ /* Forced colors drop the tint, so the chosen count keeps an outline there. */
1642
+ @media (forced-colors: active) {
1643
+ .sub-count[aria-pressed="true"] {
1644
+ outline: 1px solid ButtonText;
1645
+ }
1646
+ }
1647
+ /* The padding and equal negative margin give a 24px target without moving the label. */
1648
+ .sub-legend-toggle {
1649
+ flex: none;
1650
+ display: inline-flex;
1651
+ align-items: center;
1652
+ gap: 8px;
1653
+ padding: 4px 6px;
1654
+ margin: -4px -6px;
1655
+ border: 0;
1656
+ background: transparent;
1657
+ color: var(--muted);
1658
+ font-size: 13px;
1659
+ border-radius: 4px;
1660
+ }
1661
+ .sub-legend-toggle:hover {
1662
+ color: var(--fg);
1663
+ }
1664
+ .sub-legend-toggle::after {
1665
+ content: "";
1666
+ width: 6px;
1667
+ height: 6px;
1668
+ margin-top: -3px;
1669
+ border-right: 2px solid currentColor;
1670
+ border-bottom: 2px solid currentColor;
1671
+ transform: rotate(45deg);
1672
+ }
1673
+ .sub-legend-toggle[aria-expanded="true"]::after {
1674
+ margin-top: 3px;
1675
+ transform: rotate(-135deg);
1676
+ }
1677
+ /* One grid for every group, each list a subgrid, so every definition starts at the same
1678
+ edge. Each group is named by an h2 in the app's label style, and the panel has no title of
1679
+ its own: the Legend toggle already names it. Its headings are a step smaller and more
1680
+ widely tracked than the base label, and set apart by space alone (a group's break, 28px,
1681
+ is three and a half rows'), so the one rule in the panel is the divider under the counts.
1682
+ A paragraph keeps the app's 1rem beneath it. The panel carries the divider across the
1683
+ card; its body is the container that caps the line length, once, for everything in it. */
1684
+ .sub-legend {
1685
+ margin-top: 14px;
1686
+ padding-top: 18px;
1687
+ border-top: 1px solid var(--line);
1688
+ font-size: 13px;
1689
+ line-height: 1.45;
1690
+ }
1691
+ .sub-legend-body {
1692
+ display: grid;
1693
+ grid-template-columns: max-content 1fr;
1694
+ column-gap: 16px;
1695
+ max-width: 88ch;
1696
+ }
1697
+ .sub-legend h2 {
1698
+ grid-column: 1 / -1;
1699
+ margin: 28px 0 10px;
1700
+ font-size: 12px;
1701
+ letter-spacing: 0.08em;
1702
+ }
1703
+ .sub-legend h2:first-child {
1704
+ margin-top: 0;
1705
+ }
1706
+ .sub-legend dl {
1707
+ grid-column: 1 / -1;
1708
+ display: grid;
1709
+ grid-template-columns: subgrid;
1710
+ row-gap: 8px;
1711
+ align-items: baseline;
1712
+ margin: 0;
1713
+ }
1714
+ .sub-legend dd {
1715
+ margin: 0;
1716
+ }
1717
+ /* An act's name is a term, not a heading: lighter than bold, so it doesn't outweigh the
1718
+ pills in the column beside it. */
1719
+ .sub-legend-act {
1720
+ font-weight: 500;
1721
+ }
1722
+ .sub-legend p {
1723
+ grid-column: 1 / -1;
1724
+ margin: 0 0 1rem;
1725
+ color: var(--muted);
1726
+ }
1727
+
1557
1728
  /* list views (posts / subscribers / sends): search on the left, filters pinned right */
1558
1729
  .list-toolbar {
1559
1730
  display: flex;
@@ -1617,8 +1788,17 @@ td a:hover {
1617
1788
  .list-table .c-status {
1618
1789
  width: 128px;
1619
1790
  }
1791
+ /* Wide enough for the longest date on one line, a date outside the current year with
1792
+ its year ("Sep 30, 2027, 12:00 AM"), with room for a wider system font. Narrower
1793
+ screens, until the table stacks, keep the room for the text column instead (the Drafts
1794
+ table has two date columns), so an off-year date wraps there. */
1620
1795
  .list-table .c-date {
1621
- width: 148px;
1796
+ width: 176px;
1797
+ }
1798
+ @media (max-width: 840px) {
1799
+ .list-table .c-date {
1800
+ width: 148px;
1801
+ }
1622
1802
  }
1623
1803
  .list-table .c-num {
1624
1804
  width: 104px;
@@ -1633,6 +1813,14 @@ td a:hover {
1633
1813
  .list-table .c-act {
1634
1814
  width: 56px;
1635
1815
  }
1816
+ /* Subscribers: how each joined ("Imported · Sendy, single opt-in"), which may wrap to two
1817
+ lines; the table takes a wider floor so the email column keeps room beside it. */
1818
+ .list-table .c-how {
1819
+ width: 152px;
1820
+ }
1821
+ .list-table.subs-table {
1822
+ min-width: 680px;
1823
+ }
1636
1824
 
1637
1825
  .dash-section table {
1638
1826
  border-color: var(--line);
@@ -1788,6 +1976,7 @@ body.web-mode .content {
1788
1976
  border-radius: 999px;
1789
1977
  background: var(--accent-on);
1790
1978
  color: var(--accent);
1979
+ cursor: default;
1791
1980
  }
1792
1981
  .web-bar-text {
1793
1982
  overflow-wrap: anywhere;
@@ -1965,13 +2154,15 @@ body.web-mode .content {
1965
2154
  /* An outcome cell reuses the tile swatch palette (sw-ok / sw-warn / sw-danger /
1966
2155
  sw-sending / sw-neutral) so a row reads the same bucket as its tile above. The
1967
2156
  swatch + label hold one line — nowrap so the label never breaks mid-word in the
1968
- fixed-width column; the consequence rides in the cell's `title` tooltip and the
1969
- reconciliation line above, not inline, so the row stays a clean one-liner. */
2157
+ fixed-width column; the consequence is said in the reconciliation line above, not
2158
+ inline or on hover (DESIGN §11), so the row stays a clean one-liner. A plain label,
2159
+ so the default cursor. */
1970
2160
  .rec-out {
1971
2161
  display: inline-flex;
1972
2162
  align-items: center;
1973
2163
  gap: 7px;
1974
2164
  white-space: nowrap;
2165
+ cursor: default;
1975
2166
  }
1976
2167
  .rec-people-empty {
1977
2168
  margin: 14px 0 0;
@@ -1997,7 +2188,7 @@ body.web-mode .content {
1997
2188
  }
1998
2189
  /* The derived-phase pill (progressing / retrying / backing-off / needs-attention /
1999
2190
  settling / …). Tones reuse the status + semantic palettes so a phase reads the same
2000
- hue as its badge elsewhere. */
2191
+ hue as its badge elsewhere. A plain label, so the default cursor (DESIGN §11). */
2001
2192
  .phase-pill {
2002
2193
  display: inline-block;
2003
2194
  padding: 2px 10px;
@@ -2006,6 +2197,7 @@ body.web-mode .content {
2006
2197
  font-weight: 600;
2007
2198
  border: 1px solid var(--line);
2008
2199
  white-space: nowrap;
2200
+ cursor: default;
2009
2201
  }
2010
2202
  .phase-pill.tone-sending {
2011
2203
  background: var(--status-sending-bg);
@@ -2189,8 +2381,9 @@ body.web-mode .content {
2189
2381
  font-size: 12.5px;
2190
2382
  color: var(--warn-fg);
2191
2383
  }
2192
- /* A scheduled send while no provider is connected (DESIGN §4): the countdown stays, and a
2193
- line beneath it in the same amber says the send will be held when it fires. */
2384
+ /* A scheduled send's card while no provider is connected (DESIGN §4): the countdown stays,
2385
+ and a line beneath it in amber says the send will be held when it fires. The editor's
2386
+ violet banner says it as a .sched-note instead, since amber text doesn't read on violet. */
2194
2387
  .sched-held {
2195
2388
  margin-top: 4px;
2196
2389
  font-size: 12.5px;
@@ -2780,6 +2973,11 @@ button.linkbtn:hover {
2780
2973
  .page-head {
2781
2974
  margin-bottom: 16px;
2782
2975
  }
2976
+ /* A head with several acts (Subscribers: Import, Export, Add subscriber) puts them under
2977
+ the title when the two don't fit side by side, rather than squeezing either. */
2978
+ .spread.page-head {
2979
+ flex-wrap: wrap;
2980
+ }
2783
2981
  .page-head-row {
2784
2982
  display: flex;
2785
2983
  align-items: center;
@@ -2998,18 +3196,6 @@ textarea.is-invalid {
2998
3196
  visibility: visible;
2999
3197
  transform: translateY(0);
3000
3198
  }
3001
- /* Drop the tip below the icon instead of above it. The default opens upward,
3002
- which clips against the top of the viewport when the icon sits at the top of a
3003
- page (e.g. the Subscribers counts card); this variant opens downward there. */
3004
- .info.tip-below::after {
3005
- top: calc(100% + 8px);
3006
- bottom: auto;
3007
- transform: translateY(-2px);
3008
- }
3009
- .info.tip-below:hover::after,
3010
- .info.tip-below:focus::after {
3011
- transform: translateY(0);
3012
- }
3013
3199
  /* The "Auto-generate from subject" toggle, grouped just under the slug field. */
3014
3200
  .slug-auto-toggle {
3015
3201
  display: inline-flex;
@@ -3075,10 +3261,12 @@ textarea.is-invalid {
3075
3261
  margin-top: 6px;
3076
3262
  font-size: 13px;
3077
3263
  }
3078
- /* The scheduled banner's line for a send too large for D1's daily writes on Workers Free's
3079
- limits (SPEC §8): the server's condition, in the banner's own violet so a warning that
3080
- blocks nothing never outweighs the send, with an amber ⚠️ to say it's worth a look. */
3081
- .sched-daily-writes {
3264
+ /* The scheduled banner's notes under the countdown (DESIGN §4): a send that will be held
3265
+ with no provider connected, and one too large for D1's daily writes on Workers Free's
3266
+ limits. Each keeps the banner's own violet, since amber words on violet fall short of
3267
+ contrast and mix two of the color meanings, and an amber ⚠️ leads it to say it's worth a
3268
+ look. Held is the stronger of the two (the send won't go out), so it keeps the weight. */
3269
+ .sched-note {
3082
3270
  display: flex;
3083
3271
  gap: 6px;
3084
3272
  align-items: baseline;
@@ -3089,6 +3277,9 @@ textarea.is-invalid {
3089
3277
  .sched-note-mark {
3090
3278
  flex: none;
3091
3279
  }
3280
+ .sched-note-held {
3281
+ font-weight: 500;
3282
+ }
3092
3283
  .sched-remade-mark {
3093
3284
  margin-top: 4px;
3094
3285
  font-size: 12.5px;
@@ -4053,6 +4244,13 @@ button.api-filter-clear svg {
4053
4244
  font-size: 13px;
4054
4245
  font-variant-numeric: tabular-nums;
4055
4246
  }
4247
+ /* A step already done (the list has subscribers): a check in place of its number. */
4248
+ .setup-steps li.setup-done::before {
4249
+ content: "✓";
4250
+ color: var(--ok-fg);
4251
+ border-color: var(--ok-line);
4252
+ background: var(--ok-bg);
4253
+ }
4056
4254
  .setup-step-main {
4057
4255
  flex: 1 1 auto;
4058
4256
  min-width: 0;
@@ -4208,7 +4406,8 @@ button.copied svg {
4208
4406
  }
4209
4407
 
4210
4408
  /* Intent chips — the grouping made visible. Editable wears the Kestrel accent;
4211
- read-only wears a warn tone; copy-out stays muted. */
4409
+ read-only wears a warn tone; copy-out stays muted. Plain labels, so the default
4410
+ cursor (DESIGN §11). */
4212
4411
  .set-chip {
4213
4412
  display: inline-flex;
4214
4413
  align-items: center;
@@ -4221,6 +4420,7 @@ button.copied svg {
4221
4420
  color: var(--muted);
4222
4421
  background: var(--card);
4223
4422
  white-space: nowrap;
4423
+ cursor: default;
4224
4424
  }
4225
4425
  .set-chip svg {
4226
4426
  width: 12px;
@@ -4474,6 +4674,12 @@ button.set-field-remove {
4474
4674
  font-size: 13.5px;
4475
4675
  overflow-x: auto;
4476
4676
  }
4677
+ /* A value with one quiet line of explanation under it (the From address and where replies go). */
4678
+ .set-kv > .set-kv-stack {
4679
+ flex-direction: column;
4680
+ align-items: flex-start;
4681
+ gap: 3px;
4682
+ }
4477
4683
  .set-kv > div:nth-last-child(1),
4478
4684
  .set-kv > div:nth-last-child(2) {
4479
4685
  border-bottom: 0;
@@ -4482,6 +4688,7 @@ button.set-field-remove {
4482
4688
  font-family: var(--mono);
4483
4689
  font-size: 13px;
4484
4690
  }
4691
+ /* A status pill in the settings reflection: a plain label, so the default cursor (DESIGN §11). */
4485
4692
  .set-pill {
4486
4693
  display: inline-flex;
4487
4694
  align-items: center;
@@ -4494,6 +4701,7 @@ button.set-field-remove {
4494
4701
  color: var(--muted);
4495
4702
  border: 1px solid var(--line-soft);
4496
4703
  white-space: nowrap;
4704
+ cursor: default;
4497
4705
  }
4498
4706
  .set-pill.ok {
4499
4707
  background: var(--ok-bg);
@@ -5366,6 +5574,7 @@ button.set-embed-copy svg {
5366
5574
  border-radius: 999px;
5367
5575
  border: 1px solid var(--line-soft);
5368
5576
  color: var(--muted);
5577
+ cursor: default;
5369
5578
  }
5370
5579
  .set-req-pill .dot {
5371
5580
  width: 6px;
@@ -5916,8 +6125,268 @@ body.has-savebar .content {
5916
6125
  .posts-table td.updated::before {
5917
6126
  content: " · edited ";
5918
6127
  }
5919
- /* Subscribers: status pill · joined <date>. */
6128
+ }
6129
+
6130
+ /* The Subscribers table has four fixed columns beside its email, so it runs out of room
6131
+ well above phone width, and where depends on the sidebar (a rail up to 1024px, then
6132
+ full), not the viewport. So it stacks by its own width, measured on its wrapper,
6133
+ whenever the email would get less than about 250px: the same stacked row as the
6134
+ phone layout above, repeated here because a container query can't share its rules. */
6135
+ .subs-wrap {
6136
+ container-type: inline-size;
6137
+ }
6138
+ @container (max-width: 759px) {
6139
+ .list-table.subs-table.stacks,
6140
+ .list-table.subs-table.stacks tbody,
6141
+ .list-table.subs-table.stacks tr {
6142
+ display: block;
6143
+ min-width: 0;
6144
+ }
6145
+ .list-table.subs-table.stacks colgroup,
6146
+ .list-table.subs-table.stacks thead {
6147
+ display: none;
6148
+ }
6149
+ .list-table.subs-table.stacks tr {
6150
+ position: relative;
6151
+ padding: 11px 14px;
6152
+ border-bottom: 1px solid var(--line);
6153
+ }
6154
+ .list-table.subs-table.stacks tr:last-child {
6155
+ border-bottom: 0;
6156
+ }
6157
+ .list-table.subs-table.stacks td {
6158
+ display: inline;
6159
+ padding: 0;
6160
+ border: 0;
6161
+ font-size: 13px;
6162
+ text-align: left;
6163
+ }
6164
+ .list-table.subs-table.stacks td:first-child {
6165
+ display: block;
6166
+ margin-bottom: 4px;
6167
+ font-size: 14px;
6168
+ padding-right: 36px;
6169
+ }
6170
+ .list-table.subs-table.stacks td.act {
6171
+ position: absolute;
6172
+ top: 6px;
6173
+ right: 6px;
6174
+ width: auto;
6175
+ }
6176
+ .list-table.subs-table.stacks td.empty {
6177
+ display: none;
6178
+ }
6179
+ /* Subscribers: status pill · how they joined · joined <date>. */
6180
+ .subs-table td.how::before {
6181
+ content: " · ";
6182
+ }
5920
6183
  .subs-table td.joined::before {
5921
6184
  content: " · joined ";
5922
6185
  }
5923
6186
  }
6187
+
6188
+ /* --- Import (DESIGN §3, Import subscribers): the format before the file, the plan of the
6189
+ file chosen with what the commit needs, then the result. Cards on the page ground, the
6190
+ drop zone dashed so it reads as a place to put something. --- */
6191
+ .imp {
6192
+ max-width: 820px;
6193
+ }
6194
+ .imp-card h2 {
6195
+ margin: 0 0 10px;
6196
+ }
6197
+ /* Where the page moves focus after a plan, a re-plan, or the result: a place to read from,
6198
+ not a control, so it takes no ring. */
6199
+ .imp-file:focus,
6200
+ .imp-act-error:focus,
6201
+ #impDoneHead:focus {
6202
+ outline: none;
6203
+ }
6204
+ .imp-card-head {
6205
+ display: flex;
6206
+ align-items: center;
6207
+ justify-content: space-between;
6208
+ gap: 12px;
6209
+ flex-wrap: wrap;
6210
+ margin-bottom: 10px;
6211
+ }
6212
+ .imp-card-head h2 {
6213
+ margin: 0;
6214
+ }
6215
+ .imp-text {
6216
+ font-size: 13.5px;
6217
+ line-height: 1.55;
6218
+ margin: 0 0 10px;
6219
+ }
6220
+ .imp-sample,
6221
+ .imp-ref {
6222
+ font-size: 12.5px;
6223
+ margin: 4px 0 8px;
6224
+ }
6225
+ .imp-sample td,
6226
+ .imp-ref td {
6227
+ vertical-align: top;
6228
+ }
6229
+ .imp-sample td {
6230
+ font-family: var(--mono);
6231
+ white-space: nowrap;
6232
+ }
6233
+ .imp-links {
6234
+ margin-top: 10px;
6235
+ }
6236
+ .imp-drop {
6237
+ display: flex;
6238
+ flex-direction: column;
6239
+ align-items: center;
6240
+ gap: 8px;
6241
+ padding: 26px 16px;
6242
+ border: 2px dashed var(--line-soft);
6243
+ border-radius: 10px;
6244
+ background: var(--card);
6245
+ text-align: center;
6246
+ }
6247
+ .imp-drop.over {
6248
+ border-color: var(--accent);
6249
+ background: var(--accent-soft);
6250
+ }
6251
+ .imp-drop > svg {
6252
+ width: 22px;
6253
+ height: 22px;
6254
+ color: var(--muted);
6255
+ }
6256
+ .imp-drop-line {
6257
+ margin: 0;
6258
+ font-size: 13.5px;
6259
+ }
6260
+ .imp-drop .field-hint {
6261
+ margin: 0;
6262
+ }
6263
+ .imp-file-error {
6264
+ margin-top: 10px;
6265
+ }
6266
+ .imp-file-error .field-error {
6267
+ margin: 0 0 10px;
6268
+ font-size: 13.5px;
6269
+ }
6270
+ .imp-file-row {
6271
+ display: flex;
6272
+ align-items: center;
6273
+ justify-content: space-between;
6274
+ gap: 12px;
6275
+ flex-wrap: wrap;
6276
+ }
6277
+ .imp-file-name {
6278
+ overflow-wrap: anywhere;
6279
+ }
6280
+ .imp-plain {
6281
+ margin-top: 14px;
6282
+ padding-top: 12px;
6283
+ border-top: 1px solid var(--line);
6284
+ display: flex;
6285
+ align-items: center;
6286
+ gap: 10px;
6287
+ flex-wrap: wrap;
6288
+ }
6289
+ .imp-plain .field-hint {
6290
+ flex-basis: 100%;
6291
+ margin: 0;
6292
+ }
6293
+ .imp-plain-label {
6294
+ font-size: 13.5px;
6295
+ }
6296
+ .imp-outcomes {
6297
+ margin: 0 0 10px;
6298
+ padding-left: 18px;
6299
+ font-size: 13.5px;
6300
+ line-height: 1.55;
6301
+ }
6302
+ .imp-outcomes li + li {
6303
+ margin-top: 4px;
6304
+ }
6305
+ .imp-kept-head {
6306
+ font-size: 13px;
6307
+ font-weight: 600;
6308
+ margin: 14px 0 4px;
6309
+ }
6310
+ /* The same amber box the Schedule dialog gives D1's daily-writes warning (DESIGN §4). */
6311
+ .hint.daily-writes.imp-warn {
6312
+ color: var(--warn-fg);
6313
+ background: var(--warn-bg);
6314
+ border: 1px solid var(--warn-line);
6315
+ border-radius: 8px;
6316
+ padding: 10px 12px;
6317
+ font-size: 13px;
6318
+ line-height: 1.5;
6319
+ margin: 12px 0 0;
6320
+ }
6321
+ .imp-check {
6322
+ display: flex;
6323
+ align-items: flex-start;
6324
+ gap: 10px;
6325
+ font-size: 13.5px;
6326
+ line-height: 1.5;
6327
+ margin: 10px 0 0;
6328
+ cursor: pointer;
6329
+ }
6330
+ .imp-check input {
6331
+ width: auto;
6332
+ margin: 3px 0 0;
6333
+ flex: 0 0 auto;
6334
+ cursor: pointer;
6335
+ }
6336
+ .imp-check + .field-error {
6337
+ margin-left: 24px;
6338
+ }
6339
+ .imp-attest-hint {
6340
+ margin: 6px 0 0 24px;
6341
+ }
6342
+ .imp-act-error {
6343
+ margin-top: 14px;
6344
+ }
6345
+ .imp-actions {
6346
+ display: flex;
6347
+ justify-content: flex-end;
6348
+ align-items: center;
6349
+ gap: 14px;
6350
+ margin-top: 18px;
6351
+ }
6352
+ /* View subscriber's title is the address, so a long one breaks rather than widens the dialog. */
6353
+ .sub-title {
6354
+ overflow-wrap: anywhere;
6355
+ }
6356
+ .imp-dl,
6357
+ .sub-dl {
6358
+ display: grid;
6359
+ grid-template-columns: max-content 1fr;
6360
+ gap: 6px 16px;
6361
+ margin: 0;
6362
+ font-size: 13.5px;
6363
+ }
6364
+ .imp-dl dt,
6365
+ .sub-dl dt {
6366
+ color: var(--muted);
6367
+ }
6368
+ .imp-dl dd,
6369
+ .sub-dl dd {
6370
+ margin: 0;
6371
+ overflow-wrap: anywhere;
6372
+ }
6373
+ .sub-evidence {
6374
+ margin: 0;
6375
+ white-space: pre-wrap;
6376
+ background: var(--bg);
6377
+ border: 1px solid var(--line);
6378
+ border-radius: 6px;
6379
+ padding: 8px 10px;
6380
+ font: 12px / 1.5 var(--mono);
6381
+ }
6382
+ @media (max-width: 540px) {
6383
+ .imp-dl,
6384
+ .sub-dl {
6385
+ grid-template-columns: 1fr;
6386
+ gap: 2px;
6387
+ }
6388
+ .imp-dl dd,
6389
+ .sub-dl dd {
6390
+ margin-bottom: 8px;
6391
+ }
6392
+ }