@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.
- package/CHANGELOG.md +66 -1
- package/README.md +4 -2
- package/cli/context.mjs +5 -1
- package/dist/public/dashboard/app.js +366 -255
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +512 -20
- package/dist/worker/index.js +3046 -632
- package/docs/README.md +3 -3
- package/docs/get-started/04-ses.md +54 -14
- package/docs/get-started/05-verify.md +5 -3
- package/docs/get-started/06-go-live.md +10 -5
- package/docs/guides/01-connect-claude.md +4 -0
- package/docs/guides/03-notifications.md +1 -1
- package/docs/publishing/01-your-list.md +86 -0
- package/docs/publishing/02-sending-rules.md +35 -0
- package/docs/publishing/03-name-your-publication.md +106 -0
- package/docs/publishing/04-write-a-post.md +98 -0
- package/docs/publishing/05-scheduling-and-sending.md +72 -0
- package/docs/publishing/06-after-a-send.md +109 -0
- package/docs/publishing/07-working-with-claude.md +48 -0
- package/docs/publishing/08-import-format.md +103 -0
- package/docs/publishing/09-share-your-subscribe-link.md +73 -0
- package/docs/publishing/10-check-your-setup.md +48 -0
- package/docs/publishing/README.md +26 -0
- package/docs/reference/01-configuration.md +7 -1
- package/docs/reference/02-sending-domain-dns.md +4 -2
- package/migrations/0005_imports.sql +74 -0
- package/migrations/0006_deliveries_fewer_indexes.sql +19 -0
- package/package.json +2 -1
- package/template/claude/CLAUDE.md +5 -2
- 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=
|
|
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=
|
|
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:
|
|
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
|
|
1969
|
-
|
|
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,
|
|
2193
|
-
line beneath it in
|
|
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
|
-
|
|
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
|
+
}
|