@kurtbruns/kestrel 1.8.0 → 1.10.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 (32) hide show
  1. package/CHANGELOG.md +66 -1
  2. package/README.md +4 -2
  3. package/cli/context.mjs +5 -1
  4. package/dist/public/dashboard/app.js +366 -255
  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 +512 -20
  8. package/dist/worker/index.js +3046 -632
  9. package/docs/README.md +3 -3
  10. package/docs/get-started/04-ses.md +54 -14
  11. package/docs/get-started/05-verify.md +5 -3
  12. package/docs/get-started/06-go-live.md +10 -5
  13. package/docs/guides/01-connect-claude.md +4 -0
  14. package/docs/guides/03-notifications.md +1 -1
  15. package/docs/publishing/01-your-list.md +86 -0
  16. package/docs/publishing/02-sending-rules.md +35 -0
  17. package/docs/publishing/03-name-your-publication.md +106 -0
  18. package/docs/publishing/04-write-a-post.md +98 -0
  19. package/docs/publishing/05-scheduling-and-sending.md +72 -0
  20. package/docs/publishing/06-after-a-send.md +109 -0
  21. package/docs/publishing/07-working-with-claude.md +48 -0
  22. package/docs/publishing/08-import-format.md +103 -0
  23. package/docs/publishing/09-share-your-subscribe-link.md +73 -0
  24. package/docs/publishing/10-check-your-setup.md +48 -0
  25. package/docs/publishing/README.md +26 -0
  26. package/docs/reference/01-configuration.md +7 -1
  27. package/docs/reference/02-sending-domain-dns.md +4 -2
  28. package/migrations/0005_imports.sql +74 -0
  29. package/migrations/0006_deliveries_fewer_indexes.sql +19 -0
  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
@@ -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=53e67a63">
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=020f590a"></script>
116
+ <script src="./app.js?v=183fc216"></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;
@@ -2680,6 +2873,15 @@ body.web-mode .content {
2680
2873
  .modal .hint.not-connected a {
2681
2874
  color: inherit;
2682
2875
  }
2876
+ /* On Workers Free's limits, a send too large for D1's daily writes (DESIGN §4): the same amber
2877
+ box, since it is a warning to read before scheduling, though it disables nothing. */
2878
+ .modal .hint.daily-writes {
2879
+ color: var(--warn-fg);
2880
+ background: var(--warn-bg);
2881
+ border: 1px solid var(--warn-line);
2882
+ border-radius: 8px;
2883
+ padding: 10px 12px;
2884
+ }
2683
2885
  .modal .actions {
2684
2886
  display: flex;
2685
2887
  justify-content: flex-end;
@@ -2771,6 +2973,11 @@ button.linkbtn:hover {
2771
2973
  .page-head {
2772
2974
  margin-bottom: 16px;
2773
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
+ }
2774
2981
  .page-head-row {
2775
2982
  display: flex;
2776
2983
  align-items: center;
@@ -2989,18 +3196,6 @@ textarea.is-invalid {
2989
3196
  visibility: visible;
2990
3197
  transform: translateY(0);
2991
3198
  }
2992
- /* Drop the tip below the icon instead of above it. The default opens upward,
2993
- which clips against the top of the viewport when the icon sits at the top of a
2994
- page (e.g. the Subscribers counts card); this variant opens downward there. */
2995
- .info.tip-below::after {
2996
- top: calc(100% + 8px);
2997
- bottom: auto;
2998
- transform: translateY(-2px);
2999
- }
3000
- .info.tip-below:hover::after,
3001
- .info.tip-below:focus::after {
3002
- transform: translateY(0);
3003
- }
3004
3199
  /* The "Auto-generate from subject" toggle, grouped just under the slug field. */
3005
3200
  .slug-auto-toggle {
3006
3201
  display: inline-flex;
@@ -3066,6 +3261,25 @@ textarea.is-invalid {
3066
3261
  margin-top: 6px;
3067
3262
  font-size: 13px;
3068
3263
  }
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 {
3270
+ display: flex;
3271
+ gap: 6px;
3272
+ align-items: baseline;
3273
+ margin-top: 6px;
3274
+ font-size: 12.5px;
3275
+ color: var(--status-scheduled-fg);
3276
+ }
3277
+ .sched-note-mark {
3278
+ flex: none;
3279
+ }
3280
+ .sched-note-held {
3281
+ font-weight: 500;
3282
+ }
3069
3283
  .sched-remade-mark {
3070
3284
  margin-top: 4px;
3071
3285
  font-size: 12.5px;
@@ -4030,6 +4244,13 @@ button.api-filter-clear svg {
4030
4244
  font-size: 13px;
4031
4245
  font-variant-numeric: tabular-nums;
4032
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
+ }
4033
4254
  .setup-step-main {
4034
4255
  flex: 1 1 auto;
4035
4256
  min-width: 0;
@@ -4185,7 +4406,8 @@ button.copied svg {
4185
4406
  }
4186
4407
 
4187
4408
  /* Intent chips — the grouping made visible. Editable wears the Kestrel accent;
4188
- 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). */
4189
4411
  .set-chip {
4190
4412
  display: inline-flex;
4191
4413
  align-items: center;
@@ -4198,6 +4420,7 @@ button.copied svg {
4198
4420
  color: var(--muted);
4199
4421
  background: var(--card);
4200
4422
  white-space: nowrap;
4423
+ cursor: default;
4201
4424
  }
4202
4425
  .set-chip svg {
4203
4426
  width: 12px;
@@ -4451,6 +4674,12 @@ button.set-field-remove {
4451
4674
  font-size: 13.5px;
4452
4675
  overflow-x: auto;
4453
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
+ }
4454
4683
  .set-kv > div:nth-last-child(1),
4455
4684
  .set-kv > div:nth-last-child(2) {
4456
4685
  border-bottom: 0;
@@ -4459,6 +4688,7 @@ button.set-field-remove {
4459
4688
  font-family: var(--mono);
4460
4689
  font-size: 13px;
4461
4690
  }
4691
+ /* A status pill in the settings reflection: a plain label, so the default cursor (DESIGN §11). */
4462
4692
  .set-pill {
4463
4693
  display: inline-flex;
4464
4694
  align-items: center;
@@ -4471,6 +4701,7 @@ button.set-field-remove {
4471
4701
  color: var(--muted);
4472
4702
  border: 1px solid var(--line-soft);
4473
4703
  white-space: nowrap;
4704
+ cursor: default;
4474
4705
  }
4475
4706
  .set-pill.ok {
4476
4707
  background: var(--ok-bg);
@@ -5343,6 +5574,7 @@ button.set-embed-copy svg {
5343
5574
  border-radius: 999px;
5344
5575
  border: 1px solid var(--line-soft);
5345
5576
  color: var(--muted);
5577
+ cursor: default;
5346
5578
  }
5347
5579
  .set-req-pill .dot {
5348
5580
  width: 6px;
@@ -5893,8 +6125,268 @@ body.has-savebar .content {
5893
6125
  .posts-table td.updated::before {
5894
6126
  content: " · edited ";
5895
6127
  }
5896
- /* 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
+ }
5897
6183
  .subs-table td.joined::before {
5898
6184
  content: " · joined ";
5899
6185
  }
5900
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
+ }